Follow-up to the merged #11 PR (which shipped the enum-based catalogue): make categories user-defined data and the PO T&C a dynamic editor. - categories are a TermsCategory TABLE (not an enum) — admins add new ones; - every PO T&C line is catalogued, incl. the previously-fixed boilerplate (seeded under a "General" category) and an "Others" bucket; - the PO form is a dynamic editor: "+ Add term", pick a category, type/pick a clause (components/po/po-terms-editor.tsx), used by new/edit/manager-edit. Migration: the already-released 20260624140000 migration is untouched; a new 20260624150000 FORWARD migration renames the enum, creates the table, migrates existing enum clauses onto category rows, adds isDefault/sortOrder + the two fixed lines under General, and adds PurchaseOrder.terms (JSON snapshot that supersedes the legacy tc* columns for export/detail; old POs fall back to tc*). Tests rewritten for category creation + catalogue/default helpers. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
50 lines
2.3 KiB
TypeScript
50 lines
2.3 KiB
TypeScript
/**
|
|
* Terms & Conditions catalogue (issue #11) — admin-managed categories + clauses
|
|
* that feed the PO's dynamic T&C editor. Categories are user-defined data.
|
|
*/
|
|
import { TC_DEFAULTS, TC_FIXED_LINE, TC_FIXED_LINE_2 } from "@/lib/validations/po";
|
|
|
|
// One chosen T&C row on a PO (stored as a JSON snapshot in PurchaseOrder.terms).
|
|
export type PoTerm = { category: string; text: string };
|
|
|
|
// A catalogue category with its active clause texts — passed to the PO editor.
|
|
export type CatalogueCategory = { id: string; name: string; clauses: string[] };
|
|
|
|
// Legacy PO (no `terms` JSON yet) → editable rows, mapping the old tc* columns +
|
|
// the previously-fixed boilerplate lines onto the new category model, in the
|
|
// original document order. Used to seed the editor when editing an old PO.
|
|
type LegacyTc = {
|
|
tcDelivery?: string | null;
|
|
tcDispatch?: string | null;
|
|
tcInspection?: string | null;
|
|
tcTransitInsurance?: string | null;
|
|
tcPaymentTerms?: string | null;
|
|
tcOthers?: string | null;
|
|
};
|
|
export function legacyPoTerms(po: LegacyTc): PoTerm[] {
|
|
const rows: PoTerm[] = [
|
|
{ category: "General", text: TC_FIXED_LINE },
|
|
{ category: "Delivery", text: po.tcDelivery ?? TC_DEFAULTS.tcDelivery },
|
|
{ category: "Dispatch Instructions", text: po.tcDispatch ?? TC_DEFAULTS.tcDispatch },
|
|
{ category: "Inspection", text: po.tcInspection ?? TC_DEFAULTS.tcInspection },
|
|
{ category: "Transit Insurance", text: po.tcTransitInsurance ?? TC_DEFAULTS.tcTransitInsurance },
|
|
{ category: "Payment Terms", text: po.tcPaymentTerms ?? TC_DEFAULTS.tcPaymentTerms },
|
|
{ category: "Others", text: po.tcOthers ?? "" },
|
|
{ category: "General", text: TC_FIXED_LINE_2 },
|
|
];
|
|
return rows.filter((r) => r.text.trim().length > 0);
|
|
}
|
|
|
|
/** Coerce an unknown (DB JSON / parsed form value) into a clean PoTerm[]. */
|
|
export function parsePoTerms(value: unknown): PoTerm[] {
|
|
if (!Array.isArray(value)) return [];
|
|
const out: PoTerm[] = [];
|
|
for (const row of value) {
|
|
if (!row || typeof row !== "object") continue;
|
|
const category = String((row as Record<string, unknown>).category ?? "").trim();
|
|
const text = String((row as Record<string, unknown>).text ?? "").trim();
|
|
// A row needs at least some text to be meaningful; category may be blank.
|
|
if (text) out.push({ category, text });
|
|
}
|
|
return out;
|
|
}
|