From c5f87afcfdf5e7288e94671e96fc0731a6ee4b92 Mon Sep 17 00:00:00 2001 From: shanshi <3316401569@qq.com> Date: Wed, 21 Oct 2026 15:07:48 +0800 Subject: [PATCH 1/7] =?UTF-8?q?=E8=B4=A2=E5=8A=A1=E5=8F=B0=E8=B4=A6?= =?UTF-8?q?=EF=BC=9A=E6=B1=87=E6=80=BB=E6=8C=87=E6=A0=87=E6=94=B9=E4=B8=BA?= =?UTF-8?q?=E7=8B=AC=E7=AB=8B=E5=8D=A1=E7=89=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../business/pages/financial-ledger/ledger.blade.php | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/resources/views/business/pages/financial-ledger/ledger.blade.php b/resources/views/business/pages/financial-ledger/ledger.blade.php index 45b55dec..a20f1f1c 100644 --- a/resources/views/business/pages/financial-ledger/ledger.blade.php +++ b/resources/views/business/pages/financial-ledger/ledger.blade.php @@ -84,10 +84,10 @@ .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 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 +165,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; } }
-- 2.47.2 From 50ae00392e086521c926425eeb884965b18c6ce9 Mon Sep 17 00:00:00 2001 From: shanshi <3316401569@qq.com> Date: Wed, 21 Oct 2026 15:10:47 +0800 Subject: [PATCH 2/7] =?UTF-8?q?=E8=B4=A2=E5=8A=A1=E5=8F=B0=E8=B4=A6?= =?UTF-8?q?=EF=BC=9A=E5=8C=BA=E5=88=86=E5=8F=AF=E7=94=A8=E4=B8=8E=E4=B8=8D?= =?UTF-8?q?=E5=8F=AF=E7=94=A8=E9=87=91=E9=A2=9D=E5=8D=A1=E7=89=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../views/business/pages/financial-ledger/ledger.blade.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/resources/views/business/pages/financial-ledger/ledger.blade.php b/resources/views/business/pages/financial-ledger/ledger.blade.php index a20f1f1c..20fc2295 100644 --- a/resources/views/business/pages/financial-ledger/ledger.blade.php +++ b/resources/views/business/pages/financial-ledger/ledger.blade.php @@ -86,6 +86,8 @@ .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: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 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; } @@ -225,8 +227,8 @@
账户数{{ number_format((int) ($ledgerAccountSummary['count'] ?? 0)) }}
-
可用金额合计{{ $money($ledgerAccountSummary['balance'] ?? 0) }}
-
不可用金额合计{{ $money($ledgerAccountSummary['blocked'] ?? 0) }}
+
可用金额合计{{ $money($ledgerAccountSummary['balance'] ?? 0) }}
+
不可用金额合计{{ $money($ledgerAccountSummary['blocked'] ?? 0) }}
每日可出金额口径可用金额 − 不可用金额
-- 2.47.2 From 56f005404dda7e9f6a29774136cafe3271c62f7c Mon Sep 17 00:00:00 2001 From: shanshi <3316401569@qq.com> Date: Wed, 21 Oct 2026 15:37:50 +0800 Subject: [PATCH 3/7] =?UTF-8?q?=E8=B4=A2=E5=8A=A1=E5=8F=B0=E8=B4=A6?= =?UTF-8?q?=EF=BC=9A=E5=A2=9E=E5=8A=A0=E8=B4=A6=E6=88=B7=E5=BC=82=E5=B8=B8?= =?UTF-8?q?=E7=8B=AC=E7=AB=8B=E5=BE=85=E5=8A=9E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Controllers/Business/FinanceLedger.php | 148 ++++++++++++++ ...00_create_finance_ledger_account_todos.php | 39 ++++ .../pages/financial-ledger/ledger.blade.php | 5 +- .../unavailable-balance-tasks.blade.php | 183 ++++++++++++++++++ routes/admin.php | 8 + .../FinanceLedgerAccountStatusTest.php | 129 ++++++++++++ 6 files changed, 511 insertions(+), 1 deletion(-) create mode 100644 database/migrations/2026_10_21_150000_create_finance_ledger_account_todos.php create mode 100644 resources/views/business/pages/financial-ledger/unavailable-balance-tasks.blade.php diff --git a/app/Http/Controllers/Business/FinanceLedger.php b/app/Http/Controllers/Business/FinanceLedger.php index a328b41f..fc202ee3 100644 --- a/app/Http/Controllers/Business/FinanceLedger.php +++ b/app/Http/Controllers/Business/FinanceLedger.php @@ -24,6 +24,154 @@ 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], + )); + $query = $connection->table('fin_accounts')->whereIn( + 'account_short', + $connection->table('fin_account_statuses') + ->select('account_short') + ->whereIn('status', $abnormalStatuses), + ); + $keyword = trim((string) $request->query('pending_q', '')); + + if ($keyword !== '') { + $query->where(function ($builder) use ($keyword): void { + foreach (['company_name', 'account_short', 'account_no', 'owner'] as $column) { + $builder->orWhere($column, 'like', '%' . $keyword . '%'); + } + }); + } + + $summaryQuery = clone $query; + $summary = [ + 'count' => (clone $query)->count(), + 'balance' => round((float) (clone $summaryQuery)->sum('current_balance'), 2), + 'blocked' => round((float) (clone $summaryQuery)->sum('blocked_amount'), 2), + 'daily_available' => round((float) (clone $summaryQuery)->sum('daily_available'), 2), + ]; + + $rows = $query + ->orderByDesc('blocked_amount') + ->orderBy('company_name') + ->orderBy('account_short') + ->paginate(50, ['*'], 'pending_page') + ->withQueryString(); + + $accountShorts = $rows->pluck('account_short')->filter()->unique()->values()->all(); + $statuses = $accountShorts === [] + ? collect() + : $connection->table('fin_account_statuses') + ->whereIn('account_short', $accountShorts) + ->pluck('status', 'account_short'); + $typeOverrides = $accountShorts === [] + ? collect() + : $connection->table('fin_account_type_overrides') + ->whereIn('account_short', $accountShorts) + ->pluck('account_type', 'account_short'); + $todos = $accountShorts === [] + ? collect() + : $connection->table('fin_account_todos') + ->whereIn('account_short', $accountShorts) + ->get() + ->keyBy('account_short'); + + $rows->getCollection()->transform(static function ($row) use ($statuses, $typeOverrides, $todos) { + $row->account_status = FinanceLedgerAccountStatus::normalize($statuses[$row->account_short] ?? null); + if ($typeOverrides->has($row->account_short)) { + $row->account_type = (string) ($typeOverrides->get($row->account_short) ?? ''); + } + $todo = $todos->get($row->account_short); + $row->todo_abnormal_reason = $todo->abnormal_reason ?? null; + $row->todo_handling_method = $todo->handling_method ?? null; + $row->todo_owner_name = $todo->owner_name ?? null; + $row->todo_started_at = $todo->started_at ?? null; + $row->todo_completed_at = $todo->completed_at ?? null; + $row->todo_current_progress = $todo->current_progress ?? null; + + return $row; + }); + + return view('business.pages.financial-ledger.unavailable-balance-tasks', [ + 'rows' => $rows, + 'summary' => $summary, + 'keyword' => $keyword, + 'backUrl' => route('business.pages.show', [ + 'section' => 'financial-operations', + 'page' => 'ledger', + ]) . '?tab=accounts', + 'canEdit' => FinanceLedgerAccess::canEditAccountStatus(user()), + 'handlingMethods' => [ + 'online' => '线上', + 'offline' => '线下', + ], + ]); + } + + 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'], + ]); + + $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(); + $values = [ + 'abnormal_reason' => $data['abnormal_reason'] ?? null, + 'handling_method' => $data['handling_method'] ?? null, + 'owner_name' => $data['owner_name'] ?? null, + 'started_at' => $data['started_at'] ?? null, + 'completed_at' => $data['completed_at'] ?? null, + 'current_progress' => $data['current_progress'] ?? null, + 'updated_by' => (string) user()->name, + 'updated_at' => $now, + ]; + $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, + ]); + } + + return redirect()->back()->with('success', '待办事项已保存'); + } + 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/resources/views/business/pages/financial-ledger/ledger.blade.php b/resources/views/business/pages/financial-ledger/ledger.blade.php index 20fc2295..01b9c906 100644 --- a/resources/views/business/pages/financial-ledger/ledger.blade.php +++ b/resources/views/business/pages/financial-ledger/ledger.blade.php @@ -88,6 +88,9 @@ .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; } @@ -228,7 +231,7 @@
账户数{{ number_format((int) ($ledgerAccountSummary['count'] ?? 0)) }}
可用金额合计{{ $money($ledgerAccountSummary['balance'] ?? 0) }}
-
不可用金额合计{{ $money($ledgerAccountSummary['blocked'] ?? 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..b8327042 --- /dev/null +++ b/resources/views/business/pages/financial-ledger/unavailable-balance-tasks.blade.php @@ -0,0 +1,183 @@ +@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 + +
+ + + + + 重置 + +
+ +
+ + + + + + + + + + + + + + + + + + + + @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..bff4536c 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,122 @@ 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, + ], + ]; + 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', + ], + ]); + $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('25.50') + ->assertSee('限额') + ->assertSee('网商银行') + ->assertSee('6222000000000099') + ->assertSee('待办测试银行') + ->assertSee('上海 / 待办测试支行') + ->assertSee('异常原因') + ->assertSee('办理方式') + ->assertSee('当前进度') + ->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' => '处理中', + ])->assertRedirect(); + + $this->assertDatabaseHas('fin_account_todos', [ + 'account_short' => '待办测试账户', + 'abnormal_reason' => '账户冻结', + 'handling_method' => 'online', + 'owner_name' => '待办负责人', + 'current_progress' => '处理中', + 'updated_by' => 'Test User', + ], 'business_mirror'); + + $this->loginAs()->get($this->unavailableBalancesUrl()) + ->assertOk() + ->assertSee('账户冻结') + ->assertSee('待办负责人') + ->assertSee('处理中'); + + $this->loginAs()->get($this->unavailableBalancesUrl() . '?pending_q=待办负责人') + ->assertOk() + ->assertSee('data-pending-account="待办测试账户"', false); + + $this->loginAs()->get($this->unavailableBalancesUrl() . '?pending_q=不存在') + ->assertOk() + ->assertSee('当前没有账户异常待办') + ->assertDontSee('data-pending-account="待办测试账户"', false); + } + public function test_financial_ledger_tab_forms_submit_to_query_tab_routes(): void { $pageUrl = $this->pageUrl(); @@ -406,7 +530,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'); } } -- 2.47.2 From ac89af9d32525113acf60868fd985b12647c0b16 Mon Sep 17 00:00:00 2001 From: shanshi <3316401569@qq.com> Date: Wed, 21 Oct 2026 15:54:53 +0800 Subject: [PATCH 4/7] =?UTF-8?q?=E8=B4=A2=E5=8A=A1=E5=8F=B0=E8=B4=A6?= =?UTF-8?q?=EF=BC=9A=E5=AE=8C=E5=96=84=E5=BC=82=E5=B8=B8=E5=BE=85=E5=8A=9E?= =?UTF-8?q?=E7=AD=9B=E9=80=89=E6=8E=92=E5=BA=8F=E5=92=8C=E5=AE=8C=E6=88=90?= =?UTF-8?q?=E8=81=94=E5=8A=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Controllers/Business/FinanceLedger.php | 231 +++++++++++++----- ...status_to_finance_ledger_account_todos.php | 39 +++ .../unavailable-balance-tasks.blade.php | 135 ++++++++-- .../FinanceLedgerAccountStatusTest.php | 86 ++++++- 4 files changed, 398 insertions(+), 93 deletions(-) create mode 100644 database/migrations/2026_10_21_160000_add_completion_status_to_finance_ledger_account_todos.php diff --git a/app/Http/Controllers/Business/FinanceLedger.php b/app/Http/Controllers/Business/FinanceLedger.php index fc202ee3..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; @@ -31,84 +32,158 @@ class FinanceLedger extends Controller array_keys(FinanceLedgerAccountStatus::OPTIONS), [FinanceLedgerAccountStatus::NORMAL], )); - $query = $connection->table('fin_accounts')->whereIn( - 'account_short', - $connection->table('fin_account_statuses') - ->select('account_short') - ->whereIn('status', $abnormalStatuses), - ); - $keyword = trim((string) $request->query('pending_q', '')); + $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', '')), + ]; - if ($keyword !== '') { - $query->where(function ($builder) use ($keyword): void { - foreach (['company_name', 'account_short', 'account_no', 'owner'] as $column) { - $builder->orWhere($column, 'like', '%' . $keyword . '%'); - } + $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('current_balance'), 2), - 'blocked' => round((float) (clone $summaryQuery)->sum('blocked_amount'), 2), - 'daily_available' => round((float) (clone $summaryQuery)->sum('daily_available'), 2), + '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 - ->orderByDesc('blocked_amount') - ->orderBy('company_name') - ->orderBy('account_short') - ->paginate(50, ['*'], 'pending_page') + ->orderBy('accounts.company_name') + ->orderBy('accounts.account_short') + ->paginate(50) + ->appends($filters) + ->appends(array_filter([ + 'pending_sort' => $sortField, + 'pending_direction' => $sortField !== '' ? $sortDirection : '', + ])) ->withQueryString(); - $accountShorts = $rows->pluck('account_short')->filter()->unique()->values()->all(); - $statuses = $accountShorts === [] - ? collect() - : $connection->table('fin_account_statuses') - ->whereIn('account_short', $accountShorts) - ->pluck('status', 'account_short'); - $typeOverrides = $accountShorts === [] - ? collect() - : $connection->table('fin_account_type_overrides') - ->whereIn('account_short', $accountShorts) - ->pluck('account_type', 'account_short'); - $todos = $accountShorts === [] - ? collect() - : $connection->table('fin_account_todos') - ->whereIn('account_short', $accountShorts) - ->get() - ->keyBy('account_short'); - - $rows->getCollection()->transform(static function ($row) use ($statuses, $typeOverrides, $todos) { - $row->account_status = FinanceLedgerAccountStatus::normalize($statuses[$row->account_short] ?? null); - if ($typeOverrides->has($row->account_short)) { - $row->account_type = (string) ($typeOverrides->get($row->account_short) ?? ''); + $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 ?? ''); } - $todo = $todos->get($row->account_short); - $row->todo_abnormal_reason = $todo->abnormal_reason ?? null; - $row->todo_handling_method = $todo->handling_method ?? null; - $row->todo_owner_name = $todo->owner_name ?? null; - $row->todo_started_at = $todo->started_at ?? null; - $row->todo_completed_at = $todo->completed_at ?? null; - $row->todo_current_progress = $todo->current_progress ?? null; + $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, - 'keyword' => $keyword, + 'filters' => $filters, + 'sortField' => $sortField, + 'sortDirection' => $sortDirection, 'backUrl' => route('business.pages.show', [ 'section' => 'financial-operations', 'page' => 'ledger', ]) . '?tab=accounts', 'canEdit' => FinanceLedgerAccess::canEditAccountStatus(user()), - 'handlingMethods' => [ - 'online' => '线上', - 'offline' => '线下', - ], + 'handlingMethods' => $handlingMethods, + 'completionStatuses' => $completionStatuses, + 'accountStatusOptions' => array_intersect_key( + FinanceLedgerAccountStatus::OPTIONS, + array_flip($abnormalStatuses), + ), + 'accountTypeOptions' => $typeOptions, + 'reasonOptions' => $reasonOptions, + 'ownerOptions' => $ownerOptions, ]); } @@ -124,6 +199,7 @@ class FinanceLedger extends Controller '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']); @@ -148,28 +224,51 @@ class FinanceLedger extends Controller ); $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' => $data['started_at'] ?? null, - 'completed_at' => $data['completed_at'] ?? 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, ]; - $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, - ]); - } + $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, + ]); + } - return redirect()->back()->with('success', '待办事项已保存'); + 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 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/unavailable-balance-tasks.blade.php b/resources/views/business/pages/financial-ledger/unavailable-balance-tasks.blade.php index b8327042..ade70920 100644 --- a/resources/views/business/pages/financial-ledger/unavailable-balance-tasks.blade.php +++ b/resources/views/business/pages/financial-ledger/unavailable-balance-tasks.blade.php @@ -21,10 +21,14 @@ .pending-balance-card b { display:block; margin-top:.2rem; overflow:hidden; font-size:.92rem; font-variant-numeric:tabular-nums; line-height:1.35; text-overflow:ellipsis; white-space:nowrap; } .pending-balance-alert { display:flex; align-items:center; gap:.35rem; padding:.65rem .8rem; margin-bottom:.75rem; border:1px solid #bbf7d0; border-radius:.5rem; background:#f0fdf4; color:var(--vip-success, #53793E); font-size:.72rem; } .pending-balance-alert.is-error { border-color:rgba(184,0,0,.24); background:#fff5f5; color:var(--vip-danger, #B80000); } - .pending-balance-toolbar { display:flex; align-items:flex-end; flex-wrap:wrap; gap:.55rem; padding:.75rem 1rem; margin-bottom:.75rem; border:1px solid var(--vip-border, #D8D2C7); border-radius:.65rem; background:#fff; } - .pending-balance-field { display:flex; flex:1 1 18rem; flex-direction:column; gap:.3rem; min-width:12rem; color:var(--vip-content-muted, #595959); font-size:.68rem; } - .pending-balance-field input { height:2.15rem; padding:0 .65rem; border:1px solid var(--vip-border, #D8D2C7); border-radius:.4rem; background:#fff; color:var(--vip-content-ink, #171717); font:inherit; } - .pending-balance-field input:focus { border-color:var(--vip-content-accent-dark, #856632); outline:none; box-shadow:0 0 0 3px rgba(133,102,50,.16); } + .pending-balance-toolbar { padding:.75rem 1rem; margin-bottom:.75rem; border:1px solid var(--vip-border, #D8D2C7); border-radius:.65rem; background:#fff; } + .pending-balance-filter-grid { display:grid; grid-template-columns:repeat(6, minmax(0, 1fr)); gap:.55rem; } + .pending-balance-field { display:flex; flex-direction:column; gap:.3rem; min-width:0; color:var(--vip-content-muted, #595959); font-size:.68rem; } + .pending-balance-field input, + .pending-balance-field select { width:100%; min-width:0; height:2.15rem; padding:0 .55rem; border:1px solid var(--vip-border, #D8D2C7); border-radius:.4rem; background:#fff; color:var(--vip-content-ink, #171717); font:inherit; } + .pending-balance-field input:focus, + .pending-balance-field select:focus { border-color:var(--vip-content-accent-dark, #856632); outline:none; box-shadow:0 0 0 3px rgba(133,102,50,.16); } + .pending-balance-toolbar-actions { display:flex; justify-content:flex-end; gap:.55rem; margin-top:.65rem; } .pending-balance-button { display:inline-flex; align-items:center; justify-content:center; gap:.35rem; height:2.15rem; padding:0 .8rem; border:1px solid var(--vip-border, #D8D2C7); border-radius:.4rem; background:#fff; color:var(--vip-content-accent-dark, #856632); font:inherit; font-weight:600; text-decoration:none; cursor:pointer; } .pending-balance-button.is-primary { border-color:var(--vip-content-accent-dark, #856632); background:var(--vip-content-accent-dark, #856632); color:#fff; } .pending-balance-table-wrap { overflow:auto; border:1px solid var(--vip-border, #D8D2C7); border-radius:.65rem; background:#fff; } @@ -37,6 +41,9 @@ .pending-balance-table tbody tr:last-child td { border-bottom:0; } .pending-balance-table tbody tr:hover td { background:#fcfaf4; } .pending-balance-table .num { font-variant-numeric:tabular-nums; text-align:right; } + .pending-balance-sort { display:inline-flex; align-items:center; justify-content:center; gap:.2rem; color:inherit; text-decoration:none; } + .pending-balance-sort:hover { color:var(--vip-content-accent-dark, #856632); } + .pending-balance-sort .material-icons-outlined { font-size:.9rem; } .pending-balance-task-input { width:100%; min-width:8rem; height:2rem; padding:0 .45rem; border:1px solid var(--vip-border, #D8D2C7); border-radius:.35rem; background:#fff; color:var(--vip-content-ink, #171717); font:inherit; } .pending-balance-task-input.is-compact { min-width:6.5rem; } .pending-balance-task-input:focus { border-color:var(--vip-content-accent-dark, #856632); outline:none; box-shadow:0 0 0 3px rgba(133,102,50,.14); } @@ -54,8 +61,10 @@ .pending-balance-pagination__links { display:flex; align-items:center; gap:.75rem; } .pending-balance-pagination a { color:var(--vip-content-accent-dark, #856632); font-weight:600; text-decoration:none; } .pending-balance-pagination a:hover { color:#6A5129; } + @media (max-width:1100px) { .pending-balance-filter-grid { grid-template-columns:repeat(3, minmax(0, 1fr)); } } @media (max-width:900px) { .pending-balance-summary { grid-template-columns:repeat(2, minmax(0, 1fr)); } } - @media (max-width:560px) { .pending-balance-page { margin:1rem .75rem 1.5rem; } .pending-balance-summary { grid-template-columns:1fr; } } + @media (max-width:700px) { .pending-balance-filter-grid { grid-template-columns:repeat(2, minmax(0, 1fr)); } } + @media (max-width:560px) { .pending-balance-page { margin:1rem .75rem 1.5rem; } .pending-balance-summary, .pending-balance-filter-grid { grid-template-columns:1fr; } }
@@ -84,21 +93,87 @@ @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 +
@@ -113,9 +188,20 @@ - - + + + @@ -156,9 +242,16 @@ @endif + @empty - + @endforelse
异常原因 办理方式 责任人员开始时间完成时间 + + 开始时间 + + + + + 完成时间 + + + 当前进度是否完成
+ +
当前没有账户异常待办
当前没有账户异常待办
diff --git a/tests/Feature/Business/FinanceLedgerAccountStatusTest.php b/tests/Feature/Business/FinanceLedgerAccountStatusTest.php index bff4536c..beec21a6 100644 --- a/tests/Feature/Business/FinanceLedgerAccountStatusTest.php +++ b/tests/Feature/Business/FinanceLedgerAccountStatusTest.php @@ -161,6 +161,17 @@ class FinanceLedgerAccountStatusTest extends FeatureTestCase '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); @@ -176,6 +187,11 @@ class FinanceLedgerAccountStatusTest extends FeatureTestCase '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' => '待办测试账户', @@ -190,7 +206,7 @@ class FinanceLedgerAccountStatusTest extends FeatureTestCase ->assertSee('待办测试公司') ->assertSee('data-pending-account="待办测试账户"', false) ->assertSee('不可用金额合计') - ->assertSee('25.50') + ->assertSee('35.50') ->assertSee('限额') ->assertSee('网商银行') ->assertSee('6222000000000099') @@ -199,6 +215,17 @@ class FinanceLedgerAccountStatusTest extends FeatureTestCase ->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-account="待办测试账户二"', false) ->assertDontSee('data-pending-account="正常状态测试账户"', false) ->assertDontSee('data-pending-account="零金额测试账户"', false); @@ -210,6 +237,7 @@ class FinanceLedgerAccountStatusTest extends FeatureTestCase 'started_at' => '2026-10-21T09:00', 'completed_at' => '2026-10-21T11:30', 'current_progress' => '处理中', + 'completion_status' => 'in_progress', ])->assertRedirect(); $this->assertDatabaseHas('fin_account_todos', [ @@ -218,22 +246,68 @@ class FinanceLedgerAccountStatusTest extends FeatureTestCase '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('处理中') + ->assertSee('进行中'); - $this->loginAs()->get($this->unavailableBalancesUrl() . '?pending_q=待办负责人') + $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); + ->assertSee('data-pending-account="待办测试账户"', false) + ->assertDontSee('data-pending-account="待办测试账户二"', false); - $this->loginAs()->get($this->unavailableBalancesUrl() . '?pending_q=不存在') + $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() - ->assertSee('当前没有账户异常待办') ->assertDontSee('data-pending-account="待办测试账户"', false); } -- 2.47.2 From c0acda261d3170d6d6ff1e0362d9c0354c26d2dc Mon Sep 17 00:00:00 2001 From: shanshi <3316401569@qq.com> Date: Wed, 21 Oct 2026 15:58:17 +0800 Subject: [PATCH 5/7] =?UTF-8?q?=E8=B4=A2=E5=8A=A1=E5=8F=B0=E8=B4=A6?= =?UTF-8?q?=EF=BC=9A=E5=9B=BA=E5=AE=9A=E5=BC=82=E5=B8=B8=E5=BE=85=E5=8A=9E?= =?UTF-8?q?=E8=A1=A8=E5=A4=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../financial-ledger/unavailable-balance-tasks.blade.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 index ade70920..fb95b291 100644 --- a/resources/views/business/pages/financial-ledger/unavailable-balance-tasks.blade.php +++ b/resources/views/business/pages/financial-ledger/unavailable-balance-tasks.blade.php @@ -31,11 +31,11 @@ .pending-balance-toolbar-actions { display:flex; justify-content:flex-end; gap:.55rem; margin-top:.65rem; } .pending-balance-button { display:inline-flex; align-items:center; justify-content:center; gap:.35rem; height:2.15rem; padding:0 .8rem; border:1px solid var(--vip-border, #D8D2C7); border-radius:.4rem; background:#fff; color:var(--vip-content-accent-dark, #856632); font:inherit; font-weight:600; text-decoration:none; cursor:pointer; } .pending-balance-button.is-primary { border-color:var(--vip-content-accent-dark, #856632); background:var(--vip-content-accent-dark, #856632); color:#fff; } - .pending-balance-table-wrap { overflow:auto; border:1px solid var(--vip-border, #D8D2C7); border-radius:.65rem; background:#fff; } + .pending-balance-table-wrap { max-height:calc(100vh - 360px); min-height:20rem; overflow:auto; border:1px solid var(--vip-border, #D8D2C7); border-radius:.65rem; background:#fff; } .pending-balance-table { width:100%; min-width:72rem; border-collapse:separate; border-spacing:0; font-size:.72rem; } .pending-balance-table th, .pending-balance-table td { padding:.55rem .65rem; border-right:1px solid #e8e3da; border-bottom:1px solid #e8e3da; text-align:left; vertical-align:middle; white-space:nowrap; } - .pending-balance-table th { background:var(--vip-content-accent-soft, #F7F0DE); font-weight:700; text-align:center; } + .pending-balance-table th { position:sticky; top:0; z-index:4; background:var(--vip-content-accent-soft, #F7F0DE); box-shadow:0 1px 0 var(--vip-border, #D8D2C7); font-weight:700; text-align:center; } .pending-balance-table th:last-child, .pending-balance-table td:last-child { border-right:0; } .pending-balance-table tbody tr:last-child td { border-bottom:0; } -- 2.47.2 From e873f35241e780549ecc9ddbee713606965c46ae Mon Sep 17 00:00:00 2001 From: shanshi <3316401569@qq.com> Date: Wed, 21 Oct 2026 16:03:36 +0800 Subject: [PATCH 6/7] =?UTF-8?q?=E8=B4=A2=E5=8A=A1=E5=8F=B0=E8=B4=A6?= =?UTF-8?q?=EF=BC=9A=E5=BC=82=E5=B8=B8=E5=BE=85=E5=8A=9E=E5=88=86=E9=A1=B5?= =?UTF-8?q?=E6=94=B9=E4=B8=BA=E6=97=A0=E5=88=B7=E6=96=B0=E5=88=87=E6=8D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../unavailable-balance-tasks.blade.php | 145 ++++++++++++++++-- .../FinanceLedgerAccountStatusTest.php | 4 + 2 files changed, 135 insertions(+), 14 deletions(-) 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 index fb95b291..14c2da7b 100644 --- a/resources/views/business/pages/financial-ledger/unavailable-balance-tasks.blade.php +++ b/resources/views/business/pages/financial-ledger/unavailable-balance-tasks.blade.php @@ -57,10 +57,21 @@ .pending-balance-status.is-limited { border-color:rgba(147,95,7,.32); background:#fff8eb; color:var(--vip-warning, #935F07); } .pending-balance-status.is-business_deregistered { border-color:rgba(89,89,89,.28); background:#f3f1ec; color:var(--vip-content-muted, #595959); } .pending-balance-empty { padding:3rem 1rem; color:var(--vip-content-muted, #595959); text-align:center; } - .pending-balance-pagination { display:flex; align-items:center; justify-content:space-between; gap:.75rem; padding:.7rem .9rem; color:var(--vip-content-muted, #595959); } - .pending-balance-pagination__links { display:flex; align-items:center; gap:.75rem; } - .pending-balance-pagination a { color:var(--vip-content-accent-dark, #856632); font-weight:600; text-decoration:none; } - .pending-balance-pagination a:hover { color:#6A5129; } + .pending-balance-page.is-paginating .pending-balance-table-wrap { opacity:.58; pointer-events:none; } + .ledger-pagination { display:flex; align-items:center; justify-content:space-between; gap:.75rem; padding:.65rem .9rem; color:var(--vip-content-muted, #595959); font-size:.72rem; } + .ledger-pagination a { margin-left:.75rem; color:var(--vip-content-accent-dark, #856632); font-weight:600; text-decoration:none; } + .ledger-pagination__controls { display:flex; align-items:center; justify-content:flex-end; gap:.45rem; margin-left:auto; } + .ledger-page-box { display:inline-flex; align-items:center; gap:.1rem; height:1.7rem; } + .ledger-page-box__arrow { display:inline-grid; place-items:center; width:1.2rem; height:1.5rem; padding:0; border:0; background:transparent; color:var(--vip-content-accent-dark, #856632); font:inherit; text-decoration:none; } + .ledger-page-box__arrow:hover { color:#6A5129; } + .ledger-page-box__arrow:focus:not(:focus-visible) { outline:none; } + .ledger-page-box__arrow:focus-visible { outline:1px solid var(--vip-content-accent-dark, #856632); outline-offset:0; border-radius:2px; } + .ledger-page-box__triangle { width:0; height:0; border-top:5px solid transparent; border-bottom:5px solid transparent; } + .ledger-page-box__triangle.is-previous { border-right:8px solid currentColor; } + .ledger-page-box__triangle.is-next { border-left:8px solid currentColor; } + .ledger-page-box__arrow.is-disabled { color:#b9b3a9; cursor:not-allowed; } + .ledger-page-box__current { min-width:2.2rem; padding:0 .15rem; color:var(--vip-content-ink, #171717); font-size:.7rem; font-variant-numeric:tabular-nums; font-weight:700; text-align:center; white-space:nowrap; } + .pending-balance-page__status { color:var(--vip-content-muted, #595959); font-size:.68rem; } @media (max-width:1100px) { .pending-balance-filter-grid { grid-template-columns:repeat(3, minmax(0, 1fr)); } } @media (max-width:900px) { .pending-balance-summary { grid-template-columns:repeat(2, minmax(0, 1fr)); } } @media (max-width:700px) { .pending-balance-filter-grid { grid-template-columns:repeat(2, minmax(0, 1fr)); } } @@ -204,7 +215,7 @@ 是否完成 - + @forelse ($rows as $row) @@ -258,19 +269,125 @@
@if ($rows->total() > 0) -
+
共 {{ number_format($rows->total()) }} 条 · 每页 {{ number_format($rows->perPage()) }} 条 - @endif
+ + diff --git a/tests/Feature/Business/FinanceLedgerAccountStatusTest.php b/tests/Feature/Business/FinanceLedgerAccountStatusTest.php index beec21a6..c0ec57c5 100644 --- a/tests/Feature/Business/FinanceLedgerAccountStatusTest.php +++ b/tests/Feature/Business/FinanceLedgerAccountStatusTest.php @@ -225,6 +225,10 @@ class FinanceLedgerAccountStatusTest extends FeatureTestCase ->assertSee('按开始时间排序') ->assertSee('按完成时间排序') ->assertDontSee('name="pending_q"', false) + ->assertSee('data-pending-tbody', false) + ->assertSee('data-pending-pagination', false) + ->assertSee('ledger-page-box', false) + ->assertSee('1 / 1', false) ->assertSee('data-pending-account="待办测试账户二"', false) ->assertDontSee('data-pending-account="正常状态测试账户"', false) ->assertDontSee('data-pending-account="零金额测试账户"', false); -- 2.47.2 From 1a9654b6d51a4f981dbd2f4e49c81393c603de5e Mon Sep 17 00:00:00 2001 From: shanshi <3316401569@qq.com> Date: Wed, 21 Oct 2026 16:09:38 +0800 Subject: [PATCH 7/7] =?UTF-8?q?=E8=B4=A2=E5=8A=A1=E5=8F=B0=E8=B4=A6?= =?UTF-8?q?=EF=BC=9A=E5=8E=8B=E7=BC=A9=E5=BE=85=E5=8A=9E=E8=A1=8C=E9=AB=98?= =?UTF-8?q?=E5=B9=B6=E6=94=AF=E6=8C=81=E8=B0=83=E6=95=B4=E5=88=97=E5=AE=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../unavailable-balance-tasks.blade.php | 174 +++++++++++++++--- .../FinanceLedgerAccountStatusTest.php | 2 + 2 files changed, 155 insertions(+), 21 deletions(-) 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 index 14c2da7b..37573f02 100644 --- a/resources/views/business/pages/financial-ledger/unavailable-balance-tasks.blade.php +++ b/resources/views/business/pages/financial-ledger/unavailable-balance-tasks.blade.php @@ -31,10 +31,12 @@ .pending-balance-toolbar-actions { display:flex; justify-content:flex-end; gap:.55rem; margin-top:.65rem; } .pending-balance-button { display:inline-flex; align-items:center; justify-content:center; gap:.35rem; height:2.15rem; padding:0 .8rem; border:1px solid var(--vip-border, #D8D2C7); border-radius:.4rem; background:#fff; color:var(--vip-content-accent-dark, #856632); font:inherit; font-weight:600; text-decoration:none; cursor:pointer; } .pending-balance-button.is-primary { border-color:var(--vip-content-accent-dark, #856632); background:var(--vip-content-accent-dark, #856632); color:#fff; } + .pending-balance-table-hint { margin:0 0 .35rem; color:var(--vip-content-muted, #595959); font-size:.66rem; text-align:right; } .pending-balance-table-wrap { max-height:calc(100vh - 360px); min-height:20rem; overflow:auto; border:1px solid var(--vip-border, #D8D2C7); border-radius:.65rem; background:#fff; } .pending-balance-table { width:100%; min-width:72rem; border-collapse:separate; border-spacing:0; font-size:.72rem; } .pending-balance-table th, - .pending-balance-table td { padding:.55rem .65rem; border-right:1px solid #e8e3da; border-bottom:1px solid #e8e3da; text-align:left; vertical-align:middle; white-space:nowrap; } + .pending-balance-table td { padding:.32rem .52rem; border-right:1px solid #e8e3da; border-bottom:1px solid #e8e3da; line-height:1.4; text-align:left; vertical-align:middle; white-space:nowrap; } + .pending-balance-table tbody td { overflow:hidden; text-overflow:ellipsis; } .pending-balance-table th { position:sticky; top:0; z-index:4; background:var(--vip-content-accent-soft, #F7F0DE); box-shadow:0 1px 0 var(--vip-border, #D8D2C7); font-weight:700; text-align:center; } .pending-balance-table th:last-child, .pending-balance-table td:last-child { border-right:0; } @@ -44,14 +46,20 @@ .pending-balance-sort { display:inline-flex; align-items:center; justify-content:center; gap:.2rem; color:inherit; text-decoration:none; } .pending-balance-sort:hover { color:var(--vip-content-accent-dark, #856632); } .pending-balance-sort .material-icons-outlined { font-size:.9rem; } - .pending-balance-task-input { width:100%; min-width:8rem; height:2rem; padding:0 .45rem; border:1px solid var(--vip-border, #D8D2C7); border-radius:.35rem; background:#fff; color:var(--vip-content-ink, #171717); font:inherit; } - .pending-balance-task-input.is-compact { min-width:6.5rem; } + .pending-balance-task-input { width:100%; min-width:0; height:1.75rem; padding:0 .42rem; border:1px solid var(--vip-border, #D8D2C7); border-radius:.35rem; background:#fff; color:var(--vip-content-ink, #171717); font:inherit; } .pending-balance-task-input:focus { border-color:var(--vip-content-accent-dark, #856632); outline:none; box-shadow:0 0 0 3px rgba(133,102,50,.14); } .pending-balance-task-input:disabled { background:#f6f4ef; color:var(--vip-content-muted, #595959); cursor:not-allowed; } - .pending-balance-progress { display:flex; align-items:center; gap:.35rem; min-width:9rem; } - .pending-balance-save { display:inline-grid; flex:0 0 auto; place-items:center; width:2rem; height:2rem; padding:0; border:1px solid var(--vip-content-accent-dark, #856632); border-radius:.35rem; background:var(--vip-content-accent-dark, #856632); color:#fff; cursor:pointer; } + .pending-balance-progress { display:flex; align-items:center; gap:.3rem; min-width:0; } + .pending-balance-save { display:inline-grid; flex:0 0 auto; place-items:center; width:1.75rem; height:1.75rem; padding:0; border:1px solid var(--vip-content-accent-dark, #856632); border-radius:.35rem; background:var(--vip-content-accent-dark, #856632); color:#fff; cursor:pointer; } .pending-balance-save:hover { border-color:#6A5129; background:#6A5129; } - .pending-balance-status { display:inline-flex; align-items:center; justify-content:center; min-height:1.6rem; padding:.1rem .4rem; border:1px solid transparent; border-radius:999px; font-size:.68rem; font-weight:700; } + .pending-balance-status { display:inline-flex; align-items:center; justify-content:center; min-height:1.45rem; padding:.08rem .35rem; border:1px solid transparent; border-radius:999px; font-size:.68rem; font-weight:700; } + .pending-column-resizer { position:absolute; top:0; right:0; z-index:6; width:.45rem; height:100%; cursor:col-resize; touch-action:none; } + .pending-column-resizer::after { content:""; position:absolute; top:20%; bottom:20%; left:50%; width:1px; background:transparent; transition:background .15s ease; } + .pending-column-resizer:hover::after, + .pending-column-resizer.is-active::after { background:var(--vip-content-accent-dark, #856632); } + .pending-column-resizer:focus { outline:none; } + .pending-column-resizer:focus-visible::after { width:2px; background:var(--vip-content-accent-dark, #856632); } + body.is-resizing-pending-column { cursor:col-resize; user-select:none; } .pending-balance-status.is-normal { border-color:rgba(83,121,62,.3); background:#f3f7ee; color:var(--vip-success, #53793E); } .pending-balance-status.is-online_banking_abnormal { border-color:rgba(184,0,0,.3); background:#fff5f5; color:var(--vip-danger, #B80000); } .pending-balance-status.is-limited { border-color:rgba(147,95,7,.32); background:#fff8eb; color:var(--vip-warning, #935F07); } @@ -185,34 +193,35 @@ ); @endphp +

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

- +
- - - - - - - - - - - + + + + + + + + + + - - - + + @@ -291,6 +300,129 @@ @endif + +
公司名称账户简称账户类型账户状态账号开户行开户城市/网点异常原因办理方式责任人员 + 公司名称账户简称账户类型账户状态账号开户行开户城市/网点异常原因办理方式责任人员 开始时间 + 完成时间 当前进度是否完成当前进度是否完成