diff --git a/app/Admin/Controllers/Finance/LedgerController.php b/app/Admin/Controllers/Finance/LedgerController.php index 7c250af..dfbd616 100644 --- a/app/Admin/Controllers/Finance/LedgerController.php +++ b/app/Admin/Controllers/Finance/LedgerController.php @@ -98,16 +98,14 @@ class LedgerController extends AdminController ->url(admin_url('api/workflow/apply')) ->method('post') ->data(['id' => '${workflow.id}']) - ), + ) + ->visibleOn('${actual_commission != null && actual_income != null}'), $this->cancelAction(), $this->rowEditLedgerAmountButton() ->visible(Admin::user()->can('admin.finance.ledgers.update_ledger_amount')), - $this->rowEditActualCommissionButton() - ->visible(Admin::user()->can('admin.finance.ledgers.update_actual_commission')) - ->visibleOn('${OR(workflow.check_status == '.CheckStatus::None->value.', workflow.check_status == '.CheckStatus::Cancel->value.', workflow.check_status == '.CheckStatus::Fail->value.')}'), - $this->rowEditActualIncomeButton() - ->visible(Admin::user()->can('admin.finance.ledgers.update_actual_income')) - ->visibleOn('${OR(workflow.check_status == '.CheckStatus::None->value.', workflow.check_status == '.CheckStatus::Cancel->value.', workflow.check_status == '.CheckStatus::Fail->value.')}'), + $this->rowEditTypeButton('drawer', 'lg') + ->visible(Admin::user()->can('admin.finance.ledgers.update')) + ->visibleOn('${ARRAYINCLUDES(['.CheckStatus::None->value.','.CheckStatus::Cancel->value.','.CheckStatus::Fail->value.'], workflow.check_status)}'), $this->rowShowButton() ->visible(Admin::user()->can('admin.finance.ledgers.view')), ]), @@ -197,26 +195,6 @@ class LedgerController extends AdminController return $this->response()->success(null, '保存成功'); } - /** - * 修改实际佣金 - */ - public function updateActualCommission($id, Request $request) - { - $this->service->update($id, $request->only(['actual_commission'])); - - return $this->response()->success(null, '保存成功'); - } - - /** - * 修改实际收益 - */ - public function updateActualIncome($id, Request $request) - { - $this->service->update($id, $request->only(['actual_income'])); - - return $this->response()->success(null, '保存成功'); - } - /** * 编辑总账金额按钮 */ @@ -241,54 +219,4 @@ class LedgerController extends AdminController ])->size('lg') ); } - - /** - * 编辑实际佣金按钮 - */ - protected function rowEditActualCommissionButton(): DrawerAction - { - return amis()->DrawerAction() - ->icon('fa-regular fa-pen-to-square') - ->label(__('finance.ledger.actual_commission')) - ->level('link') - ->drawer( - amis()->Drawer()->title(__('finance.ledger.actual_commission'))->body([ - amis()->Form()->title('') - ->api('post:'.admin_url('finance/ledgers/${id}/actual-commission')) - ->body([ - amis()->NumberControl() - ->name('actual_commission') - ->label(__('finance.ledger.actual_commission')) - ->precision(2) - ->showSteps(false) - ->required(), - ]), - ])->size('lg') - ); - } - - /** - * 编辑实际收益按钮 - */ - protected function rowEditActualIncomeButton(): DrawerAction - { - return amis()->DrawerAction() - ->icon('fa-regular fa-pen-to-square') - ->label(__('finance.ledger.actual_income')) - ->level('link') - ->drawer( - amis()->Drawer()->title(__('finance.ledger.actual_income'))->body([ - amis()->Form()->title('') - ->api('post:'.admin_url('finance/ledgers/${id}/actual-income')) - ->body([ - amis()->NumberControl() - ->name('actual_income') - ->label(__('finance.ledger.actual_income')) - ->precision(2) - ->showSteps(false) - ->required(), - ]), - ])->size('lg') - ); - } } diff --git a/app/Admin/routes.php b/app/Admin/routes.php index 5b7e1bb..7bafef6 100644 --- a/app/Admin/routes.php +++ b/app/Admin/routes.php @@ -128,8 +128,6 @@ Route::group([ // 上报数据 $router->resource('ledgers', LedgerController::class); $router->post('ledgers/{ledger}/ledger-amount', [LedgerController::class, 'updateLedgerAmount'])->name('ledgers.update_ledger_amount'); - $router->post('ledgers/{ledger}/actual-commission', [LedgerController::class, 'updateActualCommission'])->name('ledgers.update_actual_commission'); - $router->post('ledgers/{ledger}/actual-income', [LedgerController::class, 'updateActualIncome'])->name('ledgers.update_actual_income'); // 佣金收入 $router->get('commission-incomes', [CommissionIncomeController::class, 'index'])->name('commission_incomes.index'); // 收支报销 diff --git a/app/Http/Controllers/Api/Account/StoreMasterCommissionController.php b/app/Http/Controllers/Api/Account/StoreMasterCommissionController.php new file mode 100644 index 0000000..eda3da4 --- /dev/null +++ b/app/Http/Controllers/Api/Account/StoreMasterCommissionController.php @@ -0,0 +1,24 @@ +user(); + + $storeMasterCommissions = $user->storeMasterCommissions() + ->onlyApproved() + ->orderBy(DB::raw("STR_TO_DATE(month, '%Y-%m')"), 'DESC') + ->simplePaginate($request->query('per_page', 20)); + + return StoreMasterCommissionResource::collection($storeMasterCommissions); + } +} diff --git a/app/Http/Controllers/Api/Auth/UserController.php b/app/Http/Controllers/Api/Auth/UserController.php index 3422b50..97eecfd 100644 --- a/app/Http/Controllers/Api/Auth/UserController.php +++ b/app/Http/Controllers/Api/Auth/UserController.php @@ -11,6 +11,7 @@ use Illuminate\Validation\ValidationException; use App\Enums\UserRole; use App\Http\Resources\KeywordResource; use App\Admin\Services\EmployeeService; +use App\Http\Resources\StoreResource; use Illuminate\Support\Facades\DB; /** @@ -29,7 +30,7 @@ class UserController extends Controller 'phone' => $user->phone, 'avatar' => $user->avatar, 'jobs' => KeywordResource::collection($user->jobs), - + 'store' => $user->store ? StoreResource::make($user->store) : null, 'unread_notifications' => 0, // 身份: user-普通员工, store-店长, admin-管理员 'role' => $user->userRole(), diff --git a/app/Http/Controllers/Api/Ledger/LedgerController.php b/app/Http/Controllers/Api/Ledger/LedgerController.php new file mode 100644 index 0000000..04c064e --- /dev/null +++ b/app/Http/Controllers/Api/Ledger/LedgerController.php @@ -0,0 +1,197 @@ +user(); + + if (! $user->isStoreMaster()) { + throw new RuntimeException('非店长不可上报数据'); + } + + // 是否是彩票店数据上报 + $isLotteryLedger = $user->store->isLotteryStore(); + + $validated = $request->validate( + rules: [ + 'date' => ['bail', 'required', 'date_format:Y-m-d'], + 'items' => $isLotteryLedger ? ['bail', 'required', 'array'] : ['bail', 'array'], + 'new_customers' => ['bail', 'required', 'int', 'min:0'], + 'sales' => ['bail', 'required', 'numeric', 'min:0'], + 'expenditure' => ['bail', 'required', 'numeric', 'min:0'], + 'handover_amount' => ['bail', 'required', 'numeric', 'min:0'], + 'photos' => ['bail', 'required', 'array'], + ], + attributes: [ + 'date' => '日期', + 'items' => '彩种数据', + 'new_customers' => '新增客户', + 'sales' => '销售合计', + 'expenditure' => $isLotteryLedger ? '兑奖合计' : '支出合计', + 'handover_amount' => '交账金额', + 'photos' => '时段报表照片', + ], + ); + + /** @var \Illuminate\Database\Eloquent\Collection */ + $lotteryTypes = Keyword::filter(['parent_key' => 'lottery_type']) + ->oldest('sort') + ->get() + // 过滤未绑定上报数据类型的彩种 + ->filter(fn (Keyword $type) => (string) $type->value !== ''); + + // 上报数据项的格式: + // [ + // ['id' => '上报数据类型1', 'sales' => '销售金额', 'expenditure' => '兑奖金额'], + // ['id' => '上报数据类型2', 'sales' => '销售金额', 'expenditure' => '兑奖金额'], + // ] + + if ($isLotteryLedger) { + $items = collect($validated['items'])->keyBy('id'); + + /** @var \App\Models\Keyword */ + foreach ($lotteryTypes as $lotteryType) { + $item = $items->get($lotteryType->value); + + if (is_null($item)) { + throw new RuntimeException("{$lotteryType->name}未填写上报数据"); + } + + Validator::validate( + data: $item, + rules: [ + 'sales' => ['bail', 'required', 'numeric', 'min:0'], + 'expenditure' => ['bail', 'required', 'numeric', 'min:0'], + ], + attributes: [ + 'sales' => "[$lotteryType->name]销售金额", + 'expenditure' => "[$lotteryType->name]兑奖金额", + ], + ); + } + } + + /** @var \App\Models\Ledger|null */ + $ledger = Ledger::where('store_id', $user->store_id) + ->where('date', $validated['date']) + ->first(); + + if ($ledger && ! $ledger->allowReReport()) { + throw new RuntimeException('上报数据已更新,不可重新上传'); + } + + $ratio = bcdiv($user->store->profit_ratio, 100, 2); + + // 计算预期佣金 + $validated['expected_commission'] = bcmul($validated['sales'], $ratio, 2); + // 计算预期收益 + $validated['expected_income'] = bcsub($validated['expected_commission'], $validated['expenditure'], 2); + + try { + DB::beginTransaction(); + + if (is_null($ledger)) { + $ledger = Ledger::create( + array_merge($validated, ['store_id' => $user->store_id]) + ); + } else { + $ledger->update($validated); + $ledger->items()->delete(); + } + + LedgerItem::insert( + collect( + $isLotteryLedger ? $validated['items'] : [ + [ + 'id' => 'ledger_item_type_other', + 'sales' => $ledger->sales, + 'expenditure' => $ledger->expenditure, + ], + ] + )->map(fn ($item) => [ + 'date' => $ledger->date, + 'store_id' => $ledger->store_id, + 'ledger_id' => $ledger->id, + 'ledger_item_type_id' => $item['id'], + 'sales' => $item['sales'], + 'expenditure' => $item['expenditure'], + 'created_at' => $ledger->updated_at, + 'updated_at' => $ledger->updated_at, + ])->all() + ); + + DB::commit(); + } catch (Throwable $e) { + DB::rollBack(); + + throw tap($e, fn ($e) => report($e)); + } + + return $this->prepareLedger($ledger); + } + + public function show(string $date, Request $request) + { + /** @var \App\Models\Employee */ + $user = $request->user(); + + /** @var \App\Models\Ledger|null */ + $ledger = Ledger::with(['items']) + ->where('store_id', $user->store_id) + ->where('date', $date) + ->first(); + + $data = null; + + if ($ledger) { + $data = [ + 'date' => $ledger->date, + 'items' => $ledger->items->map(fn ($item) => [ + 'id' => $item->ledger_item_type_id, + 'sales' => $item->sales, + 'expenditure' => $item->expenditure, + ]), + 'new_customers' => $ledger->new_customers, + 'sales' => $ledger->sales, + 'expenditure' => $ledger->expenditure, + 'handover_amount' => $ledger->handover_amount, + 'photos' => $ledger->photos, + ]; + } + + return [ + 'data' => $ledger ? $this->prepareLedger($ledger) : null, + ]; + } + + protected function prepareLedger(Ledger $ledger) + { + return [ + 'date' => $ledger->date, + 'items' => $ledger->items->map(fn ($item) => [ + 'id' => $item->ledger_item_type_id, + 'sales' => $item->sales, + 'expenditure' => $item->expenditure, + ]), + 'new_customers' => $ledger->new_customers, + 'sales' => $ledger->sales, + 'expenditure' => $ledger->expenditure, + 'handover_amount' => $ledger->handover_amount, + 'photos' => $ledger->photos, + ]; + } +} diff --git a/app/Http/Controllers/Api/Ledger/LotteryTypeController.php b/app/Http/Controllers/Api/Ledger/LotteryTypeController.php new file mode 100644 index 0000000..822d1b0 --- /dev/null +++ b/app/Http/Controllers/Api/Ledger/LotteryTypeController.php @@ -0,0 +1,22 @@ + 'lottery_type'])->oldest('sort')->get(); + + return $lotteryTypes + ->filter(fn (Keyword $type) => (string) $type->value !== '') + ->map(fn ($item) => [ + 'id' => $item->value, + 'name' => $item->name, + ]); + } +} diff --git a/app/Http/Controllers/Api/LotteryTypeController.php b/app/Http/Controllers/Api/LotteryTypeController.php deleted file mode 100644 index 6c0ebf7..0000000 --- a/app/Http/Controllers/Api/LotteryTypeController.php +++ /dev/null @@ -1,19 +0,0 @@ - 'lottery_type'])->oldest('sort')->get(); - - return $lotteryTypes->map(fn ($item) => [ - 'id' => $item->value, - 'name' => $item->name, - ]); - } -} diff --git a/app/Http/Controllers/Api/Reimbursement/ReimbursementTypeController.php b/app/Http/Controllers/Api/Reimbursement/ReimbursementTypeController.php new file mode 100644 index 0000000..284f9cd --- /dev/null +++ b/app/Http/Controllers/Api/Reimbursement/ReimbursementTypeController.php @@ -0,0 +1,17 @@ + 'reimbursement_type'])->get(); + + return KeywordResource::collection($keywords); + } +} diff --git a/app/Http/Resources/KeywordResource.php b/app/Http/Resources/KeywordResource.php index c0535e0..2ecb230 100644 --- a/app/Http/Resources/KeywordResource.php +++ b/app/Http/Resources/KeywordResource.php @@ -15,7 +15,7 @@ class KeywordResource extends JsonResource public function toArray(Request $request): array { return [ - 'id' => $this->id, + 'id' => $this->key, 'name' => $this->name, ]; } diff --git a/app/Http/Resources/StoreMasterCommissionResource.php b/app/Http/Resources/StoreMasterCommissionResource.php new file mode 100644 index 0000000..57321e7 --- /dev/null +++ b/app/Http/Resources/StoreMasterCommissionResource.php @@ -0,0 +1,26 @@ + + */ + public function toArray(Request $request): array + { + return [ + 'id' => $this->id, + 'month' => $this->month, + 'commission' => $this->commission, + 'daily_expenses' => $this->daily_expenses, + 'employee_expenses' => $this->employee_expenses, + 'other_expenses' => $this->other_expenses, + ]; + } +} diff --git a/app/Http/Resources/StoreResource.php b/app/Http/Resources/StoreResource.php index a7e38aa..73adbdb 100644 --- a/app/Http/Resources/StoreResource.php +++ b/app/Http/Resources/StoreResource.php @@ -21,6 +21,7 @@ class StoreResource extends JsonResource 'lon' => $this->lon, 'lat' => $this->lat, 'address' => $this->address, + 'is_lottery_store' => $this->isLotteryStore(), ]; } } diff --git a/app/Models/Employee.php b/app/Models/Employee.php index d2f457a..623394d 100644 --- a/app/Models/Employee.php +++ b/app/Models/Employee.php @@ -13,7 +13,6 @@ use Illuminate\Database\Eloquent\Model; use Laravel\Sanctum\HasApiTokens; use Slowlyo\OwlAdmin\Models\AdminUser; use App\Enums\UserRole; -use Illuminate\Support\Str; /** * 员工 @@ -77,6 +76,14 @@ class Employee extends Model implements AuthenticatableContract return $this->hasMany(EmployeeSign::class, 'employee_id'); } + /** + * 店长佣金提成 + */ + public function storeMasterCommissions() + { + return $this->hasMany(StoreMasterCommission::class, 'store_master_id'); + } + // 管理的门店(店长) // public function masterStore() // { diff --git a/app/Models/Ledger.php b/app/Models/Ledger.php index e8afe95..cf538bf 100644 --- a/app/Models/Ledger.php +++ b/app/Models/Ledger.php @@ -19,6 +19,9 @@ class Ledger extends Model protected $attributes = [ 'new_customers' => 0, + 'ledger_amount' => null, + 'actual_commission' => null, + 'actual_income' => null, ]; protected $fillable = [ @@ -46,6 +49,14 @@ class Ledger extends Model return $this->hasMany(LedgerItem::class); } + /** + * 是否允许重新上报 + */ + public function allowReReport(): bool + { + return is_null($this->ledger_amount) && is_null($this->actual_commission) && is_null($this->actual_income); + } + protected function ledgerDifference(): Attribute { return Attribute::make( diff --git a/app/Models/Store.php b/app/Models/Store.php index 26c8e22..c7c2178 100644 --- a/app/Models/Store.php +++ b/app/Models/Store.php @@ -62,6 +62,19 @@ class Store extends Model return $this->hasMany(Employee::class, 'store_id'); } + public function ledgers() + { + return $this->hasMany(Ledger::class); + } + + /** + * 确认此门店是否是彩票店 + */ + public function isLotteryStore(): bool + { + return preg_match('/^store_category_lottery_/', $this->category_id); + } + protected function businessStatusText(): Attribute { return new Attribute( diff --git a/app/Models/StoreMasterCommission.php b/app/Models/StoreMasterCommission.php index 7e09580..c549580 100644 --- a/app/Models/StoreMasterCommission.php +++ b/app/Models/StoreMasterCommission.php @@ -2,9 +2,11 @@ namespace App\Models; +use App\Enums\CheckStatus; use App\Traits\HasCheckable; use App\Traits\HasDateTimeFormatter; use EloquentFilter\Filterable; +use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; @@ -23,6 +25,14 @@ class StoreMasterCommission extends Model 'other_expenses', ]; + /** + * 仅查询审核通过的店长佣金提成 + */ + public function scopeOnlyApproved(Builder $query): void + { + $query->whereRelation('workflow', 'check_status', '=', CheckStatus::Success); + } + public function store() { return $this->belongsTo(Store::class, 'store_id'); diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 393b32a..9b5fad0 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -3,6 +3,7 @@ namespace App\Providers; use Illuminate\Database\Eloquent\Relations\Relation; +use Illuminate\Http\Resources\Json\JsonResource; use Illuminate\Support\ServiceProvider; class AppServiceProvider extends ServiceProvider @@ -20,6 +21,8 @@ class AppServiceProvider extends ServiceProvider */ public function boot() { + JsonResource::withoutWrapping(); + $this->definePolymorphicTypes(); } diff --git a/database/seeders/AdminPermissionSeeder.php b/database/seeders/AdminPermissionSeeder.php index 061b39f..1839245 100644 --- a/database/seeders/AdminPermissionSeeder.php +++ b/database/seeders/AdminPermissionSeeder.php @@ -201,8 +201,6 @@ class AdminPermissionSeeder extends Seeder 'uri' => '/finance/ledgers', 'resource' => ['list', 'update', 'view'], 'children' => [ - 'update_actual_commission' => '编辑实际佣金', - 'update_actual_income' => '编辑实际收益', 'update_ledger_amount' => '编辑总账金额', ], ], diff --git a/routes/api.php b/routes/api.php index 72a95cd..91a7162 100644 --- a/routes/api.php +++ b/routes/api.php @@ -1,9 +1,12 @@ 'reimbursement'], function () { + Route::get('reimbursement-types', [ReimbursementTypeController::class, 'index']); + }); + // 举报投诉 Route::post('complaints', [ComplaintController::class, 'store']); // 意见箱