u
This commit is contained in:
@@ -7,7 +7,7 @@ description: >-
|
|||||||
|
|
||||||
# Dash API Routes Overview (All)
|
# Dash API Routes Overview (All)
|
||||||
|
|
||||||
Overview of all 249 API endpoints across all modules.
|
Overview of all 248 API endpoints across all modules.
|
||||||
|
|
||||||
### Notation
|
### Notation
|
||||||
|
|
||||||
@@ -406,18 +406,6 @@ _none_
|
|||||||
#### Create delivery — `POST /delivery`
|
#### 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 } }[] }`
|
- 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`
|
- 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}`
|
#### Delete delivery — `DELETE /delivery/{id}`
|
||||||
- Params: `id: int`
|
- Params: `id: int`
|
||||||
- Returns: `204 void`
|
- Returns: `204 void`
|
||||||
|
|||||||
@@ -96,83 +96,4 @@ export const delivery = {
|
|||||||
return http(`/api/delivery/${id}/file/${fileId}`, { method: 'DELETE' });
|
return http(`/api/delivery/${id}/file/${fileId}`, { method: 'DELETE' });
|
||||||
},
|
},
|
||||||
// #endregion
|
// #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
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -327,6 +327,24 @@ const actions = {
|
|||||||
void chatAutomat.actions.loadConversations();
|
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
|
* Start an implicit new conversation with the first preset model. Nothing is
|
||||||
* created on the server until the first message is sent.
|
* created on the server until the first message is sent.
|
||||||
|
|||||||
@@ -199,9 +199,9 @@ export default class ChatDrawer extends PureComponent {
|
|||||||
activeTurns: state.activeTurns || [],
|
activeTurns: state.activeTurns || [],
|
||||||
};
|
};
|
||||||
if (state.pendingInput != null) {
|
if (state.pendingInput != null) {
|
||||||
patch.inputText = state.pendingInput;
|
patch.inputText = this.state.inputText
|
||||||
patch.stagedAttachments = [];
|
? `${this.state.inputText}\n\n${state.pendingInput}`
|
||||||
patch.stagedReferences = [];
|
: state.pendingInput;
|
||||||
queueMicrotask(() => {
|
queueMicrotask(() => {
|
||||||
if (chatAutomat.state.pendingInput != null) {
|
if (chatAutomat.state.pendingInput != null) {
|
||||||
chatAutomat.setState({ pendingInput: null });
|
chatAutomat.setState({ pendingInput: null });
|
||||||
|
|||||||
@@ -5,12 +5,12 @@ import Tooltip from '@mui/material/Tooltip';
|
|||||||
import Typography from '@mui/material/Typography';
|
import Typography from '@mui/material/Typography';
|
||||||
import TextSnippetOutlinedIcon from '@mui/icons-material/TextSnippetOutlined';
|
import TextSnippetOutlinedIcon from '@mui/icons-material/TextSnippetOutlined';
|
||||||
import PictureAsPdfIcon from '@mui/icons-material/PictureAsPdf';
|
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 DeleteOutlineOutlinedIcon from '@mui/icons-material/DeleteOutlineOutlined';
|
||||||
import { tileBaseSx, TILE_SIZE } from 'dashgui/tiles';
|
import { tileBaseSx, TILE_SIZE } from 'dashgui/tiles';
|
||||||
import SkeleHashImage from '../settings/SkeleHashImage';
|
import SkeleHashImage from '../settings/SkeleHashImage';
|
||||||
import { generateTextThumbnail } from '../../utils/textThumbnail';
|
import { generateTextThumbnail } from '../../utils/textThumbnail';
|
||||||
import { notifyErrorMessage } from '../../utils/notify';
|
import chatAutomat from '../../automats/chatAutomat';
|
||||||
import {
|
import {
|
||||||
SCRIM_ACTION,
|
SCRIM_ACTION,
|
||||||
SCRIM_ATTACHMENT,
|
SCRIM_ATTACHMENT,
|
||||||
@@ -61,18 +61,40 @@ export class Attachment extends PureComponent {
|
|||||||
|
|
||||||
handleRescan = (e) => {
|
handleRescan = (e) => {
|
||||||
e?.stopPropagation?.();
|
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;
|
const { attachment, onRescan } = this.props;
|
||||||
if (attachment && onRescan) {
|
if (attachment && onRescan) {
|
||||||
onRescan(attachment);
|
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) => {
|
handleDelete = (e) => {
|
||||||
e?.stopPropagation?.();
|
e?.stopPropagation?.();
|
||||||
const { attachment, onDelete } = this.props;
|
const { attachment, onDelete } = this.props;
|
||||||
@@ -237,51 +259,51 @@ export class Attachment extends PureComponent {
|
|||||||
}}
|
}}
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
>
|
>
|
||||||
{kind === 'delivery' && Boolean(onRescan) && (isPdf || isText) ? (
|
{kind === 'delivery' && Boolean(onRescan) && isPdf ? (
|
||||||
<Tooltip
|
<IconButton
|
||||||
title={
|
size="small"
|
||||||
hasArticles
|
disabled={disabled}
|
||||||
? 'Neu analysieren nicht möglich, solange Artikel vorhanden sind'
|
onClick={this.handleRescan}
|
||||||
: 'Neu analysieren'
|
aria-label="Zu Text"
|
||||||
}
|
sx={{
|
||||||
|
bgcolor: SCRIM_ACTION,
|
||||||
|
color: WHITE,
|
||||||
|
p: 0.5,
|
||||||
|
backdropFilter: 'blur(2px)',
|
||||||
|
'&:hover': {
|
||||||
|
bgcolor: SCRIM_HEAVY,
|
||||||
|
},
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<span
|
<TextSnippetOutlinedIcon sx={{ fontSize: 16 }} />
|
||||||
style={{ display: 'inline-block' }}
|
</IconButton>
|
||||||
onClick={
|
|
||||||
hasArticles
|
|
||||||
? (e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
notifyErrorMessage(
|
|
||||||
'Neu analysieren nicht möglich, solange Artikel vorhanden sind. Bitte entfernen Sie zuerst alle Zeilen.',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<IconButton
|
|
||||||
size="small"
|
|
||||||
disabled={disabled || hasArticles}
|
|
||||||
onClick={this.handleRescan}
|
|
||||||
sx={{
|
|
||||||
bgcolor: SCRIM_ACTION,
|
|
||||||
color: hasArticles ? VEIL_DISABLED : WHITE,
|
|
||||||
p: 0.5,
|
|
||||||
backdropFilter: 'blur(2px)',
|
|
||||||
cursor: hasArticles ? 'not-allowed' : 'pointer',
|
|
||||||
'&:hover': {
|
|
||||||
bgcolor: hasArticles ? SCRIM_ACTION : SCRIM_HEAVY,
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<SyncIcon sx={{ fontSize: 16 }} />
|
|
||||||
</IconButton>
|
|
||||||
</span>
|
|
||||||
</Tooltip>
|
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
|
{isText ? (
|
||||||
|
<IconButton
|
||||||
|
size="small"
|
||||||
|
disabled={disabled}
|
||||||
|
onClick={this.handleMentionInChat}
|
||||||
|
aria-label="Im Chat erwähnen"
|
||||||
|
sx={{
|
||||||
|
bgcolor: SCRIM_ACTION,
|
||||||
|
color: WHITE,
|
||||||
|
p: 0.5,
|
||||||
|
backdropFilter: 'blur(2px)',
|
||||||
|
'&:hover': {
|
||||||
|
bgcolor: SCRIM_HEAVY,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ChatBubbleOutlineRoundedIcon sx={{ fontSize: 16 }} />
|
||||||
|
</IconButton>
|
||||||
|
) : null}
|
||||||
|
|
||||||
<IconButton
|
<IconButton
|
||||||
size="small"
|
size="small"
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
onClick={this.handleDelete}
|
onClick={this.handleDelete}
|
||||||
|
aria-label="Löschen"
|
||||||
sx={{
|
sx={{
|
||||||
bgcolor: SCRIM_ACTION,
|
bgcolor: SCRIM_ACTION,
|
||||||
color: WHITE,
|
color: WHITE,
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import Attachment from './Attachment';
|
|||||||
|
|
||||||
export class AttachmentList extends PureComponent {
|
export class AttachmentList extends PureComponent {
|
||||||
render() {
|
render() {
|
||||||
const { attachments, onRescan, onDelete, onClick, disabled, hasArticles, kind, docId } = this.props;
|
const { attachments, onRescan, onDelete, onClick, onMentionInChat, disabled, hasArticles, kind, docId } = this.props;
|
||||||
if (!Array.isArray(attachments) || attachments.length === 0) {
|
if (!Array.isArray(attachments) || attachments.length === 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -31,6 +31,7 @@ export class AttachmentList extends PureComponent {
|
|||||||
onRescan={onRescan}
|
onRescan={onRescan}
|
||||||
onDelete={onDelete}
|
onDelete={onDelete}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
|
onMentionInChat={onMentionInChat}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
hasArticles={hasArticles}
|
hasArticles={hasArticles}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -40,11 +40,14 @@ class ManifestFormLine extends PureComponent {
|
|||||||
cachedArticle: sku ? getArticle(sku) : null,
|
cachedArticle: sku ? getArticle(sku) : null,
|
||||||
loadingArticle: false,
|
loadingArticle: false,
|
||||||
fallbackSuppliers: [],
|
fallbackSuppliers: [],
|
||||||
|
editingField: null,
|
||||||
};
|
};
|
||||||
this.handleRemove = this.handleRemove.bind(this);
|
this.handleRemove = this.handleRemove.bind(this);
|
||||||
this.handleMoveUp = this.handleMoveUp.bind(this);
|
this.handleMoveUp = this.handleMoveUp.bind(this);
|
||||||
this.handleMoveDown = this.handleMoveDown.bind(this);
|
this.handleMoveDown = this.handleMoveDown.bind(this);
|
||||||
this.handleBlur = this.handleBlur.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.handleCreateArticle = this.handleCreateArticle.bind(this);
|
||||||
this.handleSearchArticle = this.handleSearchArticle.bind(this);
|
this.handleSearchArticle = this.handleSearchArticle.bind(this);
|
||||||
this.handleRemoveSupplierArticle = this.handleRemoveSupplierArticle.bind(this);
|
this.handleRemoveSupplierArticle = this.handleRemoveSupplierArticle.bind(this);
|
||||||
@@ -134,7 +137,25 @@ class ManifestFormLine extends PureComponent {
|
|||||||
this.props.onMove?.(this.props.line.key, 1);
|
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) {
|
handleBlur(e, columnId) {
|
||||||
|
if (this.state.editingField === columnId) {
|
||||||
|
this.setState({ editingField: null });
|
||||||
|
}
|
||||||
if (columnId === 'sku' && this.props.onSkuBlur) {
|
if (columnId === 'sku' && this.props.onSkuBlur) {
|
||||||
this.props.onSkuBlur(this.props.line.key, e.target.value);
|
this.props.onSkuBlur(this.props.line.key, e.target.value);
|
||||||
}
|
}
|
||||||
@@ -229,6 +250,8 @@ class ManifestFormLine extends PureComponent {
|
|||||||
renderField(column, colIdx, totalCols) {
|
renderField(column, colIdx, totalCols) {
|
||||||
const { id, align, type } = column;
|
const { id, align, type } = column;
|
||||||
const { line, saving, onChange, columns } = this.props;
|
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') {
|
if (id === 'pos') {
|
||||||
const posVal =
|
const posVal =
|
||||||
@@ -240,8 +263,10 @@ class ManifestFormLine extends PureComponent {
|
|||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
height: '100%',
|
height: isFirstLine ? 40 : 41,
|
||||||
minHeight: 38,
|
minHeight: isFirstLine ? 40 : 41,
|
||||||
|
maxHeight: isFirstLine ? 40 : 41,
|
||||||
|
mt: isFirstLine ? 0 : '-1px',
|
||||||
fontWeight: 700,
|
fontWeight: 700,
|
||||||
fontSize: '0.82rem',
|
fontSize: '0.82rem',
|
||||||
color: 'text.secondary',
|
color: 'text.secondary',
|
||||||
@@ -297,65 +322,197 @@ class ManifestFormLine extends PureComponent {
|
|||||||
const isFirstInput = hasPosCol ? colIdx === 1 : colIdx === 0;
|
const isFirstInput = hasPosCol ? colIdx === 1 : colIdx === 0;
|
||||||
const isLast = colIdx === totalCols - 1;
|
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 { dirtyFields } = this.props;
|
||||||
const isFieldDirty = Boolean(
|
const isFieldDirty = Boolean(
|
||||||
dirtyFields && (dirtyFields.__all__ || dirtyFields[id])
|
dirtyFields && (dirtyFields.__all__ || dirtyFields[id])
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
const isEditing = this.state.editingField === id;
|
||||||
<TextField
|
const isDisabled = saving || (id === 'sku' && Boolean(this.props.isOnShipment));
|
||||||
size="small"
|
const titleText = id === 'sku' && this.props.isOnShipment
|
||||||
fullWidth
|
? 'Artikel ist bereits auf einem Lieferschein und kann nicht geändert werden'
|
||||||
type={type}
|
: undefined;
|
||||||
value={val ?? ''}
|
|
||||||
error={isError}
|
if (isEditing) {
|
||||||
disabled={saving || (id === 'sku' && Boolean(this.props.isOnShipment))}
|
return (
|
||||||
title={id === 'sku' && this.props.isOnShipment ? 'Artikel ist bereits auf einem Lieferschein und kann nicht geändert werden' : undefined}
|
<TextField
|
||||||
autoComplete="off"
|
autoFocus
|
||||||
onChange={(e) => onChange(line.key, id, e.target.value)}
|
size="small"
|
||||||
onBlur={(e) => this.handleBlur(e, id)}
|
type={type}
|
||||||
slotProps={{
|
value={val ?? ''}
|
||||||
htmlInput: {
|
error={isError}
|
||||||
inputMode: type === 'number' ? 'decimal' : 'text',
|
disabled={isDisabled}
|
||||||
style: { textAlign: align },
|
title={titleText}
|
||||||
autoComplete: 'off',
|
autoComplete="off"
|
||||||
step: type === 'number' ? 'any' : undefined,
|
onChange={(e) => onChange(line.key, id, e.target.value)}
|
||||||
},
|
onBlur={(e) => this.handleBlur(e, id)}
|
||||||
}}
|
onKeyDown={this.handleKeyDown}
|
||||||
sx={{
|
slotProps={{
|
||||||
'& .MuiOutlinedInput-root': {
|
htmlInput: {
|
||||||
borderRadius: isFirstInput ? '4px 0 0 4px' : isLast ? '0 4px 4px 0' : 0,
|
inputMode: type === 'number' ? 'decimal' : 'text',
|
||||||
|
style: { textAlign: align },
|
||||||
|
autoComplete: 'off',
|
||||||
|
step: type === 'number' ? 'any' : undefined,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
sx={{
|
||||||
|
position: 'relative',
|
||||||
ml: isFirstInput ? 0 : '-1px',
|
ml: isFirstInput ? 0 : '-1px',
|
||||||
...(isFieldDirty && {
|
width: isFirstInput ? '100%' : 'calc(100% + 1px)',
|
||||||
bgcolor: 'rgba(237, 108, 2, 0.08)',
|
mt: isFirstLine ? 0 : '-1px',
|
||||||
}),
|
height: isFirstLine ? 40 : 41,
|
||||||
'&:hover': {
|
zIndex: 3,
|
||||||
zIndex: 1,
|
'& .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': {
|
'& .MuiOutlinedInput-notchedOutline': {
|
||||||
borderColor: 'warning.main',
|
borderWidth: '1px !important',
|
||||||
borderWidth: 2,
|
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': {
|
'&: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 (
|
||||||
|
<Box
|
||||||
|
tabIndex={isDisabled ? -1 : 0}
|
||||||
|
role={isDisabled ? undefined : 'button'}
|
||||||
|
onClick={() => 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]': {
|
...(isError && !isFieldDirty && {
|
||||||
MozAppearance: 'textfield',
|
bgcolor: 'rgba(211, 47, 47, 0.04)',
|
||||||
},
|
borderColor: 'error.main',
|
||||||
'& input[type=number]::-webkit-outer-spin-button, & input[type=number]::-webkit-inner-spin-button':
|
zIndex: 2,
|
||||||
{
|
}),
|
||||||
WebkitAppearance: 'none',
|
'&:hover': isDisabled
|
||||||
margin: 0,
|
? 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,
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
/>
|
>
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
sx={{
|
||||||
|
textAlign: align,
|
||||||
|
width: '100%',
|
||||||
|
overflow: 'hidden',
|
||||||
|
textOverflow: 'ellipsis',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
fontSize: '0.875rem',
|
||||||
|
lineHeight: '20px',
|
||||||
|
fontWeight: id === 'sku' ? 600 : 400,
|
||||||
|
color: isError
|
||||||
|
? 'error.main'
|
||||||
|
: hasValue
|
||||||
|
? 'text.primary'
|
||||||
|
: 'text.disabled',
|
||||||
|
fontStyle: 'normal',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{hasValue ? String(val) : '—'}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -979,7 +1136,11 @@ class ManifestFormLine extends PureComponent {
|
|||||||
sx={{
|
sx={{
|
||||||
p: 0,
|
p: 0,
|
||||||
width: col.width,
|
width: col.width,
|
||||||
verticalAlign: 'middle',
|
minWidth: col.width,
|
||||||
|
height: 40,
|
||||||
|
verticalAlign: 'top',
|
||||||
|
position: 'relative',
|
||||||
|
overflow: 'visible',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{this.renderField(col, colIdx, columns.length)}
|
{this.renderField(col, colIdx, columns.length)}
|
||||||
|
|||||||
@@ -243,6 +243,7 @@ class ManifestHeader extends PureComponent {
|
|||||||
onPdfDelete,
|
onPdfDelete,
|
||||||
onPdfClick,
|
onPdfClick,
|
||||||
onAttachmentClick,
|
onAttachmentClick,
|
||||||
|
onMentionInChat,
|
||||||
onDateChange,
|
onDateChange,
|
||||||
onNumberChange,
|
onNumberChange,
|
||||||
onExternalNumberChange,
|
onExternalNumberChange,
|
||||||
@@ -456,6 +457,7 @@ class ManifestHeader extends PureComponent {
|
|||||||
onRescan={onPdfRescan}
|
onRescan={onPdfRescan}
|
||||||
onDelete={onPdfDelete}
|
onDelete={onPdfDelete}
|
||||||
onClick={onAttachmentClick ?? onPdfClick}
|
onClick={onAttachmentClick ?? onPdfClick}
|
||||||
|
onMentionInChat={onMentionInChat}
|
||||||
disabled={saving || pdfUploading}
|
disabled={saving || pdfUploading}
|
||||||
hasArticles={hasArticles}
|
hasArticles={hasArticles}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -36,7 +36,6 @@ import {
|
|||||||
ArticleSearchDialog,
|
ArticleSearchDialog,
|
||||||
BookDeliveryDialog,
|
BookDeliveryDialog,
|
||||||
} from '../../components/manifest';
|
} from '../../components/manifest';
|
||||||
import ManifestPdfDialog from '../../components/manifest/ManifestPdfDialog';
|
|
||||||
import { loadParties, apiFor } from '../../components/manifest/manifestConfig';
|
import { loadParties, apiFor } from '../../components/manifest/manifestConfig';
|
||||||
import { article as articleApi, delivery as deliveryApi } from '../../api';
|
import { article as articleApi, delivery as deliveryApi } from '../../api';
|
||||||
import { getCategoryState } from '../../automats/categoryAutomat';
|
import { getCategoryState } from '../../automats/categoryAutomat';
|
||||||
@@ -68,9 +67,6 @@ class LieferungNeuPage extends PureComponent {
|
|||||||
partiesLoading: true,
|
partiesLoading: true,
|
||||||
saving: false,
|
saving: false,
|
||||||
pdfUploading: false,
|
pdfUploading: false,
|
||||||
pdfDialogOpen: false,
|
|
||||||
pdfDialogData: null,
|
|
||||||
pdfFileName: '',
|
|
||||||
textEditorOpen: false,
|
textEditorOpen: false,
|
||||||
textEditorAttachment: null,
|
textEditorAttachment: null,
|
||||||
viewerPosition: userSettingsAutomat.state.viewerPosition,
|
viewerPosition: userSettingsAutomat.state.viewerPosition,
|
||||||
@@ -105,8 +101,6 @@ class LieferungNeuPage extends PureComponent {
|
|||||||
this.handlePdfUpload = this.handlePdfUpload.bind(this);
|
this.handlePdfUpload = this.handlePdfUpload.bind(this);
|
||||||
this.handlePdfRescan = this.handlePdfRescan.bind(this);
|
this.handlePdfRescan = this.handlePdfRescan.bind(this);
|
||||||
this.handlePdfDelete = this.handlePdfDelete.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.handleAttachmentClick = this.handleAttachmentClick.bind(this);
|
||||||
this.handleCloseTextEditor = this.handleCloseTextEditor.bind(this);
|
this.handleCloseTextEditor = this.handleCloseTextEditor.bind(this);
|
||||||
this.handleTextEditorPositionChange = this.handleTextEditorPositionChange.bind(this);
|
this.handleTextEditorPositionChange = this.handleTextEditorPositionChange.bind(this);
|
||||||
@@ -159,10 +153,6 @@ class LieferungNeuPage extends PureComponent {
|
|||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
this._cancelled = true;
|
this._cancelled = true;
|
||||||
if (this._parseAbortController) {
|
|
||||||
this._parseAbortController.abort();
|
|
||||||
this._parseAbortController = null;
|
|
||||||
}
|
|
||||||
if (this._skuDebounceTimers) {
|
if (this._skuDebounceTimers) {
|
||||||
Object.values(this._skuDebounceTimers).forEach(clearTimeout);
|
Object.values(this._skuDebounceTimers).forEach(clearTimeout);
|
||||||
this._skuDebounceTimers = null;
|
this._skuDebounceTimers = null;
|
||||||
@@ -301,7 +291,6 @@ class LieferungNeuPage extends PureComponent {
|
|||||||
|
|
||||||
handleCreateSupplierFromPdf(supplierName) {
|
handleCreateSupplierFromPdf(supplierName) {
|
||||||
const name = (supplierName ?? this.state.unmatchedSupplierName ?? '').trim();
|
const name = (supplierName ?? this.state.unmatchedSupplierName ?? '').trim();
|
||||||
this.handleClosePdfDialog();
|
|
||||||
const target = `/lieferung/lieferanten?name=${encodeURIComponent(name)}&returnTo=/lieferung/neu`;
|
const target = `/lieferung/lieferanten?name=${encodeURIComponent(name)}&returnTo=/lieferung/neu`;
|
||||||
this.props.navigate(target, { state: { 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);
|
this.automat.actions.addAttachment(textAttachment);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Run SSE parsing with backend (POST /api/delivery/parse)
|
|
||||||
await this.runParse(file);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async handlePdfRescan(attachment) {
|
async handlePdfRescan(attachment) {
|
||||||
@@ -697,8 +683,6 @@ class LieferungNeuPage extends PureComponent {
|
|||||||
console.error('Fehler beim Aktualisieren der Textdatei:', txtErr);
|
console.error('Fehler beim Aktualisieren der Textdatei:', txtErr);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.runParse(attachment.file);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
handlePdfDelete(attachment) {
|
handlePdfDelete(attachment) {
|
||||||
@@ -706,227 +690,6 @@ class LieferungNeuPage extends PureComponent {
|
|||||||
this.automat.actions.removeAttachment(attachment.id);
|
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) {
|
async ensureAttachmentFile(attachment) {
|
||||||
if (!attachment) return null;
|
if (!attachment) return null;
|
||||||
const hasContent =
|
const hasContent =
|
||||||
@@ -1127,7 +890,7 @@ class LieferungNeuPage extends PureComponent {
|
|||||||
isViewerOpen() {
|
isViewerOpen() {
|
||||||
return Boolean(
|
return Boolean(
|
||||||
(this.state.textEditorOpen && this.state.textEditorAttachment) ||
|
(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,
|
partiesLoading,
|
||||||
saving,
|
saving,
|
||||||
pdfUploading,
|
pdfUploading,
|
||||||
pdfDialogOpen,
|
|
||||||
pdfDialogData,
|
|
||||||
pdfFileName,
|
|
||||||
deliveryEntities,
|
deliveryEntities,
|
||||||
date,
|
date,
|
||||||
partyId,
|
partyId,
|
||||||
@@ -1376,15 +1136,7 @@ class LieferungNeuPage extends PureComponent {
|
|||||||
</PanelTile>
|
</PanelTile>
|
||||||
)}
|
)}
|
||||||
</PanelTileGrid>
|
</PanelTileGrid>
|
||||||
<ManifestPdfDialog
|
|
||||||
open={pdfDialogOpen}
|
|
||||||
filename={pdfFileName}
|
|
||||||
data={pdfDialogData}
|
|
||||||
unmatchedSupplierName={this.state.unmatchedSupplierName}
|
|
||||||
onCreateSupplier={this.handleCreateSupplierFromPdf}
|
|
||||||
onClose={this.handleClosePdfDialog}
|
|
||||||
onAbort={this.handleAbortParse}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<ArticleSearchDialog
|
<ArticleSearchDialog
|
||||||
open={this.state.articleSearchDialogOpen}
|
open={this.state.articleSearchDialogOpen}
|
||||||
@@ -1456,8 +1208,8 @@ class LieferungNeuPage extends PureComponent {
|
|||||||
status === 'booked'
|
status === 'booked'
|
||||||
? 'success.light'
|
? 'success.light'
|
||||||
: status === 'partial'
|
: status === 'partial'
|
||||||
? 'warning.light'
|
? 'warning.light'
|
||||||
: 'divider',
|
: 'divider',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Stack spacing={2}>
|
<Stack spacing={2}>
|
||||||
@@ -1554,8 +1306,8 @@ class LieferungNeuPage extends PureComponent {
|
|||||||
{status === 'booked'
|
{status === 'booked'
|
||||||
? 'Buchungsdetails anzeigen'
|
? 'Buchungsdetails anzeigen'
|
||||||
: status === 'partial'
|
: status === 'partial'
|
||||||
? 'Restliche Mengen einbuchen'
|
? 'Restliche Mengen einbuchen'
|
||||||
: 'Ins Lager buchen'}
|
: 'Ins Lager buchen'}
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { defineConfig, searchForWorkspaceRoot, createLogger } from 'vite';
|
|||||||
import react from '@vitejs/plugin-react';
|
import react from '@vitejs/plugin-react';
|
||||||
import { VitePWA } from 'vite-plugin-pwa';
|
import { VitePWA } from 'vite-plugin-pwa';
|
||||||
import pc from 'picocolors';
|
import pc from 'picocolors';
|
||||||
|
import qrcode from 'qrcode';
|
||||||
|
|
||||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||||
const useHttps = process.env.VITE_HTTPS === '1';
|
const useHttps = process.env.VITE_HTTPS === '1';
|
||||||
@@ -94,6 +95,7 @@ const sharedResolve = {
|
|||||||
|
|
||||||
const sharedServer = {
|
const sharedServer = {
|
||||||
// HTTP :4209 — HTTPS :4308 (self-signed; see npm run cert / dev:https)
|
// HTTP :4209 — HTTPS :4308 (self-signed; see npm run cert / dev:https)
|
||||||
|
host: true,
|
||||||
port: useHttps ? 4308 : 4209,
|
port: useHttps ? 4308 : 4209,
|
||||||
https: httpsOptions(),
|
https: httpsOptions(),
|
||||||
allowedHosts: true,
|
allowedHosts: true,
|
||||||
@@ -266,6 +268,20 @@ function pidReporterPlugin() {
|
|||||||
server.config.logger.info(
|
server.config.logger.info(
|
||||||
` ${pc.green('➜')} ${pc.bold('PID')}: ${pc.cyan(process.pid)}${pmId}`,
|
` ${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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user