From b4385bc671c33762407b37d78163160d2c66383e Mon Sep 17 00:00:00 2001 From: shanshi <3316401569@qq.com> Date: Wed, 21 Oct 2026 17:57:13 +0800 Subject: [PATCH] =?UTF-8?q?=E8=B4=A2=E5=8A=A1=E5=8F=B0=E8=B4=A6=EF=BC=9A?= =?UTF-8?q?=E6=96=B0=E5=A2=9E=E4=B8=8D=E4=BF=9D=E7=95=99=E8=B4=A6=E6=88=B7?= =?UTF-8?q?=E5=A4=84=E7=90=86=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 | 223 ++++++++ .../FinanceLedgerAccountShouldKeep.php | 20 + ...eate_finance_ledger_account_keep_todos.php | 39 ++ .../View/FinanceLedgerPageData.php | 1 + .../pages/financial-ledger/ledger.blade.php | 1 + .../unretained-account-tasks.blade.php | 521 ++++++++++++++++++ routes/admin.php | 8 + .../FinanceLedgerAccountKeepTodoTest.php | 354 ++++++++++++ 8 files changed, 1167 insertions(+) create mode 100644 database/migrations/2026_10_21_170000_create_finance_ledger_account_keep_todos.php create mode 100644 resources/views/business/pages/financial-ledger/unretained-account-tasks.blade.php create mode 100644 tests/Feature/Business/FinanceLedgerAccountKeepTodoTest.php diff --git a/app/Http/Controllers/Business/FinanceLedger.php b/app/Http/Controllers/Business/FinanceLedger.php index 37737c0d..6fd7ce12 100644 --- a/app/Http/Controllers/Business/FinanceLedger.php +++ b/app/Http/Controllers/Business/FinanceLedger.php @@ -188,6 +188,229 @@ class FinanceLedger extends Controller ]); } + public function unretainedAccountTasks(Request $request) + { + $connection = DB::connection((string) config('upstream_mirrors.connection', 'business_mirror')); + $handlingMethods = [ + 'online' => '线上', + 'offline' => '线下', + ]; + $completionStatuses = [ + 'completed' => '已完成', + 'in_progress' => '进行中', + 'not_started' => '待开始', + ]; + $filters = [ + 'keep_short' => trim((string) $request->query('keep_short', '')), + 'keep_type' => trim((string) $request->query('keep_type', '')), + 'keep_status' => trim((string) $request->query('keep_status', '')), + 'keep_method' => trim((string) $request->query('keep_method', '')), + 'keep_owner' => trim((string) $request->query('keep_owner', '')), + 'keep_progress' => trim((string) $request->query('keep_progress', '')), + ]; + + $query = $connection->table('fin_accounts as accounts') + ->leftJoin('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_keep_todos as account_todo', 'account_todo.account_short', '=', 'accounts.account_short') + ->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.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', + ]); + FinanceLedgerAccountShouldKeep::applyUnretainedCondition( + $query, + 'accounts.account_should_keep', + 'accounts.company_plan', + ); + + if ($filters['keep_short'] !== '') { + $query->where('accounts.account_short', 'like', '%' . $filters['keep_short'] . '%'); + } + if ($filters['keep_type'] !== '') { + $query->where(function ($effectiveType) use ($filters): void { + $effectiveType->where(function ($baseType) use ($filters): void { + $baseType->whereNull('type_override.id'); + if ($filters['keep_type'] === '__empty__') { + $baseType->where(function ($blank): void { + $blank->whereNull('accounts.account_type')->orWhere('accounts.account_type', ''); + }); + } else { + $baseType->where('accounts.account_type', $filters['keep_type']); + } + })->orWhere(function ($overrideType) use ($filters): void { + $overrideType->whereNotNull('type_override.id'); + if ($filters['keep_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['keep_type']); + } + }); + }); + } + if ($filters['keep_status'] !== '' && array_key_exists($filters['keep_status'], FinanceLedgerAccountStatus::OPTIONS)) { + if ($filters['keep_status'] === FinanceLedgerAccountStatus::NORMAL) { + $query->where(function ($normalStatus): void { + $normalStatus->whereNull('account_status.status') + ->orWhere('account_status.status', FinanceLedgerAccountStatus::NORMAL); + }); + } else { + $query->where('account_status.status', $filters['keep_status']); + } + } + if ($filters['keep_method'] !== '' && array_key_exists($filters['keep_method'], $handlingMethods)) { + $query->where('account_todo.handling_method', $filters['keep_method']); + } + if ($filters['keep_owner'] !== '') { + $query->where('account_todo.owner_name', 'like', '%' . $filters['keep_owner'] . '%'); + } + if ($filters['keep_progress'] !== '' && array_key_exists($filters['keep_progress'], $completionStatuses)) { + if ($filters['keep_progress'] === 'not_started') { + $query->where(function ($notStarted): void { + $notStarted->whereNull('account_todo.current_progress') + ->orWhere('account_todo.current_progress', 'not_started'); + }); + } else { + $query->where('account_todo.current_progress', $filters['keep_progress']); + } + } + + $summaryQuery = clone $query; + $summary = [ + 'count' => (clone $query)->count(), + 'in_progress' => (clone $summaryQuery)->where('account_todo.current_progress', 'in_progress')->count(), + 'not_started' => (clone $summaryQuery)->where(function ($notStarted): void { + $notStarted->whereNull('account_todo.current_progress') + ->orWhere('account_todo.current_progress', 'not_started'); + })->count(), + 'completed' => (clone $summaryQuery)->where('account_todo.current_progress', 'completed')->count(), + ]; + + $sortField = (string) $request->query('keep_sort', ''); + $sortField = in_array($sortField, ['started_at', 'completed_at'], true) ? $sortField : ''; + $sortDirection = strtolower((string) $request->query('keep_direction', 'asc')); + $sortDirection = $sortDirection === 'desc' ? 'desc' : 'asc'; + if ($sortField !== '') { + $query->orderBy('account_todo.' . $sortField, $sortDirection); + } + + $rows = $query + ->orderBy('accounts.company_name') + ->orderBy('accounts.account_short') + ->paginate(50) + ->appends($filters) + ->appends(array_filter([ + 'keep_sort' => $sortField, + 'keep_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_current_progress = $row->todo_current_progress ?: '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(), + ); + $ownerOptions = $connection->table('fin_account_keep_todos') + ->whereNotNull('owner_name')->where('owner_name', '<>', '') + ->distinct()->orderBy('owner_name')->pluck('owner_name')->all(); + + return view('business.pages.financial-ledger.unretained-account-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' => FinanceLedgerAccountStatus::OPTIONS, + 'accountTypeOptions' => $typeOptions, + 'ownerOptions' => $ownerOptions, + ]); + } + + public function updateAccountKeepTodo(Request $request) + { + abort_unless(FinanceLedgerAccess::canEditAccountStatus(user()), 403, '只有财务台账管理员可以修改不保留账户待办。'); + + $data = $request->validate([ + 'account_short' => ['required', 'string', 'max:100'], + 'handling_method' => ['nullable', 'string', Rule::in(['online', 'offline'])], + 'owner_name' => ['nullable', 'string', 'max:100'], + 'started_at' => ['nullable', 'date'], + 'completed_at' => ['nullable', 'date'], + 'current_progress' => ['required', 'string', Rule::in(['completed', 'in_progress', 'not_started'])], + ]); + + $accountShort = trim((string) $data['account_short']); + $connection = DB::connection((string) config('upstream_mirrors.connection', 'business_mirror')); + $account = $connection->table('fin_accounts') + ->where('account_short', $accountShort) + ->first(['account_should_keep', 'company_plan']); + abort_if($account === null, 422, '账户简称不存在,请刷新待办清单后重试。'); + abort_unless( + FinanceLedgerAccountShouldKeep::display( + $account->account_should_keep ?? null, + $account->company_plan ?? null, + ) === FinanceLedgerAccountShouldKeep::NO, + 422, + '账户当前已设置为保留,无需继续填写不保留账户待办。', + ); + + $now = now(); + $values = [ + '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' => (string) $data['current_progress'], + 'updated_by' => (string) user()->name, + 'updated_at' => $now, + ]; + + $todos = $connection->table('fin_account_keep_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 updateAccountTodo(Request $request, FinanceLedgerCalculator $calculator) { abort_unless(FinanceLedgerAccess::canEditAccountStatus(user()), 403, '只有财务台账管理员可以修改待办事项。'); diff --git a/app/Utilities/FinanceLedgerAccountShouldKeep.php b/app/Utilities/FinanceLedgerAccountShouldKeep.php index bf928203..9efe79ea 100644 --- a/app/Utilities/FinanceLedgerAccountShouldKeep.php +++ b/app/Utilities/FinanceLedgerAccountShouldKeep.php @@ -2,6 +2,8 @@ namespace App\Utilities; +use Illuminate\Database\Query\Builder; + class FinanceLedgerAccountShouldKeep { public const YES = '是'; @@ -42,4 +44,22 @@ class FinanceLedgerAccountShouldKeep return str_contains($plan, '保留') ? self::YES : self::NO; } + + public static function applyUnretainedCondition( + Builder $query, + string $explicitColumn = 'account_should_keep', + string $companyPlanColumn = 'company_plan', + ): Builder { + return $query->where(function (Builder $keep) use ($explicitColumn, $companyPlanColumn): void { + $keep->where($explicitColumn, self::NO) + ->orWhere(function (Builder $inferred) use ($explicitColumn, $companyPlanColumn): void { + $inferred->where(function (Builder $blank) use ($explicitColumn): void { + $blank->whereNull($explicitColumn)->orWhere($explicitColumn, ''); + }) + ->whereNotNull($companyPlanColumn) + ->where($companyPlanColumn, '<>', '') + ->where($companyPlanColumn, 'not like', '%保留%'); + }); + }); + } } diff --git a/database/migrations/2026_10_21_170000_create_finance_ledger_account_keep_todos.php b/database/migrations/2026_10_21_170000_create_finance_ledger_account_keep_todos.php new file mode 100644 index 00000000..d9058edc --- /dev/null +++ b/database/migrations/2026_10_21_170000_create_finance_ledger_account_keep_todos.php @@ -0,0 +1,39 @@ +connection()); + if (! $schema->hasTable('fin_accounts') || $schema->hasTable('fin_account_keep_todos')) { + return; + } + + $schema->create('fin_account_keep_todos', function (Blueprint $table): void { + $table->bigIncrements('id'); + $table->string('account_short', 100)->unique('fin_account_keep_todos_account_unique'); + $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', 20)->default('not_started'); + $table->string('updated_by', 100)->nullable(); + $table->timestamps(); + $table->index(['current_progress'], 'fin_account_keep_todos_progress_index'); + }); + } + + public function down(): void + { + Schema::connection($this->connection())->dropIfExists('fin_account_keep_todos'); + } +}; diff --git a/modules/EcommerceFinance/View/FinanceLedgerPageData.php b/modules/EcommerceFinance/View/FinanceLedgerPageData.php index f9ad2be5..2c69a6a6 100644 --- a/modules/EcommerceFinance/View/FinanceLedgerPageData.php +++ b/modules/EcommerceFinance/View/FinanceLedgerPageData.php @@ -159,6 +159,7 @@ class FinanceLedgerPageData $summaryQuery = clone $query; $summary = [ 'count' => (clone $query)->count(), + 'unretained' => FinanceLedgerAccountShouldKeep::applyUnretainedCondition(clone $query)->count(), 'current_balance' => round((float) (clone $summaryQuery)->sum('current_balance'), 2), 'balance' => round((float) (clone $summaryQuery)->sum('daily_available'), 2), 'blocked' => round((float) (clone $summaryQuery)->sum('blocked_amount'), 2), diff --git a/resources/views/business/pages/financial-ledger/ledger.blade.php b/resources/views/business/pages/financial-ledger/ledger.blade.php index 78f06150..b64c3d88 100644 --- a/resources/views/business/pages/financial-ledger/ledger.blade.php +++ b/resources/views/business/pages/financial-ledger/ledger.blade.php @@ -263,6 +263,7 @@
账户数{{ number_format((int) ($ledgerAccountSummary['count'] ?? 0)) }}
+ 不保留账户数{{ number_format((int) ($ledgerAccountSummary['unretained'] ?? 0)) }}
可用金额合计(万元){{ $moneyWan($ledgerAccountSummary['balance'] ?? 0) }}
不可用金额合计(万元){{ $moneyWan($ledgerAccountSummary['blocked'] ?? 0) }}
金额口径正常计可用,异常计不可用
diff --git a/resources/views/business/pages/financial-ledger/unretained-account-tasks.blade.php b/resources/views/business/pages/financial-ledger/unretained-account-tasks.blade.php new file mode 100644 index 00000000..109846a6 --- /dev/null +++ b/resources/views/business/pages/financial-ledger/unretained-account-tasks.blade.php @@ -0,0 +1,521 @@ +@php + $dateTimeValue = static fn ($value): string => $value + ? \Illuminate\Support\Carbon::parse($value)->format('Y-m-d\TH:i') + : ''; +@endphp + + + 不保留账户处理待办 + + + + +
+
+
+

不保留账户处理待办

+

账户总表中“账户是否保留”为“否”的账户会自动进入本清单;不与其他页面的待办事项合并。

+
+ + + 返回账户总表 + +
+ +
+
待处理账户{{ number_format((int) ($summary['count'] ?? 0)) }}
+
进行中{{ number_format((int) ($summary['in_progress'] ?? 0)) }}
+
待开始{{ number_format((int) ($summary['not_started'] ?? 0)) }}
+
已完成{{ number_format((int) ($summary['completed'] ?? 0)) }}
+
+ + @if (session('success')) +
{{ session('success') }}
+ @endif + @if ($errors->any()) + + @endif + +
+ @if ($sortField !== '') + + + @endif +
+ + + + + + +
+
+ + + + 重置 + +
+
+ + @php + $sortQuery = request()->query(); + unset($sortQuery['page']); + $sortUrl = static fn (string $field): string => route( + 'business.financial-ledger.unretained-accounts', + array_merge($sortQuery, [ + 'keep_sort' => $field, + 'keep_direction' => $sortField === $field && $sortDirection === 'asc' ? 'desc' : 'asc', + ]), + ); + @endphp + +

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

+
+ + + + + + + + + + + + + + + + + + + + @forelse ($rows as $row) + + + + + + + + + + + + + + + + @empty + + @endforelse + +
公司名称账户简称账户类型账户状态账号开户行开户城市/网点账户是否保留办理方式责任人员 + + 开始时间 + + + + + 完成时间 + + + 当前进度
+
+ @csrf + +
+ {{ $row->company_name ?: '未填写' }} +
{{ $row->account_short ?: '未填写' }}{{ $row->account_type ?: '未填写' }}{{ $accountStatusOptions[$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 d8f33ca2..3662449a 100644 --- a/routes/admin.php +++ b/routes/admin.php @@ -705,10 +705,18 @@ Route::get('workspace/financial-operations/ledger/unavailable-balances', [\App\H ->middleware('business.page:financial-operations,ledger') ->name('business.financial-ledger.unavailable-balances'); +Route::get('workspace/financial-operations/ledger/unretained-accounts', [\App\Http\Controllers\Business\FinanceLedger::class, 'unretainedAccountTasks']) + ->middleware('business.page:financial-operations,ledger') + ->name('business.financial-ledger.unretained-accounts'); + 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::post('workspace/financial-ledger/accounts/keep-todo', [\App\Http\Controllers\Business\FinanceLedger::class, 'updateAccountKeepTodo']) + ->middleware('business.page:financial-operations,ledger') + ->name('business.financial-ledger.accounts.keep-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/FinanceLedgerAccountKeepTodoTest.php b/tests/Feature/Business/FinanceLedgerAccountKeepTodoTest.php new file mode 100644 index 00000000..43138400 --- /dev/null +++ b/tests/Feature/Business/FinanceLedgerAccountKeepTodoTest.php @@ -0,0 +1,354 @@ +user->attachPermission( + Permission::where('name', 'read-admin-panel')->firstOrFail() + ); + } + + private function ledgerUrl(): string + { + return route('business.pages.show', [ + 'company_id' => $this->company->id, + 'section' => 'financial-operations', + 'page' => 'ledger', + ]); + } + + private function keepTasksUrl(array $parameters = []): string + { + return route('business.financial-ledger.unretained-accounts', array_merge([ + 'company_id' => $this->company->id, + ], $parameters)); + } + + private function keepTodoUrl(): string + { + return route('business.financial-ledger.accounts.keep-todo', [ + 'company_id' => $this->company->id, + ]); + } + + public function test_ledger_card_follows_account_count_and_links_to_independent_keep_tasks(): void + { + DB::connection('business_mirror')->table('fin_accounts')->insert([ + [ + 'company_name' => '不保留测试公司甲', + 'account_short' => '显式不保留账户', + 'company_plan' => '保留', + 'account_should_keep' => '否', + ], + [ + 'company_name' => '不保留测试公司乙', + 'account_short' => '规划不保留账户', + 'company_plan' => '待注销', + 'account_should_keep' => null, + ], + [ + 'company_name' => '不保留测试公司丙', + 'account_short' => '明确保留账户', + 'company_plan' => '待注销', + 'account_should_keep' => '是', + ], + [ + 'company_name' => '不保留测试公司丁', + 'account_short' => '规划保留账户', + 'company_plan' => '保留', + 'account_should_keep' => null, + ], + ]); + + $response = $this->loginAs()->get($this->ledgerUrl())->assertOk(); + $content = $response->getContent(); + + $this->assertMatchesRegularExpression( + '/账户数<\/span>4<\/b><\/div>\s*]+aria-label="查看不保留账户处理待办"[^>]*>不保留账户数<\/span>2<\/b><\/a>/u', + $content, + ); + $response->assertSee('href="' . $this->keepTasksUrl() . '"', false); + } + + public function test_keep_tasks_page_uses_required_columns_filters_and_retention_rule(): void + { + $connection = DB::connection('business_mirror'); + $connection->table('fin_accounts')->insert([ + [ + 'company_name' => '展示测试公司甲', + 'account_short' => '显式不保留账户', + 'account_type' => '公户', + 'account_no' => '6222000000000001', + 'bank_name' => '展示测试银行甲', + 'bank_city_branch' => '上海 / 测试支行', + 'company_plan' => '保留', + 'account_should_keep' => '否', + ], + [ + 'company_name' => '展示测试公司乙', + 'account_short' => '规划不保留账户', + 'account_type' => '个人卡', + 'account_no' => null, + 'bank_name' => null, + 'bank_city_branch' => null, + 'company_plan' => '待注销', + 'account_should_keep' => null, + ], + [ + 'company_name' => '展示测试公司丙', + 'account_short' => '明确保留账户', + 'account_type' => '支付宝', + 'account_no' => null, + 'bank_name' => null, + 'bank_city_branch' => null, + 'company_plan' => '待注销', + 'account_should_keep' => '是', + ], + [ + 'company_name' => '展示测试公司丁', + 'account_short' => '规划保留账户', + 'account_type' => '网商银行', + 'account_no' => null, + 'bank_name' => null, + 'bank_city_branch' => null, + 'company_plan' => '保留', + 'account_should_keep' => null, + ], + ]); + $connection->table('fin_account_statuses')->insert([ + 'account_short' => '显式不保留账户', + 'status' => 'limited', + 'updated_by' => 'Test User', + ]); + + $response = $this->loginAs()->get($this->keepTasksUrl())->assertOk(); + $content = $response->getContent(); + + $response + ->assertSee('不保留账户处理待办') + ->assertSee('不与其他页面的待办事项合并') + ->assertSee('data-keep-account="显式不保留账户"', false) + ->assertSee('data-keep-account="规划不保留账户"', false) + ->assertDontSee('data-keep-account="明确保留账户"', false) + ->assertDontSee('data-keep-account="规划保留账户"', false) + ->assertSee('name="keep_short"', false) + ->assertSee('name="keep_type"', false) + ->assertSee('name="keep_status"', false) + ->assertSee('name="keep_method"', false) + ->assertSee('name="keep_owner"', false) + ->assertSee('name="keep_progress"', false) + ->assertSee('按开始时间排序') + ->assertSee('按完成时间排序') + ->assertSee('线上') + ->assertSee('线下') + ->assertSee('已完成') + ->assertSee('进行中') + ->assertSee('待开始') + ->assertSee('data-keep-tbody', false) + ->assertSee('data-keep-pagination', false) + ->assertSee('拖动表头右侧边界可调整列宽') + ->assertSee('cursor:col-resize', false) + ->assertSee('position:sticky', false) + ->assertSee('currentBody.replaceWith', false) + ->assertSee('history.replaceState', false) + ->assertSee('nextWrap.scrollTop = scrollTop', false); + + preg_match_all('/assertSame([ + 'company_name', + 'account_short', + 'account_type', + 'account_status', + 'account_no', + 'bank_name', + 'bank_city_branch', + 'account_should_keep', + 'handling_method', + 'owner_name', + 'started_at', + 'completed_at', + 'current_progress', + ], $matches[1]); + } + + public function test_keep_tasks_filters_sort_and_ajax_pagination_work(): void + { + $connection = DB::connection('business_mirror'); + $connection->table('fin_accounts')->insert([ + [ + 'company_name' => '筛选公司甲', + 'account_short' => '筛选账户甲', + 'account_type' => '公户', + 'company_plan' => '保留', + 'account_should_keep' => '否', + ], + [ + 'company_name' => '筛选公司乙', + 'account_short' => '筛选账户乙', + 'account_type' => '个人卡', + 'company_plan' => '待注销', + 'account_should_keep' => null, + ], + ]); + $connection->table('fin_account_statuses')->insert([ + 'account_short' => '筛选账户甲', + 'status' => 'limited', + 'updated_by' => 'Test User', + ]); + $connection->table('fin_account_keep_todos')->insert([ + [ + 'account_short' => '筛选账户甲', + 'handling_method' => 'online', + 'owner_name' => '甲负责人', + 'started_at' => '2026-10-21 09:00:00', + 'current_progress' => 'in_progress', + ], + [ + 'account_short' => '筛选账户乙', + 'handling_method' => 'offline', + 'owner_name' => '乙负责人', + 'started_at' => '2026-10-22 09:00:00', + 'current_progress' => 'not_started', + ], + ]); + + $this->loginAs()->get($this->keepTasksUrl([ + 'keep_short' => '筛选账户甲', + 'keep_type' => '公户', + 'keep_status' => 'limited', + 'keep_method' => 'online', + 'keep_owner' => '甲负责人', + 'keep_progress' => 'in_progress', + ])) + ->assertOk() + ->assertSee('data-keep-account="筛选账户甲"', false) + ->assertDontSee('data-keep-account="筛选账户乙"', false) + ->assertSee('value="limited" selected', false) + ->assertSee('value="online" selected', false) + ->assertSee('value="in_progress" selected', false); + + $sortResponse = $this->loginAs()->get($this->keepTasksUrl([ + 'keep_sort' => 'started_at', + 'keep_direction' => 'desc', + ]))->assertOk()->assertSee('aria-sort="descending"', false); + preg_match_all('/data-keep-account="([^"]+)"/u', $sortResponse->getContent(), $sortedAccounts); + $this->assertSame(['筛选账户乙', '筛选账户甲'], $sortedAccounts[1]); + + $pageRows = []; + for ($index = 1; $index <= 51; $index++) { + $pageRows[] = [ + 'company_name' => '分页公司', + 'account_short' => 'keep-page-' . str_pad((string) $index, 2, '0', STR_PAD_LEFT), + 'account_should_keep' => '否', + ]; + } + $connection->table('fin_accounts')->insert($pageRows); + + $this->loginAs()->get($this->keepTasksUrl(['keep_short' => 'keep-page-'])) + ->assertOk() + ->assertSee('1 / 2') + ->assertSee('aria-label="下一页"', false) + ->assertSee('data-keep-pagination', false); + $this->loginAs()->get($this->keepTasksUrl(['keep_short' => 'keep-page-', 'page' => 2])) + ->assertOk() + ->assertSee('2 / 2') + ->assertSee('data-keep-account="keep-page-51"', false); + } + + public function test_keep_todo_save_is_isolated_from_abnormal_tasks_and_permission_protected(): void + { + $connection = DB::connection('business_mirror'); + $connection->table('fin_accounts')->insert([ + [ + 'company_name' => '待办保存公司甲', + 'account_short' => '待办不保留账户', + 'company_plan' => '保留', + 'account_should_keep' => '否', + ], + [ + 'company_name' => '待办保存公司乙', + 'account_short' => '规划不保留待办账户', + 'company_plan' => '待注销', + 'account_should_keep' => null, + ], + [ + 'company_name' => '待办保存公司丙', + 'account_short' => '待办保留账户', + 'company_plan' => '保留', + 'account_should_keep' => '是', + ], + ]); + + $payload = [ + 'account_short' => '待办不保留账户', + 'handling_method' => 'online', + 'owner_name' => '待办负责人', + 'started_at' => '2026-10-21T09:00', + 'completed_at' => '2026-10-21T11:30', + 'current_progress' => 'in_progress', + ]; + $this->loginAs()->post($this->keepTodoUrl(), $payload)->assertRedirect(); + + $this->assertDatabaseHas('fin_account_keep_todos', [ + 'account_short' => '待办不保留账户', + 'handling_method' => 'online', + 'owner_name' => '待办负责人', + 'current_progress' => 'in_progress', + 'updated_by' => 'Test User', + ], 'business_mirror'); + $this->assertDatabaseCount('fin_account_todos', 0, 'business_mirror'); + + $this->loginAs()->post($this->keepTodoUrl(), array_merge($payload, [ + 'current_progress' => 'completed', + ]))->assertRedirect(); + $this->assertDatabaseCount('fin_account_keep_todos', 1, 'business_mirror'); + $this->assertDatabaseHas('fin_account_keep_todos', [ + 'account_short' => '待办不保留账户', + 'current_progress' => 'completed', + ], 'business_mirror'); + + $this->loginAs()->post($this->keepTodoUrl(), [ + 'account_short' => '规划不保留待办账户', + 'current_progress' => 'not_started', + ])->assertRedirect(); + $this->assertDatabaseHas('fin_account_keep_todos', [ + 'account_short' => '规划不保留待办账户', + 'current_progress' => 'not_started', + ], 'business_mirror'); + + $this->withExceptionHandling()->loginAs()->post($this->keepTodoUrl(), [ + 'account_short' => '待办保留账户', + 'current_progress' => 'not_started', + ])->assertStatus(422); + + $viewer = user_model_class()::create([ + 'name' => '不保留账户只读用户', + 'email' => 'keep-viewer-' . uniqid() . '@example.com', + 'password' => 'local-test-password', + 'locale' => 'zh-CN', + 'enabled' => 1, + 'landing_page' => 'dashboard', + 'created_from' => 'core::test', + ]); + $viewer->companies()->attach($this->company->id); + $viewer->roles()->attach(Role::where('name', 'user')->firstOrFail()->id); + $viewer->attachPermission(Permission::where('name', 'read-admin-panel')->firstOrFail()); + + $this->loginAs($viewer)->get($this->keepTasksUrl()) + ->assertOk() + ->assertSee('disabled', false) + ->assertDontSee('title="保存待办"', false); + $this->withExceptionHandling()->loginAs($viewer)->post($this->keepTodoUrl(), $payload) + ->assertForbidden(); + } +} -- 2.47.2