responsiveness and ui fixers

This commit is contained in:
2026-09-23 16:55:46 +05:30
parent 4c5e9b347a
commit f5e8e0e5b9
88 changed files with 9408 additions and 3576 deletions
@@ -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>–