pelagia-portal/App/lib/cost-centre-groups.ts
Hardik 565f9d5833 feat: searchable accounting code picker + cost centres grouped by site
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>
2026-05-30 17:54:43 +05:30

59 lines
2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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