From 92f1aadb818787356b2c40fb70dd5b02607e9ca0 Mon Sep 17 00:00:00 2001
From: DeepnaPooja
Date: Tue, 6 Oct 2026 13:28:37 +0530
Subject: [PATCH] Add item and GRN tracking forms
---
.../catalogue/items/_components/item-form.tsx | 406 +++++++++++++++++
.../items/_components/item-sheet.tsx | 423 +-----------------
.../items/_components/item-view-sheet.tsx | 21 +
.../items/_components/items-client.tsx | 23 +-
.../(dashboard)/catalogue/items/new/page.tsx | 32 ++
.../_components/product-capabilities-card.tsx | 220 +++++++++
.../app/(dashboard)/configuration/page.tsx | 44 +-
.../configuration/products/page.tsx | 234 ----------
.../grn/_components/grn-client.tsx | 30 +-
.../grn/_components/grn-tracking.tsx | 324 ++++++++++++++
.../grn/_components/grn-view-sheet.tsx | 28 +-
.../procurement/grn/direct/new/page.tsx | 316 +++++++++++++
.../grn-sheet.tsx => po/new/page.tsx} | 153 +++++--
distribution_fe/config/nav.ts | 2 +-
distribution_fe/lib/api/procurement.ts | 28 +-
distribution_fe/types/api.ts | 16 +-
16 files changed, 1536 insertions(+), 764 deletions(-)
create mode 100644 distribution_fe/app/(dashboard)/catalogue/items/_components/item-form.tsx
create mode 100644 distribution_fe/app/(dashboard)/catalogue/items/new/page.tsx
create mode 100644 distribution_fe/app/(dashboard)/configuration/_components/product-capabilities-card.tsx
delete mode 100644 distribution_fe/app/(dashboard)/configuration/products/page.tsx
create mode 100644 distribution_fe/app/(dashboard)/procurement/grn/_components/grn-tracking.tsx
create mode 100644 distribution_fe/app/(dashboard)/procurement/grn/direct/new/page.tsx
rename distribution_fe/app/(dashboard)/procurement/grn/{_components/grn-sheet.tsx => po/new/page.tsx} (70%)
diff --git a/distribution_fe/app/(dashboard)/catalogue/items/_components/item-form.tsx b/distribution_fe/app/(dashboard)/catalogue/items/_components/item-form.tsx
new file mode 100644
index 0000000..77122b0
--- /dev/null
+++ b/distribution_fe/app/(dashboard)/catalogue/items/_components/item-form.tsx
@@ -0,0 +1,406 @@
+'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 { Button } from '@/components/ui/button'
+import { FieldGroup } from '@/components/ui/field'
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from '@/components/ui/select'
+import { toast } from '@/components/ui/toast'
+import { items as itemsApi, categories as categoriesApi, brands as brandsApi, uoms as uomsApi, sysConfig, ITEM_TRACKING_KEYS, getApiError } from '@/lib/api/index'
+import type { ItemSummaryResponse, CategoryResponse, BrandResponse, UomResponse } from '@/types/api'
+
+const schema = z.object({
+ sku: z.string().min(1, 'SKU is required'),
+ name: z.string().min(1, 'Name is required'),
+ categoryId: z.string().min(1, 'Category is required'),
+ brandId: z.string().optional(),
+ baseUomId: z.string().min(1, 'Base UOM is required'),
+ salePrice: z.number().min(0, 'Sale price must be 0 or more'),
+ tracksSerialNumbers: z.boolean(),
+ tracksBatches: z.boolean(),
+ tracksExpiryDate: z.boolean(),
+ hasWarranty: z.boolean(),
+ warrantyPeriodDays: z.number().min(0).optional(),
+ allowCustomWarrantyPeriod: z.boolean(),
+ isActive: z.boolean(),
+})
+
+type FormValues = z.infer
+
+interface TrackingToggles {
+ serialNumberTracking: boolean
+ batchTracking: boolean
+ expiryDateTracking: boolean
+ warrantyTracking: boolean
+ customWarrantyOverride: boolean
+}
+
+const NO_TRACKING_ENABLED: TrackingToggles = {
+ serialNumberTracking: false,
+ batchTracking: false,
+ expiryDateTracking: false,
+ warrantyTracking: false,
+ customWarrantyOverride: false,
+}
+
+const EMPTY_VALUES: FormValues = {
+ sku: '',
+ name: '',
+ categoryId: '',
+ brandId: undefined,
+ baseUomId: '',
+ salePrice: 0,
+ tracksSerialNumbers: false,
+ tracksBatches: false,
+ tracksExpiryDate: false,
+ hasWarranty: false,
+ warrantyPeriodDays: undefined,
+ allowCustomWarrantyPeriod: false,
+ isActive: true,
+}
+
+interface ItemFormProps {
+ item: ItemSummaryResponse | null
+ onSuccess: () => void
+ onCancel: () => void
+ twoColumn?: boolean
+}
+
+export function ItemForm({ item, onSuccess, onCancel, twoColumn = false }: ItemFormProps) {
+ const isEdit = !!item
+
+ const [categories, setCategories] = useState([])
+ const [brandsList, setBrandsList] = useState([])
+ const [uomsList, setUomsList] = useState([])
+ const [tracking, setTracking] = useState(NO_TRACKING_ENABLED)
+
+ const form = useForm({
+ resolver: zodResolver(schema),
+ defaultValues: EMPTY_VALUES,
+ })
+
+ useEffect(() => {
+ Promise.all([
+ categoriesApi.list(),
+ brandsApi.list(),
+ uomsApi.list(),
+ sysConfig.getAll(),
+ ]).then(([cats, brandsRes, uomsRes, configs]) => {
+ setCategories(cats)
+ setBrandsList(brandsRes)
+ setUomsList(uomsRes)
+ const byKey = Object.fromEntries(configs.map(c => [c.key, c.value]))
+ setTracking({
+ serialNumberTracking: byKey[ITEM_TRACKING_KEYS.serialNumberTracking] === 'true',
+ batchTracking: byKey[ITEM_TRACKING_KEYS.batchTracking] === 'true',
+ expiryDateTracking: byKey[ITEM_TRACKING_KEYS.expiryDateTracking] === 'true',
+ warrantyTracking: byKey[ITEM_TRACKING_KEYS.warrantyTracking] === 'true',
+ customWarrantyOverride: byKey[ITEM_TRACKING_KEYS.customWarrantyOverride] === 'true',
+ })
+ }).catch((err) => {
+ toast.add({ title: 'Failed to load reference data', description: getApiError(err), type: 'error' })
+ })
+
+ form.reset(
+ item
+ ? {
+ sku: item.sku,
+ name: item.name,
+ categoryId: item.categoryId,
+ brandId: item.brandId ?? undefined,
+ baseUomId: item.baseUomId,
+ salePrice: item.salePrice,
+ tracksSerialNumbers: item.tracksSerialNumbers,
+ tracksBatches: item.tracksBatches,
+ tracksExpiryDate: item.tracksExpiryDate,
+ hasWarranty: item.hasWarranty,
+ warrantyPeriodDays: item.warrantyPeriodDays ?? undefined,
+ allowCustomWarrantyPeriod: item.allowCustomWarrantyPeriod,
+ isActive: item.isActive,
+ }
+ : EMPTY_VALUES
+ )
+ }, [item])
+
+ const hasWarranty = form.watch('hasWarranty')
+
+ async function onSubmit(values: FormValues) {
+ try {
+ const body = {
+ ...values,
+ brandId: values.brandId || null,
+ warrantyPeriodDays: values.hasWarranty && values.allowCustomWarrantyPeriod ? values.warrantyPeriodDays ?? null : null,
+ }
+ if (isEdit) {
+ await itemsApi.update(item!.id, body)
+ toast.add({ title: 'Product updated', type: 'success' })
+ } else {
+ await itemsApi.create(body)
+ toast.add({ title: 'Product created', type: 'success' })
+ }
+ onSuccess()
+ } catch (err) {
+ toast.add({ title: 'Failed to save', description: getApiError(err), type: 'error' })
+ }
+ }
+
+ return (
+
+ )
+}
diff --git a/distribution_fe/app/(dashboard)/catalogue/items/_components/item-sheet.tsx b/distribution_fe/app/(dashboard)/catalogue/items/_components/item-sheet.tsx
index b0908c5..17511e4 100644
--- a/distribution_fe/app/(dashboard)/catalogue/items/_components/item-sheet.tsx
+++ b/distribution_fe/app/(dashboard)/catalogue/items/_components/item-sheet.tsx
@@ -1,437 +1,40 @@
'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 { Button } from '@/components/ui/button'
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogDescription,
- DialogFooter,
} from '@/components/ui/dialog'
-import { FieldGroup } from '@/components/ui/field'
-import {
- Select,
- SelectContent,
- SelectItem,
- SelectTrigger,
- SelectValue,
-} from '@/components/ui/select'
-import { toast } from '@/components/ui/toast'
-import { items as itemsApi, categories as categoriesApi, brands as brandsApi, uoms as uomsApi, sysConfig, ITEM_TRACKING_KEYS, getApiError } from '@/lib/api/index'
-import type { ItemSummaryResponse, CategoryResponse, BrandResponse, UomResponse } from '@/types/api'
+import type { ItemSummaryResponse } from '@/types/api'
-const schema = z.object({
- sku: z.string().min(1, 'SKU is required'),
- name: z.string().min(1, 'Name is required'),
- categoryId: z.string().min(1, 'Category is required'),
- brandId: z.string().optional(),
- baseUomId: z.string().min(1, 'Base UOM is required'),
- salePrice: z.number().min(0, 'Sale price must be 0 or more'),
- tracksSerialNumbers: z.boolean(),
- tracksBatches: z.boolean(),
- tracksExpiryDate: z.boolean(),
- hasWarranty: z.boolean(),
- warrantyPeriodDays: z.number().min(0).optional(),
- allowCustomWarrantyPeriod: z.boolean(),
- isActive: z.boolean(),
-})
-
-type FormValues = z.infer
-
-interface TrackingToggles {
- serialNumberTracking: boolean
- batchTracking: boolean
- expiryDateTracking: boolean
- warrantyTracking: boolean
- customWarrantyOverride: boolean
-}
-
-const NO_TRACKING_ENABLED: TrackingToggles = {
- serialNumberTracking: false,
- batchTracking: false,
- expiryDateTracking: false,
- warrantyTracking: false,
- customWarrantyOverride: false,
-}
+import { ItemForm } from './item-form'
interface ItemSheetProps {
open: boolean
onClose: () => void
- item: ItemSummaryResponse | null
+ item: ItemSummaryResponse
onSuccess: () => void
}
export function ItemSheet({ open, onClose, item, onSuccess }: ItemSheetProps) {
- const isEdit = !!item
-
- const [categories, setCategories] = useState([])
- const [brandsList, setBrandsList] = useState([])
- const [uomsList, setUomsList] = useState([])
- const [tracking, setTracking] = useState(NO_TRACKING_ENABLED)
-
- const form = useForm({
- resolver: zodResolver(schema),
- defaultValues: {
- sku: '',
- name: '',
- categoryId: '',
- brandId: undefined,
- baseUomId: '',
- salePrice: 0,
- tracksSerialNumbers: false,
- tracksBatches: false,
- tracksExpiryDate: false,
- hasWarranty: false,
- warrantyPeriodDays: undefined,
- allowCustomWarrantyPeriod: false,
- isActive: true,
- },
- })
-
- useEffect(() => {
- if (open) {
- Promise.all([
- categoriesApi.list(),
- brandsApi.list(),
- uomsApi.list(),
- sysConfig.getAll(),
- ]).then(([cats, brandsRes, uomsRes, configs]) => {
- setCategories(cats)
- setBrandsList(brandsRes)
- setUomsList(uomsRes)
- const byKey = Object.fromEntries(configs.map(c => [c.key, c.value]))
- setTracking({
- serialNumberTracking: byKey[ITEM_TRACKING_KEYS.serialNumberTracking] === 'true',
- batchTracking: byKey[ITEM_TRACKING_KEYS.batchTracking] === 'true',
- expiryDateTracking: byKey[ITEM_TRACKING_KEYS.expiryDateTracking] === 'true',
- warrantyTracking: byKey[ITEM_TRACKING_KEYS.warrantyTracking] === 'true',
- customWarrantyOverride: byKey[ITEM_TRACKING_KEYS.customWarrantyOverride] === 'true',
- })
- }).catch((err) => {
- toast.add({ title: 'Failed to load reference data', description: getApiError(err), type: 'error' })
- })
-
- form.reset(
- item
- ? {
- sku: item.sku,
- name: item.name,
- categoryId: item.categoryId,
- brandId: item.brandId ?? undefined,
- baseUomId: item.baseUomId,
- salePrice: item.salePrice,
- tracksSerialNumbers: item.tracksSerialNumbers,
- tracksBatches: item.tracksBatches,
- tracksExpiryDate: item.tracksExpiryDate,
- hasWarranty: item.hasWarranty,
- warrantyPeriodDays: item.warrantyPeriodDays ?? undefined,
- allowCustomWarrantyPeriod: item.allowCustomWarrantyPeriod,
- isActive: item.isActive,
- }
- : {
- sku: '',
- name: '',
- categoryId: '',
- brandId: undefined,
- baseUomId: '',
- salePrice: 0,
- tracksSerialNumbers: false,
- tracksBatches: false,
- tracksExpiryDate: false,
- hasWarranty: false,
- warrantyPeriodDays: undefined,
- allowCustomWarrantyPeriod: false,
- isActive: true,
- }
- )
- }
- }, [open, item])
-
- const hasWarranty = form.watch('hasWarranty')
-
- async function onSubmit(values: FormValues) {
- try {
- const body = {
- ...values,
- brandId: values.brandId || null,
- warrantyPeriodDays: values.hasWarranty && values.allowCustomWarrantyPeriod ? values.warrantyPeriodDays ?? null : null,
- }
- if (isEdit) {
- await itemsApi.update(item!.id, body)
- toast.add({ title: 'Product updated', type: 'success' })
- } else {
- await itemsApi.create(body)
- toast.add({ title: 'Product 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)/catalogue/items/_components/item-view-sheet.tsx b/distribution_fe/app/(dashboard)/catalogue/items/_components/item-view-sheet.tsx
index 27a1fa0..9e047b8 100644
--- a/distribution_fe/app/(dashboard)/catalogue/items/_components/item-view-sheet.tsx
+++ b/distribution_fe/app/(dashboard)/catalogue/items/_components/item-view-sheet.tsx
@@ -44,6 +44,27 @@ export function ItemViewSheet({ open, onClose, item }: ItemViewSheetProps) {
+
+ {item.tracksSerialNumbers && Serial}
+ {item.tracksBatches && Batch}
+ {item.tracksExpiryDate && Expiry}
+ {item.hasWarranty && (
+
+ Warranty {item.allowCustomWarrantyPeriod && item.warrantyPeriodDays != null
+ ? `${item.warrantyPeriodDays}d`
+ : '(default period)'}
+
+ )}
+
+ ) : (
+ '—'
+ )
+ }
+ />
{item.isActive ? 'Active' : 'Inactive'}} />
diff --git a/distribution_fe/app/(dashboard)/catalogue/items/_components/items-client.tsx b/distribution_fe/app/(dashboard)/catalogue/items/_components/items-client.tsx
index 9d11f7c..1f5a72f 100644
--- a/distribution_fe/app/(dashboard)/catalogue/items/_components/items-client.tsx
+++ b/distribution_fe/app/(dashboard)/catalogue/items/_components/items-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'
@@ -16,6 +17,7 @@ import { ItemSheet } from './item-sheet'
import { ItemViewSheet } from './item-view-sheet'
export function ItemsClient() {
+ const router = useRouter()
const [data, setData] = useState([])
const [loading, setLoading] = useState(true)
const [total, setTotal] = useState(0)
@@ -44,11 +46,6 @@ export function ItemsClient() {
fetchData(page, pageSize)
}, [fetchData, page, pageSize])
- function openCreate() {
- setEditing(null)
- setSheetOpen(true)
- }
-
function openEdit(item: ItemSummaryResponse) {
setEditing(item)
setSheetOpen(true)
@@ -164,7 +161,7 @@ export function ItemsClient() {
Products available for sale and distribution
-