From 617c0713a196a9aff86adccda4330a172617765f Mon Sep 17 00:00:00 2001
From: DeepnaPooja
Date: Wed, 9 Sep 2026 16:31:35 +0530
Subject: [PATCH] remove PriceListSheet component and integrate new price list
creation page
---
.../pricing/price-lists/[id]/edit/page.tsx | 224 ++++++++++++
.../pricing/price-lists/[id]/view/page.tsx | 114 ++++++
.../_components/price-list-items-section.tsx | 334 ++++++++++++++++++
.../_components/price-list-sheet.tsx | 222 ------------
.../_components/price-lists-client.tsx | 64 ++--
.../pricing/price-lists/new/page.tsx | 172 +++++++++
.../schemes/_components/scheme-view-sheet.tsx | 87 +++++
.../schemes/_components/schemes-client.tsx | 56 ++-
.../procurement/orders/new/page.tsx | 4 +-
.../app/(dashboard)/sales/new/page.tsx | 50 ++-
.../_components/supplier-view-sheet.tsx | 50 +++
.../_components/suppliers-client.tsx | 56 ++-
12 files changed, 1145 insertions(+), 288 deletions(-)
create mode 100644 distribution_fe/app/(dashboard)/pricing/price-lists/[id]/edit/page.tsx
create mode 100644 distribution_fe/app/(dashboard)/pricing/price-lists/[id]/view/page.tsx
create mode 100644 distribution_fe/app/(dashboard)/pricing/price-lists/_components/price-list-items-section.tsx
delete mode 100644 distribution_fe/app/(dashboard)/pricing/price-lists/_components/price-list-sheet.tsx
create mode 100644 distribution_fe/app/(dashboard)/pricing/price-lists/new/page.tsx
create mode 100644 distribution_fe/app/(dashboard)/pricing/schemes/_components/scheme-view-sheet.tsx
create mode 100644 distribution_fe/app/(dashboard)/suppliers/_components/supplier-view-sheet.tsx
diff --git a/distribution_fe/app/(dashboard)/pricing/price-lists/[id]/edit/page.tsx b/distribution_fe/app/(dashboard)/pricing/price-lists/[id]/edit/page.tsx
new file mode 100644
index 0000000..98a2911
--- /dev/null
+++ b/distribution_fe/app/(dashboard)/pricing/price-lists/[id]/edit/page.tsx
@@ -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
+
+export default function EditPriceListPage() {
+ const router = useRouter()
+ const params = useParams<{ id: string }>()
+ const [priceList, setPriceList] = useState(null)
+ const [regionsList, setRegionsList] = useState([])
+ const [loading, setLoading] = useState(true)
+ const [itemsSectionKey, setItemsSectionKey] = useState(0)
+
+ const form = useForm({
+ 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 Loading…
+ }
+
+ if (!priceList) {
+ return Price list not found.
+ }
+
+ return (
+
+
+
+
+
Edit Price List
+
{priceList.name}
+
+
+
+
+
+
+
+ )
+}
diff --git a/distribution_fe/app/(dashboard)/pricing/price-lists/[id]/view/page.tsx b/distribution_fe/app/(dashboard)/pricing/price-lists/[id]/view/page.tsx
new file mode 100644
index 0000000..f3b9b4a
--- /dev/null
+++ b/distribution_fe/app/(dashboard)/pricing/price-lists/[id]/view/page.tsx
@@ -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 (
+
+ {label}
+ {value}
+
+ )
+}
+
+export default function ViewPriceListPage() {
+ const router = useRouter()
+ const params = useParams<{ id: string }>()
+ const [priceList, setPriceList] = useState(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 Loading…
+ }
+
+ if (!priceList) {
+ return Price list not found.
+ }
+
+ return (
+
+
+
+
+
+
{priceList.name}
+
Price List Details
+
+
+
+
+
+
+
+
+ {priceList.isActive ? 'Active' : 'Inactive'}} />
+
+
+
+
+
+
+
Items
+
+ {priceList.items.length === 0 ? (
+
No items priced on this list yet.
+ ) : (
+
+
+
+
+ | Item |
+ Price |
+ Min Price |
+ Retail Price |
+ Agency % |
+
+
+
+ {priceList.items.map((item) => (
+
+ |
+ {item.itemName}
+ ({item.sku})
+ |
+ {fmtAmount(item.price)} |
+ {item.minPrice != null ? fmtAmount(item.minPrice) : '—'} |
+ {item.retailPrice != null ? fmtAmount(item.retailPrice) : '—'} |
+ {item.agencyMarginPercent != null ? `${item.agencyMarginPercent}%` : '—'} |
+
+ ))}
+
+
+
+ )}
+
+
+ )
+}
diff --git a/distribution_fe/app/(dashboard)/pricing/price-lists/_components/price-list-items-section.tsx b/distribution_fe/app/(dashboard)/pricing/price-lists/_components/price-list-items-section.tsx
new file mode 100644
index 0000000..b19e4d0
--- /dev/null
+++ b/distribution_fe/app/(dashboard)/pricing/price-lists/_components/price-list-items-section.tsx
@@ -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([])
+ const [itemsList, setItemsList] = useState([])
+ const [isActive, setIsActive] = useState(true)
+ const [loading, setLoading] = useState(true)
+ const [editingSku, setEditingSku] = useState(null)
+ const [editForm, setEditForm] = useState(emptyForm())
+
+ const [newItemId, setNewItemId] = useState('')
+ const [newForm, setNewForm] = useState(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 (
+
+
Items
+
+ {!loading && !isActive && (
+
+ This price list is Inactive — reactivate it above (check "Active" and Save) before adding or changing item prices.
+
+ )}
+
+ {loading ? (
+
Loading items…
+ ) : (
+
+ )}
+
+ )
+}
diff --git a/distribution_fe/app/(dashboard)/pricing/price-lists/_components/price-list-sheet.tsx b/distribution_fe/app/(dashboard)/pricing/price-lists/_components/price-list-sheet.tsx
deleted file mode 100644
index 9ec5f14..0000000
--- a/distribution_fe/app/(dashboard)/pricing/price-lists/_components/price-list-sheet.tsx
+++ /dev/null
@@ -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
-
-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([])
-
- const form = useForm({
- 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 (
-
- )
-}
diff --git a/distribution_fe/app/(dashboard)/pricing/price-lists/_components/price-lists-client.tsx b/distribution_fe/app/(dashboard)/pricing/price-lists/_components/price-lists-client.tsx
index b464396..989bfbe 100644
--- a/distribution_fe/app/(dashboard)/pricing/price-lists/_components/price-lists-client.tsx
+++ b/distribution_fe/app/(dashboard)/pricing/price-lists/_components/price-lists-client.tsx
@@ -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([])
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(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>(() => ({
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
-