refactor: revert cost centre to vessels only, remove vessel-site link
Cost Centre on PO forms now shows only Vessels (plain vesselId field). Sites are a separate concept and not selectable as cost centres. - PurchaseOrder.vesselId is required again (NOT NULL restored) - Vessel.siteId and vessel->site relation removed from schema - DB migration: drops Vessel.siteId column, restores PO.vesselId NOT NULL - All PO forms (new/edit/import/manager-edit): plain vessel <select> with code-prefixed labels (e.g. "HNR1 — HNR 1") - History, approvals, dashboard, my-orders, payments: back to vesselId filter params and po.vessel.name display - Admin vessels: removed Site column and site-assignment dropdown - Admin sites detail page: removed "Assigned Vessels" section - Sites table: removed Vessels count column (no longer linked) - seed-prod.ts and seed.ts: vessels created without siteId - SearchableSelect accounting code picker retained from previous commit Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
565f9d5833
commit
280966a369
39 changed files with 190 additions and 424 deletions
|
|
@ -31,7 +31,6 @@ export default async function SiteDetailPage({ params }: Props) {
|
|||
db.site.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
vessels: { select: { id: true, name: true, isActive: true } },
|
||||
inventory: {
|
||||
include: { product: { select: { id: true, name: true, code: true } } },
|
||||
orderBy: { quantity: "desc" },
|
||||
|
|
@ -100,7 +99,7 @@ export default async function SiteDetailPage({ params }: Props) {
|
|||
)}
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Link href={`/po/new?costCentreRef=s:${site.id}`} className="rounded-lg bg-primary-600 px-4 py-2 text-sm font-semibold text-white hover:bg-primary-700">
|
||||
<Link href={`/po/new`} className="rounded-lg bg-primary-600 px-4 py-2 text-sm font-semibold text-white hover:bg-primary-700">
|
||||
+ Create PO
|
||||
</Link>
|
||||
{canEdit && <EditSiteButton site={{ id: site.id, name: site.name, code: site.code, address: site.address, latitude: site.latitude, longitude: site.longitude, isActive: site.isActive }} />}
|
||||
|
|
@ -108,11 +107,7 @@ export default async function SiteDetailPage({ params }: Props) {
|
|||
</div>
|
||||
|
||||
{/* Summary cards */}
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<div className="rounded-lg border border-neutral-200 bg-white px-5 py-4">
|
||||
<p className="text-xs text-neutral-500 mb-1">Assigned Vessels</p>
|
||||
<p className="text-2xl font-semibold text-neutral-900">{site.vessels.length}</p>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="rounded-lg border border-neutral-200 bg-white px-5 py-4">
|
||||
<p className="text-xs text-neutral-500 mb-1">Items Tracked</p>
|
||||
<p className="text-2xl font-semibold text-neutral-900">{site.inventory.length}</p>
|
||||
|
|
@ -167,20 +162,6 @@ export default async function SiteDetailPage({ params }: Props) {
|
|||
<ConsumptionForm siteId={site.id} products={products} />
|
||||
</div>
|
||||
|
||||
{/* Assigned vessels */}
|
||||
{site.vessels.length > 0 && (
|
||||
<div className="rounded-lg border border-neutral-200 bg-white p-6">
|
||||
<h2 className="text-sm font-semibold text-neutral-900 mb-4">Assigned Vessels (Cost Centres)</h2>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{site.vessels.map((v) => (
|
||||
<Link key={v.id} href={`/admin/vessels/${v.id}`}
|
||||
className="rounded-lg border border-neutral-200 px-3 py-1.5 text-sm font-medium text-neutral-700 hover:bg-neutral-50">
|
||||
{v.name}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Recent POs */}
|
||||
{site.purchaseOrders.length > 0 && (
|
||||
|
|
|
|||
|
|
@ -85,7 +85,6 @@ export async function deleteSite(id: string): Promise<Result> {
|
|||
await tx.purchaseOrder.updateMany({ where: { siteId: id, status: "DRAFT" }, data: { siteId: null } });
|
||||
await tx.itemConsumption.deleteMany({ where: { siteId: id } });
|
||||
await tx.itemInventory.deleteMany({ where: { siteId: id } });
|
||||
await tx.vessel.updateMany({ where: { siteId: id }, data: { siteId: null } });
|
||||
await tx.site.delete({ where: { id } });
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ export default async function SitesPage() {
|
|||
const sites = await db.site.findMany({
|
||||
orderBy: { name: "asc" },
|
||||
include: {
|
||||
_count: { select: { vessels: true, inventory: true } },
|
||||
_count: { select: { inventory: true } },
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -32,7 +32,6 @@ export default async function SitesPage() {
|
|||
latitude: s.latitude ?? null,
|
||||
longitude: s.longitude ?? null,
|
||||
isActive: s.isActive,
|
||||
vesselCount: s._count.vessels,
|
||||
inventoryCount: s._count.inventory,
|
||||
}))}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -18,7 +18,6 @@ export type SiteRow = {
|
|||
latitude: number | null;
|
||||
longitude: number | null;
|
||||
isActive: boolean;
|
||||
vesselCount: number;
|
||||
inventoryCount: number;
|
||||
};
|
||||
|
||||
|
|
@ -123,7 +122,6 @@ export function SitesTable({
|
|||
<SortableTh sortKey="name" activeSortKey={sortKey as string | null} sortDir={sortDir} onSort={(k) => toggleSort(k as keyof SiteRow)}>Name</SortableTh>
|
||||
<SortableTh sortKey="code" activeSortKey={sortKey as string | null} sortDir={sortDir} onSort={(k) => toggleSort(k as keyof SiteRow)}>Code</SortableTh>
|
||||
<SortableTh sortKey="address" activeSortKey={sortKey as string | null} sortDir={sortDir} onSort={(k) => toggleSort(k as keyof SiteRow)}>Address</SortableTh>
|
||||
<th className="px-4 py-3 text-right font-medium text-neutral-600">Vessels</th>
|
||||
<th className="px-4 py-3 text-right font-medium text-neutral-600">Items tracked</th>
|
||||
<th className="px-4 py-3 text-left font-medium text-neutral-600">Location</th>
|
||||
<SortableTh sortKey="isActive" activeSortKey={sortKey as string | null} sortDir={sortDir} onSort={(k) => toggleSort(k as keyof SiteRow)}>Status</SortableTh>
|
||||
|
|
@ -133,7 +131,7 @@ export function SitesTable({
|
|||
<tbody className="divide-y divide-neutral-100">
|
||||
{filtered.length === 0 && (
|
||||
<tr>
|
||||
<td colSpan={canEdit ? 8 : 7} className="px-4 py-8 text-center text-neutral-400">
|
||||
<td colSpan={canEdit ? 7 : 6} className="px-4 py-8 text-center text-neutral-400">
|
||||
No sites match your search.
|
||||
</td>
|
||||
</tr>
|
||||
|
|
@ -149,9 +147,6 @@ export function SitesTable({
|
|||
<td className="px-4 py-3 text-neutral-500 max-w-xs truncate">
|
||||
{site.address ?? <span className="italic text-neutral-400">—</span>}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-right text-neutral-600">
|
||||
{site.vesselCount || <span className="text-neutral-400">—</span>}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-right text-neutral-600">
|
||||
{site.inventoryCount || <span className="text-neutral-400">—</span>}
|
||||
</td>
|
||||
|
|
|
|||
|
|
@ -24,7 +24,6 @@ export default async function VesselDetailPage({ params }: Props) {
|
|||
const vessel = await db.vessel.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
site: true,
|
||||
purchaseOrders: {
|
||||
select: { id: true, poNumber: true, status: true, totalAmount: true, createdAt: true, vendor: { select: { name: true } } },
|
||||
orderBy: { createdAt: "desc" },
|
||||
|
|
@ -60,13 +59,8 @@ export default async function VesselDetailPage({ params }: Props) {
|
|||
</span>
|
||||
</div>
|
||||
<h1 className="text-2xl font-semibold text-neutral-900">{vessel.name}</h1>
|
||||
{vessel.site && (
|
||||
<p className="mt-1 text-sm text-neutral-500">
|
||||
Home site: <Link href={`/admin/sites/${vessel.site.id}`} className="text-primary-600 hover:underline">{vessel.site.name}</Link>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<Link href={`/po/new?costCentreRef=v:${vessel.id}`} className="rounded-lg bg-primary-600 px-4 py-2 text-sm font-semibold text-white hover:bg-primary-700">
|
||||
<Link href={`/po/new?vesselId=${vessel.id}`} className="rounded-lg bg-primary-600 px-4 py-2 text-sm font-semibold text-white hover:bg-primary-700">
|
||||
+ Create PO
|
||||
</Link>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@ type ActionResult = { ok: true } | { error: string };
|
|||
const vesselSchema = z.object({
|
||||
name: z.string().min(1, "Vessel name is required"),
|
||||
code: z.string().optional(),
|
||||
siteId: z.string().optional(),
|
||||
});
|
||||
|
||||
export async function createVessel(formData: FormData): Promise<ActionResult> {
|
||||
|
|
@ -24,7 +23,6 @@ export async function createVessel(formData: FormData): Promise<ActionResult> {
|
|||
const parsed = vesselSchema.safeParse({
|
||||
name: formData.get("name"),
|
||||
code: (formData.get("code") as string).trim() || undefined,
|
||||
siteId: (formData.get("siteId") as string) || undefined,
|
||||
});
|
||||
if (!parsed.success) return { error: parsed.error.errors[0]?.message ?? "Validation failed" };
|
||||
|
||||
|
|
@ -32,16 +30,14 @@ export async function createVessel(formData: FormData): Promise<ActionResult> {
|
|||
|
||||
let code: string;
|
||||
if (parsed.data.code) {
|
||||
// User supplied a custom code — validate uniqueness
|
||||
const conflict = await db.vessel.findUnique({ where: { code: parsed.data.code } });
|
||||
if (conflict) return { error: `Code "${parsed.data.code}" is already in use by another vessel.` };
|
||||
code = parsed.data.code;
|
||||
} else {
|
||||
// Auto-generate next available code
|
||||
code = nextId("SITE", existingCodes.map((v) => v.code));
|
||||
}
|
||||
|
||||
await db.vessel.create({ data: { name: parsed.data.name, code, siteId: parsed.data.siteId ?? null } });
|
||||
await db.vessel.create({ data: { name: parsed.data.name, code } });
|
||||
revalidatePath("/admin/vessels");
|
||||
return { ok: true };
|
||||
}
|
||||
|
|
@ -57,11 +53,10 @@ export async function updateVessel(formData: FormData): Promise<ActionResult> {
|
|||
|
||||
const parsed = vesselSchema.safeParse({
|
||||
name: formData.get("name"),
|
||||
siteId: (formData.get("siteId") as string) || undefined,
|
||||
});
|
||||
if (!parsed.success) return { error: parsed.error.errors[0]?.message ?? "Validation failed" };
|
||||
|
||||
await db.vessel.update({ where: { id }, data: { name: parsed.data.name, siteId: parsed.data.siteId ?? null } });
|
||||
await db.vessel.update({ where: { id }, data: { name: parsed.data.name } });
|
||||
revalidatePath("/admin/vessels");
|
||||
return { ok: true };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,13 +14,9 @@ export default async function AdminVesselsPage() {
|
|||
|
||||
if (!hasPermission(session.user.role, "manage_vessels_accounts")) redirect("/dashboard");
|
||||
|
||||
const [vessels, sites] = await Promise.all([
|
||||
db.vessel.findMany({
|
||||
const vessels = await db.vessel.findMany({
|
||||
orderBy: { name: "asc" },
|
||||
include: { site: { select: { name: true } } },
|
||||
}),
|
||||
db.site.findMany({ where: { isActive: true }, orderBy: { name: "asc" }, select: { id: true, name: true } }),
|
||||
]);
|
||||
});
|
||||
|
||||
const suggestedCode = nextId("SITE", vessels.map((v) => v.code));
|
||||
|
||||
|
|
@ -31,11 +27,8 @@ export default async function AdminVesselsPage() {
|
|||
id: v.id,
|
||||
code: v.code,
|
||||
name: v.name,
|
||||
siteId: v.siteId ?? null,
|
||||
siteName: v.site?.name ?? null,
|
||||
isActive: v.isActive,
|
||||
}))}
|
||||
sites={sites}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,38 +5,25 @@ import { useRouter } from "next/navigation";
|
|||
import { AdminDialog } from "@/components/ui/admin-dialog";
|
||||
import { createVessel, updateVessel, toggleVesselActive } from "./actions";
|
||||
|
||||
type SiteOption = { id: string; name: string };
|
||||
|
||||
type VesselRow = {
|
||||
id: string;
|
||||
name: string;
|
||||
code: string;
|
||||
siteId: string | null;
|
||||
isActive: boolean;
|
||||
};
|
||||
|
||||
const INPUT = "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";
|
||||
|
||||
function VesselFormFields({
|
||||
vessel,
|
||||
sites,
|
||||
suggestedCode,
|
||||
}: {
|
||||
vessel?: VesselRow;
|
||||
sites: SiteOption[];
|
||||
suggestedCode?: string;
|
||||
}) {
|
||||
function VesselFormFields({ vessel, suggestedCode }: { vessel?: VesselRow; suggestedCode?: string }) {
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-neutral-700 mb-1">Code *</label>
|
||||
{vessel ? (
|
||||
/* Editing: show code read-only — code changes on existing data would break references */
|
||||
<p className="font-mono text-sm text-neutral-700 px-3 py-2 bg-neutral-50 rounded-lg border border-neutral-200">
|
||||
{vessel.code}
|
||||
</p>
|
||||
) : (
|
||||
/* Creating: editable, pre-filled with next available code */
|
||||
<input
|
||||
name="code"
|
||||
required
|
||||
|
|
@ -50,22 +37,11 @@ function VesselFormFields({
|
|||
<label className="block text-xs font-medium text-neutral-700 mb-1">Vessel Name *</label>
|
||||
<input name="name" defaultValue={vessel?.name} required className={INPUT} />
|
||||
</div>
|
||||
{sites.length > 0 && (
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-neutral-700 mb-1">Assigned Site</label>
|
||||
<select name="siteId" defaultValue={vessel?.siteId ?? ""} className={INPUT}>
|
||||
<option value="">— No site —</option>
|
||||
{sites.map((s) => (
|
||||
<option key={s.id} value={s.id}>{s.name}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function AddVesselButton({ sites, suggestedCode }: { sites: SiteOption[]; suggestedCode?: string }) {
|
||||
export function AddVesselButton({ suggestedCode }: { suggestedCode?: string }) {
|
||||
const router = useRouter();
|
||||
const [open, setOpen] = useState(false);
|
||||
const [pending, setPending] = useState(false);
|
||||
|
|
@ -88,7 +64,7 @@ export function AddVesselButton({ sites, suggestedCode }: { sites: SiteOption[];
|
|||
</button>
|
||||
<AdminDialog title="Add Vessel" open={open} onClose={() => setOpen(false)}>
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<VesselFormFields sites={sites} suggestedCode={suggestedCode} />
|
||||
<VesselFormFields suggestedCode={suggestedCode} />
|
||||
{error && <p className="text-sm text-danger-700 bg-danger-50 rounded-lg px-3 py-2">{error}</p>}
|
||||
<div className="flex justify-end gap-3 pt-1">
|
||||
<button type="button" onClick={() => setOpen(false)}
|
||||
|
|
@ -108,12 +84,10 @@ export function AddVesselButton({ sites, suggestedCode }: { sites: SiteOption[];
|
|||
|
||||
export function EditVesselButton({
|
||||
vessel,
|
||||
sites,
|
||||
open: controlledOpen,
|
||||
onOpenChange,
|
||||
}: {
|
||||
vessel: VesselRow;
|
||||
sites: SiteOption[];
|
||||
open?: boolean;
|
||||
onOpenChange?: (v: boolean) => void;
|
||||
}) {
|
||||
|
|
@ -147,7 +121,7 @@ export function EditVesselButton({
|
|||
)}
|
||||
<AdminDialog title="Edit Vessel" open={open} onClose={() => setOpen(false)}>
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<VesselFormFields vessel={vessel} sites={sites} />
|
||||
<VesselFormFields vessel={vessel} />
|
||||
{error && <p className="text-sm text-danger-700 bg-danger-50 rounded-lg px-3 py-2">{error}</p>}
|
||||
<div className="flex justify-end gap-3 pt-1">
|
||||
<button type="button" onClick={() => setOpen(false)}
|
||||
|
|
|
|||
|
|
@ -13,16 +13,12 @@ export type VesselRow = {
|
|||
id: string;
|
||||
code: string;
|
||||
name: string;
|
||||
siteId: string | null;
|
||||
siteName: string | null;
|
||||
isActive: boolean;
|
||||
};
|
||||
|
||||
type SiteOption = { id: string; name: string };
|
||||
|
||||
const CHIPS = ["Active", "Inactive"];
|
||||
|
||||
function VesselActionsMenu({ vessel, sites }: { vessel: VesselRow; sites: SiteOption[] }) {
|
||||
function VesselActionsMenu({ vessel }: { vessel: VesselRow }) {
|
||||
const [editOpen, setEditOpen] = useState(false);
|
||||
const [deleteOpen, setDeleteOpen] = useState(false);
|
||||
const [toggleOpen, setToggleOpen] = useState(false);
|
||||
|
|
@ -39,8 +35,7 @@ function VesselActionsMenu({ vessel, sites }: { vessel: VesselRow; sites: SiteOp
|
|||
</RowActionsMenu>
|
||||
|
||||
<EditVesselButton
|
||||
vessel={{ id: vessel.id, name: vessel.name, code: vessel.code, siteId: vessel.siteId, isActive: vessel.isActive }}
|
||||
sites={sites}
|
||||
vessel={{ id: vessel.id, name: vessel.name, code: vessel.code, isActive: vessel.isActive }}
|
||||
open={editOpen}
|
||||
onOpenChange={setEditOpen}
|
||||
/>
|
||||
|
|
@ -62,13 +57,12 @@ function VesselActionsMenu({ vessel, sites }: { vessel: VesselRow; sites: SiteOp
|
|||
);
|
||||
}
|
||||
|
||||
export function VesselsTable({ vessels, sites, suggestedCode }: { vessels: VesselRow[]; sites: SiteOption[]; suggestedCode?: string }) {
|
||||
export function VesselsTable({ vessels, suggestedCode }: { vessels: VesselRow[]; suggestedCode?: string }) {
|
||||
const { search, setSearch, sortKey, sortDir, toggleSort, activeFilters, toggleFilter, filtered } =
|
||||
useTableControls<VesselRow>({
|
||||
rows: vessels,
|
||||
defaultSortKey: "code",
|
||||
searchText: (v) =>
|
||||
[v.code, v.name, v.siteName ?? "", v.isActive ? "active" : "inactive"].join(" "),
|
||||
searchText: (v) => [v.code, v.name, v.isActive ? "active" : "inactive"].join(" "),
|
||||
chipMatch: (v, chip) => {
|
||||
if (chip.toLowerCase() === "active") return v.isActive;
|
||||
if (chip.toLowerCase() === "inactive") return !v.isActive;
|
||||
|
|
@ -76,7 +70,6 @@ export function VesselsTable({ vessels, sites, suggestedCode }: { vessels: Vesse
|
|||
},
|
||||
sortValue: (v, key) => {
|
||||
if (key === "isActive") return v.isActive ? "Active" : "Inactive";
|
||||
if (key === "siteName") return v.siteName ?? "";
|
||||
const val = v[key as keyof VesselRow];
|
||||
if (val === null || val === undefined) return "";
|
||||
return typeof val === "string" || typeof val === "number" || typeof val === "boolean" ? val : String(val);
|
||||
|
|
@ -87,7 +80,7 @@ export function VesselsTable({ vessels, sites, suggestedCode }: { vessels: Vesse
|
|||
<div>
|
||||
<div className="mb-6 flex items-center justify-between">
|
||||
<h1 className="text-2xl font-semibold text-neutral-900">Vessel Management</h1>
|
||||
<AddVesselButton sites={sites} suggestedCode={suggestedCode} />
|
||||
<AddVesselButton suggestedCode={suggestedCode} />
|
||||
</div>
|
||||
|
||||
<TableControls
|
||||
|
|
@ -105,7 +98,6 @@ export function VesselsTable({ vessels, sites, suggestedCode }: { vessels: Vesse
|
|||
<tr>
|
||||
<SortableTh sortKey="code" activeSortKey={sortKey as string | null} sortDir={sortDir} onSort={(k) => toggleSort(k as keyof VesselRow)}>Code</SortableTh>
|
||||
<SortableTh sortKey="name" activeSortKey={sortKey as string | null} sortDir={sortDir} onSort={(k) => toggleSort(k as keyof VesselRow)}>Name</SortableTh>
|
||||
<SortableTh sortKey="siteName" activeSortKey={sortKey as string | null} sortDir={sortDir} onSort={(k) => toggleSort(k as keyof VesselRow)}>Site</SortableTh>
|
||||
<SortableTh sortKey="isActive" activeSortKey={sortKey as string | null} sortDir={sortDir} onSort={(k) => toggleSort(k as keyof VesselRow)}>Status</SortableTh>
|
||||
<th className="px-4 py-3 w-10"></th>
|
||||
</tr>
|
||||
|
|
@ -113,7 +105,7 @@ export function VesselsTable({ vessels, sites, suggestedCode }: { vessels: Vesse
|
|||
<tbody className="divide-y divide-neutral-100">
|
||||
{filtered.length === 0 && (
|
||||
<tr>
|
||||
<td colSpan={5} className="px-4 py-8 text-center text-neutral-400">
|
||||
<td colSpan={4} className="px-4 py-8 text-center text-neutral-400">
|
||||
No vessels match your search.
|
||||
</td>
|
||||
</tr>
|
||||
|
|
@ -122,9 +114,6 @@ export function VesselsTable({ vessels, sites, suggestedCode }: { vessels: Vesse
|
|||
<tr key={vessel.id} className="hover:bg-neutral-50">
|
||||
<td className="px-4 py-3 font-mono text-xs text-neutral-600">{vessel.code}</td>
|
||||
<td className="px-4 py-3 font-medium text-neutral-900">{vessel.name}</td>
|
||||
<td className="px-4 py-3 text-neutral-500">
|
||||
{vessel.siteName ?? <span className="italic text-neutral-400">—</span>}
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
<span className={`rounded-full px-2.5 py-0.5 text-xs font-medium ${
|
||||
vessel.isActive ? "bg-success-100 text-success-700" : "bg-neutral-100 text-neutral-500"
|
||||
|
|
@ -133,7 +122,7 @@ export function VesselsTable({ vessels, sites, suggestedCode }: { vessels: Vesse
|
|||
</span>
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
<VesselActionsMenu vessel={vessel} sites={sites} />
|
||||
<VesselActionsMenu vessel={vessel} />
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import { LineItemsEditor } from "@/components/po/po-line-items-editor";
|
|||
import { TC_DEFAULTS, TC_FIXED_LINE } from "@/lib/validations/po";
|
||||
import type { LineItemInput } from "@/lib/validations/po";
|
||||
import type { Vendor, PurchaseOrder } from "@prisma/client";
|
||||
import type { CostCentreGroup, AccountGroup } from "@/app/(portal)/po/new/new-po-form";
|
||||
import type { VesselOption, AccountGroup } from "@/app/(portal)/po/new/new-po-form";
|
||||
import { SearchableSelect } from "@/components/ui/searchable-select";
|
||||
|
||||
type SerializedLineItem = {
|
||||
|
|
@ -35,8 +35,7 @@ type PoFull = Omit<PurchaseOrder, "totalAmount"> & {
|
|||
|
||||
interface Props {
|
||||
po: PoFull;
|
||||
costCentres: CostCentreGroup[];
|
||||
initialCostCentreRef: string;
|
||||
vessels: VesselOption[];
|
||||
accounts: AccountGroup[];
|
||||
vendors: Vendor[];
|
||||
}
|
||||
|
|
@ -51,7 +50,7 @@ function ManagerAccountSelect({ accountId, accounts }: { accountId: string; acco
|
|||
return <SearchableSelect name="accountId" value={value} onChange={setValue} groups={accounts} placeholder="Search accounting code…" required />;
|
||||
}
|
||||
|
||||
export function ManagerEditPoForm({ po, costCentres, initialCostCentreRef, accounts, vendors }: Props) {
|
||||
export function ManagerEditPoForm({ po, vessels, accounts, vendors }: Props) {
|
||||
const router = useRouter();
|
||||
const [editing, setEditing] = useState(false);
|
||||
const [pending, setPending] = useState(false);
|
||||
|
|
@ -160,17 +159,10 @@ export function ManagerEditPoForm({ po, costCentres, initialCostCentreRef, accou
|
|||
</div>
|
||||
<div>
|
||||
<label className={LABEL}>Cost Centre <span className="text-danger">*</span></label>
|
||||
<select name="costCentreRef" required defaultValue={initialCostCentreRef} className={INPUT}>
|
||||
<select name="vesselId" required defaultValue={po.vesselId ?? ""} className={INPUT}>
|
||||
<option value="">Select cost centre…</option>
|
||||
{costCentres.map((group) => (
|
||||
<optgroup key={group.siteId ?? "__unassigned"} label={group.siteName}>
|
||||
{group.siteRef && (
|
||||
<option value={group.siteRef}>{group.siteName} (Site)</option>
|
||||
)}
|
||||
{group.vessels.map((v) => (
|
||||
<option key={v.ref} value={v.ref}>{v.label}</option>
|
||||
))}
|
||||
</optgroup>
|
||||
{vessels.map((v) => (
|
||||
<option key={v.id} value={v.id}>{v.code} — {v.name}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ export async function managerEditPo(
|
|||
|
||||
const parsed = createPoSchema.safeParse({
|
||||
title: formData.get("title"),
|
||||
costCentreRef: formData.get("costCentreRef"),
|
||||
vesselId: formData.get("vesselId"),
|
||||
accountId: formData.get("accountId"),
|
||||
projectCode: formData.get("projectCode") || undefined,
|
||||
dateRequired: formData.get("dateRequired") || undefined,
|
||||
|
|
@ -67,8 +67,6 @@ export async function managerEditPo(
|
|||
}
|
||||
|
||||
const data = parsed.data;
|
||||
const newVesselId = data.costCentreRef.startsWith("v:") ? data.costCentreRef.slice(2) : null;
|
||||
const newCostCentreSiteId = data.costCentreRef.startsWith("s:") ? data.costCentreRef.slice(2) : null;
|
||||
const newTotal = data.lineItems.reduce(
|
||||
(sum, item) => sum + item.quantity * item.unitPrice * (1 + item.gstRate),
|
||||
0
|
||||
|
|
@ -85,7 +83,7 @@ export async function managerEditPo(
|
|||
};
|
||||
const original = {
|
||||
title: po.title,
|
||||
vesselId: po.vesselId ?? po.siteId ?? "",
|
||||
vesselId: po.vesselId,
|
||||
accountId: po.accountId,
|
||||
vendorId: po.vendorId,
|
||||
projectCode: po.projectCode,
|
||||
|
|
@ -114,8 +112,7 @@ export async function managerEditPo(
|
|||
where: { id: poId },
|
||||
data: {
|
||||
title: data.title,
|
||||
vesselId: newVesselId,
|
||||
siteId: newCostCentreSiteId,
|
||||
vesselId: data.vesselId,
|
||||
accountId: data.accountId,
|
||||
vendorId: data.vendorId ?? null,
|
||||
projectCode: data.projectCode ?? null,
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { notFound, redirect } from "next/navigation";
|
|||
import { ApprovalActions } from "./approval-actions";
|
||||
import { PoDetail } from "@/components/po/po-detail";
|
||||
import { ManagerEditPoForm } from "./manager-edit-po-form";
|
||||
import { buildCostCentreGroups, buildAccountGroups } from "@/lib/cost-centre-groups";
|
||||
import { buildAccountGroups } from "@/lib/cost-centre-groups";
|
||||
import type { Metadata } from "next";
|
||||
|
||||
interface Props {
|
||||
|
|
@ -22,14 +22,13 @@ export default async function ApprovalDetailPage({ params }: Props) {
|
|||
|
||||
const { id } = await params;
|
||||
|
||||
// Check if manager has uploaded a signature — required to approve
|
||||
const currentUser = await db.user.findUnique({
|
||||
where: { id: session.user.id },
|
||||
select: { signatureKey: true },
|
||||
});
|
||||
const hasSignature = !!(currentUser?.signatureKey);
|
||||
|
||||
const [po, vessels, sites, accounts, vendors] = await Promise.all([
|
||||
const [po, vessels, leafAccounts, vendors] = await Promise.all([
|
||||
db.purchaseOrder.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
|
|
@ -44,8 +43,7 @@ export default async function ApprovalDetailPage({ params }: Props) {
|
|||
receipt: true,
|
||||
},
|
||||
}),
|
||||
db.vessel.findMany({ where: { isActive: true }, orderBy: { name: "asc" }, select: { id: true, name: true, code: true, siteId: true } }),
|
||||
db.site.findMany({ where: { isActive: true }, orderBy: { name: "asc" }, select: { id: true, name: true } }),
|
||||
db.vessel.findMany({ where: { isActive: true }, orderBy: { name: "asc" }, select: { id: true, name: true, code: true } }),
|
||||
db.account.findMany({
|
||||
where: { isActive: true, children: { none: {} } },
|
||||
orderBy: { code: "asc" },
|
||||
|
|
@ -57,9 +55,7 @@ export default async function ApprovalDetailPage({ params }: Props) {
|
|||
if (!po) notFound();
|
||||
if (po.status !== "MGR_REVIEW") redirect(`/po/${id}`);
|
||||
|
||||
const costCentres = buildCostCentreGroups(vessels, sites);
|
||||
const accountGroups = buildAccountGroups(accounts);
|
||||
const initialCostCentreRef = po ? (po.vesselId ? `v:${po.vesselId}` : po.siteId ? `s:${po.siteId}` : "") : "";
|
||||
const accounts = buildAccountGroups(leafAccounts);
|
||||
|
||||
const serializedPo = {
|
||||
...po,
|
||||
|
|
@ -93,13 +89,11 @@ export default async function ApprovalDetailPage({ params }: Props) {
|
|||
|
||||
<PoDetail po={po} currentUserId={session.user.id} currentRole={session.user.role} readOnly />
|
||||
|
||||
{/* Direct field editing is desktop-only */}
|
||||
<div className="hidden md:block">
|
||||
<ManagerEditPoForm
|
||||
po={serializedPo}
|
||||
costCentres={costCentres}
|
||||
initialCostCentreRef={initialCostCentreRef}
|
||||
accounts={accountGroups}
|
||||
vessels={vessels}
|
||||
accounts={accounts}
|
||||
vendors={vendors}
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -115,10 +109,7 @@ export default async function ApprovalDetailPage({ params }: Props) {
|
|||
<p className="text-sm text-warning-700 mt-0.5">
|
||||
You must upload your approval signature before you can approve, reject, or request edits on purchase orders.
|
||||
</p>
|
||||
<a
|
||||
href="/profile"
|
||||
className="mt-2 inline-block text-sm font-medium text-primary-600 hover:text-primary-700 underline"
|
||||
>
|
||||
<a href="/profile" className="mt-2 inline-block text-sm font-medium text-primary-600 hover:text-primary-700 underline">
|
||||
Go to Profile to upload your signature →
|
||||
</a>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -4,33 +4,31 @@ import { useRouter, useSearchParams } from "next/navigation";
|
|||
import { useState } from "react";
|
||||
|
||||
interface Props {
|
||||
costCentres: { ref: string; name: string }[];
|
||||
vessels: { id: string; name: string }[];
|
||||
}
|
||||
|
||||
export function ApprovalsSearch({ costCentres }: Props) {
|
||||
export function ApprovalsSearch({ vessels }: Props) {
|
||||
const router = useRouter();
|
||||
const sp = useSearchParams();
|
||||
|
||||
const [q, setQ] = useState(sp.get("q") ?? "");
|
||||
const [costCentreRef, setCostCentreRef] = useState(sp.get("costCentreRef") ?? "");
|
||||
const [vesselId, setVesselId] = useState(sp.get("vesselId") ?? "");
|
||||
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 (vesselId) params.set("vesselId", vesselId);
|
||||
if (dateFrom) params.set("dateFrom", dateFrom);
|
||||
if (dateTo) params.set("dateTo", dateTo);
|
||||
router.push(`/approvals?${params.toString()}`);
|
||||
}
|
||||
|
||||
function clear() {
|
||||
setQ(""); setCostCentreRef(""); setDateFrom(""); setDateTo("");
|
||||
setQ(""); setVesselId(""); setDateFrom("");
|
||||
router.push("/approvals");
|
||||
}
|
||||
|
||||
const hasFilters = q || costCentreRef || dateFrom || dateTo;
|
||||
const hasFilters = q || vesselId || dateFrom;
|
||||
|
||||
return (
|
||||
<div className="mb-4 rounded-lg border border-neutral-200 bg-white p-4">
|
||||
|
|
@ -44,12 +42,10 @@ export function ApprovalsSearch({ costCentres }: Props) {
|
|||
</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)}
|
||||
<select value={vesselId} onChange={(e) => setVesselId(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>
|
||||
))}
|
||||
{vessels.map((v) => <option key={v.id} value={v.id}>{v.name}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
|
|
|
|||
|
|
@ -13,9 +13,8 @@ export const metadata: Metadata = { title: "Approvals" };
|
|||
interface Props {
|
||||
searchParams: Promise<{
|
||||
q?: string;
|
||||
costCentreRef?: string;
|
||||
vesselId?: string;
|
||||
dateFrom?: string;
|
||||
dateTo?: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
|
|
@ -25,7 +24,7 @@ export default async function ApprovalsPage({ searchParams }: Props) {
|
|||
|
||||
if (!hasPermission(session.user.role, "approve_po")) redirect("/dashboard");
|
||||
|
||||
const { q, costCentreRef, dateFrom } = await searchParams;
|
||||
const { q, vesselId, dateFrom } = await searchParams;
|
||||
|
||||
const where: NonNullable<Parameters<typeof db.purchaseOrder.findMany>[0]>["where"] = {
|
||||
status: "MGR_REVIEW",
|
||||
|
|
@ -38,27 +37,18 @@ export default async function ApprovalsPage({ searchParams }: Props) {
|
|||
{ title: { contains: q.trim(), mode: "insensitive" } },
|
||||
];
|
||||
}
|
||||
if (costCentreRef) {
|
||||
if (costCentreRef.startsWith("v:")) where.vesselId = costCentreRef.slice(2);
|
||||
else if (costCentreRef.startsWith("s:")) where.siteId = costCentreRef.slice(2);
|
||||
}
|
||||
if (vesselId) where.vesselId = vesselId;
|
||||
if (dateFrom) where.submittedAt = { gte: new Date(dateFrom) };
|
||||
|
||||
const [pending, vessels, sites] = await Promise.all([
|
||||
const [pending, vessels] = await Promise.all([
|
||||
db.purchaseOrder.findMany({
|
||||
where,
|
||||
include: { submitter: true, vessel: true, site: { select: { name: true } }, account: true },
|
||||
include: { submitter: true, vessel: true, account: true },
|
||||
orderBy: { submittedAt: "asc" },
|
||||
}),
|
||||
db.vessel.findMany({ orderBy: { name: "asc" }, select: { id: true, name: true } }),
|
||||
db.site.findMany({ orderBy: { name: "asc" }, select: { id: true, name: true } }),
|
||||
]);
|
||||
|
||||
const costCentres = [
|
||||
...vessels.map((v) => ({ ref: `v:${v.id}`, name: v.name })),
|
||||
...sites.map((s) => ({ ref: `s:${s.id}`, name: s.name })),
|
||||
];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="mb-4">
|
||||
|
|
@ -69,7 +59,7 @@ export default async function ApprovalsPage({ searchParams }: Props) {
|
|||
</div>
|
||||
|
||||
<Suspense>
|
||||
<ApprovalsSearch costCentres={costCentres} />
|
||||
<ApprovalsSearch vessels={vessels} />
|
||||
</Suspense>
|
||||
|
||||
{pending.length === 0 ? (
|
||||
|
|
@ -98,7 +88,7 @@ export default async function ApprovalsPage({ searchParams }: Props) {
|
|||
<td className="px-4 py-3 font-mono text-xs text-neutral-600">{po.poNumber}</td>
|
||||
<td className="px-4 py-3 font-medium text-neutral-900 max-w-xs truncate">{po.title}</td>
|
||||
<td className="px-4 py-3 text-neutral-600">{po.submitter.name}</td>
|
||||
<td className="px-4 py-3 text-neutral-600">{po.vessel?.name ?? po.site?.name ?? "—"}</td>
|
||||
<td className="px-4 py-3 text-neutral-600">{po.vessel.name}</td>
|
||||
<td className="px-4 py-3 text-right font-mono text-sm">
|
||||
{formatCurrency(Number(po.totalAmount), po.currency)}
|
||||
</td>
|
||||
|
|
@ -130,7 +120,7 @@ export default async function ApprovalsPage({ searchParams }: Props) {
|
|||
<p className="font-semibold text-neutral-900 leading-snug mb-2">{po.title}</p>
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span className="text-neutral-500 truncate max-w-[55%]">
|
||||
{po.submitter.name} · {po.vessel?.name ?? po.site?.name ?? "—"}
|
||||
{po.submitter.name} · {po.vessel.name}
|
||||
</span>
|
||||
<span className="font-mono font-semibold text-neutral-900 shrink-0">
|
||||
{formatCurrency(Number(po.totalAmount), po.currency)}
|
||||
|
|
|
|||
|
|
@ -206,7 +206,7 @@ async function ManagerDashboard() {
|
|||
</Link>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-neutral-900 max-w-xs truncate">{po.title}</td>
|
||||
<td className="px-4 py-3 text-neutral-600">{po.vessel?.name ?? po.site?.name ?? "—"}</td>
|
||||
<td className="px-4 py-3 text-neutral-600">{po.vessel.name}</td>
|
||||
<td className="px-4 py-3">
|
||||
<PoStatusBadge status={po.status} />
|
||||
</td>
|
||||
|
|
|
|||
|
|
@ -18,33 +18,33 @@ const STATUSES = [
|
|||
];
|
||||
|
||||
interface Props {
|
||||
costCentres: { ref: string; name: string }[];
|
||||
vessels: { id: string; name: string }[];
|
||||
}
|
||||
|
||||
export function HistoryFilters({ costCentres }: Props) {
|
||||
export function HistoryFilters({ vessels }: Props) {
|
||||
const router = useRouter();
|
||||
const sp = useSearchParams();
|
||||
|
||||
const [dateFrom, setDateFrom] = useState(sp.get("dateFrom") ?? "");
|
||||
const [dateTo, setDateTo] = useState(sp.get("dateTo") ?? "");
|
||||
const [costCentreRef, setCostCentreRef] = useState(sp.get("costCentreRef") ?? "");
|
||||
const [vesselId, setVesselId] = useState(sp.get("vesselId") ?? "");
|
||||
const [status, setStatus] = useState(sp.get("status") ?? "");
|
||||
|
||||
function apply() {
|
||||
const params = new URLSearchParams();
|
||||
if (dateFrom) params.set("dateFrom", dateFrom);
|
||||
if (dateTo) params.set("dateTo", dateTo);
|
||||
if (costCentreRef) params.set("costCentreRef", costCentreRef);
|
||||
if (vesselId) params.set("vesselId", vesselId);
|
||||
if (status) params.set("status", status);
|
||||
router.push(`/history?${params.toString()}`);
|
||||
}
|
||||
|
||||
function clear() {
|
||||
setDateFrom(""); setDateTo(""); setCostCentreRef(""); setStatus("");
|
||||
setDateFrom(""); setDateTo(""); setVesselId(""); setStatus("");
|
||||
router.push("/history");
|
||||
}
|
||||
|
||||
const hasFilters = dateFrom || dateTo || costCentreRef || status;
|
||||
const hasFilters = dateFrom || dateTo || vesselId || status;
|
||||
|
||||
return (
|
||||
<div className="mb-4 rounded-lg border border-neutral-200 bg-white p-4">
|
||||
|
|
@ -61,12 +61,10 @@ export function HistoryFilters({ costCentres }: Props) {
|
|||
</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)}
|
||||
<select value={vesselId} onChange={(e) => setVesselId(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>
|
||||
))}
|
||||
{vessels.map((v) => <option key={v.id} value={v.id}>{v.name}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { db } from "@/lib/db";
|
|||
import { hasPermission } from "@/lib/permissions";
|
||||
import { redirect } from "next/navigation";
|
||||
import Link from "next/link";
|
||||
import { formatCurrency, formatDate, PO_STATUS_LABELS } from "@/lib/utils";
|
||||
import { formatCurrency, formatDate } from "@/lib/utils";
|
||||
import { PoStatusBadge } from "@/components/po/po-status-badge";
|
||||
import { HistoryFilters } from "./history-filters";
|
||||
import { Suspense } from "react";
|
||||
|
|
@ -16,7 +16,7 @@ interface Props {
|
|||
searchParams: Promise<{
|
||||
dateFrom?: string;
|
||||
dateTo?: string;
|
||||
costCentreRef?: string;
|
||||
vesselId?: string;
|
||||
status?: string;
|
||||
}>;
|
||||
}
|
||||
|
|
@ -27,7 +27,7 @@ export default async function HistoryPage({ searchParams }: Props) {
|
|||
|
||||
if (!hasPermission(session.user.role, "export_reports")) redirect("/dashboard");
|
||||
|
||||
const { dateFrom, dateTo, costCentreRef, status } = await searchParams;
|
||||
const { dateFrom, dateTo, vesselId, status } = await searchParams;
|
||||
|
||||
const where: NonNullable<Parameters<typeof db.purchaseOrder.findMany>[0]>["where"] = {};
|
||||
if (dateFrom || dateTo) {
|
||||
|
|
@ -40,32 +40,23 @@ export default async function HistoryPage({ searchParams }: Props) {
|
|||
}
|
||||
where.createdAt = createdAt;
|
||||
}
|
||||
if (costCentreRef) {
|
||||
if (costCentreRef.startsWith("v:")) where.vesselId = costCentreRef.slice(2);
|
||||
else if (costCentreRef.startsWith("s:")) where.siteId = costCentreRef.slice(2);
|
||||
}
|
||||
if (vesselId) where.vesselId = vesselId;
|
||||
if (status) where.status = status as POStatus;
|
||||
|
||||
const [orders, vessels, sites] = await Promise.all([
|
||||
const [orders, vessels] = await Promise.all([
|
||||
db.purchaseOrder.findMany({
|
||||
where,
|
||||
include: { submitter: true, vessel: true, site: { select: { name: true } }, account: true },
|
||||
include: { submitter: true, vessel: true, account: true },
|
||||
orderBy: { createdAt: "desc" },
|
||||
take: 200,
|
||||
}),
|
||||
db.vessel.findMany({ orderBy: { name: "asc" }, select: { id: true, name: true } }),
|
||||
db.site.findMany({ orderBy: { name: "asc" }, select: { id: true, name: true } }),
|
||||
]);
|
||||
|
||||
const costCentres = [
|
||||
...vessels.map((v) => ({ ref: `v:${v.id}`, name: v.name })),
|
||||
...sites.map((s) => ({ ref: `s:${s.id}`, name: s.name })),
|
||||
];
|
||||
|
||||
const exportParams = new URLSearchParams({ format: "csv" });
|
||||
if (dateFrom) exportParams.set("dateFrom", dateFrom);
|
||||
if (dateTo) exportParams.set("dateTo", dateTo);
|
||||
if (costCentreRef) exportParams.set("costCentreRef", costCentreRef);
|
||||
if (vesselId) exportParams.set("vesselId", vesselId);
|
||||
if (status) exportParams.set("status", status);
|
||||
|
||||
return (
|
||||
|
|
@ -91,7 +82,7 @@ export default async function HistoryPage({ searchParams }: Props) {
|
|||
</div>
|
||||
|
||||
<Suspense>
|
||||
<HistoryFilters costCentres={costCentres} />
|
||||
<HistoryFilters vessels={vessels} />
|
||||
</Suspense>
|
||||
|
||||
<div className="rounded-lg border border-neutral-200 bg-white overflow-hidden">
|
||||
|
|
@ -116,7 +107,7 @@ export default async function HistoryPage({ searchParams }: Props) {
|
|||
</Link>
|
||||
</td>
|
||||
<td className="px-4 py-3 font-medium text-neutral-900 max-w-xs truncate">{po.title}</td>
|
||||
<td className="px-4 py-3 text-neutral-600">{po.vessel?.name ?? po.site?.name ?? "—"}</td>
|
||||
<td className="px-4 py-3 text-neutral-600">{po.vessel.name}</td>
|
||||
<td className="px-4 py-3 text-neutral-600">{po.submitter.name}</td>
|
||||
<td className="px-4 py-3">
|
||||
<PoStatusBadge status={po.status} />
|
||||
|
|
|
|||
|
|
@ -23,7 +23,6 @@ export default async function MyOrdersPage() {
|
|||
orderBy: { updatedAt: "desc" },
|
||||
include: {
|
||||
vessel: { select: { name: true } },
|
||||
site: { select: { name: true } },
|
||||
account: { select: { name: true, code: true } },
|
||||
actions: {
|
||||
where: {
|
||||
|
|
@ -68,8 +67,7 @@ type PoRow = {
|
|||
title: string;
|
||||
status: import("@prisma/client").POStatus;
|
||||
totalAmount: import("@prisma/client").Prisma.Decimal;
|
||||
vessel: { name: string } | null;
|
||||
site: { name: string } | null;
|
||||
vessel: { name: string };
|
||||
account: { name: string; code: string };
|
||||
updatedAt: Date;
|
||||
managerNote: string | null;
|
||||
|
|
@ -111,7 +109,7 @@ function PoTable({ title, rows, className = "" }: { title: string; rows: PoRow[]
|
|||
</p>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-neutral-600">{po.vessel?.name ?? po.site?.name ?? "—"}</td>
|
||||
<td className="px-4 py-3 text-neutral-600">{po.vessel.name}</td>
|
||||
<td className="px-4 py-3"><PoStatusBadge status={po.status} /></td>
|
||||
<td className="px-4 py-3 text-right font-mono text-xs">{formatCurrency(Number(po.totalAmount))}</td>
|
||||
<td className="px-4 py-3 text-neutral-500">{formatDate(po.updatedAt)}</td>
|
||||
|
|
|
|||
|
|
@ -117,7 +117,7 @@ export default async function PaymentHistoryPage({ searchParams }: Props) {
|
|||
<td className="px-4 py-3 font-medium text-neutral-900 max-w-xs truncate">
|
||||
{po.title}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-neutral-600">{po.vessel?.name ?? po.site?.name ?? "—"}</td>
|
||||
<td className="px-4 py-3 text-neutral-600">{po.vessel.name}</td>
|
||||
<td className="px-4 py-3 text-neutral-600">{po.vendor?.name ?? "—"}</td>
|
||||
<td className="px-4 py-3 text-neutral-600">{po.submitter.name}</td>
|
||||
<td className="px-4 py-3">
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ export default async function PaymentsPage() {
|
|||
</div>
|
||||
<h3 className="font-medium text-neutral-900 truncate">{po.title}</h3>
|
||||
<div className="mt-1 flex flex-wrap gap-3 text-sm text-neutral-500">
|
||||
<span>{po.vessel?.name ?? po.site?.name ?? "—"}</span>
|
||||
<span>{po.vessel.name}</span>
|
||||
<span>·</span>
|
||||
<span>{po.submitter.name}</span>
|
||||
{po.vendor && (
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ export async function updatePo(
|
|||
|
||||
const parsed = createPoSchema.safeParse({
|
||||
title: formData.get("title"),
|
||||
costCentreRef: formData.get("costCentreRef"),
|
||||
vesselId: formData.get("vesselId"),
|
||||
accountId: formData.get("accountId"),
|
||||
projectCode: formData.get("projectCode") || undefined,
|
||||
dateRequired: formData.get("dateRequired") || undefined,
|
||||
|
|
@ -69,8 +69,6 @@ export async function updatePo(
|
|||
}
|
||||
|
||||
const data = parsed.data;
|
||||
const newVesselId = data.costCentreRef.startsWith("v:") ? data.costCentreRef.slice(2) : null;
|
||||
const newCostCentreSiteId = data.costCentreRef.startsWith("s:") ? data.costCentreRef.slice(2) : null;
|
||||
const total = data.lineItems.reduce(
|
||||
(sum, item) => sum + item.quantity * item.unitPrice * (1 + item.gstRate),
|
||||
0
|
||||
|
|
@ -102,7 +100,6 @@ export async function updatePo(
|
|||
include: {
|
||||
lineItems: { orderBy: { sortOrder: "asc" } },
|
||||
vessel: true,
|
||||
site: { select: { name: true } },
|
||||
account: true,
|
||||
vendor: true,
|
||||
},
|
||||
|
|
@ -120,8 +117,8 @@ export async function updatePo(
|
|||
})),
|
||||
fields: {
|
||||
title: currentPo.title,
|
||||
vessel: currentPo.vessel?.name ?? currentPo.site?.name ?? null,
|
||||
vesselId: currentPo.vesselId ?? currentPo.siteId ?? "",
|
||||
vessel: currentPo.vessel?.name ?? null,
|
||||
vesselId: currentPo.vesselId,
|
||||
account: `${currentPo.account.name} (${currentPo.account.code})`,
|
||||
accountId: currentPo.accountId,
|
||||
vendor: currentPo.vendor?.name ?? null,
|
||||
|
|
@ -138,8 +135,7 @@ export async function updatePo(
|
|||
where: { id: poId },
|
||||
data: {
|
||||
title: data.title,
|
||||
vesselId: newVesselId,
|
||||
siteId: newCostCentreSiteId,
|
||||
vesselId: data.vesselId,
|
||||
accountId: data.accountId,
|
||||
vendorId: data.vendorId ?? null,
|
||||
projectCode: data.projectCode ?? null,
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { useState } from "react";
|
|||
import { useRouter } from "next/navigation";
|
||||
import { updatePo } from "./actions";
|
||||
import type { Vendor, PurchaseOrder } from "@prisma/client";
|
||||
import type { CostCentreGroup, AccountGroup } from "@/app/(portal)/po/new/new-po-form";
|
||||
import type { VesselOption, AccountGroup } from "@/app/(portal)/po/new/new-po-form";
|
||||
import { LineItemsEditor } from "@/components/po/po-line-items-editor";
|
||||
import { SearchableSelect } from "@/components/ui/searchable-select";
|
||||
import type { LineItemInput } from "@/lib/validations/po";
|
||||
|
|
@ -36,14 +36,13 @@ type PoWithItems = Omit<PurchaseOrder, "totalAmount"> & {
|
|||
|
||||
interface Props {
|
||||
po: PoWithItems;
|
||||
costCentres: CostCentreGroup[];
|
||||
initialCostCentreRef: string;
|
||||
vessels: VesselOption[];
|
||||
accounts: AccountGroup[];
|
||||
vendors: Vendor[];
|
||||
managerNoteAuthor?: string | null;
|
||||
}
|
||||
|
||||
export function EditPoForm({ po, costCentres, initialCostCentreRef, accounts, vendors, managerNoteAuthor }: Props) {
|
||||
export function EditPoForm({ po, vessels, accounts, vendors, managerNoteAuthor }: Props) {
|
||||
const router = useRouter();
|
||||
const [lineItems, setLineItems] = useState<LineItemInput[]>(
|
||||
po.lineItems.map((li) => ({
|
||||
|
|
@ -131,17 +130,10 @@ export function EditPoForm({ po, costCentres, initialCostCentreRef, accounts, ve
|
|||
<label className="block text-sm font-medium text-neutral-700 mb-1.5">
|
||||
Cost Centre <span className="text-danger">*</span>
|
||||
</label>
|
||||
<select name="costCentreRef" required defaultValue={initialCostCentreRef} className={INPUT_CLS}>
|
||||
<select name="vesselId" required defaultValue={po.vesselId} className={INPUT_CLS}>
|
||||
<option value="">Select cost centre…</option>
|
||||
{costCentres.map((group) => (
|
||||
<optgroup key={group.siteId ?? "__unassigned"} label={group.siteName}>
|
||||
{group.siteRef && (
|
||||
<option value={group.siteRef}>{group.siteName} (Site)</option>
|
||||
)}
|
||||
{group.vessels.map((v) => (
|
||||
<option key={v.ref} value={v.ref}>{v.label}</option>
|
||||
))}
|
||||
</optgroup>
|
||||
{vessels.map((v) => (
|
||||
<option key={v.id} value={v.id}>{v.code} — {v.name}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { auth } from "@/auth";
|
|||
import { db } from "@/lib/db";
|
||||
import { notFound, redirect } from "next/navigation";
|
||||
import { EditPoForm } from "./edit-po-form";
|
||||
import { buildCostCentreGroups, buildAccountGroups } from "@/lib/cost-centre-groups";
|
||||
import { buildAccountGroups } from "@/lib/cost-centre-groups";
|
||||
import type { Metadata } from "next";
|
||||
|
||||
interface Props {
|
||||
|
|
@ -23,15 +23,13 @@ export default async function EditPoPage({ params }: Props) {
|
|||
});
|
||||
|
||||
if (!po) notFound();
|
||||
|
||||
if (!["DRAFT", "EDITS_REQUESTED"].includes(po.status)) redirect(`/po/${id}`);
|
||||
|
||||
const canEdit = po.submitterId === session.user.id || session.user.role === "SUPERUSER";
|
||||
if (!canEdit) redirect(`/po/${id}`);
|
||||
|
||||
const [vessels, sites, leafAccounts, vendors, noteAction] = await Promise.all([
|
||||
db.vessel.findMany({ where: { isActive: true }, orderBy: { name: "asc" }, select: { id: true, name: true, code: true, siteId: true } }),
|
||||
db.site.findMany({ where: { isActive: true }, orderBy: { name: "asc" }, select: { id: true, name: true } }),
|
||||
const [vessels, leafAccounts, vendors, noteAction] = await Promise.all([
|
||||
db.vessel.findMany({ where: { isActive: true }, orderBy: { name: "asc" }, select: { id: true, name: true, code: true } }),
|
||||
db.account.findMany({
|
||||
where: { isActive: true, children: { none: {} } },
|
||||
orderBy: { code: "asc" },
|
||||
|
|
@ -47,9 +45,7 @@ export default async function EditPoPage({ params }: Props) {
|
|||
: Promise.resolve(null),
|
||||
]);
|
||||
|
||||
const costCentres = buildCostCentreGroups(vessels, sites);
|
||||
const accountGroups = buildAccountGroups(leafAccounts);
|
||||
const initialCostCentreRef = po.vesselId ? `v:${po.vesselId}` : po.siteId ? `s:${po.siteId}` : "";
|
||||
const accounts = buildAccountGroups(leafAccounts);
|
||||
|
||||
const serializedPo = {
|
||||
...po,
|
||||
|
|
@ -71,9 +67,8 @@ export default async function EditPoPage({ params }: Props) {
|
|||
</div>
|
||||
<EditPoForm
|
||||
po={serializedPo}
|
||||
costCentres={costCentres}
|
||||
initialCostCentreRef={initialCostCentreRef}
|
||||
accounts={accountGroups}
|
||||
vessels={vessels}
|
||||
accounts={accounts}
|
||||
vendors={vendors}
|
||||
managerNoteAuthor={noteAction?.actor.name ?? null}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -26,7 +26,6 @@ export default async function PoDetailPage({ params }: Props) {
|
|||
include: {
|
||||
submitter: true,
|
||||
vessel: true,
|
||||
site: { select: { id: true, name: true } },
|
||||
account: true,
|
||||
vendor: true,
|
||||
lineItems: { orderBy: { sortOrder: "asc" } },
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ export async function confirmReceipt({
|
|||
include: {
|
||||
submitter: true,
|
||||
lineItems: true,
|
||||
vessel: { include: { site: true } },
|
||||
vessel: true,
|
||||
},
|
||||
});
|
||||
if (!po) return { error: "PO not found" };
|
||||
|
|
@ -134,7 +134,6 @@ export async function confirmReceipt({
|
|||
// Auto-update inventory for delivered quantities
|
||||
const siteId =
|
||||
(po as typeof po & { siteId?: string | null }).siteId ??
|
||||
po.vessel?.site?.id ??
|
||||
null;
|
||||
|
||||
if (siteId) {
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import type { ParsedImportLine } from "@/app/api/po/import/route";
|
|||
|
||||
export type ImportPoInput = {
|
||||
title: string;
|
||||
costCentreRef: string;
|
||||
vesselId: string;
|
||||
accountId: string;
|
||||
vendorId?: string;
|
||||
piQuotationNo?: string;
|
||||
|
|
@ -32,9 +32,6 @@ export async function importPo(
|
|||
return { error: "You do not have permission to import purchase orders." };
|
||||
}
|
||||
|
||||
const importVesselId = input.costCentreRef.startsWith("v:") ? input.costCentreRef.slice(2) : null;
|
||||
const importSiteId = input.costCentreRef.startsWith("s:") ? input.costCentreRef.slice(2) : null;
|
||||
|
||||
const total = input.lineItems.reduce(
|
||||
(sum, item) => sum + item.quantity * item.unitPrice * (1 + (item.gstRate ?? 0.18)),
|
||||
0
|
||||
|
|
@ -47,8 +44,7 @@ export async function importPo(
|
|||
status: "DRAFT",
|
||||
totalAmount: total,
|
||||
currency: "INR",
|
||||
vesselId: importVesselId,
|
||||
siteId: importSiteId,
|
||||
vesselId: input.vesselId,
|
||||
accountId: input.accountId,
|
||||
vendorId: input.vendorId ?? null,
|
||||
piQuotationNo: input.piQuotationNo ?? null,
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
import { useState, useRef } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import type { Vendor } from "@prisma/client";
|
||||
import type { CostCentreGroup, AccountGroup } from "@/app/(portal)/po/new/new-po-form";
|
||||
import type { VesselOption, AccountGroup } from "@/app/(portal)/po/new/new-po-form";
|
||||
import { SearchableSelect } from "@/components/ui/searchable-select";
|
||||
import { importPo } from "./actions";
|
||||
import type { ParsedImport } from "@/app/api/po/import/route";
|
||||
|
|
@ -13,7 +13,7 @@ const INPUT_CLS =
|
|||
"w-full rounded-lg border border-neutral-300 px-3 py-2.5 text-sm focus:border-primary-500 focus:outline-none focus:ring-2 focus:ring-primary-500/20";
|
||||
|
||||
interface Props {
|
||||
costCentres: CostCentreGroup[];
|
||||
vessels: VesselOption[];
|
||||
accounts: AccountGroup[];
|
||||
vendors: Vendor[];
|
||||
}
|
||||
|
|
@ -21,12 +21,12 @@ interface Props {
|
|||
type PreviewState = {
|
||||
parsed: ParsedImport;
|
||||
title: string;
|
||||
costCentreRef: string;
|
||||
vesselId: string;
|
||||
accountId: string;
|
||||
vendorId: string;
|
||||
};
|
||||
|
||||
export function ImportForm({ costCentres, accounts, vendors }: Props) {
|
||||
export function ImportForm({ vessels, accounts, vendors }: Props) {
|
||||
const router = useRouter();
|
||||
const fileRef = useRef<HTMLInputElement>(null);
|
||||
const [parsing, setParsing] = useState(false);
|
||||
|
|
@ -66,7 +66,7 @@ export function ImportForm({ costCentres, accounts, vendors }: Props) {
|
|||
title: parsed.vendorName
|
||||
? `${parsed.vendorName} — Import`
|
||||
: "Imported Purchase Order",
|
||||
costCentreRef: costCentres[0]?.siteRef ?? costCentres[0]?.vessels[0]?.ref ?? "",
|
||||
vesselId: vessels[0]?.id ?? "",
|
||||
accountId: accounts[0]?.items[0]?.id ?? "",
|
||||
vendorId: matchedVendor?.id ?? "",
|
||||
});
|
||||
|
|
@ -85,7 +85,7 @@ export function ImportForm({ costCentres, accounts, vendors }: Props) {
|
|||
|
||||
const result = await importPo({
|
||||
title: preview.title,
|
||||
costCentreRef: preview.costCentreRef,
|
||||
vesselId: preview.vesselId,
|
||||
accountId: preview.accountId,
|
||||
vendorId: preview.vendorId || undefined,
|
||||
piQuotationNo: preview.parsed.piQuotationNo || undefined,
|
||||
|
|
@ -184,21 +184,14 @@ export function ImportForm({ costCentres, accounts, vendors }: Props) {
|
|||
Cost Centre <span className="text-danger">*</span>
|
||||
</label>
|
||||
<select
|
||||
value={preview.costCentreRef}
|
||||
onChange={(e) => setPreview({ ...preview, costCentreRef: e.target.value })}
|
||||
value={preview.vesselId}
|
||||
onChange={(e) => setPreview({ ...preview, vesselId: e.target.value })}
|
||||
required
|
||||
className={INPUT_CLS}
|
||||
>
|
||||
<option value="">Select cost centre…</option>
|
||||
{costCentres.map((group) => (
|
||||
<optgroup key={group.siteId ?? "__unassigned"} label={group.siteName}>
|
||||
{group.siteRef && (
|
||||
<option value={group.siteRef}>{group.siteName} (Site)</option>
|
||||
)}
|
||||
{group.vessels.map((v) => (
|
||||
<option key={v.ref} value={v.ref}>{v.label}</option>
|
||||
))}
|
||||
</optgroup>
|
||||
{vessels.map((v) => (
|
||||
<option key={v.id} value={v.id}>{v.code} — {v.name}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
|
@ -295,7 +288,7 @@ export function ImportForm({ costCentres, accounts, vendors }: Props) {
|
|||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={submitting || !preview.costCentreRef || !preview.accountId}
|
||||
disabled={submitting || !preview.vesselId || !preview.accountId}
|
||||
className="rounded-lg bg-primary-600 px-5 py-2.5 text-sm font-semibold text-white hover:bg-primary-700 disabled:opacity-60 transition-colors"
|
||||
>
|
||||
{submitting ? "Creating…" : "Create as Draft"}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { auth } from "@/auth";
|
|||
import { db } from "@/lib/db";
|
||||
import { redirect } from "next/navigation";
|
||||
import { ImportForm } from "./import-form";
|
||||
import { buildCostCentreGroups, buildAccountGroups } from "@/lib/cost-centre-groups";
|
||||
import { buildAccountGroups } from "@/lib/cost-centre-groups";
|
||||
import type { Metadata } from "next";
|
||||
|
||||
export const metadata: Metadata = { title: "Import Purchase Order" };
|
||||
|
|
@ -14,9 +14,8 @@ export default async function ImportPoPage() {
|
|||
const { role } = session.user;
|
||||
if (!["MANAGER", "SUPERUSER", "ADMIN"].includes(role)) redirect("/dashboard");
|
||||
|
||||
const [vessels, sites, leafAccounts, vendors] = await Promise.all([
|
||||
db.vessel.findMany({ where: { isActive: true }, orderBy: { name: "asc" }, select: { id: true, name: true, code: true, siteId: true } }),
|
||||
db.site.findMany({ where: { isActive: true }, orderBy: { name: "asc" }, select: { id: true, name: true } }),
|
||||
const [vessels, leafAccounts, vendors] = await Promise.all([
|
||||
db.vessel.findMany({ where: { isActive: true }, orderBy: { name: "asc" }, select: { id: true, name: true, code: true } }),
|
||||
db.account.findMany({
|
||||
where: { isActive: true, children: { none: {} } },
|
||||
orderBy: { code: "asc" },
|
||||
|
|
@ -25,7 +24,6 @@ export default async function ImportPoPage() {
|
|||
db.vendor.findMany({ orderBy: { name: "asc" } }),
|
||||
]);
|
||||
|
||||
const costCentres = buildCostCentreGroups(vessels, sites);
|
||||
const accounts = buildAccountGroups(leafAccounts);
|
||||
|
||||
return (
|
||||
|
|
@ -37,7 +35,7 @@ export default async function ImportPoPage() {
|
|||
You then select the cost centre, accounting code, and confirm before saving as a draft.
|
||||
</p>
|
||||
</div>
|
||||
<ImportForm costCentres={costCentres} accounts={accounts} vendors={vendors} />
|
||||
<ImportForm vessels={vessels} accounts={accounts} vendors={vendors} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ export async function createPo(
|
|||
|
||||
const parsed = createPoSchema.safeParse({
|
||||
title: formData.get("title"),
|
||||
costCentreRef: formData.get("costCentreRef"),
|
||||
vesselId: formData.get("vesselId"),
|
||||
accountId: formData.get("accountId"),
|
||||
projectCode: formData.get("projectCode") || undefined,
|
||||
dateRequired: formData.get("dateRequired") || undefined,
|
||||
|
|
@ -75,9 +75,6 @@ export async function createPo(
|
|||
}
|
||||
|
||||
const data = parsed.data;
|
||||
const vesselId = data.costCentreRef.startsWith("v:") ? data.costCentreRef.slice(2) : null;
|
||||
const costCentreSiteId = data.costCentreRef.startsWith("s:") ? data.costCentreRef.slice(2) : null;
|
||||
|
||||
// totalAmount = grand total including GST
|
||||
const total = data.lineItems.reduce(
|
||||
(sum, item) => sum + item.quantity * item.unitPrice * (1 + item.gstRate),
|
||||
|
|
@ -91,8 +88,7 @@ export async function createPo(
|
|||
status: intent === "submit" ? "SUBMITTED" : "DRAFT",
|
||||
totalAmount: total,
|
||||
currency: data.currency,
|
||||
vesselId,
|
||||
siteId: costCentreSiteId,
|
||||
vesselId: data.vesselId,
|
||||
accountId: data.accountId,
|
||||
vendorId: data.vendorId ?? null,
|
||||
projectCode: data.projectCode ?? null,
|
||||
|
|
|
|||
|
|
@ -11,15 +11,7 @@ import { uploadAndLinkFiles } from "@/lib/upload-files";
|
|||
import type { LineItemInput } from "@/lib/validations/po";
|
||||
import { TC_DEFAULTS, TC_FIXED_LINE, TC_FIXED_LINE_2 } from "@/lib/validations/po";
|
||||
|
||||
// Cost centres grouped by site: the site itself is selectable, vessels are listed under it
|
||||
export type CostCentreGroup = {
|
||||
siteId: string | null; // null = "Unassigned Vessels" fallback group
|
||||
siteName: string;
|
||||
siteRef: string | null; // "s:siteId" — selectable; null = no site option
|
||||
vessels: { ref: string; label: string }[];
|
||||
};
|
||||
|
||||
// Accounting codes grouped by sub-category
|
||||
export type VesselOption = { id: string; code: string; name: string };
|
||||
export type AccountGroup = { group: string; items: { id: string; code: string; name: string }[] };
|
||||
|
||||
const INPUT_CLS =
|
||||
|
|
@ -28,15 +20,15 @@ const INPUT_CLS =
|
|||
const EMPTY_LINE: LineItemInput = { name: "", description: "", quantity: 1, unit: "pc", size: "", unitPrice: 0, gstRate: 0.18 };
|
||||
|
||||
interface Props {
|
||||
costCentres: CostCentreGroup[];
|
||||
vessels: VesselOption[];
|
||||
accounts: AccountGroup[];
|
||||
vendors: Vendor[];
|
||||
initialLineItems?: LineItemInput[];
|
||||
initialVendorId?: string;
|
||||
initialCostCentreRef?: string;
|
||||
initialVesselId?: string;
|
||||
}
|
||||
|
||||
export function NewPoForm({ costCentres, accounts, vendors, initialLineItems, initialVendorId, initialCostCentreRef }: Props) {
|
||||
export function NewPoForm({ vessels, accounts, vendors, initialLineItems, initialVendorId, initialVesselId }: Props) {
|
||||
const router = useRouter();
|
||||
const [lineItems, setLineItems] = useState<LineItemInput[]>(
|
||||
initialLineItems && initialLineItems.length > 0 ? initialLineItems : [EMPTY_LINE]
|
||||
|
|
@ -96,22 +88,15 @@ export function NewPoForm({ costCentres, accounts, vendors, initialLineItems, in
|
|||
<input name="title" required className={INPUT_CLS} placeholder="Brief description of what is being ordered" />
|
||||
</div>
|
||||
|
||||
{/* Cost Centre — grouped by site */}
|
||||
{/* Cost Centre — vessels only */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-neutral-700 mb-1.5">
|
||||
Cost Centre <span className="text-danger">*</span>
|
||||
</label>
|
||||
<select name="costCentreRef" required defaultValue={initialCostCentreRef ?? ""} className={INPUT_CLS}>
|
||||
<select name="vesselId" required defaultValue={initialVesselId ?? ""} className={INPUT_CLS}>
|
||||
<option value="">Select cost centre…</option>
|
||||
{costCentres.map((group) => (
|
||||
<optgroup key={group.siteId ?? "__unassigned"} label={group.siteName}>
|
||||
{group.siteRef && (
|
||||
<option value={group.siteRef}>{group.siteName} (Site)</option>
|
||||
)}
|
||||
{group.vessels.map((v) => (
|
||||
<option key={v.ref} value={v.ref}>{v.label}</option>
|
||||
))}
|
||||
</optgroup>
|
||||
{vessels.map((v) => (
|
||||
<option key={v.id} value={v.id}>{v.code} — {v.name}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { db } from "@/lib/db";
|
|||
import { hasPermission } from "@/lib/permissions";
|
||||
import { redirect } from "next/navigation";
|
||||
import { NewPoForm } from "./new-po-form";
|
||||
import { buildCostCentreGroups, buildAccountGroups } from "@/lib/cost-centre-groups";
|
||||
import { buildAccountGroups } from "@/lib/cost-centre-groups";
|
||||
import type { Metadata } from "next";
|
||||
import type { LineItemInput } from "@/lib/validations/po";
|
||||
import type { CartItem } from "@/lib/cart";
|
||||
|
|
@ -11,18 +11,16 @@ import type { CartItem } from "@/lib/cart";
|
|||
export const metadata: Metadata = { title: "New Purchase Order" };
|
||||
|
||||
interface Props {
|
||||
searchParams: Promise<{ cart?: string; costCentreRef?: string }>;
|
||||
searchParams: Promise<{ cart?: string; vesselId?: string }>;
|
||||
}
|
||||
|
||||
export default async function NewPoPage({ searchParams }: Props) {
|
||||
const session = await auth();
|
||||
if (!session?.user) redirect("/login");
|
||||
|
||||
if (!hasPermission(session.user.role, "create_po")) {
|
||||
redirect("/dashboard");
|
||||
}
|
||||
if (!hasPermission(session.user.role, "create_po")) redirect("/dashboard");
|
||||
|
||||
const { cart, costCentreRef: initialCostCentreRef } = await searchParams;
|
||||
const { cart, vesselId: initialVesselId } = await searchParams;
|
||||
|
||||
let initialLineItems: LineItemInput[] | undefined;
|
||||
let initialVendorId: string | undefined;
|
||||
|
|
@ -44,13 +42,12 @@ export default async function NewPoPage({ searchParams }: Props) {
|
|||
if (vendorIds.length === 1) initialVendorId = vendorIds[0];
|
||||
}
|
||||
} catch {
|
||||
// malformed cart param — ignore and start empty
|
||||
// malformed cart param — ignore
|
||||
}
|
||||
}
|
||||
|
||||
const [vessels, sites, leafAccounts, vendors] = await Promise.all([
|
||||
db.vessel.findMany({ where: { isActive: true }, orderBy: { name: "asc" }, select: { id: true, name: true, code: true, siteId: true } }),
|
||||
db.site.findMany({ where: { isActive: true }, orderBy: { name: "asc" }, select: { id: true, name: true } }),
|
||||
const [vessels, leafAccounts, vendors] = await Promise.all([
|
||||
db.vessel.findMany({ where: { isActive: true }, orderBy: { name: "asc" }, select: { id: true, name: true, code: true } }),
|
||||
db.account.findMany({
|
||||
where: { isActive: true, children: { none: {} } },
|
||||
orderBy: { code: "asc" },
|
||||
|
|
@ -59,7 +56,6 @@ export default async function NewPoPage({ searchParams }: Props) {
|
|||
db.vendor.findMany({ where: { isActive: true }, orderBy: { name: "asc" } }),
|
||||
]);
|
||||
|
||||
const costCentres = buildCostCentreGroups(vessels, sites);
|
||||
const accounts = buildAccountGroups(leafAccounts);
|
||||
|
||||
return (
|
||||
|
|
@ -71,12 +67,12 @@ export default async function NewPoPage({ searchParams }: Props) {
|
|||
</p>
|
||||
</div>
|
||||
<NewPoForm
|
||||
costCentres={costCentres}
|
||||
vessels={vessels}
|
||||
accounts={accounts}
|
||||
vendors={vendors}
|
||||
initialLineItems={initialLineItems}
|
||||
initialVendorId={initialVendorId}
|
||||
initialCostCentreRef={initialCostCentreRef}
|
||||
initialVesselId={initialVesselId}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ export async function GET(request: NextRequest) {
|
|||
const format = sp.get("format") ?? "csv";
|
||||
const dateFrom = sp.get("dateFrom");
|
||||
const dateTo = sp.get("dateTo");
|
||||
const costCentreRef = sp.get("costCentreRef") ?? sp.get("vesselId");
|
||||
const vesselId = sp.get("vesselId");
|
||||
const status = sp.get("status");
|
||||
|
||||
const where: NonNullable<Parameters<typeof db.purchaseOrder.findMany>[0]>["where"] = {};
|
||||
|
|
@ -38,16 +38,12 @@ export async function GET(request: NextRequest) {
|
|||
}
|
||||
where.createdAt = createdAt;
|
||||
}
|
||||
if (costCentreRef) {
|
||||
if (costCentreRef.startsWith("v:")) where.vesselId = costCentreRef.slice(2);
|
||||
else if (costCentreRef.startsWith("s:")) where.siteId = costCentreRef.slice(2);
|
||||
else where.vesselId = costCentreRef; // legacy plain vesselId
|
||||
}
|
||||
if (vesselId) where.vesselId = vesselId;
|
||||
if (status) where.status = status as POStatus;
|
||||
|
||||
const orders = await db.purchaseOrder.findMany({
|
||||
where,
|
||||
include: { submitter: true, vessel: true, site: { select: { name: true } }, account: true, vendor: true },
|
||||
include: { submitter: true, vessel: true, account: true, vendor: true },
|
||||
orderBy: { createdAt: "desc" },
|
||||
});
|
||||
|
||||
|
|
@ -57,7 +53,7 @@ export async function GET(request: NextRequest) {
|
|||
<td>${po.poNumber}</td>
|
||||
<td>${po.title}</td>
|
||||
<td>${PO_STATUS_LABELS[po.status] ?? po.status}</td>
|
||||
<td>${po.vessel?.name ?? po.site?.name ?? "—"}</td>
|
||||
<td>${po.vessel.name}</td>
|
||||
<td>${po.submitter.name}</td>
|
||||
<td>${po.vendor?.name ?? "—"}</td>
|
||||
<td style="text-align:right">${Number(po.totalAmount).toLocaleString("en-IN", { style: "currency", currency: "INR" })}</td>
|
||||
|
|
@ -111,7 +107,7 @@ export async function GET(request: NextRequest) {
|
|||
po.poNumber,
|
||||
`"${po.title.replace(/"/g, '""')}"`,
|
||||
po.status,
|
||||
po.vessel?.name ?? po.site?.name ?? "",
|
||||
po.vessel.name,
|
||||
po.account.name,
|
||||
po.vendor?.name ?? "",
|
||||
po.submitter.name,
|
||||
|
|
|
|||
|
|
@ -38,8 +38,7 @@ type PoWithRelations = {
|
|||
paidAt: Date | null;
|
||||
closedAt: Date | null;
|
||||
submitter: { id: string; name: string; email: string };
|
||||
vessel: { id: string; name: string } | null;
|
||||
site?: { id: string; name: string } | null;
|
||||
vessel: { id: string; name: string };
|
||||
account: { id: string; name: string; code: string };
|
||||
vendor: {
|
||||
id: string;
|
||||
|
|
@ -229,7 +228,7 @@ export async function PoDetail({ po, currentUserId, currentRole, readOnly = fals
|
|||
po.status === "MGR_REVIEW" &&
|
||||
(currentRole === "MANAGER" || currentRole === "SUPERUSER") && (() => {
|
||||
const snap = resubmitSnapshot.fields;
|
||||
const currentVessel = po.vessel?.name ?? po.site?.name ?? null;
|
||||
const currentVessel = po.vessel?.name ?? null;
|
||||
const currentAccount = `${po.account.name} (${po.account.code})`;
|
||||
const currentVendor = po.vendor?.name ?? null;
|
||||
const currentDateRequired = po.dateRequired?.toISOString() ?? null;
|
||||
|
|
@ -237,7 +236,7 @@ export async function PoDetail({ po, currentUserId, currentRole, readOnly = fals
|
|||
const fieldChanges: { label: string; before: string | null; after: string | null }[] = [];
|
||||
if (snap.title !== po.title)
|
||||
fieldChanges.push({ label: "Title", before: snap.title, after: po.title });
|
||||
if (snap.vesselId !== (po.vessel?.id ?? po.site?.id ?? ""))
|
||||
if (snap.vesselId !== po.vessel.id)
|
||||
fieldChanges.push({ label: "Cost Centre", before: snap.vessel, after: currentVessel });
|
||||
if (snap.accountId !== po.account.id)
|
||||
fieldChanges.push({ label: "Account", before: snap.account, after: currentAccount });
|
||||
|
|
@ -287,7 +286,7 @@ export async function PoDetail({ po, currentUserId, currentRole, readOnly = fals
|
|||
<div className="rounded-lg border border-neutral-200 bg-white p-4 md:p-6">
|
||||
<h3 className="text-sm font-semibold text-neutral-900 mb-4">Order Details</h3>
|
||||
<dl className="grid grid-cols-1 sm:grid-cols-2 gap-x-6 gap-y-3 text-sm">
|
||||
<div><dt className="text-neutral-500">Cost Centre</dt><dd className="font-medium text-neutral-900">{po.vessel?.name ?? po.site?.name ?? "—"}</dd></div>
|
||||
<div><dt className="text-neutral-500">Cost Centre</dt><dd className="font-medium text-neutral-900">{po.vessel?.name ?? "—"}</dd></div>
|
||||
<div><dt className="text-neutral-500">Accounting Code</dt><dd className="font-medium text-neutral-900">{po.account.name} ({po.account.code})</dd></div>
|
||||
<div><dt className="text-neutral-500">Requested By</dt><dd className="font-medium text-neutral-900">{po.submitter.name}</dd></div>
|
||||
{approvalAction && (
|
||||
|
|
|
|||
|
|
@ -1,40 +1,3 @@
|
|||
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.
|
||||
|
|
|
|||
|
|
@ -29,10 +29,7 @@ export const TC_DEFAULTS = {
|
|||
|
||||
export const createPoSchema = z.object({
|
||||
title: z.string().min(1, "Title is required").max(200),
|
||||
costCentreRef: z.string().min(1, "Cost Centre is required").refine(
|
||||
(v) => v.startsWith("v:") || v.startsWith("s:"),
|
||||
"Invalid cost centre selection"
|
||||
),
|
||||
vesselId: z.string().min(1, "Cost Centre is required"),
|
||||
accountId: z.string().min(1, "Accounting Code is required"),
|
||||
projectCode: z.string().optional(),
|
||||
dateRequired: z.string().optional(),
|
||||
|
|
|
|||
|
|
@ -0,0 +1,7 @@
|
|||
-- Remove vessel → site relationship (vessels are standalone cost centres)
|
||||
ALTER TABLE "Vessel" DROP COLUMN IF EXISTS "siteId";
|
||||
|
||||
-- Restore vesselId as required on PurchaseOrder
|
||||
-- First null out any rows that have no vesselId (set to first vessel as fallback, should be none in practice)
|
||||
UPDATE "PurchaseOrder" SET "vesselId" = (SELECT id FROM "Vessel" LIMIT 1) WHERE "vesselId" IS NULL;
|
||||
ALTER TABLE "PurchaseOrder" ALTER COLUMN "vesselId" SET NOT NULL;
|
||||
|
|
@ -100,7 +100,6 @@ model Site {
|
|||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
vessels Vessel[]
|
||||
purchaseOrders PurchaseOrder[]
|
||||
inventory ItemInventory[]
|
||||
consumption ItemConsumption[]
|
||||
|
|
@ -112,9 +111,6 @@ model Vessel {
|
|||
code String @unique
|
||||
isActive Boolean @default(true)
|
||||
|
||||
siteId String?
|
||||
site Site? @relation(fields: [siteId], references: [id])
|
||||
|
||||
purchaseOrders PurchaseOrder[]
|
||||
}
|
||||
|
||||
|
|
@ -257,8 +253,8 @@ model PurchaseOrder {
|
|||
|
||||
submitterId String
|
||||
submitter User @relation("Submitter", fields: [submitterId], references: [id])
|
||||
vesselId String?
|
||||
vessel Vessel? @relation(fields: [vesselId], references: [id])
|
||||
vesselId String
|
||||
vessel Vessel @relation(fields: [vesselId], references: [id])
|
||||
accountId String
|
||||
account Account @relation(fields: [accountId], references: [id])
|
||||
vendorId String?
|
||||
|
|
|
|||
|
|
@ -19,20 +19,20 @@ const db = new PrismaClient();
|
|||
// ─── Users ────────────────────────────────────────────────────────────────────
|
||||
|
||||
const USERS: { employeeId: string; name: string; email: string; role: Role }[] = [
|
||||
{ employeeId: "PMS-001", name: "Akshata Teli", email: "akshata@pelagiamarine.com", role: Role.ACCOUNTS },
|
||||
{ employeeId: "PMS-002", name: "Chhagan Sarang", email: "chhagan.sarang@pelagiamarine.com", role: Role.MANAGER },
|
||||
{ employeeId: "PMS-003", name: "Dipali K", email: "dipali.k@pelagiamarine.com", role: Role.ACCOUNTS },
|
||||
{ employeeId: "PMS-004", name: "Eeshan Singh", email: "eeshan.singh@pelagiamarine.com", role: Role.TECHNICAL },
|
||||
{ employeeId: "PMS-005", name: "Kaushal Pal Singh", email: "kps@pelagiamarine.com", role: Role.MANAGER },
|
||||
{ employeeId: "PMS-006", name: "Manjuprasad B", email: "manjuprasad.b@pelagiamarine.com", role: Role.TECHNICAL },
|
||||
{ employeeId: "PMS-007", name: "Mayur Deore", email: "mayur@pelagiamarine.com", role: Role.MANNING },
|
||||
{ employeeId: "PMS-008", name: "Nikita Accounts", email: "nikita.m@pelagiamarine.com", role: Role.ACCOUNTS },
|
||||
{ employeeId: "PMS-009", name: "Rakesh Kumar Pandey", email: "rkp@pelagiamarine.com", role: Role.MANAGER },
|
||||
{ employeeId: "PMS-010", name: "Shailesh B", email: "shailesh.b@pelagiamarine.com", role: Role.ACCOUNTS },
|
||||
{ employeeId: "PMS-011", name: "Shrikant T", email: "shrikant.t@pelagiamarine.com", role: Role.TECHNICAL },
|
||||
{ employeeId: "PMS-012", name: "Sunil Gupta", email: "sunil.gupta@pelagiamarine.com", role: Role.MANNING },
|
||||
{ employeeId: "PMS-013", name: "Supriya Sutar", email: "supriya.s@pelagiamarine.com", role: Role.TECHNICAL },
|
||||
{ employeeId: "PMS-014", name: "Tajinder Kaur", email: "tajinder.kaur@pelagiamarine.com", role: Role.MANAGER },
|
||||
{ employeeId: "ACC-001", name: "Akshata Teli", email: "akshata@pelagiamarine.com", role: Role.ACCOUNTS },
|
||||
{ employeeId: "ACC-002", name: "Dipali K", email: "dipali.k@pelagiamarine.com", role: Role.ACCOUNTS },
|
||||
{ employeeId: "ACC-003", name: "Nikita Accounts", email: "nikita.m@pelagiamarine.com", role: Role.ACCOUNTS },
|
||||
{ employeeId: "ACC-004", name: "Shailesh B", email: "shailesh.b@pelagiamarine.com", role: Role.ACCOUNTS },
|
||||
{ employeeId: "MGR-001", name: "Chhagan Sarang", email: "chhagan.sarang@pelagiamarine.com", role: Role.MANAGER },
|
||||
{ employeeId: "MGR-002", name: "Kaushal Pal Singh", email: "kps@pelagiamarine.com", role: Role.MANAGER },
|
||||
{ employeeId: "MGR-003", name: "Rakesh Kumar Pandey", email: "rkp@pelagiamarine.com", role: Role.MANAGER },
|
||||
{ employeeId: "MGR-004", name: "Tajinder Kaur", email: "tajinder.kaur@pelagiamarine.com", role: Role.MANAGER },
|
||||
{ employeeId: "MAN-001", name: "Mayur Deore", email: "mayur@pelagiamarine.com", role: Role.MANNING },
|
||||
{ employeeId: "MAN-002", name: "Sunil Gupta", email: "sunil.gupta@pelagiamarine.com", role: Role.MANNING },
|
||||
{ employeeId: "TCH-001", name: "Eeshan Singh", email: "eeshan.singh@pelagiamarine.com", role: Role.TECHNICAL },
|
||||
{ employeeId: "TCH-002", name: "Manjuprasad B", email: "manjuprasad.b@pelagiamarine.com", role: Role.TECHNICAL },
|
||||
{ employeeId: "TCH-003", name: "Shrikant T", email: "shrikant.t@pelagiamarine.com", role: Role.TECHNICAL },
|
||||
{ employeeId: "TCH-004", name: "Supriya Sutar", email: "supriya.s@pelagiamarine.com", role: Role.TECHNICAL },
|
||||
];
|
||||
|
||||
// ─── Sites ────────────────────────────────────────────────────────────────────
|
||||
|
|
@ -49,17 +49,17 @@ const SITES: { code: string; name: string }[] = [
|
|||
|
||||
// ─── Vessels (code, name, site code) ─────────────────────────────────────────
|
||||
|
||||
const VESSELS: { code: string; name: string; siteCode: string }[] = [
|
||||
{ code: "HNR1", name: "HNR 1", siteCode: "HLDA" },
|
||||
{ code: "HNR2", name: "HNR 2", siteCode: "LACD" },
|
||||
{ code: "HNR3", name: "HNR 3", siteCode: "THKM" },
|
||||
{ code: "HNR4", name: "HNR 4", siteCode: "THNK" },
|
||||
{ code: "CHAMPION", name: "Champion", siteCode: "PMSK" },
|
||||
{ code: "HANUNAM", name: "Hanunam", siteCode: "KVRT" },
|
||||
{ code: "SEJAL", name: "Sejal", siteCode: "HLDA" },
|
||||
{ code: "SEJAL2", name: "Sejal 2", siteCode: "LACD" },
|
||||
{ code: "GD3000", name: "GD 3000", siteCode: "THKM" },
|
||||
{ code: "THILAKKAM", name: "Thilakkam", siteCode: "THNK" },
|
||||
const VESSELS: { code: string; name: string }[] = [
|
||||
{ code: "HNR1", name: "HNR 1" },
|
||||
{ code: "HNR2", name: "HNR 2" },
|
||||
{ code: "HNR3", name: "HNR 3" },
|
||||
{ code: "HNR4", name: "HNR 4" },
|
||||
{ code: "CHAMPION", name: "Champion" },
|
||||
{ code: "HANUNAM", name: "Hanunam" },
|
||||
{ code: "SEJAL", name: "Sejal" },
|
||||
{ code: "SEJAL2", name: "Sejal 2" },
|
||||
{ code: "GD3000", name: "GD 3000" },
|
||||
{ code: "THILAKKAM", name: "Thilakkam" },
|
||||
];
|
||||
|
||||
// ─── Main ─────────────────────────────────────────────────────────────────────
|
||||
|
|
@ -86,31 +86,24 @@ async function main() {
|
|||
|
||||
// ── Sites ──────────────────────────────────────────────────────────────────
|
||||
console.log("\n📍 Seeding sites…");
|
||||
const siteIdMap = new Map<string, string>();
|
||||
for (const s of SITES) {
|
||||
const site = await db.site.upsert({
|
||||
await db.site.upsert({
|
||||
where: { code: s.code },
|
||||
update: { name: s.name },
|
||||
create: { code: s.code, name: s.name },
|
||||
});
|
||||
siteIdMap.set(s.code, site.id);
|
||||
console.log(` ✓ ${s.name} (${s.code})`);
|
||||
}
|
||||
|
||||
// ── Vessels ────────────────────────────────────────────────────────────────
|
||||
console.log("\n🚢 Seeding vessels…");
|
||||
for (const v of VESSELS) {
|
||||
const siteId = siteIdMap.get(v.siteCode);
|
||||
if (!siteId) {
|
||||
console.warn(` ⚠ Unknown site code "${v.siteCode}" for vessel ${v.code} — skipping`);
|
||||
continue;
|
||||
}
|
||||
await db.vessel.upsert({
|
||||
where: { code: v.code },
|
||||
update: { name: v.name, siteId },
|
||||
create: { code: v.code, name: v.name, siteId },
|
||||
update: { name: v.name },
|
||||
create: { code: v.code, name: v.name },
|
||||
});
|
||||
console.log(` ✓ ${v.name} (${v.code}) → ${v.siteCode}`);
|
||||
console.log(` ✓ ${v.name} (${v.code})`);
|
||||
}
|
||||
|
||||
// ── Accounting Codes ───────────────────────────────────────────────────────
|
||||
|
|
|
|||
|
|
@ -158,25 +158,23 @@ async function main() {
|
|||
});
|
||||
|
||||
// ─── Vessels (Cost Centres) ──────────────────────────────────────────────────
|
||||
const findOrCreateVessel = async (name: string, siteId: string, code: string) => {
|
||||
const findOrCreateVessel = async (name: string, code: string) => {
|
||||
const vessel = await db.vessel.findFirst({ where: { name } });
|
||||
if (vessel) {
|
||||
return db.vessel.update({ where: { id: vessel.id }, data: { siteId } });
|
||||
}
|
||||
return db.vessel.create({ data: { name, code, siteId } });
|
||||
if (vessel) return vessel;
|
||||
return db.vessel.create({ data: { name, code } });
|
||||
};
|
||||
|
||||
const mvStar = await findOrCreateVessel("MV Pelagia Star", siteBOM.id, "SITE-001");
|
||||
const mvWind = await findOrCreateVessel("MV Aegean Wind", siteJNP.id, "SITE-002");
|
||||
const mvPoseidon = await findOrCreateVessel("MV Poseidon", siteKDL.id, "SITE-003");
|
||||
const mvNereid = await findOrCreateVessel("MV Nereid", siteCHE.id, "SITE-004");
|
||||
const mvThetis = await findOrCreateVessel("MV Thetis", siteKOC.id, "SITE-005");
|
||||
const mvTriton = await findOrCreateVessel("MV Triton", siteVIZ.id, "SITE-006");
|
||||
const mvAmphitrite = await findOrCreateVessel("MV Amphitrite", siteHAL.id, "SITE-007");
|
||||
const mvProteus = await findOrCreateVessel("MV Proteus", sitePAR.id, "SITE-008");
|
||||
const mvGalatea = await findOrCreateVessel("MV Galatea", siteMNG.id, "SITE-009");
|
||||
const mvCallisto = await findOrCreateVessel("MV Callisto", siteGOA.id, "SITE-010");
|
||||
await findOrCreateVessel("MV Doris", siteCHE.id, "SITE-011");
|
||||
const mvStar = await findOrCreateVessel("MV Pelagia Star", "SITE-001");
|
||||
const mvWind = await findOrCreateVessel("MV Aegean Wind", "SITE-002");
|
||||
const mvPoseidon = await findOrCreateVessel("MV Poseidon", "SITE-003");
|
||||
const mvNereid = await findOrCreateVessel("MV Nereid", "SITE-004");
|
||||
const mvThetis = await findOrCreateVessel("MV Thetis", "SITE-005");
|
||||
const mvTriton = await findOrCreateVessel("MV Triton", "SITE-006");
|
||||
const mvAmphitrite = await findOrCreateVessel("MV Amphitrite", "SITE-007");
|
||||
const mvProteus = await findOrCreateVessel("MV Proteus", "SITE-008");
|
||||
const mvGalatea = await findOrCreateVessel("MV Galatea", "SITE-009");
|
||||
const mvCallisto = await findOrCreateVessel("MV Callisto", "SITE-010");
|
||||
await findOrCreateVessel("MV Doris", "SITE-011");
|
||||
|
||||
// ─── Accounting Codes (hierarchical) ─────────────────────────────────────────
|
||||
// Seed in two passes: first create all entries without parentId, then link parents
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue