Reworks the T&C feature per review:
- categories are user-defined DATA, not a fixed enum — admins add new ones;
- ALL PO T&Cs are 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.
- schema: TermsCategory (name/sortOrder/isActive) + TermsCondition (categoryId
FK + text + isDefault + isActive). PurchaseOrder.terms Json snapshot. Migration
seeds every standard line as a clause (named slots, the two fixed lines under
General, empty Others); isDefault rows pre-fill new POs.
- admin /admin/terms: Add/Edit clause form's category is a combobox — typing a
new name creates the category; isDefault checkbox.
- PO editor components/po/po-terms-editor.tsx: dynamic rows (category + clause
comboboxes), used by new/edit/manager-edit forms; new POs pre-fill from
getDefaultPoTerms, edits load po.terms or legacyPoTerms (old tc* + fixed lines).
- storage: PurchaseOrder.terms ([{category,text}]) supersedes tc* for export +
detail; null on old POs falls back to tc* + fixed lines. parsePoTerms validates.
- export route + po-detail render from terms when present.
- 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;
|
|
}
|