From 0d17672ea96bbd6677355f3fb0fa1ecba28c14d1 Mon Sep 17 00:00:00 2001 From: Hardik Date: Sat, 30 May 2026 03:27:31 +0530 Subject: [PATCH] feat(accounts): hierarchical accounting codes with 6-digit format and category tree MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 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 --- .../(portal)/admin/accounts/account-form.tsx | 66 ++- .../admin/accounts/accounts-table.tsx | 243 +++++++---- App/app/(portal)/admin/accounts/actions.ts | 37 +- App/app/(portal)/admin/accounts/page.tsx | 34 +- .../approvals/[id]/manager-edit-po-form.tsx | 12 +- App/app/(portal)/approvals/[id]/page.tsx | 20 +- .../(portal)/po/[id]/edit/edit-po-form.tsx | 16 +- App/app/(portal)/po/[id]/edit/page.tsx | 20 +- App/app/(portal)/po/import/import-form.tsx | 16 +- App/app/(portal)/po/import/page.tsx | 20 +- App/app/(portal)/po/new/new-po-form.tsx | 15 +- App/app/(portal)/po/new/page.tsx | 27 +- App/prisma/accounting-codes-data.ts | 387 ++++++++++++++++++ .../migration.sql | 5 + App/prisma/schema.prisma | 4 + App/prisma/seed.ts | 104 ++--- 16 files changed, 803 insertions(+), 223 deletions(-) create mode 100644 App/prisma/accounting-codes-data.ts create mode 100644 App/prisma/migrations/20260530000001_account_hierarchy/migration.sql diff --git a/App/app/(portal)/admin/accounts/account-form.tsx b/App/app/(portal)/admin/accounts/account-form.tsx index 243bc03..e2d6d63 100644 --- a/App/app/(portal)/admin/accounts/account-form.tsx +++ b/App/app/(portal)/admin/accounts/account-form.tsx @@ -5,40 +5,80 @@ import { useRouter } from "next/navigation"; import { AdminDialog } from "@/components/ui/admin-dialog"; import { createAccount, updateAccount } from "./actions"; +type ParentOption = { id: string; code: string; name: string; parentId: string | null }; + type AccountRow = { id: string; code: string; name: string; description: string | null; + parentId: string | null; isActive: boolean; }; -function AccountFormFields({ account, suggestedCode }: { account?: AccountRow; suggestedCode?: string }) { +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 AccountFormFields({ + account, + allAccounts, +}: { + account?: AccountRow; + allAccounts: ParentOption[]; +}) { + // Only allow parent to be a top-level (parentId == null) or sub-category (parentId != null but has no grandparent) + // i.e. depth 0 or depth 1 nodes can be parents + const topLevel = allAccounts.filter((a) => a.parentId === null && (!account || a.id !== account.id)); + const subLevel = allAccounts.filter((a) => a.parentId !== null && (!account || a.id !== account.id)); + return (
- - + +
- +
+
+ + +
- +
); } -export function AddAccountButton({ suggestedCode }: { suggestedCode?: string }) { +export function AddAccountButton({ allAccounts }: { allAccounts: ParentOption[] }) { const router = useRouter(); const [open, setOpen] = useState(false); const [pending, setPending] = useState(false); @@ -61,7 +101,7 @@ export function AddAccountButton({ suggestedCode }: { suggestedCode?: string }) setOpen(false)}>
- + {error &&

{error}

}
); } diff --git a/App/app/(portal)/po/import/import-form.tsx b/App/app/(portal)/po/import/import-form.tsx index b29e406..42e1f2d 100644 --- a/App/app/(portal)/po/import/import-form.tsx +++ b/App/app/(portal)/po/import/import-form.tsx @@ -2,8 +2,8 @@ import { useState, useRef } from "react"; import { useRouter } from "next/navigation"; -import type { Account, Vendor } from "@prisma/client"; -import type { CostCentreOption } from "@/app/(portal)/po/new/new-po-form"; +import type { Vendor } from "@prisma/client"; +import type { CostCentreOption, AccountGroup } from "@/app/(portal)/po/new/new-po-form"; import { importPo } from "./actions"; import type { ParsedImport } from "@/app/api/po/import/route"; import { formatCurrency } from "@/lib/utils"; @@ -13,7 +13,7 @@ const INPUT_CLS = interface Props { costCentres: CostCentreOption[]; - accounts: Account[]; + accounts: AccountGroup[]; vendors: Vendor[]; } @@ -66,7 +66,7 @@ export function ImportForm({ costCentres, accounts, vendors }: Props) { ? `${parsed.vendorName} — Import` : "Imported Purchase Order", costCentreRef: costCentres[0]?.ref ?? "", - accountId: accounts[0]?.id ?? "", + accountId: accounts[0]?.items[0]?.id ?? "", vendorId: matchedVendor?.id ?? "", }); } catch { @@ -211,8 +211,12 @@ export function ImportForm({ costCentres, accounts, vendors }: Props) { required className={INPUT_CLS} > - - {accounts.map((a) => )} + + {accounts.map(({ group, items }) => ( + + {items.map((a) => )} + + ))} diff --git a/App/app/(portal)/po/import/page.tsx b/App/app/(portal)/po/import/page.tsx index 7facdba..5f2af25 100644 --- a/App/app/(portal)/po/import/page.tsx +++ b/App/app/(portal)/po/import/page.tsx @@ -2,7 +2,7 @@ import { auth } from "@/auth"; import { db } from "@/lib/db"; import { redirect } from "next/navigation"; import { ImportForm } from "./import-form"; -import type { CostCentreOption } from "@/app/(portal)/po/new/new-po-form"; +import type { CostCentreOption, AccountGroup } from "@/app/(portal)/po/new/new-po-form"; import type { Metadata } from "next"; export const metadata: Metadata = { title: "Import Purchase Order" }; @@ -17,10 +17,24 @@ export default async function ImportPoPage() { const [vessels, sites, accounts, 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 }, orderBy: { name: "asc" } }), + 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({ orderBy: { name: "asc" } }), ]); + const accountGroupMap = new Map(); + for (const a of accounts) { + 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 accountGroups: AccountGroup[] = Array.from(accountGroupMap.entries()).map(([group, items]) => ({ group, items })); + const costCentres: CostCentreOption[] = [ ...vessels.map((v) => ({ ref: `v:${v.id}`, label: `${v.code} — ${v.name}`, group: "Vessels" as const })), ...sites.map((s) => ({ ref: `s:${s.id}`, label: `${s.code} — ${s.name}`, group: "Sites" as const })), @@ -35,7 +49,7 @@ export default async function ImportPoPage() { You then select the cost centre, accounting code, and confirm before saving as a draft.

- + ); } diff --git a/App/app/(portal)/po/new/new-po-form.tsx b/App/app/(portal)/po/new/new-po-form.tsx index eae1c8f..b28bdeb 100644 --- a/App/app/(portal)/po/new/new-po-form.tsx +++ b/App/app/(portal)/po/new/new-po-form.tsx @@ -3,7 +3,7 @@ import { useState } from "react"; import { useRouter } from "next/navigation"; import { createPo } from "./actions"; -import type { Account, Vendor } from "@prisma/client"; +import type { Vendor } from "@prisma/client"; import { LineItemsEditor } from "@/components/po/po-line-items-editor"; import { FileUploader } from "@/components/po/file-uploader"; import { uploadAndLinkFiles } from "@/lib/upload-files"; @@ -11,6 +11,7 @@ import type { LineItemInput } from "@/lib/validations/po"; import { TC_DEFAULTS, TC_FIXED_LINE, TC_FIXED_LINE_2 } from "@/lib/validations/po"; export type CostCentreOption = { ref: string; label: string; group: "Vessels" | "Sites" }; +export type AccountGroup = { group: string; items: { id: string; code: string; name: string }[] }; 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"; @@ -19,7 +20,7 @@ const EMPTY_LINE: LineItemInput = { name: "", description: "", quantity: 1, unit interface Props { costCentres: CostCentreOption[]; - accounts: Account[]; + accounts: AccountGroup[]; vendors: Vendor[]; initialLineItems?: LineItemInput[]; initialVendorId?: string; @@ -125,8 +126,12 @@ export function NewPoForm({ costCentres, accounts, vendors, initialLineItems, in onChange={(e) => setDefaultAccountId(e.target.value)} className={INPUT_CLS} > - - {accounts.map((a) => )} + + {accounts.map(({ group, items }) => ( + + {items.map((a) => )} + + ))}
@@ -191,7 +196,7 @@ export function NewPoForm({ costCentres, accounts, vendors, initialLineItems, in items={lineItems} onChange={setLineItems} multiAccount={multiAccount} - accounts={accounts.map((a) => ({ id: a.id, name: a.name, code: a.code }))} + accounts={accounts.flatMap((g) => g.items.map((a) => ({ id: a.id, name: a.name, code: a.code })))} defaultAccountId={defaultAccountId || undefined} /> diff --git a/App/app/(portal)/po/new/page.tsx b/App/app/(portal)/po/new/page.tsx index 214fe02..7f43ac9 100644 --- a/App/app/(portal)/po/new/page.tsx +++ b/App/app/(portal)/po/new/page.tsx @@ -49,13 +49,36 @@ export default async function NewPoPage({ searchParams }: Props) { } } - const [vessels, sites, accounts, vendors] = await Promise.all([ + 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 }, orderBy: { name: "asc" } }), + 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(); + 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 })), diff --git a/App/prisma/accounting-codes-data.ts b/App/prisma/accounting-codes-data.ts new file mode 100644 index 0000000..1d0e93f --- /dev/null +++ b/App/prisma/accounting-codes-data.ts @@ -0,0 +1,387 @@ +// Accounting Codes (Rev. 01/251227) — hierarchical structure +// Each entry: [code, name, parentCode | null] +// parentCode null = top-level category +// parentCode ending in 0000 = top-level → sub-category +// parentCode ending in 00 = sub-category → leaf item + +export type ACEntry = { code: string; name: string; parentCode: string | null }; + +export const ACCOUNTING_CODES: ACEntry[] = [ + // ── CAPITAL EXPENSES ───────────────────────────────────────────────────────── + { code: "100000", name: "CAPITAL EXPENSES", parentCode: null }, + { code: "100100", name: "DREDGER COST", parentCode: "100000" }, + { code: "100101", name: "Dredger cost-Shipyard", parentCode: "100100" }, + { code: "100102", name: "Custom duty", parentCode: "100100" }, + { code: "100103", name: "Stamp duty", parentCode: "100100" }, + { code: "100104", name: "CHA Charges", parentCode: "100100" }, + { code: "100105", name: "Transportation by sea- Dredger", parentCode: "100100" }, + { code: "100106", name: "Transportation by road- Dredger", parentCode: "100100" }, + { code: "100107", name: "Dredger cost- Others", parentCode: "100100" }, + { code: "100200", name: "PIPELINE COST", parentCode: "100000" }, + { code: "100201", name: "Pipeline Cost", parentCode: "100200" }, + { code: "100202", name: "Pipeline- Nuts & Bolts", parentCode: "100200" }, + { code: "100203", name: "Pipeline- Packing", parentCode: "100200" }, + { code: "100204", name: "Pipeline- Repairs", parentCode: "100200" }, + { code: "100205", name: "Transportation by sea- Pipeline", parentCode: "100200" }, + { code: "100206", name: "Transportation by road- Pipeline", parentCode: "100200" }, + { code: "100207", name: "Pipeline cost- Others", parentCode: "100200" }, + { code: "100300", name: "HOSES COST", parentCode: "100000" }, + { code: "100301", name: "Hoses Cost", parentCode: "100300" }, + { code: "100302", name: "Hoses- Nuts & Bolts", parentCode: "100300" }, + { code: "100303", name: "Hoses- Packing", parentCode: "100300" }, + { code: "100304", name: "Hoses- Repairs", parentCode: "100300" }, + { code: "100305", name: "Transportation by sea- Hoses", parentCode: "100300" }, + { code: "100306", name: "Transportation by road- Hoses", parentCode: "100300" }, + { code: "100307", name: "Hoses cost- Others", parentCode: "100300" }, + { code: "100400", name: "FLOATS COST", parentCode: "100000" }, + { code: "100401", name: "Floats Cost", parentCode: "100400" }, + { code: "100402", name: "Floats- Nuts & Bolts", parentCode: "100400" }, + { code: "100403", name: "Floats- Packing", parentCode: "100400" }, + { code: "100404", name: "Floats- Repairs", parentCode: "100400" }, + { code: "100405", name: "Transportation by sea- Floats", parentCode: "100400" }, + { code: "100406", name: "Transportation by road- Floats", parentCode: "100400" }, + { code: "100407", name: "Floats cost- Others", parentCode: "100400" }, + { code: "100500", name: "DOCKING EXPENSES (CAPITAL)", parentCode: "100000" }, + { code: "100501", name: "Dock charges", parentCode: "100500" }, + { code: "100502", name: "Towing charges", parentCode: "100500" }, + { code: "100503", name: "Drydock- Agency charges", parentCode: "100500" }, + { code: "100504", name: "Drydock- Port charges", parentCode: "100500" }, + { code: "100505", name: "Drydock- Manpower", parentCode: "100500" }, + { code: "100506", name: "Drydock- Paints", parentCode: "100500" }, + { code: "100507", name: "Drydock- Steel", parentCode: "100500" }, + { code: "100508", name: "Drydock- Pipelines", parentCode: "100500" }, + { code: "100509", name: "Drydock- Welding consumables", parentCode: "100500" }, + { code: "100510", name: "Drydock- Gases", parentCode: "100500" }, + { code: "100511", name: "Drydock- Cranage", parentCode: "100500" }, + { code: "100512", name: "Drydock- Workshop charges", parentCode: "100500" }, + { code: "100513", name: "Drydock- Technician visit charges", parentCode: "100500" }, + { code: "100514", name: "Drydock- Transportation", parentCode: "100500" }, + { code: "100515", name: "Drydock- Supdt. Travel Expenses", parentCode: "100500" }, + { code: "100516", name: "Drydock- Supdt. Boarding & Lodging", parentCode: "100500" }, + { code: "100517", name: "Drydock- Stores", parentCode: "100500" }, + { code: "100600", name: "MAJOR REPAIRS", parentCode: "100000" }, + { code: "100601", name: "Spares- DP Engine", parentCode: "100600" }, + { code: "100602", name: "Spares- Hyd. Engine", parentCode: "100600" }, + { code: "100603", name: "Spares- Dredge Pump", parentCode: "100600" }, + { code: "100604", name: "Spares- Deck generator", parentCode: "100600" }, + { code: "100605", name: "Spares- Cutter", parentCode: "100600" }, + { code: "100606", name: "Spares- Winches", parentCode: "100600" }, + { code: "100607", name: "Spares- Hydraulics", parentCode: "100600" }, + { code: "100608", name: "Spares- Gear box", parentCode: "100600" }, + { code: "100609", name: "Spares- Transmission system", parentCode: "100600" }, + { code: "100610", name: "Spares- Attached pumps & Coolers", parentCode: "100600" }, + { code: "100611", name: "Spares- Safety Equipment", parentCode: "100600" }, + { code: "100612", name: "Spares- Others", parentCode: "100600" }, + { code: "100613", name: "Spares- Steel Wires", parentCode: "100600" }, + { code: "100614", name: "Spares- Electricals", parentCode: "100600" }, + { code: "100615", name: "Spares- Electronics", parentCode: "100600" }, + { code: "100616", name: "Spares- Gauges & Instrumentation", parentCode: "100600" }, + { code: "100617", name: "Spares- Controls and Automation", parentCode: "100600" }, + { code: "100618", name: "Technician expenses", parentCode: "100600" }, + { code: "100619", name: "Workshop charges", parentCode: "100600" }, + { code: "100620", name: "Transportation", parentCode: "100600" }, + { code: "100621", name: "Other cost", parentCode: "100600" }, + { code: "100622", name: "Equipment rent", parentCode: "100600" }, + { code: "100623", name: "Equipment diesel", parentCode: "100600" }, + { code: "100700", name: "OTHER EQUIPMENT COST", parentCode: "100000" }, + { code: "100701", name: "Dredger equipment", parentCode: "100700" }, + { code: "100702", name: "Office equipment", parentCode: "100700" }, + { code: "100703", name: "Site equipment", parentCode: "100700" }, + { code: "100704", name: "Transportation- Other Equipment", parentCode: "100700" }, + { code: "100800", name: "OTHER INVESTMENT EXPENSES", parentCode: "100000" }, + { code: "100801", name: "Purchase cost", parentCode: "100800" }, + { code: "100802", name: "Upgradation cost", parentCode: "100800" }, + + // ── BUSINESS DEVELOPMENT EXP ───────────────────────────────────────────────── + { code: "200000", name: "BUSINESS DEVELOPMENT EXP", parentCode: null }, + { code: "200100", name: "TENDER FEE", parentCode: "200000" }, + { code: "200101", name: "Tender fee payment", parentCode: "200100" }, + { code: "200200", name: "EMD", parentCode: "200000" }, + { code: "200201", name: "Earnest Money Deposit", parentCode: "200200" }, + { code: "200300", name: "DOCUMENTATION FEE", parentCode: "200000" }, + { code: "200301", name: "Documentation fee payment", parentCode: "200300" }, + { code: "200400", name: "BUSINESS TRAVEL", parentCode: "200000" }, + { code: "200401", name: "Air/train ticket", parentCode: "200400" }, + { code: "200402", name: "Boarding & Lodging", parentCode: "200400" }, + { code: "200403", name: "Local travel", parentCode: "200400" }, + { code: "200404", name: "Food expense", parentCode: "200400" }, + { code: "200500", name: "ENTERTAINMENT EXPENSES", parentCode: "200000" }, + { code: "200501", name: "Client entertainment expenses BD", parentCode: "200500" }, + + // ── OFFICE ADMINISTRATION ───────────────────────────────────────────────────── + { code: "300000", name: "OFFICE ADMINISTRATION", parentCode: null }, + { code: "300100", name: "OFFICE RENT", parentCode: "300000" }, + { code: "300101", name: "Premises rent", parentCode: "300100" }, + { code: "300200", name: "OFFICE PROPERTY TAX", parentCode: "300000" }, + { code: "300201", name: "Property tax payment", parentCode: "300200" }, + { code: "300300", name: "OFFICE BILLS", parentCode: "300000" }, + { code: "300301", name: "Electricity bill- Office", parentCode: "300300" }, + { code: "300302", name: "Water bill- Office", parentCode: "300300" }, + { code: "300400", name: "OFFICE MAINTENANCE EXPENSES", parentCode: "300000" }, + { code: "300401", name: "Society dues", parentCode: "300400" }, + { code: "300402", name: "AMC charges", parentCode: "300400" }, + { code: "300403", name: "Other repairs- Office", parentCode: "300400" }, + { code: "300404", name: "Refreshment/Pantry expenses", parentCode: "300400" }, + { code: "300405", name: "Housekeeping expenses", parentCode: "300400" }, + { code: "300406", name: "Other Expenses- Office", parentCode: "300400" }, + { code: "300500", name: "OFFICE STATIONERY", parentCode: "300000" }, + { code: "300501", name: "Stationery- Office", parentCode: "300500" }, + { code: "300600", name: "COMPUTERS & PERIPHERALS", parentCode: "300000" }, + { code: "300601", name: "Software expenses", parentCode: "300600" }, + { code: "300602", name: "Hardware expenses", parentCode: "300600" }, + { code: "300700", name: "OFFICE COMMUNICATION", parentCode: "300000" }, + { code: "300701", name: "Courier & Postage", parentCode: "300700" }, + { code: "300702", name: "Telephone and Data", parentCode: "300700" }, + { code: "300703", name: "Mobile bills", parentCode: "300700" }, + { code: "300800", name: "OFFICE VEHICLE EXPENSES", parentCode: "300000" }, + { code: "300801", name: "Vehicle hire", parentCode: "300800" }, + { code: "300802", name: "Fuel", parentCode: "300800" }, + { code: "300803", name: "Repairs", parentCode: "300800" }, + { code: "300804", name: "Insurance premium", parentCode: "300800" }, + { code: "300805", name: "Tax & Tolls", parentCode: "300800" }, + { code: "300900", name: "OFFICE STAFF TRAVEL", parentCode: "300000" }, + { code: "300901", name: "Travel Expense- Official", parentCode: "300900" }, + { code: "300902", name: "Travel allowance", parentCode: "300900" }, + { code: "301000", name: "OFFICE STAFF SALARY", parentCode: "300000" }, + { code: "301001", name: "Office Staff Wages", parentCode: "301000" }, + { code: "301100", name: "OFFICE STAFF PF", parentCode: "300000" }, + { code: "301101", name: "Office Staff EPF- Company Cost", parentCode: "301100" }, + { code: "301200", name: "OFFICE STAFF BONUS", parentCode: "300000" }, + { code: "301201", name: "Annual Bonus", parentCode: "301200" }, + { code: "301202", name: "Performance Bonus", parentCode: "301200" }, + { code: "301300", name: "OFFICE STAFF INSURANCE PREM", parentCode: "300000" }, + { code: "301301", name: "Mediclaim Policy Premium", parentCode: "301300" }, + { code: "301302", name: "Term Policy Premium", parentCode: "301300" }, + { code: "301400", name: "CONSULTANT HIRE CHARGES", parentCode: "300000" }, + { code: "301401", name: "Consultant service charges", parentCode: "301400" }, + { code: "301402", name: "Consultant travel charges", parentCode: "301400" }, + { code: "301403", name: "Consultant- Other charges", parentCode: "301400" }, + { code: "301500", name: "BANKING CHARGES", parentCode: "300000" }, + { code: "301501", name: "Bank charges- Regular", parentCode: "301500" }, + { code: "301502", name: "Processing fee / Account renewal", parentCode: "301500" }, + { code: "301503", name: "Bank charges- Remittance", parentCode: "301500" }, + { code: "301504", name: "Bank charges- POD", parentCode: "301500" }, + { code: "301505", name: "Bank charges- Others", parentCode: "301500" }, + { code: "301600", name: "BANK INTEREST CHARGES", parentCode: "300000" }, + { code: "301601", name: "Bank Interest on hypothecated loan", parentCode: "301600" }, + { code: "301602", name: "Bank Interest on unsecured loan", parentCode: "301600" }, + { code: "301603", name: "Bank penalty & other charges", parentCode: "301600" }, + { code: "301604", name: "Bank Interest on CC/OD Loan", parentCode: "301600" }, + { code: "301700", name: "MCA DUES", parentCode: "300000" }, + { code: "301701", name: "MCA Fee", parentCode: "301700" }, + { code: "301800", name: "OFFICE AUDIT EXPENSES", parentCode: "300000" }, + { code: "301801", name: "Audit fee", parentCode: "301800" }, + { code: "301802", name: "Surveyor travel expenses", parentCode: "301800" }, + { code: "301803", name: "Surveyor entertainment expenses", parentCode: "301800" }, + { code: "301900", name: "TRAINING & DEVELOPMENT", parentCode: "300000" }, + { code: "301901", name: "Training fee", parentCode: "301900" }, + { code: "301902", name: "Other expenses", parentCode: "301900" }, + { code: "302000", name: "LEGAL EXP", parentCode: "300000" }, + { code: "302001", name: "Advocate fee", parentCode: "302000" }, + { code: "302002", name: "Arbitrator fee", parentCode: "302000" }, + { code: "302003", name: "Court charges", parentCode: "302000" }, + { code: "302004", name: "Fines & Penalties", parentCode: "302000" }, + { code: "302005", name: "Stamp duty", parentCode: "302000" }, + { code: "302006", name: "Legal- Travel & Logistics", parentCode: "302000" }, + { code: "302007", name: "Legal cost- Others", parentCode: "302000" }, + { code: "302100", name: "GUEST HOUSE EXPENSES", parentCode: "300000" }, + { code: "302101", name: "Guest house rent", parentCode: "302100" }, + { code: "302102", name: "Electricity bill", parentCode: "302100" }, + { code: "302103", name: "Maintenance cost", parentCode: "302100" }, + { code: "302104", name: "Other expenses", parentCode: "302100" }, + { code: "302200", name: "MEMBERSHIP & SUBSCRIPTION CHARGES", parentCode: "300000" }, + { code: "302201", name: "Subscription Fee", parentCode: "302200" }, + { code: "302300", name: "LICENSE RENEWAL CHARGES", parentCode: "300000" }, + { code: "302301", name: "License renewal fee", parentCode: "302300" }, + { code: "302400", name: "OFFICE STAFF WELFARE EXPENSES", parentCode: "300000" }, + { code: "302401", name: "Office staff Medical Expenses", parentCode: "302400" }, + { code: "302402", name: "Office staff recreation expenses", parentCode: "302400" }, + { code: "302500", name: "SECURITY DEPOSIT", parentCode: "300000" }, + { code: "302501", name: "Security deposit for site equipment", parentCode: "302500" }, + { code: "302502", name: "Security deposit for Office Equipment", parentCode: "302500" }, + + // ── PROJECT EXPENSES ────────────────────────────────────────────────────────── + { code: "400000", name: "PROJECT EXPENSES", parentCode: null }, + { code: "400100", name: "CHARTER HIRE", parentCode: "400000" }, + { code: "400101", name: "Charter hire payment", parentCode: "400100" }, + { code: "400200", name: "MOB & DEMOB", parentCode: "400000" }, + { code: "400201", name: "Manpower", parentCode: "400200" }, + { code: "400202", name: "Cranes/Hydra", parentCode: "400200" }, + { code: "400203", name: "Transportation", parentCode: "400200" }, + { code: "400204", name: "Lashing material", parentCode: "400200" }, + { code: "400205", name: "Towing", parentCode: "400200" }, + { code: "400206", name: "Workshop expenses", parentCode: "400200" }, + { code: "400207", name: "Welding & Cutting", parentCode: "400200" }, + { code: "400208", name: "Berth rent", parentCode: "400200" }, + { code: "400209", name: "Union dues", parentCode: "400200" }, + { code: "400210", name: "Facilitation expenses", parentCode: "400200" }, + { code: "400211", name: "Mob-demob Agency charges", parentCode: "400200" }, + { code: "400212", name: "Mob-demob lumpsum cost", parentCode: "400200" }, + { code: "400213", name: "Equipment Insurance Cost", parentCode: "400200" }, + { code: "400300", name: "SITE EXPENSES", parentCode: "400000" }, + { code: "400301", name: "Victualling expense", parentCode: "400300" }, + { code: "400302", name: "Saloon Stores", parentCode: "400300" }, + { code: "400303", name: "Crew welfare", parentCode: "400300" }, + { code: "400304", name: "Crew transport", parentCode: "400300" }, + { code: "400305", name: "Transportation", parentCode: "400300" }, + { code: "400306", name: "Medical", parentCode: "400300" }, + { code: "400307", name: "Hotel stay and food", parentCode: "400300" }, + { code: "400308", name: "Accommodation rent", parentCode: "400300" }, + { code: "400309", name: "Electricity bill", parentCode: "400300" }, + { code: "400310", name: "Union dues", parentCode: "400300" }, + { code: "400311", name: "Contracted Labour Charges", parentCode: "400300" }, + { code: "400312", name: "Boat expenses", parentCode: "400300" }, + { code: "400313", name: "Vehicle hire", parentCode: "400300" }, + { code: "400314", name: "Vehicle diesel", parentCode: "400300" }, + { code: "400315", name: "Vehicle maintenance", parentCode: "400300" }, + { code: "400316", name: "Vehicle tax/toll/parking", parentCode: "400300" }, + { code: "400317", name: "Stationery", parentCode: "400300" }, + { code: "400318", name: "Computers & Peripherals", parentCode: "400300" }, + { code: "400319", name: "Postage & Courier", parentCode: "400300" }, + { code: "400320", name: "Communication", parentCode: "400300" }, + { code: "400321", name: "Agency charges", parentCode: "400300" }, + { code: "400322", name: "Entertainment", parentCode: "400300" }, + { code: "400323", name: "Sundries", parentCode: "400300" }, + { code: "400324", name: "Covid expenses incl. insurance", parentCode: "400300" }, + { code: "400400", name: "RECLAMATION EXPENSES", parentCode: "400000" }, + { code: "400401", name: "Survey expenses", parentCode: "400400" }, + { code: "400402", name: "Excavator / Hydra Hire", parentCode: "400400" }, + { code: "400403", name: "Excavator Diesel", parentCode: "400400" }, + { code: "400404", name: "Excavator Lubes", parentCode: "400400" }, + { code: "400405", name: "Excavator Spares", parentCode: "400400" }, + { code: "400406", name: "Excavator Repairs", parentCode: "400400" }, + { code: "400407", name: "Pipeline repairs & maintenance", parentCode: "400400" }, + { code: "400408", name: "Transportation (Reclaim)", parentCode: "400400" }, + { code: "400409", name: "Reclamation stores & consumables", parentCode: "400400" }, + { code: "400500", name: "PORT DUES", parentCode: "400000" }, + { code: "400501", name: "Port dues charges", parentCode: "400500" }, + { code: "400502", name: "Bank charges for PBG", parentCode: "400500" }, + { code: "400600", name: "PERFORMANCE BANK GUARANTEE", parentCode: "400000" }, + { code: "400601", name: "Value of PBG", parentCode: "400600" }, + { code: "400602", name: "Banking charges", parentCode: "400600" }, + { code: "400700", name: "BANKING CHARGES", parentCode: "400000" }, + { code: "400800", name: "DOCUMENTATION CHARGES", parentCode: "400000" }, + { code: "400900", name: "PROJECT RELATED TRAVEL", parentCode: "400000" }, + { code: "400901", name: "Office staff travel", parentCode: "400900" }, + { code: "400902", name: "Other staff travel", parentCode: "400900" }, + { code: "400903", name: "Office staff hotel & food expense", parentCode: "400900" }, + { code: "400904", name: "Other staff hotel & food expense", parentCode: "400900" }, + { code: "401000", name: "REPAIRS & MAINTENANCE", parentCode: "400000" }, + { code: "401001", name: "Spares- DP Engine", parentCode: "401000" }, + { code: "401002", name: "Spares- Hyd. Engine", parentCode: "401000" }, + { code: "401003", name: "Spares- Dredge Pump", parentCode: "401000" }, + { code: "401004", name: "Spares- Deck generator", parentCode: "401000" }, + { code: "401005", name: "Spares- Cutter", parentCode: "401000" }, + { code: "401006", name: "Spares- Winches", parentCode: "401000" }, + { code: "401007", name: "Spares- Hydraulics", parentCode: "401000" }, + { code: "401008", name: "Spares- Gear box", parentCode: "401000" }, + { code: "401009", name: "Spares- Transmission system", parentCode: "401000" }, + { code: "401010", name: "Spares- Attached pumps & Coolers", parentCode: "401000" }, + { code: "401011", name: "Spares- Safety Equipment", parentCode: "401000" }, + { code: "401012", name: "Spares- Others", parentCode: "401000" }, + { code: "401013", name: "Spares- Steel Wires", parentCode: "401000" }, + { code: "401014", name: "Spares- Electricals", parentCode: "401000" }, + { code: "401015", name: "Spares- Electronics", parentCode: "401000" }, + { code: "401016", name: "Spares- Gauges & Instrumentation", parentCode: "401000" }, + { code: "401017", name: "Spares- Controls and Automation", parentCode: "401000" }, + { code: "401018", name: "Stores- Steel", parentCode: "401000" }, + { code: "401019", name: "Stores- Pipelines", parentCode: "401000" }, + { code: "401020", name: "Stores- Consumables", parentCode: "401000" }, + { code: "401021", name: "Stores- Ropes", parentCode: "401000" }, + { code: "401022", name: "Stores- Lifting gear", parentCode: "401000" }, + { code: "401023", name: "Stores- Safety Equipment & PPE", parentCode: "401000" }, + { code: "401024", name: "Stores- Cleaning gear", parentCode: "401000" }, + { code: "401025", name: "Stores- Welding consumables", parentCode: "401000" }, + { code: "401026", name: "Stores- Gases", parentCode: "401000" }, + { code: "401027", name: "Stores- Paints", parentCode: "401000" }, + { code: "401028", name: "Stores- Electrical", parentCode: "401000" }, + { code: "401029", name: "Stores- Nuts & Bolts", parentCode: "401000" }, + { code: "401030", name: "Stores- Tools", parentCode: "401000" }, + { code: "401031", name: "Stores- Others", parentCode: "401000" }, + { code: "401032", name: "Workshop charges", parentCode: "401000" }, + { code: "401033", name: "Technician visit charges", parentCode: "401000" }, + { code: "401034", name: "Travel & Transportation", parentCode: "401000" }, + { code: "401035", name: "Crane/Hydra charges for repairs", parentCode: "401000" }, + { code: "401100", name: "CLIENT ENTERTAINMENT", parentCode: "400000" }, + { code: "401101", name: "Client entertainment expenses Project", parentCode: "401100" }, + + // ── MANNING ─────────────────────────────────────────────────────────────────── + { code: "500000", name: "MANNING", parentCode: null }, + { code: "500100", name: "CREW WAGES", parentCode: "500000" }, + { code: "500101", name: "Salary", parentCode: "500100" }, + { code: "500102", name: "Employee Provident Fund Contribution", parentCode: "500100" }, + { code: "500103", name: "Seaman Provident Fund Contribution", parentCode: "500100" }, + { code: "500104", name: "Levy to Seaman Employment Office", parentCode: "500100" }, + { code: "500105", name: "Crew Welfare to Seaman Employment Office", parentCode: "500100" }, + { code: "500106", name: "ESIC Contribution", parentCode: "500100" }, + { code: "500107", name: "WC Premium", parentCode: "500100" }, + { code: "500108", name: "Income Tax to Seafarer", parentCode: "500100" }, + { code: "500109", name: "Professional Tax to Seafarer", parentCode: "500100" }, + { code: "500110", name: "Bonus to Seafarer", parentCode: "500100" }, + { code: "500111", name: "Labour Welfare Fund Contribution", parentCode: "500100" }, + { code: "500200", name: "SIGN-ON/OFF", parentCode: "500000" }, + { code: "500201", name: "Interstate travel", parentCode: "500200" }, + { code: "500202", name: "Local Travel", parentCode: "500200" }, + { code: "500203", name: "Medical", parentCode: "500200" }, + { code: "500204", name: "Hotel", parentCode: "500200" }, + { code: "500205", name: "Food allowance", parentCode: "500200" }, + { code: "500206", name: "Personal Protective Equipment", parentCode: "500200" }, + { code: "500207", name: "License fee etc.", parentCode: "500200" }, + { code: "500208", name: "Course/ Training fee", parentCode: "500200" }, + { code: "500209", name: "Travelling Allowance", parentCode: "500200" }, + { code: "500210", name: "Covid related expenses", parentCode: "500200" }, + { code: "500211", name: "International travel Air Fare", parentCode: "500200" }, + { code: "500212", name: "Crew Agency charges", parentCode: "500200" }, + { code: "500213", name: "Visa charges", parentCode: "500200" }, + { code: "500214", name: "Meet n Greet Charges", parentCode: "500200" }, + { code: "500215", name: "Change of Command Fee", parentCode: "500200" }, + + // ── TECHNICAL ───────────────────────────────────────────────────────────────── + { code: "600000", name: "TECHNICAL", parentCode: null }, + { code: "600100", name: "DOCKING EXPENSES (TECHNICAL)", parentCode: "600000" }, + { code: "600101", name: "Drydock- Stores n Consumables", parentCode: "600100" }, + { code: "600102", name: "Drydock- Ropes n wires", parentCode: "600100" }, + { code: "600103", name: "Drydock- Lifting gear", parentCode: "600100" }, + { code: "600104", name: "Drydock- Safety Equipment & PPE", parentCode: "600100" }, + { code: "600105", name: "Drydock- Cleaning gear", parentCode: "600100" }, + { code: "600106", name: "Drydock- Nuts n bolts", parentCode: "600100" }, + { code: "600107", name: "Drydock- Electrical stores", parentCode: "600100" }, + { code: "600108", name: "Drydock- Tools", parentCode: "600100" }, + { code: "600109", name: "Drydock- Cranage", parentCode: "600100" }, + { code: "600110", name: "Drydock- Transportation", parentCode: "600100" }, + { code: "600200", name: "SURVEY & CERTIFICATION", parentCode: "600000" }, + { code: "600201", name: "Flag Survey fee", parentCode: "600200" }, + { code: "600202", name: "Classification Society fee", parentCode: "600200" }, + { code: "600203", name: "Survey travel expenses", parentCode: "600200" }, + { code: "600204", name: "Entertainment expenses", parentCode: "600200" }, + { code: "600300", name: "INSURANCE PREMIUM", parentCode: "600000" }, + { code: "600301", name: "H&M Insurance premium", parentCode: "600300" }, + { code: "600302", name: "Loss of Hire Insurance", parentCode: "600300" }, + { code: "600303", name: "P&I Insurance", parentCode: "600300" }, + { code: "600304", name: "War risk", parentCode: "600300" }, + { code: "600305", name: "Other insurance", parentCode: "600300" }, + + // ── BUNKER, LUBES & WATER ───────────────────────────────────────────────────── + { code: "700000", name: "BUNKER, LUBES & WATER", parentCode: null }, + { code: "700100", name: "FUEL OIL", parentCode: "700000" }, + { code: "700101", name: "Diesel", parentCode: "700100" }, + { code: "700102", name: "Bio-diesel", parentCode: "700100" }, + { code: "700103", name: "CNG/LNG", parentCode: "700100" }, + { code: "700104", name: "Other fuels", parentCode: "700100" }, + { code: "700105", name: "Transportation (fuel oil)", parentCode: "700100" }, + { code: "700200", name: "OTHER OILS ETC.", parentCode: "700000" }, + { code: "700201", name: "Engine oil", parentCode: "700200" }, + { code: "700202", name: "Hydraulic Oil", parentCode: "700200" }, + { code: "700203", name: "Gear Oil", parentCode: "700200" }, + { code: "700204", name: "Grease", parentCode: "700200" }, + { code: "700205", name: "Coolant", parentCode: "700200" }, + { code: "700206", name: "Distil Water", parentCode: "700200" }, + { code: "700207", name: "Transportation (Other oils)", parentCode: "700200" }, + { code: "700300", name: "ANALYSIS EXPENSE", parentCode: "700000" }, + { code: "700301", name: "Fuel oil analysis", parentCode: "700300" }, + { code: "700302", name: "Engine oil analysis", parentCode: "700300" }, + { code: "700303", name: "Hydraulic oil analysis", parentCode: "700300" }, + { code: "700304", name: "Coolant analysis", parentCode: "700300" }, + { code: "700305", name: "Other oils analysis", parentCode: "700300" }, + { code: "700306", name: "Courier charges", parentCode: "700300" }, +]; diff --git a/App/prisma/migrations/20260530000001_account_hierarchy/migration.sql b/App/prisma/migrations/20260530000001_account_hierarchy/migration.sql new file mode 100644 index 0000000..0ddb492 --- /dev/null +++ b/App/prisma/migrations/20260530000001_account_hierarchy/migration.sql @@ -0,0 +1,5 @@ +-- AlterTable: add parentId for hierarchical accounting codes (3-level: TopCategory > SubCategory > Item) +ALTER TABLE "Account" ADD COLUMN "parentId" TEXT; + +ALTER TABLE "Account" ADD CONSTRAINT "Account_parentId_fkey" + FOREIGN KEY ("parentId") REFERENCES "Account"("id") ON DELETE SET NULL ON UPDATE CASCADE; diff --git a/App/prisma/schema.prisma b/App/prisma/schema.prisma index 777b6ad..268c5a9 100644 --- a/App/prisma/schema.prisma +++ b/App/prisma/schema.prisma @@ -125,6 +125,10 @@ model Account { description String? isActive Boolean @default(true) + parentId String? + parent Account? @relation("AccountHierarchy", fields: [parentId], references: [id]) + children Account[] @relation("AccountHierarchy") + purchaseOrders PurchaseOrder[] lineItems POLineItem[] } diff --git a/App/prisma/seed.ts b/App/prisma/seed.ts index a195cc9..edc4222 100644 --- a/App/prisma/seed.ts +++ b/App/prisma/seed.ts @@ -1,5 +1,6 @@ import { PrismaClient, Role } from "@prisma/client"; import bcrypt from "bcryptjs"; +import { ACCOUNTING_CODES } from "./accounting-codes-data"; const db = new PrismaClient(); @@ -177,78 +178,43 @@ async function main() { const mvCallisto = await findOrCreateVessel("MV Callisto", siteGOA.id, "SITE-010"); await findOrCreateVessel("MV Doris", siteCHE.id, "SITE-011"); - // ─── Accounts ──────────────────────────────────────────────────────────────── - const accTechOps = await db.account.upsert({ - where: { code: "TECH-OPS" }, - update: {}, - create: { code: "TECH-OPS", name: "Technical Operations", description: "Engine, deck equipment and spare parts" }, - }); + // ─── Accounting Codes (hierarchical) ───────────────────────────────────────── + // Seed in two passes: first create all entries without parentId, then link parents + const codeIdMap = new Map(); - const accCrewMgt = await db.account.upsert({ - where: { code: "CREW-MGT" }, - update: {}, - create: { code: "CREW-MGT", name: "Crew Management", description: "Manning and crew welfare" }, - }); + // Pass 1: upsert all entries without parentId to get their IDs + for (const entry of ACCOUNTING_CODES) { + const rec = await db.account.upsert({ + where: { code: entry.code }, + update: { name: entry.name }, + create: { code: entry.code, name: entry.name }, + }); + codeIdMap.set(entry.code, rec.id); + } - const accFuel = await db.account.upsert({ - where: { code: "FUEL-BNK" }, - update: {}, - create: { code: "FUEL-BNK", name: "Fuel & Bunkers", description: "Fuel procurement and bunkering" }, - }); + // Pass 2: link parent relationships + for (const entry of ACCOUNTING_CODES) { + if (entry.parentCode) { + const parentId = codeIdMap.get(entry.parentCode); + if (parentId) { + await db.account.update({ + where: { code: entry.code }, + data: { parentId }, + }); + } + } + } - const accSafety = await db.account.upsert({ - where: { code: "SAFETY" }, - update: {}, - create: { code: "SAFETY", name: "Safety & Lifesaving", description: "LSA, firefighting, immersion suits, EPIRBs" }, - }); - - const accPaint = await db.account.upsert({ - where: { code: "PAINT-MAINT" }, - update: {}, - create: { code: "PAINT-MAINT", name: "Paint & Maintenance", description: "Hull painting, surface prep, coatings" }, - }); - - const accElect = await db.account.upsert({ - where: { code: "ELECT" }, - update: {}, - create: { code: "ELECT", name: "Electrical Systems", description: "Navigation lights, batteries, marine cable" }, - }); - - const accNavig = await db.account.upsert({ - where: { code: "NAVIG" }, - update: {}, - create: { code: "NAVIG", name: "Navigation & Charts", description: "ECDIS updates, chart folios, publications" }, - }); - - await db.account.upsert({ - where: { code: "PROVISION" }, - update: {}, - create: { code: "PROVISION", name: "Crew Provisions", description: "Food, water and domestic supplies" }, - }); - - const accStores = await db.account.upsert({ - where: { code: "GEN-STORES" }, - update: {}, - create: { code: "GEN-STORES", name: "General Stores", description: "Consumables, cleaning materials, PPE" }, - }); - - await db.account.upsert({ - where: { code: "CHEM-TREAT" }, - update: {}, - create: { code: "CHEM-TREAT", name: "Chemicals & Treatments", description: "Boiler water treatment, biocides, cleaners" }, - }); - - const accDeck = await db.account.upsert({ - where: { code: "DECK-EQUIP" }, - update: {}, - create: { code: "DECK-EQUIP", name: "Deck Equipment", description: "Mooring, anchor, deck machinery" }, - }); - - await db.account.upsert({ - where: { code: "ROPE-RIGG" }, - update: {}, - create: { code: "ROPE-RIGG", name: "Rope & Rigging", description: "Mooring ropes, pilot ladders, wire rope" }, - }); + // Convenience variables for PO seed data below (map to real leaf codes) + const accTechOps = { id: codeIdMap.get("401012")! }; // Spares- Others + const accCrewMgt = { id: codeIdMap.get("500101")! }; // Salary + const accFuel = { id: codeIdMap.get("700101")! }; // Diesel + const accSafety = { id: codeIdMap.get("401023")! }; // Stores- Safety Equipment & PPE + const accPaint = { id: codeIdMap.get("401027")! }; // Stores- Paints + const accElect = { id: codeIdMap.get("401028")! }; // Stores- Electrical + const accNavig = { id: codeIdMap.get("600201")! }; // Flag Survey fee + const accStores = { id: codeIdMap.get("401031")! }; // Stores- Others + const accDeck = { id: codeIdMap.get("401030")! }; // Stores- Tools // ─── Vendors ───────────────────────────────────────────────────────────────── const v1 = await db.vendor.upsert({