diff --git a/app/Admin/Controllers/Complaint/ComplaintController.php b/app/Admin/Controllers/Complaint/ComplaintController.php new file mode 100644 index 0000000..841831f --- /dev/null +++ b/app/Admin/Controllers/Complaint/ComplaintController.php @@ -0,0 +1,164 @@ +baseCRUD() + ->headerToolbar([ + ...$this->baseHeaderToolBar(), + ]) + ->bulkActions([]) + ->filter($this->baseFilter()->body([ + amis()->GroupControl()->mode('horizontal')->body([ + amis()->TextControl() + ->name('employee_name') + ->label(__('complaint.complaint.employee')) + ->placeholder(__('complaint.complaint.employee')), + amis()->InputDatetimeRange() + ->name('created_at') + ->label(__('complaint.complaint.created_at')) + ->format('YYYY-MM-DD HH:mm:ss'), + amis()->SelectControl('complaint_status', __('complaint.complaint.complaint_status')) + ->multiple() + ->options(ComplaintStatus::options()), + ]), + ])) + ->columns([ + amis()->TableColumn()->name('id')->label(__('complaint.complaint.id')), + amis()->TableColumn() + ->name('_employee') + ->label(__('complaint.complaint.employee')) + ->value('${anonymous ? "匿名" : employee.name}'), + amis()->TableColumn() + ->name('content') + ->label(__('complaint.complaint.content')) + ->popOver('${content}') + ->type('tpl') + ->set('tpl', '${content|truncate:50}'), + amis()->TableColumn() + ->name('result') + ->label(__('complaint.complaint.result')) + ->popOver('${result}') + ->type('tpl') + ->set('tpl', '${result|truncate:50}'), + amis()->TableColumn() + ->name('complaint_status') + ->label(__('complaint.complaint.complaint_status')) + ->type('mapping') + ->map(ComplaintStatus::labelMap()), + amis()->TableColumn()->name('created_at')->label(__('complaint.complaint.created_at')), + $this->rowActions([ + $this->rowProcessStartButton() + ->visible(Admin::user()->can('complaint.complaints.start')) + ->visibleOn('${complaint_status === '.ComplaintStatus::Pending->value.'}'), + $this->rowProcessCompleteButton() + ->visible(Admin::user()->can('complaint.complaints.complete')) + ->visibleOn('${complaint_status === '.ComplaintStatus::Processing->value.'}'), + ]), + ]); + + return $this->baseList($crud); + } + + /** + * 处理开始 + */ + public function start($id) + { + /** @var Complaint */ + $complaint = Complaint::findOrFail($id); + + if (! $complaint->isPending()) { + admin_abort('举报投诉记录的状态不是待审核'); + } + + $complaint->update([ + 'complaint_status' => ComplaintStatus::Processing->value, + ]); + + return $this->response()->successMessage('操作成功'); + } + + /** + * 处理结束 + */ + public function complete($id, Request $request) + { + $validator = Validator::make( + data: $request->input(), + rules: [ + 'result' => ['bail', 'required', 'string'], + ], + attributes: [ + 'result' => __('complaint.complaint.result'), + ], + ); + + if ($validator->fails()) { + admin_abort($validator->errors()->first()); + } + + /** @var Complaint */ + $complaint = Complaint::findOrFail($id); + + if (! $complaint->isProcessing()) { + admin_abort('举报投诉记录的状态不是处理中'); + } + + $complaint->update([ + 'result' => $request->input('result'), + 'complaint_status' => ComplaintStatus::Processed->value, + ]); + + return $this->response()->successMessage('操作成功'); + } + + /** + * 处理开始按钮 + */ + protected function rowProcessStartButton(): AjaxAction + { + return amis()->AjaxAction() + ->icon('fa fa-play-circle-o') + ->label(__('complaint.complaint.start')) + ->level('link') + ->api('post:'.admin_url('/complaint/complaints/$id/start')) + ->confirmText('是否开始处理选中的举报投诉记录') + ->confirmTitle('系统消息'); + } + + /** + * 处理结束按钮 + */ + protected function rowProcessCompleteButton(): DrawerAction + { + return amis()->DrawerAction()->icon('fa fa-stop-circle-o')->label(__('complaint.complaint.complete'))->level('link')->drawer( + amis()->Drawer()->title(__('complaint.complaint.result'))->body([ + amis()->Form()->title('') + ->api('post:'.admin_url('/complaint/complaints/$id/complete')) + ->body([ + amis()->TextareaControl('result', __('complaint.complaint.result'))->required()->minRows(15), + ]), + ])->size('lg') + ); + } +} diff --git a/app/Admin/Controllers/Complaint/FeedbackController.php b/app/Admin/Controllers/Complaint/FeedbackController.php new file mode 100644 index 0000000..2347923 --- /dev/null +++ b/app/Admin/Controllers/Complaint/FeedbackController.php @@ -0,0 +1,50 @@ +baseCRUD() + ->headerToolbar([ + ...$this->baseHeaderToolBar(), + ]) + ->bulkActions([]) + ->filter($this->baseFilter()->body([ + amis()->GroupControl()->mode('horizontal')->body([ + amis()->TextControl() + ->name('employee_name') + ->label(__('complaint.feedback.employee')) + ->placeholder(__('complaint.feedback.employee')) + ->columnRatio(4), + amis()->InputDatetimeRange() + ->name('created_at') + ->label(__('complaint.feedback.created_at')) + ->format('YYYY-MM-DD HH:mm:ss') + ->columnRatio(4), + ]), + ])) + ->columns([ + amis()->TableColumn()->name('id')->label(__('complaint.feedback.id')), + amis()->TableColumn()->name('employee.name')->label(__('complaint.feedback.employee')), + amis()->TableColumn()->name('content')->label(__('complaint.feedback.content')), + amis()->TableColumn()->name('created_at')->label(__('complaint.feedback.created_at')), + $this->rowActions([ + $this->rowDeleteButton()->visible(Admin::user()->can('admin.complaint.feedback.delete')), + ]), + ]); + + return $this->baseList($crud); + } +} diff --git a/app/Admin/Filters/ComplaintFilter.php b/app/Admin/Filters/ComplaintFilter.php new file mode 100644 index 0000000..58e784e --- /dev/null +++ b/app/Admin/Filters/ComplaintFilter.php @@ -0,0 +1,24 @@ +where('anonymous', false) + ->whereRelation('employee', 'name', 'like', "%{$name}%"); + } + + public function complaintStatus($status) + { + $this->whereIn('complaint_status', explode(',', $status)); + } + + public function createdAt($createdAt) + { + $this->whereBetween('created_at', explode(',', $createdAt)); + } +} diff --git a/app/Admin/Filters/FeedbackFilter.php b/app/Admin/Filters/FeedbackFilter.php new file mode 100644 index 0000000..2bc841b --- /dev/null +++ b/app/Admin/Filters/FeedbackFilter.php @@ -0,0 +1,18 @@ +related('employee', 'name', 'like', "%{$name}%"); + } + + public function createdAt($createdAt) + { + $this->whereBetween('created_at', explode(',', $createdAt)); + } +} diff --git a/app/Admin/Services/Complaint/ComplaintService.php b/app/Admin/Services/Complaint/ComplaintService.php new file mode 100644 index 0000000..37169a0 --- /dev/null +++ b/app/Admin/Services/Complaint/ComplaintService.php @@ -0,0 +1,21 @@ +resource('business', OfficalBusinessController::class); }); + /* + |-------------------------------------------------------------------------- + | 投诉意见 + |-------------------------------------------------------------------------- + */ + $router->group([ + 'prefix' => 'complaint', + 'as' => 'complaint.', + ], function (Router $router) { + // 举报投诉 + $router->resource('complaints', ComplaintController::class)->only(['index']); + $router->post('complaints/{complaint}/start', [ComplaintController::class, 'start'])->name('complaints.start'); + $router->post('complaints/{complaint}/complete', [ComplaintController::class, 'complete'])->name('complaints.complete'); + // 意见箱 + $router->resource('feedback', FeedbackController::class)->only(['index', 'destroy']); + }); + /* |-------------------------------------------------------------------------- | 财务管理 diff --git a/app/Enums/ComplaintStatus.php b/app/Enums/ComplaintStatus.php new file mode 100644 index 0000000..16fbf9b --- /dev/null +++ b/app/Enums/ComplaintStatus.php @@ -0,0 +1,33 @@ +value]; + } + + public static function options(): array + { + return [ + self::Pending->value => '待处理', + self::Processing->value => '处理中', + self::Processed->value => '已处理', + ]; + } + + public static function labelMap(): array + { + return [ + self::Pending->value => ''.self::Pending->text().'', + self::Processing->value => ''.self::Processing->text().'', + self::Processed->value => ''.self::Processed->text().'', + ]; + } +} diff --git a/app/Models/Complaint.php b/app/Models/Complaint.php new file mode 100644 index 0000000..fccd56d --- /dev/null +++ b/app/Models/Complaint.php @@ -0,0 +1,44 @@ + false, + 'complaint_status' => ComplaintStatus::Pending, + ]; + + protected $casts = [ + 'anonymous' => 'bool', + 'complaint_status' => ComplaintStatus::class, + ]; + + protected $fillable = [ + 'employee_id', 'content', 'result', 'anonymous', 'complaint_status', + ]; + + public function employee(): BelongsTo + { + return $this->belongsTo(Employee::class); + } + + public function isPending(): bool + { + return $this->complaint_status === ComplaintStatus::Pending; + } + + public function isProcessing(): bool + { + return $this->complaint_status === ComplaintStatus::Processing; + } +} diff --git a/app/Models/Feedback.php b/app/Models/Feedback.php new file mode 100644 index 0000000..aae17be --- /dev/null +++ b/app/Models/Feedback.php @@ -0,0 +1,23 @@ +belongsTo(Employee::class); + } +} diff --git a/database/factories/ComplaintFactory.php b/database/factories/ComplaintFactory.php new file mode 100644 index 0000000..7e89dae --- /dev/null +++ b/database/factories/ComplaintFactory.php @@ -0,0 +1,45 @@ + + */ +class ComplaintFactory extends Factory +{ + protected $model = Complaint::class; + + protected static array $employees = []; + + /** + * Define the model's default state. + * + * @return array + */ + public function definition(): array + { + return [ + 'content' => fake()->paragraph(), + 'result' => null, + 'anonymous' => false, + 'complaint_status' => ComplaintStatus::Pending, + ]; + } + + /** + * 已处理的投诉 + */ + public function processed(): static + { + return $this->state(fn (array $attributes) => [ + 'result' => fake()->paragraph(), + 'complaint_status' => ComplaintStatus::Processed, + ]); + } +} diff --git a/database/factories/FeedbackFactory.php b/database/factories/FeedbackFactory.php new file mode 100644 index 0000000..5282dd7 --- /dev/null +++ b/database/factories/FeedbackFactory.php @@ -0,0 +1,30 @@ + + */ +class FeedbackFactory extends Factory +{ + protected $model = Feedback::class; + + protected static array $employees = []; + + /** + * Define the model's default state. + * + * @return array + */ + public function definition(): array + { + return [ + 'content' => fake()->paragraph(), + ]; + } +} diff --git a/database/migrations/2024_04_02_154348_create_complaints_table.php b/database/migrations/2024_04_02_154348_create_complaints_table.php new file mode 100644 index 0000000..77dbb71 --- /dev/null +++ b/database/migrations/2024_04_02_154348_create_complaints_table.php @@ -0,0 +1,32 @@ +id(); + $table->foreignId('employee_id'); + $table->text('content')->nullable()->comment('投诉内容'); + $table->text('result')->nullable()->comment('处理结果'); + $table->boolean('anonymous')->default(false)->comment('是否匿名'); + $table->tinyInteger('complaint_status')->default(1)->comment('1: 未处理, 2 处理中, 3 已处理'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('complaints'); + } +}; diff --git a/database/migrations/2024_04_02_154750_create_feedback_table.php b/database/migrations/2024_04_02_154750_create_feedback_table.php new file mode 100644 index 0000000..63ae761 --- /dev/null +++ b/database/migrations/2024_04_02_154750_create_feedback_table.php @@ -0,0 +1,29 @@ +id(); + $table->foreignId('employee_id'); + $table->text('content')->nullable()->comment('反馈内容'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('feedback'); + } +}; diff --git a/database/seeders/AdminPermissionSeeder.php b/database/seeders/AdminPermissionSeeder.php index 1897887..764c14b 100644 --- a/database/seeders/AdminPermissionSeeder.php +++ b/database/seeders/AdminPermissionSeeder.php @@ -149,6 +149,36 @@ class AdminPermissionSeeder extends Seeder ], ], + /* + |-------------------------------------------------------------------------- + | 投诉意见 + |-------------------------------------------------------------------------- + */ + 'complaint' => [ + 'name' => '投诉意见', + 'icon' => 'mdi:star-four-points-box-outline', + 'uri' => '/complaint', + 'children' => [ + 'complaints' => [ + 'name' => '举报投诉', + 'icon' => 'pixelarticons:list-box', + 'uri' => '/complaint/complaints', + 'resource' => ['list'], + 'children' => [ + 'start' => '开始', + 'complete' => '完成', + ], + ], + 'feedback' => [ + 'name' => '意见箱', + 'icon' => 'tabler:box', + 'uri' => '/complaint/feedback', + 'resource' => ['list', 'delete'], + 'children' => [], + ], + ], + ], + /* |-------------------------------------------------------------------------- | 财务报表 diff --git a/database/seeders/ComplaintSeeder.php b/database/seeders/ComplaintSeeder.php new file mode 100644 index 0000000..8eed016 --- /dev/null +++ b/database/seeders/ComplaintSeeder.php @@ -0,0 +1,38 @@ +merge(Complaint::factory()->count(5)->make()) + ->merge(Complaint::factory()->count(5)->state(['anonymous' => true])->make()) + ->merge(Complaint::factory()->count(5)->state(['complaint_status' => ComplaintStatus::Processing])->make()) + ->merge(Complaint::factory()->count(5)->processed()->make()) + ->map(function (Complaint $instance) use ($timestamp, $employees) { + return array_merge($instance->toArray(), [ + 'employee_id' => $employees->random(), + 'created_at' => $timestamp, + 'updated_at' => $timestamp, + ]); + }) + ->all() + ); + } +} diff --git a/database/seeders/FeedbackSeeder.php b/database/seeders/FeedbackSeeder.php new file mode 100644 index 0000000..1181904 --- /dev/null +++ b/database/seeders/FeedbackSeeder.php @@ -0,0 +1,33 @@ +count(10)->make() + ->map(function (Feedback $instance) use ($timestamp, $employees) { + return array_merge($instance->toArray(), [ + 'employee_id' => $employees->random(), + 'created_at' => $timestamp, + 'updated_at' => $timestamp, + ]); + }) + ->all() + ); + } +} diff --git a/lang/zh_CN/complaint.php b/lang/zh_CN/complaint.php new file mode 100644 index 0000000..04d1f7c --- /dev/null +++ b/lang/zh_CN/complaint.php @@ -0,0 +1,21 @@ + [ + 'id' => 'ID', + 'employee' => '投诉人', + 'content' => '投诉内容', + 'result' => '处理结果', + 'complaint_status' => '状态', + 'created_at' => '投诉时间', + 'start' => '开始', + 'complete' => '完成', + ], + + 'feedback' => [ + 'id' => 'ID', + 'employee' => '反馈人', + 'content' => '反馈内容', + 'created_at' => '反馈时间', + ], +];