From 785885c383920b328c552ea5978f36b4cae76f5c Mon Sep 17 00:00:00 2001 From: suryac Date: Tue, 7 Jul 2026 17:38:41 +0530 Subject: [PATCH] order and calls are done,fixed detail view with those table for grid --- src/components/dv/DetailView.tsx | 108 +++++++++++-- src/components/forms/DynamicForm.tsx | 70 ++++++--- .../forms/fields/SmartGridField.tsx | 145 ++++++++++++++++++ src/components/forms/fields/index.ts | 1 + src/components/reusable/Modal.tsx | 20 ++- src/screens/CallsPage.tsx | 35 ++++- src/screens/DailyLogsPage.tsx | 3 +- src/screens/OrdersPage.tsx | 35 ++++- src/screens/StoresPage.tsx | 3 +- 9 files changed, 369 insertions(+), 51 deletions(-) create mode 100644 src/components/forms/fields/SmartGridField.tsx diff --git a/src/components/dv/DetailView.tsx b/src/components/dv/DetailView.tsx index bf66ce5..baf69a3 100644 --- a/src/components/dv/DetailView.tsx +++ b/src/components/dv/DetailView.tsx @@ -3,6 +3,7 @@ import { cn } from '../../lib/cn'; import { formatValue } from '../../lib/format'; import type { ZinoClient } from '../../api/client'; import type { AuditEntry } from '../../api/types'; +import { WORKFLOWS } from '../../api/config'; import { Card } from '../reusable/Card'; import { Spinner } from '../reusable/Spinner'; import { EmptyState } from '../reusable/EmptyState'; @@ -22,6 +23,23 @@ export interface DetailViewProps { columns?: 1 | 2 | 3; } +/** + * Helper to resolve a human-readable activity name from its UID. + */ +function getActivityName(uid: string): string | undefined { + for (const wf of Object.values(WORKFLOWS)) { + for (const [actName, actDef] of Object.entries(wf.activities)) { + if (actDef.uid === uid) { + return actName + .split('_') + .map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()) + .join(' '); + } + } + } + return undefined; +} + /** * Generic Zino detail view. Fetches `GET /app/{id}/view/audit` and * renders the audit trail as a labeled definition grid. @@ -81,22 +99,80 @@ export function DetailView({ client, instanceId, title, columns = 2 }: DetailVie return (
- {entries.map((entry, i) => ( - -
- {Object.entries(entry.data).map(([key, value]) => ( -
-
- {key.replace(/_/g, ' ')} -
-
- {formatValue(value)} -
-
- ))} -
-
- ))} + {entries.map((entry, i) => { + const actName = getActivityName(entry.activity_id); + const headerTitle = actName || (i === 0 && title ? title : 'Update'); + const updatedAt = new Date(entry.created_at).toLocaleString(); + + return ( + {updatedAt}} + > +
+ {Object.entries(entry.data).map(([key, value]) => { + const isGridArray = + Array.isArray(value) && + value.length > 0 && + typeof value[0] === 'object' && + value[0] !== null && + !('original_name' in value[0]) && + !('url' in value[0]); + + if (isGridArray) { + const rows = value as Record[]; + // Use all unique keys found across all rows just in case they differ slightly + const headers = Array.from(new Set(rows.flatMap(r => Object.keys(r)))); + + return ( +
+
+ {key.replace(/_/g, ' ')} +
+
+ + + + {headers.map(h => ( + + ))} + + + + {rows.map((row, idx) => ( + + {headers.map(h => ( + + ))} + + ))} + +
+ {h.replace(/_/g, ' ')} +
+ {formatValue(row[h])} +
+
+
+ ); + } + + return ( +
+
+ {key.replace(/_/g, ' ')} +
+
+ {formatValue(value)} +
+
+ ); + })} +
+
+ ); + })}
); } diff --git a/src/components/forms/DynamicForm.tsx b/src/components/forms/DynamicForm.tsx index 9509a9f..9c847e2 100644 --- a/src/components/forms/DynamicForm.tsx +++ b/src/components/forms/DynamicForm.tsx @@ -5,7 +5,7 @@ import { Button } from '../buttons/Button'; import { Spinner } from '../reusable/Spinner'; import { FileInput, - GridInput, + SmartGridField, GeolocationInput, PhoneInput, SelectField, @@ -24,9 +24,10 @@ export interface DynamicFormProps { instanceId?: number | string; onSuccess?: () => void; onCancel?: () => void; + ignorePrefill?: boolean; } -export function DynamicForm({ client, activityId: initialActivityId, instanceId: initialInstanceId, onSuccess, onCancel }: DynamicFormProps) { +export function DynamicForm({ client, activityId: initialActivityId, instanceId: initialInstanceId, onSuccess, onCancel, ignorePrefill }: DynamicFormProps) { const [currentActivityId, setCurrentActivityId] = useState(initialActivityId); const [currentInstanceId, setCurrentInstanceId] = useState(initialInstanceId); const [chainQueue, setChainQueue] = useState>([]); @@ -71,15 +72,18 @@ export function DynamicForm({ client, activityId: initialActivityId, instanceId: res.fields.filter(f => f.data_type === 'image' || f.data_type === 'file').map(f => f.id) ); - if (res.prefill_data) { - Object.entries(res.prefill_data).forEach(([k, v]) => { - if (!imageFieldIds.has(k)) defaultValues[k] = v; - }); - } else if (res.data) { - Object.entries(res.data).forEach(([k, v]) => { - if (!imageFieldIds.has(k)) defaultValues[k] = v; - }); + if (!ignorePrefill) { + if (res.prefill_data) { + Object.entries(res.prefill_data).forEach(([k, v]) => { + if (!imageFieldIds.has(k)) defaultValues[k] = v; + }); + } else if (res.data) { + Object.entries(res.data).forEach(([k, v]) => { + if (!imageFieldIds.has(k)) defaultValues[k] = v; + }); + } } + setValues(defaultValues); setLoading(false); } @@ -97,6 +101,26 @@ export function DynamicForm({ client, activityId: initialActivityId, instanceId: const [submitting, setSubmitting] = useState(false); const [submitError, setSubmitError] = useState(null); + const handleFieldChange = (fieldId: string, newVal: unknown) => { + setValues(prev => { + const next = { ...prev, [fieldId]: newVal }; + + // Auto-calculate order_details totals + if (fieldId === 'order_details' && Array.isArray(newVal)) { + let totalBags = 0; + let totalKgs = 0; + newVal.forEach(row => { + totalBags += Number(row.bags) || 0; + totalKgs += Number(row.row_kgs) || 0; + }); + next['total_bags'] = totalBags; + next['total_kgs'] = totalKgs; + } + + return next; + }); + }; + if (loading) { return
; } @@ -193,7 +217,7 @@ export function DynamicForm({ client, activityId: initialActivityId, instanceId: activityId={currentActivityId} fieldId={f.id} formData={values} - onChange={(newVal) => setValues(prev => ({ ...prev, [f.id]: newVal }))} + onChange={(newVal) => handleFieldChange(f.id, newVal)} /> ); } @@ -205,7 +229,7 @@ export function DynamicForm({ client, activityId: initialActivityId, instanceId: required={f.mandatory} value={(val as string) ?? ''} options={f.properties?.options || []} - onChange={(newVal) => setValues(prev => ({ ...prev, [f.id]: newVal }))} + onChange={(newVal) => handleFieldChange(f.id, newVal)} /> ); } @@ -217,7 +241,7 @@ export function DynamicForm({ client, activityId: initialActivityId, instanceId: required={f.mandatory} value={(val as string) ?? ''} options={f.properties?.options || []} - onChange={(newVal) => setValues(prev => ({ ...prev, [f.id]: newVal }))} + onChange={(newVal) => handleFieldChange(f.id, newVal)} /> ); } @@ -229,18 +253,18 @@ export function DynamicForm({ client, activityId: initialActivityId, instanceId: type={type} required={f.mandatory} value={val} - onChange={(newVal) => setValues(prev => ({ ...prev, [f.id]: newVal }))} + onChange={(newVal) => handleFieldChange(f.id, newVal)} /> ); } if (type.startsWith('grid')) { return ( - []) || []} - onChange={(newVal) => setValues(prev => ({ ...prev, [f.id]: newVal }))} + onChange={(newVal) => handleFieldChange(f.id, newVal)} /> ); } @@ -251,7 +275,7 @@ export function DynamicForm({ client, activityId: initialActivityId, instanceId: label={f.name} required={f.mandatory} value={(val as { latitude: number; longitude: number }) || null} - onChange={(newVal) => setValues(prev => ({ ...prev, [f.id]: newVal }))} + onChange={(newVal) => handleFieldChange(f.id, newVal)} /> ); } @@ -263,7 +287,7 @@ export function DynamicForm({ client, activityId: initialActivityId, instanceId: label={f.name} required={f.mandatory} value={phoneStr} - onChange={(newVal) => setValues(prev => ({ ...prev, [f.id]: newVal }))} + onChange={(newVal) => handleFieldChange(f.id, newVal)} /> ); } @@ -274,7 +298,7 @@ export function DynamicForm({ client, activityId: initialActivityId, instanceId: label={f.name} required={f.mandatory} value={(val as string) ?? ''} - onChange={(newVal) => setValues(prev => ({ ...prev, [f.id]: newVal }))} + onChange={(newVal) => handleFieldChange(f.id, newVal)} /> ); } @@ -285,7 +309,7 @@ export function DynamicForm({ client, activityId: initialActivityId, instanceId: label={f.name} required={f.mandatory} value={(val as string) ?? ''} - onChange={(newVal) => setValues(prev => ({ ...prev, [f.id]: newVal }))} + onChange={(newVal) => handleFieldChange(f.id, newVal)} /> ); } @@ -296,7 +320,7 @@ export function DynamicForm({ client, activityId: initialActivityId, instanceId: label={f.name} required={f.mandatory} value={(val as string) ?? ''} - onChange={(newVal) => setValues(prev => ({ ...prev, [f.id]: newVal }))} + onChange={(newVal) => handleFieldChange(f.id, newVal)} /> ); } @@ -307,7 +331,7 @@ export function DynamicForm({ client, activityId: initialActivityId, instanceId: label={f.name} required={f.mandatory} value={(val as string) ?? ''} - onChange={(newVal) => setValues(prev => ({ ...prev, [f.id]: newVal }))} + onChange={(newVal) => handleFieldChange(f.id, newVal)} /> ); } @@ -318,7 +342,7 @@ export function DynamicForm({ client, activityId: initialActivityId, instanceId: required={f.mandatory} type={type} value={(val as string) ?? ''} - onChange={(newVal) => setValues(prev => ({ ...prev, [f.id]: newVal }))} + onChange={(newVal) => handleFieldChange(f.id, newVal)} /> ); }; diff --git a/src/components/forms/fields/SmartGridField.tsx b/src/components/forms/fields/SmartGridField.tsx new file mode 100644 index 0000000..8c45803 --- /dev/null +++ b/src/components/forms/fields/SmartGridField.tsx @@ -0,0 +1,145 @@ +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[]; + 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]; + 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 ( +
+ {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') { + // 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 ( + updateRow(i, col.id, col.data_type === 'number' ? Number(e.target.value) : e.target.value)} + /> + ); + })} +
+
+ ))} +
+ )} + +
+ ); +} diff --git a/src/components/forms/fields/index.ts b/src/components/forms/fields/index.ts index 37dd11a..3424ba8 100644 --- a/src/components/forms/fields/index.ts +++ b/src/components/forms/fields/index.ts @@ -10,3 +10,4 @@ export * from './DateField'; export * from './TimeField'; export * from './WfLookupField'; export * from './RadioField'; +export * from './SmartGridField'; diff --git a/src/components/reusable/Modal.tsx b/src/components/reusable/Modal.tsx index 00b34c5..abdda9e 100644 --- a/src/components/reusable/Modal.tsx +++ b/src/components/reusable/Modal.tsx @@ -19,11 +19,12 @@ export interface ModalProps { subtitle?: ReactNode; /** @default "md" */ width?: ModalWidth; + actions?: ReactNode; children?: ReactNode; } /** Portal modal host — backdrop, Esc / click-out close, scroll-locked body. */ -export function Modal({ open, onClose, title, subtitle, width = 'md', children }: ModalProps) { +export function Modal({ open, onClose, title, subtitle, width = 'md', actions, children }: ModalProps) { useEffect(() => { if (!open) return; const onKey = (e: KeyboardEvent) => { @@ -61,13 +62,16 @@ export function Modal({ open, onClose, title, subtitle, width = 'md', children } {title &&

{title}

} {subtitle &&
{subtitle}
} - +
+ {actions &&
{actions}
} + +
{children}
diff --git a/src/screens/CallsPage.tsx b/src/screens/CallsPage.tsx index 24b254a..0bd6938 100644 --- a/src/screens/CallsPage.tsx +++ b/src/screens/CallsPage.tsx @@ -10,10 +10,12 @@ import { ORDER_BOOKING } from '../api/config'; import { orderBookingClient } from '../api/clients'; export function CallsPage() { - const { instanceId } = useParams(); + const params = useParams(); + const instanceId = params.instanceId ? Number(params.instanceId) : undefined; const navigate = useNavigate(); const [isCreating, setIsCreating] = useState(false); const [refreshKey, setRefreshKey] = useState(0); + const [activeActivity, setActiveActivity] = useState<{ id: string; name: string } | null>(null); return ( <> @@ -52,9 +54,40 @@ export function CallsPage() { onClose={() => navigate(`/calls`)} title={instanceId != null ? `Call #${instanceId}` : undefined} width="lg" + actions={ + <> + + + + } > {instanceId != null && } + + setActiveActivity(null)} + title={activeActivity?.name} + width="md" + > + {activeActivity && instanceId != null && ( + { + setActiveActivity(null); + setRefreshKey(k => k + 1); + }} + onCancel={() => setActiveActivity(null)} + /> + )} + ); } diff --git a/src/screens/DailyLogsPage.tsx b/src/screens/DailyLogsPage.tsx index 62c2c48..908304b 100644 --- a/src/screens/DailyLogsPage.tsx +++ b/src/screens/DailyLogsPage.tsx @@ -10,7 +10,8 @@ import { DAILY_REPORTS } from '../api/config'; import { dailyReportsClient } from '../api/clients'; export function DailyLogsPage() { - const { instanceId } = useParams(); + const params = useParams(); + const instanceId = params.instanceId ? Number(params.instanceId) : undefined; const navigate = useNavigate(); const [isCreating, setIsCreating] = useState(false); const [punchOutInstanceId, setPunchOutInstanceId] = useState(null); diff --git a/src/screens/OrdersPage.tsx b/src/screens/OrdersPage.tsx index 1db86dd..3f85b3a 100644 --- a/src/screens/OrdersPage.tsx +++ b/src/screens/OrdersPage.tsx @@ -10,10 +10,12 @@ import { ORDER_BOOKING } from '../api/config'; import { orderBookingClient } from '../api/clients'; export function OrdersPage() { - const { instanceId } = useParams(); + const params = useParams(); + const instanceId = params.instanceId ? Number(params.instanceId) : undefined; const navigate = useNavigate(); const [isCreating, setIsCreating] = useState(false); const [refreshKey, setRefreshKey] = useState(0); + const [activeActivity, setActiveActivity] = useState<{ id: string; name: string } | null>(null); return ( <> @@ -52,9 +54,40 @@ export function OrdersPage() { onClose={() => navigate(`/orders`)} title={instanceId != null ? `Order #${instanceId}` : undefined} width="lg" + actions={ + <> + + + + } > {instanceId != null && } + + setActiveActivity(null)} + title={activeActivity?.name} + width="md" + > + {activeActivity && instanceId != null && ( + { + setActiveActivity(null); + setRefreshKey(k => k + 1); + }} + onCancel={() => setActiveActivity(null)} + /> + )} + ); } diff --git a/src/screens/StoresPage.tsx b/src/screens/StoresPage.tsx index 8bc799a..dd9e1cc 100644 --- a/src/screens/StoresPage.tsx +++ b/src/screens/StoresPage.tsx @@ -10,7 +10,8 @@ import { STORE } from '../api/config'; import { storeClient } from '../api/clients'; export function StoresPage() { - const { instanceId } = useParams(); + const params = useParams(); + const instanceId = params.instanceId ? Number(params.instanceId) : undefined; const navigate = useNavigate(); const [isCreating, setIsCreating] = useState(false); const [editingInstanceId, setEditingInstanceId] = useState(null);