diff --git a/app/Admin/Controllers/Finance/LedgerController.php b/app/Admin/Controllers/Finance/LedgerController.php
new file mode 100644
index 0000000..0a7f1a7
--- /dev/null
+++ b/app/Admin/Controllers/Finance/LedgerController.php
@@ -0,0 +1,170 @@
+baseCRUD()
+ ->tableLayout('fixed')
+ ->headerToolbar([
+ ...$this->baseHeaderToolBar(),
+ ])
+ ->bulkActions([])
+ ->filter($this->baseFilter()->body([
+ amis()->GroupControl()->mode('horizontal')->body([
+ amis()->SelectControl('store_id', __('finance.ledger.store'))
+ ->source(admin_url('api/stores'))
+ ->labelField('title')
+ ->valueField('id')
+ ->clearable()
+ ->columnRatio(3),
+ amis()->SelectControl('status', __('finance.ledger.ledger_status'))
+ ->multiple()
+ ->options(LedgerStatus::options())
+ ->columnRatio(3),
+ amis()->DateRangeControl('date_range', __('finance.ledger.date'))
+ ->valueFormat('YYYY-MM-DD'),
+ ]),
+ ]))
+ ->columns([
+ amis()->TableColumn()->name('id')->label(__('finance.ledger.id')),
+ amis()->TableColumn()->name('date')->label(__('finance.ledger.date')),
+ amis()->TableColumn()->name('store.title')->label(__('finance.ledger.store')),
+ amis()->TableColumn()->name('sales')->label(__('finance.ledger.sales')),
+ amis()->TableColumn()->name('expenditure')->label(__('finance.ledger.expenditure')),
+ amis()->TableColumn()->name('expected_commission')->label(__('finance.ledger.expected_commission')),
+ amis()->TableColumn()->name('actual_commission')->label(__('finance.ledger.actual_commission')),
+ amis()->TableColumn()->name('expected_income')->label(__('finance.ledger.expected_income')),
+ amis()->TableColumn()->name('actual_income')->label(__('finance.ledger.actual_income')),
+ amis()->TableColumn()
+ ->name('ledger_status')
+ ->label(__('finance.ledger.ledger_status'))
+ ->type('mapping')
+ ->map(LedgerStatus::labelMap()),
+ amis()->TableColumn()->name('created_at')->label(__('finance.ledger.created_at')),
+ $this->rowActions([
+ $this->rowApprovalButton()
+ ->visibleOn('${ledger_status == 2}'),
+ $this->rowEditButton(true)
+ ->visible(Admin::user()->can('admin.finance.ledgers.update'))
+ ->visibleOn('${ledger_status == 1 || ledger_status == 4}'),
+ $this->rowShowButton()
+ ->visible(Admin::user()->can('admin.finance.ledgers.view')),
+ ]),
+ ]);
+
+ return $this->baseList($crud);
+ }
+
+ public function form(): Form
+ {
+ return $this->baseForm()->title('')->body([
+ amis()->NumberControl()
+ ->name('actual_commission')
+ ->label(__('finance.ledger.actual_commission'))
+ ->precision(2)
+ ->showSteps(false)
+ ->required(),
+
+ amis()->NumberControl()
+ ->name('actual_income')
+ ->label(__('finance.ledger.actual_income'))
+ ->precision(2)
+ ->showSteps(false)
+ ->required(),
+ ]);
+ }
+
+ public function detail(): Form
+ {
+ return $this->baseDetail()->title()->body([
+ amis()->Property()->title('上报数据')->column(2)->items([
+ ['label' => __('finance.ledger.date'), 'content' => '${date}'],
+ ['label' => __('finance.ledger.store'), 'content' => '${store.title}'],
+ ['label' => __('finance.ledger.sales'), 'content' => '${sales}'],
+ ['label' => __('finance.ledger.expenditure'), 'content' => '${expenditure}'],
+ ['label' => __('finance.ledger.expected_commission'), 'content' => '${expected_commission}'],
+ ['label' => __('finance.ledger.actual_commission'), 'content' => '${actual_commission}'],
+ ['label' => __('finance.ledger.expected_income'), 'content' => '${expected_income}'],
+ ['label' => __('finance.ledger.actual_income'), 'content' => '${actual_income}'],
+ ['label' => __('finance.ledger.photos'), 'content' => amis()->Images()->enlargeAble()->source('${photos}')->enlargeWithGallary(), 'span' => 2],
+ ['label' => __('finance.ledger.ledger_status'), 'content' => amis()->Mapping()->map(LedgerStatus::labelMap())->value('${ledger_status}'), 'span' => 2],
+ ])
+ ]);
+ }
+
+ /**
+ * 审核
+ */
+ public function approval($id, Request $request)
+ {
+ $validator = Validator::make(
+ data: $request->all(),
+ rules: [
+ 'approval_result' => ['bail', 'required', Rule::in([1, 2])],
+ 'failed_reason' => ['bail', 'required_if:approval_result,2'],
+ ],
+ attributes: [
+ 'approval_result' => __('finance.ledger.approval_result'),
+ 'failed_reason' => __('finance.ledger.failed_reason'),
+ ],
+ );
+
+ if ($validator->fails()) {
+ return $this->response()->fail($validator->errors()->first());
+ }
+
+ $ledger = Ledger::findOrFail($id);
+
+ if ($ledger->ledger_status !== LedgerStatus::Processing) {
+ return $this->response()->fail('上报数据不是待审核状态');
+ }
+
+ if ($request->input('approval_result') == 1) {
+ $ledger->ledger_status = LedgerStatus::Passed;
+ } else {
+ $ledger->ledger_status = LedgerStatus::Rejected;
+ }
+
+ $ledger->save();
+
+ return $this->response()->success(null, '操作成功');
+ }
+
+ protected function rowApprovalButton()
+ {
+ return amis()->DialogAction()->icon('fa-regular fa-check-circle')->label(__('finance.ledger.approval'))->level('link')->dialog(
+ amis()->Dialog()->title(__('finance.ledger.approval'))->body([
+ amis()->Form()->title('')
+ ->api('post:'.admin_url('finance/ledgers/${id}/approval'))
+ ->body([
+ amis()->RadiosControl('approval_result', __('finance.ledger.approval_result'))
+ ->options([
+ ['label' => '通过', 'value' => 1],
+ ['label' => '驳回', 'value' => 2],
+ ])
+ ->selectFirst()
+ ->required(),
+ amis()->TextareaControl('failed_reason', __('finance.ledger.failed_reason'))
+ ->visibleOn('${approval_result == 2}')
+ ->required(),
+ ]),
+ ])->size('md')
+ );
+ }
+}
diff --git a/app/Admin/Filters/LedgerFilter.php b/app/Admin/Filters/LedgerFilter.php
new file mode 100644
index 0000000..e17881b
--- /dev/null
+++ b/app/Admin/Filters/LedgerFilter.php
@@ -0,0 +1,23 @@
+query->where('store_id', $id);
+ }
+
+ public function dateRange($dateRange)
+ {
+ $this->query->whereBetween('date', explode(',', $dateRange));
+ }
+
+ public function status($status)
+ {
+ $this->query->whereIn('ledger_status', explode(',', $status));
+ }
+}
diff --git a/app/Admin/Services/LedgerService.php b/app/Admin/Services/LedgerService.php
new file mode 100644
index 0000000..40f3668
--- /dev/null
+++ b/app/Admin/Services/LedgerService.php
@@ -0,0 +1,47 @@
+ ['bail', 'required', 'numeric'],
+ 'actual_income' => ['bail', 'required', 'numeric'],
+ ], [], [
+ 'actual_commission' => __('finance.ledger.actual_commission'),
+ 'actual_income' => __('finance.ledger.actual_income'),
+ ]);
+
+ $model = $this->query()->whereKey($primaryKey)->firstOrFail();
+
+ switch ($model->ledger_status) {
+ case LedgerStatus::Processing:
+ abort(400, '不能编辑待审核的上报数据');
+ break;
+
+ case LedgerStatus::Passed:
+ abort(400, '不能编辑已完成的上报数据');
+ break;
+ }
+
+ return $model->update([
+ 'actual_commission' => $data['actual_commission'],
+ 'actual_income' => $data['actual_income'],
+ 'ledger_status' => LedgerStatus::Processing,
+ ]);
+ }
+}
diff --git a/app/Admin/routes.php b/app/Admin/routes.php
index 948fd82..3008c6b 100644
--- a/app/Admin/routes.php
+++ b/app/Admin/routes.php
@@ -1,15 +1,16 @@
resource('rests', RestController::class);
});
+ /*
+ |--------------------------------------------------------------------------
+ | 财务管理
+ |--------------------------------------------------------------------------
+ */
+ $router->group([
+ 'prefix' => 'finance',
+ 'as' => 'finance.',
+ ], function (Router $router) {
+ // 上报数据
+ $router->resource('ledgers', LedgerController::class);
+ $router->post('ledgers/{ledger}/approval', [LedgerController::class, 'approval'])->name('ledgers.approval');
+ });
/*
|--------------------------------------------------------------------------
diff --git a/app/Enums/LedgerStatus.php b/app/Enums/LedgerStatus.php
new file mode 100644
index 0000000..235d11d
--- /dev/null
+++ b/app/Enums/LedgerStatus.php
@@ -0,0 +1,41 @@
+ '待编辑',
+ static::Processing => '待审核',
+ static::Passed => '已完成',
+ static::Rejected => '未通过',
+ };
+ }
+
+ public static function options(): array
+ {
+ return collect(self::cases())
+ ->map(fn (LedgerStatus $case) => [
+ 'label' => $case->label(),
+ 'value' => $case->value,
+ ])
+ ->all();
+ }
+
+ public static function labelMap(): array
+ {
+ return [
+ static::Pending->value => '待编辑',
+ static::Processing->value => '待审核',
+ static::Passed->value => '已完成',
+ static::Rejected->value => '未通过',
+ ];
+ }
+}
diff --git a/app/Models/Ledger.php b/app/Models/Ledger.php
new file mode 100644
index 0000000..76d78ad
--- /dev/null
+++ b/app/Models/Ledger.php
@@ -0,0 +1,69 @@
+ LedgerStatus::Pending,
+ ];
+
+ protected $casts = [
+ 'ledger_status' => LedgerStatus::class,
+ ];
+
+ protected $fillable = [
+ 'store_id',
+ 'date',
+ 'sales',
+ 'expenditure',
+ 'expected_commission',
+ 'actual_commission',
+ 'expected_income',
+ 'actual_income',
+ 'photos',
+ 'ledger_status',
+ ];
+
+ public function store(): BelongsTo
+ {
+ return $this->belongsTo(Store::class);
+ }
+
+ 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 : []),
+ );
+ }
+
+ protected function ledgerStatusColor(): Attribute
+ {
+ return Attribute::make(
+ get: fn (mixed $value) => $this->ledger_status->color(),
+ );
+ }
+
+ protected function ledgerStatusLabel(): Attribute
+ {
+ return Attribute::make(
+ get: fn (mixed $value) => $this->ledger_status->label(),
+ );
+ }
+}
diff --git a/database/migrations/2024_03_27_090406_create_ledgers_table.php b/database/migrations/2024_03_27_090406_create_ledgers_table.php
new file mode 100644
index 0000000..ae732f3
--- /dev/null
+++ b/database/migrations/2024_03_27_090406_create_ledgers_table.php
@@ -0,0 +1,39 @@
+id();
+ $table->bigInteger('store_id')->unsigned()->comment('门店ID');
+ $table->date('date')->comment('日期');
+ $table->decimal('sales', 10, 2)->comment('销量');
+ $table->decimal('expenditure', 10, 2)->comment('支出');
+ $table->decimal('expected_commission', 10, 2)->comment('预期佣金=销售金额*佣金比例');
+ $table->decimal('actual_commission', 10, 2)->nullable()->comment('实际佣金');
+ $table->decimal('expected_income', 10, 2)->comment('预期收益=预计佣金-支出');
+ $table->decimal('actual_income', 10, 2)->nullable()->comment('实际收益');
+ $table->text('photos')->nullable()->comment('照片');
+ $table->tinyInteger('ledger_status')->default(1)->comment('状态: 1 待编辑, 2 待审核, 3 已通过, 4 未通过');
+ $table->timestamps();
+
+ $table->unique(['store_id', 'date']);
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::dropIfExists('ledgers');
+ }
+};
diff --git a/database/seeders/AdminPermissionSeeder.php b/database/seeders/AdminPermissionSeeder.php
index a129b7d..0ee99e3 100644
--- a/database/seeders/AdminPermissionSeeder.php
+++ b/database/seeders/AdminPermissionSeeder.php
@@ -119,6 +119,26 @@ class AdminPermissionSeeder extends Seeder
],
],
+ /*
+ |--------------------------------------------------------------------------
+ | 财务报表
+ |--------------------------------------------------------------------------
+ */
+ 'finance' => [
+ 'name' => '财务报表',
+ 'icon' => 'material-symbols:finance-mode',
+ 'uri' => '/finance',
+ 'children' => [
+ 'ledgers' => [
+ 'name' => '上报数据',
+ 'icon' => 'mdi:database',
+ 'uri' => '/finance/ledgers',
+ 'resource' => ['list', 'update', 'view'],
+ 'children' => [],
+ ],
+ ],
+ ],
+
/*
|--------------------------------------------------------------------------
| 系统管理
diff --git a/lang/zh_CN/finance.php b/lang/zh_CN/finance.php
new file mode 100644
index 0000000..a98bf5c
--- /dev/null
+++ b/lang/zh_CN/finance.php
@@ -0,0 +1,22 @@
+ [
+ 'id' => 'ID',
+ 'date' => '日期',
+ 'store' => '门店',
+ 'sales' => '销量',
+ 'expenditure' => '支出',
+ 'expected_commission' => '预期佣金',
+ 'actual_commission' => '实际佣金',
+ 'expected_income' => '预期收益',
+ 'actual_income' => '实际收益',
+ 'photos' => '上传图片',
+ 'ledger_status' => '状态',
+ 'created_at' => '创建时间',
+ 'updated_at' => '更新时间',
+ 'approval' => '审核',
+ 'approval_result' => '审核结果',
+ 'failed_reason' => '驳回原因',
+ ],
+];