fixed the form table smart grid

This commit is contained in:
suryacp23 2026-07-14 15:40:33 +05:30
parent 95d0c00dd4
commit 3eb445d506
6 changed files with 194 additions and 110 deletions

View File

@ -3,7 +3,7 @@ import { cn } from '../../lib/cn';
import './style.css'
export type ButtonVariant = 'primary' | 'navy' | 'secondary' | 'ghost' | 'danger' | 'outline';
export type ButtonSize = 'sm' | 'md' | 'lg';
export type ButtonSize = 'sm' | 'md' | 'lg' | 'fab';
export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
/** Visual style. @default "primary" */
@ -20,6 +20,7 @@ const SIZES: Record<ButtonSize, string> = {
sm: 'h-[34px] px-3.5 text-[13px] rounded-sm',
md: 'h-[42px] px-[18px] text-base rounded-md',
lg: 'h-[50px] px-6 text-md rounded-md',
fab: 'h-[56px] w-[56px] text-sm rounded-full',
};
const VARIANTS: Record<ButtonVariant, string> = {
@ -54,7 +55,7 @@ export function Button({
'disabled:opacity-50 disabled:cursor-not-allowed disabled:active:scale-100',
SIZES[size],
VARIANTS[variant],
full ? 'w-full' : 'w-auto',
full ? 'w-full' : '',
className,
)}
{...rest}

View File

@ -17,6 +17,7 @@ import {
WfLookupField,
RadioField,
} from './fields';
import { ORDER_BOOKING } from '../../api/config';
export interface DynamicFormProps {
client: ZinoClient;
@ -172,12 +173,25 @@ export function DynamicForm({ client, activityId: initialActivityId, instanceId:
}
let pending = [...chainQueue];
const newActivities = (schema.activity_chain || []).filter(
a => a.activity_uid !== currentActivityId
let chainSource = (res as any).activity_chain || schema.activity_chain || [];
// Centralized activity chaining logic
if (currentActivityId === ORDER_BOOKING.activities.PRODUCTIVITY_OF_VISIT.uid) {
const actionValue = String(payload[ORDER_BOOKING.activities.PRODUCTIVITY_OF_VISIT.fields.action] || '').toLowerCase().trim();
const normalized = actionValue.replace(/[\s_]+/g, '');
if (normalized === 'order') {
chainSource = [{ activity_uid: ORDER_BOOKING.activities.PLACE_ORDER.uid, activity_name: 'Place Order' }];
} else if (normalized === 'noorder') {
chainSource = [{ activity_uid: ORDER_BOOKING.activities.POTENTIAL_MINING.uid, activity_name: 'Potential Mining' }];
}
}
const newActivities = chainSource.filter(
(a: any) => a.activity_uid !== currentActivityId
);
// Remove any existing occurrences from pending to avoid duplicates
pending = pending.filter(p => !newActivities.some(n => n.activity_uid === p.activity_uid));
pending = pending.filter(p => !newActivities.some((n: any) => n.activity_uid === p.activity_uid));
// Prepend the new activities for depth-first execution (nested chaining)
pending = [...newActivities, ...pending];

View File

@ -1,6 +1,9 @@
import { useState } from 'react';
import { Button } from '../../buttons/Button';
import { Select } from '../../reusable/Select';
import { Input } from '../../reusable/Input';
import { Modal } from '../../reusable/Modal';
import { Trash2, Pencil } from 'lucide-react';
import type { FormScreenField } from '../../../api/types';
export function SmartGridField({
@ -14,7 +17,14 @@ export function SmartGridField({
value: Record<string, unknown>[];
onChange: (val: Record<string, unknown>[]) => void;
}) {
const addRow = () => onChange([...value, {}]);
const [isModalOpen, setIsModalOpen] = useState(false);
const [newRow, setNewRow] = useState<Record<string, unknown>>({});
const [editingIdx, setEditingIdx] = useState<number | null>(null);
const visibleColumns = columns.filter(c => {
const normalized = c.id.toLowerCase().replace(/[^a-z]/g, '');
return ['productcategory', 'productname', 'bags'].includes(normalized);
});
const removeRow = (idx: number) => {
const next = [...value];
@ -22,9 +32,20 @@ export function SmartGridField({
onChange(next);
};
const updateRow = (idx: number, fieldId: string, val: unknown) => {
const next = [...value];
let row = { ...next[idx], [fieldId]: val };
const startEdit = (idx: number) => {
setEditingIdx(idx);
setNewRow({ ...value[idx] });
setIsModalOpen(true);
};
const startAdd = () => {
setEditingIdx(null);
setNewRow({});
setIsModalOpen(true);
};
const updateNewRowField = (fieldId: string, val: unknown) => {
let row = { ...newRow, [fieldId]: val };
const colDef = columns.find(c => c.id === fieldId);
@ -42,7 +63,6 @@ export function SmartGridField({
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];
@ -56,38 +76,87 @@ export function SmartGridField({
const bagsVal = Number(row.bags) || 0;
row.row_kgs = skuVal * bagsVal;
next[idx] = row;
setNewRow(row);
};
const submitNewRow = () => {
if (editingIdx !== null) {
const next = [...value];
next[editingIdx] = newRow;
onChange(next);
} else {
onChange([...value, newRow]);
}
setIsModalOpen(false);
setNewRow({});
setEditingIdx(null);
};
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">
<div className="overflow-x-auto">
<table className="w-full text-left border-collapse bg-white border border-border-subtle shadow-sm rounded">
<thead>
<tr className="bg-sunk border-b border-border-default">
{visibleColumns.map(col => (
<th key={col.id} className="p-3 text-xs font-semibold text-muted uppercase tracking-wider whitespace-nowrap">
{col.name}
</th>
))}
<th className="p-3 text-xs font-semibold text-muted uppercase tracking-wider w-16"></th>
</tr>
</thead>
<tbody>
{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 => {
<tr key={i} className="border-b border-border-subtle last:border-b-0 hover:bg-slate-50/50">
{visibleColumns.map(col => (
<td key={col.id} className="p-3 text-sm text-strong whitespace-nowrap">
{(() => {
const val = row[col.id];
if (col.data_type === 'select' || col.data_type === 'multiselect') {
// Cascade filter options
const opt = col.properties?.options?.find(o => String(o.value) === String(val));
return opt ? opt.label : String(val ?? '-');
}
return String(val ?? '-');
})()}
</td>
))}
<td className="p-2 align-middle text-center flex items-center justify-end gap-1">
<button type="button" onClick={() => startEdit(i)} className="text-blue-600 hover:bg-blue-50 p-1.5 rounded transition-colors" title="Edit row">
<Pencil size={16} />
</button>
<button type="button" onClick={() => removeRow(i)} className="text-ruby-600 hover:bg-ruby-50 p-1.5 rounded transition-colors" title="Remove row">
<Trash2 size={16} />
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
<Button type="button" variant="secondary" size="sm" onClick={startAdd} className="mt-2 self-start">
+ Add Row
</Button>
<Modal open={isModalOpen} onClose={() => setIsModalOpen(false)} title={editingIdx !== null ? "Edit Row" : "Add Row"} width="sm">
<form onSubmit={(e) => { e.preventDefault(); e.stopPropagation(); submitNewRow(); }} className="flex flex-col gap-4">
{visibleColumns.map(col => {
const val = newRow[col.id];
if (col.data_type === 'select' || col.data_type === 'multiselect') {
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;
const selectedCategory = newRow[catCol.id] as string;
if (selectedCategory) {
filteredOpts = allOpts.filter(opt => {
const labelStr = String(opt.label || opt.value || '');
@ -96,10 +165,9 @@ export function SmartGridField({
}
}
} 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)) {
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)) {
@ -116,7 +184,7 @@ export function SmartGridField({
label={col.name}
required={col.mandatory}
value={(val as string) ?? ''}
onChange={(e) => updateRow(i, col.id, e.target.value)}
onChange={(e) => updateNewRowField(col.id, e.target.value)}
options={[{ value: '', label: 'Select...' }, ...filteredOpts.map((o: any) => ({ value: String(o.value), label: o.label }))]}
/>
);
@ -128,18 +196,16 @@ export function SmartGridField({
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)}
onChange={(e) => updateNewRowField(col.id, col.data_type === 'number' ? Number(e.target.value) : e.target.value)}
/>
);
})}
<div className="flex justify-end gap-3 mt-4">
<Button type="button" variant="ghost" onClick={() => setIsModalOpen(false)}>Cancel</Button>
<Button type="submit" variant="primary">{editingIdx !== null ? "Save" : "Add"}</Button>
</div>
</div>
))}
</div>
)}
<Button type="button" variant="secondary" size="sm" onClick={addRow} className="mt-2 self-start">
+ Add Row
</Button>
</form>
</Modal>
</div>
);
}

View File

@ -52,7 +52,7 @@ export function Modal({ open, onClose, title, subtitle, width = 'md', actions, c
role="dialog"
aria-modal="true"
className={cn(
'w-full bg-card rounded-t-2xl sm:rounded-lg border border-border-subtle shadow-lg sm:my-auto',
'relative w-full bg-card rounded-t-2xl sm:rounded-lg border border-border-subtle shadow-lg sm:my-auto',
'flex flex-col max-h-[92vh] sm:max-h-[85vh]',
WIDTH[width],
)}

View File

@ -16,6 +16,7 @@ export function CallsPage() {
const [isCreating, setIsCreating] = useState(false);
const [refreshKey, setRefreshKey] = useState(0);
const [activeActivity, setActiveActivity] = useState<{ id: string; name: string } | null>(null);
const [isFabOpen, setIsFabOpen] = useState(false);
return (
<>
@ -51,21 +52,33 @@ export function CallsPage() {
<Modal
open={instanceId != null}
onClose={() => navigate(`/calls`)}
onClose={() => { navigate(`/calls`); setIsFabOpen(false); }}
title={instanceId != null ? `Call #${instanceId}` : undefined}
width="lg"
actions={
>
{instanceId != null && (
<>
<Button size="sm" variant="secondary" onClick={() => setActiveActivity({ id: ORDER_BOOKING.activities.POTENTIAL_MINING.uid, name: 'Potential Mining' })}>
<CallDetail instanceId={instanceId} />
<div className="absolute bottom-6 right-6 flex flex-col items-end gap-3 z-50">
{isFabOpen && (
<div className="flex flex-col gap-2 bg-white p-3 rounded-sm shadow-xl border border-border-subtle animate-in fade-in slide-in-from-bottom-2">
<Button size="sm" variant="secondary" onClick={() => { setActiveActivity({ id: ORDER_BOOKING.activities.POTENTIAL_MINING.uid, name: 'Potential Mining' }); setIsFabOpen(false); }}>
Potential Mining
</Button>
<Button size="sm" onClick={() => setActiveActivity({ id: ORDER_BOOKING.activities.PLACE_ORDER.uid, name: 'Place Order' })}>
<Button size="sm" onClick={() => { setActiveActivity({ id: ORDER_BOOKING.activities.PLACE_ORDER.uid, name: 'Place Order' }); setIsFabOpen(false); }}>
Place Order
</Button>
</>
}
</div>
)}
<Button size='fab'
className="rounded-full shadow-xl flex items-center justify-center !p-0"
onClick={() => setIsFabOpen(!isFabOpen)}
>
{instanceId != null && <CallDetail instanceId={instanceId} />}
<Plus size={24} className={`transition-transform duration-200 ${isFabOpen ? "rotate-45" : ""}`} />
</Button>
</div>
</>
)}
</Modal>
<Modal

View File

@ -54,16 +54,6 @@ export function OrdersPage() {
onClose={() => navigate(`/orders`)}
title={instanceId != null ? `Order #${instanceId}` : undefined}
width="lg"
actions={
<>
<Button size="sm" variant="secondary" onClick={() => setActiveActivity({ id: ORDER_BOOKING.activities.POTENTIAL_MINING.uid, name: 'Potential Mining' })}>
Potential Mining
</Button>
<Button size="sm" onClick={() => setActiveActivity({ id: ORDER_BOOKING.activities.PLACE_ORDER.uid, name: 'Place Order' })}>
Place Order
</Button>
</>
}
>
{instanceId != null && <OrderDetail instanceId={instanceId} />}
</Modal>