diff --git a/app/Http/Controllers/Business/FinanceLedger.php b/app/Http/Controllers/Business/FinanceLedger.php index a328b41f..d8a1f773 100644 --- a/app/Http/Controllers/Business/FinanceLedger.php +++ b/app/Http/Controllers/Business/FinanceLedger.php @@ -9,6 +9,7 @@ use App\Utilities\FinanceLedgerAccess; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; use Illuminate\Routing\Controller; +use Illuminate\Support\Carbon; use Illuminate\Support\Facades\DB; use Illuminate\Validation\Rule; use Modules\EcommerceFinance\Services\FinanceLedgerAccountTemplate; @@ -24,6 +25,252 @@ class FinanceLedger extends Controller return $template->download(); } + public function unavailableBalanceTasks(Request $request) + { + $connection = DB::connection((string) config('upstream_mirrors.connection', 'business_mirror')); + $abnormalStatuses = array_values(array_diff( + array_keys(FinanceLedgerAccountStatus::OPTIONS), + [FinanceLedgerAccountStatus::NORMAL], + )); + $handlingMethods = [ + 'online' => '线上', + 'offline' => '线下', + ]; + $completionStatuses = [ + 'completed' => '已完成', + 'in_progress' => '进行中', + 'not_started' => '待开始', + 'incomplete' => '未完成', + ]; + $filters = [ + 'pending_short' => trim((string) $request->query('pending_short', '')), + 'pending_type' => trim((string) $request->query('pending_type', '')), + 'pending_status' => trim((string) $request->query('pending_status', '')), + 'pending_reason' => trim((string) $request->query('pending_reason', '')), + 'pending_method' => trim((string) $request->query('pending_method', '')), + 'pending_owner' => trim((string) $request->query('pending_owner', '')), + ]; + + $query = $connection->table('fin_accounts as accounts') + ->join('fin_account_statuses as account_status', 'account_status.account_short', '=', 'accounts.account_short') + ->leftJoin('fin_account_type_overrides as type_override', 'type_override.account_short', '=', 'accounts.account_short') + ->leftJoin('fin_account_todos as account_todo', 'account_todo.account_short', '=', 'accounts.account_short') + ->whereIn('account_status.status', $abnormalStatuses); + + if ($filters['pending_short'] !== '') { + $query->where('accounts.account_short', 'like', '%' . $filters['pending_short'] . '%'); + } + if ($filters['pending_type'] !== '') { + $query->where(function ($effectiveType) use ($filters): void { + $effectiveType->where(function ($baseType) use ($filters): void { + $baseType->whereNull('type_override.id'); + if ($filters['pending_type'] === '__empty__') { + $baseType->where(function ($blank): void { + $blank->whereNull('accounts.account_type')->orWhere('accounts.account_type', ''); + }); + } else { + $baseType->where('accounts.account_type', $filters['pending_type']); + } + })->orWhere(function ($overrideType) use ($filters): void { + $overrideType->whereNotNull('type_override.id'); + if ($filters['pending_type'] === '__empty__') { + $overrideType->where(function ($blank): void { + $blank->whereNull('type_override.account_type')->orWhere('type_override.account_type', ''); + }); + } else { + $overrideType->where('type_override.account_type', $filters['pending_type']); + } + }); + }); + } + if ($filters['pending_status'] !== '' && array_key_exists($filters['pending_status'], FinanceLedgerAccountStatus::OPTIONS)) { + $query->where('account_status.status', $filters['pending_status']); + } + if ($filters['pending_reason'] !== '') { + $query->where('account_todo.abnormal_reason', 'like', '%' . $filters['pending_reason'] . '%'); + } + if ($filters['pending_method'] !== '' && array_key_exists($filters['pending_method'], $handlingMethods)) { + $query->where('account_todo.handling_method', $filters['pending_method']); + } + if ($filters['pending_owner'] !== '') { + $query->where('account_todo.owner_name', 'like', '%' . $filters['pending_owner'] . '%'); + } + + $summaryQuery = clone $query; + $summary = [ + 'count' => (clone $query)->count(), + 'balance' => round((float) (clone $summaryQuery)->sum('accounts.current_balance'), 2), + 'blocked' => round((float) (clone $summaryQuery)->sum('accounts.blocked_amount'), 2), + 'daily_available' => round((float) (clone $summaryQuery)->sum('accounts.daily_available'), 2), + ]; + + $sortField = (string) $request->query('pending_sort', ''); + $sortField = in_array($sortField, ['started_at', 'completed_at'], true) ? $sortField : ''; + $sortDirection = strtolower((string) $request->query('pending_direction', 'asc')); + $sortDirection = $sortDirection === 'desc' ? 'desc' : 'asc'; + + $query->select([ + 'accounts.*', + 'account_status.status as effective_account_status', + 'type_override.id as type_override_id', + 'type_override.account_type as effective_account_type', + 'account_todo.abnormal_reason as todo_abnormal_reason', + 'account_todo.handling_method as todo_handling_method', + 'account_todo.owner_name as todo_owner_name', + 'account_todo.started_at as todo_started_at', + 'account_todo.completed_at as todo_completed_at', + 'account_todo.current_progress as todo_current_progress', + 'account_todo.completion_status as todo_completion_status', + ]); + + if ($sortField !== '') { + $query->orderBy('account_todo.' . $sortField, $sortDirection); + } else { + $query->orderByDesc('accounts.blocked_amount'); + } + $rows = $query + ->orderBy('accounts.company_name') + ->orderBy('accounts.account_short') + ->paginate(50) + ->appends($filters) + ->appends(array_filter([ + 'pending_sort' => $sortField, + 'pending_direction' => $sortField !== '' ? $sortDirection : '', + ])) + ->withQueryString(); + + $rows->getCollection()->transform(static function ($row) { + $row->account_status = FinanceLedgerAccountStatus::normalize($row->effective_account_status ?? null); + if ($row->type_override_id !== null) { + $row->account_type = (string) ($row->effective_account_type ?? ''); + } + $row->todo_completion_status = $row->todo_completion_status ?: 'not_started'; + + return $row; + }); + + $typeOptions = FinanceLedgerAccountType::options( + $connection->table('fin_accounts') + ->whereNotNull('account_type')->where('account_type', '<>', '') + ->distinct()->orderBy('account_type')->pluck('account_type')->all(), + $connection->table('fin_account_type_overrides') + ->whereNotNull('account_type')->where('account_type', '<>', '') + ->distinct()->orderBy('account_type')->pluck('account_type')->all(), + ); + $reasonOptions = $connection->table('fin_account_todos') + ->whereNotNull('abnormal_reason')->where('abnormal_reason', '<>', '') + ->distinct()->orderBy('abnormal_reason')->pluck('abnormal_reason')->all(); + $ownerOptions = $connection->table('fin_account_todos') + ->whereNotNull('owner_name')->where('owner_name', '<>', '') + ->distinct()->orderBy('owner_name')->pluck('owner_name')->all(); + + return view('business.pages.financial-ledger.unavailable-balance-tasks', [ + 'rows' => $rows, + 'summary' => $summary, + 'filters' => $filters, + 'sortField' => $sortField, + 'sortDirection' => $sortDirection, + 'backUrl' => route('business.pages.show', [ + 'section' => 'financial-operations', + 'page' => 'ledger', + ]) . '?tab=accounts', + 'canEdit' => FinanceLedgerAccess::canEditAccountStatus(user()), + 'handlingMethods' => $handlingMethods, + 'completionStatuses' => $completionStatuses, + 'accountStatusOptions' => array_intersect_key( + FinanceLedgerAccountStatus::OPTIONS, + array_flip($abnormalStatuses), + ), + 'accountTypeOptions' => $typeOptions, + 'reasonOptions' => $reasonOptions, + 'ownerOptions' => $ownerOptions, + ]); + } + + public function updateAccountTodo(Request $request) + { + abort_unless(FinanceLedgerAccess::canEditAccountStatus(user()), 403, '只有财务台账管理员可以修改待办事项。'); + + $data = $request->validate([ + 'account_short' => ['required', 'string', 'max:100'], + 'abnormal_reason' => ['nullable', 'string', 'max:1000'], + 'handling_method' => ['nullable', 'string', Rule::in(['online', 'offline'])], + 'owner_name' => ['nullable', 'string', 'max:100'], + 'started_at' => ['nullable', 'date'], + 'completed_at' => ['nullable', 'date'], + 'current_progress' => ['nullable', 'string', 'max:100'], + 'completion_status' => ['nullable', 'string', Rule::in(['completed', 'in_progress', 'not_started', 'incomplete'])], + ]); + + $accountShort = trim((string) $data['account_short']); + $connection = DB::connection((string) config('upstream_mirrors.connection', 'business_mirror')); + abort_unless( + $connection->table('fin_accounts')->where('account_short', $accountShort)->exists(), + 422, + '账户简称不存在,请刷新待办清单后重试。', + ); + + $abnormalStatuses = array_values(array_diff( + array_keys(FinanceLedgerAccountStatus::OPTIONS), + [FinanceLedgerAccountStatus::NORMAL], + )); + $status = $connection->table('fin_account_statuses') + ->where('account_short', $accountShort) + ->value('status'); + abort_unless( + $status !== null && in_array(FinanceLedgerAccountStatus::normalize((string) $status), $abnormalStatuses, true), + 422, + '账户当前已不是异常状态,无需继续填写待办。', + ); + + $now = now(); + $completionStatus = (string) ($data['completion_status'] ?? 'not_started'); + $values = [ + 'abnormal_reason' => $data['abnormal_reason'] ?? null, + 'handling_method' => $data['handling_method'] ?? null, + 'owner_name' => $data['owner_name'] ?? null, + 'started_at' => filled($data['started_at'] ?? null) + ? Carbon::parse($data['started_at'])->format('Y-m-d H:i:s') + : null, + 'completed_at' => filled($data['completed_at'] ?? null) + ? Carbon::parse($data['completed_at'])->format('Y-m-d H:i:s') + : null, + 'current_progress' => $data['current_progress'] ?? null, + 'completion_status' => $completionStatus, + 'updated_by' => (string) user()->name, + 'updated_at' => $now, + ]; + $connection->transaction(function () use ($connection, $accountShort, $values, $now, $completionStatus): void { + $todos = $connection->table('fin_account_todos'); + $existing = $todos->where('account_short', $accountShort)->exists(); + if ($existing) { + $todos->where('account_short', $accountShort)->update($values); + } else { + $todos->insert($values + [ + 'account_short' => $accountShort, + 'created_at' => $now, + ]); + } + + if ($completionStatus === 'completed') { + $connection->table('fin_account_statuses') + ->where('account_short', $accountShort) + ->update([ + 'status' => FinanceLedgerAccountStatus::NORMAL, + 'updated_by' => (string) user()->name, + 'updated_at' => $now, + ]); + } + }); + + return redirect()->back()->with( + 'success', + $completionStatus === 'completed' + ? '待办已完成,账户状态已自动设为正常' + : '待办事项已保存', + ); + } + public function updateMapping(Request $request, int $id, FinanceLedgerCalculator $calculator): JsonResponse { abort_unless(FinanceLedgerAccess::canEditMapping(user()), 403, '只有财务台账管理员可以修改账户映射。'); diff --git a/database/migrations/2026_10_21_150000_create_finance_ledger_account_todos.php b/database/migrations/2026_10_21_150000_create_finance_ledger_account_todos.php new file mode 100644 index 00000000..62f548b7 --- /dev/null +++ b/database/migrations/2026_10_21_150000_create_finance_ledger_account_todos.php @@ -0,0 +1,39 @@ +connection()); + if (! $schema->hasTable('fin_accounts') || $schema->hasTable('fin_account_todos')) { + return; + } + + $schema->create('fin_account_todos', function (Blueprint $table): void { + $table->bigIncrements('id'); + $table->string('account_short', 100)->unique('fin_account_todos_account_unique'); + $table->text('abnormal_reason')->nullable(); + $table->string('handling_method', 20)->nullable(); + $table->string('owner_name', 100)->nullable(); + $table->dateTime('started_at')->nullable(); + $table->dateTime('completed_at')->nullable(); + $table->string('current_progress', 100)->nullable(); + $table->string('updated_by', 100)->nullable(); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::connection($this->connection())->dropIfExists('fin_account_todos'); + } +}; diff --git a/database/migrations/2026_10_21_160000_add_completion_status_to_finance_ledger_account_todos.php b/database/migrations/2026_10_21_160000_add_completion_status_to_finance_ledger_account_todos.php new file mode 100644 index 00000000..18ce9b16 --- /dev/null +++ b/database/migrations/2026_10_21_160000_add_completion_status_to_finance_ledger_account_todos.php @@ -0,0 +1,39 @@ +connection()); + if (! $schema->hasTable('fin_account_todos') || $schema->hasColumn('fin_account_todos', 'completion_status')) { + return; + } + + $schema->table('fin_account_todos', function (Blueprint $table): void { + $table->string('completion_status', 20)->default('not_started')->after('current_progress'); + $table->index(['completion_status'], 'fin_account_todos_completion_status_index'); + }); + } + + public function down(): void + { + $schema = Schema::connection($this->connection()); + if (! $schema->hasTable('fin_account_todos') || ! $schema->hasColumn('fin_account_todos', 'completion_status')) { + return; + } + + $schema->table('fin_account_todos', function (Blueprint $table): void { + $table->dropIndex('fin_account_todos_completion_status_index'); + $table->dropColumn('completion_status'); + }); + } +}; diff --git a/resources/views/business/pages/financial-ledger/ledger.blade.php b/resources/views/business/pages/financial-ledger/ledger.blade.php index 45b55dec..01b9c906 100644 --- a/resources/views/business/pages/financial-ledger/ledger.blade.php +++ b/resources/views/business/pages/financial-ledger/ledger.blade.php @@ -84,10 +84,15 @@ .ledger-mapping-bulk-actions { display:flex; flex-wrap:wrap; gap:.45rem; } .ledger-mapping-bulk .ledger-btn { height:1.95rem; } .ledger-download-status { flex-basis:100%; color:var(--vip-success, #53793E); font-size:.68rem; font-weight:600; line-height:1.4; text-align:right; } - .ledger-summary { display:flex; flex-wrap:wrap; gap:.6rem 1.5rem; padding:.7rem 1rem; margin-bottom:.85rem; border:1px solid var(--vip-border, #D8D2C7); border-radius:.65rem; background:var(--vip-content-accent-soft, #F7F0DE); } - .ledger-summary__item { display:flex; flex-direction:column; gap:.12rem; min-width:7rem; } - .ledger-summary__item span { color:var(--vip-content-muted, #595959); font-size:.68rem; } - .ledger-summary__item b { color:var(--vip-content-ink, #171717); font-size:.92rem; font-variant-numeric:tabular-nums; } + .ledger-summary { display:grid; grid-template-columns:repeat(4, minmax(0, 1fr)); gap:.65rem; margin-bottom:.85rem; } + .ledger-summary__item { display:flex; flex-direction:column; justify-content:center; gap:.2rem; min-width:0; min-height:3.4rem; padding:.6rem .8rem; border:1px solid var(--vip-border, #D8D2C7); border-radius:.55rem; background:var(--vip-content-accent-soft, #F7F0DE); box-shadow:0 1px 2px rgba(16,24,40,.04); } + .ledger-summary__item--available { background:#f3f7ee; } + .ledger-summary__item--blocked { background:#fff5f5; } + .ledger-summary__item--link { color:inherit; text-decoration:none; transition:border-color .15s ease, box-shadow .15s ease; } + .ledger-summary__item--link:hover { border-color:rgba(184,0,0,.42); box-shadow:0 2px 6px rgba(184,0,0,.08); } + .ledger-summary__item--link:focus-visible { outline:2px solid var(--vip-danger, #B80000); outline-offset:2px; } + .ledger-summary__item span { overflow:hidden; color:var(--vip-content-muted, #595959); font-size:.68rem; line-height:1.35; text-overflow:ellipsis; white-space:nowrap; } + .ledger-summary__item b { overflow:hidden; color:var(--vip-content-ink, #171717); font-size:.92rem; font-variant-numeric:tabular-nums; line-height:1.35; text-overflow:ellipsis; white-space:nowrap; } .ledger-note { padding:.65rem .9rem; margin-bottom:.85rem; border:1px solid var(--vip-border, #D8D2C7); border-radius:.55rem; background:#fff; color:var(--vip-content-muted, #595959); font-size:.72rem; line-height:1.6; } .ledger-table-wrap { overflow:auto; border:1px solid var(--vip-border, #D8D2C7); border-radius:.65rem; background:#fff; } .ledger-table { width:100%; min-width:82rem; border-collapse:separate; border-spacing:0; font-size:.72rem; } @@ -165,7 +170,8 @@ .ledger-toast.is-error { border-color:var(--vip-danger, #B80000); color:var(--vip-danger, #B80000); } .ledger-source-tag { display:inline-flex; align-items:center; padding:.1rem .4rem; border-radius:.25rem; background:#f4f1eb; color:var(--vip-content-muted, #595959); font-size:.68rem; } .ledger-mono { font-variant-numeric:tabular-nums; } - @media (max-width: 900px) { .ledger-hero { align-items:flex-start; flex-direction:column; } .ledger-field { min-width:calc(50% - .5rem); } } + @media (max-width: 900px) { .ledger-hero { align-items:flex-start; flex-direction:column; } .ledger-field { min-width:calc(50% - .5rem); } .ledger-summary { grid-template-columns:repeat(2, minmax(0, 1fr)); } } + @media (max-width: 560px) { .ledger-summary { grid-template-columns:1fr; } }
@@ -224,8 +230,8 @@
账户数{{ number_format((int) ($ledgerAccountSummary['count'] ?? 0)) }}
-
可用金额合计{{ $money($ledgerAccountSummary['balance'] ?? 0) }}
-
不可用金额合计{{ $money($ledgerAccountSummary['blocked'] ?? 0) }}
+
可用金额合计{{ $money($ledgerAccountSummary['balance'] ?? 0) }}
+ 不可用金额合计{{ $money($ledgerAccountSummary['blocked'] ?? 0) }}
每日可出金额口径可用金额 − 不可用金额
diff --git a/resources/views/business/pages/financial-ledger/unavailable-balance-tasks.blade.php b/resources/views/business/pages/financial-ledger/unavailable-balance-tasks.blade.php new file mode 100644 index 00000000..37573f02 --- /dev/null +++ b/resources/views/business/pages/financial-ledger/unavailable-balance-tasks.blade.php @@ -0,0 +1,525 @@ +@php + $money = static fn ($value): string => $value === null || $value === '' ? '' : number_format((float) $value, 2); +@endphp + + + 账户异常待办 + + + + +
+
+
+

账户异常待办

+

账户状态为“网银异常”“限额”或“工商注销”时自动进入本清单;不与其他页面的待办事项合并。

+
+ + + 返回账户总表 + +
+ +
+
待处理账户{{ number_format((int) ($summary['count'] ?? 0)) }}
+
不可用金额合计{{ $money($summary['blocked'] ?? 0) }}
+
可用金额合计{{ $money($summary['balance'] ?? 0) }}
+
每日可出金额合计{{ $money($summary['daily_available'] ?? 0) }}
+
+ + @if (session('success')) +
{{ session('success') }}
+ @endif + @if ($errors->any()) + + @endif + +
+ @if ($sortField !== '') + + + @endif +
+ + + + + + +
+
+ + + + 重置 + +
+
+ + @php + $sortQuery = request()->query(); + unset($sortQuery['pending_page']); + $sortUrl = static fn (string $field): string => route( + 'business.financial-ledger.unavailable-balances', + array_merge($sortQuery, [ + 'pending_sort' => $field, + 'pending_direction' => $sortField === $field && $sortDirection === 'asc' ? 'desc' : 'asc', + ]), + ); + @endphp + +

拖动表头右侧边界可调整列宽,双击边界恢复默认

+
+ + + + + + + + + + + + + + + + + + + + + @forelse ($rows as $row) + + + + + + + + + + + + + + + + + @empty + + @endforelse + +
公司名称账户简称账户类型账户状态账号开户行开户城市/网点异常原因办理方式责任人员 + + 开始时间 + + + + + 完成时间 + + + 当前进度是否完成
+
+ @csrf + +
+ {{ $row->company_name ?: '未填写' }} +
{{ $row->account_short ?: '未填写' }}{{ $row->account_type ?: '未填写' }}{{ \App\Utilities\FinanceLedgerAccountStatus::label($row->account_status) }}{{ $row->account_no ?: '未填写' }}{{ $row->bank_name ?: '未填写' }}{{ $row->bank_city_branch ?: '未填写' }} + + +
+ + @if ($canEdit) + + @endif +
+
+ +
当前没有账户异常待办
+
+ + @if ($rows->total() > 0) +
+ 共 {{ number_format($rows->total()) }} 条 · 每页 {{ number_format($rows->perPage()) }} 条 +
+ + +
+
+ @endif +
+ + + + +
+
diff --git a/routes/admin.php b/routes/admin.php index 34d6f88e..cc3013ff 100644 --- a/routes/admin.php +++ b/routes/admin.php @@ -697,6 +697,14 @@ Route::post('workspace/financial-ledger/mapping/payable/apply', [\App\Http\Contr ->middleware('business.page:financial-operations,ledger') ->name('business.financial-ledger.mapping.payable.apply'); +Route::get('workspace/financial-operations/ledger/unavailable-balances', [\App\Http\Controllers\Business\FinanceLedger::class, 'unavailableBalanceTasks']) + ->middleware('business.page:financial-operations,ledger') + ->name('business.financial-ledger.unavailable-balances'); + +Route::post('workspace/financial-ledger/accounts/todo', [\App\Http\Controllers\Business\FinanceLedger::class, 'updateAccountTodo']) + ->middleware('business.page:financial-operations,ledger') + ->name('business.financial-ledger.accounts.todo'); + Route::get('workspace/financial-ledger/{page?}/{tab?}', function (Request $request, ?string $page = null, ?string $tab = null) { abort_unless($page === null || $page === 'ledger', 404); diff --git a/tests/Feature/Business/FinanceLedgerAccountStatusTest.php b/tests/Feature/Business/FinanceLedgerAccountStatusTest.php index 39d2598a..54d8caea 100644 --- a/tests/Feature/Business/FinanceLedgerAccountStatusTest.php +++ b/tests/Feature/Business/FinanceLedgerAccountStatusTest.php @@ -35,6 +35,20 @@ class FinanceLedgerAccountStatusTest extends FeatureTestCase ]); } + private function unavailableBalancesUrl(): string + { + return route('business.financial-ledger.unavailable-balances', [ + 'company_id' => $this->company->id, + ]); + } + + private function accountTodoUrl(): string + { + return route('business.financial-ledger.accounts.todo', [ + 'company_id' => $this->company->id, + ]); + } + private function statusUrl(): string { return route('business.financial-ledger.accounts.status', [ @@ -107,12 +121,202 @@ class FinanceLedgerAccountStatusTest extends FeatureTestCase ->assertSee('恢复默认') ->assertSee('可用金额合计') ->assertSee('不可用金额合计') + ->assertSee('href="' . $this->unavailableBalancesUrl() . '"', false) + ->assertSee('查看账户异常待办') ->assertSee('可用金额 − 不可用金额') ->assertSee('未填写') ->assertDontSee('data-account-filter-state', false) ->assertSee('value="normal" selected', false); } + public function test_unavailable_balance_tasks_page_is_separate_and_only_lists_blocked_accounts(): void + { + $connection = DB::connection('business_mirror'); + $accounts = [ + [ + 'company_name' => '待办测试公司', + 'account_short' => '待办测试账户', + 'account_no' => '6222000000000099', + 'account_type' => '公户', + 'bank_name' => '待办测试银行', + 'bank_city_branch' => '上海 / 待办测试支行', + 'current_balance' => 100, + 'blocked_amount' => 25.5, + 'daily_available' => 74.5, + 'owner' => '待办负责人', + 'next_action' => '核实冻结原因', + 'remark' => '独立待办备注', + ], + [ + 'company_name' => '零金额测试公司', + 'account_short' => '零金额测试账户', + 'current_balance' => 100, + 'blocked_amount' => 0, + 'daily_available' => 100, + ], + [ + 'company_name' => '正常状态测试公司', + 'account_short' => '正常状态测试账户', + 'current_balance' => 100, + 'blocked_amount' => 50, + 'daily_available' => 50, + ], + [ + 'company_name' => '待办测试公司二', + 'account_short' => '待办测试账户二', + 'account_no' => '6222000000000100', + 'account_type' => '个人卡', + 'bank_name' => '待办测试银行二', + 'bank_city_branch' => '杭州 / 待办测试支行', + 'current_balance' => 100, + 'blocked_amount' => 10, + 'daily_available' => 90, + ], + ]; + foreach ($accounts as $account) { + $connection->table('fin_accounts')->insert($account); + } + $connection->table('fin_account_statuses')->insert([ + [ + 'account_short' => '待办测试账户', + 'status' => 'limited', + 'updated_by' => 'Test User', + ], + [ + 'account_short' => '正常状态测试账户', + 'status' => 'normal', + 'updated_by' => 'Test User', + ], + [ + 'account_short' => '待办测试账户二', + 'status' => 'online_banking_abnormal', + 'updated_by' => 'Test User', + ], + ]); + $connection->table('fin_account_type_overrides')->insert([ + 'account_short' => '待办测试账户', + 'account_type' => '网商银行', + 'updated_by' => 'Test User', + ]); + + $this->loginAs()->get($this->unavailableBalancesUrl()) + ->assertOk() + ->assertSee('账户异常待办') + ->assertSee('不与其他页面的待办事项合并') + ->assertSee('待办测试公司') + ->assertSee('data-pending-account="待办测试账户"', false) + ->assertSee('不可用金额合计') + ->assertSee('35.50') + ->assertSee('限额') + ->assertSee('网商银行') + ->assertSee('6222000000000099') + ->assertSee('待办测试银行') + ->assertSee('上海 / 待办测试支行') + ->assertSee('异常原因') + ->assertSee('办理方式') + ->assertSee('当前进度') + ->assertSee('是否完成') + ->assertSee('name="pending_short"', false) + ->assertSee('name="pending_type"', false) + ->assertSee('name="pending_status"', false) + ->assertSee('name="pending_reason"', false) + ->assertSee('name="pending_method"', false) + ->assertSee('name="pending_owner"', false) + ->assertSee('按开始时间排序') + ->assertSee('按完成时间排序') + ->assertDontSee('name="pending_q"', false) + ->assertSee('data-pending-tbody', false) + ->assertSee('data-pending-pagination', false) + ->assertSee('data-pending-table', false) + ->assertSee('拖动表头右侧边界可调整列宽') + ->assertSee('ledger-page-box', false) + ->assertSee('1 / 1', false) + ->assertSee('data-pending-account="待办测试账户二"', false) + ->assertDontSee('data-pending-account="正常状态测试账户"', false) + ->assertDontSee('data-pending-account="零金额测试账户"', false); + + $this->loginAs()->post($this->accountTodoUrl(), [ + 'account_short' => '待办测试账户', + 'abnormal_reason' => '账户冻结', + 'handling_method' => 'online', + 'owner_name' => '待办负责人', + 'started_at' => '2026-10-21T09:00', + 'completed_at' => '2026-10-21T11:30', + 'current_progress' => '处理中', + 'completion_status' => 'in_progress', + ])->assertRedirect(); + + $this->assertDatabaseHas('fin_account_todos', [ + 'account_short' => '待办测试账户', + 'abnormal_reason' => '账户冻结', + 'handling_method' => 'online', + 'owner_name' => '待办负责人', + 'current_progress' => '处理中', + 'completion_status' => 'in_progress', + 'updated_by' => 'Test User', + ], 'business_mirror'); + + $connection->table('fin_account_todos')->insert([ + 'account_short' => '待办测试账户二', + 'abnormal_reason' => '网银证书过期', + 'handling_method' => 'offline', + 'owner_name' => '其他负责人', + 'started_at' => '2026-10-21 10:00:00', + 'current_progress' => '待联系银行', + 'completion_status' => 'not_started', + 'updated_by' => 'Test User', + ]); + + $this->loginAs()->get($this->unavailableBalancesUrl()) + ->assertOk() + ->assertSee('账户冻结') + ->assertSee('待办负责人') + ->assertSee('处理中') + ->assertSee('进行中'); + + $filteredUrl = $this->unavailableBalancesUrl() . '?' . http_build_query([ + 'pending_short' => '待办测试账户', + 'pending_type' => '网商银行', + 'pending_status' => 'limited', + 'pending_reason' => '账户冻结', + 'pending_method' => 'online', + 'pending_owner' => '待办负责人', + ]); + $this->loginAs()->get($filteredUrl) + ->assertOk() + ->assertSee('data-pending-account="待办测试账户"', false) + ->assertDontSee('data-pending-account="待办测试账户二"', false); + + $sortResponse = $this->loginAs()->get( + $this->unavailableBalancesUrl() . '?pending_sort=started_at&pending_direction=desc' + )->assertOk()->assertSee('aria-sort="descending"', false); + preg_match_all('/data-pending-account="([^"]+)"/u', $sortResponse->getContent(), $sortedAccounts); + $this->assertSame(['待办测试账户二', '待办测试账户'], array_values(array_intersect( + $sortedAccounts[1], + ['待办测试账户', '待办测试账户二'], + ))); + + $this->loginAs()->post($this->accountTodoUrl(), [ + 'account_short' => '待办测试账户', + 'abnormal_reason' => '账户冻结', + 'handling_method' => 'online', + 'owner_name' => '待办负责人', + 'started_at' => '2026-10-21T09:00', + 'completed_at' => '2026-10-21T12:00', + 'current_progress' => '已处理', + 'completion_status' => 'completed', + ])->assertRedirect(); + + $this->assertDatabaseHas('fin_account_statuses', [ + 'account_short' => '待办测试账户', + 'status' => 'normal', + ], 'business_mirror'); + + $this->loginAs()->get($this->unavailableBalancesUrl()) + ->assertOk() + ->assertDontSee('data-pending-account="待办测试账户"', false); + } + public function test_financial_ledger_tab_forms_submit_to_query_tab_routes(): void { $pageUrl = $this->pageUrl(); @@ -406,7 +610,12 @@ class FinanceLedgerAccountStatusTest extends FeatureTestCase 'account_short' => '测试账户', 'account_should_keep' => '否', ])->assertForbidden(); + $this->withExceptionHandling()->loginAs($viewer)->post($this->accountTodoUrl(), [ + 'account_short' => '测试账户', + 'current_progress' => '处理中', + ])->assertForbidden(); $this->assertDatabaseCount('fin_account_statuses', 0, 'business_mirror'); + $this->assertDatabaseCount('fin_account_todos', 0, 'business_mirror'); } }