form grid implemented

This commit is contained in:
suryacp23 2026-07-22 19:00:00 +05:30
parent f7efd4d9cc
commit 77c596e3a1
4 changed files with 881 additions and 406 deletions

View File

@ -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<Record<string, unknown>>({});
const [submitting, setSubmitting] = useState(false);
const [submitError, setSubmitError] = useState<string | null>(null);
const [showErrors, setShowErrors] = useState(false);
const clickedActionRef = useRef<string | null>(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,15 +191,192 @@ export function DynamicForm({ client, activityId: initialActivityId, instanceId:
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setSubmitting(true);
setShowErrors(true);
setSubmitError(null);
try {
const payload: Record<string, unknown> = {};
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<string, unknown>[]) || [];
if (rawGrid.length > 0) {
const merged: Record<string, unknown>[] = [];
const indexMap = new Map<string, number>();
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<string, unknown> = {};
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<string, unknown>, 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<string, unknown>[]) || [];
// 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<string, unknown> = {};
for (const f of fields) {
const val = finalValues[f.id];
if (val == null) continue;
@ -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 (
<form onSubmit={handleSubmit} className="flex flex-col gap-4">
{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<string, unknown>[]) || []}
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 (
<TextField
label={f.name}
@ -487,6 +699,7 @@ export function DynamicForm({ client, activityId: initialActivityId, instanceId:
type={type}
value={(val as string) ?? ''}
onChange={(newVal) => handleFieldChange(f.id, newVal)}
error={currentFieldError}
/>
);
};

View File

@ -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<string, unknown>[];
onChange: (val: Record<string, unknown>[]) => 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 (
<div className="flex flex-col gap-2 font-sans border border-border-default rounded-md p-4 bg-slate-50">
<span className="text-sm font-semibold text-strong mb-2">{label}</span>
{value.length === 0 ? (
<span className="text-sm text-faint italic">No rows added.</span>
) : (
<div className="flex flex-col gap-4">
{value.map((row, i) => (
<div key={i} className="flex flex-col gap-3 p-3 bg-white border border-border-subtle rounded relative shadow-sm">
<div className="absolute top-2 right-2">
<button type="button" onClick={() => removeRow(i)} className="text-xs text-ruby-600 font-medium hover:underline">
Remove
</button>
</div>
<span className="text-xs font-bold text-muted uppercase tracking-wider">Row {i + 1}</span>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
{columns.map(col => {
const val = row[col.id];
if (col.data_type === 'select' || col.data_type === 'multiselect') {
const opts = col.properties?.options || [];
return (
<Select
key={col.id}
label={col.name}
required={col.mandatory}
value={(val as string) ?? ''}
onChange={(e) => updateRow(i, col.id, e.target.value)}
options={[{ value: '', label: 'Select...' }, ...opts.map((o: any) => ({ value: String(o.value), label: o.label }))]}
/>
);
}
return (
<Input
key={col.id}
label={col.name}
required={col.mandatory}
type={col.data_type === 'number' ? 'number' : col.data_type === 'email' ? 'email' : 'text'}
value={(val as string) ?? ''}
onChange={(e) => updateRow(i, col.id, col.data_type === 'number' ? Number(e.target.value) : e.target.value)}
/>
);
})}
</div>
</div>
))}
</div>
)}
<Button type="button" variant="secondary" size="sm" onClick={addRow} className="mt-2 self-start">
+ Add Row
</Button>
</div>
);
export function GridInput(props: SmartGridFieldProps) {
return <SmartGridField {...props} />;
}

File diff suppressed because it is too large Load Diff

View File

@ -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 (
<Input
label={label}
required={required}
disabled={disabled}
type={type === 'number' ? 'number' : type === 'email' ? 'email' : 'text'}
value={value ?? ''}
onChange={(e) => onChange(e.target.value)}
error={error}
/>
);
}