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,
+ );
+ }
+ });
+ }
};
};