From 7366ca93c07adb7e43da4703e69bb20abe0d2a6c Mon Sep 17 00:00:00 2001 From: Sasanka20 Date: Thu, 23 Jul 2026 12:53:26 +0530 Subject: [PATCH] feat: implement theme toggle and enhance UI components --- Frontend/erp-system/app/dashboard/layout.tsx | 2 +- .../app/dashboard/procurement/page.tsx | 52 ----------------- .../procurement/purchase-orders/new/page.tsx | 33 +++++++++-- .../app/dashboard/products/[id]/page.tsx | 6 +- .../app/dashboard/receiving/grn/new/page.tsx | 19 ++++--- .../app/dashboard/warehouse/page.tsx | 42 +++++++++----- Frontend/erp-system/app/layout.tsx | 6 +- .../components/Layouts/AppSidebar.tsx | 57 +++++++++++-------- .../erp-system/components/Layouts/Header.tsx | 37 ++++++------ .../erp-system/components/theme-toggle.tsx | 38 +++++++++++++ Frontend/erp-system/package.json | 1 + 11 files changed, 173 insertions(+), 120 deletions(-) create mode 100644 Frontend/erp-system/components/theme-toggle.tsx diff --git a/Frontend/erp-system/app/dashboard/layout.tsx b/Frontend/erp-system/app/dashboard/layout.tsx index 55e4289..17b3edc 100644 --- a/Frontend/erp-system/app/dashboard/layout.tsx +++ b/Frontend/erp-system/app/dashboard/layout.tsx @@ -18,7 +18,7 @@ export default function DashboardLayout({
-
+
{children}
diff --git a/Frontend/erp-system/app/dashboard/procurement/page.tsx b/Frontend/erp-system/app/dashboard/procurement/page.tsx index cc3a0c4..bb36515 100644 --- a/Frontend/erp-system/app/dashboard/procurement/page.tsx +++ b/Frontend/erp-system/app/dashboard/procurement/page.tsx @@ -1,35 +1,3 @@ -import Link from "next/link" -import { ClipboardList, FileText, PackageX, ShoppingCart, type LucideIcon } from "lucide-react" - -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" - -const areas: { title: string; description: string; href: string; icon: LucideIcon }[] = [ - { - title: "Requisitions", - description: "Raise a purchase requisition and submit it into procurement.", - href: "/dashboard/procurement/requisitions", - icon: ClipboardList, - }, - { - title: "RFQs", - description: "Request quotations from vendors, record pricing, and compare side by side.", - href: "/dashboard/procurement/rfqs", - icon: FileText, - }, - { - title: "Purchase Orders", - description: "Save as draft (editable/deletable) or submit to lock; cancel an issued PO before receipt.", - href: "/dashboard/procurement/purchase-orders", - icon: ShoppingCart, - }, - { - title: "Purchase Returns", - description: "Return received goods to a vendor, referencing the original GRN line.", - href: "/dashboard/procurement/purchase-returns", - icon: PackageX, - }, -] - export default function ProcurementHubPage() { return (
@@ -39,26 +7,6 @@ export default function ProcurementHubPage() { Requisition → RFQ (optional) → Purchase Order → Purchase Return (FR-PROC-01..09).

- -
- {areas.map((area) => ( - - - -
-
- -
- {area.title} -
-
- -

{area.description}

-
-
- - ))} -
) } diff --git a/Frontend/erp-system/app/dashboard/procurement/purchase-orders/new/page.tsx b/Frontend/erp-system/app/dashboard/procurement/purchase-orders/new/page.tsx index 411799d..f956a5d 100644 --- a/Frontend/erp-system/app/dashboard/procurement/purchase-orders/new/page.tsx +++ b/Frontend/erp-system/app/dashboard/procurement/purchase-orders/new/page.tsx @@ -238,17 +238,40 @@ function NewPurchaseOrderContent() { {!loading && ( <> -
+
- - value={vendorId} onValueChange={setVendorId}> + + + value={vendorId} + onValueChange={setVendorId} + items={(vendors ?? []).map((v) => ({ label: v.code, value: v.vendorId }))} + > - + {(vendors ?? []).map((v) => ( - {v.code} — {v.name} + {v.code} + + ))} + + +
+
+ + + value={vendorId} + onValueChange={setVendorId} + items={(vendors ?? []).map((v) => ({ label: v.name, value: v.vendorId }))} + > + + + + + {(vendors ?? []).map((v) => ( + + {v.name} ))} diff --git a/Frontend/erp-system/app/dashboard/products/[id]/page.tsx b/Frontend/erp-system/app/dashboard/products/[id]/page.tsx index 63ff5ab..4636569 100644 --- a/Frontend/erp-system/app/dashboard/products/[id]/page.tsx +++ b/Frontend/erp-system/app/dashboard/products/[id]/page.tsx @@ -113,7 +113,11 @@ export default function ItemDetailPage() { try { const result = await itemsApi.update( item.itemId, - { sku, name, description: description || null, categoryId: categoryId as number, subCategoryId, brandId, baseUomId: baseUomId as number, defaultVendorId, stockNature, trackingMode, taxClass: taxClass || null }, + { + sku, name, description: description || null, categoryId: categoryId as number, subCategoryId, brandId, + baseUomId: baseUomId as number, defaultVendorId, stockNature, trackingMode, + taxClass: taxClass || null, + }, etag ) applyItem(result.data) diff --git a/Frontend/erp-system/app/dashboard/receiving/grn/new/page.tsx b/Frontend/erp-system/app/dashboard/receiving/grn/new/page.tsx index 99ce3ef..e8eb828 100644 --- a/Frontend/erp-system/app/dashboard/receiving/grn/new/page.tsx +++ b/Frontend/erp-system/app/dashboard/receiving/grn/new/page.tsx @@ -385,13 +385,18 @@ export default function NewGrnPage() {
-

Lines

- {mode === "direct" && ( - - )} +
+

Lines

+ {mode === "po" && ( +

+ Lines default from the PO's open quantities — add a row for anything received that wasn't ordered. +

+ )} +
+
{poLoading && } diff --git a/Frontend/erp-system/app/dashboard/warehouse/page.tsx b/Frontend/erp-system/app/dashboard/warehouse/page.tsx index 823785e..8a2aad4 100644 --- a/Frontend/erp-system/app/dashboard/warehouse/page.tsx +++ b/Frontend/erp-system/app/dashboard/warehouse/page.tsx @@ -5,7 +5,7 @@ import Link from "next/link" import { ArrowLeft, Plus, Warehouse as WarehouseIcon } from "lucide-react" import { warehousesApi } from "@/lib/api/warehouses" -import { errorMessage, fieldErrors } from "@/lib/error-map" +import { errorMessage } from "@/lib/error-map" import { cn } from "@/lib/utils" import { Bin, Warehouse } from "@/types/master-data" @@ -24,17 +24,36 @@ import { Skeleton } from "@/components/ui/skeleton" import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table" import { toast } from "@/components/ui/toast" +/** First word of the name, uppercased and stripped to alphanumerics — falls back to "WH" + * so an empty/punctuation-only name still yields a usable base. */ +function warehouseCodeBase(name: string): string { + const firstWord = name.trim().split(/\s+/)[0] ?? "" + const cleaned = firstWord.toUpperCase().replace(/[^A-Z0-9]/g, "") + return cleaned.slice(0, 10) || "WH" +} + +/** Appends a numeric suffix until the code doesn't collide with an existing one — the + * backend enforces global uniqueness (409 on conflict) but has no generation of its own. */ +function generateWarehouseCode(name: string, existingCodes: string[]): string { + const base = `WH-${warehouseCodeBase(name)}` + if (!existingCodes.includes(base)) return base + let suffix = 2 + while (existingCodes.includes(`${base}${suffix}`)) suffix += 1 + return `${base}${suffix}` +} + export default function WarehousesPage() { const [warehouses, setWarehouses] = useState(null) const [bins, setBins] = useState(null) const [error, setError] = useState(null) const [open, setOpen] = useState(false) - const [code, setCode] = useState("") const [name, setName] = useState("") const [errors, setErrors] = useState>({}) const [submitting, setSubmitting] = useState(false) + const generatedCode = name.trim() ? generateWarehouseCode(name, (warehouses ?? []).map((w) => w.code)) : "" + function load() { warehousesApi .list() @@ -54,23 +73,21 @@ export default function WarehousesPage() { async function handleCreate() { const nextErrors: Record = {} - if (!code.trim()) nextErrors.code = "Warehouse code is required" if (!name.trim()) nextErrors.name = "Warehouse name is required" setErrors(nextErrors) if (Object.keys(nextErrors).length > 0) return setSubmitting(true) try { - const warehouse = await warehousesApi.create({ code, name }) + const warehouse = await warehousesApi.create({ code: generatedCode, name }) toast.success("Warehouse created", `${warehouse.code} — ${warehouse.name}`) setOpen(false) - setCode("") setName("") setErrors({}) load() } catch (err) { - const fe = fieldErrors(err) - if (fe?.code) setErrors({ code: fe.code }) + // A 409 here means another creation raced ours for the same generated code — the + // proactive de-dupe above only knows about warehouses loaded when the dialog opened. toast.error("Could not create warehouse", errorMessage(err)) } finally { setSubmitting(false) @@ -104,19 +121,18 @@ export default function WarehousesPage() { New warehouse - Create a new warehouse. Bins are added from its detail page. + Create a new warehouse. Its code is generated from the name. Bins are added from its detail page. - - Code - setCode(e.target.value)} placeholder="WH-MAIN" aria-invalid={!!errors.code} /> - - Name setName(e.target.value)} placeholder="Main Warehouse - Negombo" aria-invalid={!!errors.name} /> + + Code (auto-generated) + +
@@ -172,28 +178,28 @@ function SidebarContent({
{!iconOnly && ( <> {item.title} {item.chevron && !hasChildren && ( )} @@ -207,8 +213,8 @@ function SidebarContent({ aria-label={isOpen ? `Collapse ${item.title}` : `Expand ${item.title}`} aria-expanded={isOpen} className={cn( - "mr-2 flex size-7 shrink-0 items-center justify-center rounded-lg transition-colors hover:bg-white/60", - isActive ? "text-indigo-500" : "text-slate-400" + "mr-2 flex size-7 shrink-0 items-center justify-center rounded-lg transition-colors hover:bg-foreground/10", + isActive ? "text-primary" : "text-muted-foreground" )} > {child.title} @@ -268,8 +274,8 @@ function SidebarContent({
-
- +
+
@@ -287,13 +293,18 @@ export function AppSidebar() { // While /auth/me hasn't resolved yet, show nothing rather than briefly // flashing the full menu to a restricted role. Once resolved, a nav item // is visible if its own code is granted, or (for parents) if any child is. + // + // "procurement" is exempted from that check (frontend-only): no role is currently + // seeded with NAV:procurement or its children server-side, which would hide the whole + // section for everyone. Remove this bypass once roles are granted the permission + // properly (Settings → Roles → Sidebar permissions) or a backend seed grants it. const visibleItems = loading ? [] : navItems - .filter((item) => navCodes.includes(item.code) || item.children?.some((c) => navCodes.includes(c.code))) + .filter((item) => item.code === "procurement" || navCodes.includes(item.code) || item.children?.some((c) => navCodes.includes(c.code))) .map((item) => ({ ...item, - children: item.children?.filter((c) => navCodes.includes(c.code)), + children: item.code === "procurement" ? item.children : item.children?.filter((c) => navCodes.includes(c.code)), })) // Close mobile menu on route change @@ -326,7 +337,7 @@ export function AppSidebar() { type="button" onClick={() => setMobileOpen(true)} aria-label="Open menu" - className="fixed top-5 left-5 z-40 flex size-10 items-center justify-center rounded-2xl bg-white shadow-sm ring-1 ring-black/5 text-slate-600 hover:bg-slate-50 lg:hidden" + className="fixed top-5 left-5 z-40 flex size-10 items-center justify-center rounded-2xl bg-card shadow-sm ring-1 ring-foreground/10 text-muted-foreground hover:bg-muted lg:hidden" > diff --git a/Frontend/erp-system/components/Layouts/Header.tsx b/Frontend/erp-system/components/Layouts/Header.tsx index e0f6f5d..8768f7c 100644 --- a/Frontend/erp-system/components/Layouts/Header.tsx +++ b/Frontend/erp-system/components/Layouts/Header.tsx @@ -9,6 +9,7 @@ import { authApi } from "@/lib/api/auth" import { clearStoredUser, displayName, getStoredUser } from "@/lib/auth-session" import { AuthUser } from "@/types/auth" import { cn } from "@/lib/utils" +import { ThemeToggle } from "@/components/theme-toggle" import { Avatar, AvatarFallback } from "@/components/ui/avatar" import { Badge } from "@/components/ui/badge" import { @@ -163,48 +164,50 @@ export function Header() { } return ( -
+
{showBackButton && ( )} -

{title}

+

{title}

+ + } > {unreadCount > 0 && ( - + )} -
-

Notifications

+
+

Notifications

{unreadCount > 0 && ( @@ -221,22 +224,22 @@ export function Header() {
-

{notification.title}

+

{notification.title}

{notification.description}

-

{notification.time}

+

{notification.time}

)) @@ -246,19 +249,19 @@ export function Header() { - + - + {initials(displayName(user))} - + {displayName(user)}
-

{displayName(user)}

+

{displayName(user)}

{user?.email &&

{user.email}

}
diff --git a/Frontend/erp-system/components/theme-toggle.tsx b/Frontend/erp-system/components/theme-toggle.tsx new file mode 100644 index 0000000..9d7fc7e --- /dev/null +++ b/Frontend/erp-system/components/theme-toggle.tsx @@ -0,0 +1,38 @@ +"use client" + +import { useEffect, useState } from "react" +import { Moon, Sun } from "lucide-react" +import { useTheme } from "next-themes" + +import { cn } from "@/lib/utils" + +/** + * Renders an empty slot until mounted: `resolvedTheme` is unknown on the server (and on + * the client's first paint, before next-themes reads localStorage), so rendering an icon + * before that would either be wrong or cause a hydration mismatch. + */ +export function ThemeToggle({ className }: { className?: string }) { + const { resolvedTheme, setTheme } = useTheme() + const [mounted, setMounted] = useState(false) + useEffect(() => setMounted(true), []) + + if (!mounted) { + return