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.
chajia/lib/mothership/ms-details-refine-validatio...

241 lines
7.3 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

/**
* 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<MsDetailsFieldKey> {
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();
}