From 9a38cc1131766977334441c0bf1faf845688c963 Mon Sep 17 00:00:00 2001 From: Sasanka20 Date: Tue, 18 Aug 2026 11:54:16 +0530 Subject: [PATCH] Refactor code structure for improved readability and maintainability --- .../app/dashboard/sales/bundles/page.tsx | 8 +- .../app/dashboard/sales/day-end/page.tsx | 2 +- .../dashboard/sales/free-issues/new/page.tsx | 4 +- .../app/dashboard/sales/invoices/new/page.tsx | 2 +- .../app/dashboard/sales/invoices/page.tsx | 18 +- .../erp-system/app/dashboard/sales/page.tsx | 8 +- .../app/dashboard/sales/slips/[id]/page.tsx | 479 ------------------ .../app/dashboard/sales/slips/new/page.tsx | 435 ---------------- .../app/dashboard/sales/slips/page.tsx | 235 --------- Frontend/erp-system/app/login/page.tsx | 2 +- .../app/print/sales/slips/[id]/page.tsx | 145 ------ .../erp-system/app/print/sales/slips/page.tsx | 114 ----- .../components/Layouts/AppSidebar.tsx | 34 +- .../components/Layouts/Breadcrumbs.tsx | 1 + .../erp-system/components/Layouts/Header.tsx | 3 + .../components/sales/FreeIssueSuggestions.tsx | 42 -- .../{logo (2).png => hexa-erp-logo.png} | Bin 17 files changed, 39 insertions(+), 1493 deletions(-) delete mode 100644 Frontend/erp-system/app/dashboard/sales/slips/[id]/page.tsx delete mode 100644 Frontend/erp-system/app/dashboard/sales/slips/new/page.tsx delete mode 100644 Frontend/erp-system/app/dashboard/sales/slips/page.tsx delete mode 100644 Frontend/erp-system/app/print/sales/slips/[id]/page.tsx delete mode 100644 Frontend/erp-system/app/print/sales/slips/page.tsx delete mode 100644 Frontend/erp-system/components/sales/FreeIssueSuggestions.tsx rename Frontend/erp-system/public/images/{logo (2).png => hexa-erp-logo.png} (100%) diff --git a/Frontend/erp-system/app/dashboard/sales/bundles/page.tsx b/Frontend/erp-system/app/dashboard/sales/bundles/page.tsx index 5d84432..75e182a 100644 --- a/Frontend/erp-system/app/dashboard/sales/bundles/page.tsx +++ b/Frontend/erp-system/app/dashboard/sales/bundles/page.tsx @@ -244,8 +244,8 @@ export default function BundleSalesPage() { {row.customerSnapshotName} {new Date(row.createdAt).toLocaleDateString()} - {row.bundlePrice.toFixed(2)} - {row.grandTotal.toFixed(2)} + {row.bundlePrice.toFixed(2)} + {row.grandTotal.toFixed(2)} {row.status} @@ -271,11 +271,11 @@ export default function BundleSalesPage() {
Rows loaded
-
{visibleRows.length}
+
{visibleRows.length}
Grand total
-
{bundleTotal.toFixed(2)}
+
{bundleTotal.toFixed(2)}
diff --git a/Frontend/erp-system/app/dashboard/sales/day-end/page.tsx b/Frontend/erp-system/app/dashboard/sales/day-end/page.tsx index a980a10..1b7143b 100644 --- a/Frontend/erp-system/app/dashboard/sales/day-end/page.tsx +++ b/Frontend/erp-system/app/dashboard/sales/day-end/page.tsx @@ -145,7 +145,7 @@ export default function SalesDayEndPage() {
    {preview.draftSlipsBlockingClose.map((s) => (
  • - {s.slipNo} — {s.grandTotal.toFixed(2)} + {s.slipNo} — {s.grandTotal.toFixed(2)}
  • ))} {preview.draftBundleSalesBlockingClose.map((b) => ( diff --git a/Frontend/erp-system/app/dashboard/sales/free-issues/new/page.tsx b/Frontend/erp-system/app/dashboard/sales/free-issues/new/page.tsx index 719d1b5..c3a577c 100644 --- a/Frontend/erp-system/app/dashboard/sales/free-issues/new/page.tsx +++ b/Frontend/erp-system/app/dashboard/sales/free-issues/new/page.tsx @@ -431,8 +431,8 @@ export default function NewFreeIssuePage() {
    {row.itemName}
    {row.itemSku} · {row.uomName}
    - {row.qty} - {row.freeQty} + {row.qty} + {row.freeQty}
    diff --git a/Frontend/erp-system/app/dashboard/sales/invoices/page.tsx b/Frontend/erp-system/app/dashboard/sales/invoices/page.tsx index d439ef7..b6a9d4a 100644 --- a/Frontend/erp-system/app/dashboard/sales/invoices/page.tsx +++ b/Frontend/erp-system/app/dashboard/sales/invoices/page.tsx @@ -75,7 +75,7 @@ export default function SalesInvoicesPage() {
    -

    Sales Invoices

    +

    Sales

    Invoice register with filters, posting flow, and settlement tracking.

    @@ -90,7 +90,7 @@ export default function SalesInvoicesPage() { - New Invoice + New Sale
    @@ -172,10 +172,10 @@ export default function SalesInvoicesPage() { {row.customerSnapshotName} {new Date(row.createdAt).toLocaleDateString()} {new Date(row.invoiceDate).toLocaleDateString()} - {row.totals.freeQtyTotal.toFixed(2)} - {row.totals.subtotal.toFixed(2)} - -{row.totals.discountTotal.toFixed(2)} - {row.totals.grandTotal.toFixed(2)} + {row.totals.freeQtyTotal.toFixed(2)} + {row.totals.subtotal.toFixed(2)} + -{row.totals.discountTotal.toFixed(2)} + {row.totals.grandTotal.toFixed(2)} {row.status} @@ -200,15 +200,15 @@ export default function SalesInvoicesPage() {
    Gross total
    -
    {grossTotal.toFixed(2)}
    +
    {grossTotal.toFixed(2)}
    Discount total
    -
    -{discountTotal.toFixed(2)}
    +
    -{discountTotal.toFixed(2)}
    Net total
    -
    {netTotal.toFixed(2)}
    +
    {netTotal.toFixed(2)}
    diff --git a/Frontend/erp-system/app/dashboard/sales/page.tsx b/Frontend/erp-system/app/dashboard/sales/page.tsx index 5ee4b12..7a1de31 100644 --- a/Frontend/erp-system/app/dashboard/sales/page.tsx +++ b/Frontend/erp-system/app/dashboard/sales/page.tsx @@ -1,5 +1,5 @@ import Link from "next/link" -import { FileBarChart, FileText, Lock, PackageX, ReceiptText, ShoppingCart } from "lucide-react" +import { FileBarChart, FileText, Lock, PackageX, ReceiptText } from "lucide-react" import { buttonVariants } from "@/components/ui/button" import { cn } from "@/lib/utils" @@ -11,12 +11,6 @@ const sections = [ href: "/dashboard/sales/invoices", icon: FileText, }, - { - title: "Slips", - description: "Counter-style sales documents.", - href: "/dashboard/sales/slips", - icon: ShoppingCart, - }, { title: "Free Issues", description: "Promotional free-issue slips.", diff --git a/Frontend/erp-system/app/dashboard/sales/slips/[id]/page.tsx b/Frontend/erp-system/app/dashboard/sales/slips/[id]/page.tsx deleted file mode 100644 index 4148db1..0000000 --- a/Frontend/erp-system/app/dashboard/sales/slips/[id]/page.tsx +++ /dev/null @@ -1,479 +0,0 @@ -"use client" - -import { use, useEffect, useMemo, useState } from "react" -import Link from "next/link" -import { ArrowLeft, ExternalLink, Minus, Plus, Printer, Save, Send, X } from "lucide-react" - -import { salesApi } from "@/lib/api/sales" -import { customersApi } from "@/lib/api/customers" -import { itemsApi } from "@/lib/api/items" -import { uomsApi } from "@/lib/api/uoms" -import { baseUomLabel } from "@/lib/uom-label" -import { warehousesApi } from "@/lib/api/warehouses" -import { usersApi } from "@/lib/api/users" -import { errorMessage } from "@/lib/error-map" -import { Button, buttonVariants } from "@/components/ui/button" -import { Input } from "@/components/ui/input" -import { Label } from "@/components/ui/label" -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" -import { Skeleton } from "@/components/ui/skeleton" -import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table" -import { cn } from "@/lib/utils" -import { getSuggestedUnitPrice } from "@/lib/sales-line-utils" -import { FreeIssuePromotionSuggestions } from "@/components/sales/FreeIssuePromotionSuggestions" -import { Customer } from "@/types/customers" -import { ItemListItem, Uom, Warehouse } from "@/types/master-data" -import { ManagedUser } from "@/types/users" -import { CreateSalesSlipLineRequest, SalesFreeIssueSuggestion, SalesSlip, SalesSlipPostingCheck } from "@/types/sales" -import { toast } from "@/components/ui/toast" - -type Line = CreateSalesSlipLineRequest & { key: string } - -const money = new Intl.NumberFormat("en-LK", { - style: "currency", - currency: "LKR", - minimumFractionDigits: 2, -}) - -const blankLine = (key: string): Line => ({ - key, - itemId: 0, - warehouseId: 0, - qty: 1, - freeQty: 0, - unitPrice: null, - allowManualPriceOverride: true, - discountMode: "Percentage", - discountPct: 0, - discountAmount: 0, - discountValue: 0, - taxPct: 0, - isFreeIssue: false, - parentLineId: null, -}) - -export default function SalesSlipDetailPage({ params }: { params: Promise<{ id: string }> }) { - const resolvedParams = use(params) - const slipId = Number(resolvedParams.id) - const [slip, setSlip] = useState(null) - const [etag, setEtag] = useState(null) - const [customers, setCustomers] = useState([]) - const [items, setItems] = useState([]) - const [uoms, setUoms] = useState([]) - const [warehouses, setWarehouses] = useState([]) - const [users, setUsers] = useState([]) - const [promotionSuggestion, setPromotionSuggestion] = useState(null) - const [postingCheck, setPostingCheck] = useState(null) - const [customerId, setCustomerId] = useState(null) - const [warehouseId, setWarehouseId] = useState(null) - const [cashierUserId, setCashierUserId] = useState(null) - const [lines, setLines] = useState([blankLine("line-1")]) - const [error, setError] = useState(null) - const [saving, setSaving] = useState(false) - const [actionBusy, setActionBusy] = useState<"post" | "cancel" | null>(null) - - useEffect(() => { - if (!Number.isFinite(slipId)) { - setError(`Invalid slip id '${resolvedParams.id}'.`) - return - } - Promise.all([ - customersApi.list({ pageSize: 200 }), - itemsApi.list({ pageSize: 200 }), - uomsApi.list({ pageSize: 200 }), - warehousesApi.list({ pageSize: 200 }), - usersApi.list({ pageSize: 200 }), - salesApi.getSlip(slipId), - ]) - .then(([cust, itemRes, uomRes, whRes, userRes, doc]) => { - setCustomers(cust.items) - setItems(itemRes.items) - setUoms(uomRes.items) - setWarehouses(whRes.items) - setUsers(userRes.items) - setSlip(doc.data) - setEtag(doc.etag) - setCustomerId(doc.data.customerId) - setWarehouseId(doc.data.warehouseId) - setCashierUserId(doc.data.cashierUserId) - setPromotionSuggestion(null) - setLines( - doc.data.lines.map((line) => ({ - key: String(line.salesSlipLineId), - itemId: line.itemId, - warehouseId: line.warehouseId, - qty: line.qty, - freeQty: line.freeQty, - unitPrice: line.unitPrice, - allowManualPriceOverride: true, - discountMode: line.discountMode, - discountPct: line.discountPct, - discountAmount: line.discountAmount, - discountValue: 0, - taxPct: line.taxPct, - isFreeIssue: line.isFreeIssue, - parentLineId: line.parentLineId, - })) - ) - }) - .then(async () => { - try { - const suggestions = await salesApi.getFreeIssueSuggestions(slipId) - setPromotionSuggestion(suggestions) - } catch { - setPromotionSuggestion(null) - } - }) - .catch((err) => setError(errorMessage(err))) - }, [resolvedParams.id, slipId]) - - useEffect(() => { - if (!slip || slip.status !== "Draft") { - setPostingCheck(null) - return - } - salesApi - .checkSlipPosting(slipId) - .then((result) => setPostingCheck(result)) - .catch(() => setPostingCheck(null)) - }, [slip, slipId]) - - const subtotal = useMemo( - () => lines.reduce((sum, line) => sum + Number(line.qty || 0) * Number(line.unitPrice ?? 0), 0), - [lines] - ) - - function updateLine(key: string, patch: Partial) { - setLines((prev) => prev.map((line) => (line.key === key ? { ...line, ...patch } : line))) - } - - function selectItem(key: string, itemId: number) { - updateLine(key, { - itemId, - unitPrice: getSuggestedUnitPrice(items, itemId), - }) - } - - function resolveLineUnitPrice(line: Line): number | null { - if (line.unitPrice !== null && line.unitPrice !== undefined) return Number(line.unitPrice) - return getSuggestedUnitPrice(items, line.itemId) - } - - function addLine() { - setLines((prev) => [...prev, blankLine(`line-${Date.now()}`)]) - } - - function removeLine(key: string) { - setLines((prev) => (prev.length === 1 ? prev : prev.filter((line) => line.key !== key))) - } - - async function save() { - if (!customerId || !warehouseId || !cashierUserId || !etag) return - setSaving(true) - setError(null) - try { - const payload = { - customerId, - warehouseId, - cashierUserId, - lines: lines.map((line) => ({ - itemId: Number(line.itemId), - warehouseId: Number(line.warehouseId), - qty: Number(line.qty), - freeQty: Number(line.freeQty), - unitPrice: resolveLineUnitPrice(line), - allowManualPriceOverride: line.allowManualPriceOverride, - discountMode: line.discountMode, - discountPct: Number(line.discountPct), - discountAmount: Number(line.discountAmount), - discountValue: Number(line.discountValue), - taxPct: Number(line.taxPct), - isFreeIssue: line.isFreeIssue, - parentLineId: line.parentLineId || null, - })), - } - const updated = await salesApi.updateSlip(slipId, payload, etag) - setSlip(updated.data) - setEtag(updated.etag) - toast.success("Slip saved", updated.data.slipNo) - } catch (err) { - setError(errorMessage(err)) - } finally { - setSaving(false) - } - } - - async function post() { - if (!postingCheck?.canPost) { - setError("Resolve stock shortages before posting this slip.") - return - } - setActionBusy("post") - setError(null) - try { - const posted = await salesApi.postSlip(slipId) - setSlip(posted) - toast.success("Slip posted", posted.slipNo) - } catch (err) { - setError(errorMessage(err)) - } finally { - setActionBusy(null) - } - } - - async function cancel() { - setActionBusy("cancel") - setError(null) - try { - const cancelled = await salesApi.cancelSlip(slipId) - setSlip(cancelled) - toast.success("Slip cancelled", cancelled.slipNo) - } catch (err) { - setError(errorMessage(err)) - } finally { - setActionBusy(null) - } - } - - if (error && !slip) return
    {error}
    - if (!slip) return
    {error ?? "Slip data is loading or unavailable."}
    - - const locked = slip.status !== "Draft" - const canPost = slip.status === "Draft" && (postingCheck?.canPost ?? true) && actionBusy === null - - return ( -
    -
    -
    - - - -
    -

    Sales Slip

    -

    {slip.slipNo} · {slip.status}

    -
    -
    -
    - - - Print - - - - -
    -
    - - {error &&
    {error}
    } - -
    -
    -
    -
    -
    Sales Slip
    -

    {slip.slipNo}

    -
    - Status: - - {slip.status} - - Date: {new Date(slip.slipDate).toLocaleDateString()} - Cashier: {users.find((u) => u.userId === slip.cashierUserId)?.displayName ?? `#${slip.cashierUserId}`} -
    -
    -
    -
    {customers.find((c) => c.customerId === slip.customerId)?.displayName ?? customers.find((c) => c.customerId === slip.customerId)?.name ?? slip.customerSnapshotName}
    -
    Warehouse: {warehouses.find((w) => w.warehouseId === slip.warehouseId)?.name ?? `#${slip.warehouseId}`}
    -
    Code: {warehouses.find((w) => w.warehouseId === slip.warehouseId)?.code ?? slip.warehouseId}
    -
    -
    -
    - -
    -
    -
    Customer
    -
    {slip.customerSnapshotName}
    -
    Customer ID: {slip.customerId}
    -
    -
    -
    Warehouse
    -
    {warehouses.find((w) => w.warehouseId === slip.warehouseId)?.name ?? `#${slip.warehouseId}`}
    -
    Code: {warehouses.find((w) => w.warehouseId === slip.warehouseId)?.code ?? slip.warehouseId}
    -
    -
    -
    Totals
    -
    -
    Subtotal
    -
    {money.format(slip.totals.subtotal)}
    -
    Discount
    -
    {money.format(slip.totals.discountTotal)}
    -
    Free qty
    -
    {slip.totals.freeQtyTotal.toFixed(0)}
    -
    Grand total
    -
    {money.format(slip.totals.grandTotal)}
    -
    -
    -
    - -
    - - - - Item - UOM - Qty - Free - Unit price - Line total - - - - {slip.lines.map((line) => ( - - -
    {line.description}
    -
    SKU {items.find((i) => i.itemId === line.itemId)?.sku ?? `#${line.itemId}`}
    -
    - {getSuggestedUnitPrice(items, line.itemId) !== null - ? `Suggested: ${money.format(getSuggestedUnitPrice(items, line.itemId) ?? 0)}` - : "No price suggestion available"} -
    -
    - {baseUomLabel(items, uoms, line.itemId)} - {line.qty.toFixed(0)} - {line.freeQty > 0 ? line.freeQty.toFixed(0) : "-"} - {money.format(line.unitPrice)} - {money.format(line.lineTotal)} -
    - ))} -
    -
    -
    -
    - - {slip.status === "Draft" && postingCheck && !postingCheck.canPost ? ( -
    -
    -
    -
    Stock shortage detected before posting
    -
    This slip cannot be posted until every line has enough available stock in the selected warehouse.
    -
    - - - Create GRN - -
    -
    - - - - - - - - - - - - {postingCheck.issues.map((issue) => ( - - - - - - - - ))} - -
    ItemWarehouseRequestedAvailableShort
    -
    {issue.itemSku}
    -
    {issue.itemName}{issue.isFreeIssue ? " · free issue" : ""}
    -
    {issue.warehouseId}{issue.requestedQty.toFixed(0)}{issue.availableQty.toFixed(0)}{issue.shortQty.toFixed(0)}
    -
    -
    - ) : null} - - {slip.status === "Draft" ? ( - <> -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - -
    -
    -

    Lines

    - -
    - - - - Item - UOM - Qty - Free - Unit price - - - - - {lines.map((line) => ( - - - - - {baseUomLabel(items, uoms, line.itemId)} - updateLine(line.key, { qty: Number(e.target.value) })} disabled={locked} /> - updateLine(line.key, { freeQty: Number(e.target.value) })} disabled={locked} /> - updateLine(line.key, { unitPrice: e.target.value === "" ? null : Number(e.target.value) })} disabled={locked} /> - - - ))} - -
    -
    Subtotal: {subtotal.toFixed(2)}
    -
    - - - -
    - Back - - -
    - - ) : null} -
    - ) -} - diff --git a/Frontend/erp-system/app/dashboard/sales/slips/new/page.tsx b/Frontend/erp-system/app/dashboard/sales/slips/new/page.tsx deleted file mode 100644 index 37f6588..0000000 --- a/Frontend/erp-system/app/dashboard/sales/slips/new/page.tsx +++ /dev/null @@ -1,435 +0,0 @@ -"use client" - -import { useEffect, useMemo, useState } from "react" -import { useRouter } from "next/navigation" -import Link from "next/link" -import { ArrowLeft, Minus, Plus, Save, Trash2 } from "lucide-react" - -import { Button, buttonVariants } from "@/components/ui/button" -import { Input } from "@/components/ui/input" -import { Label } from "@/components/ui/label" -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" -import { Badge } from "@/components/ui/badge" -import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table" -import { cn } from "@/lib/utils" -import { getSuggestedUnitPrice } from "@/lib/sales-line-utils" -import { errorMessage } from "@/lib/error-map" -import { salesApi } from "@/lib/api/sales" -import { customersApi } from "@/lib/api/customers" -import { itemsApi } from "@/lib/api/items" -import { uomsApi } from "@/lib/api/uoms" -import { baseUomLabel } from "@/lib/uom-label" -import { warehousesApi } from "@/lib/api/warehouses" -import { usersApi } from "@/lib/api/users" -import { Customer } from "@/types/customers" -import { ItemListItem, Uom, Warehouse } from "@/types/master-data" -import { ManagedUser } from "@/types/users" -import { CreateSalesSlipLineRequest, CreateSalesSlipRequest } from "@/types/sales" -import { toast } from "@/components/ui/toast" - -type Line = CreateSalesSlipLineRequest & { key: string } - -const blankLine = (key: string): Line => ({ - key, - itemId: 0, - warehouseId: 0, - qty: 1, - freeQty: 0, - unitPrice: null, - allowManualPriceOverride: true, - discountMode: "Percentage", - discountPct: 0, - discountAmount: 0, - discountValue: 0, - taxPct: 0, - isFreeIssue: false, - parentLineId: null, -}) - -const lkr = new Intl.NumberFormat("en-LK", { - style: "currency", - currency: "LKR", - minimumFractionDigits: 2, -}) - -export default function NewSalesSlipPage() { - const router = useRouter() - const [customers, setCustomers] = useState([]) - const [items, setItems] = useState([]) - const [uoms, setUoms] = useState([]) - const [warehouses, setWarehouses] = useState([]) - const [users, setUsers] = useState([]) - const [customerId, setCustomerId] = useState(null) - const [warehouseId, setWarehouseId] = useState(null) - const [cashierUserId, setCashierUserId] = useState(null) - const [lines, setLines] = useState([blankLine("line-1")]) - const [loading, setLoading] = useState(true) - const [submitError, setSubmitError] = useState(null) - const [saving, setSaving] = useState(false) - - useEffect(() => { - Promise.all([ - customersApi.list({ pageSize: 200 }), - itemsApi.list({ pageSize: 200 }), - uomsApi.list({ pageSize: 200 }), - warehousesApi.list({ pageSize: 200 }), - usersApi.list({ pageSize: 200 }), - ]) - .then(([cust, itemRes, uomRes, whRes, userRes]) => { - setCustomers(cust.items) - setItems(itemRes.items) - setUoms(uomRes.items) - setWarehouses(whRes.items) - setUsers(userRes.items) - setCustomerId(cust.items[0]?.customerId ?? null) - setWarehouseId(whRes.items[0]?.warehouseId ?? null) - setCashierUserId(userRes.items[0]?.userId ?? null) - setLines([ - { - ...blankLine("line-1"), - itemId: itemRes.items[0]?.itemId ?? 0, - warehouseId: whRes.items[0]?.warehouseId ?? 0, - unitPrice: getSuggestedUnitPrice(itemRes.items, itemRes.items[0]?.itemId), - }, - ]) - }) - .catch((err) => setSubmitError(errorMessage(err))) - .finally(() => setLoading(false)) - }, []) - - function updateLine(key: string, patch: Partial) { - setLines((prev) => prev.map((line) => (line.key === key ? { ...line, ...patch } : line))) - } - - function selectItem(key: string, itemId: number) { - const item = items.find((candidate) => candidate.itemId === itemId) - updateLine(key, { - itemId, - unitPrice: getSuggestedUnitPrice(items, itemId), - }) - } - - function resolveLineUnitPrice(line: Line): number | null { - if (line.unitPrice !== null && line.unitPrice !== undefined) return Number(line.unitPrice) - return getSuggestedUnitPrice(items, line.itemId) - } - - function addLine() { - setLines((prev) => [...prev, blankLine(`line-${Date.now()}`)]) - } - - function removeLine(key: string) { - setLines((prev) => (prev.length === 1 ? prev : prev.filter((line) => line.key !== key))) - } - - const grossTotal = useMemo( - () => lines.reduce((sum, line) => sum + Number(line.unitPrice ?? 0) * Number(line.qty || 0), 0), - [lines], - ) - const discountTotal = useMemo( - () => - lines.reduce((sum, line) => { - const gross = Number(line.unitPrice ?? 0) * Number(line.qty || 0) - const mode = String(line.discountMode) - return sum + (mode === "Amount" ? Number(line.discountAmount || 0) : gross * (Number(line.discountPct || 0) / 100)) - }, 0), - [lines], - ) - const taxTotal = useMemo( - () => - lines.reduce((sum, line) => { - const gross = Number(line.unitPrice ?? 0) * Number(line.qty || 0) - const mode = String(line.discountMode) - const discount = mode === "Amount" ? Number(line.discountAmount || 0) : gross * (Number(line.discountPct || 0) / 100) - const taxable = Math.max(0, gross - discount) - return sum + taxable * (Number(line.taxPct || 0) / 100) - }, 0), - [lines], - ) - const netTotal = Math.max(0, grossTotal - discountTotal) - const payableTotal = netTotal + taxTotal - - async function submit() { - if (!customerId || !warehouseId || !cashierUserId) return setSubmitError("Select customer, warehouse, and cashier.") - if (lines.some((line) => Number(line.itemId) === 0)) return setSubmitError("Select an item for every line.") - if (lines.some((line) => Number(line.warehouseId) === 0)) return setSubmitError("Select a warehouse for every line.") - - const payload: CreateSalesSlipRequest = { - customerId, - warehouseId, - cashierUserId, - lines: lines.map((line) => ({ - itemId: Number(line.itemId), - warehouseId: Number(line.warehouseId), - qty: Number(line.qty), - freeQty: Number(line.freeQty), - unitPrice: resolveLineUnitPrice(line), - allowManualPriceOverride: line.allowManualPriceOverride, - discountMode: line.discountMode, - discountPct: Number(line.discountPct), - discountAmount: Number(line.discountAmount), - discountValue: Number(line.discountValue), - taxPct: Number(line.taxPct), - isFreeIssue: line.isFreeIssue, - parentLineId: line.parentLineId || null, - })), - } - - setSaving(true) - setSubmitError(null) - try { - const created = await salesApi.createSlip(payload) - toast.success("Slip created", created.data.slipNo) - router.push(`/dashboard/sales/slips/${created.data.salesSlipId}`) - } catch (err) { - setSubmitError(errorMessage(err)) - } finally { - setSaving(false) - } - } - - if (loading) { - return
    Loading masters...
    - } - - return ( -
    -
    -
    - - - -
    -

    New sales slip

    -

    Create counter sales slips from the live backend.

    -
    -
    -
    - - -
    -
    - - {submitError ?
    {submitError}
    : null} - -
    -

    Slip header

    -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - -
    - - Draft - - {lkr.format(payableTotal)} -
    -
    -
    -
    - -
    -
    -

    Slip lines

    - -
    -
    - - - - # - Item - UOM - Qty - Free - Unit price - Discount % - Line total - - - - - {lines.map((line, idx) => { - const lineGross = Number(line.unitPrice ?? 0) * Number(line.qty || 0) - const lineDiscount = - String(line.discountMode) === "Amount" - ? Number(line.discountAmount || 0) - : lineGross * (Number(line.discountPct || 0) / 100) - const lineNet = Math.max(0, lineGross - lineDiscount) - - return ( - - {idx + 1} - - - - - {baseUomLabel(items, uoms, line.itemId)} - - - updateLine(line.key, { qty: Number(e.target.value) })} - className="h-9 w-24 text-right font-mono text-sm tabular-nums" - /> - - - updateLine(line.key, { freeQty: Number(e.target.value) })} - className="h-9 w-24 text-right font-mono text-sm tabular-nums" - /> - - - updateLine(line.key, { unitPrice: e.target.value === "" ? null : Number(e.target.value) })} - className="h-9 w-28 text-right font-mono text-sm tabular-nums" - /> -
    - {getSuggestedUnitPrice(items, line.itemId) !== null - ? `Suggested: ${lkr.format(getSuggestedUnitPrice(items, line.itemId) ?? 0)}` - : "No price suggestion available"} -
    -
    - - updateLine(line.key, { discountPct: Number(e.target.value) || 0 })} - className="h-9 w-24 text-right font-mono text-sm tabular-nums" - /> - - {lkr.format(lineNet)} - - - -
    - ) - })} -
    -
    -
    -
    - -
    -
    -

    Slip notes

    -
      -
    • Cashier posting follows the standard sales-slip workflow.
    • -
    • Free issue lines are captured from the slip itself, not from a separate register here.
    • -
    • Use the slip detail page after save to post or cancel.
    • -
    -
    - -
    - {[ - ["Gross", lkr.format(grossTotal)], - ["Discount", `-${lkr.format(discountTotal)}`], - ["Net", lkr.format(netTotal)], - ["Tax", lkr.format(taxTotal)], - ].map(([k, v]) => ( -
    -
    {k}
    -
    {v}
    -
    - ))} -
    -
    Payable
    -
    {lkr.format(payableTotal)}
    -
    -
    -
    - -
    - - Cancel - - -
    -
    - ) -} diff --git a/Frontend/erp-system/app/dashboard/sales/slips/page.tsx b/Frontend/erp-system/app/dashboard/sales/slips/page.tsx deleted file mode 100644 index ad31b4a..0000000 --- a/Frontend/erp-system/app/dashboard/sales/slips/page.tsx +++ /dev/null @@ -1,235 +0,0 @@ -"use client" - -import { useEffect, useMemo, useState } from "react" -import Link from "next/link" -import { ChevronLeft, ChevronRight, Eye, Filter, Package2, Plus, Printer } from "lucide-react" - -import { salesApi } from "@/lib/api/sales" -import { errorMessage } from "@/lib/error-map" -import { Button, buttonVariants } from "@/components/ui/button" -import { Badge } from "@/components/ui/badge" -import { Input } from "@/components/ui/input" -import { Skeleton } from "@/components/ui/skeleton" -import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table" -import { cn } from "@/lib/utils" -import { PaginationMeta } from "@/types/common" -import { SalesSlipStatus, SalesSlipSummary } from "@/types/sales" - -type StatusFilter = SalesSlipStatus | "All" - -const PAGE_SIZE = 10 -const tabs = ["All", "Draft", "Posted", "Cancelled"] as const - -function statusClass(status: SalesSlipStatus) { - switch (status) { - case "Draft": - return "border-amber-200 bg-amber-50 text-amber-800" - case "Posted": - return "border-emerald-200 bg-emerald-50 text-emerald-800" - case "Cancelled": - return "border-rose-200 bg-rose-50 text-rose-800" - } -} - -export default function SalesSlipsPage() { - const [rows, setRows] = useState(null) - const [pagination, setPagination] = useState(null) - const [error, setError] = useState(null) - const [status, setStatus] = useState("All") - const [searchInput, setSearchInput] = useState("") - const [query, setQuery] = useState("") - const [page, setPage] = useState(1) - - useEffect(() => { - const timeout = setTimeout(() => setQuery(searchInput.trim()), 300) - return () => clearTimeout(timeout) - }, [searchInput]) - - useEffect(() => setPage(1), [query, status]) - - useEffect(() => { - setError(null) - salesApi - .listSlips({ page, pageSize: PAGE_SIZE, status: status === "All" ? undefined : status }) - .then((res) => { - setRows(res.items) - setPagination(res.pagination) - }) - .catch((err) => setError(errorMessage(err))) - }, [page, status]) - - const visibleRows = useMemo( - () => - rows?.filter((row) => - `${row.slipNo} ${row.customerSnapshotName}`.toLowerCase().includes(query.toLowerCase()) - ) ?? [], - [rows, query] - ) - const hasFilters = status !== "All" || query.length > 0 - const grossTotal = visibleRows.reduce((sum, row) => sum + row.totals.subtotal, 0) - const discountTotal = visibleRows.reduce((sum, row) => sum + row.totals.discountTotal, 0) - const netTotal = visibleRows.reduce((sum, row) => sum + row.totals.grandTotal, 0) - const printHref = `/print/sales/slips?status=${encodeURIComponent(status)}&q=${encodeURIComponent(query)}` - - return ( -
    -
    -
    -

    Sales Slips

    -

    Counter sales register with posting and cancellation flow.

    -
    -
    - - - Print batch - - - - New Slip - -
    -
    - -
    -
    -
    - {tabs.map((t) => ( - - ))} -
    - -
    - setSearchInput(e.target.value)} - placeholder="Filter by customer or slip number" - className="h-12 w-full lg:max-w-sm" - /> - -
    -
    - - {error &&
    {error}
    } - - {!error && rows === null && ( -
    - {Array.from({ length: 5 }).map((_, i) => ( - - ))} -
    - )} - - {!error && rows !== null && visibleRows.length === 0 && ( -
    - -

    {hasFilters ? "No slips match your filters." : "No slips yet."}

    -
    - )} - - {!error && rows !== null && visibleRows.length > 0 && ( - <> -
    - - - - Slip - Customer - Date - Status - Lines - Gross - Discount - Net - Actions - - - - {visibleRows.map((row) => ( - - - - {row.slipNo} - - - {row.customerSnapshotName} - {new Date(row.createdAt).toLocaleDateString()} - - - {row.status} - - - {row.totals.freeQtyTotal.toFixed(2)} - {row.totals.subtotal.toFixed(2)} - -{row.totals.discountTotal.toFixed(2)} - {row.totals.grandTotal.toFixed(2)} - -
    - - - -
    -
    -
    - ))} -
    -
    -
    - -
    -
    -
    -
    Gross total
    -
    {grossTotal.toFixed(2)}
    -
    -
    -
    Discount total
    -
    -{discountTotal.toFixed(2)}
    -
    -
    -
    Net total
    -
    {netTotal.toFixed(2)}
    -
    -
    -
    - - {pagination && pagination.totalPages > 1 && ( -
    -

    - Showing {(pagination.page - 1) * pagination.pageSize + 1}-{Math.min(pagination.page * pagination.pageSize, pagination.totalItems)} of {pagination.totalItems} -

    -
    - - Page {pagination.page} of {pagination.totalPages} - -
    -
    - )} - - )} -
    -
    - ) -} diff --git a/Frontend/erp-system/app/login/page.tsx b/Frontend/erp-system/app/login/page.tsx index 345bfa1..d2b9876 100644 --- a/Frontend/erp-system/app/login/page.tsx +++ b/Frontend/erp-system/app/login/page.tsx @@ -80,7 +80,7 @@ function LoginForm() {
    - Hexa ERP + Hexa ERP
    diff --git a/Frontend/erp-system/app/print/sales/slips/[id]/page.tsx b/Frontend/erp-system/app/print/sales/slips/[id]/page.tsx deleted file mode 100644 index a86f7e3..0000000 --- a/Frontend/erp-system/app/print/sales/slips/[id]/page.tsx +++ /dev/null @@ -1,145 +0,0 @@ -"use client" - -import { use, useEffect, useMemo, useState } from "react" -import { Printer } from "lucide-react" - -import { salesApi } from "@/lib/api/sales" -import { customersApi } from "@/lib/api/customers" -import { itemsApi } from "@/lib/api/items" -import { uomsApi } from "@/lib/api/uoms" -import { baseUomLabel } from "@/lib/uom-label" -import { warehousesApi } from "@/lib/api/warehouses" -import { usersApi } from "@/lib/api/users" -import { errorMessage } from "@/lib/error-map" -import { Button } from "@/components/ui/button" -import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table" -import { Customer } from "@/types/customers" -import { ItemListItem, Uom, Warehouse } from "@/types/master-data" -import { ManagedUser } from "@/types/users" -import { SalesSlip } from "@/types/sales" - -export default function SalesSlipPrintPage({ params }: { params: Promise<{ id: string }> }) { - const resolvedParams = use(params) - const slipId = Number(resolvedParams.id) - const [slip, setSlip] = useState(null) - const [customers, setCustomers] = useState([]) - const [items, setItems] = useState([]) - const [uoms, setUoms] = useState([]) - const [warehouses, setWarehouses] = useState([]) - const [users, setUsers] = useState([]) - const [error, setError] = useState(null) - - useEffect(() => { - if (!Number.isFinite(slipId)) { - setError(`Invalid slip id '${resolvedParams.id}'.`) - return - } - - Promise.all([ - customersApi.list({ pageSize: 200 }), - itemsApi.list({ pageSize: 200 }), - uomsApi.list({ pageSize: 200 }), - warehousesApi.list({ pageSize: 200 }), - usersApi.list({ pageSize: 200 }), - salesApi.getSlip(slipId), - ]) - .then(([cust, itemRes, uomRes, whRes, userRes, doc]) => { - setCustomers(cust.items) - setItems(itemRes.items) - setUoms(uomRes.items) - setWarehouses(whRes.items) - setUsers(userRes.items) - setSlip(doc.data) - }) - .catch((err) => setError(errorMessage(err))) - }, [resolvedParams.id, slipId]) - - const subtotal = useMemo(() => slip?.totals.subtotal ?? 0, [slip]) - - if (error && !slip) { - return
    {error}
    - } - - if (!slip) { - return
    {error ?? "Slip print data is loading or unavailable."}
    - } - - const customer = customers.find((c) => c.customerId === slip.customerId) - const warehouse = warehouses.find((w) => w.warehouseId === slip.warehouseId) - - return ( -
    -
    - -
    - -
    -
    Sales Slip
    -

    {slip.slipNo}

    -
    Status: {slip.status} · Date: {new Date(slip.slipDate).toLocaleDateString()}
    -
    - -
    -
    -
    Customer
    -
    {slip.customerSnapshotName}
    -
    Customer ID: {slip.customerId}
    - {customer?.displayName ?
    Customer: {customer.displayName}
    : null} -
    -
    -
    Warehouse
    -
    {warehouse?.name ?? `#${slip.warehouseId}`}
    -
    Code: {warehouse?.code ?? slip.warehouseId}
    -
    -
    -
    Totals
    -
    -
    Subtotal
    {slip.totals.subtotal.toFixed(2)}
    -
    Discount
    {slip.totals.discountTotal.toFixed(2)}
    -
    Free qty
    {slip.totals.freeQtyTotal.toFixed(2)}
    -
    Net total
    {slip.totals.grandTotal.toFixed(2)}
    -
    -
    -
    - -
    - - - - Item - UOM - Qty - Free - Unit price - Line total - - - - {slip.lines.map((line) => ( - - -
    {line.description}
    -
    SKU {items.find((i) => i.itemId === line.itemId)?.sku ?? `#${line.itemId}`}
    -
    - {baseUomLabel(items, uoms, line.itemId)} - {line.qty.toFixed(0)} - {line.freeQty > 0 ? line.freeQty.toFixed(0) : "—"} - {line.unitPrice.toFixed(2)} - {line.lineTotal.toFixed(2)} -
    - ))} -
    -
    -
    - -
    -
    Cashier
    -
    {users.find((u) => u.userId === slip.cashierUserId)?.displayName ?? `#${slip.cashierUserId}`}
    -
    Subtotal: {subtotal.toFixed(2)}
    -
    -
    - ) -} diff --git a/Frontend/erp-system/app/print/sales/slips/page.tsx b/Frontend/erp-system/app/print/sales/slips/page.tsx deleted file mode 100644 index 3a2ac7d..0000000 --- a/Frontend/erp-system/app/print/sales/slips/page.tsx +++ /dev/null @@ -1,114 +0,0 @@ -"use client" - -import { Suspense, useEffect, useState } from "react" -import { useSearchParams } from "next/navigation" -import { Printer } from "lucide-react" - -import { salesApi } from "@/lib/api/sales" -import { errorMessage } from "@/lib/error-map" -import { Button } from "@/components/ui/button" -import { Badge } from "@/components/ui/badge" -import { Skeleton } from "@/components/ui/skeleton" -import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table" -import { SalesSlipSummary } from "@/types/sales" - -function statusClass(status: SalesSlipSummary["status"]) { - switch (status) { - case "Draft": - return "border-amber-200 bg-amber-50 text-amber-800" - case "Posted": - return "border-emerald-200 bg-emerald-50 text-emerald-800" - case "Cancelled": - return "border-rose-200 bg-rose-50 text-rose-800" - } -} - -function SalesSlipBatchPrintContent() { - const searchParams = useSearchParams() - const status = searchParams.get("status") ?? "All" - const query = searchParams.get("q") ?? "" - const [rows, setRows] = useState(null) - const [error, setError] = useState(null) - - useEffect(() => { - salesApi - .listSlips({ page: 1, pageSize: 200 }) - .then((res) => setRows(res.items)) - .catch((err) => setError(errorMessage(err))) - }, []) - - const visibleRows = rows?.filter((row) => { - const matchesStatus = status === "All" || row.status === status - const matchesQuery = - `${row.slipNo} ${row.customerSnapshotName}`.toLowerCase().includes(query.toLowerCase()) - return matchesStatus && matchesQuery - }) - - if (error && !rows) { - return
    {error}
    - } - - if (!rows) { - return
    {error ?? "Slip batch print data is loading or unavailable."}
    - } - - return ( -
    -
    - -
    - -
    -
    Sales Slips
    -

    Slip Batch Print

    -

    Printed register snapshot of current slips.

    -
    - -
    - - - - Slip - Customer - Date - Status - Lines - Gross - Discount - Net - - - - {visibleRows?.map((row) => ( - - {row.slipNo} - {row.customerSnapshotName} - {new Date(row.createdAt).toLocaleDateString()} - - - {row.status} - - - {row.totals.freeQtyTotal.toFixed(2)} - {row.totals.subtotal.toFixed(2)} - -{row.totals.discountTotal.toFixed(2)} - {row.totals.grandTotal.toFixed(2)} - - ))} - -
    -
    -
    - ) -} - -export default function SalesSlipBatchPrintPage() { - return ( - }> - - - ) -} diff --git a/Frontend/erp-system/components/Layouts/AppSidebar.tsx b/Frontend/erp-system/components/Layouts/AppSidebar.tsx index 9237fba..751590f 100644 --- a/Frontend/erp-system/components/Layouts/AppSidebar.tsx +++ b/Frontend/erp-system/components/Layouts/AppSidebar.tsx @@ -1,6 +1,7 @@ "use client" import { useEffect, useState } from "react" +import Image from "next/image" import Link from "next/link" import { usePathname } from "next/navigation" import { @@ -114,15 +115,14 @@ const navItems: { icon: ReceiptText, chevron: true, children: [ - { title: "Invoices", code: "sales.invoices", href: "/dashboard/sales/invoices", icon: FileText }, - { title: "Slips", code: "sales.slips", href: "/dashboard/sales/slips", icon: ShoppingCart }, + { title: "Sales", code: "sales.invoices", href: "/dashboard/sales/invoices", icon: FileText }, { title: "Bundle Sales", code: "sales.bundle-sales", href: "/dashboard/sales/bundles", icon: Boxes }, { title: "Free Issues", code: "sales.free-issues", href: "/dashboard/sales/free-issues", icon: PackageX }, // { title: "Reports", code: "sales.reports", href: "/dashboard/sales/reports", icon: FileBarChart }, ], }, { title: "Sales Returns", code: "sales-returns", href: "/dashboard/sales/sales-returns", icon: Undo2 }, - { title: "Receiving", code: "receiving", href: "/dashboard/receiving/grn", icon: PackageCheck, chevron: true }, + { title: "GRN", code: "receiving", href: "/dashboard/receiving/grn", icon: PackageCheck, chevron: true }, { title: "Stock", code: "stock", @@ -144,7 +144,6 @@ const navItems: { ], }, { title: "Warehouses", code: "warehouses", href: "/dashboard/warehouse", icon: Building2, chevron: true }, - { title: "Orders", code: "orders", href: "/dashboard/orders", icon: ShoppingCart, chevron: true }, { title: "Production", code: "production", @@ -275,13 +274,14 @@ function SidebarContent({ )} > -
    - - - -
    - {!iconOnly && ( - Hexa ERP + {iconOnly ? ( +
    + + + +
    + ) : ( + Hexa ERP )} @@ -407,13 +407,11 @@ function SidebarContent({ })}
-
-
- - - -
-
+ {!iconOnly && ( +

+ © {new Date().getFullYear()} Hexa ERP +

+ )} ) } diff --git a/Frontend/erp-system/components/Layouts/Breadcrumbs.tsx b/Frontend/erp-system/components/Layouts/Breadcrumbs.tsx index a5c4ad9..70deacf 100644 --- a/Frontend/erp-system/components/Layouts/Breadcrumbs.tsx +++ b/Frontend/erp-system/components/Layouts/Breadcrumbs.tsx @@ -38,6 +38,7 @@ const SEGMENT_LABELS: Record = { production: "Production", templates: "Templates", runs: "Runs", + invoices: "Sales", } function labelFor(segment: string): string { diff --git a/Frontend/erp-system/components/Layouts/Header.tsx b/Frontend/erp-system/components/Layouts/Header.tsx index 6b5aba8..b3a1329 100644 --- a/Frontend/erp-system/components/Layouts/Header.tsx +++ b/Frontend/erp-system/components/Layouts/Header.tsx @@ -79,6 +79,9 @@ const STOCK_TITLES: Record = { } function titleFromPath(pathname: string) { + if (pathname === "/dashboard/sales/invoices") return "Sales" + if (pathname === "/dashboard/sales/invoices/new") return "New Sale" + if (pathname === "/dashboard/receiving/grn") return "Goods Receipt Notes" if (pathname === "/dashboard/receiving/grn/new") return "Create Goods Receipt Note" if (/^\/dashboard\/receiving\/grn\/[^/]+$/.test(pathname)) return "Goods Receipt Note" diff --git a/Frontend/erp-system/components/sales/FreeIssueSuggestions.tsx b/Frontend/erp-system/components/sales/FreeIssueSuggestions.tsx deleted file mode 100644 index 2813912..0000000 --- a/Frontend/erp-system/components/sales/FreeIssueSuggestions.tsx +++ /dev/null @@ -1,42 +0,0 @@ -"use client" - -import Link from "next/link" - -import { buttonVariants } from "@/components/ui/button" -import { cn } from "@/lib/utils" -import { SalesSlipSummary } from "@/types/sales" - -export function FreeIssueSuggestions({ rows }: { rows: SalesSlipSummary[] }) { - if (rows.length === 0) return null - - return ( -
-
-
-

Free issue suggestions

-

Recent free-issue slips created from the free-issues page.

-
- - Open free issues - -
- -
- {rows.map((row) => ( - -
-
{row.slipNo}
-
{row.status}
-
-
{row.customerSnapshotName}
-
Free qty total: {row.totals.freeQtyTotal.toFixed(2)}
- - ))} -
-
- ) -} \ No newline at end of file diff --git a/Frontend/erp-system/public/images/logo (2).png b/Frontend/erp-system/public/images/hexa-erp-logo.png similarity index 100% rename from Frontend/erp-system/public/images/logo (2).png rename to Frontend/erp-system/public/images/hexa-erp-logo.png