feat: add EmptyState component for improved no-data display in supplier payments
This commit is contained in:
+12
-8
@@ -31,6 +31,8 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@
|
|||||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
|
||||||
import { toast } from '@/components/ui/toast'
|
import { toast } from '@/components/ui/toast'
|
||||||
import { TopLoader } from '@/components/shared/top-loader'
|
import { TopLoader } from '@/components/shared/top-loader'
|
||||||
|
import { EmptyState } from '@/components/shared/empty-state'
|
||||||
|
import { ReceiptDollarIcon } from '@hugeicons/core-free-icons'
|
||||||
|
|
||||||
function formatDate(value: string) {
|
function formatDate(value: string) {
|
||||||
return new Date(value).toLocaleDateString('en-GB', { day: '2-digit', month: 'short', year: 'numeric' })
|
return new Date(value).toLocaleDateString('en-GB', { day: '2-digit', month: 'short', year: 'numeric' })
|
||||||
@@ -84,6 +86,13 @@ export function SupplierPaymentsClient({ supplierId }: { supplierId: string }) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="rounded-md border mt-4">
|
<div className="rounded-md border mt-4">
|
||||||
|
{!loading && payments.length === 0 ? (
|
||||||
|
<EmptyState
|
||||||
|
icon={ReceiptDollarIcon}
|
||||||
|
title="No payments yet"
|
||||||
|
description="Record a payment to settle one or more GRNs for this supplier."
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
@@ -96,12 +105,7 @@ export function SupplierPaymentsClient({ supplierId }: { supplierId: string }) {
|
|||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{loading ? (
|
{payments.map(p => (
|
||||||
<TableRow><TableCell colSpan={6}>Loading…</TableCell></TableRow>
|
|
||||||
) : payments.length === 0 ? (
|
|
||||||
<TableRow><TableCell colSpan={6}>No payments yet.</TableCell></TableRow>
|
|
||||||
) : (
|
|
||||||
payments.map(p => (
|
|
||||||
<TableRow key={p.id}>
|
<TableRow key={p.id}>
|
||||||
<TableCell>{formatDate(p.paidAt)}</TableCell>
|
<TableCell>{formatDate(p.paidAt)}</TableCell>
|
||||||
<TableCell><Badge variant="secondary">{p.method}</Badge></TableCell>
|
<TableCell><Badge variant="secondary">{p.method}</Badge></TableCell>
|
||||||
@@ -112,10 +116,10 @@ export function SupplierPaymentsClient({ supplierId }: { supplierId: string }) {
|
|||||||
<TableCell className="text-right">{p.allocations.length}</TableCell>
|
<TableCell className="text-right">{p.allocations.length}</TableCell>
|
||||||
<TableCell className="text-sm text-muted-foreground">{p.notes ?? '—'}</TableCell>
|
<TableCell className="text-sm text-muted-foreground">{p.notes ?? '—'}</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))
|
))}
|
||||||
)}
|
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center justify-between mt-3 text-sm">
|
<div className="flex items-center justify-between mt-3 text-sm">
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { HugeiconsIcon } from '@hugeicons/react'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
type HugeiconDef = React.ComponentProps<typeof HugeiconsIcon>['icon']
|
||||||
|
|
||||||
|
interface EmptyStateProps {
|
||||||
|
icon?: HugeiconDef
|
||||||
|
title: string
|
||||||
|
description?: string
|
||||||
|
action?: React.ReactNode
|
||||||
|
className?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Centered empty-state for lists and tables. Soft icon in a ringed circle,
|
||||||
|
* tight title and a muted one-liner underneath. Caller can slot an action button.
|
||||||
|
*/
|
||||||
|
export function EmptyState({ icon, title, description, action, className }: EmptyStateProps) {
|
||||||
|
return (
|
||||||
|
<div className={cn('flex flex-col items-center justify-center gap-3 px-6 py-16 text-center', className)}>
|
||||||
|
{icon && (
|
||||||
|
<div
|
||||||
|
className="relative flex h-14 w-14 items-center justify-center rounded-full
|
||||||
|
bg-gradient-to-b from-muted/60 to-muted/20
|
||||||
|
ring-1 ring-border/60
|
||||||
|
shadow-[inset_0_0_0_1px_rgba(255,255,255,0.03)]"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="absolute inset-0 rounded-full opacity-70 blur-xl"
|
||||||
|
style={{ background: 'radial-gradient(closest-side, rgba(255,255,255,0.06), transparent 70%)' }}
|
||||||
|
/>
|
||||||
|
<HugeiconsIcon icon={icon} size={22} className="relative text-muted-foreground/80" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="space-y-1">
|
||||||
|
<p className="text-sm font-medium text-foreground">{title}</p>
|
||||||
|
{description && (
|
||||||
|
<p className="text-xs text-muted-foreground max-w-sm mx-auto leading-relaxed">
|
||||||
|
{description}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{action && <div className="mt-2">{action}</div>}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user