diff --git a/src/components/settings/ImportExportSettingsPanel.jsx b/src/components/settings/ImportExportSettingsPanel.jsx index 10ee7d0..0285c6d 100644 --- a/src/components/settings/ImportExportSettingsPanel.jsx +++ b/src/components/settings/ImportExportSettingsPanel.jsx @@ -4,13 +4,8 @@ import Box from '@mui/material/Box'; import Button from '@mui/material/Button'; import CircularProgress from '@mui/material/CircularProgress'; import Divider from '@mui/material/Divider'; -import FormControl from '@mui/material/FormControl'; -import FormControlLabel from '@mui/material/FormControlLabel'; -import FormLabel from '@mui/material/FormLabel'; import IconButton from '@mui/material/IconButton'; import LinearProgress from '@mui/material/LinearProgress'; -import Radio from '@mui/material/Radio'; -import RadioGroup from '@mui/material/RadioGroup'; import Stack from '@mui/material/Stack'; import Typography from '@mui/material/Typography'; import CloudDownloadOutlinedIcon from '@mui/icons-material/CloudDownloadOutlined'; @@ -51,42 +46,14 @@ const PROGRESS_PHASES = [ { key: 'receipts', label: 'Belege', totalKey: 'receiptstotal', doneKey: 'receiptsdone' }, ]; -const EXPORT_OPTIONS = [ - { - id: 'articles', - label: 'Artikel & Medien (ZIP)', - desc: 'artikel.csv inklusive aller referenzierten AVIF-Bilder und Preise.', - endpoint: '/api/article/export.zip', - defaultFilename: 'artikel-export.zip', - }, - { - id: 'categories', - label: 'Kategorien & Bilder (ZIP)', - desc: 'kategorien.csv inklusive Kategoriebäumen und AVIF-Vorschaubildern.', - endpoint: '/api/category/export.zip', - defaultFilename: 'kategorien-export.zip', - }, - { - id: 'company', - label: 'Gesamtsicherung / Mandant (ZIP)', - desc: 'Vollständiger Datenbestand (Artikel, Kategorien, Kunden, Belege und Medien).', - endpoint: '/api/company/export', - defaultFilename: 'mandant-gesamtsicherung.zip', - isSse: true, - }, -]; - export default class ImportExportSettingsPanel extends Component { constructor(props) { super(props); this.state = { // Export state - exportType: 'articles', downloading: false, downloadProgress: null, // null = unknown, number 0-100 downloadStatus: null, - downloadLoaded: 0, - downloadTotal: 0, downloadError: null, // Import state (SSE streaming & upload tracking) @@ -127,117 +94,40 @@ export default class ImportExportSettingsPanel extends Component { } async handleStartDownload() { - const { exportType } = this.state; - const option = EXPORT_OPTIONS.find((opt) => opt.id === exportType) ?? EXPORT_OPTIONS[0]; - this.setState({ downloading: true, downloadProgress: null, - downloadLoaded: 0, - downloadTotal: 0, + downloadStatus: null, downloadError: null, }); this.abortController = new AbortController(); - if (option.isSse || option.id === 'company') { - try { - await company.exportCatalog( - { - onStatus: (message) => { - this.setState({ downloadStatus: message }); - }, - onProgress: (progress) => { - const percent = typeof progress?.percent === 'number' ? progress.percent : null; - const stage = progress?.stage; - this.setState((prev) => ({ - downloadProgress: percent ?? prev.downloadProgress, - downloadStatus: stage - ? `Verarbeite ${stage} (${percent != null ? `${percent}%` : '...'})` - : prev.downloadStatus, - })); - }, - onError: (errMessage) => { - this.setState({ downloadError: errMessage }); - }, - }, - { signal: this.abortController.signal, triggerDownload: true } - ); - - enqueueSnackbar(`${option.label} erfolgreich heruntergeladen.`, { variant: 'success' }); - this.setState({ downloading: false, downloadProgress: 100, downloadStatus: null }); - } catch (err) { - if (err.name === 'AbortError') { - enqueueSnackbar('Download abgebrochen.', { variant: 'info' }); - } else { - this.setState({ downloadError: err.message || String(err) }); - } - } finally { - this.setState({ downloading: false }); - this.abortController = null; - } - return; - } - try { - const response = await fetch(option.endpoint, { - method: 'GET', - credentials: 'include', - signal: this.abortController.signal, - }); + await company.exportCatalog( + { + onStatus: (message) => { + this.setState({ downloadStatus: message }); + }, + onProgress: (progress) => { + const percent = typeof progress?.percent === 'number' ? progress.percent : null; + const stage = progress?.stage; + this.setState((prev) => ({ + downloadProgress: percent ?? prev.downloadProgress, + downloadStatus: stage + ? `Verarbeite ${stage} (${percent != null ? `${percent}%` : '...'})` + : prev.downloadStatus, + })); + }, + onError: (errMessage) => { + this.setState({ downloadError: errMessage }); + }, + }, + { signal: this.abortController.signal, triggerDownload: true } + ); - if (!response.ok) { - let errMessage = `Fehler beim Herunterladen (${response.status})`; - try { - const json = await response.json(); - errMessage = json.error || json.message || errMessage; - } catch { - // Ignore json parse error - } - throw new Error(errMessage); - } - - const contentLengthHeader = response.headers.get('content-length'); - const totalBytes = contentLengthHeader ? parseInt(contentLengthHeader, 10) : 0; - const reader = response.body.getReader(); - const chunks = []; - let loadedBytes = 0; - - while (true) { - const { done, value } = await reader.read(); - if (done) break; - - chunks.push(value); - loadedBytes += value.length; - - if (totalBytes > 0) { - const percent = Math.min(100, Math.round((loadedBytes / totalBytes) * 100)); - this.setState({ - downloadProgress: percent, - downloadLoaded: loadedBytes, - downloadTotal: totalBytes, - }); - } else { - this.setState({ - downloadProgress: null, - downloadLoaded: loadedBytes, - }); - } - } - - // Stream completed — trigger browser file save - const blob = new Blob(chunks, { type: 'application/zip' }); - const blobUrl = URL.createObjectURL(blob); - const a = document.createElement('a'); - a.href = blobUrl; - a.download = option.defaultFilename; - document.body.appendChild(a); - a.click(); - document.body.removeChild(a); - URL.revokeObjectURL(blobUrl); - - enqueueSnackbar(`${option.label} erfolgreich heruntergeladen.`, { variant: 'success' }); - this.setState({ downloading: false, downloadProgress: 100 }); + enqueueSnackbar('Katalog-Export erfolgreich heruntergeladen.', { variant: 'success' }); + this.setState({ downloading: false, downloadProgress: 100, downloadStatus: null }); } catch (err) { if (err.name === 'AbortError') { enqueueSnackbar('Download abgebrochen.', { variant: 'info' }); @@ -392,12 +282,9 @@ export default class ImportExportSettingsPanel extends Component { render() { const { - exportType, downloading, downloadProgress, downloadStatus, - downloadLoaded, - downloadTotal, downloadError, selectedFile, uploading, @@ -447,10 +334,10 @@ export default class ImportExportSettingsPanel extends Component { - Daten exportieren (Download) + Katalog exportieren (Download) - Große Datenmengen werden als ZIP-Archiv gestreamt. + Exportiert den vollständigen Mandantenbestand (Kategorien, Artikel, Varianten, Marken, Pfand und Bilder) als ZIP-Archiv. @@ -463,44 +350,13 @@ export default class ImportExportSettingsPanel extends Component { )} - - - Export-Umfang auswählen - - this.setState({ exportType: e.target.value })} - > - {EXPORT_OPTIONS.map((opt) => ( - } - label={ - - - {opt.label} - - - {opt.desc} - - - } - /> - ))} - - - {downloading && ( {downloadStatus ? (downloadProgress != null ? `${downloadStatus} (${downloadProgress}%)` : downloadStatus) - : downloadProgress != null - ? `${downloadProgress}% (${formatBytes(downloadLoaded)} von ${formatBytes(downloadTotal)})` - : `${formatBytes(downloadLoaded)} geladen (wird gestreamt...)`} + : (downloadProgress != null ? `${downloadProgress}% geladen...` : 'Export wird vorbereitet...')}