feat(po): Project Code dropdown on PO forms
All checks were successful
PR checks / checks (pull_request) Successful in 51s
PR checks / integration (pull_request) Successful in 29s

Replace the free-text Project Code input with a native <select> carrying a
fixed list of project codes (Petronet LNG Cochin, COMACOE Trombay, Haldia
Reach, Haldia MMT, COMACOE Mandvi) plus an empty "— none —" option, across
all three PO forms (new / edit / manager-edit).

- Add a shared PROJECT_CODES constant in lib/validations/po.ts as the single
  source of truth.
- Add a reusable <ProjectCodeField> (mirrors <DeliveryLocationField>): plain
  HTML select keeping name="projectCode" so the server actions are unchanged.
- The field stays optional; projectCode remains a nullable free-text snapshot
  (no schema/migration, no validation tightening) so legacy/imported values
  are not rejected. On edit, a current value not in the list is preserved as a
  leading "(current)" option so it is never silently dropped.
- Add unit tests for the new field.

Fixes #124

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Claude (auto-fix) 2026-06-24 13:35:59 +05:30
parent 2fcb207add
commit 4ed27d668b
6 changed files with 101 additions and 3 deletions

View file

@ -10,6 +10,7 @@ import type { VesselOption, AccountGroup, CompanyOption } from "@/app/(portal)/p
import { SearchableSelect } from "@/components/ui/searchable-select";
import { VendorSelect } from "@/components/ui/vendor-select";
import { DeliveryLocationField } from "@/components/po/delivery-location-field";
import { ProjectCodeField } from "@/components/po/project-code-field";
import { PoTermsEditor } from "@/components/po/po-terms-editor";
import type { CatalogueCategory, PoTerm } from "@/lib/terms";
@ -195,7 +196,7 @@ export function ManagerEditPoForm({ po, vessels, accounts, vendors, companies, d
</div>
<div>
<label className={LABEL}>Project Code</label>
<input name="projectCode" defaultValue={po.projectCode ?? ""} className={INPUT} placeholder="Optional" />
<ProjectCodeField current={po.projectCode} className={INPUT} />
</div>
<div>
<label className={LABEL}>Delivery Date Required</label>

View file

@ -9,6 +9,7 @@ import { LineItemsEditor } from "@/components/po/po-line-items-editor";
import { SearchableSelect } from "@/components/ui/searchable-select";
import { VendorSelect } from "@/components/ui/vendor-select";
import { DeliveryLocationField } from "@/components/po/delivery-location-field";
import { ProjectCodeField } from "@/components/po/project-code-field";
import { PoTermsEditor } from "@/components/po/po-terms-editor";
import { UnsavedChangesGuard } from "@/components/po/unsaved-changes-guard";
import type { CatalogueCategory, PoTerm } from "@/lib/terms";
@ -197,7 +198,7 @@ export function EditPoForm({ po, vessels, accounts, vendors, companies, delivery
</div>
<div>
<label className="block text-sm font-medium text-neutral-700 mb-1.5">Project Code</label>
<input name="projectCode" defaultValue={po.projectCode ?? ""} className={INPUT_CLS} placeholder="Optional" />
<ProjectCodeField current={po.projectCode} className={INPUT_CLS} />
</div>
<div>
<label className="block text-sm font-medium text-neutral-700 mb-1.5">Delivery Date Required</label>

View file

@ -9,6 +9,7 @@ import { FileUploader } from "@/components/po/file-uploader";
import { SearchableSelect } from "@/components/ui/searchable-select";
import { VendorSelect } from "@/components/ui/vendor-select";
import { DeliveryLocationField } from "@/components/po/delivery-location-field";
import { ProjectCodeField } from "@/components/po/project-code-field";
import { PoTermsEditor } from "@/components/po/po-terms-editor";
import { UnsavedChangesGuard } from "@/components/po/unsaved-changes-guard";
import type { CatalogueCategory, PoTerm } from "@/lib/terms";
@ -161,7 +162,7 @@ export function NewPoForm({ vessels, accounts, vendors, companies, deliveryOptio
</div>
<div>
<label className="block text-sm font-medium text-neutral-700 mb-1.5">Project Code</label>
<input name="projectCode" className={INPUT_CLS} placeholder="Optional" />
<ProjectCodeField className={INPUT_CLS} />
</div>
<div>
<label className="block text-sm font-medium text-neutral-700 mb-1.5">Delivery Date Required</label>

View file

@ -0,0 +1,34 @@
/**
* Project Code dropdown (issue #124) a native <select name="projectCode">
* carrying the fixed `PROJECT_CODES` list plus an empty "— none —" option
* (the field stays optional). Plain HTML so it works with the forms' native
* FormData submission (no client state needed), matching DeliveryLocationField.
*
* `current` is the PO's existing project code; if it isn't one of the fixed
* options (legacy / imported / a since-removed code) it is preserved as a
* leading "(current)" option so an edit never silently drops it.
*/
import { PROJECT_CODES } from "@/lib/validations/po";
export function ProjectCodeField({
current,
className,
}: {
current?: string | null;
className?: string;
}) {
const cur = (current ?? "").trim();
const currentMissing = cur.length > 0 && !(PROJECT_CODES as readonly string[]).includes(cur);
return (
<select name="projectCode" defaultValue={cur} className={className}>
<option value=""> none </option>
{currentMissing && <option value={cur}>{cur} (current)</option>}
{PROJECT_CODES.map((code) => (
<option key={code} value={code}>
{code}
</option>
))}
</select>
);
}

View file

@ -18,6 +18,20 @@ export const TC_FIXED_LINE =
export const TC_FIXED_LINE_2 =
"We encourage bulk packaging and avoid plastic. No asbestos to be used in any product or packing material.";
/**
* Fixed list of selectable Project Codes (issue #124). The PO `projectCode`
* column stays a nullable free-text snapshot this list only constrains how
* the value is picked in the three PO forms (so legacy / imported values are
* never rejected). Extend here to add a code everywhere at once.
*/
export const PROJECT_CODES = [
"Petronet LNG Cochin",
"COMACOE Trombay",
"Haldia Reach",
"Haldia MMT",
"COMACOE Mandvi",
] as const;
export const TC_DEFAULTS = {
tcDelivery: "Within 4 to 5 days",
tcDispatch: "To be transported to site address as above. Freight Supplier's A/C",

View file

@ -0,0 +1,47 @@
import { describe, it, expect } from "vitest";
import { render, screen } from "@testing-library/react";
import { ProjectCodeField } from "@/components/po/project-code-field";
import { PROJECT_CODES } from "@/lib/validations/po";
function options(container: HTMLElement) {
return Array.from(container.querySelectorAll("option")).map((o) => ({
value: o.getAttribute("value"),
text: o.textContent,
}));
}
describe("ProjectCodeField", () => {
it("renders a select named projectCode with an empty option + every fixed code", () => {
const { container } = render(<ProjectCodeField />);
const select = container.querySelector("select");
expect(select?.getAttribute("name")).toBe("projectCode");
const opts = options(container);
// empty "none" option first, then exactly the fixed codes
expect(opts[0].value).toBe("");
expect(opts.slice(1).map((o) => o.value)).toEqual([...PROJECT_CODES]);
});
it("selects a current value that is one of the fixed codes (no duplicate option)", () => {
const { container } = render(<ProjectCodeField current="Haldia Reach" />);
const select = container.querySelector("select") as HTMLSelectElement;
expect(select.value).toBe("Haldia Reach");
// only the fixed codes + empty option — no extra "(current)" entry
expect(container.querySelectorAll("option")).toHaveLength(PROJECT_CODES.length + 1);
});
it("preserves a legacy current value not in the list as a leading (current) option", () => {
const { container } = render(<ProjectCodeField current="Legacy Project X" />);
const select = container.querySelector("select") as HTMLSelectElement;
expect(select.value).toBe("Legacy Project X");
expect(screen.getByText("Legacy Project X (current)")).toBeInTheDocument();
// empty + (current) + fixed codes
expect(container.querySelectorAll("option")).toHaveLength(PROJECT_CODES.length + 2);
});
it("defaults to the empty option when no current value is given", () => {
const { container } = render(<ProjectCodeField current={null} />);
const select = container.querySelector("select") as HTMLSelectElement;
expect(select.value).toBe("");
});
});