pelagia-portal/App/lib/terms.ts
Hardik 3babfe26ef
All checks were successful
PR checks / checks (pull_request) Successful in 45s
PR checks / integration (pull_request) Successful in 32s
feat(po): user-defined T&C categories + dynamic PO terms editor (#11)
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>
2026-06-24 04:43:24 +05:30

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