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' => '反馈时间',
+ ],
+];