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...')}