responsiveness and ui fixers
This commit is contained in:
@@ -6,7 +6,7 @@
|
||||
import { useEffect, useState } from "react"
|
||||
import { useParams, usePathname } from "next/navigation"
|
||||
import Link from "next/link"
|
||||
import { BarChart3, Building2, MapPin, Phone, ReceiptText } from "lucide-react"
|
||||
import { ArrowLeft, BarChart3, Building2, MapPin, Phone, ReceiptText } from "lucide-react"
|
||||
|
||||
import { employeesApi } from "@/lib/api/employees"
|
||||
import { branchesHrmApi } from "@/lib/api/hrm-masters"
|
||||
@@ -20,13 +20,12 @@ import { buttonVariants } from "@/components/ui/button"
|
||||
import PieChart from "@/components/ui/pie-chart"
|
||||
import { Skeleton } from "@/components/ui/skeleton"
|
||||
|
||||
// Status colors, not a categorical series — same green/gray the Active/Inactive
|
||||
// badges already use elsewhere on this page, so the chart reads as an extension
|
||||
// of that badge rather than an unrelated palette.
|
||||
const STATUS_COLORS = { active: "#22c55e", inactive: "#9ca3af" }
|
||||
// Status colors — same green/red the item detail page's charts use (Purchased/Sold),
|
||||
// so chart colors read as one consistent palette across the app.
|
||||
const STATUS_COLORS = { active: "rgba(34,197,94,1)", inactive: "rgba(239,68,68,1)" }
|
||||
|
||||
// Sales invoice status — same three colors as the invoice status Badge elsewhere in the app.
|
||||
const SALES_STATUS_COLORS = { posted: "#22c55e", draft: "#9ca3af", cancelled: "#ef4444" }
|
||||
// Sales invoice status — same green/red as the item detail page's charts, plus gray for Draft.
|
||||
const SALES_STATUS_COLORS = { posted: "rgba(34,197,94,1)", draft: "#9ca3af", cancelled: "rgba(239,68,68,1)" }
|
||||
|
||||
const CURRENCY_FORMAT = new Intl.NumberFormat("en-LK", { style: "currency", currency: "LKR" })
|
||||
|
||||
@@ -109,9 +108,17 @@ export function BranchDetailPage() {
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-6">
|
||||
<div className="flex items-center gap-3">
|
||||
<Link
|
||||
href={listHref}
|
||||
className="inline-flex w-fit items-center gap-1.5 text-sm font-medium text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
<ArrowLeft className="size-4" />
|
||||
Back to Branches
|
||||
</Link>
|
||||
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
<div>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
<h1 className="text-2xl font-bold text-foreground">{branch.name}</h1>
|
||||
<Badge
|
||||
variant="outline"
|
||||
@@ -227,10 +234,6 @@ export function BranchDetailPage() {
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<Link href={listHref} className={cn(buttonVariants({ variant: "outline" }), "ml-auto w-fit")}>
|
||||
Back to branches
|
||||
</Link>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -27,6 +27,9 @@ import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@
|
||||
import { toast } from "@/components/ui/toast"
|
||||
|
||||
const PAGE_SIZE_OPTIONS = [5, 10, 20, 25]
|
||||
// Mobile has no page-size selector — tapping "Show more" just grows the page size and
|
||||
// re-fetches from page 1, so previously loaded cards stay put and more append below.
|
||||
const MOBILE_PAGE_SIZE_STEP = 10
|
||||
|
||||
export function BranchMasterPage() {
|
||||
const pathname = usePathname()
|
||||
@@ -47,6 +50,7 @@ export function BranchMasterPage() {
|
||||
const [errors, setErrors] = useState<Record<string, string>>({})
|
||||
const [submitting, setSubmitting] = useState(false)
|
||||
const [deletingId, setDeletingId] = useState<number | null>(null)
|
||||
const [showMoreLoading, setShowMoreLoading] = useState(false)
|
||||
|
||||
function load() {
|
||||
branchesHrmApi
|
||||
@@ -56,6 +60,12 @@ export function BranchMasterPage() {
|
||||
setPagination(res.pagination)
|
||||
})
|
||||
.catch((err) => setError(errorMessage(err)))
|
||||
.finally(() => setShowMoreLoading(false))
|
||||
}
|
||||
|
||||
function handleShowMore() {
|
||||
setShowMoreLoading(true)
|
||||
setPageSize((s) => s + MOBILE_PAGE_SIZE_STEP)
|
||||
}
|
||||
|
||||
useEffect(load, [page, pageSize, debouncedSearch])
|
||||
@@ -180,9 +190,9 @@ export function BranchMasterPage() {
|
||||
<Input id="branch-phone" value={phoneNumber} onChange={(e) => setPhoneNumber(e.target.value)} />
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
<div className="flex justify-center gap-3 pt-2">
|
||||
<Button variant="outline" className="min-w-32" onClick={() => setOpen(false)} disabled={submitting}>Cancel</Button>
|
||||
<Button className="min-w-32" onClick={handleSubmit} disabled={submitting}>
|
||||
<div className="flex flex-col-reverse gap-3 pt-2 sm:flex-row sm:justify-center">
|
||||
<Button variant="outline" className="w-full sm:w-auto sm:min-w-32" onClick={() => setOpen(false)} disabled={submitting}>Cancel</Button>
|
||||
<Button className="w-full sm:w-auto sm:min-w-32" onClick={handleSubmit} disabled={submitting}>
|
||||
{submitting ? (editing ? "Saving…" : "Creating…") : editing ? "Save" : "Create"}
|
||||
</Button>
|
||||
</div>
|
||||
@@ -261,110 +271,213 @@ export function BranchMasterPage() {
|
||||
)}
|
||||
|
||||
{!error && items !== null && items.length > 0 && (
|
||||
<div className="overflow-hidden rounded-xl border">
|
||||
<div className="overflow-x-auto">
|
||||
<Table className="text-base">
|
||||
<TableHeader>
|
||||
<TableRow className="hover:bg-transparent">
|
||||
<TableHead className="h-11 w-48 px-4 text-sm">Branch</TableHead>
|
||||
<TableHead className="h-11 w-32 px-4 text-sm">Code</TableHead>
|
||||
<TableHead className="h-11 px-4 text-sm">Address</TableHead>
|
||||
<TableHead className="h-11 w-40 px-4 text-sm">Phone Number</TableHead>
|
||||
<TableHead className="h-11 w-28 px-4 text-sm">Status</TableHead>
|
||||
<TableHead className="h-11 w-40 px-4 text-center text-sm">Actions</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{items.map((item) => (
|
||||
<TableRow key={item.branchId} className="align-top hover:bg-muted/20">
|
||||
<TableCell className="px-4 py-4 font-semibold text-foreground">{item.name}</TableCell>
|
||||
<TableCell className="px-4 py-4">
|
||||
<span className="font-mono text-xs text-muted-foreground">{item.code}</span>
|
||||
</TableCell>
|
||||
<TableCell className="px-4 py-4 text-sm text-muted-foreground">{item.address ?? "—"}</TableCell>
|
||||
<TableCell className="px-4 py-4 text-sm text-muted-foreground">{item.phoneNumber ?? "—"}</TableCell>
|
||||
<TableCell className="px-4 py-4">
|
||||
<Badge
|
||||
variant="outline"
|
||||
className={cn(
|
||||
"h-5 w-fit justify-center bg-white px-2 text-xs",
|
||||
item.status === "Active" ? "border-success/40 text-success" : "border-input text-muted-foreground"
|
||||
)}
|
||||
>
|
||||
{item.status}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="px-4 py-4">
|
||||
<div className="flex items-center justify-center gap-1">
|
||||
<Link
|
||||
href={`${pathname}/${item.branchId}`}
|
||||
className={cn(buttonVariants({ variant: "ghost", size: "icon-sm" }))}
|
||||
aria-label={`View ${item.name}`}
|
||||
title={`View ${item.name}`}
|
||||
>
|
||||
<Eye className="size-4" />
|
||||
</Link>
|
||||
<>
|
||||
{/* Mobile: stacked cards. */}
|
||||
<div className="flex flex-col gap-3 md:hidden">
|
||||
{items.map((item) => (
|
||||
<div key={item.branchId} className="flex flex-col gap-3 rounded-xl border bg-card p-4 shadow-sm">
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div className="min-w-0">
|
||||
<p className="truncate font-semibold text-foreground">{item.name}</p>
|
||||
<span className="font-mono text-xs text-muted-foreground">{item.code}</span>
|
||||
</div>
|
||||
<Badge
|
||||
variant="outline"
|
||||
className={cn(
|
||||
"h-6 w-fit shrink-0 justify-center border-transparent px-2.5 text-sm",
|
||||
item.status === "Active" ? "bg-success/10 text-success" : "bg-muted text-muted-foreground"
|
||||
)}
|
||||
>
|
||||
{item.status}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-3 border-t pt-3 text-sm">
|
||||
<div>
|
||||
<p className="text-muted-foreground">Address</p>
|
||||
<p className="font-medium text-foreground">{item.address ?? "—"}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-muted-foreground">Phone Number</p>
|
||||
<p className="font-medium text-foreground">{item.phoneNumber ?? "—"}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-end gap-1 border-t pt-3">
|
||||
<Link
|
||||
href={`${pathname}/${item.branchId}`}
|
||||
className={cn(buttonVariants({ variant: "ghost", size: "icon-sm" }))}
|
||||
aria-label={`View ${item.name}`}
|
||||
title={`View ${item.name}`}
|
||||
>
|
||||
<Eye className="size-4" />
|
||||
</Link>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
aria-label={`Edit ${item.name}`}
|
||||
title={`Edit ${item.name}`}
|
||||
onClick={() => openEditDialog(item)}
|
||||
>
|
||||
<Pencil className="size-4" />
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
className={cn(
|
||||
item.status === "Active"
|
||||
? "text-destructive hover:bg-destructive/10"
|
||||
: "text-success hover:bg-success/10"
|
||||
)}
|
||||
aria-label={item.status === "Active" ? `Deactivate ${item.name}` : `Activate ${item.name}`}
|
||||
title={item.status === "Active" ? `Deactivate ${item.name}` : `Activate ${item.name}`}
|
||||
disabled={actionPendingId === item.branchId}
|
||||
onClick={() => toggleStatus(item)}
|
||||
>
|
||||
{item.status === "Active" ? <XCircle className="size-4" /> : <CheckCircle2 className="size-4" />}
|
||||
</Button>
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger
|
||||
render={
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
aria-label={`Edit ${item.name}`}
|
||||
title={`Edit ${item.name}`}
|
||||
onClick={() => openEditDialog(item)}
|
||||
>
|
||||
<Pencil className="size-4" />
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
className={cn(
|
||||
item.status === "Active"
|
||||
? "text-destructive hover:bg-destructive/10"
|
||||
: "text-success hover:bg-success/10"
|
||||
)}
|
||||
aria-label={item.status === "Active" ? `Deactivate ${item.name}` : `Activate ${item.name}`}
|
||||
title={item.status === "Active" ? `Deactivate ${item.name}` : `Activate ${item.name}`}
|
||||
disabled={actionPendingId === item.branchId}
|
||||
onClick={() => toggleStatus(item)}
|
||||
>
|
||||
{item.status === "Active" ? <XCircle className="size-4" /> : <CheckCircle2 className="size-4" />}
|
||||
</Button>
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger
|
||||
render={
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
className="text-destructive hover:bg-destructive/10"
|
||||
aria-label={`Delete ${item.name}`}
|
||||
title={`Delete ${item.name}`}
|
||||
disabled={deletingId === item.branchId}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Trash2 className="size-4" />
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent
|
||||
variant="destructive"
|
||||
title={`Delete ${item.name}?`}
|
||||
description="This permanently removes the branch. Branches referenced by employees, warehouses, or other records cannot be deleted."
|
||||
confirmLabel="Delete"
|
||||
onConfirm={() => handleDelete(item)}
|
||||
/>
|
||||
</AlertDialog>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
className="text-destructive hover:bg-destructive/10"
|
||||
aria-label={`Delete ${item.name}`}
|
||||
title={`Delete ${item.name}`}
|
||||
disabled={deletingId === item.branchId}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Trash2 className="size-4" />
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent
|
||||
variant="destructive"
|
||||
title={`Delete ${item.name}?`}
|
||||
description="This permanently removes the branch. Branches referenced by employees, warehouses, or other records cannot be deleted."
|
||||
confirmLabel="Delete"
|
||||
onConfirm={() => handleDelete(item)}
|
||||
/>
|
||||
</AlertDialog>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{pagination && items.length < pagination.totalItems && (
|
||||
<Button type="button" variant="outline" size="lg" className="w-full" onClick={handleShowMore} disabled={showMoreLoading}>
|
||||
{showMoreLoading ? "Loading…" : "Show more"}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Desktop: table. */}
|
||||
<div className="hidden overflow-hidden rounded-xl border md:block">
|
||||
<div className="overflow-x-auto">
|
||||
<Table className="text-base">
|
||||
<TableHeader>
|
||||
<TableRow className="hover:bg-transparent">
|
||||
<TableHead className="h-11 w-48 px-4 text-sm">Branch</TableHead>
|
||||
<TableHead className="h-11 w-32 px-4 text-sm">Code</TableHead>
|
||||
<TableHead className="h-11 px-4 text-sm">Address</TableHead>
|
||||
<TableHead className="h-11 w-40 px-4 text-sm">Phone Number</TableHead>
|
||||
<TableHead className="h-11 w-28 px-4 text-sm">Status</TableHead>
|
||||
<TableHead className="h-11 w-40 px-4 text-center text-sm">Actions</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{items.map((item) => (
|
||||
<TableRow key={item.branchId} className="align-top hover:bg-muted/20">
|
||||
<TableCell className="px-4 py-4 font-semibold text-foreground">{item.name}</TableCell>
|
||||
<TableCell className="px-4 py-4">
|
||||
<span className="font-mono text-xs text-muted-foreground">{item.code}</span>
|
||||
</TableCell>
|
||||
<TableCell className="px-4 py-4 text-sm text-muted-foreground">{item.address ?? "—"}</TableCell>
|
||||
<TableCell className="px-4 py-4 text-sm text-muted-foreground">{item.phoneNumber ?? "—"}</TableCell>
|
||||
<TableCell className="px-4 py-4">
|
||||
<Badge
|
||||
variant="outline"
|
||||
className={cn(
|
||||
"h-5 w-fit justify-center bg-white px-2 text-xs",
|
||||
item.status === "Active" ? "border-success/40 text-success" : "border-input text-muted-foreground"
|
||||
)}
|
||||
>
|
||||
{item.status}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="px-4 py-4">
|
||||
<div className="flex items-center justify-center gap-1">
|
||||
<Link
|
||||
href={`${pathname}/${item.branchId}`}
|
||||
className={cn(buttonVariants({ variant: "ghost", size: "icon-sm" }))}
|
||||
aria-label={`View ${item.name}`}
|
||||
title={`View ${item.name}`}
|
||||
>
|
||||
<Eye className="size-4" />
|
||||
</Link>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
aria-label={`Edit ${item.name}`}
|
||||
title={`Edit ${item.name}`}
|
||||
onClick={() => openEditDialog(item)}
|
||||
>
|
||||
<Pencil className="size-4" />
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
className={cn(
|
||||
item.status === "Active"
|
||||
? "text-destructive hover:bg-destructive/10"
|
||||
: "text-success hover:bg-success/10"
|
||||
)}
|
||||
aria-label={item.status === "Active" ? `Deactivate ${item.name}` : `Activate ${item.name}`}
|
||||
title={item.status === "Active" ? `Deactivate ${item.name}` : `Activate ${item.name}`}
|
||||
disabled={actionPendingId === item.branchId}
|
||||
onClick={() => toggleStatus(item)}
|
||||
>
|
||||
{item.status === "Active" ? <XCircle className="size-4" /> : <CheckCircle2 className="size-4" />}
|
||||
</Button>
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger
|
||||
render={
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
className="text-destructive hover:bg-destructive/10"
|
||||
aria-label={`Delete ${item.name}`}
|
||||
title={`Delete ${item.name}`}
|
||||
disabled={deletingId === item.branchId}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Trash2 className="size-4" />
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent
|
||||
variant="destructive"
|
||||
title={`Delete ${item.name}?`}
|
||||
description="This permanently removes the branch. Branches referenced by employees, warehouses, or other records cannot be deleted."
|
||||
confirmLabel="Delete"
|
||||
onConfirm={() => handleDelete(item)}
|
||||
/>
|
||||
</AlertDialog>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{pagination && pagination.totalItems > 0 && (
|
||||
<div className="flex flex-col-reverse items-center gap-3 sm:flex-row sm:justify-between">
|
||||
<div className="hidden items-center gap-3 md:flex md:justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Showing <span className="font-medium text-foreground">{(pagination.page - 1) * pagination.pageSize + 1}</span>–
|
||||
|
||||
Reference in New Issue
Block a user