From b01ebccf90ebd1ef575c5ca710ea0738cd321bed Mon Sep 17 00:00:00 2001 From: seb Date: Thu, 8 Oct 2026 04:41:27 +0200 Subject: [PATCH] u --- src/api/index.js | 127 +++++ src/components/CompanySetupDialog.jsx | 2 +- .../article/boxes/ArticleViewLists.jsx | 505 ++++++++++------- .../article/tabs/ArticleSettingsTab.jsx | 2 +- .../settings/ImportExportSettingsPanel.jsx | 518 ++++++++++++------ src/devtools/inspector/AutomatEntry.jsx | 4 +- 6 files changed, 796 insertions(+), 362 deletions(-) diff --git a/src/api/index.js b/src/api/index.js index d6ae656..91df7cc 100644 --- a/src/api/index.js +++ b/src/api/index.js @@ -1686,6 +1686,133 @@ export const company = { body: { companyId }, }); }, + + /** + * POST /api/company/import + * Uploads and streams catalog import (.zip) archive via Server-Sent Events. + * Tracks client upload progress (bytes/percentage) and processes incoming SSE events in real-time. + * + * @param {File|Blob|null} file + * @param {object} [callbacks] + * @param {(progress: { loaded: number, total: number, percent: number }) => void} [callbacks.onUploadProgress] + * @param {(message: string) => void} [callbacks.onStatus] + * @param {(progress: { phase: string, current: number, total: number, item?: string }) => void} [callbacks.onProgress] + * @param {(stats: object) => void} [callbacks.onDone] + * @param {(error: string) => void} [callbacks.onError] + * @param {object} [options] + * @param {AbortSignal} [options.signal] + */ + importCatalog(file, callbacks = {}, { signal } = {}) { + return new Promise((resolve, reject) => { + const xhr = new XMLHttpRequest(); + xhr.open('POST', '/api/company/import'); + xhr.withCredentials = true; + xhr.setRequestHeader('Accept', 'text/event-stream'); + + if (signal) { + if (signal.aborted) { + return reject(new DOMException('Aborted', 'AbortError')); + } + signal.addEventListener('abort', () => { + xhr.abort(); + reject(new DOMException('Aborted', 'AbortError')); + }); + } + + // Track request body upload progress + if (file && xhr.upload) { + xhr.upload.onprogress = (event) => { + if (event.lengthComputable && event.total > 0) { + const percent = Math.min(100, Math.round((event.loaded / event.total) * 100)); + callbacks.onUploadProgress?.({ + loaded: event.loaded, + total: event.total, + percent, + }); + } + }; + } + + // Stream and parse incoming SSE chunks as the server processes the archive + let processedIndex = 0; + let buffer = ''; + + const processChunks = () => { + const text = xhr.responseText || ''; + if (text.length <= processedIndex) return; + const newChunk = text.slice(processedIndex); + processedIndex = text.length; + + buffer += newChunk; + const messages = buffer.split('\n\n'); + buffer = messages.pop() || ''; + + for (const msg of messages) { + if (!msg.trim()) continue; + + const eventMatch = msg.match(/^event:\s*(\w+)/m); + const dataMatch = msg.match(/^data:\s*(.+)$/m); + + const eventType = eventMatch ? eventMatch[1] : 'message'; + let eventData = null; + if (dataMatch) { + try { + eventData = JSON.parse(dataMatch[1]); + } catch { + eventData = dataMatch[1]; + } + } + + switch (eventType) { + case 'status': + callbacks.onStatus?.(eventData?.message); + break; + case 'progress': + callbacks.onProgress?.(eventData); + break; + case 'done': + callbacks.onDone?.(eventData?.stats ?? eventData); + break; + case 'error': + callbacks.onError?.(eventData?.message || 'Fehler beim Importieren'); + break; + } + } + }; + + xhr.onprogress = processChunks; + + xhr.onload = () => { + processChunks(); + if (xhr.status >= 200 && xhr.status < 300) { + resolve(); + } else { + let errMsg = `Upload fehlgeschlagen (HTTP ${xhr.status})`; + try { + const parsed = JSON.parse(xhr.responseText); + errMsg = parsed.error || parsed.message || errMsg; + } catch { + // ignore + } + reject(new Error(errMsg)); + } + }; + + xhr.onerror = () => { + reject(new Error('Netzwerkfehler beim Upload der Datei')); + }; + + xhr.onabort = () => { + reject(new DOMException('Aborted', 'AbortError')); + }; + + const formData = new FormData(); + if (file) { + formData.append('file', file); + } + xhr.send(formData); + }); + }, }; export const posterminal = { diff --git a/src/components/CompanySetupDialog.jsx b/src/components/CompanySetupDialog.jsx index 96c9af7..ecd2913 100644 --- a/src/components/CompanySetupDialog.jsx +++ b/src/components/CompanySetupDialog.jsx @@ -406,7 +406,7 @@ class CompanySetupDialog extends Component { Wählen Sie eine Voreinstellung oder passen Sie Präfix, Stellen und Startnummer an: - + {PRESETS.map((p) => { const isSelected = selectedPreset === p.id; return ( diff --git a/src/components/article/boxes/ArticleViewLists.jsx b/src/components/article/boxes/ArticleViewLists.jsx index 07a91df..74de477 100644 --- a/src/components/article/boxes/ArticleViewLists.jsx +++ b/src/components/article/boxes/ArticleViewLists.jsx @@ -39,6 +39,8 @@ import articleIndexAutomat from '../../../automats/articleIndexAutomat'; import { MAGENTA_PANEL, SHADOW_CARD, + WHITE, + alpha, } from '../../../colors'; export default class ArticleViewLists extends PureComponent { @@ -1095,9 +1097,21 @@ export default class ArticleViewLists extends PureComponent { assignedMatch?.attributeValue || null; - const siblings = (parentArticle?.children || []).filter( - (c) => c.sku !== articleData.sku && c.id !== articleData.id + const childrenList = parentArticle?.children || []; + const hasSelf = childrenList.some( + (c) => c.sku === articleData.sku || (articleData.id != null && c.id === articleData.id) ); + const siblings = hasSelf + ? childrenList + : [ + ...childrenList, + { + id: articleData.id, + sku: articleData.sku, + name: articleData.name, + attributeValue: variationValue, + }, + ]; return ( @@ -1206,10 +1220,10 @@ export default class ArticleViewLists extends PureComponent { - {/* Link(s) Geschwister */} + {/* Varianten */} - Geschwister: + Varianten: {siblings.length === 0 ? ( @@ -1217,82 +1231,131 @@ export default class ArticleViewLists extends PureComponent { ) : ( - {siblings.map((sibling) => ( - - { + const isSelf = + sibling.sku === articleData.sku || + (articleData.id != null && sibling.id === articleData.id); + + return ( + - - {sibling.attributeValue || sibling.name || sibling.sku} - - - - {sibling.sku} - - - - - - ))} + + {sibling.attributeValue || variationValue || sibling.name || sibling.sku} + + + + {sibling.sku} + + + + ) : ( + + + {sibling.attributeValue || sibling.name || sibling.sku} + + + + {sibling.sku} + + + + + )} + + ); + })} )} @@ -1510,6 +1573,176 @@ export default class ArticleViewLists extends PureComponent { ) : null} + {/* Vaterartikel */} + + + Vaterartikel: + + + + + {articleData.name || articleData.sku} + + + + {articleData.sku} + + + + + + + {/* Varianten */} + + + Varianten: + + {children.length === 0 ? ( + + Noch keine Varianten angelegt. + + ) : ( + + {children.map((child) => ( + + + + {child.attributeValue || child.name || child.sku} + + + + {child.sku} + + + + + + this.handleOpenRemoveDialog(child)} + title="Variante entfernen" + sx={{ + p: 0.35, + color: 'text.secondary', + '&:hover': { color: 'error.main' }, + }} + > + + + + ))} + + )} + + {/* Add Child Variant Row */} {isCustomChildValue || availableParentValues.length === 0 ? ( @@ -1533,7 +1766,17 @@ export default class ArticleViewLists extends PureComponent { label="Variante" value={selectedChildValueId} disabled={busy} - onChange={(e) => this.setState({ selectedChildValueId: e.target.value })} + onChange={(e) => { + if (e.target.value === '__custom__') { + this.setState({ + isCustomChildValue: true, + selectedChildValueId: '', + newChildText: '', + }); + } else { + this.setState({ selectedChildValueId: e.target.value }); + } + }} sx={{ flex: 1, minWidth: 0 }} > @@ -1544,6 +1787,9 @@ export default class ArticleViewLists extends PureComponent { {v.value} ))} + + + Neuer Wert... + )} @@ -1597,117 +1843,6 @@ export default class ArticleViewLists extends PureComponent { ) : null} - - {/* Children List */} - - {children.length === 0 ? ( - - Noch keine Varianten angelegt. - - ) : ( - children.map((child) => ( - - - - {child.attributeValue || child.name || child.sku} - - - - {child.sku} - - - - - - this.handleOpenRemoveDialog(child)} - title="Variante entfernen" - sx={{ - p: 0.35, - color: 'text.secondary', - '&:hover': { color: 'error.main' }, - }} - > - - - - )) - )} - ) : isChild && parentSku ? ( /* ── Mode 2: Child Variant Article ───────────────────────────────── */ diff --git a/src/components/article/tabs/ArticleSettingsTab.jsx b/src/components/article/tabs/ArticleSettingsTab.jsx index ab9a161..a004236 100644 --- a/src/components/article/tabs/ArticleSettingsTab.jsx +++ b/src/components/article/tabs/ArticleSettingsTab.jsx @@ -131,7 +131,7 @@ export default class ArticleSettingsTab extends Component { return ( - + ) : ( - + - - + + {selectedFile.name} @@ -585,22 +691,88 @@ export default class ImportExportSettingsPanel extends Component { )} + {/* Real-time Streaming Progress Ticker */} {uploading && ( - - - - {uploadProgress}% ({formatBytes(uploadLoaded)} von {formatBytes(uploadTotal)}) - - - - + + {uploadStage === 'upload' ? ( + <> + + + Datei wird übertragen… + + + + + + {uploadProgress}% ({formatBytes(uploadLoaded)} von {formatBytes(uploadTotal)}) + + + + + ) : ( + <> + + + {(importPhase && PHASE_LABELS[importPhase]) || statusMessage || 'Verarbeitung läuft…'} + + + + {importPhaseTotal > 0 ? ( + <> + + + {importItem || 'Fortschritt'} + + + {importPhaseCurrent} / {importPhaseTotal} ({progressPercent}%) + + + + + ) : ( + <> + {statusMessage && ( + + {statusMessage} + + )} + + + )} + + )} )} @@ -617,7 +789,7 @@ export default class ImportExportSettingsPanel extends Component { disabled={uploading || !selectedFile} onClick={this.handleStartUpload} > - {uploading ? 'Wird hochgeladen...' : 'Upload starten'} + {uploading ? 'Import läuft…' : 'Upload & Import starten'} diff --git a/src/devtools/inspector/AutomatEntry.jsx b/src/devtools/inspector/AutomatEntry.jsx index dbf6f6a..4528c36 100644 --- a/src/devtools/inspector/AutomatEntry.jsx +++ b/src/devtools/inspector/AutomatEntry.jsx @@ -71,7 +71,7 @@ export default function AutomatEntry({ automat, isExpanded, flash, onToggle, onC {isExpanded ? ( - + {automat.url ? : null} {automat.persist ? : null} {automat.error ? ( @@ -89,7 +89,7 @@ export default function AutomatEntry({ automat, isExpanded, flash, onToggle, onC - + {actionNames.length === 0 ? ( keine