import React, { useState } from 'react'; import { useTranslation } from 'react-i18next'; import { BarChart3, Building2, Calendar, CalendarDays, Download, FileText, Loader2, Plus, Printer, Save, ShieldCheck, Trash2, Users, } from 'lucide-react'; import clsx from 'clsx'; import { PrintButton } from '@/components/common/PrintButton'; import { printPdfBlob } from '@/utils/print'; import { useToast } from '@/components/ui/Toast'; import { useReportTemplates, useCreateReportTemplate, useUpdateReportTemplate, useDeleteReportTemplate, useReportPresets, useGenerateReport, useGeneratePresetReport, downloadBlob, type ReportTemplate, type OutputFormat, type PresetReportInfo, } from '@/api/reports'; // ─── Icon mapping for presets ────────────────────────────────────────────── const PRESET_ICONS: Record = { Users: , Calendar: , CalendarDays: , Building2: , ShieldCheck: , }; // ─── Download history entry ──────────────────────────────────────────────── interface DownloadEntry { id: string; name: string; format: string; timestamp: string; } // ─── Component ───────────────────────────────────────────────────────────── export function ReportsPage() { const { t } = useTranslation(); const toast = useToast(); // Data hooks const { data: templates = [], isLoading: templatesLoading } = useReportTemplates(); const { data: presets = [] } = useReportPresets(); // Mutations const createTemplate = useCreateReportTemplate(); const updateTemplate = useUpdateReportTemplate(); const deleteTemplate = useDeleteReportTemplate(); const generateReport = useGenerateReport(); const generatePreset = useGeneratePresetReport(); // Local state const [selectedTemplate, setSelectedTemplate] = useState(null); const [editorContent, setEditorContent] = useState(''); const [editorName, setEditorName] = useState(''); const [editorFormat, setEditorFormat] = useState('pdf'); const [isNewTemplate, setIsNewTemplate] = useState(false); const [jsonData, setJsonData] = useState('{}'); const [downloadHistory, setDownloadHistory] = useState([]); const [activePresetFormat, setActivePresetFormat] = useState('pdf'); // Select a template for editing const selectTemplate = (tpl: ReportTemplate) => { setSelectedTemplate(tpl); setEditorContent(tpl.content); setEditorName(tpl.name); setEditorFormat(tpl.output_format); setIsNewTemplate(false); }; // Start creating a new template const startNewTemplate = () => { setSelectedTemplate(null); setEditorContent('\n

{{ title }}

\n

{{ message }}

\n'); setEditorName(''); setEditorFormat('pdf'); setIsNewTemplate(true); }; // Save template (create or update) const handleSaveTemplate = async () => { if (!editorName.trim()) { toast.error(t('reports.errorNameRequired', 'Name is required')); return; } if (!editorContent.trim()) { toast.error(t('reports.errorContentRequired', 'Template content is required')); return; } try { if (isNewTemplate) { await createTemplate.mutateAsync({ name: editorName, content: editorContent, output_format: editorFormat, template_type: 'jinja2', }); toast.success(t('reports.templateCreated', 'Template created successfully')); } else if (selectedTemplate) { await updateTemplate.mutateAsync({ id: selectedTemplate.id, name: editorName, content: editorContent, output_format: editorFormat, }); toast.success(t('reports.templateUpdated', 'Template updated successfully')); } } catch (err: any) { toast.error(err?.message || t('reports.saveFailed', 'Failed to save template')); } }; // Delete template const handleDeleteTemplate = async (tpl: ReportTemplate) => { if (!confirm(t('reports.confirmDelete', 'Delete this template?'))) return; try { await deleteTemplate.mutateAsync(tpl.id); if (selectedTemplate?.id === tpl.id) { setSelectedTemplate(null); setEditorContent(''); } toast.success(t('reports.templateDeleted', 'Template deleted')); } catch (err: any) { toast.error(err?.message || t('reports.deleteFailed', 'Failed to delete template')); } }; // Generate report from selected template const handleGenerate = async () => { if (!selectedTemplate) return; let parsedData: Record; try { parsedData = JSON.parse(jsonData); } catch { toast.error(t('reports.invalidJson', 'Invalid JSON data')); return; } try { const response = await generateReport.mutateAsync({ template_id: selectedTemplate.id, data: parsedData, output_format: editorFormat, }); const filename = `report_${selectedTemplate.name}.${editorFormat === 'excel' ? 'xlsx' : editorFormat}`; if (editorFormat === 'print') { printPdfBlob(response.data as Blob); } else { downloadBlob(response.data as Blob, filename); } setDownloadHistory((prev) => [ { id: Date.now().toString(), name: selectedTemplate.name, format: editorFormat, timestamp: new Date().toLocaleString() }, ...prev, ]); toast.success(t('reports.generated', 'Report generated successfully')); } catch (err: any) { toast.error(err?.message || t('reports.generateFailed', 'Failed to generate report')); } }; // Generate preset report const handleGeneratePreset = async (preset: PresetReportInfo) => { let parsedParams: Record = {}; try { parsedParams = JSON.parse(jsonData); } catch { // Use empty params if JSON is invalid parsedParams = {}; } try { const response = await generatePreset.mutateAsync({ preset: preset.key, output_format: activePresetFormat, parameters: parsedParams, }); const ext = activePresetFormat === 'excel' ? 'xlsx' : activePresetFormat === 'print' ? 'pdf' : activePresetFormat; const filename = `${preset.key}_report.${ext}`; if (activePresetFormat === 'print') { printPdfBlob(response.data as Blob); } else { downloadBlob(response.data as Blob, filename); } setDownloadHistory((prev) => [ { id: Date.now().toString(), name: preset.name, format: activePresetFormat, timestamp: new Date().toLocaleString() }, ...prev, ]); toast.success(t('reports.generated', 'Report generated successfully')); } catch (err: any) { toast.error(err?.message || t('reports.generateFailed', 'Failed to generate report')); } }; const isGenerating = generateReport.isPending || generatePreset.isPending; const isSaving = createTemplate.isPending || updateTemplate.isPending; return (
{/* Header */}

{t('reports.title', 'Reports')}

{/* Preset Quick Actions */}

{t('reports.presetReports', 'Preset Reports')}

{presets.map((preset) => (
{PRESET_ICONS[preset.icon] || } {preset.name}

{preset.description}

{preset.output_formats.map((fmt) => ( ))}
))}
{/* Main 3-column layout */}
{/* Left: Template List */}

{t('reports.templates', 'Templates')}

{templatesLoading ? (
) : templates.length === 0 ? (

{t('reports.noTemplates', 'No templates yet')}

) : (
    {templates.map((tpl) => (
  • selectTemplate(tpl)} >

    {tpl.name}

    {tpl.output_format}

  • ))}
)}
{/* Center: Template Editor */}

{isNewTemplate ? t('reports.newTemplate', 'New Template') : selectedTemplate ? t('reports.editTemplate', 'Edit Template') : t('reports.selectTemplate', 'Select a template')}

setEditorName(e.target.value)} placeholder={t('reports.templateName', 'Template name')} className="w-full text-sm border border-secondary-300 rounded px-3 py-2" data-testid="input-template-name" />