product and sales fixers
This commit is contained in:
@@ -43,14 +43,12 @@ import {
|
||||
PieChart,
|
||||
Receipt,
|
||||
ReceiptText,
|
||||
Ruler,
|
||||
Scale,
|
||||
ScrollText,
|
||||
Settings,
|
||||
ShieldCheck,
|
||||
ShoppingCart,
|
||||
SlidersHorizontal,
|
||||
Tag,
|
||||
Truck,
|
||||
Undo2,
|
||||
Users,
|
||||
@@ -73,11 +71,7 @@ const navItems: {
|
||||
/** Where clicking the row actually navigates, if different from `href`. `href` itself
|
||||
* stays the section prefix used to decide whether this row is "active". */
|
||||
landingHref?: string
|
||||
/** This item's own href has no page of its own (no landingHref either) — clicking the
|
||||
* row should only expand/collapse its children, never navigate. */
|
||||
expandOnly?: boolean
|
||||
icon: LucideIcon
|
||||
chevron?: boolean
|
||||
children?: {
|
||||
title: string
|
||||
code: string
|
||||
@@ -94,12 +88,16 @@ const navItems: {
|
||||
code: "products",
|
||||
href: "/dashboard/products",
|
||||
icon: Package,
|
||||
chevron: true,
|
||||
children: [
|
||||
{ title: "Item", code: "products.item", href: "/dashboard/products", icon: Boxes },
|
||||
{ title: "Category", code: "products.category", href: "/dashboard/products/categories", icon: ListTree },
|
||||
{ title: "Brand", code: "products.brand", href: "/dashboard/products/brands", icon: Tag },
|
||||
{ title: "UOM", code: "products.uom", href: "/dashboard/products/uoms", icon: Ruler },
|
||||
// Category, Brand and UOM share one tabbed page — any of the three permissions shows it.
|
||||
{
|
||||
title: "Category/Brand/UOM",
|
||||
code: "products.category",
|
||||
href: "/dashboard/products/categories",
|
||||
icon: ListTree,
|
||||
altCodes: ["products.brand", "products.uom"],
|
||||
},
|
||||
{ title: "Tax Rates", code: "products.tax-rates", href: "/dashboard/products/tax-rates", icon: Percent },
|
||||
],
|
||||
},
|
||||
@@ -111,7 +109,6 @@ const navItems: {
|
||||
// has nothing on it (its card grid was removed once the sidebar grew these sub-items).
|
||||
landingHref: "/dashboard/procurement/purchase-orders",
|
||||
icon: ClipboardList,
|
||||
chevron: true,
|
||||
children: [
|
||||
{ title: "Purchase Orders", code: "procurement.purchase-orders", href: "/dashboard/procurement/purchase-orders", icon: ShoppingCart },
|
||||
{ title: "Purchase Returns", code: "procurement.purchase-returns", href: "/dashboard/procurement/purchase-returns", icon: PackageX },
|
||||
@@ -124,9 +121,11 @@ const navItems: {
|
||||
href: "/dashboard/sales",
|
||||
landingHref: "/dashboard/sales/invoices",
|
||||
icon: ReceiptText,
|
||||
chevron: true,
|
||||
children: [
|
||||
{ title: "Sales", code: "sales.invoices", href: "/dashboard/sales/invoices", icon: FileText },
|
||||
// Shortcut straight to the New Sale form — reuses the sales.invoices permission code (same
|
||||
// access as the Sales list), so it needs no seeded SubNavItem of its own.
|
||||
{ title: "Create Sale", code: "sales.invoices", href: "/dashboard/sales/invoices/new", icon: ShoppingCart },
|
||||
// Reuses the sales.invoices permission code — customers aren't yet their own
|
||||
// seeded NavItem/Permission on the backend, so this rides along with Sales access.
|
||||
{ title: "Customers", code: "sales.invoices", href: "/dashboard/customers", icon: Users },
|
||||
@@ -137,7 +136,7 @@ const navItems: {
|
||||
],
|
||||
},
|
||||
{ title: "Sales Returns", code: "sales-returns", href: "/dashboard/sales/sales-returns", icon: Undo2 },
|
||||
{ title: "GRN", code: "receiving", href: "/dashboard/receiving/grn", icon: PackageCheck, chevron: true },
|
||||
{ title: "GRN", code: "receiving", href: "/dashboard/receiving/grn", icon: PackageCheck },
|
||||
{
|
||||
title: "Stock",
|
||||
code: "stock",
|
||||
@@ -146,7 +145,6 @@ const navItems: {
|
||||
// it (its card grid was removed once the sidebar grew these sub-items), same as Procurement.
|
||||
landingHref: "/dashboard/stock/ledger",
|
||||
icon: Warehouse,
|
||||
chevron: true,
|
||||
children: [
|
||||
{ title: "Stock Ledger", code: "stock.ledger", href: "/dashboard/stock/ledger", icon: ScrollText },
|
||||
{ title: "Stock Enquiry", code: "stock.enquiry", href: "/dashboard/stock/enquiry", icon: PackageSearch },
|
||||
@@ -158,14 +156,13 @@ const navItems: {
|
||||
{ title: "Wastage", code: "stock.wastage", href: "/dashboard/stock/wastage", icon: AlertOctagon },
|
||||
],
|
||||
},
|
||||
{ title: "Warehouses", code: "warehouses", href: "/dashboard/warehouse", icon: Building2, chevron: true },
|
||||
{ title: "Warehouses", code: "warehouses", href: "/dashboard/warehouse", icon: Building2 },
|
||||
{
|
||||
title: "Production",
|
||||
code: "production",
|
||||
href: "/dashboard/production",
|
||||
landingHref: "/dashboard/production/runs",
|
||||
icon: Factory,
|
||||
chevron: true,
|
||||
children: [
|
||||
{ title: "Templates", code: "production.templates", href: "/dashboard/production/templates", icon: LayoutTemplate },
|
||||
{ title: "Runs", code: "production.runs", href: "/dashboard/production/runs", icon: PlayCircle },
|
||||
@@ -178,7 +175,6 @@ const navItems: {
|
||||
href: "/dashboard/hrm",
|
||||
landingHref: "/dashboard/hrm/employees",
|
||||
icon: IdCard,
|
||||
chevron: true,
|
||||
children: [
|
||||
{ title: "Employees", code: "hrm.employees", href: "/dashboard/hrm/employees", icon: Users },
|
||||
{ title: "Attendance", code: "hrm.attendance", href: "/dashboard/hrm/attendance", icon: CalendarCheck },
|
||||
@@ -193,7 +189,6 @@ const navItems: {
|
||||
code: "ledgers",
|
||||
href: "/dashboard/ledgers",
|
||||
icon: Landmark,
|
||||
chevron: true,
|
||||
children: [
|
||||
{ title: "Trial Balance", code: "ledgers.trial-balance", href: "/dashboard/ledgers/trial-balance", icon: Scale },
|
||||
{ title: "Balance Sheet", code: "ledgers.balance-sheet", href: "/dashboard/ledgers/balance-sheet", icon: Landmark },
|
||||
@@ -209,7 +204,6 @@ const navItems: {
|
||||
code: "accounts",
|
||||
href: "/dashboard/accounts",
|
||||
icon: CreditCard,
|
||||
chevron: true,
|
||||
children: [
|
||||
{ title: "Cash / Bank Accounts", code: "accounts.bank-accounts", href: "/dashboard/accounts/bank-accounts", icon: Wallet },
|
||||
{ title: "Cheque Books", code: "accounts.cheque-books", href: "/dashboard/accounts/cheque-books", icon: BookText },
|
||||
@@ -220,9 +214,7 @@ const navItems: {
|
||||
title: "Settings",
|
||||
code: "settings",
|
||||
href: "/dashboard/settings",
|
||||
expandOnly: true,
|
||||
icon: Settings,
|
||||
chevron: true,
|
||||
children: [
|
||||
{ title: "Branches", code: "settings.branches", href: "/dashboard/settings/branches", icon: MapPin },
|
||||
{ title: "Roles", code: "settings.roles", href: "/dashboard/settings/roles", icon: ShieldCheck },
|
||||
@@ -329,49 +321,46 @@ function SidebarContent({
|
||||
isActive ? "bg-primary/10" : "hover:bg-muted"
|
||||
)}
|
||||
>
|
||||
<Link
|
||||
href={item.landingHref ?? item.href}
|
||||
title={iconOnly ? item.title : undefined}
|
||||
onClick={() => {
|
||||
onClose?.()
|
||||
if (hasChildren) setExpanded((prev) => (prev[item.code] ? prev : { ...prev, [item.code]: true }))
|
||||
}}
|
||||
className={cn(
|
||||
"flex flex-1 items-center gap-3 rounded-2xl px-4 py-1.5 text-base font-normal",
|
||||
iconOnly && "justify-center px-0",
|
||||
isActive ? "text-primary" : "text-foreground"
|
||||
)}
|
||||
>
|
||||
<item.icon
|
||||
className={cn("size-3.5 shrink-0", isActive ? "text-primary" : "text-muted-foreground")}
|
||||
/>
|
||||
{!iconOnly && (
|
||||
<>
|
||||
<span className="flex-1">{item.title}</span>
|
||||
{item.chevron && !hasChildren && (
|
||||
<ChevronRight
|
||||
className={cn("size-4 shrink-0", isActive ? "text-primary/70" : "text-muted-foreground")}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Link>
|
||||
|
||||
{hasChildren && (
|
||||
{hasChildren ? (
|
||||
// Parents with sub-items only expand/collapse on click — navigation
|
||||
// happens through the sub-items themselves.
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => toggleExpand(item.code)}
|
||||
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-foreground/10",
|
||||
isActive ? "text-primary" : "text-muted-foreground"
|
||||
"flex flex-1 items-center gap-3 rounded-2xl px-4 py-1.5 text-left text-base font-normal",
|
||||
isActive ? "text-primary" : "text-foreground"
|
||||
)}
|
||||
>
|
||||
<item.icon
|
||||
className={cn("size-3.5 shrink-0", isActive ? "text-primary" : "text-muted-foreground")}
|
||||
/>
|
||||
<span className="flex-1">{item.title}</span>
|
||||
<ChevronRight
|
||||
className={cn("size-4 transition-transform duration-300 ease-in-out", isOpen && "rotate-90")}
|
||||
className={cn(
|
||||
"size-4 shrink-0 transition-transform duration-300 ease-in-out",
|
||||
isActive ? "text-primary" : "text-muted-foreground",
|
||||
isOpen && "rotate-90"
|
||||
)}
|
||||
/>
|
||||
</button>
|
||||
) : (
|
||||
<Link
|
||||
href={item.landingHref ?? item.href}
|
||||
title={iconOnly ? item.title : undefined}
|
||||
onClick={onClose}
|
||||
className={cn(
|
||||
"flex flex-1 items-center gap-3 rounded-2xl px-4 py-1.5 text-base font-normal",
|
||||
iconOnly && "justify-center px-0",
|
||||
isActive ? "text-primary" : "text-foreground"
|
||||
)}
|
||||
>
|
||||
<item.icon
|
||||
className={cn("size-3.5 shrink-0", isActive ? "text-primary" : "text-muted-foreground")}
|
||||
/>
|
||||
{!iconOnly && <span className="flex-1">{item.title}</span>}
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -132,10 +132,10 @@ function titleFromPath(pathname: string) {
|
||||
|
||||
if (pathname === "/dashboard/products") return "Items"
|
||||
if (pathname === "/dashboard/products/new") return "New Item"
|
||||
if (pathname === "/dashboard/products/categories") return "Categories"
|
||||
if (pathname === "/dashboard/products/categories") return "Category/Brand/UOM"
|
||||
if (pathname.startsWith("/dashboard/products/categories/")) return "Subcategories"
|
||||
if (pathname === "/dashboard/products/brands") return "Brand/UOM"
|
||||
if (pathname === "/dashboard/products/item-types") return "Item Types"
|
||||
if (pathname === "/dashboard/products/item-types") return "Item Variants"
|
||||
if (pathname === "/dashboard/products/warranty-periods") return "Warranty Periods"
|
||||
if (pathname === "/dashboard/products/settings") return "Product Configuration"
|
||||
if (pathname === "/dashboard/settings/notification-config") return "Notification Config"
|
||||
if (pathname === "/dashboard/settings/company-profile") return "Company Profile"
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
"use client"
|
||||
|
||||
import { useEffect, useRef, useState } from "react"
|
||||
import { usePathname, useSearchParams } from "next/navigation"
|
||||
|
||||
/**
|
||||
* Thin top-of-page progress bar for client-side navigations.
|
||||
*
|
||||
* The App Router has no "navigation started" event, so the start is detected from clicks on
|
||||
* internal links (capture phase, before next/link handles them) and back/forward. The route
|
||||
* committing — pathname or query changing — finishes it. Programmatic router.push() isn't
|
||||
* seen at start; app/dashboard/loading.tsx covers those with an in-content spinner.
|
||||
*/
|
||||
export function RouteProgress() {
|
||||
const pathname = usePathname()
|
||||
const searchParams = useSearchParams()
|
||||
const [progress, setProgress] = useState(0)
|
||||
const [visible, setVisible] = useState(false)
|
||||
// Set by the mount effect; lets the route-change effect end the bar.
|
||||
const finishRef = useRef<() => void>(() => {})
|
||||
|
||||
useEffect(() => {
|
||||
let active = false
|
||||
let trickle: ReturnType<typeof setInterval> | null = null
|
||||
let safety: ReturnType<typeof setTimeout> | null = null
|
||||
let hide: ReturnType<typeof setTimeout> | null = null
|
||||
|
||||
function clearTimers() {
|
||||
if (trickle) clearInterval(trickle)
|
||||
if (safety) clearTimeout(safety)
|
||||
if (hide) clearTimeout(hide)
|
||||
trickle = safety = hide = null
|
||||
}
|
||||
|
||||
function start() {
|
||||
if (active) return
|
||||
active = true
|
||||
clearTimers()
|
||||
setVisible(true)
|
||||
setProgress(8)
|
||||
// Ease toward 90% — never reaches the end until the route actually commits.
|
||||
trickle = setInterval(() => setProgress((p) => (p < 90 ? p + (90 - p) * 0.1 : p)), 200)
|
||||
// Never leave a bar hanging if a navigation is cancelled or fails silently.
|
||||
safety = setTimeout(finish, 15000)
|
||||
}
|
||||
|
||||
function finish() {
|
||||
if (!active) return
|
||||
active = false
|
||||
clearTimers()
|
||||
setProgress(100)
|
||||
hide = setTimeout(() => {
|
||||
setVisible(false)
|
||||
setProgress(0)
|
||||
}, 250)
|
||||
}
|
||||
finishRef.current = finish
|
||||
|
||||
function onClick(e: MouseEvent) {
|
||||
if (e.defaultPrevented || e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return
|
||||
const anchor = (e.target as Element | null)?.closest?.("a")
|
||||
if (!anchor || !anchor.href) return
|
||||
if (anchor.target && anchor.target !== "_self") return
|
||||
if (anchor.hasAttribute("download")) return
|
||||
|
||||
const next = new URL(anchor.href, window.location.href)
|
||||
if (next.origin !== window.location.origin) return
|
||||
// API downloads (e.g. document links) aren't page navigations.
|
||||
if (next.pathname.startsWith("/api/")) return
|
||||
// Same page (or hash-only jump) — nothing will commit, so don't start.
|
||||
if (next.pathname === window.location.pathname && next.search === window.location.search) return
|
||||
start()
|
||||
}
|
||||
|
||||
document.addEventListener("click", onClick, true)
|
||||
window.addEventListener("popstate", start)
|
||||
return () => {
|
||||
document.removeEventListener("click", onClick, true)
|
||||
window.removeEventListener("popstate", start)
|
||||
clearTimers()
|
||||
}
|
||||
}, [])
|
||||
|
||||
// Route committed ⇒ done. Keyed on the full URL so query-only navigations count too.
|
||||
const url = `${pathname}?${searchParams.toString()}`
|
||||
useEffect(() => {
|
||||
finishRef.current()
|
||||
}, [url])
|
||||
|
||||
return (
|
||||
<div
|
||||
aria-hidden
|
||||
className="pointer-events-none fixed inset-x-0 top-0 z-100 h-0.5 print:hidden"
|
||||
style={{ opacity: visible ? 1 : 0, transition: "opacity 250ms ease" }}
|
||||
>
|
||||
<div
|
||||
className="h-full bg-primary shadow-[0_0_8px_var(--color-primary)]"
|
||||
style={{ width: `${progress}%`, transition: "width 200ms ease-out" }}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user