UI changes
This commit is contained in:
@@ -0,0 +1,54 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import Link from 'next/link'
|
||||||
|
import { HugeiconsIcon } from '@hugeicons/react'
|
||||||
|
import { Package01Icon, ArrowRight01Icon } from '@hugeicons/core-free-icons'
|
||||||
|
import { Card, CardHeader, CardTitle, CardDescription, CardAction } from '@/components/ui/card'
|
||||||
|
|
||||||
|
interface ConfigCategory {
|
||||||
|
title: string
|
||||||
|
description: string
|
||||||
|
href: string
|
||||||
|
icon: typeof Package01Icon
|
||||||
|
}
|
||||||
|
|
||||||
|
const CATEGORIES: ConfigCategory[] = [
|
||||||
|
{
|
||||||
|
title: 'Products',
|
||||||
|
description: 'Serial number, batch, expiry and warranty tracking for catalogue items',
|
||||||
|
href: '/configuration/products',
|
||||||
|
icon: Package01Icon,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
export default function configurationPage() {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-6">
|
||||||
|
<div>
|
||||||
|
<h1 className="text-2xl font-semibold text-foreground">Configuration</h1>
|
||||||
|
<p className="text-sm text-muted-foreground mt-0.5">System-wide settings, grouped by area</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||||
|
{CATEGORIES.map((category) => (
|
||||||
|
<Link key={category.href} href={category.href} className="block">
|
||||||
|
<Card className="h-full transition-shadow hover:shadow-md">
|
||||||
|
<CardHeader>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-muted text-muted-foreground">
|
||||||
|
<HugeiconsIcon icon={category.icon} size={18} />
|
||||||
|
</div>
|
||||||
|
<CardTitle>{category.title}</CardTitle>
|
||||||
|
</div>
|
||||||
|
<CardDescription className="mt-3">{category.description}</CardDescription>
|
||||||
|
<CardAction>
|
||||||
|
<HugeiconsIcon icon={ArrowRight01Icon} size={16} className="text-muted-foreground" />
|
||||||
|
</CardAction>
|
||||||
|
</CardHeader>
|
||||||
|
</Card>
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -71,7 +71,7 @@ const STEPS = [
|
|||||||
n: 3,
|
n: 3,
|
||||||
label: 'Items',
|
label: 'Items',
|
||||||
title: 'What should be loaded from Shop POs?',
|
title: 'What should be loaded from Shop POs?',
|
||||||
help: "Expand a shop to tick items and set quantities. If the main warehouse doesn't have enough stock, use \"Take remaining from another warehouse\" to top up the rest.",
|
help: "Expand a shop to tick items and set quantities. If the main warehouse can't cover an item, a top-up row is added automatically — just pick which other warehouse to take the rest from.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
n: 4,
|
n: 4,
|
||||||
@@ -321,10 +321,16 @@ function LoadingSheetForm() {
|
|||||||
let changed = false
|
let changed = false
|
||||||
const next: Record<string, PickedLine> = {}
|
const next: Record<string, PickedLine> = {}
|
||||||
for (const [key, l] of Object.entries(prev)) {
|
for (const [key, l] of Object.entries(prev)) {
|
||||||
const extras = l.extras.filter((e) => e.warehouseId !== warehouseId)
|
let extras = l.extras.filter((e) => e.warehouseId !== warehouseId)
|
||||||
const avail = stockByWh[warehouseId]?.[l.itemId] ?? 0
|
const avail = stockByWh[warehouseId]?.[l.itemId] ?? 0
|
||||||
const quantity = Math.min(l.quantity, avail)
|
const quantity = Math.min(l.quantity, avail)
|
||||||
const checked = l.checked && (quantity > 0 || extras.length > 0)
|
const checked = l.checked && (quantity > 0 || extras.length > 0)
|
||||||
|
// Pre-add an empty top-up row if the new main warehouse leaves this line short
|
||||||
|
// and one isn't already pending, mirroring toggleLine's auto-suggestion.
|
||||||
|
const shortBy = l.remainingQty - quantity - extras.reduce((sum, e) => sum + (Number(e.quantity) || 0), 0)
|
||||||
|
if (checked && shortBy > 0 && !extras.some((e) => !e.warehouseId)) {
|
||||||
|
extras = [...extras, { warehouseId: '', quantity: 0 }]
|
||||||
|
}
|
||||||
if (extras.length !== l.extras.length || quantity !== l.quantity || checked !== l.checked) changed = true
|
if (extras.length !== l.extras.length || quantity !== l.quantity || checked !== l.checked) changed = true
|
||||||
next[key] = { ...l, extras, quantity, checked }
|
next[key] = { ...l, extras, quantity, checked }
|
||||||
}
|
}
|
||||||
@@ -452,7 +458,12 @@ function LoadingSheetForm() {
|
|||||||
const avail = availIn(warehouseId, line.itemId)
|
const avail = availIn(warehouseId, line.itemId)
|
||||||
if (!stockLoaded || avail <= 0) return
|
if (!stockLoaded || avail <= 0) return
|
||||||
const quantity = Math.max(0, Math.min(line.remainingQty - extrasTotal(line), avail))
|
const quantity = Math.max(0, Math.min(line.remainingQty - extrasTotal(line), avail))
|
||||||
setLines((prev) => ({ ...prev, [line.shopPOItemId]: { ...line, checked, quantity } }))
|
// The main warehouse can't cover the full remaining quantity, so a top-up row is
|
||||||
|
// pre-added (still editable/removable) instead of making the user click "Take
|
||||||
|
// remaining from another warehouse" themselves.
|
||||||
|
const shortBy = line.remainingQty - quantity - extrasTotal(line)
|
||||||
|
const extras = shortBy > 0 ? [...line.extras, { warehouseId: '', quantity: 0 }] : line.extras
|
||||||
|
setLines((prev) => ({ ...prev, [line.shopPOItemId]: { ...line, checked, quantity, extras } }))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
setLines((prev) => ({ ...prev, [line.shopPOItemId]: { ...line, checked } }))
|
setLines((prev) => ({ ...prev, [line.shopPOItemId]: { ...line, checked } }))
|
||||||
@@ -912,10 +923,18 @@ function LoadingSheetForm() {
|
|||||||
<tr>
|
<tr>
|
||||||
<td className="px-3 pb-2"></td>
|
<td className="px-3 pb-2"></td>
|
||||||
<td className="px-3 pb-2" colSpan={7}>
|
<td className="px-3 pb-2" colSpan={7}>
|
||||||
<span className="mr-3 text-xs text-muted-foreground">{shortBy} will stay open on the PO for another sheet</span>
|
{line.extras.some((e) => !e.warehouseId) ? (
|
||||||
<Button type="button" variant="outline" size="sm" onClick={() => addExtra(line)}>
|
<span className="text-xs text-amber-600">
|
||||||
Take remaining from another warehouse
|
{shortBy} short — pick a warehouse above to top it up, or it stays open on the PO for another sheet.
|
||||||
</Button>
|
</span>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<span className="mr-3 text-xs text-muted-foreground">{shortBy} will stay open on the PO for another sheet</span>
|
||||||
|
<Button type="button" variant="outline" size="sm" onClick={() => addExtra(line)}>
|
||||||
|
Take remaining from another warehouse
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ import {
|
|||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from '@/components/ui/tooltip'
|
} from '@/components/ui/tooltip'
|
||||||
import { useAuth, type Role } from '@/contexts/auth-context'
|
import { useAuth, type Role } from '@/contexts/auth-context'
|
||||||
import { DASHBOARD_ITEM, NAV_GROUPS, type NavGroup, type NavItem } from '@/config/nav'
|
import { DASHBOARD_ITEM, CONFIGURATION_ITEM, NAV_GROUPS, type NavGroup, type NavItem } from '@/config/nav'
|
||||||
|
|
||||||
function canSee(item: NavItem, permissions: string[]): boolean {
|
function canSee(item: NavItem, permissions: string[]): boolean {
|
||||||
if (!item.permission) return true
|
if (!item.permission) return true
|
||||||
@@ -54,6 +54,8 @@ export function AppSidebar() {
|
|||||||
const pathname = usePathname()
|
const pathname = usePathname()
|
||||||
|
|
||||||
const dashboardActive = pathname === DASHBOARD_ITEM.href
|
const dashboardActive = pathname === DASHBOARD_ITEM.href
|
||||||
|
const configurationActive =
|
||||||
|
pathname === CONFIGURATION_ITEM.href || pathname.startsWith(CONFIGURATION_ITEM.href + '/')
|
||||||
const [openGroups, setOpenGroups] = useState<Record<string, boolean>>({})
|
const [openGroups, setOpenGroups] = useState<Record<string, boolean>>({})
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -182,6 +184,22 @@ export function AppSidebar() {
|
|||||||
</Collapsible>
|
</Collapsible>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
|
|
||||||
|
{/* Configuration — standalone, no sub-nav */}
|
||||||
|
<SidebarGroup className="py-1">
|
||||||
|
<SidebarMenu>
|
||||||
|
<SidebarMenuItem>
|
||||||
|
<SidebarMenuButton
|
||||||
|
render={<Link href={CONFIGURATION_ITEM.href} />}
|
||||||
|
isActive={configurationActive}
|
||||||
|
tooltip={CONFIGURATION_ITEM.title}
|
||||||
|
>
|
||||||
|
<HugeiconsIcon icon={CONFIGURATION_ITEM.icon} size={18} />
|
||||||
|
<span>{CONFIGURATION_ITEM.title}</span>
|
||||||
|
</SidebarMenuButton>
|
||||||
|
</SidebarMenuItem>
|
||||||
|
</SidebarMenu>
|
||||||
|
</SidebarGroup>
|
||||||
</SidebarContent>
|
</SidebarContent>
|
||||||
|
|
||||||
<SidebarFooter>
|
<SidebarFooter>
|
||||||
|
|||||||
Reference in New Issue
Block a user