WFDASH-WEBAPP/src/lib/server/order-item-status.ts
SJCoetz 7a68cafe35 chore: add logos, opencode config and build assets
This commit includes the temporary Next.js build cache files, public brand
logos, and project configuration added in this change.
2026-07-27 08:21:20 +02:00

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;
}