Accounting Code search (new/edit/import/manager-edit PO forms):
- New SearchableSelect component (components/ui/searchable-select.tsx):
type-to-filter by code or name, results grouped by sub-category with
sticky headers, highlighted selected item, clear button, Escape/outside-click
to dismiss
- Replaces the plain <select> for the main Accounting Code field on all PO forms
- LineItemsEditor per-row account column also uses SearchableSelect (compact size)
when multi-account mode is active
Cost Centre dropdown reorganised by site:
- New type CostCentreGroup replaces flat CostCentreOption
- Each site becomes an <optgroup> label (unselectable); the site itself is the
first selectable option inside ("Haldia (Site)"), followed by its vessels
- Vessels with no site assigned appear under an "Unassigned Vessels" group
- Shared helpers buildCostCentreGroups() and buildAccountGroups() in
lib/cost-centre-groups.ts — used by all four PO form pages
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
59 lines
2 KiB
TypeScript
59 lines
2 KiB
TypeScript
import type { CostCentreGroup } from "@/app/(portal)/po/new/new-po-form";
|
||
|
||
type VesselLike = { id: string; name: string; code: string; siteId: string | null };
|
||
type SiteLike = { id: string; name: string };
|
||
|
||
/**
|
||
* Builds the grouped cost-centre list used by PO form dropdowns.
|
||
* Each group = one site (as an optgroup), with the site itself as a selectable
|
||
* option followed by its vessels.
|
||
* Vessels with no site appear under an "Unassigned Vessels" group at the end.
|
||
*/
|
||
export function buildCostCentreGroups(
|
||
vessels: VesselLike[],
|
||
sites: SiteLike[]
|
||
): CostCentreGroup[] {
|
||
const groups: CostCentreGroup[] = sites.map((s) => ({
|
||
siteId: s.id,
|
||
siteName: s.name,
|
||
siteRef: `s:${s.id}`,
|
||
vessels: vessels
|
||
.filter((v) => v.siteId === s.id)
|
||
.map((v) => ({ ref: `v:${v.id}`, label: `${v.code} — ${v.name}` })),
|
||
}));
|
||
|
||
const unassigned = vessels.filter((v) => !v.siteId);
|
||
if (unassigned.length > 0) {
|
||
groups.push({
|
||
siteId: null,
|
||
siteName: "Unassigned Vessels",
|
||
siteRef: null,
|
||
vessels: unassigned.map((v) => ({ ref: `v:${v.id}`, label: `${v.code} — ${v.name}` })),
|
||
});
|
||
}
|
||
|
||
return groups;
|
||
}
|
||
|
||
/**
|
||
* Builds grouped accounting codes for the SearchableSelect component.
|
||
* Only returns leaf items (no children), grouped by sub-category.
|
||
*/
|
||
export function buildAccountGroups(
|
||
leafAccounts: {
|
||
id: string;
|
||
code: string;
|
||
name: string;
|
||
parent: { name: string; code: string; parent: { name: string; code: string } | null } | null;
|
||
}[]
|
||
) {
|
||
const map = new Map<string, { id: string; code: string; name: string }[]>();
|
||
for (const a of leafAccounts) {
|
||
const subLabel = a.parent ? `${a.parent.code} — ${a.parent.name}` : "Uncategorised";
|
||
const topLabel = a.parent?.parent ? `${a.parent.parent.name} › ` : "";
|
||
const key = `${topLabel}${subLabel}`;
|
||
if (!map.has(key)) map.set(key, []);
|
||
map.get(key)!.push({ id: a.id, code: a.code, name: a.name });
|
||
}
|
||
return Array.from(map.entries()).map(([group, items]) => ({ group, items }));
|
||
}
|