diff --git a/app/Http/Controllers/Business/FinanceLedger.php b/app/Http/Controllers/Business/FinanceLedger.php index 0c877320..a328b41f 100644 --- a/app/Http/Controllers/Business/FinanceLedger.php +++ b/app/Http/Controllers/Business/FinanceLedger.php @@ -3,6 +3,7 @@ namespace App\Http\Controllers\Business; use App\Utilities\FinanceLedgerAccountStatus; +use App\Utilities\FinanceLedgerAccountShouldKeep; use App\Utilities\FinanceLedgerAccountType; use App\Utilities\FinanceLedgerAccess; use Illuminate\Http\JsonResponse; @@ -176,6 +177,84 @@ class FinanceLedger extends Controller ], 200, ['Cache-Control' => 'private, no-store']); } + public function updateAccountPlan(Request $request): JsonResponse + { + abort_unless(FinanceLedgerAccess::canEditAccountStatus(user()), 403, '只有财务台账管理员可以修改公司规划。'); + + $data = $request->validate([ + 'account_short' => ['required', 'string', 'max:100'], + 'company_plan' => ['nullable', 'string', 'max:50'], + ]); + $accountShort = trim((string) $data['account_short']); + $plan = trim((string) ($data['company_plan'] ?? '')); + if ($accountShort === '') { + abort(422, '账户简称不能为空。'); + } + + $connection = DB::connection((string) config('upstream_mirrors.connection', 'business_mirror')); + abort_unless( + $connection->table('fin_accounts')->where('account_short', $accountShort)->exists(), + 422, + '账户简称不存在,请刷新账户总表后重试。', + ); + $allowedPlans = $connection->table('fin_accounts') + ->whereNotNull('company_plan') + ->where('company_plan', '<>', '') + ->distinct() + ->pluck('company_plan') + ->map(static fn (mixed $value): string => trim((string) $value)) + ->filter() + ->values() + ->all(); + abort_unless($plan === '' || in_array($plan, $allowedPlans, true), 422, '请选择现有公司规划。'); + + $connection->table('fin_accounts')->where('account_short', $accountShort)->update([ + 'company_plan' => $plan !== '' ? $plan : null, + 'updated_at' => now(), + ]); + + return response()->json([ + 'ok' => true, + 'account_short' => $accountShort, + 'company_plan' => $plan, + 'label' => $plan !== '' ? $plan : '未填写', + ], 200, ['Cache-Control' => 'private, no-store']); + } + + public function updateAccountKeep(Request $request): JsonResponse + { + abort_unless(FinanceLedgerAccess::canEditAccountStatus(user()), 403, '只有财务台账管理员可以修改账户是否保留。'); + + $data = $request->validate([ + 'account_short' => ['required', 'string', 'max:100'], + 'account_should_keep' => ['nullable', 'string', Rule::in(FinanceLedgerAccountShouldKeep::OPTIONS)], + ]); + $accountShort = trim((string) $data['account_short']); + $keep = FinanceLedgerAccountShouldKeep::normalize((string) ($data['account_should_keep'] ?? '')); + if ($accountShort === '') { + abort(422, '账户简称不能为空。'); + } + + $connection = DB::connection((string) config('upstream_mirrors.connection', 'business_mirror')); + abort_unless( + $connection->table('fin_accounts')->where('account_short', $accountShort)->exists(), + 422, + '账户简称不存在,请刷新账户总表后重试。', + ); + + $connection->table('fin_accounts')->where('account_short', $accountShort)->update([ + 'account_should_keep' => $keep !== '' ? $keep : null, + 'updated_at' => now(), + ]); + + return response()->json([ + 'ok' => true, + 'account_short' => $accountShort, + 'account_should_keep' => $keep, + 'label' => $keep !== '' ? $keep : '未填写', + ], 200, ['Cache-Control' => 'private, no-store']); + } + public function previewShopMapping(FinanceLedgerShopMappingBulk $bulk): JsonResponse { abort_unless(FinanceLedgerAccess::canEditMapping(user()), 403, '只有财务台账管理员可以批量修改账户映射。'); diff --git a/modules/EcommerceFinance/View/FinanceLedgerPageData.php b/modules/EcommerceFinance/View/FinanceLedgerPageData.php index 1d5a9951..f76310ef 100644 --- a/modules/EcommerceFinance/View/FinanceLedgerPageData.php +++ b/modules/EcommerceFinance/View/FinanceLedgerPageData.php @@ -59,6 +59,8 @@ class FinanceLedgerPageData 'ledgerAccountOptions' => $this->accountOptions(), 'ledgerAccountFilterOptions' => $this->accountFilterOptions(), 'ledgerAccountStatusOptions' => FinanceLedgerAccountStatus::OPTIONS, + 'ledgerAccountPlanOptions' => $this->accountPlanOptions(), + 'ledgerAccountKeepOptions' => FinanceLedgerAccountShouldKeep::OPTIONS, 'ledgerCanEditMapping' => FinanceLedgerAccess::canEditMapping(user()), 'ledgerCanEditAccountStatus' => FinanceLedgerAccess::canEditAccountStatus(user()), 'ledgerCanEditAccountType' => FinanceLedgerAccess::canEditAccountType(user()), @@ -376,6 +378,21 @@ class FinanceLedgerPageData ]; } + private function accountPlanOptions(): array + { + return $this->db()->table('fin_accounts') + ->whereNotNull('company_plan') + ->where('company_plan', '<>', '') + ->distinct() + ->orderBy('company_plan') + ->pluck('company_plan') + ->map(static fn (mixed $value): string => trim((string) $value)) + ->filter() + ->unique() + ->values() + ->all(); + } + private function db() { return DB::connection((string) config('upstream_mirrors.connection', 'business_mirror')); diff --git a/resources/views/business/pages/financial-ledger/ledger.blade.php b/resources/views/business/pages/financial-ledger/ledger.blade.php index df2880b3..ced7755e 100644 --- a/resources/views/business/pages/financial-ledger/ledger.blade.php +++ b/resources/views/business/pages/financial-ledger/ledger.blade.php @@ -9,6 +9,8 @@ $accountStatusOptions = $ledgerAccountStatusOptions ?? \App\Utilities\FinanceLedgerAccountStatus::OPTIONS; $accountFilterOptions = $ledgerAccountFilterOptions ?? ['regions' => [], 'types' => []]; $accountTypeOptions = $accountFilterOptions['types'] ?? []; + $accountPlanOptions = $ledgerAccountPlanOptions ?? []; + $accountKeepOptions = $ledgerAccountKeepOptions ?? \App\Utilities\FinanceLedgerAccountShouldKeep::OPTIONS; $activeAccountFilterCount = count(array_filter([ request('accounts_q'), request('accounts_short'), @@ -133,6 +135,8 @@ .ledger-inline-select { display:block; height:1.75rem; margin:0 auto; padding:0 .1rem; border:1px solid var(--vip-border, #D8D2C7); border-radius:.4rem; background:#fff; color:var(--vip-content-ink, #171717); font:inherit; font-size:.68rem; font-weight:600; text-align:center; text-align-last:center; cursor:pointer; } .ledger-status-select { width:4.15rem; min-width:4.15rem; } .ledger-type-select { width:5.2rem; min-width:5.2rem; } + .ledger-plan-select { width:6.5rem; min-width:6.5rem; } + .ledger-keep-select { width:4.8rem; min-width:4.8rem; } .ledger-inline-select:focus { border-color:var(--vip-content-accent-dark, #856632); outline:none; box-shadow:0 0 0 3px rgba(133,102,50,.16); } .ledger-inline-select[data-status="normal"] { border-color:rgba(83,121,62,.38); background:#f3f7ee; color:var(--vip-success, #53793E); } .ledger-inline-select[data-status="online_banking_abnormal"] { border-color:rgba(184,0,0,.34); background:#fff5f5; color:var(--vip-danger, #B80000); } @@ -247,7 +251,29 @@ @endif {{ $row->region }}{{ $row->account_no }}{{ $row->bank_name }}{{ $row->bank_city_branch }} - {{ $row->company_plan }}{{ $row->account_should_keep }}{{ $row->account_last4 }}{{ $row->single_limit }}{{ $row->daily_limit }}{{ $row->monthly_limit }}{{ $row->yearly_limit }}{{ $money($row->current_balance) }}{{ $money($row->blocked_amount) }}{{ $money($row->daily_available) }}{{ $row->clearing_days !== null ? number_format((float) $row->clearing_days, 2) : '' }}{{ $row->raise_target }}{{ $row->raise_status }}{{ $row->raise_actions }}{{ $row->done_actions }}{{ $row->next_action }}{{ $row->remark }}{{ $row->owner }} + + {{ $row->company_plan ?: '未填写' }} + @if ($canEditAccountStatus) + + @endif + + + {{ $row->account_should_keep ?: '未填写' }} + @if ($canEditAccountStatus) + + @endif + + {{ $row->account_last4 }}{{ $row->single_limit }}{{ $row->daily_limit }}{{ $row->monthly_limit }}{{ $row->yearly_limit }}{{ $money($row->current_balance) }}{{ $money($row->blocked_amount) }}{{ $money($row->daily_available) }}{{ $row->clearing_days !== null ? number_format((float) $row->clearing_days, 2) : '' }}{{ $row->raise_target }}{{ $row->raise_status }}{{ $row->raise_actions }}{{ $row->done_actions }}{{ $row->next_action }}{{ $row->remark }}{{ $row->owner }} @empty
没有匹配的账户数据。
@@ -787,6 +813,9 @@ display.textContent = payload.label || (value || '未填写'); if (field === 'status') display.dataset.status = value; } + if (field === 'account_should_keep') { + item.closest('td')?.setAttribute('data-account-should-keep', value); + } }); notify(successLabel + ':' + (payload.label || '')); } catch (error) { @@ -802,6 +831,8 @@ }; bindInlineAccountSelect('[data-account-status]', 'status', '账户状态已保存'); bindInlineAccountSelect('[data-account-type]', 'account_type', '账户类型已保存'); + bindInlineAccountSelect('[data-account-plan]', 'company_plan', '公司规划已保存'); + bindInlineAccountSelect('[data-account-keep]', 'account_should_keep', '账户是否保留已保存'); applyAccountEditMode(accountEditToggle?.checked === true); let accountPageRequest = null; @@ -864,6 +895,8 @@ } bindInlineAccountSelect('[data-account-status]', 'status', '账户状态已保存'); bindInlineAccountSelect('[data-account-type]', 'account_type', '账户类型已保存'); + bindInlineAccountSelect('[data-account-plan]', 'company_plan', '公司规划已保存'); + bindInlineAccountSelect('[data-account-keep]', 'account_should_keep', '账户是否保留已保存'); applyAccountEditMode(accountEditToggle?.checked === true); applyAccountColumnLayout(); bindAccountPagination(); diff --git a/routes/admin.php b/routes/admin.php index e21de198..34d6f88e 100644 --- a/routes/admin.php +++ b/routes/admin.php @@ -669,6 +669,14 @@ Route::post('workspace/financial-ledger/accounts/type', [\App\Http\Controllers\B ->middleware('business.page:financial-operations,ledger') ->name('business.financial-ledger.accounts.type'); +Route::post('workspace/financial-ledger/accounts/plan', [\App\Http\Controllers\Business\FinanceLedger::class, 'updateAccountPlan']) + ->middleware('business.page:financial-operations,ledger') + ->name('business.financial-ledger.accounts.plan'); + +Route::post('workspace/financial-ledger/accounts/keep', [\App\Http\Controllers\Business\FinanceLedger::class, 'updateAccountKeep']) + ->middleware('business.page:financial-operations,ledger') + ->name('business.financial-ledger.accounts.keep'); + Route::get('workspace/financial-ledger/accounts/template', [\App\Http\Controllers\Business\FinanceLedger::class, 'downloadAccountTemplate']) ->middleware('business.page:financial-operations,ledger') ->name('business.financial-ledger.accounts.template'); diff --git a/tests/Feature/Business/FinanceLedgerAccountStatusTest.php b/tests/Feature/Business/FinanceLedgerAccountStatusTest.php index 3111d229..99959389 100644 --- a/tests/Feature/Business/FinanceLedgerAccountStatusTest.php +++ b/tests/Feature/Business/FinanceLedgerAccountStatusTest.php @@ -49,6 +49,20 @@ class FinanceLedgerAccountStatusTest extends FeatureTestCase ]); } + private function planUrl(): string + { + return route('business.financial-ledger.accounts.plan', [ + 'company_id' => $this->company->id, + ]); + } + + private function keepUrl(): string + { + return route('business.financial-ledger.accounts.keep', [ + 'company_id' => $this->company->id, + ]); + } + public function test_account_table_renders_status_selector_with_four_options(): void { $this->loginAs()->get($this->pageUrl()) @@ -64,6 +78,10 @@ class FinanceLedgerAccountStatusTest extends FeatureTestCase ->assertSee('name="accounts_region"', false) ->assertSee('name="accounts_type"', false) ->assertSee('data-account-type', false) + ->assertSee('data-account-plan', false) + ->assertSee('data-account-keep', false) + ->assertSee('data-account-plan-display', false) + ->assertSee('data-account-keep-display', false) ->assertSee('ledger-type-select', false) ->assertSee('data-mapping-bulk', false) ->assertSee('导出录入模板') @@ -303,6 +321,41 @@ class FinanceLedgerAccountStatusTest extends FeatureTestCase ->assertSee('value="normal" selected', false); } + public function test_company_plan_and_account_keep_can_be_updated(): void + { + DB::connection('business_mirror')->table('fin_accounts')->insert([ + 'company_name' => '规划选项公司', + 'account_short' => '规划选项账户', + 'company_plan' => '待注销', + ]); + + $this->loginAs()->postJson($this->planUrl(), [ + 'account_short' => '测试账户', + 'company_plan' => '待注销', + ])->assertOk()->assertJsonPath('label', '待注销'); + + $this->loginAs()->postJson($this->keepUrl(), [ + 'account_short' => '测试账户', + 'account_should_keep' => '否', + ])->assertOk()->assertJsonPath('label', '否'); + + $this->assertDatabaseHas('fin_accounts', [ + 'account_short' => '测试账户', + 'company_plan' => '待注销', + 'account_should_keep' => '否', + ], 'business_mirror'); + + $this->withExceptionHandling()->loginAs()->postJson($this->planUrl(), [ + 'account_short' => '测试账户', + 'company_plan' => '不存在的规划', + ])->assertUnprocessable()->assertJsonPath('message', '请选择现有公司规划。'); + + $this->withExceptionHandling()->loginAs()->postJson($this->keepUrl(), [ + 'account_short' => '测试账户', + 'account_should_keep' => '可能', + ])->assertUnprocessable()->assertJsonValidationErrors('account_should_keep'); + } + public function test_invalid_status_or_account_is_rejected(): void { $this->withExceptionHandling()->loginAs()->postJson($this->statusUrl(), [ @@ -344,6 +397,14 @@ class FinanceLedgerAccountStatusTest extends FeatureTestCase 'account_short' => '测试账户', 'status' => 'limited', ])->assertForbidden(); + $this->withExceptionHandling()->loginAs($viewer)->postJson($this->planUrl(), [ + 'account_short' => '测试账户', + 'company_plan' => '保留', + ])->assertForbidden(); + $this->withExceptionHandling()->loginAs($viewer)->postJson($this->keepUrl(), [ + 'account_short' => '测试账户', + 'account_should_keep' => '否', + ])->assertForbidden(); $this->assertDatabaseCount('fin_account_statuses', 0, 'business_mirror'); }