mirror of
https://github.com/johndoe6345789/workforce-pay-bill-p.git
synced 2026-04-24 13:24:57 +00:00
Generated by Spark: Add export functionality to AdvancedDataTable for CSV/Excel downloads
This commit is contained in:
@@ -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<T> {
|
||||
data: T[]
|
||||
@@ -12,6 +15,8 @@ interface AdvancedDataTableProps<T> {
|
||||
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<T>({
|
||||
initialPageSize = 20,
|
||||
showSearch = true,
|
||||
showPagination = true,
|
||||
showExport = true,
|
||||
exportFilename = 'export',
|
||||
emptyMessage = 'No data available',
|
||||
rowKey,
|
||||
onRowClick,
|
||||
@@ -44,6 +51,49 @@ export function AdvancedDataTable<T>({
|
||||
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 <CaretUpDown size={16} className="text-muted-foreground" />
|
||||
@@ -56,27 +106,56 @@ export function AdvancedDataTable<T>({
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{showSearch && (
|
||||
{(showSearch || showExport) && (
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="relative flex-1">
|
||||
<MagnifyingGlass className="absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground" size={18} />
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Search..."
|
||||
value={state.searchQuery}
|
||||
onChange={(e) => actions.setSearch(e.target.value)}
|
||||
className="pl-10"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{state.searchQuery && (
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => actions.setSearch('')}
|
||||
size="sm"
|
||||
>
|
||||
Clear Search
|
||||
</Button>
|
||||
{showSearch && (
|
||||
<>
|
||||
<div className="relative flex-1">
|
||||
<MagnifyingGlass className="absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground" size={18} />
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Search..."
|
||||
value={state.searchQuery}
|
||||
onChange={(e) => actions.setSearch(e.target.value)}
|
||||
className="pl-10"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{state.searchQuery && (
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => actions.setSearch('')}
|
||||
size="sm"
|
||||
>
|
||||
Clear Search
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{showExport && (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="outline" size="sm">
|
||||
<Export className="mr-2" size={18} />
|
||||
Export
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem onClick={() => handleExport('csv')}>
|
||||
<FileCsv className="mr-2" size={18} />
|
||||
Export as CSV
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => handleExport('xlsx')}>
|
||||
<FileXls className="mr-2" size={18} />
|
||||
Export as Excel
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => handleExport('json')}>
|
||||
<FileCode className="mr-2" size={18} />
|
||||
Export as JSON
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -337,13 +337,6 @@ export function BillingView({
|
||||
placeholder={t('billing.searchPlaceholder')}
|
||||
/>
|
||||
|
||||
<Stack direction="horizontal" spacing={4}>
|
||||
<Button variant="outline">
|
||||
<Download size={18} className="mr-2" />
|
||||
{t('billing.export')}
|
||||
</Button>
|
||||
</Stack>
|
||||
|
||||
<AdvancedDataTable
|
||||
data={filteredInvoices}
|
||||
columns={invoiceColumns}
|
||||
@@ -352,6 +345,8 @@ export function BillingView({
|
||||
emptyMessage={t('billing.noInvoicesFound')}
|
||||
showSearch={true}
|
||||
showPagination={true}
|
||||
showExport={true}
|
||||
exportFilename={`invoices-${new Date().toISOString().split('T')[0]}`}
|
||||
initialPageSize={20}
|
||||
/>
|
||||
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
</TabsContent>
|
||||
|
||||
@@ -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 version="1.0"?><?mso-application progid="Excel.Sheet"?>'
|
||||
xml += '<Workbook xmlns="urn:schemas-microsoft-com:office:spreadsheet" '
|
||||
xml += 'xmlns:ss="urn:schemas-microsoft-com:office:spreadsheet">'
|
||||
xml += '<Worksheet ss:Name="Sheet1"><Table>'
|
||||
|
||||
if (includeHeaders) {
|
||||
xml += '<Row>'
|
||||
keys.forEach((key) => {
|
||||
xml += `<Cell><Data ss:Type="String">${escapeXml(String(key))}</Data></Cell>`
|
||||
})
|
||||
xml += '</Row>'
|
||||
}
|
||||
|
||||
data.forEach((row) => {
|
||||
xml += '<Row>'
|
||||
keys.forEach((key) => {
|
||||
const value = row[key]
|
||||
if (value === null || value === undefined) {
|
||||
xml += '<Cell><Data ss:Type="String"></Data></Cell>'
|
||||
} else if (typeof value === 'number') {
|
||||
xml += `<Cell><Data ss:Type="Number">${value}</Data></Cell>`
|
||||
} else {
|
||||
xml += `<Cell><Data ss:Type="String">${escapeXml(String(value))}</Data></Cell>`
|
||||
}
|
||||
})
|
||||
xml += '</Row>'
|
||||
})
|
||||
|
||||
xml += '</Table></Worksheet></Workbook>'
|
||||
|
||||
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, '"')
|
||||
.replace(/'/g, ''')
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user