You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

137 lines
3.9 KiB

import type { Priority1DisplayLine } from "@/modules/priority1/quote-storage";
export type Priority1CalendarCell = {
date: Date;
inMonth: boolean;
priceUsd: number | null;
line: Priority1DisplayLine | null;
};
const WEEKDAYS = ["SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT"] as const;
function parseCalendarAnchor(raw: string): Date {
const trimmed = raw.trim();
const iso = trimmed.match(/^(\d{4})-(\d{2})-(\d{2})$/);
if (iso) {
return new Date(Number(iso[1]), Number(iso[2]) - 1, Number(iso[3]), 12);
}
const slash = trimmed.match(/^(\d{2})\/(\d{2})\/(\d{4})$/);
if (slash) {
return new Date(Number(slash[3]), Number(slash[1]) - 1, Number(slash[2]), 12);
}
return new Date(`${trimmed}T12:00:00`);
}
function resolveLineDay(
line: Priority1DisplayLine,
fallbackDay: number,
year: number,
month: number,
): number | null {
if (line.deliveryDate) {
const dayTag = line.deliveryDate.match(/^DAY:(\d{1,2})$/);
if (dayTag) {
const day = Number(dayTag[1]);
return day >= 1 && day <= 31 ? day : null;
}
const iso = line.deliveryDate.match(/^(\d{4})-(\d{2})-(\d{2})/);
if (iso) {
const date = new Date(
Number(iso[1]),
Number(iso[2]) - 1,
Number(iso[3]),
12,
);
if (date.getFullYear() === year && date.getMonth() === month) {
return date.getDate();
}
}
const slash = line.deliveryDate.match(/^(\d{2})\/(\d{2})\/(\d{4})$/);
if (slash) {
const date = new Date(
Number(slash[3]),
Number(slash[1]) - 1,
Number(slash[2]),
12,
);
if (date.getFullYear() === year && date.getMonth() === month) {
return date.getDate();
}
}
}
return fallbackDay;
}
export function weekdayLabels(): readonly string[] {
return WEEKDAYS;
}
export function weekdayLabelsZh(): readonly string[] {
return ["周日", "周一", "周二", "周三", "周四", "周五", "周六"] as const;
}
/** 将 FTL 日历报价按提货日起连续填入当月日历格 */
export function buildPriority1CalendarGrid(
pickupDateIso: string,
lines: Priority1DisplayLine[],
): { monthLabel: string; cells: Priority1CalendarCell[] } {
const anchor = parseCalendarAnchor(pickupDateIso);
if (Number.isNaN(anchor.getTime())) {
return { monthLabel: "", cells: [] };
}
const year = anchor.getFullYear();
const month = anchor.getMonth();
const monthLabel = anchor.toLocaleString("en-US", { month: "short" });
const firstOfMonth = new Date(year, month, 1);
const startPad = firstOfMonth.getDay();
const daysInMonth = new Date(year, month + 1, 0).getDate();
const sorted = [...lines].sort((a, b) => a.rank - b.rank);
const priceByDay = new Map<number, Priority1DisplayLine>();
let cursor = anchor.getDate();
for (const line of sorted) {
while (cursor > daysInMonth) break;
const day = resolveLineDay(line, cursor, year, month);
if (day != null && day >= 1 && day <= daysInMonth) {
priceByDay.set(day, line);
cursor = day + 1;
continue;
}
priceByDay.set(cursor, line);
cursor += 1;
}
const cells: Priority1CalendarCell[] = [];
for (let i = 0; i < startPad; i += 1) {
const d = new Date(year, month, -(startPad - 1 - i));
cells.push({ date: d, inMonth: false, priceUsd: null, line: null });
}
for (let day = 1; day <= daysInMonth; day += 1) {
const date = new Date(year, month, day);
const line = priceByDay.get(day) ?? null;
cells.push({
date,
inMonth: true,
priceUsd:
line != null
? line.totalUsd > 0
? line.totalUsd
: line.final_total_usd
: null,
line,
});
}
while (cells.length % 7 !== 0) {
const last = cells[cells.length - 1]!.date;
const next = new Date(last);
next.setDate(next.getDate() + 1);
cells.push({ date: next, inMonth: false, priceUsd: null, line: null });
}
return { monthLabel, cells };
}