middleware('auth'); $this->middleware('company.identify'); } public function index(Request $request): View { $tenantId = (int) (company_id() ?: 1); $period = $this->period($request); $companyPlanSyncError = $this->syncCompanyPlan($tenantId); $serviceCompanies = $this->repository->serviceCompanies($tenantId, $period); return view('business.pages.financial-operations.service-company-reconciliation', [ 'isDetail' => false, 'period' => $period, 'periodOptions' => $this->periodOptions(), 'serviceCompanies' => $serviceCompanies, 'serviceCompany' => null, 'relations' => [], 'settlements' => [], 'assignedPeople' => [], 'availablePeople' => [], 'storeCompanies' => [], 'serviceTypes' => ServiceCompanyReconciliationRepository::SERVICE_TYPES, 'baseMetrics' => ServiceCompanyReconciliationRepository::BASE_METRICS, 'companyDirectory' => $this->repository->companyDirectory(), 'companyPlanSyncError' => $companyPlanSyncError, 'canEdit' => FinanceLedgerAccess::canEditMapping(user()), 'summary' => [ 'companies' => count($serviceCompanies), 'people' => array_sum(array_column($serviceCompanies, 'people_count')), 'labor_cost' => array_sum(array_column($serviceCompanies, 'labor_cost')), 'settlement_amount' => array_sum(array_column($serviceCompanies, 'settlement_amount')), 'pending' => count(array_filter( $serviceCompanies, static fn (array $row): bool => in_array( $row['reconciliation_status'], ['待生成', '待确认', '未配置'], true, ), )), ], ]); } public function show(Request $request, int $serviceCompany): View { $tenantId = (int) (company_id() ?: 1); $companyPlanSyncError = $this->syncCompanyPlan($tenantId); $company = $this->repository->serviceCompany($tenantId, $serviceCompany); abort_if($company === null, 404); $period = $this->period($request); $relations = $this->repository->relations($tenantId, $serviceCompany); $settlements = $this->repository->settlements($tenantId, $serviceCompany, $period); $assignedPeople = $this->repository->assignedPeople($tenantId, $serviceCompany); $availablePeople = array_values($this->repository->availablePeople($tenantId)); $periodStart = CarbonImmutable::createFromFormat('!Y-m', $period)->startOfMonth(); $periodEnd = $periodStart->endOfMonth(); $currentPersonKeys = array_column( array_filter($assignedPeople, static fn (array $row): bool => $row['is_current']), 'person_key', ); return view('business.pages.financial-operations.service-company-reconciliation', [ 'isDetail' => true, 'period' => $period, 'periodOptions' => $this->periodOptions(), 'serviceCompanies' => [], 'serviceCompany' => $company, 'relations' => $relations, 'settlements' => $settlements, 'assignedPeople' => $assignedPeople, 'availablePeople' => $availablePeople, 'currentPersonKeys' => array_flip($currentPersonKeys), 'storeCompanies' => $this->repository->storeCompanies($tenantId), 'serviceTypes' => ServiceCompanyReconciliationRepository::SERVICE_TYPES, 'baseMetrics' => ServiceCompanyReconciliationRepository::BASE_METRICS, 'companyDirectory' => $this->repository->companyDirectory(), 'companyPlanSyncError' => $companyPlanSyncError, 'canEdit' => FinanceLedgerAccess::canEditMapping(user()), 'summary' => [ 'people' => count($currentPersonKeys), 'labor_cost' => array_sum(array_column( array_filter($assignedPeople, static fn (array $row): bool => $row['is_current']), 'salary', )), 'stores' => count(array_filter( $relations, static fn (array $row): bool => $row['status'] === 'active' && $row['effective_from'] <= $periodEnd->toDateString() && ($row['effective_to'] === null || $row['effective_to'] >= $periodStart->toDateString()), )), 'settlement_amount' => array_sum(array_column($settlements, 'service_fee')), ], ]); } public function storeCompany(Request $request): RedirectResponse { $this->authorizeEdit(); $validated = $this->validateCompany($request); $company = $this->repository->saveServiceCompany( (int) (company_id() ?: 1), $validated, (string) user()->name, ); return redirect() ->route('business.service-company-reconciliation.show', [ 'serviceCompany' => $company['id'], ]) ->with('success', '服务公司已登记。'); } public function updateCompany(Request $request, int $serviceCompany): RedirectResponse { $this->authorizeEdit(); $validated = $this->validateCompany($request); $this->repository->saveServiceCompany( (int) (company_id() ?: 1), $validated, (string) user()->name, $serviceCompany, ); return back()->with('success', '服务公司信息已更新。'); } public function storeRelation(Request $request, int $serviceCompany): RedirectResponse { $this->authorizeEdit(); $validated = $this->validateRelation($request); $this->repository->saveRelation( (int) (company_id() ?: 1), $serviceCompany, $validated, (string) user()->name, ); return back()->with('success', '店铺公司结算关系已保存。'); } public function updateRelation( Request $request, int $serviceCompany, int $relation, ): RedirectResponse { $this->authorizeEdit(); $validated = $this->validateRelation($request); $this->repository->saveRelation( (int) (company_id() ?: 1), $serviceCompany, $validated, (string) user()->name, $relation, ); return back()->with('success', '店铺公司结算关系已更新并生成新版本。'); } public function assignPerson(Request $request, int $serviceCompany): RedirectResponse { $this->authorizeEdit(); $validated = $request->validate([ 'person_key' => ['required', 'string', 'max:191'], 'effective_from' => ['required', 'date_format:Y-m-d'], ]); $this->repository->assignPerson( (int) (company_id() ?: 1), $serviceCompany, $validated['person_key'], $validated['effective_from'], (string) user()->name, ); return back()->with('success', '人员归属已保存。'); } public function endPerson(Request $request, int $serviceCompany, int $assignment): RedirectResponse { $this->authorizeEdit(); $validated = $request->validate([ 'effective_to' => ['required', 'date_format:Y-m-d'], ]); $this->repository->endPersonAssignment( (int) (company_id() ?: 1), $serviceCompany, $assignment, $validated['effective_to'], ); return back()->with('success', '人员归属已结束。'); } public function generateSettlements(Request $request, int $serviceCompany): RedirectResponse { $this->authorizeEdit(); $validated = $request->validate([ 'period' => ['required', 'date_format:Y-m'], ]); $result = $this->repository->generateSettlements( (int) (company_id() ?: 1), $serviceCompany, $validated['period'], (string) user()->name, ); $message = "已生成 {$result['generated']} 条月度对账预览。"; if ($result['errors'] !== []) { $message .= ' 未生成:' . implode(';', array_slice($result['errors'], 0, 5)); } return redirect() ->route('business.service-company-reconciliation.show', [ 'serviceCompany' => $serviceCompany, 'period' => $validated['period'], 'tab' => 'reconciliation', ]) ->with($result['generated'] > 0 ? 'success' : 'error', $message); } public function updateSettlement( Request $request, int $serviceCompany, int $settlement, ): RedirectResponse { $this->authorizeEdit(); $validated = $request->validate([ 'action' => ['nullable', 'in:confirm,save'], 'invoice_status' => ['nullable', 'in:pending,partial,invoiced,waived'], 'invoice_amount' => ['nullable', 'numeric', 'min:0'], 'payment_status' => ['nullable', 'in:pending,partial,paid'], 'paid_amount' => ['nullable', 'numeric', 'min:0'], 'notes' => ['nullable', 'string', 'max:1000'], ]); $this->repository->updateSettlement( (int) (company_id() ?: 1), $serviceCompany, $settlement, $validated, (string) user()->name, ); return back()->with('success', '开票与到账状态已更新。'); } private function period(Request $request): string { $period = trim((string) $request->query('period', '')); if (preg_match('/^\d{4}-(0[1-9]|1[0-2])$/', $period) === 1) { return $period; } return CarbonImmutable::now()->subMonthNoOverflow()->format('Y-m'); } /** @return list */ private function periodOptions(): array { $current = CarbonImmutable::now()->startOfMonth(); return collect(range(0, 11))->map(static function (int $monthsAgo) use ($current): array { $month = $current->subMonthsNoOverflow($monthsAgo); return [ 'value' => $month->format('Y-m'), 'label' => $month->format('Y年n月'), ]; })->all(); } private function authorizeEdit(): void { abort_unless(FinanceLedgerAccess::canEditMapping(user()), 403, '当前账号没有维护服务公司对账的权限。'); } private function syncCompanyPlan(int $tenantId): ?string { try { $rows = $this->companyPlanRepository->serviceCompanyRows($tenantId); $this->repository->syncCompanyPlanServiceCompanies($tenantId, $rows); return null; } catch (\Throwable $exception) { report($exception); return '公司盘点服务公司同步暂时失败,当前先显示已登记的服务公司。'; } } /** @return array */ private function validateCompany(Request $request): array { return $request->validate([ 'company_name' => ['required', 'string', 'max:200'], 'unified_social_credit_code' => ['nullable', 'string', 'max:64'], 'service_type_code' => ['required', 'in:' . implode(',', array_keys( ServiceCompanyReconciliationRepository::SERVICE_TYPES, ))], 'taxpayer_type' => ['nullable', 'string', 'max:64'], 'business_registration_type' => ['nullable', 'string', 'max:64'], 'company_status' => ['nullable', 'in:正常,注销中,已注销'], 'planning_target_amount' => ['nullable', 'numeric', 'min:0'], 'status' => ['required', 'in:active,inactive'], 'notes' => ['nullable', 'string', 'max:500'], ]); } /** @return array */ private function validateRelation(Request $request): array { return $request->validate([ 'store_company_key' => ['required', 'string', 'max:191'], 'base_metric' => ['required', 'in:' . implode(',', array_keys( ServiceCompanyReconciliationRepository::BASE_METRICS, ))], 'fee_rate_percent' => ['required', 'numeric', 'min:0', 'max:100'], 'effective_from' => ['required', 'date_format:Y-m-d'], 'effective_to' => ['nullable', 'date_format:Y-m-d', 'after_or_equal:effective_from'], 'contract_no' => ['nullable', 'string', 'max:64'], 'contract_status' => ['required', 'in:pending,stamped,waived'], 'status' => ['required', 'in:active,inactive'], 'notes' => ['nullable', 'string', 'max:500'], ]); } }