From 77c596e3a1c7fb93c1bc9bb697e502c5f8a2a649 Mon Sep 17 00:00:00 2001 From: suryacp23 Date: Wed, 22 Jul 2026 19:00:00 +0530 Subject: [PATCH] form grid implemented --- src/components/forms/DynamicForm.tsx | 231 ++++- src/components/forms/fields/GridInput.tsx | 86 +- .../forms/fields/SmartGridField.tsx | 960 ++++++++++++------ src/components/forms/fields/TextField.tsx | 10 +- 4 files changed, 881 insertions(+), 406 deletions(-) diff --git a/src/components/forms/DynamicForm.tsx b/src/components/forms/DynamicForm.tsx index f2581f2..aba237f 100644 --- a/src/components/forms/DynamicForm.tsx +++ b/src/components/forms/DynamicForm.tsx @@ -1,6 +1,6 @@ import { useState, useEffect, useRef } from 'react'; import type { ZinoClient } from '../../api/client'; -import type { FormScreenResponse } from '../../api/types'; +import type { FormScreenResponse, FormScreenField } from '../../api/types'; import { Button } from '../buttons/Button'; import { Spinner } from '../reusable/Spinner'; import { @@ -121,10 +121,14 @@ export function DynamicForm({ client, activityId: initialActivityId, instanceId: const [values, setValues] = useState>({}); const [submitting, setSubmitting] = useState(false); const [submitError, setSubmitError] = useState(null); + const [showErrors, setShowErrors] = useState(false); const clickedActionRef = useRef(null); const handleFieldChange = (fieldId: string, newVal: unknown, fullRow?: any) => { + if (submitError) { + setSubmitError(null); + } setValues(prev => { const next = { ...prev, [fieldId]: newVal }; if (fullRow) { @@ -151,8 +155,11 @@ export function DynamicForm({ client, activityId: initialActivityId, instanceId: )?.id || 'row_kgs'; newVal.forEach(row => { - totalBags += Number(row[bagsColId]) || 0; - totalKgs += Number(row[rowKgsColId]) || 0; + const bags = Number(row[bagsColId] ?? row.bags ?? row.quantity) || 0; + const sku = Number(row.sku) || 0; + const rowKgs = Number(row[rowKgsColId] ?? row.row_kgs) || (bags * sku); + totalBags += bags; + totalKgs += rowKgs; }); const tbField = schema?.fields.find(f => f.id === 'total_bags' || getBaseId(f.id) === 'total_bags' || f.mapped_workflow_field === 'total_bags'); @@ -184,14 +191,191 @@ export function DynamicForm({ client, activityId: initialActivityId, instanceId: const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); - setSubmitting(true); + setShowErrors(true); setSubmitError(null); + + const finalValues = { ...values }; + if (actionField && clickedActionRef.current) { + finalValues[actionField.id] = clickedActionRef.current; + } + + // 1. Pre-submit consolidation for grid fields & total fields + normalFields.forEach((f) => { + if (f.data_type.startsWith('grid')) { + const rawGrid = (finalValues[f.id] as Record[]) || []; + if (rawGrid.length > 0) { + const merged: Record[] = []; + const indexMap = new Map(); + + for (const r of rawGrid) { + const cat = String(r.category || r.product_category || '').trim().toLowerCase(); + const prod = String(r.productName || r.product_name || r.product || '').trim().toLowerCase(); + const currentBags = Number(r.bags || r.quantity || 0); + + if (cat && prod) { + const key = `${cat}::${prod}`; + if (indexMap.has(key)) { + const targetIdx = indexMap.get(key)!; + const existingRow = merged[targetIdx]; + const existingBags = Number(existingRow.bags || existingRow.quantity || 0); + const addedBags = currentBags > 0 ? currentBags : 1; + const newBags = existingBags + addedBags; + const skuVal = Number(existingRow.sku || r.sku || 0); + + merged[targetIdx] = { + ...existingRow, + bags: newBags, + quantity: newBags, + row_kgs: skuVal * newBags, + }; + continue; + } else { + indexMap.set(key, merged.length); + } + } + merged.push(r); + } + + // Clean virtual keys + const cleanGrid = merged.map((r) => { + const clean: Record = {}; + Object.keys(r).forEach((k) => { + if (k !== '_row_total_kgs' && k !== 'row_kgs' && k !== 'row_total') { + clean[k] = r[k]; + } + }); + return clean; + }); + + finalValues[f.id] = cleanGrid; + + // Recalculate total_bags and total_kgs + let totalB = 0; + let totalK = 0; + cleanGrid.forEach((r) => { + const b = Number(r.bags || r.quantity) || 0; + const sku = Number(r.sku) || 0; + totalB += b; + totalK += b * sku; + }); + + const getBaseId = (id: string) => id.replace(/_\d+$/, ''); + normalFields.forEach((tf) => { + const baseId = getBaseId(tf.id); + const mapped = (tf.mapped_workflow_field || '').toLowerCase(); + if (baseId === 'total_bags' || mapped === 'total_bags' || tf.id === 'total_bags') { + finalValues[tf.id] = Math.round(totalB); + } + if (baseId === 'total_kgs' || mapped === 'total_kgs' || tf.id === 'total_kgs') { + finalValues[tf.id] = Math.round(totalK); + } + }); + } + } + }); + + // 2. Validate grid fields (Order Details) + const extractGridRowDetails = (r: Record, columns?: FormScreenField[]) => { + let cat = String(r.category || r.product_category || r.cat || '').trim(); + let prod = String(r.productName || r.product_name || r.product || r.name || '').trim(); + + let bagsNum = 0; + if (r.bags !== undefined && r.bags !== null && r.bags !== '') bagsNum = Number(r.bags); + else if (r.quantity !== undefined && r.quantity !== null && r.quantity !== '') bagsNum = Number(r.quantity); + else if (r.actual_potential !== undefined && r.actual_potential !== null && r.actual_potential !== '') bagsNum = Number(r.actual_potential); + else if (r.store_potential !== undefined && r.store_potential !== null && r.store_potential !== '') bagsNum = Number(r.store_potential); + + if (columns && columns.length > 0) { + for (const c of columns) { + const idL = c.id.toLowerCase(); + const nL = (c.name || '').toLowerCase(); + const mL = (c.mapped_workflow_field || '').toLowerCase(); + + if (!cat && (idL.includes('category') || nL.includes('category') || mL.includes('category'))) { + cat = String(r[c.id] || '').trim(); + } + if (!prod && (idL.includes('product') || nL.includes('product') || mL.includes('product') || idL.includes('name') || nL.includes('name'))) { + prod = String(r[c.id] || '').trim(); + } + if (bagsNum <= 0 && (idL.includes('bags') || nL.includes('bags') || mL.includes('bags') || idL.includes('quantity') || nL.includes('quantity') || idL.includes('potential') || nL.includes('potential'))) { + const val = Number(r[c.id]); + if (!isNaN(val) && val > 0) bagsNum = val; + } + } + } + + if (bagsNum <= 0) { + for (const k of Object.keys(r)) { + const kL = k.toLowerCase(); + if ((kL.includes('bags') || kL.includes('quantity') || kL.includes('potential')) && !kL.includes('total') && !kL.includes('kgs')) { + const val = Number(r[k]); + if (!isNaN(val) && val > 0) { + bagsNum = val; + break; + } + } + } + } + + return { cat, prod, bags: isNaN(bagsNum) ? 0 : bagsNum }; + }; + + for (const f of normalFields) { + if (f.data_type.startsWith('grid')) { + const gridRows = (finalValues[f.id] as Record[]) || []; + + // Check if grid has at least one valid row + const validRows = gridRows.filter((r) => { + const { cat, prod, bags } = extractGridRowDetails(r, f.columns); + return cat && prod && bags > 0; + }); + + if (validRows.length === 0) { + setSubmitError('Please add at least one complete product with Category, Product Name, and Bags.'); + return; + } + + // Check if any row is partially filled + const hasIncompleteRow = gridRows.some((r) => { + const { cat, prod, bags } = extractGridRowDetails(r, f.columns); + + if (!cat && !prod && bags <= 0) return false; + if (cat && (!prod || bags <= 0)) return true; + if (prod && (!cat || bags <= 0)) return true; + if (bags > 0 && (!cat || !prod)) return true; + return false; + }); + + if (hasIncompleteRow) { + setSubmitError('Please complete all required product details (Product Name and Bags) before submitting.'); + return; + } + } + } + + // 3. Validate mandatory normal fields using finalValues + const getBaseId = (id: string) => id.replace(/_\d+$/, ''); + for (const f of normalFields) { + if (f.mandatory) { + if (f.data_type.startsWith('grid')) continue; + const baseId = getBaseId(f.id); + const mapped = (f.mapped_workflow_field || '').toLowerCase(); + if (baseId === 'total_bags' || mapped === 'total_bags' || f.id === 'total_bags' || + baseId === 'total_kgs' || mapped === 'total_kgs' || f.id === 'total_kgs') { + continue; // Total fields are calculated automatically from grid + } + + const v = finalValues[f.id]; + if (v === undefined || v === null || String(v).trim() === '') { + setSubmitError(`Please fill in required field: ${f.name}`); + return; + } + } + } + + setSubmitting(true); try { const payload: Record = {}; - const finalValues = { ...values }; - if (actionField && clickedActionRef.current) { - finalValues[actionField.id] = clickedActionRef.current; - } for (const f of fields) { const val = finalValues[f.id]; @@ -341,9 +525,31 @@ export function DynamicForm({ client, activityId: initialActivityId, instanceId: } }; + const getBaseIdForField = (id: string) => id.replace(/_\d+$/, ''); + const hasGridField = normalFields.some(f => f.data_type.startsWith('grid')); + + const tbField = normalFields.find(f => f.id === 'total_bags' || getBaseIdForField(f.id) === 'total_bags' || f.mapped_workflow_field === 'total_bags'); + const tkField = normalFields.find(f => f.id === 'total_kgs' || getBaseIdForField(f.id) === 'total_kgs' || f.mapped_workflow_field === 'total_kgs'); + + const displayFields = normalFields.filter(f => { + if (hasGridField) { + const baseId = getBaseIdForField(f.id); + const mapped = (f.mapped_workflow_field || '').toLowerCase(); + const isTotalField = + baseId === 'total_bags' || + baseId === 'total_kgs' || + mapped === 'total_bags' || + mapped === 'total_kgs' || + f.id === 'total_bags' || + f.id === 'total_kgs'; + if (isTotalField) return false; + } + return true; + }); + return (
- {normalFields.map(f => { + {displayFields.map(f => { const type = f.data_type; const val = values[f.id]; const isDisabled = schema.field_defaults?.[f.id]?.disabled; @@ -409,6 +615,9 @@ export function DynamicForm({ client, activityId: initialActivityId, instanceId: columns={f.columns || []} value={(val as Record[]) || []} onChange={(newVal) => handleFieldChange(f.id, newVal)} + totalBagsValue={tbField ? Number(values[tbField.id]) || 0 : undefined} + totalKgsValue={tkField ? Number(values[tkField.id]) || 0 : undefined} + showErrors={showErrors} /> ); } @@ -480,6 +689,9 @@ export function DynamicForm({ client, activityId: initialActivityId, instanceId: ); } + const isValEmpty = val === undefined || val === null || String(val).trim() === ''; + const currentFieldError = showErrors && f.mandatory && isValEmpty ? 'Required' : undefined; + return ( handleFieldChange(f.id, newVal)} + error={currentFieldError} /> ); }; diff --git a/src/components/forms/fields/GridInput.tsx b/src/components/forms/fields/GridInput.tsx index 601c24a..3b50ef0 100644 --- a/src/components/forms/fields/GridInput.tsx +++ b/src/components/forms/fields/GridInput.tsx @@ -1,85 +1,5 @@ -import { Button } from '../../buttons/Button'; -import { Select } from '../../reusable/Select'; -import { Input } from '../../reusable/Input'; -import type { FormScreenField } from '../../../api/types'; +import { SmartGridField, type SmartGridFieldProps } from './SmartGridField'; -export function GridInput({ - label, - columns, - value = [], - onChange, -}: { - label: string; - columns: FormScreenField[]; - value: Record[]; - onChange: (val: Record[]) => void; -}) { - const addRow = () => { - onChange([...value, {}]); - }; - - const removeRow = (idx: number) => { - const next = [...value]; - next.splice(idx, 1); - onChange(next); - }; - - const updateRow = (idx: number, fieldId: string, val: unknown) => { - const next = [...value]; - next[idx] = { ...next[idx], [fieldId]: val }; - onChange(next); - }; - - return ( -
- {label} - {value.length === 0 ? ( - No rows added. - ) : ( -
- {value.map((row, i) => ( -
-
- -
- Row {i + 1} -
- {columns.map(col => { - const val = row[col.id]; - if (col.data_type === 'select' || col.data_type === 'multiselect') { - const opts = col.properties?.options || []; - return ( - updateRow(i, col.id, col.data_type === 'number' ? Number(e.target.value) : e.target.value)} - /> - ); - })} -
-
- ))} -
- )} - -
- ); +export function GridInput(props: SmartGridFieldProps) { + return ; } diff --git a/src/components/forms/fields/SmartGridField.tsx b/src/components/forms/fields/SmartGridField.tsx index 912664d..7abfa47 100644 --- a/src/components/forms/fields/SmartGridField.tsx +++ b/src/components/forms/fields/SmartGridField.tsx @@ -1,361 +1,697 @@ -import { useState, useRef } from 'react'; -import { Button } from '../../buttons/Button'; +import { useMemo, useCallback, useEffect } from 'react'; +import type { FormScreenField } from '../../../api/types'; import { Select } from '../../reusable/Select'; import { Input } from '../../reusable/Input'; -import { Pencil } from 'lucide-react'; -import type { FormScreenField } from '../../../api/types'; -import { PotentialMiningTable } from '../../dv/PotentialMiningTable'; -import { GridTable } from '../../dv/GridTable'; + +// Product dataset — category, name, sku (kg per unit), br code, sku code +export const FALLBACK_PRODUCTS = [ + { cat: "ATTA REGULAR", name: "ATTA REGULAR 30 kgs", sku: "30", br: "AR", skucode: "AR30" }, + { cat: "ATTA REGULAR", name: "ATTA REGULAR 50 kgs", sku: "50", br: "AR", skucode: "AR50" }, + { cat: "ATTA TANDOORI", name: "ATTA TANDOORI 50 kgs", sku: "50", br: "AT", skucode: "AT50" }, + { cat: "BANSI RAWA", name: "BANSI RAWA 1kg", sku: "1", br: "BNS", skucode: "BNS-1" }, + { cat: "BANSI RAWA", name: "BANSI RAWA 1kg*30", sku: "30", br: "BNS", skucode: "BN13" }, + { cat: "BANSI RAWA", name: "BANSI RAWA 30 kgs", sku: "30", br: "BNS", skucode: "BNS30" }, + { cat: "CHAKKI ATTA", name: "CHAKKI ATTA 10kg*3", sku: "30", br: "CAP", skucode: "CA103" }, + { cat: "CHAKKI ATTA", name: "CHAKKI ATTA 1kg*30", sku: "30", br: "CAP", skucode: "CA13" }, + { cat: "CHAKKI ATTA", name: "CHAKKI ATTA 500g*60", sku: "30", br: "CAP", skucode: "CA500" }, + { cat: "CHAKKI ATTA", name: "CHAKKI ATTA 5kg*6", sku: "30", br: "CAP", skucode: "CA56" }, + { cat: "CHAKKI ATTA PREMIUM", name: "CHAKKI ATTA PREMIUM 500g", sku: "0.5", br: "CAP", skucode: "CAP-500" }, + { cat: "CHAKKI ATTA PREMIUM", name: "CHAKKI ATTA PREMIUM 1 kgs", sku: "1", br: "CAP", skucode: "CAP-1" }, + { cat: "CHAKKI ATTA PREMIUM", name: "CHAKKI ATTA PREMIUM 5 kgs", sku: "5", br: "CAP", skucode: "CAP-5" }, + { cat: "CHAKKI ATTA PREMIUM", name: "CHAKKI ATTA PREMIUM 10 kgs", sku: "10", br: "CAP", skucode: "CAP-10" }, + { cat: "CHAKKI ATTA PREMIUM", name: "CHAKKI ATTA PREMIUM 30 kgs", sku: "30", br: "CAP", skucode: "CAP30" }, + { cat: "CHAKKI ATTA PREMIUM", name: "CHAKKI ATTA PREMIUM 50 kgs", sku: "50", br: "CAP", skucode: "CAP50" }, + { cat: "CHAKKI ATTA SUPER VALUE", name: "CHAKKI ATTA SUPER VALUE 30 kgs", sku: "30", br: "CASV", skucode: "CASV30" }, + { cat: "CHAKKI ATTA SUPER VALUE", name: "CHAKKI ATTA SUPER VALUE 50 kgs", sku: "50", br: "CASV", skucode: "CASV50" }, + { cat: "CHAKKI ATTA VALUE", name: "CHAKKI ATTA VALUE 30 kgs", sku: "30", br: "CAV", skucode: "CAV30" }, + { cat: "CHAKKI ATTA VALUE", name: "CHAKKI ATTA VALUE 50 kgs", sku: "50", br: "CAV", skucode: "CAV50" }, + { cat: "CHIROTI", name: "CHIROTI 1kg*30", sku: "30", br: "CPR", skucode: "C13" }, + { cat: "CHIROTI", name: "CHIROTI 500g*60", sku: "30", br: "CPR", skucode: "C56" }, + { cat: "CHIROTI RAWA", name: "CHIROTI RAWA 500g", sku: "0.5", br: "CR", skucode: "C1-500" }, + { cat: "CHIROTI RAWA", name: "CHIROTI RAWA 1kg", sku: "1", br: "CR", skucode: "C1-1" }, + { cat: "CHIROTI PREMIUM", name: "CHIROTI PREMIUM 30 kgs", sku: "30", br: "CPR", skucode: "CPR30" }, + { cat: "CHIROTI PREMIUM", name: "CHIROTI PREMIUM 50 kgs", sku: "50", br: "CPR", skucode: "CPR50" }, + { cat: "DALIYA", name: "DALIYA 500g", sku: "0.5", br: "DALIYA", skucode: "DL-1" }, + { cat: "DALIYA", name: "DALIYA 30 kgs", sku: "30", br: "DALIYA", skucode: "DAL30" }, + { cat: "DALIYA", name: "DALIYA 500g*40", sku: "20", br: "DALIYA", skucode: "D500" }, + { cat: "FORTIFIED MAIDA", name: "FORTIFIED MAIDA 500G", sku: "0.5", br: "MPR", skucode: "MF1-500" }, + { cat: "FORTIFIED MAIDA", name: "FORTIFIED MAIDA 1kg", sku: "1", br: "MPR", skucode: "MF1-1" }, + { cat: "FORTIFIED MAIDA", name: "FORTIFIED MAIDA 1kg*30", sku: "30", br: "MPR", skucode: "M13" }, + { cat: "FORTIFIED MAIDA", name: "FORTIFIED MAIDA 500GX60", sku: "30", br: "MPR", skucode: "M56" }, + { cat: "MAIDA ALL PURPOSE", name: "MAIDA ALL PURPOSE 30 kgs", sku: "30", br: "MAP", skucode: "MAP30" }, + { cat: "MAIDA ALL PURPOSE", name: "MAIDA ALL PURPOSE 50 kgs", sku: "50", br: "MAP", skucode: "MAP50" }, + { cat: "MAIDA BAKERY", name: "MAIDA BAKERY 50 kgs", sku: "50", br: "MB", skucode: "MB50" }, + { cat: "MAIDA PREMIUM", name: "MAIDA PREMIUM 30 kgs", sku: "30", br: "MPR", skucode: "MPR30" }, + { cat: "MAIDA PREMIUM", name: "MAIDA PREMIUM 50 kgs", sku: "50", br: "MPR", skucode: "MPR50" }, + { cat: "MAIDA PREMIUM", name: "MAIDA PREMIUM (TN) 50 kgs", sku: "50", br: "MPRTN", skucode: "MPRTN50" }, + { cat: "MAIDA PREMIUM", name: "MAIDA PREMIUM (TN) 30 kgs", sku: "30", br: "MPRTN", skucode: "MPRTN30" }, + { cat: "MAIDA PREMIUM", name: "MAIDA PREMIUM (TN) 10X5", sku: "50", br: "MPRTN", skucode: "MPRTN10" }, + { cat: "MAIDA PUFF", name: "MAIDA PUFF 30 kgs", sku: "30", br: "PM", skucode: "MP30" }, + { cat: "MAIDA PUFF", name: "MAIDA PUFF 50 kgs", sku: "50", br: "PM", skucode: "MP50" }, + { cat: "MAIDA SUPER VALUE", name: "MAIDA SUPER VALUE 30 kgs", sku: "30", br: "MSV", skucode: "MSV30" }, + { cat: "MAIDA SUPER VALUE", name: "MAIDA SUPER VALUE 50 kgs", sku: "50", br: "MSV", skucode: "MSV50" }, + { cat: "SUJI", name: "SUJI 1kg*30", sku: "30", br: "SPR", skucode: "S13" }, + { cat: "SUJI", name: "SUJI 500g*60", sku: "30", br: "SPR", skucode: "S56" }, + { cat: "SUJI", name: "SUJI 1Kg", sku: "1", br: "SPR", skucode: "S1-1" }, + { cat: "SUJI", name: "SUJI 500g", sku: "0.5", br: "SPR", skucode: "S1-500" }, + { cat: "SUJI PREMIUM", name: "SUJI PREMIUM 30 kgs", sku: "30", br: "SPR", skucode: "SPR30" }, + { cat: "SUJI PREMIUM", name: "SUJI PREMIUM 50 kgs", sku: "50", br: "SPR", skucode: "SPR50" }, + { cat: "SUJI SUPER VALUE", name: "SUJI SUPER VALUE 30 kgs", sku: "30", br: "SSV", skucode: "SSV30" }, + { cat: "SUJI SUPER VALUE", name: "SUJI SUPER VALUE 50 kgs", sku: "50", br: "SSV", skucode: "SSV50" }, + { cat: "SUPER VALUE CHAKKI ATTA", name: "SUPER VALUE CHAKKI ATTA 1kg*30", sku: "30", br: "CASV", skucode: "CASV13" }, + { cat: "SUPER VALUE CHAKKI ATTA", name: "SUPER VALUE CHAKKI ATTA 5kg*6", sku: "30", br: "CASV", skucode: "CASV56" }, + { cat: "WHEAT BRAN DELUX", name: "WHEAT BRAN DELUX 40 kgs (UNBRANED)", sku: "40", br: "WBD", skucode: "WBD40" }, + { cat: "WHEAT BRAN FINE", name: "WHEAT BRAN FINE 45 kgs (UNBRANDED)", sku: "45", br: "WBF", skucode: "WBF45" }, + { cat: "WHEAT BRAN FLAKES", name: "WHEAT BRAN FLAKES 34 kgs (UNBRANDED)", sku: "34", br: "WBF", skucode: "WBF34" }, + { cat: "WHEAT BRAN WHOLE WHEAT", name: "WHEAT BRAN WHOLE WHEAT 45 kgs (UNBRANDED)", sku: "45", br: "WBW", skucode: "WBWW45" }, + { cat: "WHOLE MEAL FLOUR", name: "WHOLE MEAL FLOUR 30 kgs", sku: "30", br: "WMF", skucode: "WMF30" }, + { cat: "BESAN", name: "BESAN 500g", sku: "0.5", br: "B5", skucode: "GF-500" }, + { cat: "BESAN", name: "BESAN 500g*40", sku: "20", br: "B54", skucode: "B54" }, + { cat: "BESAN PREMIUM", name: "BESAN PREMIUM 10kg*2", sku: "20", br: "BP", skucode: "BP102" }, + { cat: "BESAN VALUE", name: "BESAN VALUE 10kg*2", sku: "20", br: "BV", skucode: "BV102" }, + { cat: "RAGI FLOUR", name: "RAGI FLOUR 1kg", sku: "1", br: "RG", skucode: "RG12" }, + { cat: "RAGI FLOUR", name: "RAGI FLOUR 1kg*20", sku: "20", br: "RG", skucode: "RG-1" }, + { cat: "RAGI FLOUR", name: "RAGI FLOUR 30 kgs", sku: "30", br: "RG", skucode: "RG30" }, + { cat: "IDLI RAWA", name: "IDLI RAWA 1kg", sku: "1", br: "IR", skucode: "IR-1" }, + { cat: "IDLI RAWA", name: "IDLI RAWA 1kg*20", sku: "20", br: "IR", skucode: "IR12" }, + { cat: "IDLI RAWA", name: "IDLI RAWA 30 kgs", sku: "30", br: "IR", skucode: "IR30" }, + { cat: "RICE FLOUR", name: "RICE FLOUR 1kg", sku: "1", br: "RC", skucode: "RC-1" }, + { cat: "RICE FLOUR", name: "RICE FLOUR 1kg*20", sku: "20", br: "RC", skucode: "RC12" }, + { cat: "RICE FLOUR", name: "RICE FLOUR 30 kgs", sku: "30", br: "RC", skucode: "RC30" }, +]; + +export const FALLBACK_CATEGORIES = Array.from(new Set(FALLBACK_PRODUCTS.map((p) => p.cat))); + +export interface SmartGridFieldProps { + label: string; + columns: FormScreenField[]; + value: Record[]; + onChange: (val: Record[]) => void; + totalBagsValue?: number; + totalKgsValue?: number; + showErrors?: boolean; +} export function SmartGridField({ label, columns, value = [], onChange, -}: { - label: string; - columns: FormScreenField[]; - value: Record[]; - onChange: (val: Record[]) => void; -}) { - const [isModalOpen, setIsModalOpen] = useState(false); - const formRef = useRef(null); - const [newRow, setNewRow] = useState>({}); - const [editingIdx, setEditingIdx] = useState(null); - const [errors, setErrors] = useState>({}); - - const visibleColumns = columns.filter(c => { - const isOrderDetails = label.toLowerCase().includes('order'); - if (isOrderDetails) { - const n = (c.name || '').toLowerCase(); - const id = (c.id || '').toLowerCase(); - // Show only Category, Name, and Bags - return n.includes('category') || n.includes('name') || n.includes('bags') || - id.includes('category') || id.includes('name') || id.includes('bags'); + totalBagsValue, + totalKgsValue, + showErrors = false, +}: SmartGridFieldProps) { + // Ensure we start with at least 1 empty row if value is empty + useEffect(() => { + if (!value || value.length === 0) { + onChange([{}]); } - return true; - }); + }, [value, onChange]); - const removeRow = (idx: number) => { - const next = [...value]; - next.splice(idx, 1); - onChange(next); - }; + const rows = value.length > 0 ? value : [{}]; - const startEdit = (idx: number) => { - setEditingIdx(idx); - setNewRow({ ...value[idx] }); - setIsModalOpen(true); - setTimeout(() => { - formRef.current?.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); - }, 150); - }; + // Precise column categorization helpers + const isCategoryColumn = useCallback((colId: string, colName: string = '') => { + const idLower = colId.toLowerCase(); + const nameLower = colName.toLowerCase(); + return idLower.includes('category') || nameLower.includes('category'); + }, []); - const startAdd = () => { - setEditingIdx(null); - setNewRow({}); - setIsModalOpen(true); - setTimeout(() => { - formRef.current?.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); - }, 150); - }; + const isProductColumn = useCallback((colId: string, colName: string = '') => { + if (isCategoryColumn(colId, colName)) return false; + const idLower = colId.toLowerCase(); + const nameLower = colName.toLowerCase(); + return ( + idLower.includes('product_name') || + idLower === 'product' || + idLower === 'name' || + nameLower.includes('product name') || + nameLower === 'product' || + (nameLower.includes('product') && !nameLower.includes('category')) + ); + }, [isCategoryColumn]); - const updateNewRowField = (fieldId: string, val: unknown) => { - let row = { ...newRow, [fieldId]: val }; - setErrors(prev => ({ ...prev, [fieldId]: '' })); - - const colDef = columns.find(c => c.id === fieldId); + // Helper function to find a column by various field aliases + const findColumn = useCallback( + (...aliases: string[]) => { + const getBaseId = (id: string) => id.replace(/_\d+$/, ''); + return columns.find((c) => { + const idLower = c.id.toLowerCase(); + const nameLower = (c.name || '').toLowerCase(); + const mappedLower = (c.mapped_workflow_field || '').toLowerCase(); + const baseIdLower = getBaseId(c.id).toLowerCase(); - // If product category changes, clear out the rest of the row's data - if (colDef && (colDef.id === 'product_category' || colDef.name === 'Product Category' || colDef.mapped_workflow_field === 'product_category')) { - Object.keys(row).forEach(k => { - if (k !== fieldId) { - row[k] = ''; - } + return aliases.some((alias) => { + const a = alias.toLowerCase(); + return ( + idLower === a || + nameLower === a || + mappedLower === a || + baseIdLower === a || + idLower.includes(a) || + nameLower.includes(a) + ); + }); }); + }, + [columns] + ); + + const catCol = useMemo(() => columns.find(c => isCategoryColumn(c.id, c.name)) || findColumn('product_category', 'category'), [columns, isCategoryColumn, findColumn]); + const prodNameCol = useMemo(() => columns.find(c => isProductColumn(c.id, c.name)) || findColumn('product_name', 'name', 'product'), [columns, isProductColumn, findColumn]); + const bagsCol = useMemo(() => findColumn('bags', 'quantity', 'actual_potential', 'store_potential'), [findColumn]); + + // Filter columns to display in grid row layout (including virtual display column Total kgs) + const visibleColumns = useMemo(() => { + const baseCols = (() => { + if (columns.length === 0) { + return [ + { id: 'product_category', name: 'Category', data_type: 'select' } as FormScreenField, + { id: 'product_name', name: 'Product name', data_type: 'select' } as FormScreenField, + { id: 'bags', name: 'Bags', data_type: 'number' } as FormScreenField, + ]; + } + const isOrderDetails = label.toLowerCase().includes('order') || label.toLowerCase().includes('product'); + if (isOrderDetails) { + const filtered = columns.filter((c) => { + return isCategoryColumn(c.id, c.name) || isProductColumn(c.id, c.name) || c.id.includes('bags') || (c.name || '').toLowerCase().includes('bags'); + }); + if (filtered.length > 0) return filtered; + } + return columns.filter((c) => { + const idL = c.id.toLowerCase(); + const nL = (c.name || '').toLowerCase(); + return !idL.includes('sku') && !nL.includes('sku') && !idL.includes('br_code') && !nL.includes('br code') && !idL.includes('row_kgs'); + }); + })(); + + const isOrderGrid = label.toLowerCase().includes('order') || label.toLowerCase().includes('product'); + if (isOrderGrid) { + return [ + ...baseCols, + { id: '_row_total_kgs', name: 'Total (kgs)', data_type: 'readonly' } as FormScreenField, + ]; } - // Auto-fill dataset keys from _raw - if (colDef && (colDef.data_type === 'select' || colDef.data_type === 'multiselect')) { - const option = colDef.properties?.options?.find(o => String(o.value) === String(val)); - if (option && option._raw) { - const getBaseId = (id: string) => id.replace(/_\d+$/, ''); - for (const key of Object.keys(option._raw)) { - const targetCol = columns.find(c => c.id === key || c.mapped_workflow_field === key || getBaseId(c.id) === key || c.name.toLowerCase().replace(/\s+/g, '') === key.toLowerCase().replace(/_/g, '')); - if (targetCol && targetCol.id !== fieldId) { - row[targetCol.id] = option._raw[key]; - } else if (!targetCol && key !== fieldId) { - row[key] = option._raw[key]; - } + return baseCols; + }, [columns, label, isCategoryColumn, isProductColumn]); + + // Determine grid columns template ratio + const gridTemplateColumns = useMemo(() => { + const numCols = visibleColumns.length; + if (numCols === 4) return '1.3fr 1.5fr 0.7fr 0.8fr auto'; + if (numCols === 3) return '1.4fr 1.6fr 0.7fr auto'; + if (numCols === 2) return '1.5fr 1.5fr auto'; + return `repeat(${numCols}, minmax(0, 1fr)) auto`; + }, [visibleColumns]); + + // Consolidation helper to merge duplicate (Category + Product Name) rows by summing bags + const consolidateRows = useCallback((rowsList: Record[]) => { + const merged: Record[] = []; + const indexMap = new Map(); + + for (const r of rowsList) { + const cat = String(r.category || r.product_category || (catCol ? r[catCol.id] : '') || '').trim().toLowerCase(); + const prod = String(r.productName || r.product_name || (prodNameCol ? r[prodNameCol.id] : '') || '').trim().toLowerCase(); + + const bagsColId = bagsCol ? bagsCol.id : 'bags'; + const currentBags = Number(r[bagsColId] ?? r.bags ?? r.quantity ?? 0); + + if (cat && prod) { + const key = `${cat}::${prod}`; + if (indexMap.has(key)) { + const targetIdx = indexMap.get(key)!; + const existingRow = merged[targetIdx]; + + const existingBags = Number(existingRow[bagsColId] ?? existingRow.bags ?? existingRow.quantity ?? 0); + const addedBags = currentBags > 0 ? currentBags : 1; + const newBags = existingBags + addedBags; + const skuVal = Number(existingRow.sku || r.sku || 0); + + const updatedRow: Record = { + ...existingRow, + bags: newBags, + quantity: newBags, + row_kgs: skuVal * newBags, + }; + if (bagsCol) updatedRow[bagsCol.id] = newBags; + + merged[targetIdx] = updatedRow; + continue; + } else { + indexMap.set(key, merged.length); } } + + merged.push(r); } - // Auto-calculate row_kgs if sku and bags are present - const getBaseId = (id: string) => id.replace(/_\d+$/, ''); - const getVal = (key: string) => { - const c = columns.find(c => c.id === key || c.mapped_workflow_field === key || getBaseId(c.id) === key || c.name.toLowerCase().replace(/\s+/g, '') === key.toLowerCase().replace(/_/g, '')); - return c ? row[c.id] : row[key]; - }; - - const skuVal = Number(getVal('sku')) || 0; - const bagsVal = Number(getVal('bags')) || 0; - - const rowKgsCol = columns.find(c => c.id === 'row_kgs' || c.mapped_workflow_field === 'row_kgs' || getBaseId(c.id) === 'row_kgs' || getBaseId(c.id) === 'rowkgs'); - if (rowKgsCol) { - row[rowKgsCol.id] = skuVal * bagsVal; - } else { - row['row_kgs'] = skuVal * bagsVal; - } + return merged.length > 0 ? merged : [{}]; + }, [catCol, prodNameCol, bagsCol]); - // Auto-calculate difference for potential mining - const potentialCol = columns.find(c => c.id.toLowerCase().includes('potential') || c.name.toLowerCase().includes('potential')); - const totalOrderedCol = columns.find(c => c.id.toLowerCase().includes('total_ordered') || c.name.toLowerCase().includes('total ordered')); - if (potentialCol && totalOrderedCol) { - const p = Number(row[potentialCol.id]) || 0; - const t = Number(row[totalOrderedCol.id]) || 0; - const diffCol = columns.find(c => c.id.toLowerCase().includes('difference') || c.name.toLowerCase().includes('difference')); - if (diffCol) { - row[diffCol.id] = t - p; - } - } + // Check if any existing row in the grid table is incomplete + const isAnyRowIncomplete = useMemo(() => { + return rows.some((r) => { + const cat = String(r.category || r.product_category || (catCol ? r[catCol.id] : '') || '').trim(); + const prod = String(r.productName || r.product_name || (prodNameCol ? r[prodNameCol.id] : '') || '').trim(); + const bags = Number(r.bags || r.quantity || (bagsCol ? r[bagsCol.id] : '') || 0); - setNewRow(row); - }; + return !cat || !prod || bags <= 0; + }); + }, [rows, catCol, prodNameCol, bagsCol]); - const submitNewRow = () => { - const newErrors: Record = {}; - const getBaseId = (id: string) => id.replace(/_\d+$/, ''); - const catColId = columns.find(c => c.id === 'product_category' || c.name === 'Product Category' || c.mapped_workflow_field === 'product_category' || getBaseId(c.id) === 'product_category')?.id; - const isCatSelected = catColId ? !!newRow[catColId] : false; - - for (const col of visibleColumns) { - const isProductName = col.id === 'product_name' || col.name === 'Product Name' || col.mapped_workflow_field === 'product_name' || getBaseId(col.id) === 'product_name'; - const isBags = col.id === 'bags' || col.name === 'Bags' || col.mapped_workflow_field === 'bags' || getBaseId(col.id) === 'bags'; - const isQuantity = col.id === 'quantity' || col.name === 'Quantity' || getBaseId(col.id) === 'quantity' || - col.id === 'actual_potential' || col.name === 'Actual Potential' || getBaseId(col.id) === 'actual_potential' || - col.id === 'store_potential' || col.name === 'Store Potential' || getBaseId(col.id) === 'store_potential'; - const isRequired = col.mandatory || (isCatSelected && (isProductName || isBags || isQuantity)); - - if (isRequired && !newRow[col.id]) { - newErrors[col.id] = 'This field is required'; - } - } - - if (Object.keys(newErrors).length > 0) { - setErrors(newErrors); + // When user clicks "+ Add product", verify that ALL existing rows are complete + const addRow = useCallback(() => { + if (isAnyRowIncomplete) { return; } - if (editingIdx !== null) { - const next = [...value]; - next[editingIdx] = newRow; - onChange(next); - } else { - const getBaseId = (id: string) => id.replace(/_\d+$/, ''); - const prodNameCol = columns.find(c => c.id === 'product_name' || getBaseId(c.id) === 'product_name' || c.mapped_workflow_field === 'product_name'); - - let foundIdx = -1; - if (prodNameCol && newRow[prodNameCol.id]) { - foundIdx = value.findIndex(r => r[prodNameCol.id] === newRow[prodNameCol.id]); - } + const consolidated = consolidateRows(rows); + const cleanRows = consolidated.map((r) => { + const clean: Record = {}; + Object.keys(r).forEach((k) => { + if (k !== '_row_total_kgs' && k !== 'row_kgs' && k !== 'row_total') { + clean[k] = r[k]; + } + }); + return clean; + }); - if (foundIdx !== -1) { - const next = [...value]; - const existingRow = { ...next[foundIdx] }; + onChange([...cleanRows, {}]); + }, [rows, isAnyRowIncomplete, consolidateRows, onChange]); + + const removeRow = useCallback( + (idx: number) => { + if (rows.length > 1) { + const next = [...rows]; + next.splice(idx, 1); - const bagsCol = columns.find(c => c.id === 'bags' || getBaseId(c.id) === 'bags' || c.mapped_workflow_field === 'bags' || c.name.toLowerCase() === 'bags'); - const rowKgsCol = columns.find(c => c.id === 'row_kgs' || getBaseId(c.id) === 'row_kgs' || c.mapped_workflow_field === 'row_kgs' || getBaseId(c.id) === 'rowkgs'); - const skuCol = columns.find(c => c.id === 'sku' || getBaseId(c.id) === 'sku' || c.mapped_workflow_field === 'sku'); - - const bagsId = bagsCol ? bagsCol.id : 'bags'; - const rowKgsId = rowKgsCol ? rowKgsCol.id : 'row_kgs'; - const skuId = skuCol ? skuCol.id : 'sku'; - - const existingBags = Number(existingRow[bagsId]) || 0; - const addedBags = Number(newRow[bagsId]) || 0; - existingRow[bagsId] = existingBags + addedBags; - - const skuVal = Number(existingRow[skuId]) || 0; - existingRow[rowKgsId] = (existingBags + addedBags) * skuVal; - - next[foundIdx] = existingRow; - onChange(next); + // Clean client-only virtual keys before notifying parent + const cleanRows = next.map(r => { + const clean: Record = {}; + Object.keys(r).forEach(k => { + if (k !== '_row_total_kgs' && k !== 'row_kgs' && k !== 'row_total') { + clean[k] = r[k]; + } + }); + return clean; + }); + + onChange(cleanRows); + } + }, + [rows, onChange] + ); + + // Derive categories list + const categoriesList = useMemo(() => { + if (catCol?.properties?.options && catCol.properties.options.length > 0) { + return catCol.properties.options.map((o: any) => ({ + value: String(o.value ?? o.label), + label: String(o.label ?? o.value), + })); + } + return FALLBACK_CATEGORIES.map((c) => ({ value: c, label: c })); + }, [catCol]); + + // Derive products list for a given category (all options selectable) + const getProductsForCategory = useCallback( + (selectedCat?: string) => { + let allOpts: { value: string; label: string; rawCat?: string; raw?: any }[] = []; + + if (prodNameCol?.properties?.options && prodNameCol.properties.options.length > 0) { + allOpts = prodNameCol.properties.options.map((o: any) => { + const val = String(o.value ?? o.label ?? ''); + const label = String(o.label ?? o.value ?? ''); + const rawCat = o._raw?.product_category || o._raw?.cat || o._raw?.category; + return { value: val, label, rawCat, raw: o._raw }; + }); + + if (selectedCat) { + allOpts = allOpts.filter((o) => { + if (o.rawCat) { + return String(o.rawCat).trim().toLowerCase() === String(selectedCat).trim().toLowerCase(); + } + return o.label.toLowerCase().startsWith(String(selectedCat).toLowerCase()); + }); + } } else { - onChange([...value, newRow]); + // Fallback PRODUCTS dataset + if (!selectedCat) { + allOpts = FALLBACK_PRODUCTS.map((p) => ({ + value: p.name, + label: p.name, + rawCat: p.cat, + raw: { product_category: p.cat, product_name: p.name, sku: p.sku, br_code: p.br, sku_code: p.skucode }, + })); + } else { + allOpts = FALLBACK_PRODUCTS.filter((p) => p.cat.trim().toLowerCase() === String(selectedCat).trim().toLowerCase()).map((p) => ({ + value: p.name, + label: p.name, + rawCat: p.cat, + raw: { product_category: p.cat, product_name: p.name, sku: p.sku, br_code: p.br, sku_code: p.skucode }, + })); + } } - } - - // Only close form if we were editing an existing row. For new rows, stay open. - if (editingIdx !== null) { - setIsModalOpen(false); - } - setNewRow({}); - setEditingIdx(null); - }; - const isPotentialGrid = label.toLowerCase().includes('potential'); - const hasOrderedColumn = columns.some(c => c.id.toLowerCase().includes('ordered') || (c.name || '').toLowerCase().includes('ordered')); + return allOpts; + }, + [prodNameCol] + ); + + const updateRowField = useCallback( + (rowIdx: number, fieldId: string, val: unknown) => { + const next = rows.map((r) => ({ ...r })); + let row = { ...next[rowIdx] }; + + const colDef = columns.find((c) => c.id === fieldId) || visibleColumns.find((c) => c.id === fieldId); + const colName = colDef?.name || ''; + + const isCatField = isCategoryColumn(fieldId, colName); + const isProdField = isProductColumn(fieldId, colName); + + if (isCatField) { + const catVal = String(val ?? ''); + row[fieldId] = catVal; + row['category'] = catVal; + row['product_category'] = catVal; + + // Reset product name & specs for this row + const pColId = prodNameCol?.id || 'product_name'; + row[pColId] = ''; + row['product_name'] = ''; + row['productName'] = ''; + row['sku'] = ''; + row['br_code'] = ''; + row['br'] = ''; + row['sku_code'] = ''; + row['skucode'] = ''; + } else if (isProdField) { + const currentCat = String(row['category'] || row['product_category'] || (catCol ? row[catCol.id] : '') || ''); + const prodOptions = getProductsForCategory(currentCat); + + const matchedOpt = prodOptions.find( + (o) => o.value === String(val) || o.label === String(val) || o.label.toLowerCase() === String(val).toLowerCase() + ); + + const matchedFallback = FALLBACK_PRODUCTS.find( + (p) => p.name === String(val) || p.skucode === String(val) || p.name.toLowerCase() === String(val).toLowerCase() + ); + + const selectedName = matchedOpt?.label || matchedFallback?.name || String(val); + const sku = matchedOpt?.raw?.sku || matchedFallback?.sku || ''; + const br = matchedOpt?.raw?.br_code || matchedOpt?.raw?.br || matchedFallback?.br || ''; + const skucode = matchedOpt?.raw?.sku_code || matchedOpt?.raw?.skucode || matchedFallback?.skucode || ''; + const cat = matchedOpt?.rawCat || matchedOpt?.raw?.product_category || matchedFallback?.cat || currentCat; + + const setVal = matchedOpt?.value || String(val); + row[fieldId] = setVal; + row['product_name'] = selectedName; + row['productName'] = selectedName; + if (prodNameCol) row[prodNameCol.id] = setVal; + + row['sku'] = sku; + const skuCol = findColumn('sku'); + if (skuCol) row[skuCol.id] = sku; + + row['br'] = br; + row['br_code'] = br; + const brCol = findColumn('br_code', 'br'); + if (brCol) row[brCol.id] = br; + + row['skucode'] = skucode; + row['sku_code'] = skucode; + const skuCodeCol = findColumn('sku_code', 'skucode'); + if (skuCodeCol) row[skuCodeCol.id] = skucode; + + if (cat) { + row['category'] = cat; + row['product_category'] = cat; + if (catCol) row[catCol.id] = cat; + } + } else { + row[fieldId] = val; + } + + next[rowIdx] = row; + + // Clean client-only virtual keys before sending to parent + const cleanRows = next.map((r) => { + const clean: Record = {}; + Object.keys(r).forEach((k) => { + if (k !== '_row_total_kgs' && k !== 'row_kgs' && k !== 'row_total') { + clean[k] = r[k]; + } + }); + return clean; + }); + + onChange(cleanRows); + }, + [rows, columns, visibleColumns, prodNameCol, catCol, findColumn, isCategoryColumn, isProductColumn, getProductsForCategory, onChange] + ); + + // Calculate overall totals across all rows + const { calculatedBags, calculatedKgs } = useMemo(() => { + let bagsAcc = 0; + let kgsAcc = 0; + for (const r of rows) { + const b = Number(r.bags ?? r.quantity) || 0; + const sku = Number(r.sku) || 0; + bagsAcc += b; + kgsAcc += b * sku; + } + return { calculatedBags: Math.round(bagsAcc), calculatedKgs: Math.round(kgsAcc) }; + }, [rows]); + + const displayTotalBags = totalBagsValue !== undefined ? totalBagsValue : calculatedBags; + const displayTotalKgs = totalKgsValue !== undefined ? totalKgsValue : calculatedKgs; + + const isOrderGrid = label.toLowerCase().includes('order') || label.toLowerCase().includes('product'); return ( -
- {label} - - {value.length === 0 ? ( - No rows added. - ) : isPotentialGrid ? ( - { - const reason = String(row.reason || row.Reason || row.potential_remarks || ''); - let badgeType: 'error' | 'success' | 'info' | 'warning' | 'default' = 'default'; - const lowerReason = reason.toLowerCase(); - if (lowerReason.includes('stock')) badgeType = 'success'; - else if (lowerReason.includes('quality')) badgeType = 'error'; - else if (lowerReason.includes('price')) badgeType = 'warning'; - else if (lowerReason.includes('competitive') || lowerReason.includes('loyalty')) badgeType = 'info'; - - let productName = 'Unknown Product'; - visibleColumns.forEach(col => { - const isName = col.id.toLowerCase().includes('name') || col.name.toLowerCase().includes('name') || (col.id.toLowerCase().includes('category') && productName === 'Unknown Product'); - if (isName) { - const val = row[col.id]; - let displayVal = String(val ?? '-'); - if (col.data_type === 'select' || col.data_type === 'multiselect') { - const opt = col.properties?.options?.find(o => String(o.value) === String(val)); - if (opt) displayVal = opt.label; - } - productName = displayVal; - } - }); - - return { - id: String(i), - productName, - badge: reason ? { label: reason, type: badgeType } : undefined, - potentialKgs: Number(row.actual_potential ?? row.Actual_Potential ?? row.store_potential ?? row.Store_Potential ?? row.quantity ?? row.Quantity ?? 0), - orderedKgs: Number(row.total_ordered ?? row.Total_Ordered ?? 0), - }; - })} - onEdit={(item) => startEdit(Number(item.id))} - onDelete={(item) => removeRow(Number(item.id))} - /> - ) : ( - - )} - -
- + + {isOrderGrid ? 'Add product' : 'Add row'} +
-
-
-
-

- {editingIdx !== null ? ( - <> Edit Row - ) : ( - <>Add New Item - )} -

-
- {visibleColumns.map(col => { - const val = newRow[col.id]; - - const getBaseId = (id: string) => id.replace(/_\d+$/, ''); - const catColId = columns.find(c => c.id === 'product_category' || c.name === 'Product Category' || c.mapped_workflow_field === 'product_category' || getBaseId(c.id) === 'product_category')?.id; - const isCatSelected = catColId ? !!newRow[catColId] : false; - const isProductName = col.id === 'product_name' || col.name === 'Product Name' || col.mapped_workflow_field === 'product_name' || getBaseId(col.id) === 'product_name'; - const isBags = col.id === 'bags' || col.name === 'Bags' || col.mapped_workflow_field === 'bags' || getBaseId(col.id) === 'bags'; - const isQuantity = col.id === 'quantity' || col.name === 'Quantity' || getBaseId(col.id) === 'quantity' || - col.id === 'actual_potential' || col.name === 'Actual Potential' || getBaseId(col.id) === 'actual_potential' || - col.id === 'store_potential' || col.name === 'Store Potential' || getBaseId(col.id) === 'store_potential'; - const isRequired = col.mandatory || (isCatSelected && (isProductName || isBags || isQuantity)); + {/* Grid Table Header */} +
+
+ {visibleColumns.map((col) => ( + {col.name} + ))} + +
- if (col.data_type === 'select' || col.data_type === 'multiselect') { - const allOpts = col.properties?.options || []; - let filteredOpts = allOpts; - - if (col.id === 'product_name' || col.name === 'Product Name' || col.mapped_workflow_field === 'product_name') { - const catCol = columns.find(c => c.id === 'product_category' || c.name === 'Product Category' || c.mapped_workflow_field === 'product_category'); - if (catCol) { - const selectedCategory = newRow[catCol.id] as string; - if (selectedCategory) { - filteredOpts = allOpts.filter(opt => { - const labelStr = String(opt.label || opt.value || ''); - return labelStr.startsWith(selectedCategory); - }); - } - } - } else { - filteredOpts = allOpts.filter(opt => { - if (!opt._raw) return true; - for (const [rowKey, rowVal] of Object.entries(newRow)) { - if (rowKey === col.id || rowVal == null || rowVal === '') continue; - const rawKey = Object.keys(opt._raw).find(rk => rk === rowKey || rk.replace(/_/g, '') === rowKey.replace(/_/g, '')); - if (rawKey && String(opt._raw[rawKey]) !== String(rowVal)) { - return false; - } - } - return true; - }); - } + {/* Rows List */} +
+ {rows.map((row, rowIdx) => { + const currentCatVal = String( + row.category ?? row.product_category ?? (catCol ? row[catCol.id] : '') ?? '' + ); + const currentProdVal = String( + row[prodNameCol?.id || 'product_name'] ?? row.product_name ?? row.productName ?? '' + ); + const currentBagsVal = row.bags ?? row.quantity ?? (bagsCol ? row[bagsCol.id] : '') ?? ''; + const prodOptions = getProductsForCategory(currentCatVal); + const isCategorySelected = Boolean(currentCatVal.trim()); + + // Compute row total kgs = sku * bags for display + const skuNum = Number(row.sku) || 0; + const bagsNum = Number(currentBagsVal) || 0; + const computedRowKgs = skuNum * bagsNum; + + // Only display red "Required" error badge when showErrors is true (on form submit) + const isMissingProd = isCategorySelected && !currentProdVal.trim(); + const prodError = showErrors && isMissingProd ? 'Required' : undefined; + + const isMissingBags = isCategorySelected && (!currentBagsVal || Number(currentBagsVal) <= 0); + const bagsError = showErrors && isMissingBags ? 'Required' : undefined; + + return ( +
+
+ {visibleColumns.map((col) => { + const fieldId = col.id; + + if (fieldId === '_row_total_kgs') { + return ( + 0 ? `${computedRowKgs.toLocaleString()} kgs` : '0 kgs'} + className="w-full font-semibold text-slate-700 bg-slate-50 border-slate-200" + /> + ); + } + + const isCat = isCategoryColumn(fieldId, col.name); + const isProd = isProductColumn(fieldId, col.name); + const isBagsOrNum = + col.data_type === 'number' || + fieldId.includes('bags') || + fieldId.includes('quantity') || + col.name?.toLowerCase().includes('bags'); + + if (isCat) { + return ( + updateRowField(rowIdx, fieldId, e.target.value)} + disabled={!isCategorySelected && categoriesList.length > 0} + options={[ + { value: '', label: isCategorySelected || categoriesList.length === 0 ? 'Select product' : 'Select category first' }, + ...prodOptions + ]} + error={prodError} + className="w-full" + /> + ); + } + + if (isBagsOrNum) { + return ( + updateRowField(rowIdx, fieldId, e.target.value)} + placeholder={isCategorySelected ? 'Bags' : '0'} + error={bagsError} + className="w-full" + /> + ); + } + + // Generic Select or Text Input + if (col.data_type === 'select' || col.data_type === 'multiselect') { + const opts = col.properties?.options || []; + return ( + updateRowField(rowIdx, fieldId, e.target.value)} + className="w-full" + /> + ); + })} + + {/* Remove Row Button */} + +
+
+ ); + })} +
+ + {/* Grid Footer - Positions the actual total_bags and total_kgs form fields right under Bags & Total kgs columns */} +
+
+ {visibleColumns.map((col, idx) => { + const fieldId = col.id; + const isBagsOrNum = + col.data_type === 'number' || + fieldId.includes('bags') || + fieldId.includes('quantity') || + col.name?.toLowerCase().includes('bags'); + + if (isBagsOrNum) { return ( - ); } - return ( - updateNewRowField(col.id, col.data_type === 'number' ? Number(e.target.value) : e.target.value)} - /> - ); + + if (fieldId === '_row_total_kgs') { + return ( + + ); + } + + if (idx === 0) { + return ( +
+ Total +
+ ); + } + + return
; })} - -
- - -
+
-
); diff --git a/src/components/forms/fields/TextField.tsx b/src/components/forms/fields/TextField.tsx index dede488..de35139 100644 --- a/src/components/forms/fields/TextField.tsx +++ b/src/components/forms/fields/TextField.tsx @@ -2,24 +2,30 @@ import { Input } from '../../reusable/Input'; export function TextField({ label, - type, + type = 'text', required, + disabled, value, onChange, + error, }: { label: string; - type: string; + type?: string; required?: boolean; + disabled?: boolean; value: string; onChange: (val: string) => void; + error?: string; }) { return ( onChange(e.target.value)} + error={error} /> ); }