From 2aa6ea6f6668c5d0629ba475e558c02b6a0e79ab Mon Sep 17 00:00:00 2001 From: waterWang <672684719@qq.com> Date: Fri, 21 Aug 2026 01:45:03 +0800 Subject: [PATCH] feat: add CSV export and loading/empty/error states to CustomReportBuilder (Closes #419) - Replace alert() in handleExport with real CSV generation and download - Add isExporting loading state with spinner on the export button - Add exportError banner above the layout for failed exports - Enhance empty state with icons and contextual guidance text - Add no-columns-selected state (previously bare text) - Add useNotification integration for success/error toast feedback - Button disabled while exporting, no columns, or no records --- frontend/src/pages/CustomReportBuilder.tsx | 129 +++++++++++++++++---- 1 file changed, 105 insertions(+), 24 deletions(-) diff --git a/frontend/src/pages/CustomReportBuilder.tsx b/frontend/src/pages/CustomReportBuilder.tsx index ef96a24f..27d8997b 100644 --- a/frontend/src/pages/CustomReportBuilder.tsx +++ b/frontend/src/pages/CustomReportBuilder.tsx @@ -1,5 +1,6 @@ -import { useState, useMemo } from 'react'; +import { useState, useMemo, useCallback } from 'react'; import { Button, Card, Icon } from '@stellar/design-system'; +import { useNotification } from '../hooks/useNotification'; // Define all possible columns for the report type ReportColumn = { @@ -64,6 +65,9 @@ const CustomReportBuilder = () => { const [selectedColumns, setSelectedColumns] = useState(ALL_COLUMNS.map((c) => c.id)); const [startDate, setStartDate] = useState('2026-02-01'); const [endDate, setEndDate] = useState('2026-02-28'); + const [isExporting, setIsExporting] = useState(false); + const [exportError, setExportError] = useState(null); + const { notifySuccess, notifyError } = useNotification(); const toggleColumn = (colId: string) => { setSelectedColumns((prev) => @@ -83,13 +87,51 @@ const CustomReportBuilder = () => { }); }, [startDate, endDate]); - const handleExport = () => { - // Simulate export logic (e.g. converting filteredData to CSV and triggering download) - alert( - `Exporting ${filteredData.length} records with columns: ${activeColumns.map((c) => c.label).join(', ')}` - ); + // Escape a cell value for CSV (wrap in quotes if it contains commas, quotes, or newlines) + const escapeCsvCell = (value: string): string => { + if (value.includes(',') || value.includes('"') || value.includes('\n')) { + return `"${value.replace(/"/g, '""')}"`; + } + return value; }; + const handleExport = useCallback(async () => { + setExportError(null); + setIsExporting(true); + + try { + // Simulate a brief async export (e.g. calling a backend endpoint) + await new Promise((resolve) => setTimeout(resolve, 500)); + + // Build CSV string + const header = activeColumns.map((c) => c.label).join(','); + const rows = filteredData.map((row) => + activeColumns.map((col) => escapeCsvCell(String(row[col.id as keyof typeof row]))).join(',') + ); + const csv = [header, ...rows].join('\n'); + + // Trigger download + const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' }); + const url = URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = url; + link.setAttribute('download', `payroll-report-${startDate}-to-${endDate}.csv`); + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + URL.revokeObjectURL(url); + + notifySuccess('Export successful', `${filteredData.length} records exported`); + } catch (error) { + const msg = + error instanceof Error ? error.message : 'An unexpected error occurred during export.'; + setExportError(msg); + notifyError('Export failed', msg); + } finally { + setIsExporting(false); + } + }, [filteredData, activeColumns, startDate, endDate, notifySuccess, notifyError]); + return (
@@ -99,6 +141,13 @@ const CustomReportBuilder = () => {

+ {exportError && ( +
+ + {exportError} +
+ )} +
{/* Controls Sidebar */}
@@ -148,13 +197,42 @@ const CustomReportBuilder = () => {
@@ -170,7 +248,24 @@ const CustomReportBuilder = () => {
- {activeColumns.length > 0 ? ( + {activeColumns.length === 0 ? ( +
+ +

No columns selected

+

+ Select at least one column from the sidebar to preview data. +

+
+ ) : filteredData.length === 0 ? ( +
+ +

No matching records

+

+ No records found for the selected date range. Try adjusting your start or end + date. +

+
+ ) : ( @@ -197,22 +292,8 @@ const CustomReportBuilder = () => { ))} ))} - {filteredData.length === 0 && ( - - - - )}
- No data found for the selected date range. -
- ) : ( -
- Please select at least one column to preview data. -
)}