|null */ public ?array $settlementSummary = null; public ?string $settlementMessage = null; public ?string $settlementError = null; public ?string $settlementDetailPeriod = null; public string $settlementDetailDate = ''; /** @var list */ public array $settlementDetailDates = []; public ?string $settlementDetailMessage = null; public function mount(): void { $this->ensureAuthenticated(); $this->settlementPeriod = now()->startOfMonth()->subMonth()->format('Y-m'); } public function calculateStorageFee(): void { $this->ensureAuthenticated(); $this->settlementMessage = null; $this->settlementError = null; $this->validate([ 'settlementPeriod' => ['required', 'regex:/^\d{4}-(0[1-9]|1[0-2])$/'], ], [ 'settlementPeriod.regex' => '结算月份格式应为 YYYY-MM。', ]); try { $result = app(ThirdPartyWarehouseStorageFeeCalculator::class) ->calculate(self::WAREHOUSE_ID, $this->warehouseName, $this->settlementPeriod); } catch (\Symfony\Component\HttpKernel\Exception\HttpException $e) { $this->settlementError = $e->getMessage(); return; } $summary = $result['summary']; ThirdPartyWarehouseStorageSettlement::query()->updateOrCreate( [ 'company_id' => (int) company_id(), 'warehouse_id' => self::WAREHOUSE_ID, 'period' => $this->settlementPeriod, ], [ 'warehouse_name' => $this->warehouseName, 'quote_id' => $summary['quote_id'] ?? null, 'inbound_rate_per_ton' => $summary['inbound_rate_per_ton'], 'outbound_rate_per_order' => $summary['outbound_rate_per_order'], 'pieces_per_pallet' => $summary['pieces_per_pallet'], 'pallet_area_sqm' => $summary['pallet_area_sqm'], 'storage_rate_per_sqm_month' => $summary['storage_rate_per_sqm_month'], 'days_in_month' => $summary['days_in_month'], 'inbound_tons' => $summary['inbound_tons'], 'outbound_qty' => $summary['outbound_qty'], 'inbound_fee' => $summary['inbound_fee'], 'outbound_fee' => $summary['outbound_fee'], 'storage_fee' => $summary['storage_fee'], 'memo_fee' => $summary['memo_fee'], 'loss_fee' => $summary['loss_fee'], 'compensation_fee' => $summary['compensation_fee'], 'total_fee' => $summary['total_fee'], 'detail_json' => [ 'daily' => $result['daily'], 'per_sku' => $result['per_sku'], 'daily_sku' => $result['daily_sku'], ], 'calculated_by' => (int) user_id(), ], ); $this->settlementSummary = $summary; $this->settlementMessage = $this->settlementPeriod . ' 仓储费核算完成,已存档。'; } public function exportSettlement(string $period): ?BinaryFileResponse { $this->ensureAuthenticated(); $settlement = ThirdPartyWarehouseStorageSettlement::query() ->where('company_id', (int) company_id()) ->where('warehouse_id', self::WAREHOUSE_ID) ->where('period', $period) ->first(); if ($settlement === null) { $this->settlementError = '未找到 ' . $period . ' 的核算存档,请先核算。'; return null; } $detail = $settlement->detail_json ?: ['daily' => [], 'per_sku' => [], 'daily_sku' => []]; $result = [ 'summary' => $this->summaryFromSettlement($settlement), 'daily' => $detail['daily'] ?? [], 'per_sku' => $detail['per_sku'] ?? [], 'daily_sku' => $detail['daily_sku'] ?? [], ]; $fileName = $settlement->warehouse_name . '仓储费用_' . $period . '.xlsx'; return Excel::download(new ThirdPartyWarehouseStorageSettlementExport($result), $fileName); } public function exportSettlementDetails(string $period): ?BinaryFileResponse { $this->ensureAuthenticated(); abort_unless(preg_match('/^\d{4}-(0[1-9]|1[0-2])$/', $period) === 1, 422); $settlement = $this->settlementForPeriod($period); if ($settlement === null) { $this->settlementError = '未找到 ' . $period . ' 的核算存档,请先核算。'; return null; } $dailySku = ($settlement->detail_json ?: [])['daily_sku'] ?? []; if ($dailySku === []) { $this->showMissingSettlementDetails($period); return null; } $fileName = $settlement->warehouse_name . '每日商品仓储费明细_' . $period . '.xlsx'; return Excel::download(new ThirdPartyWarehouseStorageDetailExport($dailySku), $fileName); } public function viewSettlementDetails(string $period): void { $this->ensureAuthenticated(); abort_unless(preg_match('/^\d{4}-(0[1-9]|1[0-2])$/', $period) === 1, 422); $this->settlementDetailPeriod = $period; $this->settlementDetailDate = ''; $this->settlementDetailDates = []; $this->settlementDetailMessage = null; $settlement = $this->settlementForPeriod($period); if ($settlement === null) { $this->settlementDetailMessage = '未找到 ' . $period . ' 的核算存档。'; $this->dispatch('warehouse-settlement-details-opened'); return; } $detail = $settlement->detail_json ?: []; $dailySku = $detail['daily_sku'] ?? []; if ($dailySku === []) { $this->showMissingSettlementDetails($period); return; } $dates = array_values(array_unique(array_column($dailySku, 'date'))); sort($dates); $this->settlementDetailDates = $dates; $this->settlementDetailDate = $dates[0] ?? ''; $this->dispatch('warehouse-settlement-details-opened'); } public function closeSettlementDetails(): void { $this->settlementDetailPeriod = null; $this->settlementDetailDate = ''; $this->settlementDetailDates = []; $this->settlementDetailMessage = null; } public function render(): View { $this->ensureAuthenticated(); $history = ThirdPartyWarehouseStorageSettlement::query() ->where('company_id', (int) company_id()) ->where('warehouse_id', self::WAREHOUSE_ID) ->orderByDesc('period') ->limit(24) ->get() ->map(fn (ThirdPartyWarehouseStorageSettlement $row): array => [ 'period' => $row->period, 'total_fee' => (float) $row->total_fee, 'inbound_fee' => (float) $row->inbound_fee, 'outbound_fee' => (float) $row->outbound_fee, 'storage_fee' => (float) $row->storage_fee, 'calculated_at' => optional($row->updated_at)->format('Y-m-d H:i'), ]) ->all(); $settlementDetail = $this->settlementDetailData(); return view('business.pages.courier-payables.partials.warehouse-freight', [ 'settlementHistory' => $history, 'settlementDetailRows' => $settlementDetail['rows'], 'settlementDetailSummary' => $settlementDetail['summary'], ]); } /** @return array */ private function summaryFromSettlement(ThirdPartyWarehouseStorageSettlement $s): array { return [ 'period' => $s->period, 'warehouse_id' => (int) $s->warehouse_id, 'warehouse_name' => $s->warehouse_name, 'quote_id' => $s->quote_id, 'days_in_month' => (int) $s->days_in_month, 'inbound_rate_per_ton' => (float) $s->inbound_rate_per_ton, 'outbound_rate_per_order' => (float) $s->outbound_rate_per_order, 'pieces_per_pallet' => (float) $s->pieces_per_pallet, 'pallet_area_sqm' => (float) $s->pallet_area_sqm, 'storage_rate_per_sqm_month' => (float) $s->storage_rate_per_sqm_month, 'inbound_tons' => (float) $s->inbound_tons, 'inbound_fee' => (float) $s->inbound_fee, 'outbound_qty' => (float) $s->outbound_qty, 'outbound_fee' => (float) $s->outbound_fee, 'storage_fee' => (float) $s->storage_fee, 'memo_fee' => (float) $s->memo_fee, 'loss_fee' => (float) $s->loss_fee, 'compensation_fee' => (float) $s->compensation_fee, 'total_fee' => (float) $s->total_fee, ]; } /** * 明细行只参与当前服务端渲染,不进入 Livewire 公共快照,避免日期切换时回传整张表。 * * @return array{rows:list>,summary:array|null} */ private function settlementDetailData(): array { $empty = ['rows' => [], 'summary' => null]; if ($this->settlementDetailPeriod === null || ! in_array($this->settlementDetailDate, $this->settlementDetailDates, true)) { return $empty; } $settlement = $this->settlementForPeriod($this->settlementDetailPeriod); if ($settlement === null) { return $empty; } $detail = $settlement->detail_json ?: []; $rows = array_values(array_filter( $detail['daily_sku'] ?? [], fn (array $row): bool => ($row['date'] ?? null) === $this->settlementDetailDate, )); usort($rows, static fn (array $left, array $right): int => strcmp( (string) ($left['sku_code'] ?? ''), (string) ($right['sku_code'] ?? ''), )); return [ 'rows' => $rows, 'summary' => [ 'sku_count' => count($rows), 'opening' => array_sum(array_column($rows, 'opening')), 'net_movement' => array_sum(array_column($rows, 'net_movement')), 'closing' => array_sum(array_column($rows, 'closing')), 'storage_fee' => array_sum(array_column($rows, 'storage_fee')), ], ]; } private function settlementForPeriod(string $period): ?ThirdPartyWarehouseStorageSettlement { return ThirdPartyWarehouseStorageSettlement::query() ->where('company_id', (int) company_id()) ->where('warehouse_id', self::WAREHOUSE_ID) ->where('period', $period) ->first(); } private function showMissingSettlementDetails(string $period): void { $this->settlementPeriod = $period; $this->settlementDetailPeriod = $period; $this->settlementDetailDate = ''; $this->settlementDetailDates = []; $this->settlementDetailMessage = $period . ' 的原存档没有每日商品明细,请重新核算该月份后查看和导出。'; $this->dispatch('warehouse-settlement-details-opened'); } private function ensureAuthenticated(): void { abort_unless(auth()->check(), 401); $section = collect(SidebarPermissions::navigationFor(user()))->firstWhere('slug', 'courier-payables'); abort_unless(collect($section['pages'] ?? [])->contains('slug', 'warehouse-freight'), 403, '当前账号没有该页面权限。'); } }