From b8ae561d0f9eb04b00e4c1675d19019a2dcefc10 Mon Sep 17 00:00:00 2001 From: saridsa2 Date: Tue, 31 Mar 2026 13:53:46 +0530 Subject: [PATCH] feat: DynamicTable adapter for Untitled UI Table + import preview upgrade - DynamicTable component: wraps Table for dynamic/unknown columns with headerRenderer support - Import wizard preview now uses DynamicTable instead of plain HTML table - Fixed modal height (80vh) to prevent jitter between wizard steps - Campaign card shows actual linked lead count, not marketing metric Co-Authored-By: Claude Opus 4.6 (1M context) --- .../application/table/dynamic-table.tsx | 61 +++++++++++ .../campaigns/lead-import-wizard.tsx | 103 ++++++++---------- 2 files changed, 106 insertions(+), 58 deletions(-) create mode 100644 src/components/application/table/dynamic-table.tsx diff --git a/src/components/application/table/dynamic-table.tsx b/src/components/application/table/dynamic-table.tsx new file mode 100644 index 0000000..c1d7d4f --- /dev/null +++ b/src/components/application/table/dynamic-table.tsx @@ -0,0 +1,61 @@ +import type { ReactNode } from 'react'; +import { TableBody as AriaTableBody } from 'react-aria-components'; +import { Table } from './table'; + +export type DynamicColumn = { + id: string; + label: string; + headerRenderer?: () => ReactNode; + width?: string; +}; + +export type DynamicRow = { + id: string; + [key: string]: any; +}; + +interface DynamicTableProps { + columns: DynamicColumn[]; + rows: T[]; + renderCell: (row: T, columnId: string) => ReactNode; + rowClassName?: (row: T) => string; + size?: 'sm' | 'md'; + maxRows?: number; +} + +export const DynamicTable = ({ + columns, + rows, + renderCell, + rowClassName, + size = 'sm', + maxRows, +}: DynamicTableProps) => { + const displayRows = maxRows ? rows.slice(0, maxRows) : rows; + + return ( + + + {columns.map(col => ( + + {col.headerRenderer?.()} + + ))} + + + {(row) => ( + + {columns.map(col => ( + + {renderCell(row, col.id)} + + ))} + + )} + +
+ ); +}; diff --git a/src/components/campaigns/lead-import-wizard.tsx b/src/components/campaigns/lead-import-wizard.tsx index c0539c7..d53c2fa 100644 --- a/src/components/campaigns/lead-import-wizard.tsx +++ b/src/components/campaigns/lead-import-wizard.tsx @@ -4,6 +4,8 @@ import { faFileImport, faCheck, faSpinner, faTriangleExclamation, faCloudArrowUp import { Dialog, Modal, ModalOverlay } from '@/components/application/modals/modal'; import { Button } from '@/components/base/buttons/button'; import { Badge } from '@/components/base/badges/badges'; +import { DynamicTable } from '@/components/application/table/dynamic-table'; +import type { DynamicColumn, DynamicRow } from '@/components/application/table/dynamic-table'; import { FeaturedIcon } from '@/components/foundations/featured-icon/featured-icon'; import { useData } from '@/providers/data-provider'; import { apiClient } from '@/lib/api-client'; @@ -202,7 +204,7 @@ export const LeadImportWizard = ({ isOpen, onOpenChange }: LeadImportWizardProps {() => ( -
+
{/* Header */}
@@ -247,7 +249,7 @@ export const LeadImportWizard = ({ isOpen, onOpenChange }: LeadImportWizardProps {campaign.campaignStatus}
- {campaign.leadCount ?? 0} leads + {leads.filter(l => l.campaignId === campaign.id).length} leads )) )} @@ -281,63 +283,48 @@ export const LeadImportWizard = ({ isOpen, onOpenChange }: LeadImportWizardProps {noPhoneCount > 0 && {noPhoneCount} no phone}
-
- - - - {mapping.map(m => ( - - ))} - - - - - {rowsWithMatch.slice(0, 20).map((item, i) => ( - - {mapping.map(m => ( - - ))} - - - ))} - -
-
- {m.csvHeader} - -
-
- Patient Match -
- {item.row[m.csvHeader] ?? ''} - - {item.matchedPatient ? ( - - {item.matchedPatient.fullName?.firstName ?? 'Patient'} - - ) : item.isDuplicate ? ( - Duplicate - ) : !item.hasPhone ? ( - No phone - ) : ( - New - )} -
+
+ + columns={[ + ...mapping.map(m => ({ + id: m.csvHeader, + label: m.csvHeader, + headerRenderer: () => ( +
+ {m.csvHeader} + +
+ ), + }) as DynamicColumn), + { id: '__match__', label: 'Patient Match' }, + ]} + rows={rowsWithMatch.map((item, i) => ({ id: `row-${i}`, ...item }))} + maxRows={20} + renderCell={(row, columnId) => { + if (columnId === '__match__') { + if (row.matchedPatient) return {row.matchedPatient.fullName?.firstName ?? 'Patient'}; + if (row.isDuplicate) return Duplicate; + if (!row.hasPhone) return No phone; + return New; + } + return {row.row?.[columnId] ?? ''}; + }} + rowClassName={(row) => cx( + row.isDuplicate && 'bg-warning-primary opacity-60', + !row.hasPhone && 'bg-error-primary opacity-40', + )} + /> {csvRows.length > 20 && ( -
+
Showing 20 of {csvRows.length} rows
)}