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