146 lines
6.0 KiB
TypeScript
146 lines
6.0 KiB
TypeScript
import { Button } from '../../buttons/Button';
|
|
import { Select } from '../../reusable/Select';
|
|
import { Input } from '../../reusable/Input';
|
|
import type { FormScreenField } from '../../../api/types';
|
|
|
|
export function SmartGridField({
|
|
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];
|
|
let row = { ...next[idx], [fieldId]: val };
|
|
|
|
const colDef = columns.find(c => c.id === fieldId);
|
|
|
|
// If product category changes, clear out the rest of the row's data
|
|
if (colDef && (colDef.id === 'product_category' || colDef.name === 'Product Category')) {
|
|
Object.keys(row).forEach(k => {
|
|
if (k !== fieldId) {
|
|
row[k] = '';
|
|
}
|
|
});
|
|
}
|
|
|
|
// 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) {
|
|
for (const key of Object.keys(option._raw)) {
|
|
// Match column ID with or without underscores (e.g., brcode <-> br_code)
|
|
const targetCol = columns.find(c => c.id === key || c.id.replace(/_/g, '') === key.replace(/_/g, ''));
|
|
if (targetCol && targetCol.id !== fieldId) {
|
|
row[targetCol.id] = option._raw[key];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Auto-calculate row_kgs if sku and bags are present
|
|
const skuVal = Number(row.sku) || 0;
|
|
const bagsVal = Number(row.bags) || 0;
|
|
row.row_kgs = skuVal * bagsVal;
|
|
|
|
next[idx] = row;
|
|
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') {
|
|
// Cascade filter options
|
|
const allOpts = col.properties?.options || [];
|
|
let filteredOpts = allOpts;
|
|
|
|
// Specific logic for product_name depending on product_category
|
|
if (col.id === 'product_name' || col.name === 'Product Name') {
|
|
const catCol = columns.find(c => c.id === 'product_category' || c.name === 'Product Category');
|
|
if (catCol) {
|
|
const selectedCategory = row[catCol.id] as string;
|
|
if (selectedCategory) {
|
|
filteredOpts = allOpts.filter(opt => {
|
|
const labelStr = String(opt.label || opt.value || '');
|
|
return labelStr.startsWith(selectedCategory);
|
|
});
|
|
}
|
|
}
|
|
} else {
|
|
// Generic _raw cascading for other fields just in case
|
|
filteredOpts = allOpts.filter(opt => {
|
|
if (!opt._raw) return true;
|
|
for (const [rowKey, rowVal] of Object.entries(row)) {
|
|
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;
|
|
});
|
|
}
|
|
|
|
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...' }, ...filteredOpts.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>
|
|
);
|
|
}
|