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, '只有财务台账管理员可以修改账户映射。'); $data = $request->validate([ 'account_short' => ['nullable', 'string', 'max:100'], ]); $account = trim((string) ($data['account_short'] ?? '')); $connection = DB::connection((string) config('upstream_mirrors.connection', 'business_mirror')); $row = $connection->table('fin_account_map')->where('id', $id)->first(); abort_if($row === null, 404); if ($account !== '') { $exists = $connection->table('fin_accounts')->where('account_short', $account)->exists(); abort_unless($exists, 422, '账户简称不存在,请从账户总表中选择。'); } $old = (string) ($row->account_short ?? ''); $connection->table('fin_account_map')->where('id', $id)->update([ 'account_short' => $account !== '' ? $account : null, 'updated_by' => (string) user()->name, 'updated_at' => now(), ]); $connection->table('fin_account_map_events')->insert([ 'source' => (string) $row->source, 'source_value' => (string) $row->source_value, 'old_account_short' => $old !== '' ? $old : null, 'new_account_short' => $account !== '' ? $account : null, 'user_id' => (int) user()->id, 'user_name' => (string) user()->name, 'created_at' => now(), ]); if ($request->boolean('recalculate', true)) { $calculator->recalculate(); } $mapped = $connection->table('fin_account_map') ->whereNotNull('account_short')->where('account_short', '<>', '')->count(); return response()->json([ 'ok' => true, 'account_short' => $account, 'mapped' => $mapped, 'total' => $connection->table('fin_account_map')->count(), ], 200, ['Cache-Control' => 'private, no-store']); } public function updateAccountStatus(Request $request): JsonResponse { abort_unless(FinanceLedgerAccess::canEditAccountStatus(user()), 403, '只有财务台账管理员可以修改账户状态。'); $data = $request->validate([ 'account_short' => ['required', 'string', 'max:100'], 'status' => ['required', 'string', Rule::in(array_keys(FinanceLedgerAccountStatus::OPTIONS))], ]); $accountShort = trim((string) $data['account_short']); abort_if($accountShort === '', 422, '账户简称不能为空。'); $connection = DB::connection((string) config('upstream_mirrors.connection', 'business_mirror')); abort_unless( $connection->table('fin_accounts')->where('account_short', $accountShort)->exists(), 422, '账户简称不存在,请刷新账户总表后重试。', ); $status = FinanceLedgerAccountStatus::normalize((string) $data['status']); $statuses = $connection->table('fin_account_statuses'); $existing = $statuses->where('account_short', $accountShort)->first(); $now = now(); $values = [ 'status' => $status, 'updated_by' => (string) user()->name, 'updated_at' => $now, ]; if ($existing === null) { $statuses->insert($values + [ 'account_short' => $accountShort, 'created_at' => $now, ]); } else { $statuses->where('account_short', $accountShort)->update($values); } return response()->json([ 'ok' => true, 'account_short' => $accountShort, 'status' => $status, 'label' => FinanceLedgerAccountStatus::label($status), ], 200, ['Cache-Control' => 'private, no-store']); } public function updateAccountType(Request $request): JsonResponse { abort_unless(FinanceLedgerAccess::canEditAccountType(user()), 403, '只有财务台账管理员可以修改账户类型。'); $data = $request->validate([ 'account_short' => ['required', 'string', 'max:100'], 'account_type' => ['nullable', 'string', 'max:50'], ]); $accountShort = trim((string) $data['account_short']); $accountType = trim((string) ($data['account_type'] ?? '')); abort_if($accountShort === '', 422, '账户简称不能为空。'); $connection = DB::connection((string) config('upstream_mirrors.connection', 'business_mirror')); abort_unless( $connection->table('fin_accounts')->where('account_short', $accountShort)->exists(), 422, '账户简称不存在,请刷新账户总表后重试。', ); if ($accountType !== '') { $allowedTypes = FinanceLedgerAccountType::options( $connection->table('fin_accounts') ->whereNotNull('account_type')->where('account_type', '<>', '') ->distinct()->pluck('account_type')->all(), $connection->table('fin_account_type_overrides') ->whereNotNull('account_type')->where('account_type', '<>', '') ->distinct()->pluck('account_type')->all(), ); abort_unless(in_array($accountType, $allowedTypes, true), 422, '请选择现有账户类型。'); } $overrides = $connection->table('fin_account_type_overrides'); $existing = $overrides->where('account_short', $accountShort)->first(); $now = now(); $values = [ 'account_type' => $accountType !== '' ? $accountType : null, 'updated_by' => (string) user()->name, 'updated_at' => $now, ]; if ($existing === null) { $overrides->insert($values + [ 'account_short' => $accountShort, 'created_at' => $now, ]); } else { $overrides->where('account_short', $accountShort)->update($values); } return response()->json([ 'ok' => true, 'account_short' => $accountShort, 'account_type' => $accountType, 'label' => $accountType !== '' ? $accountType : '未填写', ], 200, ['Cache-Control' => 'private, no-store']); } public function updateAccountPlan(Request $request): JsonResponse { abort_unless(FinanceLedgerAccess::canEditAccountStatus(user()), 403, '只有财务台账管理员可以修改公司规划。'); $data = $request->validate([ 'account_short' => ['required', 'string', 'max:100'], 'company_plan' => ['nullable', 'string', 'max:50'], ]); $accountShort = trim((string) $data['account_short']); $plan = trim((string) ($data['company_plan'] ?? '')); if ($accountShort === '') { abort(422, '账户简称不能为空。'); } $connection = DB::connection((string) config('upstream_mirrors.connection', 'business_mirror')); abort_unless( $connection->table('fin_accounts')->where('account_short', $accountShort)->exists(), 422, '账户简称不存在,请刷新账户总表后重试。', ); $allowedPlans = $connection->table('fin_accounts') ->whereNotNull('company_plan') ->where('company_plan', '<>', '') ->distinct() ->pluck('company_plan') ->map(static fn (mixed $value): string => trim((string) $value)) ->filter() ->values() ->all(); abort_unless($plan === '' || in_array($plan, $allowedPlans, true), 422, '请选择现有公司规划。'); $connection->table('fin_accounts')->where('account_short', $accountShort)->update([ 'company_plan' => $plan !== '' ? $plan : null, 'updated_at' => now(), ]); return response()->json([ 'ok' => true, 'account_short' => $accountShort, 'company_plan' => $plan, 'label' => $plan !== '' ? $plan : '未填写', ], 200, ['Cache-Control' => 'private, no-store']); } public function updateAccountKeep(Request $request): JsonResponse { abort_unless(FinanceLedgerAccess::canEditAccountStatus(user()), 403, '只有财务台账管理员可以修改账户是否保留。'); $data = $request->validate([ 'account_short' => ['required', 'string', 'max:100'], 'account_should_keep' => ['nullable', 'string', Rule::in(FinanceLedgerAccountShouldKeep::OPTIONS)], ]); $accountShort = trim((string) $data['account_short']); $keep = FinanceLedgerAccountShouldKeep::normalize((string) ($data['account_should_keep'] ?? '')); if ($accountShort === '') { abort(422, '账户简称不能为空。'); } $connection = DB::connection((string) config('upstream_mirrors.connection', 'business_mirror')); abort_unless( $connection->table('fin_accounts')->where('account_short', $accountShort)->exists(), 422, '账户简称不存在,请刷新账户总表后重试。', ); $connection->table('fin_accounts')->where('account_short', $accountShort)->update([ 'account_should_keep' => $keep !== '' ? $keep : null, 'updated_at' => now(), ]); return response()->json([ 'ok' => true, 'account_short' => $accountShort, 'account_should_keep' => $keep, 'label' => $keep !== '' ? $keep : '未填写', ], 200, ['Cache-Control' => 'private, no-store']); } public function previewShopMapping(FinanceLedgerShopMappingBulk $bulk): JsonResponse { abort_unless(FinanceLedgerAccess::canEditMapping(user()), 403, '只有财务台账管理员可以批量修改账户映射。'); $preview = $bulk->preview(); return response()->json([ 'ok' => true, 'total' => $preview['total'], 'matched' => $preview['matched'], 'unmatched' => $preview['unmatched'], 'sample' => array_slice($preview['candidates'], 0, 10), 'unmatched_sample' => array_slice($preview['unmatched_values'], 0, 10), ], 200, ['Cache-Control' => 'private, no-store']); } public function applyShopMapping(FinanceLedgerShopMappingBulk $bulk): JsonResponse { abort_unless(FinanceLedgerAccess::canEditMapping(user()), 403, '只有财务台账管理员可以批量修改账户映射。'); $result = $bulk->apply(user()); return response()->json([ 'ok' => true, 'total' => $result['total'], 'mapped' => $result['mapped'], 'unmatched' => $result['unmatched'], ], 200, ['Cache-Control' => 'private, no-store']); } public function previewPayableMapping(FinanceLedgerPayableMappingBulk $bulk): JsonResponse { abort_unless(FinanceLedgerAccess::canEditMapping(user()), 403, '只有财务台账管理员可以批量修改账户映射。'); $preview = $bulk->preview(); return response()->json([ 'ok' => true, 'total' => $preview['total'], 'eligible' => $preview['eligible'], 'ambiguous' => $preview['ambiguous'], 'missing' => $preview['missing'], 'sample' => array_slice($preview['candidates'], 0, 10), 'ambiguous_sample' => array_slice($preview['ambiguous_values'], 0, 10), 'missing_sample' => array_slice($preview['missing_values'], 0, 10), ], 200, ['Cache-Control' => 'private, no-store']); } public function applyPayableMapping(FinanceLedgerPayableMappingBulk $bulk): JsonResponse { abort_unless(FinanceLedgerAccess::canEditMapping(user()), 403, '只有财务台账管理员可以批量修改账户映射。'); $result = $bulk->apply(user()); return response()->json([ 'ok' => true, 'total' => $result['total'], 'mapped' => $result['mapped'], 'ambiguous' => $result['ambiguous'], 'missing' => $result['missing'], ], 200, ['Cache-Control' => 'private, no-store']); } }