diff --git a/app/Admin/Controllers/AdminController.php b/app/Admin/Controllers/AdminController.php index ad5abd0..3382a67 100644 --- a/app/Admin/Controllers/AdminController.php +++ b/app/Admin/Controllers/AdminController.php @@ -16,6 +16,24 @@ use App\Traits\CustomActionTrait; abstract class AdminController extends Controller { use CustomActionTrait; + + public function store(Request $request) + { + try { + if ($this->actionOfQuickEdit()) { + $result = $this->service->quickEdit($request->all()); + } else { + $result = $this->service->store($request->all()); + } + } catch (Throwable $th) { + return $this->renderExceptionResponse( + tap($th, fn () => report($th)) + ); + } + + return $this->autoResponse($result, __('admin.save')); + } + public function update(Request $request) { $input = $request->all(); diff --git a/app/Admin/Controllers/Finance/CommissionIncomeController.php b/app/Admin/Controllers/Finance/CommissionIncomeController.php new file mode 100644 index 0000000..6a6e201 --- /dev/null +++ b/app/Admin/Controllers/Finance/CommissionIncomeController.php @@ -0,0 +1,88 @@ +actionOfGetData()) { + return $this->response()->success([ + 'items' => [$this->getCommissionIncomeStatistics(request())], + ]); + } + + return $this->response()->success( + $this->baseList( + $this->baseCRUD() + ->headerToolbar([ + amis('filter-toggler')->align('right'), + ]) + ->footerToolbar([]) + ->bulkActions([]) + ->filter($this->baseFilter()->body([ + amis()->GroupControl()->mode('horizontal')->body([ + amis()->DateRangeControl('date_range', '日期') + ->valueFormat('YYYY-MM-DD') + ->columnRatio(6), + amis()->InputCityControl('region', '区域') + ->allowDistrict(false) + ->extractValue(false), + amis()->SelectControl('store_id', __('finance.ledger.store')) + ->source(admin_url('api/stores?region=${region}')) + ->labelField('title') + ->valueField('id') + ->clearable(), + ]), + ])) + ->columns([ + amis()->TableColumn('expected_commission', '预期佣金'), + amis()->TableColumn('actual_commission', '实际佣金'), + amis()->TableColumn('diff_commission', '佣金差异'), + amis()->TableColumn('expected_income', '预期收益'), + amis()->TableColumn('actual_income', '实际收益'), + amis()->TableColumn('diff_income', '收益差异'), + ]) + ) + ); + } + + protected function getCommissionIncomeStatistics(Request $request): array + { + $aggregate = Ledger::select([ + DB::raw('SUM(expenditure) as expenditure'), + DB::raw('SUM(expected_commission) as expected_commission'), + DB::raw('SUM(actual_commission) as actual_commission'), + DB::raw('SUM(expected_income) as expected_income'), + DB::raw('SUM(actual_income) as actual_income'), + ]) + ->filter($request->input(), LedgerFilter::class) + ->first(); + + $expectedCommission = $aggregate->expected_commission ?? '0'; + $actualCommission = $aggregate->actual_commission ?? '0'; + $expectedIncome = $aggregate->expected_income ?? '0'; + $actualIncome = $aggregate->actual_income ?? '0'; + + return [ + 'expected_commission' => trim_zeros($expectedCommission), + 'actual_commission' => trim_zeros($actualCommission), + 'diff_commission' => trim_zeros(bcsub($actualCommission, $expectedCommission, 2)), + 'expected_income' => trim_zeros($expectedIncome), + 'actual_income' => trim_zeros($actualIncome), + 'diff_income' => trim_zeros(bcsub($actualIncome, $expectedIncome, 2)), + ]; + } +} diff --git a/app/Admin/Controllers/Finance/SalesStatisticController.php b/app/Admin/Controllers/Finance/SalesStatisticController.php index a66fc42..bf1ac4a 100644 --- a/app/Admin/Controllers/Finance/SalesStatisticController.php +++ b/app/Admin/Controllers/Finance/SalesStatisticController.php @@ -36,7 +36,6 @@ class SalesStatisticController extends AdminController ->valueFormat('YYYY-MM-DD') ->columnRatio(6), amis()->InputCityControl('region', '区域') - ->inputClassName('w-40') ->allowDistrict(false) ->extractValue(false), amis()->SelectControl('store_id', __('finance.ledger.store')) diff --git a/app/Admin/Controllers/Finance/StoreMasterCommissionController.php b/app/Admin/Controllers/Finance/StoreMasterCommissionController.php new file mode 100644 index 0000000..57a1f19 --- /dev/null +++ b/app/Admin/Controllers/Finance/StoreMasterCommissionController.php @@ -0,0 +1,184 @@ +all(), + rules: [ + 'approval_result' => ['bail', 'required', Rule::in([1, 2])], + 'failed_reason' => ['bail', 'required_if:approval_result,2'], + ], + attributes: [ + 'approval_result' => __('finance.store_master_commission.approval_result'), + 'failed_reason' => __('finance.store_master_commission.failed_reason'), + ], + ); + + if ($validator->fails()) { + return $this->response()->fail($validator->errors()->first()); + } + + $masterCommission = StoreMasterCommission::findOrFail($id); + + if ($masterCommission->approval_status !== StoreMasterCommissionApprovalStatus::Pending) { + return $this->response()->fail('店长提成不是待审核状态'); + } + + if ($request->input('approval_result') == 1) { + $masterCommission->approval_status = StoreMasterCommissionApprovalStatus::Passed; + } else { + $masterCommission->approval_status = StoreMasterCommissionApprovalStatus::Rejected; + } + + $masterCommission->save(); + + return $this->response()->success(null, '操作成功'); + } + + public function list(): Page + { + $crud = $this->baseCRUD() + ->headerToolbar([ + $this->createTypeButton('drawer', 'lg') + ->visible(Admin::user()->can('admin.finance.store_master_commissions.create')), + ...$this->baseHeaderToolBar(), + ]) + ->bulkActions([]) + ->filter($this->baseFilter()->body([ + amis()->GroupControl()->mode('horizontal')->body([ + amis()->MonthControl() + ->name('month') + ->label(__('finance.store_master_commission.month')) + ->valueFormat('YYYY-MM') + ->columnRatio(3), + amis()->SelectControl('store_id', __('finance.ledger.store')) + ->source(admin_url('api/stores')) + ->labelField('title') + ->valueField('id') + ->clearable() + ->columnRatio(3), + amis()->SelectControl('status', __('finance.store_master_commission.approval_status')) + ->multiple() + ->options(StoreMasterCommissionApprovalStatus::options()) + ->columnRatio(3), + ]), + ])) + ->columns([ + amis()->TableColumn()->name('id')->label(__('finance.store_master_commission.id')), + amis()->TableColumn()->name('month')->label(__('finance.store_master_commission.month')), + amis()->TableColumn()->name('store.title')->label(__('finance.store_master_commission.store')), + amis()->TableColumn()->name('master.name')->label(__('finance.store_master_commission.store_master')), + amis()->TableColumn()->name('commission')->label(__('finance.store_master_commission.commission')), + amis()->TableColumn()->name('daily_expenses')->label(__('finance.store_master_commission.daily_expenses')), + amis()->TableColumn()->name('employee_expenses')->label(__('finance.store_master_commission.employee_expenses')), + amis()->TableColumn()->name('other_expenses')->label(__('finance.store_master_commission.other_expenses')), + amis()->TableColumn() + ->name('approval_status') + ->label(__('finance.store_master_commission.approval_status')) + ->type('mapping') + ->map(StoreMasterCommissionApprovalStatus::labelMap()), + amis()->TableColumn()->name('created_at')->label(__('finance.store_master_commission.created_at')), + $this->rowActions([ + $this->rowApprovalButton() + ->visibleOn('${approval_status == '.StoreMasterCommissionApprovalStatus::Pending->value.'}'), + $this->rowEditTypeButton('drawer', 'lg') + ->visible(Admin::user()->can('admin.finance.store_master_commissions.update')) + ->visibleOn('${approval_status == '.StoreMasterCommissionApprovalStatus::Rejected->value.'}'), + $this->rowDeleteButton()->visible(Admin::user()->can('admin.finance.store_master_commissions.delete')), + ]), + ]); + + return $this->baseList($crud); + } + + public function form(): Form + { + return $this->baseForm()->title('')->body([ + amis()->MonthControl() + ->name('month') + ->label(__('finance.store_master_commission.month')) + ->required() + ->valueFormat('YYYY-MM'), + + amis()->SelectControl('store_id', __('finance.store_master_commission.store')) + ->source(admin_url('api/stores')) + ->labelField('title') + ->valueField('id') + ->required() + ->clearable(), + + amis()->NumberControl() + ->name('commission') + ->label(__('finance.store_master_commission.commission')) + ->precision(2) + ->showSteps(false) + ->required(), + + amis()->NumberControl() + ->name('daily_expenses') + ->label(__('finance.store_master_commission.daily_expenses')) + ->precision(2) + ->showSteps(false) + ->required(), + + amis()->NumberControl() + ->name('employee_expenses') + ->label(__('finance.store_master_commission.employee_expenses')) + ->precision(2) + ->showSteps(false) + ->required(), + + amis()->NumberControl() + ->name('other_expenses') + ->label(__('finance.store_master_commission.other_expenses')) + ->precision(2) + ->showSteps(false) + ->required(), + ]); + } + + protected function rowApprovalButton() + { + return amis()->DialogAction()->icon('fa-regular fa-check-circle')->label(__('finance.store_master_commission.approval'))->level('link')->dialog( + amis()->Dialog()->title(__('finance.store_master_commission.approval'))->body([ + amis()->Form()->title('') + ->api('post:'.admin_url('finance/store-master-commissions/${id}/approval')) + ->body([ + amis()->RadiosControl('approval_result', __('finance.store_master_commission.approval_result')) + ->options([ + ['label' => '通过', 'value' => 1], + ['label' => '驳回', 'value' => 2], + ]) + ->selectFirst() + ->required(), + amis()->TextareaControl('failed_reason', __('finance.store_master_commission.failed_reason')) + ->visibleOn('${approval_result == 2}') + ->required(), + ]), + ])->size('md') + ); + } +} diff --git a/app/Admin/Filters/LedgerFilter.php b/app/Admin/Filters/LedgerFilter.php index 5a80b70..6c2e6fc 100644 --- a/app/Admin/Filters/LedgerFilter.php +++ b/app/Admin/Filters/LedgerFilter.php @@ -24,11 +24,12 @@ class LedgerFilter extends ModelFilter public function region($region) { - if (! is_array($region)) { + if (array_key_exists('store_id', $this->input) || ! is_array($region)) { return; } $provinceCode = Arr::get($region, 'provinceCode'); + $cityCode = Arr::get($region, 'cityCode'); if (empty($provinceCode) && empty($cityCode)) { diff --git a/app/Admin/Filters/LedgerItemFilter.php b/app/Admin/Filters/LedgerItemFilter.php index a8d2b9e..f8c7865 100644 --- a/app/Admin/Filters/LedgerItemFilter.php +++ b/app/Admin/Filters/LedgerItemFilter.php @@ -14,7 +14,7 @@ class LedgerItemFilter extends ModelFilter public function region($region) { - if ($this->input('store_id') !== null || ! is_array($region)) { + if (array_key_exists('store_id', $this->input) || ! is_array($region)) { return; } diff --git a/app/Admin/Filters/StoreMasterCommissionFilter.php b/app/Admin/Filters/StoreMasterCommissionFilter.php new file mode 100644 index 0000000..48865d6 --- /dev/null +++ b/app/Admin/Filters/StoreMasterCommissionFilter.php @@ -0,0 +1,23 @@ +where('month', $month); + } + + public function store($id) + { + $this->where('store_id', $id); + } + + public function status($status) + { + $this->whereIn('approval_status', explode(',', $status)); + } +} diff --git a/app/Admin/Services/Finance/StoreMasterCommissionService.php b/app/Admin/Services/Finance/StoreMasterCommissionService.php new file mode 100644 index 0000000..fded0e0 --- /dev/null +++ b/app/Admin/Services/Finance/StoreMasterCommissionService.php @@ -0,0 +1,77 @@ + ['bail', 'required', 'date_format:Y-m'], + 'store_id' => ['bail', 'required', 'int'], + 'commission' => ['bail', 'required', 'numeric'], + 'daily_expenses' => ['bail', 'required', 'numeric'], + 'employee_expenses' => ['bail', 'required', 'numeric'], + 'other_expenses' => ['bail', 'required', 'numeric'], + ], [], [ + 'month' => __('finance.store_master_commission.month'), + 'store_id' => __('finance.store_master_commission.store'), + 'commission' => __('finance.store_master_commission.commission'), + 'daily_expenses' => __('finance.store_master_commission.daily_expenses'), + 'employee_expenses' => __('finance.store_master_commission.employee_expenses'), + 'other_expenses' => __('finance.store_master_commission.other_expenses'), + ]); + + $store = Store::findOrFail($validated['store_id']); + + $this->modelName::create( + array_merge($validated, ['store_master_id' => $store->master_id]) + ); + + return true; + } + + public function update($primaryKey, $data): bool + { + $validated = Validator::validate($data, [ + 'month' => ['bail', 'required', 'date_format:Y-m'], + 'store_id' => ['bail', 'required', 'int'], + 'commission' => ['bail', 'required', 'numeric'], + 'daily_expenses' => ['bail', 'required', 'numeric'], + 'employee_expenses' => ['bail', 'required', 'numeric'], + 'other_expenses' => ['bail', 'required', 'numeric'], + ], [], [ + 'month' => __('finance.store_master_commission.month'), + 'store_id' => __('finance.store_master_commission.store'), + 'commission' => __('finance.store_master_commission.commission'), + 'daily_expenses' => __('finance.store_master_commission.daily_expenses'), + 'employee_expenses' => __('finance.store_master_commission.employee_expenses'), + 'other_expenses' => __('finance.store_master_commission.other_expenses'), + ]); + + $model = $this->query()->whereKey($primaryKey)->firstOrFail(); + + if ($model->approval_status !== StoreMasterCommissionApprovalStatus::Rejected) { + abort(400, '只能编辑状态是“未通过”的店长提成'); + } + + $attributes = array_merge( + $validated, ['approval_status' => StoreMasterCommissionApprovalStatus::Pending] + ); + + return $model->update($attributes); + } +} diff --git a/app/Admin/routes.php b/app/Admin/routes.php index 6bfe51b..1b80cd4 100644 --- a/app/Admin/routes.php +++ b/app/Admin/routes.php @@ -1,8 +1,10 @@ resource('ledgers', LedgerController::class); $router->post('ledgers/{ledger}/approval', [LedgerController::class, 'approval'])->name('ledgers.approval'); + // 佣金收入 + $router->get('commission-incomes', [CommissionIncomeController::class, 'index'])->name('commission_incomes.index'); // 销售统计 $router->get('sales-statistics', [SalesStatisticController::class, 'index'])->name('sales_statistics.index'); // 门店统计 $router->get('store-statistics', [StoreStatisticController::class, 'index'])->name('store_statistics.index'); + // 店长提成 + $router->resource('store-master-commissions', StoreMasterCommissionController::class); + $router->post('store-master-commissions/{store_master_commission}/approval', [StoreMasterCommissionController::class, 'approval'])->name('store_statistics.approval'); }); /* diff --git a/app/Enums/StoreMasterCommissionApprovalStatus.php b/app/Enums/StoreMasterCommissionApprovalStatus.php new file mode 100644 index 0000000..4d56047 --- /dev/null +++ b/app/Enums/StoreMasterCommissionApprovalStatus.php @@ -0,0 +1,38 @@ + '待审核', + self::Passed => '已通过', + self::Rejected => '未通过', + }; + } + + public static function options(): array + { + return collect(self::cases()) + ->map(fn (StoreMasterCommissionApprovalStatus $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/StoreMasterCommission.php b/app/Models/StoreMasterCommission.php new file mode 100644 index 0000000..b67c36b --- /dev/null +++ b/app/Models/StoreMasterCommission.php @@ -0,0 +1,44 @@ + StoreMasterCommissionApprovalStatus::Pending, + ]; + + protected $casts = [ + 'approval_status' => StoreMasterCommissionApprovalStatus::class, + ]; + + protected $fillable = [ + 'month', + 'store_id', + 'store_master_id', + 'commission', + 'daily_expenses', + 'employee_expenses', + 'other_expenses', + 'approval_status', + ]; + + public function store() + { + return $this->belongsTo(Store::class, 'store_id'); + } + + public function master(): BelongsTo + { + return $this->belongsTo(Employee::class, 'store_master_id'); + } +} diff --git a/database/migrations/2024_03_29_155959_create_store_master_commissions_table.php b/database/migrations/2024_03_29_155959_create_store_master_commissions_table.php new file mode 100644 index 0000000..f525b01 --- /dev/null +++ b/database/migrations/2024_03_29_155959_create_store_master_commissions_table.php @@ -0,0 +1,35 @@ +id(); + $table->string('month')->comment('月份: 2024-03'); + $table->foreignId('store_id')->comment('门店ID'); + $table->foreignId('store_master_id')->comment('店长ID'); + $table->decimal('commission', 10, 2)->comment('佣金'); + $table->decimal('daily_expenses', 10, 2)->comment('日常支出'); + $table->decimal('employee_expenses', 10, 2)->comment('员工支出'); + $table->decimal('other_expenses', 10, 2)->comment('其他支出'); + $table->tinyInteger('approval_status')->default(1)->comment('状态: 1 待审核, 2 已通过, 3 已拒绝'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('store_master_commissions'); + } +}; diff --git a/database/seeders/AdminPermissionSeeder.php b/database/seeders/AdminPermissionSeeder.php index e8482c1..9b3ae8d 100644 --- a/database/seeders/AdminPermissionSeeder.php +++ b/database/seeders/AdminPermissionSeeder.php @@ -148,6 +148,15 @@ class AdminPermissionSeeder extends Seeder 'resource' => ['list', 'update', 'view'], 'children' => [], ], + 'commission_incomes' => [ + 'name' => '佣金收入', + 'icon' => 'ri:money-cny-circle-line', + 'uri' => '/finance/commission-incomes', + 'resource' => false, + 'children' => [ + 'index' => '佣金收入', + ], + ], 'sales_statistics' => [ 'name' => '销售统计', 'icon' => 'ri:bar-chart-2-line', @@ -166,6 +175,13 @@ class AdminPermissionSeeder extends Seeder 'index' => '门店统计', ], ], + 'store_master_commissions' => [ + 'name' => '店长提成', + 'icon' => 'icon-park-outline:paper-money', + 'uri' => '/finance/store-master-commissions', + 'resource' => ['list', 'create', 'update', 'delete'], + 'children' => [], + ], ], ], diff --git a/lang/zh_CN/finance.php b/lang/zh_CN/finance.php index a98bf5c..7ac469d 100644 --- a/lang/zh_CN/finance.php +++ b/lang/zh_CN/finance.php @@ -19,4 +19,21 @@ return [ 'approval_result' => '审核结果', 'failed_reason' => '驳回原因', ], + + 'store_master_commission' => [ + 'id' => 'ID', + 'month' => '月份', + 'store' => '门店', + 'store_master' => '店长', + 'commission' => '佣金', + 'daily_expenses' => '日常支出', + 'employee_expenses' => '员工支出', + 'other_expenses' => '其他支出', + 'approval' => '审核', + 'approval_status' => '状态', + 'approval_result' => '审核结果', + 'failed_reason' => '驳回原因', + 'created_at' => '创建时间', + 'updated_at' => '更新时间', + ], ];