CaiWuXiTong/app/Http/Controllers/Business/CourierQuoteChanges.php

250 lines
9.4 KiB
PHP

<?php
namespace App\Http\Controllers\Business;
use App\Models\Business\CourierQuoteChange;
use App\Services\Esign\EsignClient;
use App\Support\CourierQuoteChangeService;
use App\Support\CourierQuoteSignatureService;
use App\Support\QuoteManagementSnapshot;
use App\Utilities\SystemAccess;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Routing\Controller;
use Illuminate\View\View;
class CourierQuoteChanges extends Controller
{
public function __construct(
private readonly CourierQuoteChangeService $service,
private readonly QuoteManagementSnapshot $snapshot,
private readonly CourierQuoteSignatureService $signatureService,
) {
}
public function index(): View
{
$changes = CourierQuoteChange::query()
->with(['items', 'version'])
->latest('updated_at')
->paginate(20);
return view('business.pages.courier-payables.quote-change-history', [
'changes' => $changes,
'canReview' => $this->canReview(),
]);
}
public function create(Request $request): View
{
$quotes = $this->snapshot->load()['quotes'] ?? [];
abort_if($quotes === [], 422, '尚未导入可用于变更的报价版本。');
$selectedId = (string) $request->query('source_quote_id', $quotes[0]['id']);
$source = $this->service->findSourceQuote($selectedId);
return view('business.pages.courier-payables.quote-change-editor', $this->editorData(null, $quotes, $source));
}
public function store(Request $request): RedirectResponse
{
$change = $this->service->create($this->validatedChange($request, true), user_id());
return redirect()->route('business.courier-quote-changes.edit', $change)
->with('success', "已保存变更草稿 {$change->number}。原始报价未被修改。");
}
public function show(CourierQuoteChange $quoteChange): View
{
$quoteChange->load(['items', 'events.actor', 'version.signature']);
return view('business.pages.courier-payables.quote-change-show', [
'change' => $quoteChange,
'canReview' => $this->canReview() && $quoteChange->created_by !== user_id(),
'canManageSignature' => $this->canReview(),
'esignConfigured' => EsignClient::configured(),
'esignEnvironment' => (string) config('services.esign.environment', 'sandbox'),
]);
}
public function edit(CourierQuoteChange $quoteChange): View
{
abort_unless($quoteChange->isEditable() && $quoteChange->created_by === user_id(), 403);
$quotes = $this->snapshot->load()['quotes'] ?? [];
$source = $this->service->findSourceQuote($quoteChange->source_quote_id);
$quoteChange->load('items');
return view('business.pages.courier-payables.quote-change-editor', $this->editorData($quoteChange, $quotes, $source));
}
public function update(Request $request, CourierQuoteChange $quoteChange): RedirectResponse
{
$change = $this->service->update($quoteChange, $this->validatedChange($request, false), user_id());
return redirect()->route('business.courier-quote-changes.edit', $change)
->with('success', "已保存变更草稿 {$change->number}。");
}
public function submit(CourierQuoteChange $quoteChange): RedirectResponse
{
$change = $this->service->submit($quoteChange, user_id());
return redirect()->route('business.courier-quote-changes.show', $change)
->with('success', "{$change->number} 已提交审核。");
}
public function approve(CourierQuoteChange $quoteChange): RedirectResponse
{
abort_unless($this->canReview(), 403, '当前账号没有报价审核权限。');
$change = $this->service->approve($quoteChange, user_id());
$message = "审核完成,已生成新报价版本 {$change->version->version_code}。";
try {
$this->signatureService->prepareVersion($change->version, user_id());
$message .= ' 签署 PDF 已生成并固定校验值。';
} catch (\Throwable $exception) {
report($exception);
$message .= ' 签署 PDF 生成失败,可在详情页重试。';
}
if ($change->isRetrospective()) {
$message .= ' 历史生效已登记,但补差核算尚未执行,请查看核算状态和阻断原因。';
}
return redirect()->route('business.courier-quote-changes.show', $change)
->with('success', $message);
}
public function returnForChanges(Request $request, CourierQuoteChange $quoteChange): RedirectResponse
{
abort_unless($this->canReview(), 403, '当前账号没有报价审核权限。');
$validated = $request->validate([
'review_note' => ['required', 'string', 'min:2', 'max:1000'],
]);
$change = $this->service->returnForChanges($quoteChange, $validated['review_note'], user_id());
return redirect()->route('business.courier-quote-changes.show', $change)
->with('success', "{$change->number} 已退回发起人修改。");
}
private function validatedChange(Request $request, bool $requiresSource): array
{
$rules = [
'change_type' => ['required', 'in:费率调整,面单调整,规则备注,执行日期,其他'],
'effective_date' => ['required', 'date_format:Y-m-d'],
'summary' => ['required', 'string', 'min:2', 'max:2000'],
'items' => ['required', 'array', 'min:1', 'max:500'],
'items.*.source_cell' => ['required', 'regex:/^[A-Z]+[1-9][0-9]*$/'],
'items.*.source_row' => ['required', 'integer', 'min:0'],
'items.*.source_column' => ['required', 'integer', 'min:0'],
'items.*.region' => ['nullable', 'string', 'max:255'],
'items.*.weight_band' => ['nullable', 'string', 'max:255'],
'items.*.charge_item' => ['nullable', 'string', 'max:255'],
'items.*.original_amount' => ['required', 'numeric', 'min:0', 'decimal:0,4'],
'items.*.new_amount' => ['required', 'numeric', 'min:0', 'decimal:0,4'],
'items.*.unit' => ['nullable', 'string', 'max:32'],
'items.*.note' => ['nullable', 'string', 'max:500'],
];
if ($requiresSource) {
$rules['source_quote_id'] = ['required', 'string', 'max:32'];
}
return $request->validate($rules);
}
private function editorData(?CourierQuoteChange $change, array $quotes, array $source): array
{
$existingItems = $change?->items->keyBy('source_cell') ?? collect();
return [
'change' => $change,
'quotes' => $quotes,
'source' => $source,
'editableCells' => $this->editableCells($source, $existingItems),
];
}
private function editableCells(array $source, $existingItems): array
{
$values = $source['values'];
$cells = [];
foreach ($values as $rowIndex => $row) {
foreach ($row as $columnIndex => $value) {
if (! is_numeric($value)) {
continue;
}
$formula = $source['formulas'][$rowIndex][$columnIndex] ?? null;
if (is_string($formula) && trim($formula) !== '') {
continue;
}
$cell = $this->columnName($columnIndex) . ($rowIndex + 1);
$existing = $existingItems->get($cell);
$cells[] = [
'source_cell' => $cell,
'source_row' => $rowIndex,
'source_column' => $columnIndex,
'region' => $this->nearestText($row, $columnIndex - 1) ?? $this->nearestText($row, 0),
'weight_band' => $this->headerText($values, $rowIndex, $columnIndex),
'charge_item' => $this->headerText($values, $rowIndex, $columnIndex),
'original_amount' => number_format((float) $value, 4, '.', ''),
'new_amount' => $existing?->new_amount ?? number_format((float) $value, 4, '.', ''),
'unit' => $existing?->unit,
'note' => $existing?->note,
'selected' => $existing !== null,
];
}
}
return $cells;
}
private function headerText(array $values, int $row, int $column): ?string
{
for ($index = $row - 1; $index >= 0; $index--) {
$candidate = $values[$index][$column] ?? null;
if (is_string($candidate) && trim($candidate) !== '') {
return trim($candidate);
}
}
return null;
}
private function nearestText(array $row, int $column): ?string
{
for ($index = $column; $index >= 0; $index--) {
$candidate = $row[$index] ?? null;
if (is_string($candidate) && trim($candidate) !== '') {
return trim($candidate);
}
}
return null;
}
private function columnName(int $column): string
{
$name = '';
do {
$name = chr(65 + ($column % 26)) . $name;
$column = intdiv($column, 26) - 1;
} while ($column >= 0);
return $name;
}
private function canReview(): bool
{
return in_array(SystemAccess::roleName(user()), [SystemAccess::SUPER_ADMIN, SystemAccess::ADMIN], true);
}
}