p2p/src/lib/format.ts
Bhanu Prakash Sai Potteri f5bc1fa119 feat: rebuild the UI on Tailwind v4 + shadcn/ui
Migrates Tailwind v3 -> v4 (CSS-first, theme tokens for brand, workflow
states and charts) and adds 23 shadcn/ui components on Radix.

- lib/workflow.ts is now the single source of truth for state name, colour,
  icon and available actions; "Financial Review" is the canonical served
  name, with "Finance Review" kept as an alias so those rows stop falling
  through to the unstyled fallback and dropping out of every count
- record view: TanStack Table with server-side sort/paginate, company column
  with initials avatar, page totals, density and a URL-synced query state
- dashboard: one cached analytics fetch shared by tiles and charts, donut +
  aging + vendor spend, click-to-filter
- detail view: rebuilt against the live detailview payload — restores the PO
  number and payment terms that the old key heuristics silently dropped,
  parses match_summary into a 3-way-match checklist, and separates the
  department's and reviewer's own words from AI-generated content
- form modal on Radix Dialog (focus trap, scroll lock, escape handling)
2026-07-29 19:47:59 +05:30

60 lines
2.3 KiB
TypeScript

// Value formatting shared by the record view and the detail view. Previously
// each had its own near-identical fmtVal/renderCell pair, which drifted.
const MONEY_HINTS = ["amount", "tax", "price", "cost", "total", "payment", "value"];
export const isMoneyField = (key: string) => {
const k = key.toLowerCase();
return MONEY_HINTS.some((h) => k.includes(h));
};
export function formatCurrency(n: number, compact = false): string {
return n.toLocaleString("en-IN", {
style: "currency",
currency: "INR",
maximumFractionDigits: 0,
...(compact ? { notation: "compact", compactDisplay: "short" } : {}),
});
}
export function formatDate(value: string): string {
const d = new Date(value);
if (Number.isNaN(d.getTime())) return value;
return d.toLocaleDateString("en-IN", { day: "2-digit", month: "short", year: "numeric" });
}
/** Clock time, or null when the value carries no time component. */
export function formatTime(value: string): string | null {
const d = new Date(value);
if (Number.isNaN(d.getTime())) return null;
if (!/[T\s]\d{2}:\d{2}/.test(value)) return null;
return d.toLocaleTimeString("en-IN", { hour: "2-digit", minute: "2-digit" });
}
/** "3 days ago" / "in 2 days" — for aging and timeline captions. */
export function formatRelative(value: string): string | null {
const d = new Date(value);
if (Number.isNaN(d.getTime())) return null;
const diffDays = Math.round((d.getTime() - Date.now()) / 86_400_000);
if (diffDays === 0) return "today";
const rtf = new Intl.RelativeTimeFormat("en", { numeric: "auto" });
if (Math.abs(diffDays) < 31) return rtf.format(diffDays, "day");
return rtf.format(Math.round(diffDays / 30), "month");
}
export function formatLabel(label: string): string {
return label.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
}
/** Plain-text formatting of a view value, honouring its declared data_type. */
export function formatValue(value: unknown, dataType: string, fieldKey: string): string {
if (value == null || value === "") return "—";
if (dataType === "number") {
const n = Number(value);
if (!Number.isFinite(n)) return String(value);
return isMoneyField(fieldKey) ? formatCurrency(n) : n.toLocaleString("en-IN");
}
if (dataType === "date" || dataType === "datetime") return formatDate(String(value));
return String(value);
}