form grid implemented
This commit is contained in:
parent
f7efd4d9cc
commit
77c596e3a1
@ -1,6 +1,6 @@
|
|||||||
import { useState, useEffect, useRef } from 'react';
|
import { useState, useEffect, useRef } from 'react';
|
||||||
import type { ZinoClient } from '../../api/client';
|
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 { Button } from '../buttons/Button';
|
||||||
import { Spinner } from '../reusable/Spinner';
|
import { Spinner } from '../reusable/Spinner';
|
||||||
import {
|
import {
|
||||||
@ -121,10 +121,14 @@ export function DynamicForm({ client, activityId: initialActivityId, instanceId:
|
|||||||
const [values, setValues] = useState<Record<string, unknown>>({});
|
const [values, setValues] = useState<Record<string, unknown>>({});
|
||||||
const [submitting, setSubmitting] = useState(false);
|
const [submitting, setSubmitting] = useState(false);
|
||||||
const [submitError, setSubmitError] = useState<string | null>(null);
|
const [submitError, setSubmitError] = useState<string | null>(null);
|
||||||
|
const [showErrors, setShowErrors] = useState(false);
|
||||||
|
|
||||||
const clickedActionRef = useRef<string | null>(null);
|
const clickedActionRef = useRef<string | null>(null);
|
||||||
|
|
||||||
const handleFieldChange = (fieldId: string, newVal: unknown, fullRow?: any) => {
|
const handleFieldChange = (fieldId: string, newVal: unknown, fullRow?: any) => {
|
||||||
|
if (submitError) {
|
||||||
|
setSubmitError(null);
|
||||||
|
}
|
||||||
setValues(prev => {
|
setValues(prev => {
|
||||||
const next = { ...prev, [fieldId]: newVal };
|
const next = { ...prev, [fieldId]: newVal };
|
||||||
if (fullRow) {
|
if (fullRow) {
|
||||||
@ -151,8 +155,11 @@ export function DynamicForm({ client, activityId: initialActivityId, instanceId:
|
|||||||
)?.id || 'row_kgs';
|
)?.id || 'row_kgs';
|
||||||
|
|
||||||
newVal.forEach(row => {
|
newVal.forEach(row => {
|
||||||
totalBags += Number(row[bagsColId]) || 0;
|
const bags = Number(row[bagsColId] ?? row.bags ?? row.quantity) || 0;
|
||||||
totalKgs += Number(row[rowKgsColId]) || 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');
|
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) => {
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setSubmitting(true);
|
setShowErrors(true);
|
||||||
setSubmitError(null);
|
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<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 {
|
try {
|
||||||
const payload: Record<string, unknown> = {};
|
const payload: Record<string, unknown> = {};
|
||||||
const finalValues = { ...values };
|
|
||||||
if (actionField && clickedActionRef.current) {
|
|
||||||
finalValues[actionField.id] = clickedActionRef.current;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const f of fields) {
|
for (const f of fields) {
|
||||||
const val = finalValues[f.id];
|
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 (
|
return (
|
||||||
<form onSubmit={handleSubmit} className="flex flex-col gap-4">
|
<form onSubmit={handleSubmit} className="flex flex-col gap-4">
|
||||||
{normalFields.map(f => {
|
{displayFields.map(f => {
|
||||||
const type = f.data_type;
|
const type = f.data_type;
|
||||||
const val = values[f.id];
|
const val = values[f.id];
|
||||||
const isDisabled = schema.field_defaults?.[f.id]?.disabled;
|
const isDisabled = schema.field_defaults?.[f.id]?.disabled;
|
||||||
@ -409,6 +615,9 @@ export function DynamicForm({ client, activityId: initialActivityId, instanceId:
|
|||||||
columns={f.columns || []}
|
columns={f.columns || []}
|
||||||
value={(val as Record<string, unknown>[]) || []}
|
value={(val as Record<string, unknown>[]) || []}
|
||||||
onChange={(newVal) => handleFieldChange(f.id, newVal)}
|
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 (
|
return (
|
||||||
<TextField
|
<TextField
|
||||||
label={f.name}
|
label={f.name}
|
||||||
@ -487,6 +699,7 @@ export function DynamicForm({ client, activityId: initialActivityId, instanceId:
|
|||||||
type={type}
|
type={type}
|
||||||
value={(val as string) ?? ''}
|
value={(val as string) ?? ''}
|
||||||
onChange={(newVal) => handleFieldChange(f.id, newVal)}
|
onChange={(newVal) => handleFieldChange(f.id, newVal)}
|
||||||
|
error={currentFieldError}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,85 +1,5 @@
|
|||||||
import { Button } from '../../buttons/Button';
|
import { SmartGridField, type SmartGridFieldProps } from './SmartGridField';
|
||||||
import { Select } from '../../reusable/Select';
|
|
||||||
import { Input } from '../../reusable/Input';
|
|
||||||
import type { FormScreenField } from '../../../api/types';
|
|
||||||
|
|
||||||
export function GridInput({
|
export function GridInput(props: SmartGridFieldProps) {
|
||||||
label,
|
return <SmartGridField {...props} />;
|
||||||
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>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -2,24 +2,30 @@ import { Input } from '../../reusable/Input';
|
|||||||
|
|
||||||
export function TextField({
|
export function TextField({
|
||||||
label,
|
label,
|
||||||
type,
|
type = 'text',
|
||||||
required,
|
required,
|
||||||
|
disabled,
|
||||||
value,
|
value,
|
||||||
onChange,
|
onChange,
|
||||||
|
error,
|
||||||
}: {
|
}: {
|
||||||
label: string;
|
label: string;
|
||||||
type: string;
|
type?: string;
|
||||||
required?: boolean;
|
required?: boolean;
|
||||||
|
disabled?: boolean;
|
||||||
value: string;
|
value: string;
|
||||||
onChange: (val: string) => void;
|
onChange: (val: string) => void;
|
||||||
|
error?: string;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<Input
|
<Input
|
||||||
label={label}
|
label={label}
|
||||||
required={required}
|
required={required}
|
||||||
|
disabled={disabled}
|
||||||
type={type === 'number' ? 'number' : type === 'email' ? 'email' : 'text'}
|
type={type === 'number' ? 'number' : type === 'email' ? 'email' : 'text'}
|
||||||
value={value ?? ''}
|
value={value ?? ''}
|
||||||
onChange={(e) => onChange(e.target.value)}
|
onChange={(e) => onChange(e.target.value)}
|
||||||
|
error={error}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user