/** * MotherShip 登录态二级 Details:刷价提交前必填/业务规则校验 * 对齐官网:Freight ready time 不得早于提货点 Opens at */ import type { MothershipLoggedInDetailsPayload } from "@/components/mothership/mothership-logged-in-details-form"; const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; /** 12 小时制整点:`8:00 AM` / `12:00 PM` → 当日分钟数;无法解析返回 null */ export function parseMsClockToMinutes(label: string): number | null { const m = /^(\d{1,2}):(\d{2})\s*(AM|PM)$/i.exec(label.trim()); if (!m) return null; let hour = Number(m[1]); const minute = Number(m[2]); const ap = m[3]!.toUpperCase(); if (!Number.isFinite(hour) || !Number.isFinite(minute)) return null; if (hour < 1 || hour > 12 || minute < 0 || minute > 59) return null; if (ap === "AM") { if (hour === 12) hour = 0; } else if (hour !== 12) { hour += 12; } return hour * 60 + minute; } export const MS_READY_BEFORE_OPENS_MESSAGE = "货运就绪时刻不能早于提货点开门时间,请调整就绪时刻或开门时间"; export const MS_OPENS_NOT_BEFORE_CLOSES_MESSAGE = "开门时间须早于关门时间"; /** MotherShip:提货与送货联系人邮箱不得相同 */ export const MS_EMAILS_MUST_DIFFER_MESSAGE = "提货与送货联系人邮箱不能相同,请分别填写"; function blank(v: string | undefined | null): boolean { return !String(v ?? "").trim(); } function normalizeEmail(v: string): string { return v.trim().toLowerCase(); } export type MsDetailsRefineAddressOpts = { pickupAddress?: string; deliveryAddress?: string; /** 一级/二级「货运就绪时刻」,与提货 Opens at 对齐官网规则 */ readyTime?: string; }; /** 二级表单字段键,用于必填空/错时红边高亮 */ export type MsDetailsFieldKey = | "pickup.address" | "pickup.companyName" | "pickup.contactEmail" | "pickup.contactPhone" | "pickup.opensAt" | "pickup.closesAt" | "pickup.readyTime" | "delivery.address" | "delivery.companyName" | "delivery.contactEmail" | "delivery.contactPhone" | "delivery.opensAt" | "delivery.closesAt" | `cargo.${number}.pieceCountType` | `cargo.${number}.pieceCountQty` | `cargo.${number}.description`; type MsDetailsIssue = { key: MsDetailsFieldKey; message: string }; /** * 字段级缺失/冲突;供红边高亮与文案列表共用。 */ export function collectMsDetailsRefineIssues( details: MothershipLoggedInDetailsPayload, opts?: MsDetailsRefineAddressOpts, ): MsDetailsIssue[] { const issues: MsDetailsIssue[] = []; if (opts && typeof opts.pickupAddress === "string" && blank(opts.pickupAddress)) { issues.push({ key: "pickup.address", message: "提货:地址" }); } if ( opts && typeof opts.deliveryAddress === "string" && blank(opts.deliveryAddress) ) { issues.push({ key: "delivery.address", message: "送货:地址" }); } const checkLoc = ( prefix: "pickup" | "delivery", side: "提货" | "送货", loc: MothershipLoggedInDetailsPayload["pickup"], ) => { if (blank(loc.companyName)) { issues.push({ key: `${prefix}.companyName`, message: `${side}:完整公司名`, }); } if (blank(loc.contactEmail)) { issues.push({ key: `${prefix}.contactEmail`, message: `${side}:现场联系人邮箱`, }); } else if (!EMAIL_RE.test(loc.contactEmail.trim())) { issues.push({ key: `${prefix}.contactEmail`, message: `${side}:现场联系人邮箱格式无效`, }); } if (blank(loc.contactPhone)) { issues.push({ key: `${prefix}.contactPhone`, message: `${side}:现场联系人电话`, }); } if (blank(loc.opensAt)) { issues.push({ key: `${prefix}.opensAt`, message: `${side}:开门时间` }); } if (blank(loc.closesAt)) { issues.push({ key: `${prefix}.closesAt`, message: `${side}:关门时间` }); } const openMin = parseMsClockToMinutes(loc.opensAt); const closeMin = parseMsClockToMinutes(loc.closesAt); if (openMin != null && closeMin != null && openMin >= closeMin) { issues.push({ key: `${prefix}.opensAt`, message: `${side}:${MS_OPENS_NOT_BEFORE_CLOSES_MESSAGE}`, }); issues.push({ key: `${prefix}.closesAt`, message: `${side}:${MS_OPENS_NOT_BEFORE_CLOSES_MESSAGE}`, }); } }; checkLoc("pickup", "提货", details.pickup); checkLoc("delivery", "送货", details.delivery); const pickupEmail = details.pickup.contactEmail.trim(); const deliveryEmail = details.delivery.contactEmail.trim(); if ( pickupEmail && deliveryEmail && EMAIL_RE.test(pickupEmail) && EMAIL_RE.test(deliveryEmail) && normalizeEmail(pickupEmail) === normalizeEmail(deliveryEmail) ) { issues.push({ key: "pickup.contactEmail", message: MS_EMAILS_MUST_DIFFER_MESSAGE, }); issues.push({ key: "delivery.contactEmail", message: MS_EMAILS_MUST_DIFFER_MESSAGE, }); } const ready = opts?.readyTime?.trim() ?? ""; const opens = details.pickup.opensAt.trim(); if (ready && opens) { const readyMin = parseMsClockToMinutes(ready); const openMin = parseMsClockToMinutes(opens); if (readyMin != null && openMin != null && readyMin < openMin) { issues.push({ key: "pickup.readyTime", message: MS_READY_BEFORE_OPENS_MESSAGE, }); } } if (!details.cargo.length) { issues.push({ key: "cargo.0.description", message: "货物:至少一行明细", }); } details.cargo.forEach((line, i) => { const n = i + 1; // 官网:Piece count type / quantity 非必填,不拦截 if (blank(line.description)) { issues.push({ key: `cargo.${i}.description`, message: `货物第${n}行:货物描述`, }); } }); return issues; } /** * 返回中文缺失/冲突项列表;空数组 = 可提交。 * 与二级表单红色 * 字段对齐。 */ export function collectMsDetailsRefineMissing( details: MothershipLoggedInDetailsPayload, opts?: MsDetailsRefineAddressOpts, ): string[] { return collectMsDetailsRefineIssues(details, opts).map((i) => i.message); } /** 需红边高亮的字段键集合 */ export function collectMsDetailsRefineInvalidFields( details: MothershipLoggedInDetailsPayload, opts?: MsDetailsRefineAddressOpts, ): Set { return new Set(collectMsDetailsRefineIssues(details, opts).map((i) => i.key)); } export function validateMsDetailsForRefine( details: MothershipLoggedInDetailsPayload, opts?: MsDetailsRefineAddressOpts, ): { ok: true } | { ok: false; message: string; missing: string[] } { const missing = collectMsDetailsRefineMissing(details, opts); if (missing.length === 0) return { ok: true }; return { ok: false, missing, message: `信息不完整,请核对后重新提交:${missing.join(";")}`, }; } /** * 若就绪时刻早于开门,拨到开门整点(与官网 Select time 选项一致)。 */ export function snapMsReadyTimeToOpensAt( readyTime: string, opensAt: string, ): string { const readyMin = parseMsClockToMinutes(readyTime); const openMin = parseMsClockToMinutes(opensAt); if (readyMin == null || openMin == null) return readyTime; if (readyMin >= openMin) return readyTime.trim(); return opensAt.trim(); }