347 lines
14 KiB
PHP
347 lines
14 KiB
PHP
<?php
|
||
|
||
namespace App\Http\Controllers\Business;
|
||
|
||
use App\Business\CompanyPlan\CompanyPlanRepository;
|
||
use App\Business\ServiceCompanyReconciliation\ServiceCompanyReconciliationRepository;
|
||
use App\Utilities\FinanceLedgerAccess;
|
||
use Carbon\CarbonImmutable;
|
||
use Illuminate\Http\RedirectResponse;
|
||
use Illuminate\Http\Request;
|
||
use Illuminate\Routing\Controller;
|
||
use Illuminate\View\View;
|
||
|
||
class ServiceCompanyReconciliation extends Controller
|
||
{
|
||
public function __construct(
|
||
private readonly ServiceCompanyReconciliationRepository $repository,
|
||
private readonly CompanyPlanRepository $companyPlanRepository,
|
||
) {
|
||
$this->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<array{value:string,label:string}> */
|
||
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<string,mixed> */
|
||
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<string,mixed> */
|
||
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'],
|
||
]);
|
||
}
|
||
}
|