This commit includes the temporary Next.js build cache files, public brand logos, and project configuration added in this change.
137 lines
No EOL
4.7 KiB
TypeScript
137 lines
No EOL
4.7 KiB
TypeScript
// Full item_status enum — must match the `order_items.item_status`
|
|
// select field on the PB schema. The 2026-07-20-delivery-tracking
|
|
// migration added the last 3 (`with_driver`, `checked_in`,
|
|
// `completed`); the others (`ready_invoice`, `invoiced`,
|
|
// `stock_booked`) were already on the PB schema but missing from
|
|
// this whitelist, which silently rejected legitimate PATCHes —
|
|
// fixed in the same change so /deliveries + n8n flows don't
|
|
// trip a stale validator.
|
|
const ITEM_STATUS_VALUES = [
|
|
"pending",
|
|
"processed",
|
|
"po_generated",
|
|
"collection_pool",
|
|
"assigned",
|
|
"collected",
|
|
"stock_verified",
|
|
"picking",
|
|
"ready_delivery",
|
|
"delivered",
|
|
"cancelled",
|
|
"ready_invoice",
|
|
"invoiced",
|
|
"stock_booked",
|
|
"with_driver",
|
|
"checked_in",
|
|
"completed",
|
|
] as const;
|
|
|
|
const DRIVER_STATUS_VALUES = [
|
|
"unassigned",
|
|
"assigned",
|
|
"en_route",
|
|
"collected",
|
|
"failed",
|
|
] as const;
|
|
|
|
export type ItemStatusValue = (typeof ITEM_STATUS_VALUES)[number];
|
|
export type DriverStatusValue = (typeof DRIVER_STATUS_VALUES)[number];
|
|
|
|
/**
|
|
* Convert a YYYY-MM-DD string to an ISO timestamp at the start of that
|
|
* day in the business's local timezone. Returns null on parse failure.
|
|
*
|
|
* The route layer normalizes YYYY-MM-DD dates from the operator-facing
|
|
* modal before they hit the normalizer. We don't bake a TZ into the
|
|
* server-side normalizer because the business timezone is a config
|
|
* concern (one day this should live in env or PB), and 00:00 in UTC is
|
|
* close enough for operator scheduling (PO dates are calendar days, not
|
|
* timestamps the driver app needs second-level precision on).
|
|
*/
|
|
export function assignedDateToIso(dateOnly: string): string | null {
|
|
if (!/^\d{4}-\d{2}-\d{2}$/.test(dateOnly)) return null;
|
|
const d = new Date(dateOnly + "T00:00:00.000Z");
|
|
if (Number.isNaN(d.getTime())) return null;
|
|
return d.toISOString();
|
|
}
|
|
|
|
export function normalizeOrderItemStatusInput(input: {
|
|
item_status?: string;
|
|
driver_status?: string;
|
|
drv_status?: string;
|
|
notes?: string;
|
|
problem_notes?: string;
|
|
/**
|
|
* Driver user id (relation to `users`). Optional on every status
|
|
* update — the assignment page writes it; the driver app's
|
|
* "I'm starting" / "I'm collected" updates leave it unchanged.
|
|
* Empty string is treated as "clear the assignment" so the
|
|
* unassign path works without a separate codepath.
|
|
*/
|
|
drv_id?: string;
|
|
/**
|
|
* ISO timestamp of the most recent assignment. Same rationale as
|
|
* `drv_id` — only the assignment page writes it; other surfaces
|
|
* leave it untouched. Empty string clears the assignment time.
|
|
*
|
|
* `assignedDate` is the date-only convenience form (YYYY-MM-DD)
|
|
* used by the operator-facing assign modal. The route layer
|
|
* converts it to a full ISO at the local business timezone
|
|
* (00:00:00 UTC) so the operator's chosen day wins over an
|
|
* auto-stamp. Both fields are independent — sending only
|
|
* `assigned_at` keeps the legacy behaviour (stamp to now).
|
|
*/
|
|
assigned_at?: string;
|
|
/** YYYY-MM-DD date the operator picked from the date input. */
|
|
assignedDate?: string;
|
|
}) {
|
|
const patch: Record<string, unknown> = {};
|
|
|
|
if (input.item_status) {
|
|
if (!ITEM_STATUS_VALUES.includes(input.item_status as ItemStatusValue)) {
|
|
throw new Error(`Invalid item_status: ${input.item_status}`);
|
|
}
|
|
patch.item_status = input.item_status;
|
|
}
|
|
|
|
const resolvedDriverStatus = input.drv_status ?? input.driver_status;
|
|
if (resolvedDriverStatus) {
|
|
if (!DRIVER_STATUS_VALUES.includes(resolvedDriverStatus as DriverStatusValue)) {
|
|
throw new Error(`Invalid driver status: ${resolvedDriverStatus}`);
|
|
}
|
|
patch.drv_status = resolvedDriverStatus;
|
|
patch.driver_status = resolvedDriverStatus;
|
|
}
|
|
|
|
if (typeof input.notes === "string") {
|
|
patch.notes = input.notes;
|
|
}
|
|
|
|
if (typeof input.problem_notes === "string") {
|
|
patch.problem_notes = input.problem_notes;
|
|
}
|
|
|
|
// drv_id + assigned_at: assignment lifecycle fields. Either both
|
|
// can be omitted (no change), or either can be present alone —
|
|
// the route handler does not enforce coupling because the
|
|
// driver app's status-only writes must keep flowing. The
|
|
// assignment page is the only surface that writes both at once.
|
|
if (typeof input.drv_id === "string") {
|
|
patch.drv_id = input.drv_id;
|
|
}
|
|
|
|
// assignedDate takes precedence over assigned_at. If the operator
|
|
// picked a date, that wins; otherwise fall through to the legacy
|
|
// auto-stamp path (caller-supplied or now).
|
|
if (typeof input.assignedDate === "string") {
|
|
const iso = assignedDateToIso(input.assignedDate);
|
|
if (!iso) {
|
|
throw new Error(`Invalid assignedDate: ${input.assignedDate}`);
|
|
}
|
|
patch.assigned_at = iso;
|
|
} else if (typeof input.assigned_at === "string") {
|
|
patch.assigned_at = input.assigned_at;
|
|
}
|
|
|
|
return patch;
|
|
} |