From d851c6e1e923737b48192a6077eaa71566a2e318 Mon Sep 17 00:00:00 2001 From: seb Date: Fri, 16 Oct 2026 13:10:58 +0200 Subject: [PATCH] u --- .agents/skills/backend/SKILL.md | 14 +- src/api/delivery.js | 79 ------ src/automats/chatAutomat.js | 18 ++ src/components/chat/ChatDrawer.jsx | 6 +- src/components/manifest/Attachment.jsx | 116 +++++---- src/components/manifest/AttachmentList.jsx | 3 +- src/components/manifest/ManifestFormLine.jsx | 257 ++++++++++++++---- src/components/manifest/ManifestHeader.jsx | 2 + src/routes/lieferung/neu.jsx | 260 +------------------ vite.config.js | 16 ++ 10 files changed, 326 insertions(+), 445 deletions(-) diff --git a/.agents/skills/backend/SKILL.md b/.agents/skills/backend/SKILL.md index 6caa8b3..bdaf042 100644 --- a/.agents/skills/backend/SKILL.md +++ b/.agents/skills/backend/SKILL.md @@ -7,7 +7,7 @@ description: >- # Dash API Routes Overview (All) -Overview of all 249 API endpoints across all modules. +Overview of all 248 API endpoints across all modules. ### Notation @@ -406,18 +406,6 @@ _none_ #### Create delivery — `POST /delivery` - Params: `body: { externalNumber: str, date: str, lines: T35[], supplierId?: int|null, files?: { mimetype: str, name: str, content: str, sort?: int, size?: int, hash?: str, created?: str, modified?: str, meta?: { [k]: str } }[] }` - Returns: `201 T9` -#### Parse document (SSE) — `POST /delivery/parse` -- Params: `form: { file: str }` -- Returns: `200 SSE` - -- Events: - - `status`: `{ "message": "…" }` - - `llm_call`: `{ "model": "…", "promptSize": 0, "messages": 0 }` - - `tool_call`: `{ "name": "output_manifest" }` - - `usage`: `{ "input": 0, "cached": 0, "output": 0, "cost": 0 }` - - `progress`: `ManifestResult` - - `result`: `ManifestResult` - - `error`: `{ "message": "…" }` #### Delete delivery — `DELETE /delivery/{id}` - Params: `id: int` - Returns: `204 void` diff --git a/src/api/delivery.js b/src/api/delivery.js index 0a9d23d..03db559 100644 --- a/src/api/delivery.js +++ b/src/api/delivery.js @@ -96,83 +96,4 @@ export const delivery = { return http(`/api/delivery/${id}/file/${fileId}`, { method: 'DELETE' }); }, // #endregion - - /** - * POST /api/delivery/parse — multipart/form-data, field "file" - * Accepts PDF, TXT or CSV document (max 50 MB) - * Streams SSE events: status, llm_call, tool_call, usage, progress, result, error - */ - // #region POST /api/delivery/parse — parseSse - async *parseSse(file, { signal } = {}) { - const formData = new FormData(); - formData.append('file', file, file.name); - const res = await fetch('/api/delivery/parse', { - method: 'POST', - headers: { ...clientIdHeaders() }, - credentials: 'include', - body: formData, - signal, - }); - if (!res.ok) { - let message = res.statusText; - try { - const data = await res.json(); - message = data.error || data.message || message; - } catch { /* ignore */ } - const err = new Error(message || `Request failed (${res.status})`); - err.status = res.status; - throw err; - } - const reader = res.body.getReader(); - const decoder = new TextDecoder(); - let buf = ''; - let currentEvent = 'message'; - - const onAbort = () => { - try { - reader.cancel(); - } catch { /* ignore */ } - }; - if (signal) { - if (signal.aborted) { - onAbort(); - return; - } - signal.addEventListener('abort', onAbort, { once: true }); - } - - try { - while (true) { - const { done, value } = await reader.read(); - if (done) break; - buf += decoder.decode(value, { stream: true }); - const lines = buf.split('\n'); - buf = lines.pop(); - for (const line of lines) { - if (line.startsWith('event:')) { - currentEvent = line.slice(6).trim(); - } else if (line.startsWith('data:')) { - const raw = line.slice(5).trim(); - let data; - try { data = JSON.parse(raw); } catch { data = raw; } - yield { event: currentEvent, data }; - currentEvent = 'message'; - } - } - } - } catch (err) { - if (signal?.aborted || err.name === 'AbortError') { - return; - } - throw err; - } finally { - if (signal) { - signal.removeEventListener('abort', onAbort); - } - try { - reader.releaseLock(); - } catch { /* ignore */ } - } - }, - // #endregion }; diff --git a/src/automats/chatAutomat.js b/src/automats/chatAutomat.js index ff328a7..5b67e92 100644 --- a/src/automats/chatAutomat.js +++ b/src/automats/chatAutomat.js @@ -327,6 +327,24 @@ const actions = { void chatAutomat.actions.loadConversations(); }, + /** + * Open the chat drawer and append text / prompt to the chat composer. + * @param {string} prompt + */ + insertPrompt(prompt = '') { + chatOpenedThisSession = true; + chatAutomat.setState((prev) => ({ + isOpen: true, + sseEstablished: true, + pendingInput: prev.pendingInput ? `${prev.pendingInput}\n\n${prompt}` : prompt, + })); + ensureCompanyEventsConnected(); + if (!chatAutomat.state.selectedConversationId && !chatAutomat.state.draftConversation) { + chatAutomat.actions.startDraft(); + } + void chatAutomat.actions.loadConversations(); + }, + /** * Start an implicit new conversation with the first preset model. Nothing is * created on the server until the first message is sent. diff --git a/src/components/chat/ChatDrawer.jsx b/src/components/chat/ChatDrawer.jsx index 287a1d2..0ac0bbd 100644 --- a/src/components/chat/ChatDrawer.jsx +++ b/src/components/chat/ChatDrawer.jsx @@ -199,9 +199,9 @@ export default class ChatDrawer extends PureComponent { activeTurns: state.activeTurns || [], }; if (state.pendingInput != null) { - patch.inputText = state.pendingInput; - patch.stagedAttachments = []; - patch.stagedReferences = []; + patch.inputText = this.state.inputText + ? `${this.state.inputText}\n\n${state.pendingInput}` + : state.pendingInput; queueMicrotask(() => { if (chatAutomat.state.pendingInput != null) { chatAutomat.setState({ pendingInput: null }); diff --git a/src/components/manifest/Attachment.jsx b/src/components/manifest/Attachment.jsx index b40879c..ecbc891 100644 --- a/src/components/manifest/Attachment.jsx +++ b/src/components/manifest/Attachment.jsx @@ -5,12 +5,12 @@ import Tooltip from '@mui/material/Tooltip'; import Typography from '@mui/material/Typography'; import TextSnippetOutlinedIcon from '@mui/icons-material/TextSnippetOutlined'; import PictureAsPdfIcon from '@mui/icons-material/PictureAsPdf'; -import SyncIcon from '@mui/icons-material/Sync'; +import ChatBubbleOutlineRoundedIcon from '@mui/icons-material/ChatBubbleOutlineRounded'; import DeleteOutlineOutlinedIcon from '@mui/icons-material/DeleteOutlineOutlined'; import { tileBaseSx, TILE_SIZE } from 'dashgui/tiles'; import SkeleHashImage from '../settings/SkeleHashImage'; import { generateTextThumbnail } from '../../utils/textThumbnail'; -import { notifyErrorMessage } from '../../utils/notify'; +import chatAutomat from '../../automats/chatAutomat'; import { SCRIM_ACTION, SCRIM_ATTACHMENT, @@ -61,18 +61,40 @@ export class Attachment extends PureComponent { handleRescan = (e) => { e?.stopPropagation?.(); - if (this.props.hasArticles) { - notifyErrorMessage( - 'Neu analysieren nicht möglich, solange Artikel vorhanden sind. Bitte entfernen Sie zuerst alle Zeilen.', - ); - return; - } const { attachment, onRescan } = this.props; if (attachment && onRescan) { onRescan(attachment); } }; + handleMentionInChat = async (e) => { + e?.stopPropagation?.(); + const { attachment, onMentionInChat } = this.props; + if (!attachment) return; + + if (onMentionInChat) { + onMentionInChat(attachment); + return; + } + + let textContent = attachment.text || ''; + if (!textContent && (attachment.file instanceof Blob || attachment.file instanceof File)) { + try { + textContent = await attachment.file.text(); + } catch (err) { + console.error('Fehler beim Lesen des Texts:', err); + } + } + + const trimmed = (textContent || '').trim(); + if (!trimmed) return; + + const title = attachment.name ? `Text aus „${attachment.name}“:` : 'Text:'; + const quote = `${title}\n\`\`\`\n${trimmed}\n\`\`\``; + + chatAutomat.actions.insertPrompt(quote); + }; + handleDelete = (e) => { e?.stopPropagation?.(); const { attachment, onDelete } = this.props; @@ -237,51 +259,51 @@ export class Attachment extends PureComponent { }} onClick={(e) => e.stopPropagation()} > - {kind === 'delivery' && Boolean(onRescan) && (isPdf || isText) ? ( - - { - e.stopPropagation(); - notifyErrorMessage( - 'Neu analysieren nicht möglich, solange Artikel vorhanden sind. Bitte entfernen Sie zuerst alle Zeilen.', - ); - } - : undefined - } - > - - - - - + + ) : null} + + {isText ? ( + + + + ) : null} + diff --git a/src/components/manifest/ManifestFormLine.jsx b/src/components/manifest/ManifestFormLine.jsx index 3bf59c7..ba23a20 100644 --- a/src/components/manifest/ManifestFormLine.jsx +++ b/src/components/manifest/ManifestFormLine.jsx @@ -40,11 +40,14 @@ class ManifestFormLine extends PureComponent { cachedArticle: sku ? getArticle(sku) : null, loadingArticle: false, fallbackSuppliers: [], + editingField: null, }; this.handleRemove = this.handleRemove.bind(this); this.handleMoveUp = this.handleMoveUp.bind(this); this.handleMoveDown = this.handleMoveDown.bind(this); this.handleBlur = this.handleBlur.bind(this); + this.handleKeyDown = this.handleKeyDown.bind(this); + this.handleStartEdit = this.handleStartEdit.bind(this); this.handleCreateArticle = this.handleCreateArticle.bind(this); this.handleSearchArticle = this.handleSearchArticle.bind(this); this.handleRemoveSupplierArticle = this.handleRemoveSupplierArticle.bind(this); @@ -134,7 +137,25 @@ class ManifestFormLine extends PureComponent { this.props.onMove?.(this.props.line.key, 1); } + handleStartEdit(columnId) { + const { saving } = this.props; + const isDisabled = saving || (columnId === 'sku' && Boolean(this.props.isOnShipment)); + if (isDisabled) return; + if (this.state.editingField !== columnId) { + this.setState({ editingField: columnId }); + } + } + + handleKeyDown(e) { + if (e.key === 'Enter' || e.key === 'Escape') { + e.target.blur(); + } + } + handleBlur(e, columnId) { + if (this.state.editingField === columnId) { + this.setState({ editingField: null }); + } if (columnId === 'sku' && this.props.onSkuBlur) { this.props.onSkuBlur(this.props.line.key, e.target.value); } @@ -229,6 +250,8 @@ class ManifestFormLine extends PureComponent { renderField(column, colIdx, totalCols) { const { id, align, type } = column; const { line, saving, onChange, columns } = this.props; + const isFirstLine = Boolean(this.props.isFirstLine ?? (this.props.index === 0)); + const isLastLine = Boolean(this.props.isLastLine); if (id === 'pos') { const posVal = @@ -240,8 +263,10 @@ class ManifestFormLine extends PureComponent { display: 'flex', alignItems: 'center', justifyContent: 'center', - height: '100%', - minHeight: 38, + height: isFirstLine ? 40 : 41, + minHeight: isFirstLine ? 40 : 41, + maxHeight: isFirstLine ? 40 : 41, + mt: isFirstLine ? 0 : '-1px', fontWeight: 700, fontSize: '0.82rem', color: 'text.secondary', @@ -297,65 +322,197 @@ class ManifestFormLine extends PureComponent { const isFirstInput = hasPosCol ? colIdx === 1 : colIdx === 0; const isLast = colIdx === totalCols - 1; + const topLeftRadius = isFirstInput && isFirstLine ? 4 : 0; + const topRightRadius = isLast && isFirstLine ? 4 : 0; + const bottomRightRadius = isLast && isLastLine ? 4 : 0; + const bottomLeftRadius = isFirstInput && isLastLine ? 4 : 0; + const borderRadius = `${topLeftRadius}px ${topRightRadius}px ${bottomRightRadius}px ${bottomLeftRadius}px`; + const { dirtyFields } = this.props; const isFieldDirty = Boolean( dirtyFields && (dirtyFields.__all__ || dirtyFields[id]) ); - return ( - onChange(line.key, id, e.target.value)} - onBlur={(e) => this.handleBlur(e, id)} - slotProps={{ - htmlInput: { - inputMode: type === 'number' ? 'decimal' : 'text', - style: { textAlign: align }, - autoComplete: 'off', - step: type === 'number' ? 'any' : undefined, - }, - }} - sx={{ - '& .MuiOutlinedInput-root': { - borderRadius: isFirstInput ? '4px 0 0 4px' : isLast ? '0 4px 4px 0' : 0, + const isEditing = this.state.editingField === id; + const isDisabled = saving || (id === 'sku' && Boolean(this.props.isOnShipment)); + const titleText = id === 'sku' && this.props.isOnShipment + ? 'Artikel ist bereits auf einem Lieferschein und kann nicht geändert werden' + : undefined; + + if (isEditing) { + return ( + onChange(line.key, id, e.target.value)} + onBlur={(e) => this.handleBlur(e, id)} + onKeyDown={this.handleKeyDown} + slotProps={{ + htmlInput: { + inputMode: type === 'number' ? 'decimal' : 'text', + style: { textAlign: align }, + autoComplete: 'off', + step: type === 'number' ? 'any' : undefined, + }, + }} + sx={{ + position: 'relative', ml: isFirstInput ? 0 : '-1px', - ...(isFieldDirty && { - bgcolor: 'rgba(237, 108, 2, 0.08)', - }), - '&:hover': { - zIndex: 1, + width: isFirstInput ? '100%' : 'calc(100% + 1px)', + mt: isFirstLine ? 0 : '-1px', + height: isFirstLine ? 40 : 41, + zIndex: 3, + '& .MuiOutlinedInput-root': { + height: isFirstLine ? 40 : 41, + borderRadius, + width: '100%', + ...(isFieldDirty && { + bgcolor: 'rgba(237, 108, 2, 0.08)', + }), + '&:hover': { + zIndex: 4, + }, + '&.Mui-focused': { + zIndex: 5, + }, }, - '&.Mui-focused': { - zIndex: 2, - }, - }, - ...(isFieldDirty && { - zIndex: 1, '& .MuiOutlinedInput-notchedOutline': { - borderColor: 'warning.main', - borderWidth: 2, + borderWidth: '1px !important', + borderColor: isFieldDirty + ? 'warning.main' + : isError + ? 'error.main' + : 'rgba(0, 0, 0, 0.23)', + }, + '&.Mui-focused .MuiOutlinedInput-notchedOutline': { + borderWidth: '1px !important', + borderColor: isFieldDirty + ? 'warning.main' + : isError + ? 'error.main' + : 'primary.main', }, '&:hover .MuiOutlinedInput-notchedOutline': { - borderColor: 'warning.main', + borderWidth: '1px !important', + borderColor: isFieldDirty + ? 'warning.main' + : isError + ? 'error.main' + : 'rgba(0, 0, 0, 0.87)', }, + '& input[type=number]': { + MozAppearance: 'textfield', + }, + '& input[type=number]::-webkit-outer-spin-button, & input[type=number]::-webkit-inner-spin-button': + { + WebkitAppearance: 'none', + margin: 0, + }, + }} + /> + ); + } + + const hasValue = val != null && String(val).trim() !== ''; + + return ( + this.handleStartEdit(id)} + onFocus={() => this.handleStartEdit(id)} + onKeyDown={(e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + this.handleStartEdit(id); + } + }} + title={titleText} + sx={{ + display: 'flex', + alignItems: 'center', + justifyContent: align === 'right' ? 'flex-end' : align === 'center' ? 'center' : 'flex-start', + height: isFirstLine ? 40 : 41, + minHeight: isFirstLine ? 40 : 41, + maxHeight: isFirstLine ? 40 : 41, + boxSizing: 'border-box', + position: 'relative', + ml: isFirstInput ? 0 : '-1px', + width: isFirstInput ? '100%' : 'calc(100% + 1px)', + mt: isFirstLine ? 0 : '-1px', + px: '14px', + py: 0, + borderRadius, + border: '1px solid', + borderColor: isFieldDirty + ? 'warning.main' + : isError + ? 'error.main' + : 'rgba(0, 0, 0, 0.23)', + cursor: isDisabled ? 'default' : 'text', + touchAction: 'manipulation', + userSelect: 'none', + zIndex: 1, + ...(isFieldDirty && { + bgcolor: 'rgba(237, 108, 2, 0.08)', + zIndex: 2, }), - '& input[type=number]': { - MozAppearance: 'textfield', - }, - '& input[type=number]::-webkit-outer-spin-button, & input[type=number]::-webkit-inner-spin-button': - { - WebkitAppearance: 'none', - margin: 0, + ...(isError && !isFieldDirty && { + bgcolor: 'rgba(211, 47, 47, 0.04)', + borderColor: 'error.main', + zIndex: 2, + }), + '&:hover': isDisabled + ? undefined + : { + bgcolor: isFieldDirty + ? 'rgba(237, 108, 2, 0.14)' + : isError + ? 'rgba(211, 47, 47, 0.08)' + : 'action.hover', + borderColor: isFieldDirty + ? 'warning.main' + : isError + ? 'error.main' + : 'rgba(0, 0, 0, 0.87)', + zIndex: 3, + }, + '&:focus-visible': { + outline: '2px solid', + outlineColor: 'primary.main', + outlineOffset: -2, + zIndex: 4, }, }} - /> + > + + {hasValue ? String(val) : '—'} + + ); } @@ -979,7 +1136,11 @@ class ManifestFormLine extends PureComponent { sx={{ p: 0, width: col.width, - verticalAlign: 'middle', + minWidth: col.width, + height: 40, + verticalAlign: 'top', + position: 'relative', + overflow: 'visible', }} > {this.renderField(col, colIdx, columns.length)} diff --git a/src/components/manifest/ManifestHeader.jsx b/src/components/manifest/ManifestHeader.jsx index 3632c8e..04c737d 100644 --- a/src/components/manifest/ManifestHeader.jsx +++ b/src/components/manifest/ManifestHeader.jsx @@ -243,6 +243,7 @@ class ManifestHeader extends PureComponent { onPdfDelete, onPdfClick, onAttachmentClick, + onMentionInChat, onDateChange, onNumberChange, onExternalNumberChange, @@ -456,6 +457,7 @@ class ManifestHeader extends PureComponent { onRescan={onPdfRescan} onDelete={onPdfDelete} onClick={onAttachmentClick ?? onPdfClick} + onMentionInChat={onMentionInChat} disabled={saving || pdfUploading} hasArticles={hasArticles} /> diff --git a/src/routes/lieferung/neu.jsx b/src/routes/lieferung/neu.jsx index 535aa03..0b75fd1 100644 --- a/src/routes/lieferung/neu.jsx +++ b/src/routes/lieferung/neu.jsx @@ -36,7 +36,6 @@ import { ArticleSearchDialog, BookDeliveryDialog, } from '../../components/manifest'; -import ManifestPdfDialog from '../../components/manifest/ManifestPdfDialog'; import { loadParties, apiFor } from '../../components/manifest/manifestConfig'; import { article as articleApi, delivery as deliveryApi } from '../../api'; import { getCategoryState } from '../../automats/categoryAutomat'; @@ -68,9 +67,6 @@ class LieferungNeuPage extends PureComponent { partiesLoading: true, saving: false, pdfUploading: false, - pdfDialogOpen: false, - pdfDialogData: null, - pdfFileName: '', textEditorOpen: false, textEditorAttachment: null, viewerPosition: userSettingsAutomat.state.viewerPosition, @@ -105,8 +101,6 @@ class LieferungNeuPage extends PureComponent { this.handlePdfUpload = this.handlePdfUpload.bind(this); this.handlePdfRescan = this.handlePdfRescan.bind(this); this.handlePdfDelete = this.handlePdfDelete.bind(this); - this.handleClosePdfDialog = this.handleClosePdfDialog.bind(this); - this.handleAbortParse = this.handleAbortParse.bind(this); this.handleAttachmentClick = this.handleAttachmentClick.bind(this); this.handleCloseTextEditor = this.handleCloseTextEditor.bind(this); this.handleTextEditorPositionChange = this.handleTextEditorPositionChange.bind(this); @@ -159,10 +153,6 @@ class LieferungNeuPage extends PureComponent { componentWillUnmount() { this._cancelled = true; - if (this._parseAbortController) { - this._parseAbortController.abort(); - this._parseAbortController = null; - } if (this._skuDebounceTimers) { Object.values(this._skuDebounceTimers).forEach(clearTimeout); this._skuDebounceTimers = null; @@ -301,7 +291,6 @@ class LieferungNeuPage extends PureComponent { handleCreateSupplierFromPdf(supplierName) { const name = (supplierName ?? this.state.unmatchedSupplierName ?? '').trim(); - this.handleClosePdfDialog(); const target = `/lieferung/lieferanten?name=${encodeURIComponent(name)}&returnTo=/lieferung/neu`; this.props.navigate(target, { state: { name, returnTo: '/lieferung/neu' } }); } @@ -635,9 +624,6 @@ class LieferungNeuPage extends PureComponent { }; this.automat.actions.addAttachment(textAttachment); } - - // 3. Run SSE parsing with backend (POST /api/delivery/parse) - await this.runParse(file); } async handlePdfRescan(attachment) { @@ -697,8 +683,6 @@ class LieferungNeuPage extends PureComponent { console.error('Fehler beim Aktualisieren der Textdatei:', txtErr); } } - - await this.runParse(attachment.file); } handlePdfDelete(attachment) { @@ -706,227 +690,6 @@ class LieferungNeuPage extends PureComponent { this.automat.actions.removeAttachment(attachment.id); } - async runParse(file) { - if (this._parseAbortController) { - this._parseAbortController.abort(); - } - const abortController = new AbortController(); - this._parseAbortController = abortController; - - this.setState({ - pdfUploading: true, - pdfDialogOpen: true, - pdfFileName: file.name, - pdfDialogData: { streaming: true, messages: [], result: null, error: null, aborted: false }, - }); - - const appendMessage = (msg) => { - if (this._cancelled || abortController.signal.aborted) return; - this.setState((prev) => ({ - pdfDialogData: { - ...prev.pdfDialogData, - messages: [...(prev.pdfDialogData?.messages ?? []), msg], - }, - })); - }; - - try { - for await (const { event, data } of deliveryApi.parseSse(file, { signal: abortController.signal })) { - if (this._cancelled || abortController.signal.aborted) return; - switch (event) { - case 'status': - appendMessage({ type: 'status', text: data.message ?? String(data) }); - break; - case 'llm_call': - appendMessage({ - type: 'llm_call', - text: `LLM-Aufruf: ${data.model} (${data.promptSize} tokens, ${data.messages} msg)`, - }); - break; - case 'tool_call': - appendMessage({ type: 'tool_call', text: `Tool-Aufruf: ${data.name}` }); - break; - case 'usage': - appendMessage({ - type: 'usage', - text: `Token-Verbrauch: ${data.input} in / ${data.cached} cached / ${data.output} out — $${(data.cost ?? 0).toFixed(4)}`, - }); - break; - case 'progress': - this.setState((prev) => ({ - pdfDialogData: { ...prev.pdfDialogData, result: data }, - })); - break; - case 'result': - case 'done': { - const result = data?.delivery ?? data; - const rawItems = Array.isArray(result?.lines) - ? result.lines - : Array.isArray(result?.items?.items) - ? result.items.items - : null; - const newLines = Array.isArray(rawItems) - ? rawItems.map((l, idx) => { - const taxVal = (l.taxrate ?? l.tax ?? l.taxRate ?? l.mwst) != null - ? String(l.taxrate ?? l.tax ?? l.taxRate ?? l.mwst) - : ''; - const netVal = (l.netprice ?? l.net ?? l.netPrice ?? (l.eknetto != null ? (l.eknetto / 100).toFixed(2) : null)) != null - ? String(l.netprice ?? l.net ?? l.netPrice ?? (l.eknetto != null ? (l.eknetto / 100).toFixed(2) : '')) - : ''; - return { - key: l.key ?? `${Date.now()}-${idx}-${Math.random().toString(36).slice(2, 8)}`, - name: l.name ?? l.articleName ?? null, - sku: l.sku ?? l.liefArtNr ?? '', - amount: (l.amount ?? l.count) != null ? String(l.amount ?? l.count) : '', - pu: (l.pu ?? l.vpe) != null ? String(l.pu ?? l.vpe) : '', - unit: l.unit ?? '', - taxrate: taxVal, - tax: taxVal, - netprice: netVal, - net: netVal, - }; - }) - : null; - - this.setState((prev) => ({ - pdfDialogData: { ...prev.pdfDialogData, streaming: false, result }, - })); - - const docDate = result?.date ?? result?.basedata?.date; - let supplierId = result?.supplierId ?? result?.basedata?.kLieferant; - const extNum = result?.externalNumber ?? result?.basedata?.manifestNr; - const docType = result?.type ?? result?.basedata?.manifestType; - const notes = Array.isArray(result?.notes) ? result.notes : []; - - const parsedSupplierName = ( - result?.supplierName ?? - result?.basedata?.lieferantName ?? - result?.lieferantName ?? - result?.basedata?.supplierName ?? - '' - ).trim(); - - // If supplierId is null, try matching supplier by name from loaded delivery entities - if (supplierId == null && parsedSupplierName && Array.isArray(this.state.deliveryEntities)) { - const nameToMatch = parsedSupplierName.toLowerCase(); - const matched = this.state.deliveryEntities.find( - (e) => - e.name && - (e.name.trim().toLowerCase() === nameToMatch || - e.name.toLowerCase().includes(nameToMatch) || - nameToMatch.includes(e.name.toLowerCase())), - ); - if (matched) supplierId = matched.id; - } - - const unmatchedSupplierName = supplierId == null && parsedSupplierName ? parsedSupplierName : null; - this.setState({ unmatchedSupplierName }); - this.automat.actions.setUnmatchedSupplierName(unmatchedSupplierName); - - - if (docDate) this.automat.actions.setDate(docDate); - if (supplierId != null) this.automat.actions.setDeliveryEntity(supplierId); - if (extNum != null) { - this.automat.actions.setExternalNumber(String(extNum)); - } - if (docType) this.automat.actions.setType(docType); - if (newLines && newLines.length > 0) { - this.automat.actions.setLineitems(newLines); - } - if (notes.length > 0) { - this.automat.actions.setNotes(notes); - } - - // Supply whole article list to GET /supplier/{id}/articles - const effectiveSupplierId = supplierId ?? this.automat.state.deliveryEntity ?? this.automat.state.partyId; - const effectiveLines = (newLines && newLines.length > 0) ? newLines : this.automat.state.lines; - if (effectiveSupplierId != null && effectiveLines && effectiveLines.length > 0) { - this.automat.actions.fetchSupplierArticles(effectiveSupplierId, effectiveLines); - } - break; - } - case 'error': - this.setState((prev) => ({ - pdfDialogData: { - ...prev.pdfDialogData, - streaming: false, - error: data.message ?? 'Analyse fehlgeschlagen.', - }, - })); - break; - default: - break; - } - } - } catch (err) { - if (this._cancelled) return; - if (abortController.signal.aborted || err.name === 'AbortError') { - this.setState((prev) => ({ - pdfDialogData: { - ...prev.pdfDialogData, - streaming: false, - aborted: true, - error: 'Analyse abgebrochen.', - }, - })); - return; - } - this.setState((prev) => ({ - pdfDialogData: { - ...prev.pdfDialogData, - streaming: false, - error: err.message ?? 'Netzwerkfehler bei Analyse.', - }, - })); - } finally { - if (this._parseAbortController === abortController) { - this._parseAbortController = null; - } - if (!this._cancelled) { - this.setState((prev) => ({ - pdfUploading: false, - pdfDialogData: prev.pdfDialogData - ? { ...prev.pdfDialogData, streaming: false } - : null, - })); - } - } - } - - async runPdfParse(file) { - await this.runParse(file); - } - - async runTxtParse(file) { - await this.runParse(file); - } - - handleAbortParse() { - if (this._parseAbortController) { - this._parseAbortController.abort(); - this._parseAbortController = null; - } - this.setState((prev) => ({ - pdfUploading: false, - pdfDialogData: prev.pdfDialogData - ? { - ...prev.pdfDialogData, - streaming: false, - aborted: true, - error: 'Analyse abgebrochen.', - } - : null, - })); - } - - handleClosePdfDialog() { - if (this._parseAbortController) { - this._parseAbortController.abort(); - this._parseAbortController = null; - } - this.setState({ pdfDialogOpen: false, pdfUploading: false }); - } - async ensureAttachmentFile(attachment) { if (!attachment) return null; const hasContent = @@ -1127,7 +890,7 @@ class LieferungNeuPage extends PureComponent { isViewerOpen() { return Boolean( (this.state.textEditorOpen && this.state.textEditorAttachment) || - (this.state.pdfViewerOpen && this.state.pdfViewerAttachment), + (this.state.pdfViewerOpen && this.state.pdfViewerAttachment), ); } @@ -1244,9 +1007,6 @@ class LieferungNeuPage extends PureComponent { partiesLoading, saving, pdfUploading, - pdfDialogOpen, - pdfDialogData, - pdfFileName, deliveryEntities, date, partyId, @@ -1376,15 +1136,7 @@ class LieferungNeuPage extends PureComponent { )} - + @@ -1554,8 +1306,8 @@ class LieferungNeuPage extends PureComponent { {status === 'booked' ? 'Buchungsdetails anzeigen' : status === 'partial' - ? 'Restliche Mengen einbuchen' - : 'Ins Lager buchen'} + ? 'Restliche Mengen einbuchen' + : 'Ins Lager buchen'} diff --git a/vite.config.js b/vite.config.js index 155adcb..2789fab 100644 --- a/vite.config.js +++ b/vite.config.js @@ -6,6 +6,7 @@ import { defineConfig, searchForWorkspaceRoot, createLogger } from 'vite'; import react from '@vitejs/plugin-react'; import { VitePWA } from 'vite-plugin-pwa'; import pc from 'picocolors'; +import qrcode from 'qrcode'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const useHttps = process.env.VITE_HTTPS === '1'; @@ -94,6 +95,7 @@ const sharedResolve = { const sharedServer = { // HTTP :4209 — HTTPS :4308 (self-signed; see npm run cert / dev:https) + host: true, port: useHttps ? 4308 : 4209, https: httpsOptions(), allowedHosts: true, @@ -266,6 +268,20 @@ function pidReporterPlugin() { server.config.logger.info( ` ${pc.green('➜')} ${pc.bold('PID')}: ${pc.cyan(process.pid)}${pmId}`, ); + const networkUrls = server.resolvedUrls?.network ?? []; + const primaryUrl = + networkUrls.find((u) => !u.includes('172.17.') && !u.includes('169.254.')) || + networkUrls[0] || + server.resolvedUrls?.local?.[0]; + if (primaryUrl) { + qrcode.toString(primaryUrl, { type: 'terminal', small: true }, (err, qr) => { + if (!err && qr) { + server.config.logger.info( + `\n ${pc.bold('Scan to open on mobile')} ${pc.dim(`(${primaryUrl})`)}:\n` + qr, + ); + } + }); + } }; };