财务台账:公司规划与保留状态增加下拉编辑 (#431)

This commit is contained in:
shanshi 2026-10-21 14:37:41 +08:00
parent 36af0e211a
commit eaeae97752
5 changed files with 199 additions and 1 deletions

View File

@ -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, '只有财务台账管理员可以批量修改账户映射。');

View File

@ -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'));

View File

@ -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
</td>
<td>{{ $row->region }}</td><td>{{ $row->account_no }}</td><td>{{ $row->bank_name }}</td><td>{{ $row->bank_city_branch }}</td>
<td>{{ $row->company_plan }}</td><td class="ledger-keep-column" data-account-short="{{ $row->account_short }}" data-account-should-keep="{{ $row->account_should_keep }}">{{ $row->account_should_keep }}</td><td>{{ $row->account_last4 }}</td><td>{{ $row->single_limit }}</td><td>{{ $row->daily_limit }}</td><td>{{ $row->monthly_limit }}</td><td>{{ $row->yearly_limit }}</td><td class="num">{{ $money($row->current_balance) }}</td><td class="num">{{ $money($row->blocked_amount) }}</td><td class="num">{{ $money($row->daily_available) }}</td><td class="num">{{ $row->clearing_days !== null ? number_format((float) $row->clearing_days, 2) : '' }}</td><td>{{ $row->raise_target }}</td><td>{{ $row->raise_status }}</td><td>{{ $row->raise_actions }}</td><td>{{ $row->done_actions }}</td><td>{{ $row->next_action }}</td><td>{{ $row->remark }}</td><td>{{ $row->owner }}</td>
<td>
<span class="ledger-account-readonly" data-account-edit-display data-account-plan-display>{{ $row->company_plan ?: '未填写' }}</span>
@if ($canEditAccountStatus)
<select class="ledger-inline-select ledger-plan-select ledger-account-editor" data-account-editor data-account-plan data-account-short="{{ $row->account_short }}" data-url="{{ route('business.financial-ledger.accounts.plan') }}" aria-label="账户 {{ $row->account_short }} 公司规划" disabled>
<option value="" @selected(($row->company_plan ?? '') === '')>未填写</option>
@foreach ($accountPlanOptions as $planOption)
<option value="{{ $planOption }}" @selected($row->company_plan === $planOption)>{{ $planOption }}</option>
@endforeach
</select>
@endif
</td>
<td class="ledger-keep-column" data-account-short="{{ $row->account_short }}" data-account-should-keep="{{ $row->account_should_keep }}">
<span class="ledger-account-readonly" data-account-edit-display data-account-keep-display>{{ $row->account_should_keep ?: '未填写' }}</span>
@if ($canEditAccountStatus)
<select class="ledger-inline-select ledger-keep-select ledger-account-editor" data-account-editor data-account-keep data-account-short="{{ $row->account_short }}" data-url="{{ route('business.financial-ledger.accounts.keep') }}" aria-label="账户 {{ $row->account_short }} 是否保留" disabled>
<option value="" @selected(($row->account_should_keep ?? '') === '')>未填写</option>
@foreach ($accountKeepOptions as $keepOption)
<option value="{{ $keepOption }}" @selected($row->account_should_keep === $keepOption)>{{ $keepOption }}</option>
@endforeach
</select>
@endif
</td>
<td>{{ $row->account_last4 }}</td><td>{{ $row->single_limit }}</td><td>{{ $row->daily_limit }}</td><td>{{ $row->monthly_limit }}</td><td>{{ $row->yearly_limit }}</td><td class="num">{{ $money($row->current_balance) }}</td><td class="num">{{ $money($row->blocked_amount) }}</td><td class="num">{{ $money($row->daily_available) }}</td><td class="num">{{ $row->clearing_days !== null ? number_format((float) $row->clearing_days, 2) : '' }}</td><td>{{ $row->raise_target }}</td><td>{{ $row->raise_status }}</td><td>{{ $row->raise_actions }}</td><td>{{ $row->done_actions }}</td><td>{{ $row->next_action }}</td><td>{{ $row->remark }}</td><td>{{ $row->owner }}</td>
</tr>
@empty
<tr><td colspan="26"><div class="ledger-empty">没有匹配的账户数据。</div></td></tr>
@ -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();

View File

@ -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');

View File

@ -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');
}