diff --git a/app/Admin/Controllers/Hr/EmployeeController.php b/app/Admin/Controllers/Hr/EmployeeController.php index d53c977..fd16584 100644 --- a/app/Admin/Controllers/Hr/EmployeeController.php +++ b/app/Admin/Controllers/Hr/EmployeeController.php @@ -8,6 +8,7 @@ use Slowlyo\OwlAdmin\Renderers\Page; use Slowlyo\OwlAdmin\Renderers\Form; use App\Enums\EmployeeStatus; use App\Models\Employee; +use Illuminate\Http\Request; class EmployeeController extends AdminController { @@ -32,15 +33,18 @@ class EmployeeController extends AdminController amisMake()->TableColumn()->name('id')->label(__('employee.id')), amisMake()->TableColumn()->name('name')->label(__('employee.name')), amisMake()->TableColumn()->name('phone')->label(__('employee.phone')), - // amisMake()->TableColumn()->name('employee_status_text')->label(__('employee.employee_status')), - amisMake()->TableColumn()->name('employee_status_text')->label(__('employee.employee_status'))->set('type', 'tag')->set('color', '${employee_status_color}'), - amisMake()->TableColumn()->name('created_at')->label(__('employee.created_at')), $this->rowActions([ $this->rowShowButton(), $this->rowEditButton(true), $this->rowDeleteButton(), + amisMake()->AjaxAction() + ->label(__('employee.leave')) + ->level('link') + ->icon('fa fa-sign-out') + ->confirmText(__('employee.leave_confirm')) + ->api('post:' . admin_url('hr/employees/${id}/leave')), ]), ]); @@ -58,8 +62,7 @@ class EmployeeController extends AdminController ->labelField('name') ->valueField('key') ->joinValues(), - - amisMake()->SelectControl()->name('employee_status')->label(__('employee.employee_status'))->options(EmployeeStatus::options()), + amisMake()->DateControl()->name('join_at')->label(__('employee.join_at'))->format('YYYY-MM-DD'), amisMake()->TextControl()->name('username')->label(__('admin.username'))->value('${admin_user.username}')->required(!$edit), amisMake()->TextControl()->name('password')->set('type', 'input-password')->label(__('admin.password'))->required(!$edit), amisMake()->TextControl()->name('confirm_password')->set('type', 'input-password')->label(__('admin.confirm_password'))->required(!$edit), @@ -71,10 +74,24 @@ class EmployeeController extends AdminController return $this->baseDetail()->title('')->body(amisMake()->Property()->items([ ['label' => __('employee.name'), 'content' => '${name}'], ['label' => __('employee.phone'), 'content' => '${phone}'], - ['label' => __('employee.jobs'), 'content' => amisMake()->TagControl()->size('full')->name('jobs')->labelField('name')->valueField('key')->joinValues()->static()], + ['label' => __('employee.jobs'), 'content' => amisMake()->Each()->name('jobs')->items(amisMake()->Tag()->label('${name}'))], ['label' => __('admin.username'), 'content' => '${admin_user.username}'], ['label' => __('employee.employee_status'), 'content' => amisMake()->Tag()->label('${employee_status_text}')->color('${employee_status_color}')], + ['label' => __('employee.join_at'), 'content' => '${join_at}'], + ['label' => __('employee.leave_at'), 'content' => '${leave_at}'], ])); } + + // 员工离职 + public function leave($id, Request $request) + { + $user = Employee::findOrFail($id); + $user->update([ + 'leave_at' => $request->input('leave_at', now()), + 'employee_status' => EmployeeStatus::Offline, + ]); + + return $this->response()->success(null, '操作成功'); + } } diff --git a/app/Admin/Controllers/Store/StoreController.php b/app/Admin/Controllers/Store/StoreController.php index 942af86..bbe7422 100644 --- a/app/Admin/Controllers/Store/StoreController.php +++ b/app/Admin/Controllers/Store/StoreController.php @@ -3,7 +3,109 @@ namespace App\Admin\Controllers\Store; use Slowlyo\OwlAdmin\Controllers\AdminController; +use App\Services\Admin\StoreService; +use Slowlyo\OwlAdmin\Renderers\Page; +use Slowlyo\OwlAdmin\Renderers\Form; class StoreController extends AdminController { + protected string $serviceName = StoreService::class; + + public function list(): Page + { + $crud = $this->baseCRUD() + ->tableLayout('fixed') + ->headerToolbar([ + $this->createButton(), + ...$this->baseHeaderToolBar(), + ]) + ->filter($this->baseFilter()->body([ + amis()->GroupControl()->mode('horizontal')->body([ + amisMake()->TextControl()->name('title')->label(__('store.title'))->columnRatio(3)->clearable(), + amisMake()->SelectControl()->name('category_id')->label(__('store.category_id'))->columnRatio(3) + ->source(admin_url('api/keywords/tree-list?parent_key=store_category')) + ->labelField('name') + ->valueField('key') + ->onlyLeaf(true) + ->clearable(), + amisMake()->SelectControl()->name('business_id')->label(__('store.business_id'))->columnRatio(3) + ->source(admin_url('api/keywords/tree-list?parent_key=store_business')) + ->labelField('name') + ->valueField('key') + ->clearable(), + ]), + amisMake()->GroupControl()->mode('horizontal')->body([ + amisMake()->SelectControl()->name('level_id')->label(__('store.level_id'))->columnRatio(3) + ->source(admin_url('api/keywords/tree-list?parent_key=store_level')) + ->labelField('name') + ->valueField('key') + ->clearable(), + amisMake()->InputCityControl()->name('region')->label(__('store.region'))->columnRatio(3) + ->allowDistrict(false) + ->extractValue(false) + ->clearable(), + amisMake()->SelectControl()->name('business_status')->label(__('store.business_status'))->columnRatio(3)->clearable(), + ]) + ])) + ->columns([ + amisMake()->TableColumn()->name('id')->label(__('store.id')), + amisMake()->TableColumn()->name('title')->label(__('store.title')), + amisMake()->TableColumn()->name('category.name')->label(__('store.category_id')), + amisMake()->TableColumn()->name('business.name')->label(__('store.business_id')), + amisMake()->TableColumn()->name('level.name')->label(__('store.level_id')), + amisMake()->TableColumn()->name('region')->label(__('store.region'))->set('type', 'tpl')->set('tpl', '${region.province}-${region.city}'), + amisMake()->TableColumn()->name('business_status_text')->label(__('store.business_status'))->set('type', 'tag')->set('color', '${business_status_color}'), + amisMake()->TableColumn()->name('created_at')->label(__('store.created_at')), + $this->rowActions([ + $this->rowShowButton(), + $this->rowEditButton(), + $this->rowDeleteButton(), + ]), + ]); + return $this->baseList($crud); + } + + public function form($edit): Form + { + return $this->baseForm()->title('')->body([ + amisMake()->TextControl()->name('title')->label(__('store.title'))->required(), + amisMake()->SelectControl()->name('master_id')->label(__('store.master_id')) + ->source(admin_url('hr/employees?_action=getData')) + ->labelField('name') + ->valueField('id') + ->required(), + amisMake()->TreeSelectControl()->name('category_id')->label(__('store.category_id')) + ->source(admin_url('api/keywords/tree-list?parent_key=store_category')) + ->labelField('name') + ->valueField('key') + ->onlyLeaf(true) + ->required(), + amisMake()->SelectControl()->name('business_id')->label(__('store.business_id')) + ->source(admin_url('api/keywords/tree-list?parent_key=store_business')) + ->labelField('name') + ->valueField('key') + ->required(), + amisMake()->SelectControl()->name('level_id')->label(__('store.level_id')) + ->source(admin_url('api/keywords/tree-list?parent_key=store_level')) + ->labelField('name') + ->valueField('key') + ->required(), + amisMake()->InputCityControl()->name('region')->label(__('store.region'))->allowDistrict(false)->extractValue(false)->required(), + amisMake()->LocationControl()->name('location')->label(__('store.location'))->ak('Qa9sxstHlyBIDfb3SjrR3Uli39yRjB6X')->autoSelectCurrentLoc(), + ]); + } + + public function detail(): Form + { + return $this->baseDetail()->title('')->body(amisMake()->Property()->items([ + ['label' => __('store.title'), 'content' => '${title}'], + ['label' => __('store.master_id'), 'content' => '${master.name}'], + ['label' => __('store.business_status'), 'content' => amisMake()->Tag()->label('${business_status_text}')->color('${business_status_color}')], + ['label' => __('store.category_id'), 'content' => '${category.name}'], + ['label' => __('store.business_id'), 'content' => '${business.name}'], + ['label' => __('store.level_id'), 'content' => '${level.name}'], + ['label' => __('store.region'), 'content' => '${region.province}-${region.city}'], + ['label' => __('store.profit_ratio'), 'content' => '${profit_ratio}%'], + ])); + } } diff --git a/app/Admin/routes.php b/app/Admin/routes.php index 2b0d8e0..588e2d0 100644 --- a/app/Admin/routes.php +++ b/app/Admin/routes.php @@ -26,6 +26,7 @@ Route::group([ |-------------------------------------------------------------------------- */ $router->resource('hr/employees', \App\Admin\Controllers\Hr\EmployeeController::class); + $router->post('hr/employees/{id}/leave', [\App\Admin\Controllers\Hr\EmployeeController::class, 'leave']); /* |-------------------------------------------------------------------------- diff --git a/app/Enums/BusinessStatus.php b/app/Enums/BusinessStatus.php new file mode 100644 index 0000000..d89cb17 --- /dev/null +++ b/app/Enums/BusinessStatus.php @@ -0,0 +1,47 @@ +value => '开业', + self::Close->value => '关闭', + ]; + } + + public static function coplorMap() + { + return [ + self::Open->value => 'success', + self::Close->value => 'warning', + ]; + } + + public static function options() + { + $list = []; + foreach (self::map() as $key => $value) { + array_push($list, ['label' => $value, 'value' => $key]); + } + return $list; + } + + public function text() + { + return data_get(self::map(), $this->value); + } + + public function color() + { + return data_get(self::coplorMap(), $this->value); + } +} diff --git a/app/Enums/EmployeeStatus.php b/app/Enums/EmployeeStatus.php index d32a2f5..b762320 100644 --- a/app/Enums/EmployeeStatus.php +++ b/app/Enums/EmployeeStatus.php @@ -2,6 +2,9 @@ namespace App\Enums; +/** + * 员工状态 + */ enum EmployeeStatus: int { case Online = 1; diff --git a/app/Enums/StoreRole.php b/app/Enums/StoreRole.php new file mode 100644 index 0000000..80f9f49 --- /dev/null +++ b/app/Enums/StoreRole.php @@ -0,0 +1,34 @@ +value => '店长', + self::Employee->value => '店员', + ]; + } + + public static function options() + { + $list = []; + foreach (self::map() as $key => $value) { + array_push($list, ['label' => $value, 'value' => $key]); + } + return $list; + } + + public function text() + { + return data_get(self::map(), $this->value); + } +} diff --git a/app/Models/Employee.php b/app/Models/Employee.php index b281b3f..bbc1c41 100644 --- a/app/Models/Employee.php +++ b/app/Models/Employee.php @@ -3,10 +3,10 @@ namespace App\Models; use Illuminate\Database\Eloquent\Model; -use EloquentFilter\Filterable; use App\Enums\EmployeeStatus; use Slowlyo\OwlAdmin\Models\AdminUser; use Illuminate\Database\Eloquent\Casts\Attribute; +use EloquentFilter\Filterable; use App\Traits\HasDateTimeFormatter; /** @@ -18,12 +18,14 @@ class Employee extends Model const JOB_KEY = 'job'; - protected $fillable = ['name', 'phone', 'prize_images', 'skill_images', 'employee_status', 'admin_user_id']; + protected $fillable = ['name', 'phone', 'prize_images', 'skill_images', 'employee_status', 'admin_user_id', 'leave_at', 'join_at']; protected $casts = [ 'employee_status' => EmployeeStatus::class, 'prize_images' => 'json', 'skill_images' => 'json', + 'leave_at' => 'datetime', + 'join_at' => 'date:Y-m-d', ]; protected $attributes = [ @@ -45,6 +47,13 @@ class Employee extends Model return $this->belongsTo(AdminUser::class, 'admin_user_id'); } + // 关联的门店 + public function stores() + { + // role(1: 店长, 2: 店员) + return $this->belongsToMany(Store::class, 'store_employees', 'employee_id', 'store_id')->withPivot(['role']); + } + protected function employeeStatusText(): Attribute { return new Attribute( diff --git a/app/Models/Filters/StoreFilter.php b/app/Models/Filters/StoreFilter.php new file mode 100644 index 0000000..289cb41 --- /dev/null +++ b/app/Models/Filters/StoreFilter.php @@ -0,0 +1,9 @@ + 'json', + 'business_status' => BusinessStatus::class, + ]; + + protected $appends = ['business_status_text', 'business_status_color']; + + // 店长 + public function master() + { + return $this->belongsTo(Employee::class, 'master_id'); + } + + // 门店分类 + public function category() + { + return $this->belongsTo(Keyword::class, 'category_id', 'key'); + } + + // 经验分类 + public function business() + { + return $this->belongsTo(Keyword::class, 'business_id', 'key'); + } + + // 门店等级 + public function level() + { + return $this->belongsTo(Keyword::class, 'level_id', 'key'); + } + + // 门店员工 + public function employees() + { + // role(1: 店长, 2: 店员) + return $this->belongsToMany(Employee::class, 'store_employees', 'store_id', 'employee_id')->withPivot(['role']); + } + + protected function businessStatusText(): Attribute + { + return new Attribute( + get: fn () => $this->business_status ? $this->business_status->text() : '', + ); + } + + protected function businessStatusColor(): Attribute + { + return new Attribute( + get: fn () => $this->business_status ? $this->business_status->color() : '', + ); + } +} diff --git a/app/Services/Admin/EmployeeService.php b/app/Services/Admin/EmployeeService.php index 218f69c..6558122 100644 --- a/app/Services/Admin/EmployeeService.php +++ b/app/Services/Admin/EmployeeService.php @@ -91,7 +91,7 @@ class EmployeeService extends BaseService } /** - * 处理职位 + * 处理职位关联 * * @param Employee $model * @param array $jobs(字典表 key 组成的数组) diff --git a/app/Services/Admin/KeywordService.php b/app/Services/Admin/KeywordService.php index 7e28bc1..7cd3e4c 100644 --- a/app/Services/Admin/KeywordService.php +++ b/app/Services/Admin/KeywordService.php @@ -17,7 +17,7 @@ class KeywordService extends BaseService public function getTree() { - $list = $this->query()->filter(request()->all(), $this->modelFilterName)->orderByDesc('sort')->get(); + $list = $this->query()->filter(request()->all(), $this->modelFilterName)->get(); $minNum = $list->min('parent_id'); return !$list->isEmpty() ? array2tree($list->toArray(), $minNum) :[]; } diff --git a/app/Services/Admin/StoreService.php b/app/Services/Admin/StoreService.php new file mode 100644 index 0000000..315db56 --- /dev/null +++ b/app/Services/Admin/StoreService.php @@ -0,0 +1,88 @@ +resloveData($data); + + $validate = $this->validate($data); + if ($validate !== true) { + $this->setError($validate); + return false; + } + + $model = $this->modelName::create($data); + + // 设置店长 + $model->employees()->attach([$data['master_id'] => ['role' => StoreRole::Master]]); + + return true; + } + + public function update($primaryKey, $data): bool + { + $model = $this->query()->whereKey($primaryKey)->firstOrFail(); + $data = $this->resloveData($data, $model); + $validate = $this->validate($data, $model); + if ($validate !== true) { + $this->setError($validate); + return false; + } + + // 修改店长 + if (isset($data['master_id']) && $data['master_id'] != $model->master_id) { + $store->employees()->detach($model->master_id); + $store->employees()->attach([$data['master_id'] => ['role' => StoreRole::Master]]); + } + + return $model->update($data); + } + + public function resloveData($data, $model = null) + { + if (isset($data['location'])) { + $data['lon'] = data_get($data['location'], 'lng'); + $data['lat'] = data_get($data['location'], 'lat'); + } + return $data; + } + + public function validate($data, $model = null) + { + $createRules = [ + 'title' => ['required'], + 'master_id' => ['required', Rule::unique('stores', 'master_id')], + 'category_id' => ['required'], + 'business_id' => ['required'], + 'level_id' => ['required'], + 'region' => ['required'], + 'lon' => ['required'], + 'lat' => ['required'], + ]; + $updateRules = [ + 'master_id' => [Rule::unique($model, 'master_id')] + ]; + $validator = Validator::make($data, $model ? $updateRules : $createRules, [ + 'master_id.unique' => '已经是店长了', + ]); + if ($validator->fails()) { + return $validator->errors()->first(); + } + return true; + } +} \ No newline at end of file diff --git a/database/migrations/2024_03_22_091409_create_employees_table.php b/database/migrations/2024_03_22_091409_create_employees_table.php index 75c02af..dc81860 100644 --- a/database/migrations/2024_03_22_091409_create_employees_table.php +++ b/database/migrations/2024_03_22_091409_create_employees_table.php @@ -19,6 +19,8 @@ return new class extends Migration $table->json('skill_images')->nullable()->comment('专业证书'); $table->unsignedInteger('employee_status')->default(1)->comment('员工状态{1: 在职, 2: 离职}'); $table->foreignId('admin_user_id')->comment('登录信息, 关联 admin_users.id'); + $table->timestamp('join_at')->nullable()->comment('入职时间'); + $table->timestamp('leave_at')->nullable()->comment('离职时间'); $table->timestamps(); diff --git a/database/migrations/2024_03_23_095309_create_stores_table.php b/database/migrations/2024_03_23_095309_create_stores_table.php new file mode 100644 index 0000000..f53706d --- /dev/null +++ b/database/migrations/2024_03_23_095309_create_stores_table.php @@ -0,0 +1,47 @@ +id(); + $table->string('title')->comment('名称'); + $table->foreignId('master_id')->comment('店长, employees.id'); + $table->string('category_id')->comment('门店分类, keywords.store_category'); + $table->string('business_id')->comment('经营分类, keywords.store_business'); + $table->string('level_id')->comment('门店等级, keywords.store_level'); + $table->json('region')->comment('地区{province,city}'); + $table->string('lon')->comment('精度'); + $table->string('lat')->comment('纬度'); + $table->unsignedInteger('profit_ratio')->default(0)->comment('佣金比例(0-100)'); + $table->decimal('profit_money')->default(0)->comment('累计佣金'); + $table->string('business_status')->default(1)->comment('营业状态{1: 开业, 2: 关闭}'); + $table->timestamps(); + $table->comment('门店'); + }); + + Schema::create('store_employees', function (Blueprint $table) { + $table->foreignId('store_id'); + $table->foreignId('employee_id'); + $table->unsignedInteger('role')->default(1)->comment('身份(1: 店长, 2: 店员)'); + $table->comment('门店-店员'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('stores'); + Schema::dropIfExists('store_employees'); + } +}; diff --git a/database/seeders/KeywordSeeder.php b/database/seeders/KeywordSeeder.php index f381135..5abb4be 100644 --- a/database/seeders/KeywordSeeder.php +++ b/database/seeders/KeywordSeeder.php @@ -35,7 +35,38 @@ class KeywordSeeder extends Seeder 'key' => 'job', 'name' => '职位', 'children' => ['普通员工', '小组长', '主管'] - ] + ], + [ + 'key' => 'store_category', + 'name' => '门店分类', + 'children' => [ + ['name' => '家用电器', 'children' => [ + ['name' => '电视'], + ['name' => '冰箱'], + ['name' => '洗衣机'], + ]], + ['name' => '手机数码', 'children' => [ + ['name' => '手机'], + ['name' => '手机配件'], + ['name' => '摄影摄像'], + ]], + ['name' => '电脑办公', 'children' => [ + ['name' => '电脑整机'], + ['name' => '外设产品'], + ['name' => '网络产品'], + ]] + ], + ], + [ + 'key' => 'store_business', + 'name' => '门店经营分类', + 'children' => ['直营', '代理'], + ], + [ + 'key' => 'store_level', + 'name' => '门店等级', + 'children' => ['AAA', 'AA', 'A', 'B', 'C', '无'], + ], ]; $this->insertKeywors($keywords); diff --git a/lang/zh_CN/employee.php b/lang/zh_CN/employee.php index b2f8ad7..42afca6 100644 --- a/lang/zh_CN/employee.php +++ b/lang/zh_CN/employee.php @@ -12,4 +12,8 @@ return [ 'employee_status' => '状态', 'admin_user_id' => '关联账户', 'jobs' => '职位', + 'leave_at' => '离职时间', + 'leave' => '离职', + 'join_at' => '入职时间', + 'leave_confirm' => '是否确定?', ]; diff --git a/lang/zh_CN/store.php b/lang/zh_CN/store.php new file mode 100644 index 0000000..5553023 --- /dev/null +++ b/lang/zh_CN/store.php @@ -0,0 +1,19 @@ + 'ID', + 'created_at' => '创建时间', + 'updated_at' => '更新时间', + 'title' => '店名', + 'master_id' => '店长', + 'category_id' => '门店分类', + 'business_id' => '经营分类', + 'level_id' => '门店等级', + 'region' => '地区', + 'lon' => '经度', + 'lat' => '维度', + 'location' => '坐标', + 'profit_ratio' => '佣金比例', + 'profit_money' => '店长提成', + 'business_status' => '状态', +];