diff --git a/src/components/AdvancedDataTable.tsx b/src/components/AdvancedDataTable.tsx index e9edfbe..f6fc83e 100644 --- a/src/components/AdvancedDataTable.tsx +++ b/src/components/AdvancedDataTable.tsx @@ -3,8 +3,11 @@ import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@ import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' +import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@/components/ui/dropdown-menu' import { useAdvancedTable, TableColumn } from '@/hooks/use-advanced-table' -import { CaretUp, CaretDown, CaretUpDown, MagnifyingGlass } from '@phosphor-icons/react' +import { useDataExport } from '@/hooks/use-data-export' +import { CaretUp, CaretDown, CaretUpDown, MagnifyingGlass, Export, FileCsv, FileXls, FileCode } from '@phosphor-icons/react' +import { toast } from 'sonner' interface AdvancedDataTableProps { data: T[] @@ -12,6 +15,8 @@ interface AdvancedDataTableProps { initialPageSize?: number showSearch?: boolean showPagination?: boolean + showExport?: boolean + exportFilename?: string emptyMessage?: string rowKey: keyof T onRowClick?: (row: T) => void @@ -24,6 +29,8 @@ export function AdvancedDataTable({ initialPageSize = 20, showSearch = true, showPagination = true, + showExport = true, + exportFilename = 'export', emptyMessage = 'No data available', rowKey, onRowClick, @@ -44,6 +51,49 @@ export function AdvancedDataTable({ filteredCount, } = useAdvancedTable(data, columns, initialPageSize) + const { exportToCSV, exportToExcel, exportToJSON } = useDataExport() + + const handleExport = (format: 'csv' | 'xlsx' | 'json') => { + try { + const exportData = items.length > 0 ? items : data + + if (exportData.length === 0) { + toast.error('No data to export') + return + } + + const exportColumns = columns.map(col => String(col.key)) + const formattedData = exportData.map(row => { + const formattedRow: any = {} + columns.forEach(col => { + const key = String(col.key) + const value = row[col.key] + formattedRow[col.label] = value + }) + return formattedRow + }) + + const options = { + filename: exportFilename, + includeHeaders: true, + } + + if (format === 'csv') { + exportToCSV(formattedData, options) + toast.success(`Exported ${formattedData.length} rows to CSV`) + } else if (format === 'xlsx') { + exportToExcel(formattedData, options) + toast.success(`Exported ${formattedData.length} rows to Excel`) + } else if (format === 'json') { + exportToJSON(formattedData, options) + toast.success(`Exported ${formattedData.length} rows to JSON`) + } + } catch (error) { + toast.error('Failed to export data') + console.error('Export error:', error) + } + } + const getSortIcon = (columnKey: keyof T) => { if (!state.sortConfig || state.sortConfig.key !== columnKey) { return @@ -56,27 +106,56 @@ export function AdvancedDataTable({ return (
- {showSearch && ( + {(showSearch || showExport) && (
-
- - actions.setSearch(e.target.value)} - className="pl-10" - /> -
- - {state.searchQuery && ( - + {showSearch && ( + <> +
+ + actions.setSearch(e.target.value)} + className="pl-10" + /> +
+ + {state.searchQuery && ( + + )} + + )} + + {showExport && ( + + + + + + handleExport('csv')}> + + Export as CSV + + handleExport('xlsx')}> + + Export as Excel + + handleExport('json')}> + + Export as JSON + + + )}
)} diff --git a/src/components/views/BillingView.tsx b/src/components/views/BillingView.tsx index 2fc7284..da7b27e 100644 --- a/src/components/views/BillingView.tsx +++ b/src/components/views/BillingView.tsx @@ -337,13 +337,6 @@ export function BillingView({ placeholder={t('billing.searchPlaceholder')} /> - - - - diff --git a/src/components/views/PayrollView.tsx b/src/components/views/PayrollView.tsx index f87f462..b7bb091 100644 --- a/src/components/views/PayrollView.tsx +++ b/src/components/views/PayrollView.tsx @@ -454,6 +454,8 @@ export function PayrollView({ timesheets, workers }: PayrollViewProps) { emptyMessage={t('payroll.noPayrollRunsYet')} showSearch={true} showPagination={true} + showExport={true} + exportFilename={`payroll-runs-${new Date().toISOString().split('T')[0]}`} initialPageSize={20} /> diff --git a/src/hooks/use-data-export.ts b/src/hooks/use-data-export.ts index 2bebb03..b37559f 100644 --- a/src/hooks/use-data-export.ts +++ b/src/hooks/use-data-export.ts @@ -67,6 +67,60 @@ export function useDataExport() { [] ) + const exportToExcel = useCallback( + (data: any[], options: ExportOptions = {}) => { + const { + filename = 'export', + columns, + includeHeaders = true, + } = options + + if (data.length === 0) { + throw new Error('No data to export') + } + + const keys = columns || Object.keys(data[0]) + + let xml = '' + xml += ' { + xml += `${escapeXml(String(key))}` + }) + xml += '' + } + + data.forEach((row) => { + xml += '' + keys.forEach((key) => { + const value = row[key] + if (value === null || value === undefined) { + xml += '' + } else if (typeof value === 'number') { + xml += `${value}` + } else { + xml += `${escapeXml(String(value))}` + } + }) + xml += '' + }) + + xml += '' + + const blob = new Blob([xml], { type: 'application/vnd.ms-excel' }) + const link = document.createElement('a') + link.href = URL.createObjectURL(blob) + link.download = `${filename}.xls` + link.click() + URL.revokeObjectURL(link.href) + }, + [] + ) + const exportData = useCallback( (data: any[], options: ExportOptions = {}) => { const { format = 'csv' } = options @@ -78,16 +132,29 @@ export function useDataExport() { case 'json': exportToJSON(data, options) break + case 'xlsx': + exportToExcel(data, options) + break default: throw new Error(`Unsupported export format: ${format}`) } }, - [exportToCSV, exportToJSON] + [exportToCSV, exportToJSON, exportToExcel] ) return { exportToCSV, exportToJSON, + exportToExcel, exportData, } } + +function escapeXml(str: string): string { + return str + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, ''') +}