diff --git a/src/components/application/table/column-toggle.tsx b/src/components/application/table/column-toggle.tsx new file mode 100644 index 0000000..3ff4d02 --- /dev/null +++ b/src/components/application/table/column-toggle.tsx @@ -0,0 +1,94 @@ +import { useState, useRef, useEffect } from 'react'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faColumns3 } from '@fortawesome/pro-duotone-svg-icons'; +import { Button } from '@/components/base/buttons/button'; +import { Checkbox } from '@/components/base/checkbox/checkbox'; +import type { FC } from 'react'; + +const ColumnsIcon: FC<{ className?: string }> = ({ className }) => ( + +); + +export type ColumnDef = { + id: string; + label: string; + defaultVisible?: boolean; +}; + +interface ColumnToggleProps { + columns: ColumnDef[]; + visibleColumns: Set; + onToggle: (columnId: string) => void; +} + +export const ColumnToggle = ({ columns, visibleColumns, onToggle }: ColumnToggleProps) => { + const [open, setOpen] = useState(false); + const panelRef = useRef(null); + + useEffect(() => { + if (!open) return; + const handler = (e: MouseEvent) => { + if (panelRef.current && !panelRef.current.contains(e.target as Node)) { + setOpen(false); + } + }; + document.addEventListener('mousedown', handler); + return () => document.removeEventListener('mousedown', handler); + }, [open]); + + return ( +
+ + + {open && ( +
+
+ Show/Hide Columns +
+
+ {columns.map(col => ( + + ))} +
+
+ )} +
+ ); +}; + +export const useColumnVisibility = (columns: ColumnDef[]) => { + const [visibleColumns, setVisibleColumns] = useState>(() => { + return new Set(columns.filter(c => c.defaultVisible !== false).map(c => c.id)); + }); + + const toggle = (columnId: string) => { + setVisibleColumns(prev => { + const next = new Set(prev); + if (next.has(columnId)) { + next.delete(columnId); + } else { + next.add(columnId); + } + return next; + }); + }; + + return { visibleColumns, toggle }; +}; diff --git a/src/components/application/table/table.tsx b/src/components/application/table/table.tsx index 5177684..72da930 100644 --- a/src/components/application/table/table.tsx +++ b/src/components/application/table/table.tsx @@ -17,7 +17,9 @@ import { Cell as AriaCell, Collection as AriaCollection, Column as AriaColumn, + ColumnResizer as AriaColumnResizer, Group as AriaGroup, + ResizableTableContainer as AriaResizableTableContainer, Row as AriaRow, Table as AriaTable, TableBody as AriaTableBody, @@ -115,9 +117,9 @@ const TableRoot = ({ className, size = "md", ...props }: TableRootProps) => { return ( -
+ cx("w-full", typeof className === "function" ? className(state) : className)} {...props} /> -
+
); }; @@ -168,9 +170,10 @@ TableHeader.displayName = "TableHeader"; interface TableHeadProps extends AriaColumnProps, Omit, "children" | "className" | "style" | "id"> { label?: string; tooltip?: string; + allowsResizing?: boolean; } -const TableHead = ({ className, tooltip, label, children, ...props }: TableHeadProps) => { +const TableHead = ({ className, tooltip, label, children, allowsResizing, ...props }: TableHeadProps) => { const { selectionBehavior } = useTableOptions(); return ( @@ -186,8 +189,8 @@ const TableHead = ({ className, tooltip, label, children, ...props }: TableHeadP } > {(state) => ( - -
+ +
{label && {label}} {typeof children === "function" ? children(state) : children}
@@ -206,6 +209,10 @@ const TableHead = ({ className, tooltip, label, children, ...props }: TableHeadP ) : ( ))} + + {allowsResizing && ( + + )}
)} diff --git a/src/components/leads/lead-table.tsx b/src/components/leads/lead-table.tsx index 8da8582..3fc53fa 100644 --- a/src/components/leads/lead-table.tsx +++ b/src/components/leads/lead-table.tsx @@ -24,6 +24,7 @@ type LeadTableProps = { sortDirection: 'asc' | 'desc'; onSort: (field: string) => void; onViewActivity?: (lead: Lead) => void; + visibleColumns?: Set; }; type TableRow = { @@ -53,6 +54,7 @@ export const LeadTable = ({ sortDirection, onSort, onViewActivity, + visibleColumns, }: LeadTableProps) => { const [expandedDupId, setExpandedDupId] = useState(null); @@ -92,22 +94,26 @@ export const LeadTable = ({ return rows; }, [leads, expandedDupId]); - const columns = [ - { id: 'phone', label: 'Phone', allowsSorting: true }, - { id: 'name', label: 'Name', allowsSorting: true }, - { id: 'email', label: 'Email', allowsSorting: false }, - { id: 'campaign', label: 'Campaign', allowsSorting: false }, - { id: 'ad', label: 'Ad', allowsSorting: false }, - { id: 'source', label: 'Source', allowsSorting: true }, - { id: 'firstContactedAt', label: 'First Contact', allowsSorting: true }, - { id: 'lastContactedAt', label: 'Last Contact', allowsSorting: true }, - { id: 'status', label: 'Status', allowsSorting: true }, - { id: 'createdAt', label: 'Age', allowsSorting: true }, - { id: 'spamScore', label: 'Spam', allowsSorting: true }, - { id: 'dups', label: 'Dups', allowsSorting: false }, - { id: 'actions', label: '', allowsSorting: false }, + const allColumns = [ + { id: 'phone', label: 'Phone', allowsSorting: true, defaultWidth: 150 }, + { id: 'name', label: 'Name', allowsSorting: true, defaultWidth: 160 }, + { id: 'email', label: 'Email', allowsSorting: false, defaultWidth: 180 }, + { id: 'campaign', label: 'Campaign', allowsSorting: false, defaultWidth: 140 }, + { id: 'ad', label: 'Ad', allowsSorting: false, defaultWidth: 80 }, + { id: 'source', label: 'Source', allowsSorting: true, defaultWidth: 100 }, + { id: 'firstContactedAt', label: 'First Contact', allowsSorting: true, defaultWidth: 130 }, + { id: 'lastContactedAt', label: 'Last Contact', allowsSorting: true, defaultWidth: 130 }, + { id: 'status', label: 'Status', allowsSorting: true, defaultWidth: 100 }, + { id: 'createdAt', label: 'Age', allowsSorting: true, defaultWidth: 80 }, + { id: 'spamScore', label: 'Spam', allowsSorting: true, defaultWidth: 70 }, + { id: 'dups', label: 'Dups', allowsSorting: false, defaultWidth: 60 }, + { id: 'actions', label: '', allowsSorting: false, defaultWidth: 50 }, ]; + const columns = visibleColumns + ? allColumns.filter(c => visibleColumns.has(c.id) || c.id === 'actions') + : allColumns; + return (
)} @@ -204,6 +213,8 @@ export const LeadTable = ({ const isDup = lead.isDuplicate === true; const isExpanded = expandedDupId === lead.id; + const isCol = (id: string) => !visibleColumns || visibleColumns.has(id); + return ( - + {isCol('phone') && {phone} - - + } + {isCol('name') && {name} - - + } + {isCol('email') && {email} - - + } + {isCol('campaign') && {lead.utmCampaign ? ( {lead.utmCampaign} @@ -230,8 +241,8 @@ export const LeadTable = ({ ) : ( {'\u2014'} )} - - + } + {isCol('ad') && {lead.adId ? ( Ad @@ -239,50 +250,50 @@ export const LeadTable = ({ ) : ( {'\u2014'} )} - - + } + {isCol('source') && {lead.leadSource ? ( ) : ( {'\u2014'} )} - - + } + {isCol('firstContactedAt') && {lead.firstContactedAt ? formatShortDate(lead.firstContactedAt) : '\u2014'} - - + } + {isCol('lastContactedAt') && {lead.lastContactedAt ? formatShortDate(lead.lastContactedAt) : '\u2014'} - - + } + {isCol('status') && {lead.leadStatus ? ( ) : ( {'\u2014'} )} - - + } + {isCol('createdAt') && {lead.createdAt ? ( ) : ( {'\u2014'} )} - - + } + {isCol('spamScore') && {lead.spamScore != null ? ( ) : ( 0% )} - - + } + {isCol('dups') && {isDup ? ( -
{ aria-label="Search leads" />
+