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; } }
账户状态为“网银异常”“限额”或“工商注销”时自动进入本清单;不与其他页面的待办事项合并。
+拖动表头右侧边界可调整列宽,双击边界恢复默认
+| 公司名称 | +账户简称 | +账户类型 | +账户状态 | +账号 | +开户行 | +开户城市/网点 | +异常原因 | +办理方式 | +责任人员 | ++ + 开始时间 + + + | ++ + 完成时间 + + + | +当前进度 | +是否完成 | +
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| + + {{ $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
+
+ |
+ + + | +
| 当前没有账户异常待办 | |||||||||||||