Files
admin-web/src/modules/appointment-calendar/tripCalendar.ts
T

84 lines
4.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { http, unwrapApiResponse, type ApiEnvelope } from '@/shared/api/http'
import type { CalendarEvent } from './AppointmentCalendarPanel.vue'
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<string, string> = {
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<TripCalendarResult>, ApiEnvelope<TripCalendarResult>>(
'/platform/v1/last-leg/fleet/trips/calendar', { params: tripQueryWindow(range), signal },
))
}
const formatters = new Map<string, Intl.DateTimeFormat>()
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: CalendarEvent[] = [], unresolved = [...result.unresolvedRecords]
const labels: Record<string, string> = { PLANNED: '计划中', IN_TRANSIT: '运输中', COMPLETED: '货物已结案', CANCELLED: '已取消' }
for (const row of result.records) {
const planned = localStamp(row.plannedDepartureAt, row.originTimezone)
if (row.originCountryCode !== 'US' || !planned) {
unresolved.push({ ...row, incompleteReasons: [...row.incompleteReasons, 'LOCAL_DATE_UNRESOLVED'] }); continue
}
if (planned.date < range.from || planned.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: 'trip', date: planned.date, time: planned.time,
title: `${row.tripNo} 用车计划`, subtitle: '运输计划,不代表收货方预约已确认',
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 }
}