Update sales pages

This commit is contained in:
2026-09-20 21:33:44 +05:30
parent 1c9ca179df
commit a9a201cce1
6 changed files with 681 additions and 351 deletions
@@ -0,0 +1,216 @@
'use client'
import { useEffect, useState } from 'react'
import { useParams, useRouter } from 'next/navigation'
import { HugeiconsIcon } from '@hugeicons/react'
import { ArrowLeft01Icon } from '@hugeicons/core-free-icons'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { toast } from '@/components/ui/toast'
import { shopPos as shopPosApi, getApiError } from '@/lib/api/index'
import type { ShopPOResponse, ShopPOStatus } from '@/types/api'
const PO_STATUS_VARIANT: Record<ShopPOStatus, 'default' | 'secondary' | 'destructive'> = {
Pending: 'secondary',
PartiallyFulfilled: 'secondary',
Fulfilled: 'default',
Cancelled: 'destructive',
}
const fmtAmount = (n: number) => `Rs. ${n.toLocaleString()}`
const truncId = (id: string) => id.substring(0, 8)
const fmtDateTime = (v: string | null) =>
v
? new Date(v).toLocaleString('en-GB', {
day: '2-digit', month: 'short', year: 'numeric', hour: '2-digit', minute: '2-digit',
})
: '—'
function Stat({ label, value }: { label: string; value: React.ReactNode }) {
return (
<div className="flex flex-col gap-0.5">
<span className="text-xs text-muted-foreground">{label}</span>
<span className="text-sm font-medium text-foreground">{value}</span>
</div>
)
}
export default function ShopPOViewPage() {
const router = useRouter()
const params = useParams<{ id: string }>()
const [po, setPo] = useState<ShopPOResponse | null>(null)
const [loading, setLoading] = useState(true)
useEffect(() => {
let cancelled = false
setLoading(true)
shopPosApi
.get(params.id)
.then((res) => {
if (!cancelled) setPo(res)
})
.catch((err) => {
toast.add({ title: 'Failed to load Shop PO', description: getApiError(err), type: 'error' })
router.push('/sales')
})
.finally(() => {
if (!cancelled) setLoading(false)
})
return () => {
cancelled = true
}
}, [params.id, router])
async function handleConfirm() {
if (!po) return
try {
const updated = await shopPosApi.confirm(po.id)
setPo(updated)
toast.add({ title: 'Shop PO confirmed', type: 'success' })
} catch (err) {
toast.add({ title: 'Failed to confirm Shop PO', description: getApiError(err), type: 'error' })
}
}
async function handleCancel() {
if (!po) return
try {
const updated = await shopPosApi.cancel(po.id)
setPo(updated)
toast.add({ title: 'Shop PO cancelled', type: 'success' })
} catch (err) {
toast.add({ title: 'Failed to cancel Shop PO', description: getApiError(err), type: 'error' })
}
}
if (loading) {
return (
<div className="flex flex-col gap-6">
<div className="flex items-center gap-4">
<Button type="button" variant="outline" size="sm" onClick={() => router.push('/sales')}>
<HugeiconsIcon icon={ArrowLeft01Icon} size={16} strokeWidth={2} />
Back
</Button>
</div>
<p className="text-sm text-muted-foreground">Loading Shop PO…</p>
</div>
)
}
if (!po) return null
const activeItems = po.items.filter((i) => i.isActive)
return (
<div className="flex flex-col gap-6">
<div className="flex items-center justify-between gap-4">
<div className="flex items-center gap-4">
<Button type="button" variant="outline" size="sm" onClick={() => router.push('/sales')}>
<HugeiconsIcon icon={ArrowLeft01Icon} size={16} strokeWidth={2} />
Back
</Button>
<div>
<div className="flex items-center gap-2">
<h1 className="text-2xl font-semibold text-foreground">
Shop PO <span className="font-mono text-lg text-muted-foreground">{truncId(po.id)}</span>
</h1>
<Badge variant={PO_STATUS_VARIANT[po.status]}>{po.status}</Badge>
</div>
<p className="text-sm text-muted-foreground">{po.storeName}</p>
</div>
</div>
<div className="flex items-center gap-2">
{!po.confirmedAt && po.status !== 'Cancelled' && (
<Button type="button" onClick={handleConfirm}>
Confirm PO
</Button>
)}
{po.status !== 'Cancelled' && po.status !== 'Fulfilled' && (
<Button type="button" variant="outline" onClick={handleCancel}>
Cancel PO
</Button>
)}
</div>
</div>
<div className="grid grid-cols-2 gap-4 rounded-2xl border p-4 sm:grid-cols-4">
<Stat
label="Invoice"
value={
po.invoiceId ? (
<Button
type="button"
variant="link"
className="h-auto p-0 font-mono text-sm"
onClick={() => router.push(`/sales/invoices/${po.invoiceId}`)}
>
{truncId(po.invoiceId)}
</Button>
) : (
'Not raised yet'
)
}
/>
<Stat label="Created" value={fmtDateTime(po.createdAt)} />
<Stat label="Confirmed" value={fmtDateTime(po.confirmedAt)} />
<Stat label="Fulfilled" value={fmtDateTime(po.fulfilledAt)} />
</div>
<div className="rounded-2xl border p-6">
<div className="mb-4">
<h3 className="text-sm font-medium text-foreground">Items</h3>
<p className="text-sm text-muted-foreground">
{activeItems.length} item{activeItems.length === 1 ? '' : 's'} on this PO.
</p>
</div>
<div className="overflow-x-auto rounded-xl border">
<table className="w-full text-sm">
<thead className="bg-muted/50">
<tr>
<th className="min-w-[220px] px-3 py-2 text-left font-medium">Item</th>
<th className="w-28 px-3 py-2 text-right font-medium">Quantity</th>
<th className="w-28 px-3 py-2 text-right font-medium">Fulfilled</th>
<th className="w-32 px-3 py-2 text-right font-medium">Unit Price</th>
<th className="w-32 px-3 py-2 text-right font-medium">Line Total</th>
</tr>
</thead>
<tbody>
{activeItems.length === 0 ? (
<tr>
<td colSpan={5} className="px-3 py-4 text-center text-muted-foreground">
No items on this PO.
</td>
</tr>
) : (
activeItems.map((item) => (
<tr key={item.id} className="border-t">
<td className="px-3 py-2">{item.itemName}</td>
<td className="px-3 py-2 text-right">{item.quantity}</td>
<td className="px-3 py-2 text-right">{item.quantityFulfilled}</td>
<td className="px-3 py-2 text-right">{fmtAmount(item.unitPrice)}</td>
<td className="px-3 py-2 text-right font-medium">{fmtAmount(item.quantity * item.unitPrice)}</td>
</tr>
))
)}
</tbody>
{activeItems.length > 0 && (
<tfoot>
<tr className="border-t bg-muted/30">
<td colSpan={4} className="px-3 py-3 text-right text-sm font-medium text-foreground">
Total
</td>
<td className="px-3 py-3 text-right text-base font-semibold text-foreground">
{fmtAmount(po.totalAmount)}
</td>
</tr>
</tfoot>
)}
</table>
</div>
</div>
</div>
)
}