import { useEffect, useMemo, useState } from 'react'; import * as XLSX from 'xlsx'; import { useLanguage } from '../../../providers/language/LanguageContext'; import styles from '../ContentPreview.module.css'; interface ExcelRendererProps { blob: Blob; fileName: string; onError: (message: string) => void; } const EXCEL_MIME_TYPES = new Set([ 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 'application/vnd.ms-excel', 'text/csv', 'application/csv', ]); export function isExcelMimeType(mimeType?: string, fileName?: string): boolean { if (mimeType && EXCEL_MIME_TYPES.has(mimeType)) return true; if (fileName && /\.(xlsx|xls|xlsm|xlsb|ods|csv)$/i.test(fileName)) return true; return false; } interface RenderedCell { display: string; rawType: 'n' | 's' | 'b' | 'd' | 'e' | 'z' | string; rowspan: number; colspan: number; skip: boolean; } interface RenderedSheet { name: string; cols: number; rows: number; colWidthsPx: number[]; rowHeightsPx: (number | null)[]; cells: RenderedCell[][]; } function renderSheet(ws: XLSX.WorkSheet, name: string): RenderedSheet { const ref = ws['!ref']; if (!ref) { return { name, cols: 0, rows: 0, colWidthsPx: [], rowHeightsPx: [], cells: [] }; } const range = XLSX.utils.decode_range(ref); const rows = range.e.r - range.s.r + 1; const cols = range.e.c - range.s.c + 1; const cells: RenderedCell[][] = Array.from({ length: rows }, () => Array.from({ length: cols }, () => ({ display: '', rawType: 'z', rowspan: 1, colspan: 1, skip: false, })), ); for (let r = 0; r < rows; r++) { for (let c = 0; c < cols; c++) { const address = XLSX.utils.encode_cell({ r: r + range.s.r, c: c + range.s.c }); const cell = ws[address] as XLSX.CellObject | undefined; if (!cell) continue; const display = cell.w ?? (cell.v !== undefined && cell.v !== null ? String(cell.v) : ''); cells[r][c].display = display; cells[r][c].rawType = cell.t ?? 'z'; } } const merges = ws['!merges'] ?? []; for (const merge of merges) { const rs = merge.s.r - range.s.r; const cs = merge.s.c - range.s.c; const re = merge.e.r - range.s.r; const ce = merge.e.c - range.s.c; if (rs < 0 || cs < 0 || re >= rows || ce >= cols) continue; cells[rs][cs].rowspan = re - rs + 1; cells[rs][cs].colspan = ce - cs + 1; for (let r = rs; r <= re; r++) { for (let c = cs; c <= ce; c++) { if (r === rs && c === cs) continue; cells[r][c].skip = true; } } } const colWidthsPx: number[] = []; const colsMeta = ws['!cols'] ?? []; for (let c = 0; c < cols; c++) { const meta = colsMeta[c + range.s.c]; if (meta?.wpx) { colWidthsPx.push(meta.wpx); } else if (meta?.wch) { colWidthsPx.push(Math.round(meta.wch * 7 + 8)); } else if (meta?.width) { colWidthsPx.push(Math.round(meta.width * 7 + 8)); } else { colWidthsPx.push(80); } } const rowHeightsPx: (number | null)[] = []; const rowsMeta = ws['!rows'] ?? []; for (let r = 0; r < rows; r++) { const meta = rowsMeta[r + range.s.r]; if (meta?.hpx) { rowHeightsPx.push(meta.hpx); } else if (meta?.hpt) { rowHeightsPx.push(Math.round((meta.hpt * 4) / 3)); } else { rowHeightsPx.push(null); } } return { name, cols, rows, colWidthsPx, rowHeightsPx, cells }; } function alignmentForCell(cell: RenderedCell): 'left' | 'right' | 'center' { if (cell.rawType === 'n' || cell.rawType === 'd') return 'right'; if (cell.rawType === 'b') return 'center'; return 'left'; } function colLabel(index: number): string { let result = ''; let n = index; do { result = String.fromCharCode(65 + (n % 26)) + result; n = Math.floor(n / 26) - 1; } while (n >= 0); return result; } export function ExcelRenderer({ blob, fileName, onError }: ExcelRendererProps) { const { t } = useLanguage(); const [sheets, setSheets] = useState([]); const [activeSheet, setActiveSheet] = useState(null); const [loading, setLoading] = useState(true); const [localError, setLocalError] = useState(null); useEffect(() => { let cancelled = false; setLoading(true); setLocalError(null); blob .arrayBuffer() .then(buffer => { const workbook = XLSX.read(buffer, { type: 'array', cellDates: true, cellNF: true, cellStyles: true, }); const parsed = workbook.SheetNames.map(name => renderSheet(workbook.Sheets[name], name), ); if (cancelled) return; setSheets(parsed); setActiveSheet(parsed[0]?.name ?? null); setLoading(false); }) .catch(err => { if (cancelled) return; const msg = err?.message ?? t('Tabelle konnte nicht gerendert werden.'); setLocalError(msg); setLoading(false); onError(msg); }); return () => { cancelled = true; }; }, [blob, onError, t]); const current = useMemo( () => sheets.find(s => s.name === activeSheet) ?? null, [sheets, activeSheet], ); if (loading) { return (
{t('Tabelle wird geladen...')}
); } if (localError) { return (
{fileName}
{localError}
); } return (
{fileName} {sheets.length > 1 && (
{sheets.map(sheet => ( ))}
)}
{current && current.rows > 0 ? ( {current.colWidthsPx.map((w, i) => ( ))} ))} {current.cells.map((row, rIdx) => ( {row.map((cell, cIdx) => { if (cell.skip) return null; return ( ); })} ))}
{current.colWidthsPx.map((_, i) => ( {colLabel(i)}
{rIdx + 1} {cell.display}
) : (
{t('Dieses Arbeitsblatt ist leer.')}
)}
); }