pelagia-portal/App/app/(portal)/po/new/page.tsx
Hardik 0d17672ea9 feat(accounts): hierarchical accounting codes with 6-digit format and category tree
- Account model gains parentId (self-referential, 3 levels: TopCategory → SubCategory → Item)
- DB migration: adds parentId FK column to Account table
- Code format changed from PREFIX-NNN to 6-digit numeric (e.g. 100101)
- Seeded all 300+ accounting codes from the official chart (Rev. 01/251227) across
  7 top categories: Capital Expenses, Business Development, Office Admin, Project
  Expenses, Manning, Technical, Bunker/Lubes
- Admin Accounting Code page: collapsible tree view (top category > sub-category > items),
  inline search, Add/Edit dialogs with parent selector and 6-digit code field
- All PO forms (new, edit, import, manager-edit): accounting code dropdown now shows
  only leaf items grouped in <optgroup> by sub-category, labelled "TopCat › SubCat"
- Seed data updated: old flat account codes replaced by mapped leaf codes from new hierarchy

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-30 03:27:31 +05:30

98 lines
3.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

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

import { auth } from "@/auth";
import { db } from "@/lib/db";
import { hasPermission } from "@/lib/permissions";
import { redirect } from "next/navigation";
import { NewPoForm } from "./new-po-form";
import type { CostCentreOption } from "./new-po-form";
import type { Metadata } from "next";
import type { LineItemInput } from "@/lib/validations/po";
import type { CartItem } from "@/lib/cart";
export const metadata: Metadata = { title: "New Purchase Order" };
interface Props {
searchParams: Promise<{ cart?: string; costCentreRef?: 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");
}
const { cart, costCentreRef: initialCostCentreRef } = await searchParams;
let initialLineItems: LineItemInput[] | undefined;
let initialVendorId: string | undefined;
if (cart) {
try {
const cartItems: CartItem[] = JSON.parse(decodeURIComponent(cart));
if (Array.isArray(cartItems) && cartItems.length > 0) {
initialLineItems = cartItems.map((item) => ({
name: item.name,
description: item.description ?? "",
quantity: item.quantity,
unit: item.unit,
size: "",
unitPrice: item.unitPrice,
gstRate: 0.18,
productId: item.productId,
}));
// Pre-fill vendor only when all items share the same vendor
const vendorIds = [...new Set(cartItems.map((i) => i.vendorId).filter(Boolean))];
if (vendorIds.length === 1) initialVendorId = vendorIds[0];
}
} catch {
// malformed cart param — ignore and start empty
}
}
const [vessels, sites, leafAccounts, vendors] = await Promise.all([
db.vessel.findMany({ where: { isActive: true }, orderBy: { name: "asc" }, select: { id: true, name: true, code: true } }),
db.site.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" },
select: {
id: true, code: true, name: true,
parent: {
select: {
name: true, code: true,
parent: { select: { name: true, code: true } },
},
},
},
}),
db.vendor.findMany({ where: { isActive: true }, orderBy: { name: "asc" } }),
]);
// Build grouped account list for optgroups: "TOP CAT > Sub Cat" → items
const accountGroupMap = new Map<string, typeof leafAccounts>();
for (const a of leafAccounts) {
const subLabel = a.parent ? `${a.parent.code}${a.parent.name}` : "Uncategorised";
const topLabel = a.parent?.parent ? `${a.parent.parent.name} ` : "";
const groupKey = `${topLabel}${subLabel}`;
if (!accountGroupMap.has(groupKey)) accountGroupMap.set(groupKey, []);
accountGroupMap.get(groupKey)!.push(a);
}
const accounts = Array.from(accountGroupMap.entries()).map(([group, items]) => ({ group, items }));
const costCentres: CostCentreOption[] = [
...vessels.map((v) => ({ ref: `v:${v.id}` as const, label: `${v.code}${v.name}`, group: "Vessels" as const })),
...sites.map((s) => ({ ref: `s:${s.id}` as const, label: `${s.code}${s.name}`, group: "Sites" as const })),
];
return (
<div className="max-w-6xl">
<div className="mb-6">
<h1 className="text-2xl font-semibold text-neutral-900">New Purchase Order</h1>
<p className="mt-1 text-sm text-neutral-500">
Fill in the details below. You can save as draft or submit directly for approval.
</p>
</div>
<NewPoForm costCentres={costCentres} accounts={accounts} vendors={vendors} initialLineItems={initialLineItems} initialVendorId={initialVendorId} initialCostCentreRef={initialCostCentreRef} />
</div>
);
}