- Undo Vessel→Cost Centre rename in admin (admin shows "Vessel Management" again) - Sidebar: "Cost Centres"→"Vessels", "Accounts"→"Accounting Codes" - PO forms (new/edit/import/manager-edit) now show both Vessels (with code) and Sites in the Cost Centre dropdown, encoded as v:<id> / s:<id> via a costCentreRef field - vesselId on PurchaseOrder is now nullable; siteId is set when a site is the cost centre - History, approvals, dashboard, my-orders, payments display vessel.name ?? site.name as Cost Centre - History and approvals cost centre filters use costCentreRef URL param supporting both types - Admin vessel form: adds Site assignment dropdown - Admin accounts: renamed to "Accounting Code" throughout (pages, forms, sidebar) - PO detail and exports: "Account" label renamed to "Accounting Code" - Site detail: "Assigned Vessels (Cost Centres)" heading; vessel detail breadcrumb fixed - Create PO links from vessel/site detail use ?costCentreRef= param - Export routes handle costCentreRef filter param (with legacy vesselId fallback) - DB migration: ALTER TABLE PurchaseOrder ALTER COLUMN vesselId DROP NOT NULL - CLAUDE.md updated with Cost Centre Model documentation Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
75 lines
3.2 KiB
TypeScript
75 lines
3.2 KiB
TypeScript
"use client";
|
|
|
|
import { useRouter, useSearchParams } from "next/navigation";
|
|
import { useState } from "react";
|
|
|
|
interface Props {
|
|
costCentres: { ref: string; name: string }[];
|
|
}
|
|
|
|
export function ApprovalsSearch({ costCentres }: Props) {
|
|
const router = useRouter();
|
|
const sp = useSearchParams();
|
|
|
|
const [q, setQ] = useState(sp.get("q") ?? "");
|
|
const [costCentreRef, setCostCentreRef] = useState(sp.get("costCentreRef") ?? "");
|
|
const [dateFrom, setDateFrom] = useState(sp.get("dateFrom") ?? "");
|
|
const [dateTo, setDateTo] = useState(sp.get("dateTo") ?? "");
|
|
|
|
function apply() {
|
|
const params = new URLSearchParams();
|
|
if (q.trim()) params.set("q", q.trim());
|
|
if (costCentreRef) params.set("costCentreRef", costCentreRef);
|
|
if (dateFrom) params.set("dateFrom", dateFrom);
|
|
if (dateTo) params.set("dateTo", dateTo);
|
|
router.push(`/approvals?${params.toString()}`);
|
|
}
|
|
|
|
function clear() {
|
|
setQ(""); setCostCentreRef(""); setDateFrom(""); setDateTo("");
|
|
router.push("/approvals");
|
|
}
|
|
|
|
const hasFilters = q || costCentreRef || dateFrom || dateTo;
|
|
|
|
return (
|
|
<div className="mb-4 rounded-lg border border-neutral-200 bg-white p-4">
|
|
<div className="grid grid-cols-2 gap-3 sm:grid-cols-4">
|
|
<div className="sm:col-span-2">
|
|
<label className="block text-xs font-medium text-neutral-600 mb-1">Search (PO number or submitter)</label>
|
|
<input type="text" value={q} onChange={(e) => setQ(e.target.value)}
|
|
onKeyDown={(e) => e.key === "Enter" && apply()}
|
|
placeholder="e.g. PO-0012 or John…"
|
|
className="w-full rounded-lg border border-neutral-300 px-3 py-2 text-sm focus:border-primary-500 focus:outline-none focus:ring-2 focus:ring-primary-500/20" />
|
|
</div>
|
|
<div>
|
|
<label className="block text-xs font-medium text-neutral-600 mb-1">Cost Centre</label>
|
|
<select value={costCentreRef} onChange={(e) => setCostCentreRef(e.target.value)}
|
|
className="w-full rounded-lg border border-neutral-300 px-3 py-2 text-sm focus:border-primary-500 focus:outline-none focus:ring-2 focus:ring-primary-500/20">
|
|
<option value="">All cost centres</option>
|
|
{costCentres.map((c) => (
|
|
<option key={c.ref} value={c.ref}>{c.name}</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label className="block text-xs font-medium text-neutral-600 mb-1">Submitted from</label>
|
|
<input type="date" value={dateFrom} onChange={(e) => setDateFrom(e.target.value)}
|
|
className="w-full rounded-lg border border-neutral-300 px-3 py-2 text-sm focus:border-primary-500 focus:outline-none focus:ring-2 focus:ring-primary-500/20" />
|
|
</div>
|
|
</div>
|
|
<div className="mt-3 flex items-center gap-2">
|
|
<button onClick={apply}
|
|
className="rounded-lg bg-primary-600 px-3 py-1.5 text-sm font-medium text-white hover:bg-primary-700 transition-colors">
|
|
Search
|
|
</button>
|
|
{hasFilters && (
|
|
<button onClick={clear}
|
|
className="rounded-lg border border-neutral-300 bg-white px-3 py-1.5 text-sm font-medium text-neutral-600 hover:bg-neutral-50 transition-colors">
|
|
Clear
|
|
</button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|