import type { Component } from 'solid-js'; import { createSignal, Show, For, onMount } from 'solid-js'; import { Portal } from 'solid-js/web'; import { X, Table, Copy, Download, CheckCircle2 } from 'lucide-solid'; import { appStore } from '../store/appStore'; interface ExportTableModalProps { show: boolean; onClose: () => void; } const AVAILABLE_COLUMNS = [ { id: 'scope', label: 'Scope' }, { id: 'subscope', label: 'Subscope' }, { id: 'description', label: 'Description' }, { id: 'quantity', label: 'Quantity' }, { id: 'unitPrice', label: 'Unit Price' }, { id: 'subtotal', label: 'Subtotal' }, { id: 'markup', label: 'Markup' }, { id: 'contingency', label: 'Contingency' }, { id: 'total', label: 'Total' } ]; const ExportTableModal: Component = (props) => { const scopes = () => appStore.scopes; const subScopes = () => appStore.subScopes; const items = () => appStore.estimateItems; const [selectedScopeIds, setSelectedScopeIds] = createSignal([]); const [selectedSubScopeIds, setSelectedSubScopeIds] = createSignal([]); const [selectedColumns, setSelectedColumns] = createSignal(AVAILABLE_COLUMNS.map(c => c.id)); const [copyToast, setCopyToast] = createSignal(false); // Initialize with all scopes and subscopes selected onMount(() => { if (props.show) { setSelectedScopeIds(scopes().map(s => s.id)); setSelectedSubScopeIds(subScopes().map(s => s.id)); } }); const toggleScope = (scopeId: string) => { const isSelected = selectedScopeIds().includes(scopeId); if (isSelected) { setSelectedScopeIds(prev => prev.filter(id => id !== scopeId)); // Deselect all its subscopes const subscopeIds = subScopes().filter(ss => ss.scopeId === scopeId).map(ss => ss.id); setSelectedSubScopeIds(prev => prev.filter(id => !subscopeIds.includes(id))); } else { setSelectedScopeIds(prev => [...prev, scopeId]); // Select all its subscopes const subscopeIds = subScopes().filter(ss => ss.scopeId === scopeId).map(ss => ss.id); setSelectedSubScopeIds(prev => [...new Set([...prev, ...subscopeIds])]); } }; const toggleSubScope = (subScopeId: string, scopeId: string) => { const isSelected = selectedSubScopeIds().includes(subScopeId); if (isSelected) { setSelectedSubScopeIds(prev => prev.filter(id => id !== subScopeId)); // If we deselected a subscope, also make sure the parent scope stays selected if we want, or deselect it if 0 // Actually, keep it simple. Just toggle the subscope. } else { setSelectedSubScopeIds(prev => [...prev, subScopeId]); // If the parent scope isn't selected, select it if (!selectedScopeIds().includes(scopeId)) { setSelectedScopeIds(prev => [...prev, scopeId]); } } }; const toggleColumn = (columnId: string) => { setSelectedColumns(prev => prev.includes(columnId) ? prev.filter(id => id !== columnId) : [...prev, columnId] ); }; const generateTableData = () => { const dataRows: Record[] = []; // Filter items that belong to the selected scopes and subscopes (or unassigned if we handled it, but let's stick to assigned) // Also handle scope-level items (no subscope) if the scope is selected const filteredItems = items().filter(item => { if (item.subScopeId) { return selectedSubScopeIds().includes(item.subScopeId); } else if (item.scopeId) { return selectedScopeIds().includes(item.scopeId); } return false; // ignore unassigned for this specific scope/subscope export }); filteredItems.forEach(item => { const scope = scopes().find(s => s.id === item.scopeId); const subScope = subScopes().find(ss => ss.id === item.subScopeId); const base = item.quantity * item.unitPrice; const markupAmt = item.markupType === 'percent' ? base * (item.markup / 100) : item.markup; const contingencyAmt = item.contingencyType === 'percent' ? base * (item.contingency / 100) : item.contingency; const total = base + markupAmt + contingencyAmt; dataRows.push({ scope: scope?.name || 'Unassigned', subscope: subScope?.name || 'Scope Level', description: item.description, quantity: item.quantity, unitPrice: item.unitPrice, subtotal: base, markup: markupAmt, contingency: contingencyAmt, total: total }); }); return dataRows; }; const copyToClipboard = async () => { const data = generateTableData(); const cols = AVAILABLE_COLUMNS.filter(c => selectedColumns().includes(c.id)); let tsv = cols.map(c => c.label).join('\t') + '\n'; data.forEach(row => { tsv += cols.map(c => { const val = row[c.id]; if (typeof val === 'number') { // For quantities don't add $ sign typically, but we will rely on excel to format it or just give raw numbers return ['unitPrice', 'subtotal', 'markup', 'contingency', 'total'].includes(c.id) ? `$${val.toFixed(2)}` : val.toString(); } return String(val || '').replace(/\t/g, ' '); }).join('\t') + '\n'; }); try { await navigator.clipboard.writeText(tsv); setCopyToast(true); setTimeout(() => setCopyToast(false), 2000); } catch (err) { console.error('Failed to copy', err); } }; const downloadCSV = () => { const data = generateTableData(); const cols = AVAILABLE_COLUMNS.filter(c => selectedColumns().includes(c.id)); let csv = cols.map(c => c.label).join(',') + '\n'; data.forEach(row => { csv += cols.map(c => { const val = row[c.id]; const strVal = typeof val === 'number' && ['unitPrice', 'subtotal', 'markup', 'contingency', 'total'].includes(c.id) ? `$${val.toFixed(2)}` : String(val || ''); return `"${strVal.replace(/"/g, '""')}"`; }).join(',') + '\n'; }); const blob = new Blob([csv], { type: 'text/csv' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `exported-table.csv`; a.click(); URL.revokeObjectURL(url); }; return (

Export Table Output

{/* Scopes & Subscopes Column */}

Include Scopes & Subscopes

{(scope) => (
ss.scopeId === scope.id)}> {(subScope) => ( )}
)}
No scopes available.
{/* Columns Column */}

Include Columns

{(col) => ( )}
); }; export default ExportTableModal;