diff --git a/app/Admin/Controllers/Finance/ReimbursementController.php b/app/Admin/Controllers/Finance/ReimbursementController.php
new file mode 100644
index 0000000..1d6c7f8
--- /dev/null
+++ b/app/Admin/Controllers/Finance/ReimbursementController.php
@@ -0,0 +1,150 @@
+all(),
+ rules: [
+ 'approval_result' => ['bail', 'required', Rule::in([1, 2])],
+ 'failed_reason' => ['bail', 'required_if:approval_result,2'],
+ ],
+ attributes: [
+ 'approval_result' => __('finance.reimbursement.approval_result'),
+ 'failed_reason' => __('finance.reimbursement.failed_reason'),
+ ],
+ );
+
+ if ($validator->fails()) {
+ return $this->response()->fail($validator->errors()->first());
+ }
+
+ /** @var \App\Models\Reimbursement */
+ $reimbursement = Reimbursement::findOrFail($id);
+
+ if (! $reimbursement->isPending()) {
+ return $this->response()->fail('收支报销不是待审核状态');
+ }
+
+ $reimbursement->update([
+ 'reimbursement_status' => $request->input('approval_result') == 1
+ ? ReimbursementStatus::Passed
+ : ReimbursementStatus::Rejected,
+ ]);
+
+ return $this->response()->success(null, '审核成功');
+ }
+
+ public function list(): Page
+ {
+ $crud = $this->baseCRUD()
+ ->headerToolbar([
+ ...$this->baseHeaderToolBar(),
+ ])
+ ->bulkActions([])
+ ->filter($this->baseFilter()->body([
+ amis()->GroupControl()->mode('horizontal')->body([
+ amis()->TextControl('employee_name', __('finance.reimbursement.employee'))
+ ->placeholder(__('finance.reimbursement.employee')),
+ amis()->SelectControl('reimbursement_type_id', __('finance.reimbursement.type'))
+ ->multiple()
+ ->source(admin_url('api/keywords/tree-list?parent_key=reimbursement_type'))
+ ->labelField('name')
+ ->valueField('key'),
+ amis()->SelectControl('status', __('finance.reimbursement.status'))
+ ->multiple()
+ ->options(ReimbursementStatus::options()),
+ ]),
+ amis()->GroupControl()->mode('horizontal')->body([
+ amis()->InputDatetimeRange()
+ ->name('datetime_range')
+ ->label(__('finance.reimbursement.created_at'))
+ ->format('YYYY-MM-DD HH:mm:ss')
+ ->columnRatio(4),
+ ]),
+ ]))
+ ->columns([
+ // amis()->TableColumn()->name('id')->label(__('finance.reimbursement.id')),
+ amis()->TableColumn()->name('employee.name')->label(__('finance.reimbursement.employee')),
+ amis()->TableColumn()->name('expense')->label(__('finance.reimbursement.expense')),
+ amis()->TableColumn()->name('reason')->label(__('finance.reimbursement.reason')),
+ amis()->TableColumn()->name('type.name')->label(__('finance.reimbursement.type')),
+ amis()->TableColumn()
+ ->name('reimbursement_status')
+ ->label(__('finance.reimbursement.status'))
+ ->type('mapping')
+ ->map(ReimbursementStatus::labelMap()),
+ amis()->TableColumn()->name('created_at')->label(__('finance.reimbursement.created_at')),
+ $this->rowActions([
+ $this->rowApprovalButton()
+ ->visibleOn('${reimbursement_status == '.ReimbursementStatus::Pending->value.'}'),
+ $this->rowShowButton()->visible(Admin::user()->can('admin.finance.reimbursements.view')),
+ ]),
+ ]);
+
+ return $this->baseList($crud);
+ }
+
+ public function detail(): Form
+ {
+ return $this->baseDetail()->title()->body([
+ amis()->Property()->items([
+ ['label' => __('finance.reimbursement.employee'), 'content' => '${employee.name}'],
+ ['label' => __('finance.reimbursement.expense'), 'content' => '${expense}'],
+ ['label' => __('finance.reimbursement.reason'), 'content' => '${reason}'],
+ ['label' => __('finance.reimbursement.type'), 'content' => '${type.name}'],
+ ['label' => __('finance.reimbursement.status'), 'content' => amis()->Mapping()->map(ReimbursementStatus::labelMap())->value('${reimbursement_status}')],
+ ['label' => __('finance.reimbursement.created_at'), 'content' => '${created_at}'],
+ ['label' => __('finance.reimbursement.photos'), 'content' => amis()->Images()->enlargeAble()->source('${photos}')->enlargeWithGallary(), 'span' => 3],
+ ]),
+ ]);
+ }
+
+ /**
+ * 审核操作按钮
+ */
+ protected function rowApprovalButton(): DialogAction
+ {
+ return amis()->DialogAction()->icon('fa-regular fa-check-circle')->label(__('finance.reimbursement.approval'))->level('link')->dialog(
+ amis()->Dialog()->title(__('finance.reimbursement.approval'))->body([
+ amis()->Form()->title('')
+ ->api('post:'.admin_url('finance/reimbursements/${id}/approval'))
+ ->body([
+ amis()->RadiosControl('approval_result', __('finance.reimbursement.approval_result'))
+ ->options([
+ ['label' => '通过', 'value' => 1],
+ ['label' => '驳回', 'value' => 2],
+ ])
+ ->selectFirst()
+ ->required(),
+ amis()->TextareaControl('failed_reason', __('finance.reimbursement.failed_reason'))
+ ->visibleOn('${approval_result == 2}')
+ ->required(),
+ ]),
+ ])->size('md')
+ );
+ }
+}
diff --git a/app/Admin/Filters/ReimbursementFilter.php b/app/Admin/Filters/ReimbursementFilter.php
new file mode 100644
index 0000000..ccd1a1d
--- /dev/null
+++ b/app/Admin/Filters/ReimbursementFilter.php
@@ -0,0 +1,28 @@
+related('employee', 'name', 'like', "%{$name}%");
+ }
+
+ public function reimbursementType($id)
+ {
+ $this->where('reimbursement_type_id', explode(',', $id));
+ }
+
+ public function datetimeRange($datetimeRange)
+ {
+ $this->whereBetween('created_at', explode(',', $datetimeRange));
+ }
+
+ public function status($status)
+ {
+ $this->whereIn('reimbursement_status', explode(',', $status));
+ }
+}
diff --git a/app/Admin/Services/Finance/ReimbursementService.php b/app/Admin/Services/Finance/ReimbursementService.php
new file mode 100644
index 0000000..fd3c72e
--- /dev/null
+++ b/app/Admin/Services/Finance/ReimbursementService.php
@@ -0,0 +1,16 @@
+post('ledgers/{ledger}/ledger-amount', [LedgerController::class, 'updateLedgerAmount'])->name('ledgers.update_ledger_amount');
// 佣金收入
$router->get('commission-incomes', [CommissionIncomeController::class, 'index'])->name('commission_incomes.index');
+ // 收支报销
+ $router->resource('reimbursements', ReimbursementController::class);
+ $router->post('reimbursements/{reimbursement}/approval', [ReimbursementController::class, 'approval'])->name('reimbursements.approval');
// 销售统计
$router->get('sales-statistics', [SalesStatisticController::class, 'index'])->name('sales_statistics.index');
// 门店统计
diff --git a/app/Enums/ReimbursementStatus.php b/app/Enums/ReimbursementStatus.php
new file mode 100644
index 0000000..a5c9ad1
--- /dev/null
+++ b/app/Enums/ReimbursementStatus.php
@@ -0,0 +1,38 @@
+ '待审核',
+ self::Passed => '已通过',
+ self::Rejected => '未通过',
+ };
+ }
+
+ public static function options(): array
+ {
+ return collect(self::cases())
+ ->map(fn (ReimbursementStatus $case) => [
+ 'label' => $case->label(),
+ 'value' => $case->value,
+ ])
+ ->all();
+ }
+
+ public static function labelMap(): array
+ {
+ return [
+ self::Pending->value => ''.self::Pending->label().'',
+ self::Passed->value => ''.self::Passed->label().'',
+ self::Rejected->value => ''.self::Rejected->label().'',
+ ];
+ }
+}
diff --git a/app/Models/Reimbursement.php b/app/Models/Reimbursement.php
new file mode 100644
index 0000000..03677e5
--- /dev/null
+++ b/app/Models/Reimbursement.php
@@ -0,0 +1,65 @@
+ ReimbursementStatus::Pending,
+ ];
+
+ protected $casts = [
+ 'reimbursement_status' => ReimbursementStatus::class,
+ ];
+
+ protected $fillable = [
+ 'employee_id',
+ 'reimbursement_type_id',
+ 'expense',
+ 'reason',
+ 'photos',
+ 'reimbursement_status',
+ ];
+
+ public function employee(): BelongsTo
+ {
+ return $this->belongsTo(Employee::class);
+ }
+
+ public function type(): BelongsTo
+ {
+ return $this->belongsTo(Keyword::class, 'reimbursement_type_id', 'key');
+ }
+
+ /**
+ * 是否是待审核
+ */
+ public function isPending(): bool
+ {
+ return $this->reimbursement_status === ReimbursementStatus::Pending;
+ }
+
+ protected function photos(): Attribute
+ {
+ return Attribute::make(
+ get: function (mixed $value) {
+ if (! is_array($photos = json_decode($value ?? '', true))) {
+ $photos = [];
+ }
+
+ return $photos;
+ },
+ set: fn (mixed $value) => json_encode(is_array($value) ? $value : []),
+ );
+ }
+}
diff --git a/database/migrations/2024_04_01_210323_create_reimbursements_table.php b/database/migrations/2024_04_01_210323_create_reimbursements_table.php
new file mode 100644
index 0000000..ddb26e5
--- /dev/null
+++ b/database/migrations/2024_04_01_210323_create_reimbursements_table.php
@@ -0,0 +1,35 @@
+id();
+ $table->foreignId('employee_id')->comment('报销人');
+ $table->string('reimbursement_type_id')->comment('报销类型, keywords.reimbursement_type');
+ $table->decimal('expense', 10, 2)->comment('报销费用');
+ $table->string('reason')->nullable()->comment('报销原因');
+ $table->text('photos')->nullable()->comment('照片');
+ $table->tinyInteger('reimbursement_status')->default(1)->comment('状态: 1 待审核, 2 已通过, 3 已拒绝');
+ $table->timestamps();
+
+ $table->index('reimbursement_type_id');
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::dropIfExists('reimbursements');
+ }
+};
diff --git a/database/seeders/AdminPermissionSeeder.php b/database/seeders/AdminPermissionSeeder.php
index 3f6466a..9115fac 100644
--- a/database/seeders/AdminPermissionSeeder.php
+++ b/database/seeders/AdminPermissionSeeder.php
@@ -171,6 +171,13 @@ class AdminPermissionSeeder extends Seeder
'index' => '佣金收入',
],
],
+ 'reimbursements' => [
+ 'name' => '收支报销',
+ 'icon' => 'ri:money-cny-circle-fill',
+ 'uri' => '/finance/reimbursements',
+ 'resource' => ['list', 'view'],
+ 'children' => [],
+ ],
'sales_statistics' => [
'name' => '销售统计',
'icon' => 'ri:bar-chart-2-line',
diff --git a/database/seeders/KeywordSeeder.php b/database/seeders/KeywordSeeder.php
index 33004bb..48a8f6c 100644
--- a/database/seeders/KeywordSeeder.php
+++ b/database/seeders/KeywordSeeder.php
@@ -88,7 +88,12 @@ class KeywordSeeder extends Seeder
'key' => 'holiday_type',
'name' => '请假类型',
'children' => ['病假', '事假'],
- ]
+ ],
+ [
+ 'key' => 'reimbursement_type',
+ 'name' => '报销类型',
+ 'children' => ['门店日常支出', '门店活动支出', '日常费用', '器材费', '差旅费', '车辆类报销', '会议费'],
+ ],
];
$this->insertKeywors($keywords);
diff --git a/lang/zh_CN/finance.php b/lang/zh_CN/finance.php
index 29fa240..8a33afb 100644
--- a/lang/zh_CN/finance.php
+++ b/lang/zh_CN/finance.php
@@ -40,4 +40,19 @@ return [
'created_at' => '创建时间',
'updated_at' => '更新时间',
],
+
+ 'reimbursement' => [
+ 'id' => 'ID',
+ 'employee' => '报销人',
+ 'type' => '类型',
+ 'expense' => '报销金额',
+ 'reason' => '报销原因',
+ 'photos' => '报销图片',
+ 'status' => '状态',
+ 'approval' => '审核',
+ 'approval_status' => '状态',
+ 'approval_result' => '审核结果',
+ 'failed_reason' => '驳回原因',
+ 'created_at' => '报销时间',
+ ],
];