From 2e86d6fd5245731159ae5b774bd1026d91133f9c Mon Sep 17 00:00:00 2001 From: Jing Li Date: Thu, 18 Apr 2024 17:16:16 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BB=BB=E5=8A=A1=E7=AE=A1=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/Admin/Controllers/Plan/PlanController.php | 141 +++++++++++++- app/Admin/Services/Plan/PlanService.php | 2 + app/Admin/Services/Plan/TaskService.php | 180 ++++++++++++++++++ app/Admin/routes.php | 3 + app/Models/Plan.php | 7 + app/Models/Task.php | 7 + app/Models/TaskPerformance.php | 8 + database/seeders/AdminPermissionSeeder.php | 6 +- lang/zh_CN/plan.php | 2 +- 9 files changed, 345 insertions(+), 11 deletions(-) diff --git a/app/Admin/Controllers/Plan/PlanController.php b/app/Admin/Controllers/Plan/PlanController.php index 6c5f149..f923508 100644 --- a/app/Admin/Controllers/Plan/PlanController.php +++ b/app/Admin/Controllers/Plan/PlanController.php @@ -13,6 +13,8 @@ use Illuminate\Support\Arr; use Illuminate\Support\Facades\DB; use Slowlyo\OwlAdmin\Admin; use Slowlyo\OwlAdmin\Renderers\AjaxAction; +use Slowlyo\OwlAdmin\Renderers\Drawer; +use Slowlyo\OwlAdmin\Renderers\DrawerAction; use Slowlyo\OwlAdmin\Renderers\Form; use Slowlyo\OwlAdmin\Renderers\Page; use Throwable; @@ -196,8 +198,8 @@ class PlanController extends AdminController amis()->CRUDTable() ->api(admin_url('api/tasks?plan_id=${id}')) ->columns([ - amis()->TableColumn('id', __('plan.task.id')), - amis()->TableColumn('name', __('plan.task.name')), + // amis()->TableColumn('id', __('plan.task.id')), + // amis()->TableColumn('name', __('plan.task.name')), amis()->TableColumn('taskable.date', __('plan.task_ledger.date')), amis()->TableColumn('taskable.store.title', __('plan.task_ledger.store')), amis()->TableColumn('taskable.store_master.name', __('plan.task_ledger.store_master')), @@ -209,13 +211,16 @@ class PlanController extends AdminController // 业绩指标 amis()->CRUDTable() + ->name('task-performance-table') ->api(admin_url('api/tasks?plan_id=${id}')) ->headerToolbar([ + $this->taskCreateButton() + ->visible(Admin::user()->can('admin.plan.plans.task_create')), amis('reload')->align('right'), ]) ->columns([ - amis()->TableColumn('id', __('plan.task.id')), - amis()->TableColumn('name', __('plan.task.name')), + // amis()->TableColumn('id', __('plan.task.id')), + // amis()->TableColumn('name', __('plan.task.name')), amis()->TableColumn('taskable.month', __('plan.task_performance.month')), amis()->TableColumn('taskable.store.title', __('plan.task_performance.store')), amis()->TableColumn('taskable.store_master.name', __('plan.task_performance.store_master')), @@ -224,24 +229,37 @@ class PlanController extends AdminController amis()->TableColumn('task_status', __('plan.task.status'))->type('mapping')->map(TaskStatus::labelMap()), amis()->TableColumn('completed_at', __('plan.task.completed_at')), amis()->TableColumn('created_at', __('plan.task.created_at')), + $this->rowActions([ + $this->taskRowEditButton() + ->visible(Admin::user()->can('admin.plan.plans.task_update')), + $this->taskRowDeleteButton() + ->visible(Admin::user()->can('admin.plan.plans.task_delete')), + ]), ]) ->visibleOn('${planable_type == "'.$planableTypePerformance.'"}'), // 清洁卫生 amis()->CRUDTable() + ->name('task-hygiene-table') ->api(admin_url('api/tasks?plan_id=${id}')) ->headerToolbar([ + $this->taskCreateButton() + ->visible(Admin::user()->can('admin.plan.plans.task_create')), amis('reload')->align('right'), ]) ->columns([ - amis()->TableColumn('id', __('plan.task.id')), - amis()->TableColumn('name', __('plan.task.name')), - amis()->TableColumn('taskable.month', __('plan.plan_hygiene.month')), - amis()->TableColumn('taskable.store.title', __('plan.plan_hygiene.store')), - amis()->TableColumn('taskable.store_master.name', __('plan.plan_hygiene.store_master')), + // amis()->TableColumn('id', __('plan.task.id')), + // amis()->TableColumn('name', __('plan.task.name')), + amis()->TableColumn('taskable.month', __('plan.task_hygiene.month')), + amis()->TableColumn('taskable.store.title', __('plan.task_hygiene.store')), + amis()->TableColumn('taskable.store_master.name', __('plan.task_hygiene.store_master')), amis()->TableColumn('task_status', __('plan.task.status'))->type('mapping')->map(TaskStatus::labelMap()), amis()->TableColumn('completed_at', __('plan.task.completed_at')), amis()->TableColumn('created_at', __('plan.task.created_at')), + $this->rowActions([ + $this->taskRowDeleteButton() + ->visible(Admin::user()->can('admin.plan.plans.task_delete')), + ]), ]) ->visibleOn('${planable_type == "'.$planableTypeHygiene.'"}'), ]); @@ -274,6 +292,111 @@ class PlanController extends AdminController ->api('post:' . admin_url('/plan/plans/${id}/publish')); } + /** + * 任务 - 创建按钮 + */ + protected function taskCreateButton(): DrawerAction + { + $planableTypePerformance = (new PlanPerformance())->getMorphClass(); + $planableTypeHygiene = (new PlanHygiene())->getMorphClass(); + + $form = amis()->Form() + ->title('') + ->api('post:'.admin_url('/plan/tasks')) + ->redirect('') + ->body([ + amis()->HiddenControl('plan_id')->value('${id}'), + // 业绩指标 + amis()->SelectControl('task_performance[store_id]', __('plan.task_performance.store')) + ->source(admin_url('api/stores')) + ->labelField('title') + ->valueField('id') + ->clearable() + ->required() + ->visibleOn('${planable_type == "'.$planableTypePerformance.'"}'), + amis()->NumberControl() + ->name('task_performance[expected_performance]') + ->label(__('plan.task_performance.expected_performance')) + ->placeholder(__('plan.task_performance.expected_performance')) + ->precision(2) + ->showSteps(false) + ->required() + ->visibleOn('${planable_type == "'.$planableTypePerformance.'"}'), + // 清洁卫生 + amis()->SelectControl('task_hygiene[store_id]', __('plan.task_hygiene.store')) + ->source(admin_url('api/stores')) + ->labelField('title') + ->valueField('id') + ->clearable() + ->required() + ->visibleOn('${planable_type == "'.$planableTypeHygiene.'"}'), + ]) + ->reload('task-performance-table,task-hygiene-table'); + + return DrawerAction::make() + ->label(__('admin.create')) + ->icon('fa fa-add') + ->level('primary') + ->drawer( + Drawer::make() + ->title(__('admin.create')) + ->size('lg') + ->closeOnOutside() + ->body($form) + ); + } + + /** + * 任务 - 行编辑按钮 + */ + protected function taskRowEditButton(): DrawerAction + { + $planableTypePerformance = (new PlanPerformance())->getMorphClass(); + + $form = amis()->Form() + ->title('') + ->api('put:'.admin_url('/plan/tasks/${id}')) + ->redirect('') + ->body([ + amis()->HiddenControl('id'), + // 任务指标 + amis()->NumberControl() + ->name('task_performance[expected_performance]') + ->label(__('plan.task_performance.expected_performance')) + ->value('${taskable.expected_performance}') + ->precision(2) + ->showSteps(false) + ->required() + ->visibleOn('${planable_type == "'.$planableTypePerformance.'"}'), + ]) + ->reload('task-performance-table'); + + return DrawerAction::make() + ->label(__('admin.edit')) + ->icon('fa-regular fa-pen-to-square') + ->level('link') + ->drawer( + Drawer::make() + ->title(__('admin.edit')) + ->size('lg') + ->closeOnOutside() + ->body($form) + ); + } + + /** + * 任务 - 行删除按钮 + */ + protected function taskRowDeleteButton(): AjaxAction + { + return amis()->AjaxAction() + ->label(__('admin.delete')) + ->icon('fa-regular fa-trash-can') + ->level('link') + ->confirmText(__('admin.confirm_delete')) + ->api('delete:'.admin_url('/plan/tasks/${id}')); + } + protected function planableTypeOptions(): array { return [ diff --git a/app/Admin/Services/Plan/PlanService.php b/app/Admin/Services/Plan/PlanService.php index 28f92bb..a7dcfcb 100644 --- a/app/Admin/Services/Plan/PlanService.php +++ b/app/Admin/Services/Plan/PlanService.php @@ -245,6 +245,8 @@ class PlanService extends BaseService if ($plans->contains(fn (Plan $plan) => $plan->isPublished())) { admin_abort('不能删除已发布的任务计划'); } + + $plans->each(fn (Plan $plan) => $plan->planable()->delete()); } public function addRelations($query, string $scene = 'list') diff --git a/app/Admin/Services/Plan/TaskService.php b/app/Admin/Services/Plan/TaskService.php index 9a839d6..43d81b2 100644 --- a/app/Admin/Services/Plan/TaskService.php +++ b/app/Admin/Services/Plan/TaskService.php @@ -4,8 +4,18 @@ namespace App\Admin\Services\Plan; use App\Admin\Filters\TaskFilter; use App\Admin\Services\BaseService; +use App\Enums\TaskStatus; +use App\Models\Ledger; +use App\Models\Plan; +use App\Models\PlanHygiene; +use App\Models\PlanPerformance; +use App\Models\Store; use App\Models\Task; +use App\Models\TaskHygiene; +use App\Models\TaskPerformance; use Illuminate\Database\Eloquent\Builder; +use Illuminate\Support\Carbon; +use Illuminate\Support\Facades\Validator; /** * @method Task getModel() @@ -16,4 +26,174 @@ class TaskService extends BaseService protected string $modelName = Task::class; protected string $modelFilterName = TaskFilter::class; + + public function store($data): bool + { + if (! isset($data['plan_id'])) { + admin_abort('任务计划未找到'); + } + + $plan = Plan::findOrFail($data['plan_id']); + + switch (get_class($planable = $plan->planable)) { + case PlanPerformance::class: + $payload = $data['task_performance'] ?? []; + + Validator::validate( + data: $data, + rules: [ + 'store_id' => ['bail', 'required'], + 'expected_performance' => ['bail', 'required', 'numeric', 'min:0'], + ], + attributes: [ + 'store_id' => __('plan.task_performance.store_id'), + 'expected_performance' => __('plan.task_performance.expected_performance'), + ], + ); + + /** @var \App\Models\Store */ + $store = Store::findOrFail($payload['store_id']); + + if ( + TaskPerformance::where('store_id', $store->id) + ->where('month', $planable->month) + ->exists() + ) { + admin_abort('门店已有业绩指标任务'); + } + + // 月份 + $month = Carbon::createFromFormat('Y-m', $planable->month); + // 开始时间 + $startAt = $month->copy()->startOfMonth(); + // 结束时间 + $endAt = $month->copy()->endOfMonth(); + + // 门店实际业绩 + $actualPerformance = Ledger::where('store_id', $store->id) + ->whereBetween('date', [$startAt->format('Y-m-d'), $endAt->format('Y-m-d')]) + ->sum('sales'); + + /** @var \App\Models\TaskPerformance */ + $taskable = TaskPerformance::create([ + 'month' => $planable->month, + 'store_id' => $store->id, + 'store_master_id' => $store->master_id, + 'expected_performance' => $payload['expected_performance'], + 'actual_performance' => $actualPerformance, + ]); + + // 月份 + $month = Carbon::createFromFormat('Y-m', $planable->month); + + $taskable->task()->create([ + 'plan_id' => $plan->id, + 'name' => '业绩指标', + 'start_at' => $month->copy()->startOfMonth(), + 'end_at' => $month->copy()->endOfMonth(), + 'task_status' => $taskable->isCompleted() ? TaskStatus::Success : TaskStatus::Pending, + 'completed_at' => $taskable->isCompleted() ? now() : null, + ]); + break; + + case PlanHygiene::class: + $payload = $data['task_hygiene'] ?? []; + + Validator::validate( + data: $data, + rules: [ + 'store_id' => ['bail', 'required'], + ], + attributes: [ + 'store_id' => __('plan.task_hygiene.store_id'), + ], + ); + + /** @var \App\Models\Store */ + $store = Store::findOrFail($payload['store_id']); + + if ( + TaskHygiene::where('store_id', $store->id) + ->where('month', $planable->month) + ->exists() + ) { + admin_abort('门店已有清洁卫生任务'); + } + + $taskable = TaskHygiene::create([ + 'month' => $planable->month, + 'store_id' => $store->id, + 'store_master_id' => $store->master_id, + ]); + + // 月份 + $month = Carbon::createFromFormat('Y-m', $planable->month); + + $taskable->task()->create([ + 'plan_id' => $plan->id, + 'name' => '清洁卫生', + 'start_at' => $month->copy()->startOfMonth(), + 'end_at' => $month->copy()->endOfMonth(), + 'task_status' => TaskStatus::Pending, + ]); + break; + + default: + admin_abort('任务计划不可新增任务'); + break; + } + + return true; + } + + public function update($primaryKey, $data): bool + { + $task = Task::findOrFail($primaryKey); + + if (in_array($task->task_status, [TaskStatus::Success, TaskStatus::Failed])) { + admin_abort("[{$task->task_status->text()}]任务不可修改"); + } + + switch (get_class($taskable = $task->taskable)) { + case TaskPerformance::class: + $payload = $data['task_performance'] ?? []; + + Validator::validate( + data: $payload, + rules: [ + 'expected_performance' => ['bail', 'required', 'numeric', 'min:0'], + ], + attributes: [ + 'expected_performance' => __('plan.task_performance.expected_performance'), + ], + ); + + $taskable->update([ + 'expected_performance' => $payload['expected_performance'], + ]); + + if ($taskable->isCompleted()) { + $task->update([ + 'task_status' => TaskStatus::Success, + 'completed_at' => now(), + ]); + } + + break; + + default: + admin_abort('任务不可修改'); + break; + } + + return true; + } + + public function preDelete(array $ids): void + { + /** @var \Illuminate\Database\Eloquent\Collection */ + $tasks = Task::findMany($ids); + + $tasks->each(fn (Task $task) => $task->taskable()->delete()); + } } diff --git a/app/Admin/routes.php b/app/Admin/routes.php index 2d4375c..811502a 100644 --- a/app/Admin/routes.php +++ b/app/Admin/routes.php @@ -154,6 +154,9 @@ Route::group([ // 任务计划 $router->resource('plans', PlanController::class); $router->post('/plans/{plan}/publish', [PlanController::class, 'publish'])->name('plans.publish'); + $router->post('/tasks', [TaskController::class, 'store'])->name('plans.task_create'); + $router->put('/tasks/{task}', [TaskController::class, 'update'])->name('plans.task_update'); + $router->delete('/tasks/{task}', [TaskController::class, 'destroy'])->name('plans.task_delete'); }); /* diff --git a/app/Models/Plan.php b/app/Models/Plan.php index 48445c0..3664504 100644 --- a/app/Models/Plan.php +++ b/app/Models/Plan.php @@ -28,6 +28,13 @@ class Plan extends Model 'planable_type', ]; + protected static function booted(): void + { + static::deleting(function (Task $model) { + $model->planable()->delete(); + }); + } + public function planable(): MorphTo { return $this->morphTo(); diff --git a/app/Models/Task.php b/app/Models/Task.php index c95e87b..759a238 100644 --- a/app/Models/Task.php +++ b/app/Models/Task.php @@ -35,6 +35,13 @@ class Task extends Model 'completed_at', ]; + protected static function booted(): void + { + static::deleting(function (Task $model) { + $model->taskable()->delete(); + }); + } + public function taskable(): MorphTo { return $this->morphTo(); diff --git a/app/Models/TaskPerformance.php b/app/Models/TaskPerformance.php index 63fcaac..e3aaef7 100644 --- a/app/Models/TaskPerformance.php +++ b/app/Models/TaskPerformance.php @@ -34,4 +34,12 @@ class TaskPerformance extends Model { return $this->belongsTo(Employee::class, 'store_master_id'); } + + /** + * 此业绩指标是否已完成 + */ + public function isCompleted(): bool + { + return bccomp($this->actual_performance, $this->expected_performance, 2) >= 0; + } } diff --git a/database/seeders/AdminPermissionSeeder.php b/database/seeders/AdminPermissionSeeder.php index 96ce86c..66e97a6 100644 --- a/database/seeders/AdminPermissionSeeder.php +++ b/database/seeders/AdminPermissionSeeder.php @@ -192,7 +192,11 @@ class AdminPermissionSeeder extends Seeder 'icon' => 'tdesign:task', 'uri' => '/plan/plans', 'resource' => true, - 'children' => [], + 'children' => [ + 'task_create' => '创建任务', + 'task_update' => '编辑任务', + 'task_delete' => '删除任务', + ], ], ], ], diff --git a/lang/zh_CN/plan.php b/lang/zh_CN/plan.php index d6de7e8..fc5eee2 100644 --- a/lang/zh_CN/plan.php +++ b/lang/zh_CN/plan.php @@ -49,7 +49,7 @@ return [ 'expected_performance' => '目标业绩', ], - 'plan_hygiene' => [ + 'task_hygiene' => [ 'month' => '月份', 'store' => '门店', 'store_master' => '店长',