任务管理

main
Jing Li 2024-04-18 17:16:16 +08:00
parent b5adac501f
commit 2e86d6fd52
9 changed files with 345 additions and 11 deletions

View File

@ -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 [

View File

@ -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')

View File

@ -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());
}
}

View File

@ -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');
});
/*

View File

@ -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();

View File

@ -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();

View File

@ -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;
}
}

View File

@ -192,7 +192,11 @@ class AdminPermissionSeeder extends Seeder
'icon' => 'tdesign:task',
'uri' => '/plan/plans',
'resource' => true,
'children' => [],
'children' => [
'task_create' => '创建任务',
'task_update' => '编辑任务',
'task_delete' => '删除任务',
],
],
],
],

View File

@ -49,7 +49,7 @@ return [
'expected_performance' => '目标业绩',
],
'plan_hygiene' => [
'task_hygiene' => [
'month' => '月份',
'store' => '门店',
'store_master' => '店长',