import { http, unwrapApiResponse, type ApiEnvelope } from '@/shared/api/http' import type { ScheduleEvent } from './schedule' export interface TripCalendarEntry { sourceType: 'TRIP_PLAN' tripNo: string status: string originWarehouseId: string | null originWarehouseCode: string | null originWarehouseName: string | null originCountryCode: string | null originTimezone: string | null originWarehouseStatus: string | null plannedDepartureAt: string | null expectedDriverEndAt: string | null departedAt: string | null completedAt: string | null stops: Array<{ name: string; address?: string | null; destinationKey?: string | null }> incompleteReasons: string[] } export interface TripCalendarResult { records: TripCalendarEntry[]; unresolvedRecords: TripCalendarEntry[] } export const tripIssueLabels: Record = { ORIGIN_WAREHOUSE_UNKNOWN: '起运仓资料缺失', ORIGIN_COUNTRY_UNKNOWN: '起运国家待核实', ORIGIN_TIMEZONE_UNKNOWN: '起运仓时区待核实', ROUTE_INCOMPLETE: '路线资料不完整', LOCAL_DATE_UNRESOLVED: '无法确定当地日期', } export function tripIssues(row: TripCalendarEntry) { return row.incompleteReasons.map(reason => tripIssueLabels[reason] || '资料待核实').join(';') } export function tripQueryWindow(range: { from: string; to: string }) { const start = Date.parse(`${range.from}T00:00:00Z`), end = Date.parse(`${range.to}T00:00:00Z`) if (!Number.isFinite(start) || !Number.isFinite(end) || end <= start || end - start > 42 * 86400000) throw new Error('日历日期范围无效') // Query all possible location dates, then place each record using its actual origin zone. return { from: new Date(start - 14 * 3600000).toISOString(), to: new Date(end + 14 * 3600000).toISOString() } } export async function listTripCalendar(range: { from: string; to: string }, signal?: AbortSignal) { return unwrapApiResponse(await http.get, ApiEnvelope>( '/platform/v1/last-leg/fleet/trips/calendar', { params: tripQueryWindow(range), signal }, )) } const formatters = new Map() function localStamp(value: string | null, zone: string | null) { if (!value || !/(Z|[+-]\d{2}:\d{2})$/.test(value) || !zone?.includes('/') || /^(Etc|SystemV)\//.test(zone)) return null const instant = new Date(value) if (!Number.isFinite(instant.getTime())) return null try { let formatter = formatters.get(zone) if (!formatter) { formatter = new Intl.DateTimeFormat('en-CA', { timeZone: zone, year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', hourCycle: 'h23' }) formatters.set(zone, formatter) } const parts = Object.fromEntries(formatter.formatToParts(instant).map(part => [part.type, part.value])) return { date: `${parts.year}-${parts.month}-${parts.day}`, time: `${parts.hour}:${parts.minute}` } } catch { return null } } export function projectTripCalendar(result: TripCalendarResult, range: { from: string; to: string }) { const events: ScheduleEvent[] = [], unresolved = [...result.unresolvedRecords] const labels: Record = { PLANNED: '计划中', IN_TRANSIT: '运输中', COMPLETED: '货物已结案', CANCELLED: '已取消' } for (const row of new Map(result.records.map(row => [row.tripNo, row])).values()) { if (row.status === 'CANCELLED') continue // Executed work stays on its actual local date even if the plan changes later. const occurrence = localStamp(row.departedAt ?? row.plannedDepartureAt, row.originTimezone) if (row.originCountryCode !== 'US' || !occurrence) { unresolved.push({ ...row, incompleteReasons: [...row.incompleteReasons, 'LOCAL_DATE_UNRESOLVED'] }); continue } if (occurrence.date < range.from || occurrence.date >= range.to) continue const actual = localStamp(row.departedAt, row.originTimezone) const driverEnd = localStamp(row.expectedDriverEndAt, row.originTimezone) events.push({ key: `trip-${row.tripNo}`, type: 'dispatch', date: occurrence.date, time: occurrence.time, title: `${row.tripNo} ${row.departedAt ? '实际发车' : '排车'}`, subtitle: row.departedAt ? '按起运仓当地实际发车时间展示' : '海外仓送往 FBA/客户仓的车辆安排', sourcePath: `/overseas-master-data/trips/${encodeURIComponent(row.tripNo)}`, fields: [ { label: '车次状态', value: labels[row.status] || '待核实' }, { label: '起运仓', value: row.originWarehouseName || row.originWarehouseCode }, { label: '地点时区', value: row.originTimezone }, { label: '计划出发(UTC)', value: row.plannedDepartureAt }, { label: '路线', value: row.stops.slice(0, 3).map(stop => stop.name).join(' → ') + (row.stops.length > 3 ? ` 等${row.stops.length}站` : '') }, { label: '实际出发', value: actual ? `${actual.date} ${actual.time}` : row.departedAt ? '时刻待核实' : '尚未记录' }, { label: '预计司机结束', value: driverEnd ? `${driverEnd.date} ${driverEnd.time}` : '待核实' }, { label: '资料提示', value: tripIssues(row) }, ].filter(field => field.value), }) } return { events, unresolved } }