remove PriceListSheet component and integrate new price list creation page

This commit is contained in:
2026-09-09 16:31:35 +05:30
parent eb0303fa3f
commit 617c0713a1
12 changed files with 1145 additions and 288 deletions
@@ -0,0 +1,224 @@
'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 { useForm, zodResolver, z } from '@/lib/form'
import { FormField } from '@/components/shared/form-field'
import { Input } from '@/components/ui/input'
import { Checkbox } from '@/components/ui/checkbox'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
import { Button } from '@/components/ui/button'
import { FieldGroup } from '@/components/ui/field'
import { toast } from '@/components/ui/toast'
import { priceLists as priceListsApi, regions as regionsApi, getApiError, getPaginatedItems } from '@/lib/api/index'
import type { PriceListResponse, RegionResponse } from '@/types/api'
import { PriceListItemsSection } from '../../_components/price-list-items-section'
const schema = z.object({
name: z.string().min(1, 'Name is required'),
regionId: z.string().optional(),
effectiveFrom: z.string().min(1, 'Required'),
effectiveTo: z.string().min(1, 'Required'),
isActive: z.boolean(),
})
type FormValues = z.infer<typeof schema>
export default function EditPriceListPage() {
const router = useRouter()
const params = useParams<{ id: string }>()
const [priceList, setPriceList] = useState<PriceListResponse | null>(null)
const [regionsList, setRegionsList] = useState<RegionResponse[]>([])
const [loading, setLoading] = useState(true)
const [itemsSectionKey, setItemsSectionKey] = useState(0)
const form = useForm<FormValues>({
resolver: zodResolver(schema),
defaultValues: { name: '', regionId: '', effectiveFrom: '', effectiveTo: '', isActive: true },
})
useEffect(() => {
Promise.all([
priceListsApi.get(params.id),
regionsApi.list({ pageSize: 100 }),
])
.then(([pl, regionsRes]) => {
setPriceList(pl)
setRegionsList(getPaginatedItems(regionsRes))
form.reset({
name: pl.name,
regionId: pl.regionId ?? '',
effectiveFrom: pl.effectiveFrom.split('T')[0],
effectiveTo: pl.effectiveTo.split('T')[0],
isActive: pl.isActive,
})
})
.catch((err) => toast.add({ title: 'Failed to load price list', description: getApiError(err), type: 'error' }))
.finally(() => setLoading(false))
}, [params.id])
async function onSubmit(values: FormValues) {
try {
const updated = await priceListsApi.update(params.id, {
name: values.name,
regionId: values.regionId || null,
effectiveFrom: values.effectiveFrom,
effectiveTo: values.effectiveTo,
isActive: values.isActive,
})
toast.add({ title: 'Price list updated', type: 'success' })
setPriceList(updated)
setItemsSectionKey((k) => k + 1)
} catch (err) {
toast.add({ title: 'Failed to update price list', description: getApiError(err), type: 'error' })
}
}
if (loading) {
return <p className="text-sm text-muted-foreground">Loading…</p>
}
if (!priceList) {
return <p className="text-sm text-muted-foreground">Price list not found.</p>
}
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('/pricing/price-lists')}>
<HugeiconsIcon icon={ArrowLeft01Icon} size={16} strokeWidth={2} />
Back
</Button>
<div>
<h1 className="text-2xl font-semibold text-foreground">Edit Price List</h1>
<p className="text-sm text-muted-foreground">{priceList.name}</p>
</div>
</div>
<form onSubmit={form.handleSubmit(onSubmit)} className="flex flex-col gap-6">
<div className="rounded-2xl border p-6">
<h3 className="mb-4 text-sm font-medium text-foreground">Price List Details</h3>
<FieldGroup>
<div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
<FormField
control={form.control}
name="name"
label="Name"
required
render={({ field, invalid }) => (
<Input
id="name"
name={field.name}
ref={field.ref}
value={field.value as string}
onChange={field.onChange}
onBlur={field.onBlur}
placeholder="e.g. Standard Q1 2025"
aria-invalid={invalid}
/>
)}
/>
<FormField
control={form.control}
name="regionId"
label="Region"
render={({ field }) => (
<Select value={field.value as string} onValueChange={field.onChange}>
<SelectTrigger>
<SelectValue placeholder="All Regions" />
</SelectTrigger>
<SelectContent>
<SelectItem value="">All Regions</SelectItem>
{regionsList.map((r) => (
<SelectItem key={r.id} value={r.id}>{r.name}</SelectItem>
))}
</SelectContent>
</Select>
)}
/>
<div className="grid grid-cols-2 gap-4">
<FormField
control={form.control}
name="effectiveFrom"
label="Effective From"
required
render={({ field, invalid }) => (
<Input
id="effectiveFrom"
name={field.name}
ref={field.ref}
type="date"
value={field.value as string}
onChange={field.onChange}
onBlur={field.onBlur}
aria-invalid={invalid}
/>
)}
/>
<FormField
control={form.control}
name="effectiveTo"
label="Effective To"
required
render={({ field, invalid }) => (
<Input
id="effectiveTo"
name={field.name}
ref={field.ref}
type="date"
value={field.value as string}
onChange={field.onChange}
onBlur={field.onBlur}
aria-invalid={invalid}
/>
)}
/>
</div>
</div>
<FormField
control={form.control}
name="isActive"
render={({ field }) => (
<div className="flex items-center gap-2">
<Checkbox
id="isActive"
checked={field.value as boolean}
onCheckedChange={field.onChange}
/>
<label htmlFor="isActive" className="text-sm font-medium cursor-pointer">
Active
</label>
</div>
)}
/>
</FieldGroup>
</div>
<div className="flex justify-end gap-2">
<Button type="button" variant="outline" onClick={() => router.push('/pricing/price-lists')}>
Done
</Button>
<Button type="submit" disabled={form.formState.isSubmitting}>
{form.formState.isSubmitting ? 'Saving…' : 'Save Changes'}
</Button>
</div>
</form>
<div className="rounded-2xl border p-6">
<PriceListItemsSection key={itemsSectionKey} priceListId={params.id} />
</div>
</div>
)
}
@@ -0,0 +1,114 @@
'use client'
import { useEffect, useState } from 'react'
import { useParams, useRouter } from 'next/navigation'
import { HugeiconsIcon } from '@hugeicons/react'
import { ArrowLeft01Icon, PencilEdit01Icon } from '@hugeicons/core-free-icons'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { Separator } from '@/components/ui/separator'
import { toast } from '@/components/ui/toast'
import { priceLists as priceListsApi, getApiError } from '@/lib/api/index'
import type { PriceListResponse } from '@/types/api'
const fmtDate = (v: string) =>
new Date(v).toLocaleDateString('en-GB', { day: '2-digit', month: 'short', year: 'numeric' })
const fmtAmount = (n: number) => `Rs. ${n.toLocaleString()}`
function Row({ label, value }: { label: string; value: React.ReactNode }) {
return (
<div className="flex items-center justify-between py-2 text-sm border-b last:border-0">
<span className="text-muted-foreground">{label}</span>
<span className="font-medium text-right">{value}</span>
</div>
)
}
export default function ViewPriceListPage() {
const router = useRouter()
const params = useParams<{ id: string }>()
const [priceList, setPriceList] = useState<PriceListResponse | null>(null)
const [loading, setLoading] = useState(true)
useEffect(() => {
priceListsApi.get(params.id)
.then(setPriceList)
.catch((err) => toast.add({ title: 'Failed to load price list', description: getApiError(err), type: 'error' }))
.finally(() => setLoading(false))
}, [params.id])
if (loading) {
return <p className="text-sm text-muted-foreground">Loading…</p>
}
if (!priceList) {
return <p className="text-sm text-muted-foreground">Price list not found.</p>
}
return (
<div className="flex flex-col gap-6">
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<Button type="button" variant="outline" size="sm" onClick={() => router.push('/pricing/price-lists')}>
<HugeiconsIcon icon={ArrowLeft01Icon} size={16} strokeWidth={2} />
Back
</Button>
<div>
<h1 className="text-2xl font-semibold text-foreground">{priceList.name}</h1>
<p className="text-sm text-muted-foreground">Price List Details</p>
</div>
</div>
<Button onClick={() => router.push(`/pricing/price-lists/${priceList.id}/edit`)}>
<HugeiconsIcon icon={PencilEdit01Icon} size={16} strokeWidth={2} />
Edit
</Button>
</div>
<div className="rounded-2xl border p-6">
<Row label="Name" value={priceList.name} />
<Row label="Region" value={priceList.regionName ?? 'All Regions'} />
<Row label="Status" value={<Badge variant={priceList.isActive ? 'default' : 'secondary'}>{priceList.isActive ? 'Active' : 'Inactive'}</Badge>} />
<Row label="Effective From" value={fmtDate(priceList.effectiveFrom)} />
<Row label="Effective To" value={fmtDate(priceList.effectiveTo)} />
<Row label="Created" value={fmtDate(priceList.createdAt)} />
</div>
<div className="rounded-2xl border p-6">
<h3 className="mb-4 text-sm font-medium text-foreground">Items</h3>
<Separator className="mb-4" />
{priceList.items.length === 0 ? (
<p className="text-sm text-muted-foreground">No items priced on this list yet.</p>
) : (
<div className="overflow-x-auto rounded-xl border">
<table className="w-full text-sm">
<thead className="bg-muted/50">
<tr>
<th className="px-3 py-2 text-left font-medium">Item</th>
<th className="px-3 py-2 text-right font-medium">Price</th>
<th className="px-3 py-2 text-right font-medium">Min Price</th>
<th className="px-3 py-2 text-right font-medium">Retail Price</th>
<th className="px-3 py-2 text-right font-medium">Agency %</th>
</tr>
</thead>
<tbody>
{priceList.items.map((item) => (
<tr key={item.id} className="border-t">
<td className="px-3 py-2">
{item.itemName}
<span className="ml-1 text-xs text-muted-foreground">({item.sku})</span>
</td>
<td className="px-3 py-2 text-right">{fmtAmount(item.price)}</td>
<td className="px-3 py-2 text-right">{item.minPrice != null ? fmtAmount(item.minPrice) : '—'}</td>
<td className="px-3 py-2 text-right">{item.retailPrice != null ? fmtAmount(item.retailPrice) : '—'}</td>
<td className="px-3 py-2 text-right">{item.agencyMarginPercent != null ? `${item.agencyMarginPercent}%` : '—'}</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
</div>
)
}
@@ -0,0 +1,334 @@
'use client'
import { useEffect, useMemo, useState } from 'react'
import { HugeiconsIcon } from '@hugeicons/react'
import { Add01Icon, Delete02Icon, PencilEdit01Icon } from '@hugeicons/core-free-icons'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
import { toast } from '@/components/ui/toast'
import { priceLists as priceListsApi, items as itemsApi, getApiError, getPaginatedItems } from '@/lib/api/index'
import type { PriceListResponse, PriceListItemResponse, ItemSummaryResponse } from '@/types/api'
const fmtAmount = (n: number) => `Rs. ${n.toLocaleString()}`
interface PriceListItemsSectionProps {
priceListId: string
onChanged?: () => void
}
interface ItemFormState {
price: number
minPrice: string
retailPrice: string
agencyMarginPercent: string
}
function emptyForm(): ItemFormState {
return { price: 0, minPrice: '', retailPrice: '', agencyMarginPercent: '' }
}
export function PriceListItemsSection({ priceListId, onChanged }: PriceListItemsSectionProps) {
const [items, setItems] = useState<PriceListItemResponse[]>([])
const [itemsList, setItemsList] = useState<ItemSummaryResponse[]>([])
const [isActive, setIsActive] = useState(true)
const [loading, setLoading] = useState(true)
const [editingSku, setEditingSku] = useState<string | null>(null)
const [editForm, setEditForm] = useState<ItemFormState>(emptyForm())
const [newItemId, setNewItemId] = useState('')
const [newForm, setNewForm] = useState<ItemFormState>(emptyForm())
const [saving, setSaving] = useState(false)
const pricedSkus = useMemo(() => new Set(items.map((i) => i.sku)), [items])
const availableItems = useMemo(
() => itemsList.filter((i) => !pricedSkus.has(i.sku)),
[itemsList, pricedSkus]
)
function loadDetail() {
setLoading(true)
Promise.all([
priceListsApi.get(priceListId),
itemsApi.list({ pageSize: 500 }),
])
.then(([pl, itemsRes]) => {
setItems(pl.items)
setIsActive(pl.isActive)
setItemsList(getPaginatedItems(itemsRes))
})
.catch((err) => toast.add({ title: 'Failed to load price list items', description: getApiError(err), type: 'error' }))
.finally(() => setLoading(false))
}
useEffect(() => {
loadDetail()
}, [priceListId])
function refresh(pl: PriceListResponse) {
setItems(pl.items)
setIsActive(pl.isActive)
onChanged?.()
}
async function handleAdd() {
if (!newItemId) {
toast.add({ title: 'Select an item first', type: 'error' })
return
}
if (newForm.price <= 0) {
toast.add({ title: 'Price must be greater than 0', type: 'error' })
return
}
const item = itemsList.find((i) => i.id === newItemId)
if (!item) return
setSaving(true)
try {
const pl = await priceListsApi.upsertItem(priceListId, item.sku, {
price: newForm.price,
minPrice: newForm.minPrice ? Number(newForm.minPrice) : null,
retailPrice: newForm.retailPrice ? Number(newForm.retailPrice) : null,
agencyMarginPercent: newForm.agencyMarginPercent ? Number(newForm.agencyMarginPercent) : null,
})
refresh(pl)
setNewItemId('')
setNewForm(emptyForm())
toast.add({ title: 'Item priced', type: 'success' })
} catch (err) {
toast.add({ title: 'Failed to price item', description: getApiError(err), type: 'error' })
} finally {
setSaving(false)
}
}
function startEdit(item: PriceListItemResponse) {
setEditingSku(item.sku)
setEditForm({
price: item.price,
minPrice: item.minPrice != null ? String(item.minPrice) : '',
retailPrice: item.retailPrice != null ? String(item.retailPrice) : '',
agencyMarginPercent: item.agencyMarginPercent != null ? String(item.agencyMarginPercent) : '',
})
}
async function handleSaveEdit(sku: string) {
if (editForm.price <= 0) {
toast.add({ title: 'Price must be greater than 0', type: 'error' })
return
}
setSaving(true)
try {
const pl = await priceListsApi.upsertItem(priceListId, sku, {
price: editForm.price,
minPrice: editForm.minPrice ? Number(editForm.minPrice) : null,
retailPrice: editForm.retailPrice ? Number(editForm.retailPrice) : null,
agencyMarginPercent: editForm.agencyMarginPercent ? Number(editForm.agencyMarginPercent) : null,
})
refresh(pl)
setEditingSku(null)
toast.add({ title: 'Item price updated', type: 'success' })
} catch (err) {
toast.add({ title: 'Failed to update item price', description: getApiError(err), type: 'error' })
} finally {
setSaving(false)
}
}
async function handleRemove(sku: string) {
setSaving(true)
try {
const pl = await priceListsApi.removeItem(priceListId, sku)
refresh(pl)
toast.add({ title: 'Item removed from price list', type: 'success' })
} catch (err) {
toast.add({ title: 'Failed to remove item', description: getApiError(err), type: 'error' })
} finally {
setSaving(false)
}
}
return (
<div className="flex flex-col gap-3">
<p className="text-sm font-medium text-foreground">Items</p>
{!loading && !isActive && (
<p className="rounded-xl border border-amber-300 bg-amber-50 px-3 py-2 text-sm text-amber-800 dark:border-amber-900 dark:bg-amber-950 dark:text-amber-200">
This price list is Inactive — reactivate it above (check &quot;Active&quot; and Save) before adding or changing item prices.
</p>
)}
{loading ? (
<p className="text-sm text-muted-foreground">Loading items…</p>
) : (
<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">Price</th>
<th className="w-28 px-3 py-2 text-right font-medium">Min Price</th>
<th className="w-28 px-3 py-2 text-right font-medium">Retail Price</th>
<th className="w-28 px-3 py-2 text-right font-medium">Agency %</th>
<th className="w-20 px-3 py-2"></th>
</tr>
</thead>
<tbody>
{items.map((item) => (
<tr key={item.id} className="border-t">
<td className="px-3 py-2">
{item.itemName}
<span className="ml-1 text-xs text-muted-foreground">({item.sku})</span>
</td>
{editingSku === item.sku ? (
<>
<td className="px-3 py-2">
<Input
type="number"
min={0.01}
step="0.01"
value={editForm.price}
onChange={(e) => setEditForm((f) => ({ ...f, price: Number(e.target.value) }))}
/>
</td>
<td className="px-3 py-2">
<Input
type="number"
min={0}
step="0.01"
placeholder="None"
value={editForm.minPrice}
onChange={(e) => setEditForm((f) => ({ ...f, minPrice: e.target.value }))}
/>
</td>
<td className="px-3 py-2">
<Input
type="number"
min={0}
step="0.01"
placeholder="None"
value={editForm.retailPrice}
onChange={(e) => setEditForm((f) => ({ ...f, retailPrice: e.target.value }))}
/>
</td>
<td className="px-3 py-2">
<Input
type="number"
min={0}
max={100}
step="0.01"
placeholder="None"
value={editForm.agencyMarginPercent}
onChange={(e) => setEditForm((f) => ({ ...f, agencyMarginPercent: e.target.value }))}
/>
</td>
<td className="px-3 py-2">
<div className="flex justify-end gap-1">
<Button type="button" size="sm" disabled={saving} onClick={() => handleSaveEdit(item.sku)}>
Save
</Button>
<Button type="button" size="sm" variant="outline" onClick={() => setEditingSku(null)}>
Cancel
</Button>
</div>
</td>
</>
) : (
<>
<td className="px-3 py-2 text-right">{fmtAmount(item.price)}</td>
<td className="px-3 py-2 text-right">{item.minPrice != null ? fmtAmount(item.minPrice) : '—'}</td>
<td className="px-3 py-2 text-right">{item.retailPrice != null ? fmtAmount(item.retailPrice) : '—'}</td>
<td className="px-3 py-2 text-right">{item.agencyMarginPercent != null ? `${item.agencyMarginPercent}%` : '—'}</td>
<td className="px-3 py-2">
<div className="flex justify-end gap-1">
<Button type="button" size="sm" variant="outline" disabled={!isActive} onClick={() => startEdit(item)}>
<HugeiconsIcon icon={PencilEdit01Icon} size={14} strokeWidth={2} />
</Button>
<Button type="button" size="sm" variant="outline" disabled={saving || !isActive} onClick={() => handleRemove(item.sku)}>
<HugeiconsIcon icon={Delete02Icon} size={14} strokeWidth={2} />
</Button>
</div>
</td>
</>
)}
</tr>
))}
<tr className="border-t bg-muted/20">
<td className="px-3 py-2">
<Select value={newItemId} onValueChange={(v) => setNewItemId(v ?? '')} disabled={!isActive}>
<SelectTrigger className="w-full">
<SelectValue placeholder="Add item..." />
</SelectTrigger>
<SelectContent className="max-h-80 min-w-[320px]">
{availableItems.map((i) => (
<SelectItem key={i.id} value={i.id}>{i.name} — {i.sku}</SelectItem>
))}
</SelectContent>
</Select>
</td>
<td className="px-3 py-2">
<Input
type="number"
min={0.01}
step="0.01"
placeholder="Price"
value={newForm.price || ''}
onChange={(e) => setNewForm((f) => ({ ...f, price: Number(e.target.value) }))}
disabled={!isActive}
/>
</td>
<td className="px-3 py-2">
<Input
type="number"
min={0}
step="0.01"
placeholder="None"
value={newForm.minPrice}
onChange={(e) => setNewForm((f) => ({ ...f, minPrice: e.target.value }))}
disabled={!isActive}
/>
</td>
<td className="px-3 py-2">
<Input
type="number"
min={0}
step="0.01"
placeholder="None"
value={newForm.retailPrice}
onChange={(e) => setNewForm((f) => ({ ...f, retailPrice: e.target.value }))}
disabled={!isActive}
/>
</td>
<td className="px-3 py-2">
<Input
type="number"
min={0}
max={100}
step="0.01"
placeholder="None"
value={newForm.agencyMarginPercent}
onChange={(e) => setNewForm((f) => ({ ...f, agencyMarginPercent: e.target.value }))}
disabled={!isActive}
/>
</td>
<td className="px-3 py-2">
<Button type="button" size="sm" disabled={saving || !isActive} onClick={handleAdd}>
<HugeiconsIcon icon={Add01Icon} size={14} strokeWidth={2} />
Add
</Button>
</td>
</tr>
</tbody>
</table>
</div>
)}
</div>
)
}
@@ -1,222 +0,0 @@
'use client'
import { useEffect, useState } from 'react'
import { useForm, zodResolver, z } from '@/lib/form'
import { FormField } from '@/components/shared/form-field'
import { Input } from '@/components/ui/input'
import { Checkbox } from '@/components/ui/checkbox'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
import { Button } from '@/components/ui/button'
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogDescription,
DialogFooter,
} from '@/components/ui/dialog'
import { FieldGroup } from '@/components/ui/field'
import { toast } from '@/components/ui/toast'
import { priceLists as priceListsApi, regions as regionsApi, getApiError, getPaginatedItems } from '@/lib/api/index'
import type { PriceListResponse, RegionResponse } from '@/types/api'
const schema = z.object({
name: z.string().min(1, 'Name is required'),
regionId: z.string().optional(),
effectiveFrom: z.string().min(1, 'Required'),
effectiveTo: z.string().min(1, 'Required'),
isActive: z.boolean(),
})
type FormValues = z.infer<typeof schema>
interface PriceListSheetProps {
open: boolean
onClose: () => void
priceList: PriceListResponse | null
onSuccess: () => void
}
export function PriceListSheet({ open, onClose, priceList, onSuccess }: PriceListSheetProps) {
const isEdit = !!priceList
const [regionsList, setRegionsList] = useState<RegionResponse[]>([])
const form = useForm<FormValues>({
resolver: zodResolver(schema),
defaultValues: { name: '', regionId: '', effectiveFrom: '', effectiveTo: '', isActive: true },
})
useEffect(() => {
if (open) {
form.reset(
priceList
? {
name: priceList.name,
regionId: priceList.regionId ?? '',
effectiveFrom: priceList.effectiveFrom.split('T')[0],
effectiveTo: priceList.effectiveTo.split('T')[0],
isActive: priceList.isActive,
}
: { name: '', regionId: '', effectiveFrom: '', effectiveTo: '', isActive: true }
)
}
}, [open, priceList])
useEffect(() => {
if (open) {
regionsApi.list({ pageSize: 100 }).then((res) => setRegionsList(getPaginatedItems(res))).catch(() => {})
}
}, [open])
async function onSubmit(values: FormValues) {
try {
const body = {
name: values.name,
regionId: values.regionId || null,
effectiveFrom: values.effectiveFrom,
effectiveTo: values.effectiveTo,
isActive: values.isActive,
items: [],
}
if (isEdit) {
await priceListsApi.update(priceList!.id, body)
toast.add({ title: 'Price list updated', type: 'success' })
} else {
await priceListsApi.create(body)
toast.add({ title: 'Price list created', type: 'success' })
}
onSuccess()
onClose()
} catch (err) {
toast.add({ title: 'Failed to save', description: getApiError(err), type: 'error' })
}
}
return (
<Dialog open={open} onOpenChange={(v) => !v && onClose()}>
<DialogContent className="overflow-y-auto sm:max-w-lg">
<DialogHeader>
<DialogTitle>{isEdit ? 'Edit Price List' : 'Add Price List'}</DialogTitle>
<DialogDescription>
{isEdit ? 'Update the price list details below.' : 'Create a new price list.'}
</DialogDescription>
</DialogHeader>
<form onSubmit={form.handleSubmit(onSubmit)} className="flex flex-col gap-6">
<FieldGroup>
<FormField
control={form.control}
name="name"
label="Name"
required
render={({ field, invalid }) => (
<Input
id="name"
name={field.name}
ref={field.ref}
value={field.value as string}
onChange={field.onChange}
onBlur={field.onBlur}
placeholder="e.g. Standard Q1 2025"
aria-invalid={invalid}
/>
)}
/>
<FormField
control={form.control}
name="regionId"
label="Region"
render={({ field }) => (
<Select
value={field.value as string}
onValueChange={field.onChange}
>
<SelectTrigger>
<SelectValue placeholder="All Regions" />
</SelectTrigger>
<SelectContent>
<SelectItem value="">All Regions</SelectItem>
{regionsList.map((r) => (
<SelectItem key={r.id} value={r.id}>{r.name}</SelectItem>
))}
</SelectContent>
</Select>
)}
/>
<FormField
control={form.control}
name="effectiveFrom"
label="Effective From"
required
render={({ field, invalid }) => (
<Input
id="effectiveFrom"
name={field.name}
ref={field.ref}
type="date"
value={field.value as string}
onChange={field.onChange}
onBlur={field.onBlur}
aria-invalid={invalid}
/>
)}
/>
<FormField
control={form.control}
name="effectiveTo"
label="Effective To"
required
render={({ field, invalid }) => (
<Input
id="effectiveTo"
name={field.name}
ref={field.ref}
type="date"
value={field.value as string}
onChange={field.onChange}
onBlur={field.onBlur}
aria-invalid={invalid}
/>
)}
/>
<FormField
control={form.control}
name="isActive"
render={({ field }) => (
<div className="flex items-center gap-2">
<Checkbox
id="isActive"
checked={field.value as boolean}
onCheckedChange={field.onChange}
/>
<label htmlFor="isActive" className="text-sm font-medium cursor-pointer">
Active
</label>
</div>
)}
/>
</FieldGroup>
<DialogFooter>
<Button type="button" variant="outline" onClick={onClose}>
Cancel
</Button>
<Button type="submit" disabled={form.formState.isSubmitting}>
{form.formState.isSubmitting ? 'Saving…' : isEdit ? 'Save Changes' : 'Create Price List'}
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
)
}
@@ -1,6 +1,7 @@
'use client'
import { useState, useEffect, useCallback, useMemo } from 'react'
import { useRouter } from 'next/navigation'
import { HugeiconsIcon } from '@hugeicons/react'
import { Add01Icon } from '@hugeicons/core-free-icons'
@@ -9,30 +10,23 @@ import type { TableConfig } from '@/components/shared/data-table'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { toast } from '@/components/ui/toast'
import { priceLists as priceListsApi, getApiError, getPaginatedItems } from '@/lib/api/index'
import { priceLists as priceListsApi, getApiError } from '@/lib/api/index'
import type { PriceListResponse } from '@/types/api'
import { PriceListSheet } from './price-list-sheet'
const fmtDate = (v: string) =>
new Date(v).toLocaleDateString('en-GB', { day: '2-digit', month: 'short', year: 'numeric' })
export function PriceListsClient() {
const router = useRouter()
const [data, setData] = useState<PriceListResponse[]>([])
const [loading, setLoading] = useState(true)
const [total, setTotal] = useState(0)
const [page, setPage] = useState(1)
const [pageSize, setPageSize] = useState(20)
const [sheetOpen, setSheetOpen] = useState(false)
const [editing, setEditing] = useState<PriceListResponse | null>(null)
const fetchData = useCallback(async (p: number, ps: number) => {
const fetchData = useCallback(async () => {
setLoading(true)
try {
const res = await priceListsApi.list({ page: p, pageSize: ps })
setData(getPaginatedItems(res))
setTotal(res.total)
const items = await priceListsApi.list()
setData(items)
} catch (err) {
toast.add({ title: 'Failed to load price lists', description: getApiError(err), type: 'error' })
} finally {
@@ -41,22 +35,21 @@ export function PriceListsClient() {
}, [])
useEffect(() => {
fetchData(page, pageSize)
}, [fetchData, page, pageSize])
fetchData()
}, [fetchData])
function openCreate() {
setEditing(null)
setSheetOpen(true)
}
function openEdit(priceList: PriceListResponse) {
setEditing(priceList)
setSheetOpen(true)
async function handleDelete(priceList: PriceListResponse) {
try {
await priceListsApi.remove(priceList.id)
toast.add({ title: 'Price list deactivated', type: 'success' })
fetchData()
} catch (err) {
toast.add({ title: 'Failed to deactivate price list', description: getApiError(err), type: 'error' })
}
}
const tableConfig = useMemo<TableConfig<PriceListResponse>>(() => ({
rowKey: 'id',
serverSide: true,
stickyHeader: true,
emptyMessage: 'No price lists yet. Add one to get started.',
pagination: { enabled: true, pageSize, pageSizeOptions: [10, 20, 50] },
@@ -104,10 +97,21 @@ export function PriceListsClient() {
],
rowActions: {
actions: () => [
{
action: 'view',
label: 'View price list',
onClick: (row: PriceListResponse) => router.push(`/pricing/price-lists/${row.id}/view`),
},
{
action: 'edit',
label: 'Edit price list',
onClick: openEdit,
onClick: (row: PriceListResponse) => router.push(`/pricing/price-lists/${row.id}/edit`),
},
{
action: 'cancel',
label: 'Delete price list',
onClick: handleDelete,
visible: (target) => target.isActive,
},
],
},
@@ -122,7 +126,7 @@ export function PriceListsClient() {
Item prices scoped by region and active date range
</p>
</div>
<Button onClick={openCreate}>
<Button onClick={() => router.push('/pricing/price-lists/new')}>
<HugeiconsIcon icon={Add01Icon} size={16} />
Add Price List
</Button>
@@ -132,19 +136,11 @@ export function PriceListsClient() {
config={tableConfig}
data={data}
loading={loading}
totalCount={total}
totalCount={data.length}
onServerSideChange={({ page: p }) => {
setPage(p.page)
setPageSize(p.pageSize)
}}
/>
<PriceListSheet
open={sheetOpen}
onClose={() => setSheetOpen(false)}
priceList={editing}
onSuccess={() => fetchData(page, pageSize)}
/>
</>
)
}
@@ -0,0 +1,172 @@
'use client'
import { useEffect, useState } from 'react'
import { useRouter } from 'next/navigation'
import { HugeiconsIcon } from '@hugeicons/react'
import { ArrowLeft01Icon } from '@hugeicons/core-free-icons'
import { useForm, zodResolver, z } from '@/lib/form'
import { FormField } from '@/components/shared/form-field'
import { Input } from '@/components/ui/input'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
import { Button } from '@/components/ui/button'
import { FieldGroup } from '@/components/ui/field'
import { toast } from '@/components/ui/toast'
import { priceLists as priceListsApi, regions as regionsApi, getApiError, getPaginatedItems } from '@/lib/api/index'
import type { RegionResponse } from '@/types/api'
const schema = z.object({
name: z.string().min(1, 'Name is required'),
regionId: z.string().optional(),
effectiveFrom: z.string().min(1, 'Required'),
effectiveTo: z.string().min(1, 'Required'),
})
type FormValues = z.infer<typeof schema>
export default function NewPriceListPage() {
const router = useRouter()
const [regionsList, setRegionsList] = useState<RegionResponse[]>([])
const form = useForm<FormValues>({
resolver: zodResolver(schema),
defaultValues: { name: '', regionId: '', effectiveFrom: '', effectiveTo: '' },
})
useEffect(() => {
regionsApi.list({ pageSize: 100 }).then((res) => setRegionsList(getPaginatedItems(res))).catch(() => {})
}, [])
async function onSubmit(values: FormValues) {
try {
const created = await priceListsApi.create({
name: values.name,
regionId: values.regionId || null,
effectiveFrom: values.effectiveFrom,
effectiveTo: values.effectiveTo,
isActive: true,
items: [],
})
toast.add({ title: 'Price list created — now add items', type: 'success' })
router.push(`/pricing/price-lists/${created.id}/edit`)
} catch (err) {
toast.add({ title: 'Failed to create price list', description: getApiError(err), type: 'error' })
}
}
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('/pricing/price-lists')}>
<HugeiconsIcon icon={ArrowLeft01Icon} size={16} strokeWidth={2} />
Back
</Button>
<div>
<h1 className="text-2xl font-semibold text-foreground">Add Price List</h1>
<p className="text-sm text-muted-foreground">Create a price list scoped to a region and date range.</p>
</div>
</div>
<form onSubmit={form.handleSubmit(onSubmit)} className="flex flex-col gap-6">
<div className="rounded-2xl border p-6">
<h3 className="mb-4 text-sm font-medium text-foreground">Price List Details</h3>
<FieldGroup>
<div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
<FormField
control={form.control}
name="name"
label="Name"
required
render={({ field, invalid }) => (
<Input
id="name"
name={field.name}
ref={field.ref}
value={field.value as string}
onChange={field.onChange}
onBlur={field.onBlur}
placeholder="e.g. Standard Q1 2025"
aria-invalid={invalid}
/>
)}
/>
<FormField
control={form.control}
name="regionId"
label="Region"
render={({ field }) => (
<Select value={field.value as string} onValueChange={field.onChange}>
<SelectTrigger>
<SelectValue placeholder="All Regions" />
</SelectTrigger>
<SelectContent>
<SelectItem value="">All Regions</SelectItem>
{regionsList.map((r) => (
<SelectItem key={r.id} value={r.id}>{r.name}</SelectItem>
))}
</SelectContent>
</Select>
)}
/>
<div className="grid grid-cols-2 gap-4">
<FormField
control={form.control}
name="effectiveFrom"
label="Effective From"
required
render={({ field, invalid }) => (
<Input
id="effectiveFrom"
name={field.name}
ref={field.ref}
type="date"
value={field.value as string}
onChange={field.onChange}
onBlur={field.onBlur}
aria-invalid={invalid}
/>
)}
/>
<FormField
control={form.control}
name="effectiveTo"
label="Effective To"
required
render={({ field, invalid }) => (
<Input
id="effectiveTo"
name={field.name}
ref={field.ref}
type="date"
value={field.value as string}
onChange={field.onChange}
onBlur={field.onBlur}
aria-invalid={invalid}
/>
)}
/>
</div>
</div>
</FieldGroup>
</div>
<div className="flex justify-end gap-2">
<Button type="button" variant="outline" onClick={() => router.push('/pricing/price-lists')}>
Cancel
</Button>
<Button type="submit" disabled={form.formState.isSubmitting}>
{form.formState.isSubmitting ? 'Creating…' : 'Create & Add Items'}
</Button>
</div>
</form>
</div>
)
}
@@ -0,0 +1,87 @@
'use client'
import { Badge } from '@/components/ui/badge'
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import type { SchemeResponse } from '@/types/api'
const fmtDate = (v: string) =>
new Date(v).toLocaleDateString('en-GB', { day: '2-digit', month: 'short', year: 'numeric' })
interface SchemeViewSheetProps {
open: boolean
onClose: () => void
scheme: SchemeResponse | null
}
function Row({ label, value }: { label: string; value: React.ReactNode }) {
return (
<div className="flex items-center justify-between py-2 text-sm border-b last:border-0">
<span className="text-muted-foreground">{label}</span>
<span className="font-medium text-right">{value}</span>
</div>
)
}
export function SchemeViewSheet({ open, onClose, scheme }: SchemeViewSheetProps) {
if (!scheme) return null
return (
<Dialog open={open} onOpenChange={(v) => !v && onClose()}>
<DialogContent className="overflow-y-auto sm:max-w-lg">
<DialogHeader>
<DialogTitle>Scheme Details</DialogTitle>
</DialogHeader>
<div className="flex flex-col">
<Row label="Name" value={scheme.name} />
<Row label="Rule Type" value={<Badge>{scheme.ruleType}</Badge>} />
<Row label="Region" value={scheme.regionName ?? 'All Regions'} />
<Row label="Eligible Segment" value={scheme.eligibleSegment ?? 'All Segments'} />
<Row label="Status" value={<Badge variant={scheme.isActive ? 'default' : 'secondary'}>{scheme.isActive ? 'Active' : 'Inactive'}</Badge>} />
<Row label="Date Range" value={`${fmtDate(scheme.dateRangeFrom)} → ${fmtDate(scheme.dateRangeTo)}`} />
<Row label="Agent Limit Exempt" value={scheme.agentLimitExempt ? 'Yes' : 'No'} />
{scheme.baselineExpectedSales > 0 && (
<Row label="Baseline Expected Sales" value={`Rs. ${scheme.baselineExpectedSales.toLocaleString()}`} />
)}
{scheme.ruleConfig && <Row label="Rule Config" value={<span className="font-mono text-xs">{scheme.ruleConfig}</span>} />}
{scheme.triggerItemName && (
<Row label="Trigger" value={`${scheme.triggerQty} × ${scheme.triggerItemName}`} />
)}
{scheme.freeItemName && (
<Row label="Free Goods" value={`${scheme.freeQty} × ${scheme.freeItemName}`} />
)}
<Row label="Created" value={fmtDate(scheme.createdAt)} />
</div>
{scheme.bundleComponents.length > 0 && (
<div>
<p className="mb-2 text-sm font-medium text-foreground">Bundle Components</p>
<div className="overflow-x-auto rounded-xl border">
<table className="w-full text-sm">
<thead className="bg-muted/50">
<tr>
<th className="px-3 py-2 text-left font-medium">Item</th>
<th className="px-3 py-2 text-right font-medium">Qty</th>
</tr>
</thead>
<tbody>
{scheme.bundleComponents.map((c) => (
<tr key={c.itemId} className="border-t">
<td className="px-3 py-2">{c.itemName} <span className="ml-1 text-xs text-muted-foreground">({c.sku})</span></td>
<td className="px-3 py-2 text-right">{c.qty}</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
)}
</DialogContent>
</Dialog>
)
}
@@ -9,10 +9,11 @@ import type { TableConfig } from '@/components/shared/data-table'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { toast } from '@/components/ui/toast'
import { schemes as schemesApi, getApiError, getPaginatedItems } from '@/lib/api/index'
import { schemes as schemesApi, getApiError } from '@/lib/api/index'
import type { SchemeResponse, SchemeRuleType } from '@/types/api'
import { SchemeSheet } from './scheme-sheet'
import { SchemeViewSheet } from './scheme-view-sheet'
const fmtDate = (v: string) =>
new Date(v).toLocaleDateString('en-GB', { day: '2-digit', month: 'short', year: 'numeric' })
@@ -27,19 +28,18 @@ const RULE_TYPE_VARIANT: Record<SchemeRuleType, 'default' | 'secondary'> = {
export function SchemesClient() {
const [data, setData] = useState<SchemeResponse[]>([])
const [loading, setLoading] = useState(true)
const [total, setTotal] = useState(0)
const [page, setPage] = useState(1)
const [pageSize, setPageSize] = useState(20)
const [sheetOpen, setSheetOpen] = useState(false)
const [editing, setEditing] = useState<SchemeResponse | null>(null)
const [viewing, setViewing] = useState<SchemeResponse | null>(null)
const [viewOpen, setViewOpen] = useState(false)
const fetchData = useCallback(async (p: number, ps: number) => {
const fetchData = useCallback(async () => {
setLoading(true)
try {
const res = await schemesApi.list({ page: p, pageSize: ps })
setData(getPaginatedItems(res))
setTotal(res.total)
const items = await schemesApi.list()
setData(items)
} catch (err) {
toast.add({ title: 'Failed to load schemes', description: getApiError(err), type: 'error' })
} finally {
@@ -48,8 +48,8 @@ export function SchemesClient() {
}, [])
useEffect(() => {
fetchData(page, pageSize)
}, [fetchData, page, pageSize])
fetchData()
}, [fetchData])
function openCreate() {
setEditing(null)
@@ -61,9 +61,23 @@ export function SchemesClient() {
setSheetOpen(true)
}
function openView(scheme: SchemeResponse) {
setViewing(scheme)
setViewOpen(true)
}
async function handleDelete(scheme: SchemeResponse) {
try {
await schemesApi.remove(scheme.id)
toast.add({ title: 'Scheme deactivated', type: 'success' })
fetchData()
} catch (err) {
toast.add({ title: 'Failed to deactivate scheme', description: getApiError(err), type: 'error' })
}
}
const tableConfig = useMemo<TableConfig<SchemeResponse>>(() => ({
rowKey: 'id',
serverSide: true,
stickyHeader: true,
emptyMessage: 'No schemes yet. Add one to get started.',
pagination: { enabled: true, pageSize, pageSizeOptions: [10, 20, 50] },
@@ -118,11 +132,22 @@ export function SchemesClient() {
],
rowActions: {
actions: () => [
{
action: 'view',
label: 'View scheme',
onClick: openView,
},
{
action: 'edit',
label: 'Edit scheme',
onClick: openEdit,
},
{
action: 'cancel',
label: 'Delete scheme',
onClick: handleDelete,
visible: (target) => target.isActive,
},
],
},
}), [pageSize])
@@ -146,9 +171,8 @@ export function SchemesClient() {
config={tableConfig}
data={data}
loading={loading}
totalCount={total}
totalCount={data.length}
onServerSideChange={({ page: p }) => {
setPage(p.page)
setPageSize(p.pageSize)
}}
/>
@@ -157,7 +181,13 @@ export function SchemesClient() {
open={sheetOpen}
onClose={() => setSheetOpen(false)}
scheme={editing}
onSuccess={() => fetchData(page, pageSize)}
onSuccess={fetchData}
/>
<SchemeViewSheet
open={viewOpen}
onClose={() => setViewOpen(false)}
scheme={viewing}
/>
</>
)
@@ -77,12 +77,12 @@ export default function NewPurchaseOrderPage() {
useEffect(() => {
Promise.all([
suppliersApi.list({ pageSize: 200 }),
suppliersApi.list(),
itemsApi.list({ pageSize: 200 }),
usersApi.list({ pageSize: 200 }),
])
.then(([suppliersRes, itemsRes, usersRes]) => {
setSuppliersList(getPaginatedItems(suppliersRes))
setSuppliersList(suppliersRes)
setItemsList(getPaginatedItems(itemsRes))
setUsersList(getPaginatedItems(usersRes))
})
@@ -26,6 +26,7 @@ import {
rides as ridesApi,
items as itemsApi,
users as usersApi,
priceLists as priceListsApi,
getApiError,
getPaginatedItems,
} from '@/lib/api/index'
@@ -83,6 +84,8 @@ export default function NewSalePage() {
})
const watchedItems = useWatch({ control: form.control, name: 'items' })
const watchedStoreId = useWatch({ control: form.control, name: 'storeId' })
const watchedSaleDate = useWatch({ control: form.control, name: 'saleDate' })
const itemsById = useMemo(
() => new Map(itemsList.map((item) => [item.id, item])),
[itemsList]
@@ -91,6 +94,38 @@ export default function NewSalePage() {
() => watchedItems.reduce((sum, item) => sum + (Number(item.quantity) || 0) * (Number(item.unitPrice) || 0), 0),
[watchedItems]
)
const [priceMinByIndex, setPriceMinByIndex] = useState<Record<number, number | null>>({})
const [priceRetailByIndex, setPriceRetailByIndex] = useState<Record<number, number | null>>({})
async function resolveAndSetPrice(index: number, itemId: string) {
const item = itemsById.get(itemId)
if (watchedStoreId && watchedSaleDate) {
try {
const resolution = await priceListsApi.resolve({ itemId, storeId: watchedStoreId, saleDate: watchedSaleDate })
form.setValue(`items.${index}.unitPrice`, resolution.price, { shouldValidate: true })
setPriceMinByIndex((prev) => ({ ...prev, [index]: resolution.minPrice }))
setPriceRetailByIndex((prev) => ({ ...prev, [index]: resolution.retailPrice }))
return
} catch {
// No resolvable price for this store/date (e.g. store/item not found) — fall back below.
}
}
if (item) {
form.setValue(`items.${index}.unitPrice`, item.salePrice, { shouldValidate: true })
setPriceMinByIndex((prev) => ({ ...prev, [index]: null }))
setPriceRetailByIndex((prev) => ({ ...prev, [index]: null }))
}
}
// Re-resolve every already-selected line's price whenever the store or sale date changes,
// since the applicable price list is scoped by the store's region and the sale date.
useEffect(() => {
if (!watchedStoreId || !watchedSaleDate) return
watchedItems.forEach((line, index) => {
if (line.itemId) resolveAndSetPrice(index, line.itemId)
})
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [watchedStoreId, watchedSaleDate])
useEffect(() => {
Promise.all([
@@ -291,10 +326,7 @@ export default function NewSalePage() {
value={field.value as string}
onValueChange={(value) => {
field.onChange(value)
const nextItem = value ? itemsById.get(value) : undefined
if (nextItem) {
form.setValue(`items.${index}.unitPrice`, nextItem.salePrice, { shouldValidate: true })
}
if (value) resolveAndSetPrice(index, value)
}}
>
<SelectTrigger>
@@ -359,6 +391,16 @@ export default function NewSalePage() {
/>
)}
/>
{priceMinByIndex[index] != null && (
<p className="mt-1 text-xs text-muted-foreground">
Negotiable down to {fmtAmount(priceMinByIndex[index]!)}
</p>
)}
{priceRetailByIndex[index] != null && (
<p className="mt-1 text-xs text-muted-foreground">
Retail price: {fmtAmount(priceRetailByIndex[index]!)}
</p>
)}
</td>
<td className="px-3 py-2 text-right font-medium">{fmtAmount(lineTotal)}</td>
@@ -0,0 +1,50 @@
'use client'
import { Badge } from '@/components/ui/badge'
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import type { SupplierResponse } from '@/types/api'
const fmtDate = (v: string) =>
new Date(v).toLocaleDateString('en-GB', { day: '2-digit', month: 'short', year: 'numeric' })
interface SupplierViewSheetProps {
open: boolean
onClose: () => void
supplier: SupplierResponse | null
}
function Row({ label, value }: { label: string; value: React.ReactNode }) {
return (
<div className="flex items-center justify-between py-2 text-sm border-b last:border-0">
<span className="text-muted-foreground">{label}</span>
<span className="font-medium text-right">{value}</span>
</div>
)
}
export function SupplierViewSheet({ open, onClose, supplier }: SupplierViewSheetProps) {
if (!supplier) return null
return (
<Dialog open={open} onOpenChange={(v) => !v && onClose()}>
<DialogContent className="overflow-y-auto sm:max-w-lg">
<DialogHeader>
<DialogTitle>Supplier Details</DialogTitle>
</DialogHeader>
<div className="flex flex-col">
<Row label="Name" value={supplier.name} />
<Row label="Contact Info" value={supplier.contactInfo ?? '—'} />
<Row label="Status" value={<Badge variant={supplier.isActive ? 'default' : 'secondary'}>{supplier.isActive ? 'Active' : 'Inactive'}</Badge>} />
<Row label="Performance Score" value={supplier.performanceScore != null ? supplier.performanceScore.toFixed(2) : '—'} />
<Row label="Created" value={fmtDate(supplier.createdAt)} />
</div>
</DialogContent>
</Dialog>
)
}
@@ -9,27 +9,27 @@ import type { TableConfig } from '@/components/shared/data-table'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { toast } from '@/components/ui/toast'
import { suppliers as suppliersApi, getApiError, getPaginatedItems } from '@/lib/api/index'
import { suppliers as suppliersApi, getApiError } from '@/lib/api/index'
import type { SupplierResponse } from '@/types/api'
import { SupplierSheet } from './supplier-sheet'
import { SupplierViewSheet } from './supplier-view-sheet'
export function SuppliersClient() {
const [data, setData] = useState<SupplierResponse[]>([])
const [loading, setLoading] = useState(true)
const [total, setTotal] = useState(0)
const [page, setPage] = useState(1)
const [pageSize, setPageSize] = useState(20)
const [sheetOpen, setSheetOpen] = useState(false)
const [editing, setEditing] = useState<SupplierResponse | null>(null)
const [viewing, setViewing] = useState<SupplierResponse | null>(null)
const [viewOpen, setViewOpen] = useState(false)
const fetchData = useCallback(async (p: number, ps: number) => {
const fetchData = useCallback(async () => {
setLoading(true)
try {
const res = await suppliersApi.list({ page: p, pageSize: ps })
setData(getPaginatedItems(res))
setTotal(res.total)
const items = await suppliersApi.list()
setData(items)
} catch (err) {
toast.add({ title: 'Failed to load suppliers', description: getApiError(err), type: 'error' })
} finally {
@@ -38,8 +38,8 @@ export function SuppliersClient() {
}, [])
useEffect(() => {
fetchData(page, pageSize)
}, [fetchData, page, pageSize])
fetchData()
}, [fetchData])
function openCreate() {
setEditing(null)
@@ -51,9 +51,23 @@ export function SuppliersClient() {
setSheetOpen(true)
}
function openView(supplier: SupplierResponse) {
setViewing(supplier)
setViewOpen(true)
}
async function handleDelete(supplier: SupplierResponse) {
try {
await suppliersApi.remove(supplier.id)
toast.add({ title: 'Supplier deactivated', type: 'success' })
fetchData()
} catch (err) {
toast.add({ title: 'Failed to deactivate supplier', description: getApiError(err), type: 'error' })
}
}
const tableConfig = useMemo<TableConfig<SupplierResponse>>(() => ({
rowKey: 'id',
serverSide: true,
stickyHeader: true,
emptyMessage: 'No suppliers yet. Add one to get started.',
pagination: { enabled: true, pageSize, pageSizeOptions: [10, 20, 50] },
@@ -101,11 +115,22 @@ export function SuppliersClient() {
],
rowActions: {
actions: () => [
{
action: 'view',
label: 'View supplier',
onClick: openView,
},
{
action: 'edit',
label: 'Edit supplier',
onClick: openEdit,
},
{
action: 'cancel',
label: 'Delete supplier',
onClick: handleDelete,
visible: (target) => target.isActive,
},
],
},
}), [pageSize])
@@ -129,9 +154,8 @@ export function SuppliersClient() {
config={tableConfig}
data={data}
loading={loading}
totalCount={total}
totalCount={data.length}
onServerSideChange={({ page: p }) => {
setPage(p.page)
setPageSize(p.pageSize)
}}
/>
@@ -140,7 +164,13 @@ export function SuppliersClient() {
open={sheetOpen}
onClose={() => setSheetOpen(false)}
supplier={editing}
onSuccess={() => fetchData(page, pageSize)}
onSuccess={fetchData}
/>
<SupplierViewSheet
open={viewOpen}
onClose={() => setViewOpen(false)}
supplier={viewing}
/>
</>
)