import { useState, useEffect, useCallback } from 'react'; import { FolderDown, ChevronRight, RefreshCw, Download, FileText, Lock, Globe, Server, Activity, Database, Package, BarChart3, AlertCircle, Loader2, Filter, } from 'lucide-react'; import client from '@/api/client'; import { useI18n } from '@/i18n'; /* ── Types ── */ interface Report { id: string; tenancy_name: string; status: string; level: number | null; created_at: string; completed_at: string | null; } interface ReportFile { id: string; file_name: string; file_type: string; file_category: string; file_size: number; } /* ── Helpers ── */ function extractSection(name: string): string { const m = name.match(/^(?:cis|obp)_([A-Za-z_]+?)_\d/); if (m) return m[1].replace(/_/g, ' '); if (name.includes('summary')) return 'Summary'; if (name.includes('error')) return 'Error'; return 'Other'; } function formatSize(bytes: number): string { if (bytes < 1024) return `${bytes} B`; if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; return `${(bytes / (1024 * 1024)).toFixed(1)} MB`; } function fileExt(name: string): string { return (name.split('.').pop() || '').toLowerCase(); } const extColors: Record = { html: '#e44d26', csv: '#217346', json: '#f5a623', xlsx: '#217346', txt: '#6b7280', pdf: '#dc2626', }; const sectionIcons: Record = { 'Identity and Access Management': Lock, 'Networking': Globe, 'Compute': Server, 'Logging and Monitoring': Activity, 'Storage Object Storage': Database, 'Storage Block Volumes': Database, 'Storage File Storage Service': Database, 'Asset Management': Package, 'Summary': BarChart3, 'Error': AlertCircle, 'Other': FileText, }; function sectionSort(a: string, b: string): number { if (a === 'Summary') return -1; if (b === 'Summary') return 1; if (a === 'Other') return 1; if (b === 'Other') return -1; return a.localeCompare(b); } function groupBySection(files: ReportFile[]): Record { const map: Record = {}; for (const f of files) { const sec = extractSection(f.file_name); (map[sec] ||= []).push(f); } return map; } /* ── Component ── */ export default function DownloadsPage() { const { t } = useI18n(); const [reports, setReports] = useState([]); const [expandedRid, setExpandedRid] = useState(null); const [filesCache, setFilesCache] = useState>({}); const [loadingFiles, setLoadingFiles] = useState(null); const [tenancyFilter, setTenancyFilter] = useState(''); const [sectionFilter, setSectionFilter] = useState(''); const [loading, setLoading] = useState(true); const [error, setError] = useState(''); const token = localStorage.getItem('t') || ''; const loadReports = useCallback(async () => { setLoading(true); setError(''); try { const data = await client.get('/reports') as unknown as Report[]; setReports(data.filter((r) => r.status === 'completed')); } catch (err) { setError(err instanceof Error ? err.message : t('dl.loadError')); } finally { setLoading(false); } }, []); useEffect(() => { loadReports(); }, [loadReports]); const handleRefresh = useCallback(() => { setFilesCache({}); setExpandedRid(null); setSectionFilter(''); loadReports(); }, [loadReports]); const toggleExpand = useCallback(async (rid: string) => { if (expandedRid === rid) { setExpandedRid(null); return; } setExpandedRid(rid); if (!filesCache[rid]) { setLoadingFiles(rid); try { const files = await client.get(`/reports/${rid}/files`) as unknown as ReportFile[]; setFilesCache((prev) => ({ ...prev, [rid]: files })); } catch { setFilesCache((prev) => ({ ...prev, [rid]: [] })); } finally { setLoadingFiles(null); } } }, [expandedRid, filesCache]); const handleDownload = useCallback((rid: string, fid: string) => { window.open(`/api/reports/${rid}/files/${fid}/download?token=${token}`, '_blank'); }, [token]); /* Derived data */ const tenancies = [...new Set(reports.map((r) => r.tenancy_name))].sort(); const filtered = tenancyFilter ? reports.filter((r) => r.tenancy_name === tenancyFilter) : reports; /* Sections available in expanded report (for filter) */ const expandedFiles = expandedRid ? filesCache[expandedRid] : null; const expandedSections = expandedFiles ? [...new Set(expandedFiles.map((f) => extractSection(f.file_name)))].sort(sectionSort) : []; return (
{/* Header */}

{t('dl.title')}

{t('dl.subtitle')}
{filtered.length} {t('dl.reports')}
{/* Toolbar */}
{/* Tenancy filter */}
{/* Section filter (only when expanded) */} {expandedRid && expandedSections.length > 1 && ( )} {filtered.length} {t('dl.reports')}
{/* Error */} {error && (
{error}
)} {/* Loading */} {loading && !reports.length && (
{t('dl.loading')}
)} {/* Empty state */} {!loading && !filtered.length && (

{t('dl.noReports')}

{t('dl.noReportsHint')}

)} {/* Report list */}
{filtered.map((report) => { const isExpanded = expandedRid === report.id; const files = filesCache[report.id]; const isLoading = loadingFiles === report.id; const fileCount = files ? files.length : null; return (
{/* Report header */} {/* Expanded file list */} {isExpanded && (
{isLoading ? (
{t('dl.loadingFiles')}
) : !files || !files.length ? (

{t('dl.noFiles')}

) : ( )}
)}
); })}
); } /* ── FileListGrouped sub-component ── */ function FileListGrouped({ files, reportId, sectionFilter, onDownload, }: { files: ReportFile[]; reportId: string; sectionFilter: string; onDownload: (rid: string, fid: string) => void; }) { const { t } = useI18n(); const grouped = groupBySection(files); let sections = Object.keys(grouped).sort(sectionSort); if (sectionFilter) sections = sections.filter((s) => s === sectionFilter); return (
{/* Header */}
{files.length} {t('dl.filesInSections').replace('{0}', String(Object.keys(grouped).length))}
{sections.map((sec) => { const secFiles = grouped[sec]; const Icon = sectionIcons[sec] || FileText; return (
{/* Section header */}
{sec} ({secFiles.length})
{/* File grid */}
{secFiles.map((f) => { const ext = fileExt(f.file_name); const color = extColors[ext] || 'var(--ac)'; return ( ); })}
); })}
); }