Update sales pages
This commit is contained in:
@@ -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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user