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, '只有财务台账管理员可以修改账户映射。'); $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']); } }