fixed the form table smart grid
This commit is contained in:
parent
95d0c00dd4
commit
3eb445d506
@ -2,8 +2,8 @@ import type { ButtonHTMLAttributes, ReactNode } from 'react';
|
||||
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 ButtonVariant = 'primary' | 'navy' | 'secondary' | 'ghost' | 'danger' | 'outline';
|
||||
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> = {
|
||||
@ -28,7 +29,7 @@ const VARIANTS: Record<ButtonVariant, string> = {
|
||||
secondary: 'bg-card text-strong border border-border-default shadow-xs',
|
||||
ghost: 'bg-transparent text-body border border-transparent',
|
||||
danger: 'bg-ruby-600 text-white border border-transparent shadow-sm',
|
||||
outline:'z-btn-outline'
|
||||
outline: 'z-btn-outline'
|
||||
};
|
||||
|
||||
/**
|
||||
@ -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}
|
||||
|
||||
@ -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];
|
||||
|
||||
@ -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,90 +76,136 @@ export function SmartGridField({
|
||||
const bagsVal = Number(row.bags) || 0;
|
||||
row.row_kgs = skuVal * bagsVal;
|
||||
|
||||
next[idx] = row;
|
||||
onChange(next);
|
||||
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">
|
||||
{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);
|
||||
});
|
||||
<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) => (
|
||||
<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') {
|
||||
const opt = col.properties?.options?.find(o => String(o.value) === String(val));
|
||||
return opt ? opt.label : String(val ?? '-');
|
||||
}
|
||||
}
|
||||
} 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>
|
||||
))}
|
||||
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={addRow} className="mt-2 self-start">
|
||||
|
||||
<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;
|
||||
|
||||
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 = newRow[catCol.id] as string;
|
||||
if (selectedCategory) {
|
||||
filteredOpts = allOpts.filter(opt => {
|
||||
const labelStr = String(opt.label || opt.value || '');
|
||||
return labelStr.startsWith(selectedCategory);
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
filteredOpts = allOpts.filter(opt => {
|
||||
if (!opt._raw) return true;
|
||||
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)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<Select
|
||||
key={col.id}
|
||||
label={col.name}
|
||||
required={col.mandatory}
|
||||
value={(val as string) ?? ''}
|
||||
onChange={(e) => updateNewRowField(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) => 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>
|
||||
</form>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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],
|
||||
)}
|
||||
|
||||
@ -16,15 +16,16 @@ 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 (
|
||||
<>
|
||||
<CallsView
|
||||
<CallsView
|
||||
refreshKey={refreshKey}
|
||||
onRowClick={(row) => {
|
||||
const id = row.instance_id as number | string | undefined;
|
||||
if (id != null) navigate(`/calls/${id}`);
|
||||
}}
|
||||
}}
|
||||
headerActions={
|
||||
<Button size="sm" iconLeft={<Plus size={14} />} onClick={() => setIsCreating(true)}>
|
||||
Log Visit
|
||||
@ -38,8 +39,8 @@ export function CallsPage() {
|
||||
title="Log Visit"
|
||||
width="md"
|
||||
>
|
||||
<DynamicForm
|
||||
client={orderBookingClient}
|
||||
<DynamicForm
|
||||
client={orderBookingClient}
|
||||
activityId={ORDER_BOOKING.activities.LOG_VISIT.uid}
|
||||
onSuccess={() => {
|
||||
setIsCreating(false);
|
||||
@ -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={
|
||||
<>
|
||||
<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 && <CallDetail instanceId={instanceId} />}
|
||||
{instanceId != null && (
|
||||
<>
|
||||
<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' }); setIsFabOpen(false); }}>
|
||||
Place Order
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
<Button size='fab'
|
||||
className="rounded-full shadow-xl flex items-center justify-center !p-0"
|
||||
onClick={() => setIsFabOpen(!isFabOpen)}
|
||||
>
|
||||
<Plus size={24} className={`transition-transform duration-200 ${isFabOpen ? "rotate-45" : ""}`} />
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
|
||||
@ -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>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user