KFH_ASM/asm_app/src/components/QueryReportExportModal.tsx

287 lines
10 KiB
TypeScript

import React, { useEffect, useMemo, useState } from 'react';
import * as XLSX from 'xlsx';
import {
FaCheckSquare,
FaDownload,
FaFileExcel,
FaFileExport,
FaFilePdf,
FaSpinner,
FaSquare,
FaTimes,
} from 'react-icons/fa';
export interface QueryReportColumn {
fieldname: string;
label?: string;
}
export type QueryReportExportFormat = 'excel' | 'pdf';
export interface QueryReportExportModalProps {
isOpen: boolean;
onClose: () => void;
title?: string;
columns: QueryReportColumn[];
rows: Record<string, unknown>[];
fileNamePrefix?: string;
}
function formatCellValue(value: unknown): string {
if (value === null || value === undefined || value === '') return '';
return String(value);
}
function downloadExcel(
rows: Record<string, unknown>[],
columns: QueryReportColumn[],
fileName: string,
) {
const wsData = [
columns.map((c) => c.label || c.fieldname),
...rows.map((row) => columns.map((c) => formatCellValue(row[c.fieldname]))),
];
const ws = XLSX.utils.aoa_to_sheet(wsData);
const wb = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(wb, ws, 'Export');
XLSX.writeFile(wb, fileName);
}
function downloadPdf(
rows: Record<string, unknown>[],
columns: QueryReportColumn[],
reportTitle: string,
) {
const printWindow = window.open('', '_blank');
if (!printWindow) {
window.alert('Please allow popups for this site to export PDF.');
return;
}
const tableHTML = `
<!DOCTYPE html>
<html>
<head>
<title>${reportTitle}</title>
<style>
body { font-family: Arial, sans-serif; margin: 20px; }
h1 { text-align: center; color: #333; margin-bottom: 8px; font-size: 18px; }
.meta { text-align: center; color: #666; margin-bottom: 20px; font-size: 11px; }
table { width: 100%; border-collapse: collapse; font-size: 10px; }
th, td { border: 1px solid #ccc; padding: 6px 8px; text-align: left; vertical-align: top; }
th { background: #047857; color: white; }
tr:nth-child(even) { background: #f9fafb; }
@media print { body { margin: 0; } }
</style>
</head>
<body>
<h1>${reportTitle}</h1>
<div class="meta">
Generated on: ${new Date().toLocaleString()} | Total Records: ${rows.length}
</div>
<table>
<thead>
<tr>
${columns.map((c) => `<th>${c.label || c.fieldname}</th>`).join('')}
</tr>
</thead>
<tbody>
${rows.map((row) => `
<tr>
${columns.map((c) => `<td>${formatCellValue(row[c.fieldname]) || '—'}</td>`).join('')}
</tr>
`).join('')}
</tbody>
</table>
<script>window.onload = function() { window.print(); }</script>
</body>
</html>
`;
printWindow.document.write(tableHTML);
printWindow.document.close();
}
const QueryReportExportModal: React.FC<QueryReportExportModalProps> = ({
isOpen,
onClose,
title = 'Export Report',
columns,
rows,
fileNamePrefix = 'report',
}) => {
const [format, setFormat] = useState<QueryReportExportFormat>('excel');
const [checkedKeys, setCheckedKeys] = useState<Set<string>>(new Set());
const [isExporting, setIsExporting] = useState(false);
const columnOptions = useMemo(
() => columns.filter((c) => c.fieldname),
[columns],
);
useEffect(() => {
if (!isOpen) return;
setFormat('excel');
setCheckedKeys(new Set(columnOptions.map((c) => c.fieldname)));
}, [isOpen, columnOptions]);
if (!isOpen) return null;
const chosenColumns = columnOptions.filter((c) => checkedKeys.has(c.fieldname));
const canExport = rows.length > 0 && chosenColumns.length > 0;
const toggleColumn = (key: string) => {
setCheckedKeys((prev) => {
const next = new Set(prev);
if (next.has(key)) next.delete(key);
else next.add(key);
return next;
});
};
const selectAll = () => setCheckedKeys(new Set(columnOptions.map((c) => c.fieldname)));
const selectNone = () => setCheckedKeys(new Set());
const handleExport = async () => {
if (!canExport) return;
setIsExporting(true);
try {
const datePart = new Date().toISOString().split('T')[0];
if (format === 'excel') {
downloadExcel(rows, chosenColumns, `${fileNamePrefix}_${datePart}.xlsx`);
} else {
downloadPdf(rows, chosenColumns, title);
}
onClose();
} catch (err) {
console.error('Export failed:', err);
window.alert(`Export failed: ${err instanceof Error ? err.message : 'Unknown error'}`);
} finally {
setIsExporting(false);
}
};
return (
<div className="fixed inset-0 bg-black/60 flex items-center justify-center z-[80] p-4">
<div className="bg-white dark:bg-gray-800 rounded-xl shadow-2xl w-full max-w-lg max-h-[90vh] flex flex-col overflow-hidden">
<div className="bg-gradient-to-r from-emerald-600 to-teal-600 px-5 py-4 flex items-center justify-between shrink-0">
<div className="flex items-center gap-2">
<FaFileExport className="text-white" />
<h3 className="text-base font-semibold text-white">{title}</h3>
</div>
<button
type="button"
onClick={onClose}
disabled={isExporting}
className="text-white/80 hover:text-white p-1 rounded-lg hover:bg-white/20"
>
<FaTimes />
</button>
</div>
<div className="p-5 space-y-5 overflow-y-auto flex-1">
<div>
<h4 className="text-sm font-semibold text-gray-700 dark:text-gray-300 mb-2">File format</h4>
<div className="flex gap-3">
<label className={`flex-1 flex items-center gap-2 p-3 rounded-lg border cursor-pointer ${
format === 'excel'
? 'border-emerald-500 bg-emerald-50 dark:bg-emerald-900/20'
: 'border-gray-200 dark:border-gray-700'
}`}>
<input
type="radio"
name="wo_feedback_export_format"
checked={format === 'excel'}
onChange={() => setFormat('excel')}
className="text-emerald-600"
/>
<FaFileExcel className="text-green-700" />
<span className="text-sm font-medium text-gray-800 dark:text-gray-200">Excel (.xlsx)</span>
</label>
<label className={`flex-1 flex items-center gap-2 p-3 rounded-lg border cursor-pointer ${
format === 'pdf'
? 'border-emerald-500 bg-emerald-50 dark:bg-emerald-900/20'
: 'border-gray-200 dark:border-gray-700'
}`}>
<input
type="radio"
name="wo_feedback_export_format"
checked={format === 'pdf'}
onChange={() => setFormat('pdf')}
className="text-emerald-600"
/>
<FaFilePdf className="text-red-600" />
<span className="text-sm font-medium text-gray-800 dark:text-gray-200">PDF</span>
</label>
</div>
</div>
<div>
<div className="flex items-center justify-between mb-2">
<h4 className="text-sm font-semibold text-gray-700 dark:text-gray-300">
Columns to export
</h4>
<div className="flex gap-3 text-xs text-emerald-600 dark:text-emerald-400">
<button type="button" onClick={selectAll} className="hover:underline">All</button>
<button type="button" onClick={selectNone} className="hover:underline">None</button>
</div>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-1.5 max-h-52 overflow-y-auto p-2 bg-gray-50 dark:bg-gray-900/50 rounded-lg border border-gray-200 dark:border-gray-700">
{columnOptions.map((col) => {
const checked = checkedKeys.has(col.fieldname);
return (
<button
key={col.fieldname}
type="button"
onClick={() => toggleColumn(col.fieldname)}
className={`flex items-center gap-2 px-2 py-1.5 rounded text-left text-xs transition-colors ${
checked
? 'bg-emerald-100 dark:bg-emerald-900/30 text-emerald-800 dark:text-emerald-200'
: 'hover:bg-gray-100 dark:hover:bg-gray-800 text-gray-600 dark:text-gray-400'
}`}
>
{checked
? <FaCheckSquare size={13} className="text-emerald-600 shrink-0" />
: <FaSquare size={13} className="text-gray-300 shrink-0" />}
<span className="truncate" title={col.label || col.fieldname}>
{col.label || col.fieldname}
</span>
</button>
);
})}
</div>
<p className="text-xs text-gray-400 mt-1.5">
{checkedKeys.size} of {columnOptions.length} columns selected · {rows.length} row{rows.length !== 1 ? 's' : ''}
</p>
</div>
</div>
<div className="px-5 py-4 border-t border-gray-200 dark:border-gray-700 flex justify-end gap-3 shrink-0">
<button
type="button"
onClick={onClose}
disabled={isExporting}
className="px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg"
>
Cancel
</button>
<button
type="button"
onClick={handleExport}
disabled={!canExport || isExporting}
className="px-4 py-2 text-sm font-medium text-white bg-emerald-600 hover:bg-emerald-700 rounded-lg flex items-center gap-2 disabled:opacity-50"
>
{isExporting ? (
<><FaSpinner className="animate-spin" size={14} /> Exporting</>
) : (
<><FaDownload size={14} /> Export</>
)}
</button>
</div>
</div>
</div>
);
};
export default QueryReportExportModal;