feat(delivery): add supplier article lookup, article creation, and unified parsing
- Unified PDF/TXT parsing to single POST /api/delivery/parse endpoint - Added supplier article lookup with SKU/EAN matching in delivery lines - Added "Artikel anlegen" button to create articles from missing SKUs - Fixed delivery line taxrate field (was sending 'tax', now 'taxrate') - Extended TXT file support for rescan functionality - Updated ApiClient with getArticles method for supplier article lookup - Added supplier article state management to deliveryEditorAutomat Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
@@ -80,6 +80,18 @@ export class ApiClient {
|
|||||||
update: (id, body) => this.#json('PATCH', `/api/supplier/${enc(id)}`, { body }),
|
update: (id, body) => this.#json('PATCH', `/api/supplier/${enc(id)}`, { body }),
|
||||||
/** DELETE /api/supplier/{id} → 204 */
|
/** DELETE /api/supplier/{id} → 204 */
|
||||||
delete: (id) => this.#empty('DELETE', `/api/supplier/${enc(id)}`),
|
delete: (id) => this.#empty('DELETE', `/api/supplier/${enc(id)}`),
|
||||||
|
/** GET /api/supplier/{id}/articles?skus=... → { articles: SupplierArticle[] } */
|
||||||
|
getArticles: (id, skus) => {
|
||||||
|
let query = '';
|
||||||
|
if (skus != null) {
|
||||||
|
const list = Array.isArray(skus) ? skus : [skus];
|
||||||
|
const filtered = list.map((s) => String(s).trim()).filter(Boolean);
|
||||||
|
if (filtered.length > 0) {
|
||||||
|
query = `?skus=${encodeURIComponent(filtered.join(','))}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return this.#json('GET', `/api/supplier/${enc(id)}/articles${query}`);
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
this.delivery = {
|
this.delivery = {
|
||||||
@@ -94,14 +106,15 @@ export class ApiClient {
|
|||||||
/** DELETE /api/delivery/{id} → 204 */
|
/** DELETE /api/delivery/{id} → 204 */
|
||||||
delete: (id) => this.#empty('DELETE', `/api/delivery/${enc(id)}`),
|
delete: (id) => this.#empty('DELETE', `/api/delivery/${enc(id)}`),
|
||||||
/**
|
/**
|
||||||
* POST /api/delivery/parsePDF — multipart/form-data, field "pdf"
|
* POST /api/delivery/parse — multipart/form-data, field "file"
|
||||||
|
* Accepts PDF or text files (max 50 MB), detection via mimetype/filename
|
||||||
* Returns an async generator that yields SSE event objects:
|
* Returns an async generator that yields SSE event objects:
|
||||||
* { event: 'status'|'llm_call'|'tool_call'|'usage'|'progress'|'result'|'error', data: object }
|
* { event: 'status'|'llm_call'|'tool_call'|'usage'|'progress'|'result'|'error', data: object }
|
||||||
*/
|
*/
|
||||||
parsePdfSse: async function* (file) {
|
parseSse: async function* (file) {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('pdf', file, file.name);
|
formData.append('file', file, file.name);
|
||||||
const res = await fetch('/api/delivery/parsePDF', {
|
const res = await fetch('/api/delivery/parse', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
credentials: 'include',
|
credentials: 'include',
|
||||||
body: formData,
|
body: formData,
|
||||||
|
|||||||
@@ -611,6 +611,22 @@ export const supplier = {
|
|||||||
delete(id) {
|
delete(id) {
|
||||||
return http(`/api/supplier/${id}`, { method: 'DELETE' });
|
return http(`/api/supplier/${id}`, { method: 'DELETE' });
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /api/supplier/:id/articles?skus=...
|
||||||
|
* → 200 { articles: SupplierArticle[] }
|
||||||
|
*/
|
||||||
|
getArticles(id, skus) {
|
||||||
|
let query = '';
|
||||||
|
if (skus != null) {
|
||||||
|
const list = Array.isArray(skus) ? skus : [skus];
|
||||||
|
const filtered = list.map((s) => String(s).trim()).filter(Boolean);
|
||||||
|
if (filtered.length > 0) {
|
||||||
|
query = `?skus=${encodeURIComponent(filtered.join(','))}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return http(`/api/supplier/${encodeURIComponent(id)}/articles${query}`);
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -662,18 +678,6 @@ export const delivery = {
|
|||||||
return http(`/api/delivery/${id}`, { method: 'DELETE' });
|
return http(`/api/delivery/${id}`, { method: 'DELETE' });
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
|
||||||
* POST /api/delivery/parse-pdf
|
|
||||||
* Body: raw PDF bytes (Content-Type: application/pdf)
|
|
||||||
*/
|
|
||||||
parsePdf(file) {
|
|
||||||
return httpBinary('/api/delivery/parse-pdf', {
|
|
||||||
method: 'POST',
|
|
||||||
contentType: 'application/pdf',
|
|
||||||
body: file,
|
|
||||||
headers: file?.name ? { 'X-Filename': encodeURIComponent(file.name) } : undefined,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Automat } from 'automat';
|
import { Automat } from 'automat';
|
||||||
|
import { supplier as supplierApi } from '../api';
|
||||||
|
|
||||||
export function createEmptyLineitem(line = {}) {
|
export function createEmptyLineitem(line = {}) {
|
||||||
return {
|
return {
|
||||||
@@ -13,6 +14,41 @@ export function createEmptyLineitem(line = {}) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function buildSupplierArticlesMap(articles = [], existingMap = {}) {
|
||||||
|
const map = { ...existingMap };
|
||||||
|
for (const item of articles) {
|
||||||
|
if (!item) continue;
|
||||||
|
// Map by supplier SKU
|
||||||
|
if (item.sku != null) {
|
||||||
|
const skuStr = String(item.sku).trim();
|
||||||
|
if (skuStr) {
|
||||||
|
map[skuStr] = item;
|
||||||
|
map[skuStr.toLowerCase()] = item;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Map by internal article SKU
|
||||||
|
if (item.article?.sku != null) {
|
||||||
|
const artSku = String(item.article.sku).trim();
|
||||||
|
if (artSku) {
|
||||||
|
map[artSku] = item;
|
||||||
|
map[artSku.toLowerCase()] = item;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Map by EANs
|
||||||
|
const eans = item.article?.eans ?? item.article?.articleean ?? [];
|
||||||
|
for (const ean of eans) {
|
||||||
|
if (ean != null) {
|
||||||
|
const eanStr = String(ean).trim();
|
||||||
|
if (eanStr) {
|
||||||
|
map[eanStr] = item;
|
||||||
|
map[eanStr.toLowerCase()] = item;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
export const initialDelivery = {
|
export const initialDelivery = {
|
||||||
id: null,
|
id: null,
|
||||||
deliveryId: null,
|
deliveryId: null,
|
||||||
@@ -28,6 +64,12 @@ export const initialDelivery = {
|
|||||||
notes: [],
|
notes: [],
|
||||||
attachments: [],
|
attachments: [],
|
||||||
files: [],
|
files: [],
|
||||||
|
supplierArticles: [],
|
||||||
|
supplierArticlesMap: {},
|
||||||
|
supplierArticlesByLineKey: {},
|
||||||
|
supplierArticlesLoading: false,
|
||||||
|
loadingLineKeys: {},
|
||||||
|
supplierArticlesError: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const initialState = {
|
export const initialState = {
|
||||||
@@ -71,6 +113,10 @@ export const deliveryEditorAutomat = new Automat(initialState, {
|
|||||||
attachments,
|
attachments,
|
||||||
files: attachments,
|
files: attachments,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (partyId && lines.length > 0) {
|
||||||
|
deliveryEditorAutomat.actions.fetchSupplierArticles(partyId, lines);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
setDate(date) {
|
setDate(date) {
|
||||||
@@ -78,11 +124,28 @@ export const deliveryEditorAutomat = new Automat(initialState, {
|
|||||||
},
|
},
|
||||||
|
|
||||||
setDeliveryEntity(entityId) {
|
setDeliveryEntity(entityId) {
|
||||||
deliveryEditorAutomat.setState({ deliveryEntity: entityId, partyId: entityId });
|
const prevId = deliveryEditorAutomat.state.deliveryEntity ?? deliveryEditorAutomat.state.partyId;
|
||||||
|
const idChanged = prevId !== entityId;
|
||||||
|
deliveryEditorAutomat.setState({
|
||||||
|
deliveryEntity: entityId,
|
||||||
|
partyId: entityId,
|
||||||
|
...(idChanged
|
||||||
|
? {
|
||||||
|
supplierArticles: [],
|
||||||
|
supplierArticlesMap: {},
|
||||||
|
supplierArticlesByLineKey: {},
|
||||||
|
loadingLineKeys: {},
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (entityId && idChanged) {
|
||||||
|
deliveryEditorAutomat.actions.fetchSupplierArticles(entityId);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
setPartyId(partyId) {
|
setPartyId(partyId) {
|
||||||
deliveryEditorAutomat.setState({ deliveryEntity: partyId, partyId });
|
deliveryEditorAutomat.actions.setDeliveryEntity(partyId);
|
||||||
},
|
},
|
||||||
|
|
||||||
setExternalNumber(externalNumber) {
|
setExternalNumber(externalNumber) {
|
||||||
@@ -151,15 +214,24 @@ export const deliveryEditorAutomat = new Automat(initialState, {
|
|||||||
removeLineitem(payload) {
|
removeLineitem(payload) {
|
||||||
const currentLines = deliveryEditorAutomat.state.lines ?? [];
|
const currentLines = deliveryEditorAutomat.state.lines ?? [];
|
||||||
let index = typeof payload === 'number' && !Number.isNaN(payload) ? payload : -1;
|
let index = typeof payload === 'number' && !Number.isNaN(payload) ? payload : -1;
|
||||||
|
let targetKey = null;
|
||||||
if (index === -1 && payload != null) {
|
if (index === -1 && payload != null) {
|
||||||
const key = typeof payload === 'string' ? payload : payload?.key;
|
const key = typeof payload === 'string' ? payload : payload?.key;
|
||||||
if (key != null) {
|
if (key != null) {
|
||||||
index = currentLines.findIndex((l) => l.key === key);
|
index = currentLines.findIndex((l) => l.key === key);
|
||||||
|
targetKey = key;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (index >= 0 && index < currentLines.length) {
|
if (index >= 0 && index < currentLines.length) {
|
||||||
|
targetKey = targetKey ?? currentLines[index]?.key;
|
||||||
const nextLines = currentLines.filter((_, i) => i !== index);
|
const nextLines = currentLines.filter((_, i) => i !== index);
|
||||||
deliveryEditorAutomat.setState({ lines: nextLines, lineitems: nextLines });
|
const byLineKey = { ...(deliveryEditorAutomat.state.supplierArticlesByLineKey || {}) };
|
||||||
|
if (targetKey != null) delete byLineKey[targetKey];
|
||||||
|
deliveryEditorAutomat.setState({
|
||||||
|
lines: nextLines,
|
||||||
|
lineitems: nextLines,
|
||||||
|
supplierArticlesByLineKey: byLineKey,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -167,6 +239,121 @@ export const deliveryEditorAutomat = new Automat(initialState, {
|
|||||||
deliveryEditorAutomat.actions.removeLineitem(payload);
|
deliveryEditorAutomat.actions.removeLineitem(payload);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async fetchSupplierArticles(targetEntityId, targetLines) {
|
||||||
|
const entityId = targetEntityId ?? deliveryEditorAutomat.state.deliveryEntity ?? deliveryEditorAutomat.state.partyId;
|
||||||
|
if (!entityId) {
|
||||||
|
deliveryEditorAutomat.setState({
|
||||||
|
supplierArticles: [],
|
||||||
|
supplierArticlesMap: {},
|
||||||
|
supplierArticlesByLineKey: {},
|
||||||
|
supplierArticlesLoading: false,
|
||||||
|
loadingLineKeys: {},
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const lines = targetLines ?? deliveryEditorAutomat.state.lines ?? [];
|
||||||
|
const skus = Array.from(
|
||||||
|
new Set(
|
||||||
|
lines
|
||||||
|
.map((l) => (l.sku != null ? String(l.sku).trim() : ''))
|
||||||
|
.filter(Boolean),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
deliveryEditorAutomat.setState({
|
||||||
|
supplierArticlesLoading: true,
|
||||||
|
supplierArticlesError: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await supplierApi.getArticles(entityId, skus);
|
||||||
|
const articles = Array.isArray(res?.articles) ? res.articles : [];
|
||||||
|
|
||||||
|
const map = buildSupplierArticlesMap(articles);
|
||||||
|
const byLineKey = {};
|
||||||
|
for (const line of lines) {
|
||||||
|
if (line.key != null) {
|
||||||
|
const rawSku = line.sku != null ? String(line.sku).trim() : '';
|
||||||
|
byLineKey[line.key] = rawSku ? (map[rawSku.toLowerCase()] || map[rawSku] || null) : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
deliveryEditorAutomat.setState({
|
||||||
|
supplierArticles: articles,
|
||||||
|
supplierArticlesMap: map,
|
||||||
|
supplierArticlesByLineKey: byLineKey,
|
||||||
|
supplierArticlesLoading: false,
|
||||||
|
loadingLineKeys: {},
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Fehler beim Abrufen der Lieferanten-Artikel:', err);
|
||||||
|
deliveryEditorAutomat.setState({
|
||||||
|
supplierArticlesLoading: false,
|
||||||
|
supplierArticlesError: err?.message || 'Fehler beim Laden der Artikel',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async fetchSupplierArticleForSku(sku, lineKey) {
|
||||||
|
const entityId = deliveryEditorAutomat.state.deliveryEntity ?? deliveryEditorAutomat.state.partyId;
|
||||||
|
const trimmed = sku != null ? String(sku).trim() : '';
|
||||||
|
|
||||||
|
if (!entityId || !trimmed) {
|
||||||
|
if (lineKey != null) {
|
||||||
|
const byLineKey = { ...(deliveryEditorAutomat.state.supplierArticlesByLineKey || {}) };
|
||||||
|
byLineKey[lineKey] = null;
|
||||||
|
deliveryEditorAutomat.setState({ supplierArticlesByLineKey: byLineKey });
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const prevLoading = deliveryEditorAutomat.state.loadingLineKeys || {};
|
||||||
|
deliveryEditorAutomat.setState({
|
||||||
|
loadingLineKeys: lineKey != null ? { ...prevLoading, [lineKey]: true } : prevLoading,
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await supplierApi.getArticles(entityId, [trimmed]);
|
||||||
|
const articles = Array.isArray(res?.articles) ? res.articles : [];
|
||||||
|
|
||||||
|
const nextMap = buildSupplierArticlesMap(articles, deliveryEditorAutomat.state.supplierArticlesMap);
|
||||||
|
const matched = nextMap[trimmed.toLowerCase()] || nextMap[trimmed] || articles[0] || null;
|
||||||
|
|
||||||
|
const byLineKey = { ...(deliveryEditorAutomat.state.supplierArticlesByLineKey || {}) };
|
||||||
|
if (lineKey != null) {
|
||||||
|
byLineKey[lineKey] = matched;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentList = deliveryEditorAutomat.state.supplierArticles || [];
|
||||||
|
const mergedList = [...currentList];
|
||||||
|
for (const a of articles) {
|
||||||
|
if (!mergedList.some((existing) => existing.id === a.id)) {
|
||||||
|
mergedList.push(a);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextLoading = { ...(deliveryEditorAutomat.state.loadingLineKeys || {}) };
|
||||||
|
if (lineKey != null) delete nextLoading[lineKey];
|
||||||
|
|
||||||
|
deliveryEditorAutomat.setState({
|
||||||
|
supplierArticles: mergedList,
|
||||||
|
supplierArticlesMap: nextMap,
|
||||||
|
supplierArticlesByLineKey: byLineKey,
|
||||||
|
loadingLineKeys: nextLoading,
|
||||||
|
supplierArticlesLoading: Object.keys(nextLoading).length > 0,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Fehler beim Abrufen des Artikels für SKU:', err);
|
||||||
|
const nextLoading = { ...(deliveryEditorAutomat.state.loadingLineKeys || {}) };
|
||||||
|
if (lineKey != null) delete nextLoading[lineKey];
|
||||||
|
deliveryEditorAutomat.setState({
|
||||||
|
loadingLineKeys: nextLoading,
|
||||||
|
supplierArticlesLoading: Object.keys(nextLoading).length > 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
addAttachment(attachment) {
|
addAttachment(attachment) {
|
||||||
const id = attachment.id ?? `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
const id = attachment.id ?? `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
||||||
const item = { ...attachment, id };
|
const item = { ...attachment, id };
|
||||||
@@ -234,6 +421,8 @@ export const {
|
|||||||
updateLine,
|
updateLine,
|
||||||
removeLineitem,
|
removeLineitem,
|
||||||
removeLine,
|
removeLine,
|
||||||
|
fetchSupplierArticles,
|
||||||
|
fetchSupplierArticleForSku,
|
||||||
addAttachment,
|
addAttachment,
|
||||||
setAttachments,
|
setAttachments,
|
||||||
updateAttachment,
|
updateAttachment,
|
||||||
@@ -253,5 +442,10 @@ export const selectDeliveryType = (state = deliveryEditorAutomat.state) => state
|
|||||||
export const selectDeliveryLineitems = (state = deliveryEditorAutomat.state) => state.lines ?? state.lineitems;
|
export const selectDeliveryLineitems = (state = deliveryEditorAutomat.state) => state.lines ?? state.lineitems;
|
||||||
export const selectDeliveryNotes = (state = deliveryEditorAutomat.state) => state.notes;
|
export const selectDeliveryNotes = (state = deliveryEditorAutomat.state) => state.notes;
|
||||||
export const selectDeliveryAttachments = (state = deliveryEditorAutomat.state) => state.attachments;
|
export const selectDeliveryAttachments = (state = deliveryEditorAutomat.state) => state.attachments;
|
||||||
|
export const selectSupplierArticles = (state = deliveryEditorAutomat.state) => state.supplierArticles;
|
||||||
|
export const selectSupplierArticlesMap = (state = deliveryEditorAutomat.state) => state.supplierArticlesMap;
|
||||||
|
export const selectSupplierArticlesByLineKey = (state = deliveryEditorAutomat.state) => state.supplierArticlesByLineKey;
|
||||||
|
export const selectSupplierArticlesLoading = (state = deliveryEditorAutomat.state) => state.supplierArticlesLoading;
|
||||||
|
export const selectLoadingLineKeys = (state = deliveryEditorAutomat.state) => state.loadingLineKeys;
|
||||||
|
|
||||||
export default deliveryEditorAutomat;
|
export default deliveryEditorAutomat;
|
||||||
|
|||||||
@@ -163,7 +163,7 @@ export class Attachment extends PureComponent {
|
|||||||
}}
|
}}
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
>
|
>
|
||||||
{isPdf ? (
|
{isPdf || isText ? (
|
||||||
<Tooltip title="Neu analysieren">
|
<Tooltip title="Neu analysieren">
|
||||||
<span>
|
<span>
|
||||||
<IconButton
|
<IconButton
|
||||||
|
|||||||
@@ -1,21 +1,47 @@
|
|||||||
import { PureComponent } from 'react';
|
import { Fragment, PureComponent } from 'react';
|
||||||
|
import Box from '@mui/material/Box';
|
||||||
|
import Button from '@mui/material/Button';
|
||||||
|
import Chip from '@mui/material/Chip';
|
||||||
|
import CircularProgress from '@mui/material/CircularProgress';
|
||||||
import IconButton from '@mui/material/IconButton';
|
import IconButton from '@mui/material/IconButton';
|
||||||
import TableCell from '@mui/material/TableCell';
|
import TableCell from '@mui/material/TableCell';
|
||||||
import TableRow from '@mui/material/TableRow';
|
import TableRow from '@mui/material/TableRow';
|
||||||
import TextField from '@mui/material/TextField';
|
import TextField from '@mui/material/TextField';
|
||||||
|
import Typography from '@mui/material/Typography';
|
||||||
import DeleteOutlinedIcon from '@mui/icons-material/DeleteOutlined';
|
import DeleteOutlinedIcon from '@mui/icons-material/DeleteOutlined';
|
||||||
|
import HelpOutlineIcon from '@mui/icons-material/HelpOutlineOutlined';
|
||||||
|
import Inventory2OutlinedIcon from '@mui/icons-material/Inventory2Outlined';
|
||||||
|
import CheckCircleOutlinedIcon from '@mui/icons-material/CheckCircleOutlined';
|
||||||
|
import AddIcon from '@mui/icons-material/Add';
|
||||||
import { LINE_COLUMNS } from './manifestConfig';
|
import { LINE_COLUMNS } from './manifestConfig';
|
||||||
|
import ArticlePhoto from '../article/ArticlePhoto';
|
||||||
|
import { formatPrice } from '../../utils/price';
|
||||||
|
|
||||||
class ManifestFormLine extends PureComponent {
|
class ManifestFormLine extends PureComponent {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.handleRemove = this.handleRemove.bind(this);
|
this.handleRemove = this.handleRemove.bind(this);
|
||||||
|
this.handleBlur = this.handleBlur.bind(this);
|
||||||
|
this.handleCreateArticle = this.handleCreateArticle.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleRemove() {
|
handleRemove() {
|
||||||
this.props.onRemove(this.props.line.key);
|
this.props.onRemove(this.props.line.key);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleBlur(e, columnId) {
|
||||||
|
if (columnId === 'sku' && this.props.onSkuBlur) {
|
||||||
|
this.props.onSkuBlur(this.props.line.key, e.target.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleCreateArticle() {
|
||||||
|
const { line, onCreateArticle } = this.props;
|
||||||
|
if (onCreateArticle) {
|
||||||
|
onCreateArticle(line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
renderField(column) {
|
renderField(column) {
|
||||||
const { id, align, type } = column;
|
const { id, align, type } = column;
|
||||||
const { line, saving, onChange } = this.props;
|
const { line, saving, onChange } = this.props;
|
||||||
@@ -28,6 +54,7 @@ class ManifestFormLine extends PureComponent {
|
|||||||
disabled={saving}
|
disabled={saving}
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
onChange={(e) => onChange(line.key, id, e.target.value)}
|
onChange={(e) => onChange(line.key, id, e.target.value)}
|
||||||
|
onBlur={(e) => this.handleBlur(e, id)}
|
||||||
slotProps={{
|
slotProps={{
|
||||||
htmlInput: {
|
htmlInput: {
|
||||||
inputMode: type === 'number' ? 'decimal' : 'text',
|
inputMode: type === 'number' ? 'decimal' : 'text',
|
||||||
@@ -51,31 +78,239 @@ class ManifestFormLine extends PureComponent {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
renderSecondLine() {
|
||||||
|
const { line, supplierArticle, loading } = this.props;
|
||||||
|
const rawSku = line?.sku != null ? String(line.sku).trim() : '';
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, py: 0.25 }}>
|
||||||
|
<CircularProgress size={14} thickness={5} sx={{ color: 'primary.main' }} />
|
||||||
|
<Typography variant="caption" color="text.secondary" sx={{ fontStyle: 'italic', fontSize: '0.72rem' }}>
|
||||||
|
Artikeldaten werden geladen…
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (supplierArticle) {
|
||||||
|
const article = supplierArticle.article || {};
|
||||||
|
const photo = article.photos?.[0] || article.articlepic?.[0];
|
||||||
|
const fullHash = photo?.hash || photo?.fullHash;
|
||||||
|
const thumbHash = photo?.thumbhash || photo?.thumbHash;
|
||||||
|
const thumbW = photo?.thumbw || photo?.thumbW;
|
||||||
|
const thumbH = photo?.thumbh || photo?.thumbH;
|
||||||
|
const hasPhoto = fullHash != null || thumbHash != null;
|
||||||
|
|
||||||
|
const eans = article.eans ?? article.articleean ?? [];
|
||||||
|
const primaryEan = Array.isArray(eans) ? eans[0] : eans;
|
||||||
|
const priceNet = supplierArticle.price?.net;
|
||||||
|
const taxRate = supplierArticle.price?.taxRate;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 1.25,
|
||||||
|
py: 0.25,
|
||||||
|
flexWrap: 'wrap',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Article Photo Thumbnail */}
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
width: 28,
|
||||||
|
height: 28,
|
||||||
|
minWidth: 28,
|
||||||
|
borderRadius: 0.75,
|
||||||
|
overflow: 'hidden',
|
||||||
|
bgcolor: '#e2e8f0',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
border: '1px solid',
|
||||||
|
borderColor: 'divider',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{hasPhoto ? (
|
||||||
|
<ArticlePhoto
|
||||||
|
photo={{ fullHash, thumbHash, thumbW, thumbH }}
|
||||||
|
size={28}
|
||||||
|
fill
|
||||||
|
objectFit="cover"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Inventory2OutlinedIcon sx={{ fontSize: 16, color: 'text.secondary' }} />
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Match Indicator */}
|
||||||
|
<CheckCircleOutlinedIcon sx={{ fontSize: 16, color: 'success.main', flexShrink: 0 }} />
|
||||||
|
|
||||||
|
{/* Internal Article SKU */}
|
||||||
|
{article.sku && (
|
||||||
|
<Chip
|
||||||
|
size="small"
|
||||||
|
label={`Art.-Nr.: ${article.sku}`}
|
||||||
|
sx={{
|
||||||
|
height: 20,
|
||||||
|
fontSize: '0.7rem',
|
||||||
|
fontWeight: 600,
|
||||||
|
bgcolor: 'primary.50',
|
||||||
|
color: 'primary.dark',
|
||||||
|
border: '1px solid',
|
||||||
|
borderColor: 'primary.200',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Article Name */}
|
||||||
|
{article.name && (
|
||||||
|
<Typography
|
||||||
|
variant="caption"
|
||||||
|
fontWeight={600}
|
||||||
|
noWrap
|
||||||
|
sx={{ color: 'text.primary', maxWidth: { xs: 200, sm: 300, md: 400 } }}
|
||||||
|
title={article.name}
|
||||||
|
>
|
||||||
|
{article.name}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Supplier SKU if distinct */}
|
||||||
|
{supplierArticle.sku && supplierArticle.sku !== rawSku && supplierArticle.sku !== article.sku && (
|
||||||
|
<Typography variant="caption" color="text.secondary" sx={{ fontSize: '0.7rem' }}>
|
||||||
|
(Lief.-SKU: <strong>{supplierArticle.sku}</strong>)
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Primary EAN */}
|
||||||
|
{primaryEan && (
|
||||||
|
<Typography
|
||||||
|
variant="caption"
|
||||||
|
color="text.secondary"
|
||||||
|
sx={{ fontFamily: 'monospace', fontSize: '0.68rem', bgcolor: 'action.hover', px: 0.5, py: 0.1, borderRadius: 0.5 }}
|
||||||
|
>
|
||||||
|
EAN: {primaryEan}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Stock */}
|
||||||
|
<Chip
|
||||||
|
size="small"
|
||||||
|
variant="outlined"
|
||||||
|
label={`Bestand: ${supplierArticle.stock ?? 0}`}
|
||||||
|
sx={{ height: 19, fontSize: '0.66rem', color: 'text.secondary' }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Packaging Unit (PU) */}
|
||||||
|
{supplierArticle.pu != null && (
|
||||||
|
<Chip
|
||||||
|
size="small"
|
||||||
|
variant="outlined"
|
||||||
|
label={`VPE: ${supplierArticle.pu}`}
|
||||||
|
sx={{ height: 19, fontSize: '0.66rem', color: 'text.secondary' }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Stored Purchase Price (Netto) */}
|
||||||
|
{priceNet != null && (
|
||||||
|
<Typography
|
||||||
|
variant="caption"
|
||||||
|
fontWeight={600}
|
||||||
|
sx={{ color: 'success.dark', ml: 'auto', fontSize: '0.72rem', whiteSpace: 'nowrap' }}
|
||||||
|
>
|
||||||
|
EK Netto: {formatPrice(priceNet)}
|
||||||
|
{taxRate != null ? ` (${taxRate}% MwSt)` : ''}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rawSku) {
|
||||||
|
return (
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.75, py: 0.25 }}>
|
||||||
|
<HelpOutlineIcon sx={{ fontSize: 15, color: 'warning.main' }} />
|
||||||
|
<Typography variant="caption" color="text.secondary" sx={{ fontSize: '0.72rem' }}>
|
||||||
|
Kein Lieferanten-Artikel für SKU „{rawSku}“ gefunden.
|
||||||
|
</Typography>
|
||||||
|
{this.props.onCreateArticle && (
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
variant="text"
|
||||||
|
startIcon={<AddIcon sx={{ fontSize: 14 }} />}
|
||||||
|
onClick={this.handleCreateArticle}
|
||||||
|
sx={{
|
||||||
|
fontSize: '0.7rem',
|
||||||
|
fontWeight: 600,
|
||||||
|
textTransform: 'none',
|
||||||
|
minWidth: 'auto',
|
||||||
|
py: 0.25,
|
||||||
|
px: 1,
|
||||||
|
color: 'primary.main',
|
||||||
|
'&:hover': {
|
||||||
|
bgcolor: 'primary.50',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Artikel anlegen
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Typography variant="caption" color="text.disabled" sx={{ fontStyle: 'italic', fontSize: '0.7rem' }}>
|
||||||
|
SKU eingeben oder Dokument scannen für automatische Artikelzuordnung
|
||||||
|
</Typography>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { columns = LINE_COLUMNS, saving } = this.props;
|
const { columns = LINE_COLUMNS, saving } = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TableRow>
|
<Fragment>
|
||||||
{columns.map((col) => (
|
{/* Row 1: Editable Line Inputs */}
|
||||||
<TableCell
|
<TableRow hover sx={{ '& > td': { borderBottom: 'none', py: 0.75 } }}>
|
||||||
key={col.id}
|
{columns.map((col) => (
|
||||||
align={col.align}
|
<TableCell
|
||||||
sx={{ py: 0.75, verticalAlign: 'middle' }}
|
key={col.id}
|
||||||
>
|
align={col.align}
|
||||||
{this.renderField(col)}
|
sx={{ py: 0.75, verticalAlign: 'middle' }}
|
||||||
|
>
|
||||||
|
{this.renderField(col)}
|
||||||
|
</TableCell>
|
||||||
|
))}
|
||||||
|
<TableCell padding="checkbox" sx={{ py: 0.75 }}>
|
||||||
|
<IconButton
|
||||||
|
size="small"
|
||||||
|
aria-label="Zeile entfernen"
|
||||||
|
disabled={saving}
|
||||||
|
onClick={this.handleRemove}
|
||||||
|
>
|
||||||
|
<DeleteOutlinedIcon fontSize="small" />
|
||||||
|
</IconButton>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
))}
|
</TableRow>
|
||||||
<TableCell padding="checkbox" sx={{ py: 0.75 }}>
|
|
||||||
<IconButton
|
{/* Row 2: Matched Supplier Article Details */}
|
||||||
size="small"
|
<TableRow sx={{ '& > td': { pt: 0, pb: 0.75, borderBottom: '1px solid', borderColor: 'divider' } }}>
|
||||||
aria-label="Zeile entfernen"
|
<TableCell
|
||||||
disabled={saving}
|
colSpan={columns.length + 1}
|
||||||
onClick={this.handleRemove}
|
sx={{
|
||||||
|
py: 0.4,
|
||||||
|
px: 2,
|
||||||
|
bgcolor: 'rgba(248, 250, 252, 0.75)',
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<DeleteOutlinedIcon fontSize="small" />
|
{this.renderSecondLine()}
|
||||||
</IconButton>
|
</TableCell>
|
||||||
</TableCell>
|
</TableRow>
|
||||||
</TableRow>
|
</Fragment>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,12 @@ class ManifestLines extends PureComponent {
|
|||||||
onRemoveLine,
|
onRemoveLine,
|
||||||
onSave,
|
onSave,
|
||||||
columns = LINE_COLUMNS,
|
columns = LINE_COLUMNS,
|
||||||
|
supplierArticlesMap = {},
|
||||||
|
supplierArticlesByLineKey = {},
|
||||||
|
supplierArticlesLoading = false,
|
||||||
|
loadingLineKeys = {},
|
||||||
|
onSkuBlur,
|
||||||
|
onCreateArticle,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -50,16 +56,31 @@ class ManifestLines extends PureComponent {
|
|||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{lines.map((line, index) => (
|
{lines.map((line, index) => {
|
||||||
<ManifestFormLine
|
const lineKey = line.key ?? index;
|
||||||
key={line.key ?? index}
|
const rawSku = line.sku != null ? String(line.sku).trim() : '';
|
||||||
line={line.key != null ? line : { ...line, key: index }}
|
const matched =
|
||||||
columns={columns}
|
supplierArticlesByLineKey[lineKey] ||
|
||||||
saving={saving}
|
(rawSku ? supplierArticlesMap[rawSku.toLowerCase()] || supplierArticlesMap[rawSku] : null);
|
||||||
onChange={onLineChange}
|
const isLineLoading =
|
||||||
onRemove={onRemoveLine}
|
loadingLineKeys[lineKey] ||
|
||||||
/>
|
(supplierArticlesLoading && !matched);
|
||||||
))}
|
|
||||||
|
return (
|
||||||
|
<ManifestFormLine
|
||||||
|
key={lineKey}
|
||||||
|
line={line.key != null ? line : { ...line, key: index }}
|
||||||
|
columns={columns}
|
||||||
|
saving={saving}
|
||||||
|
supplierArticle={matched}
|
||||||
|
loading={Boolean(isLineLoading)}
|
||||||
|
onChange={onLineChange}
|
||||||
|
onRemove={onRemoveLine}
|
||||||
|
onSkuBlur={onSkuBlur}
|
||||||
|
onCreateArticle={onCreateArticle}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
</TableContainer>
|
</TableContainer>
|
||||||
|
|||||||
@@ -114,9 +114,9 @@ export function buildLines(lines, { requireAtLeastOne }) {
|
|||||||
!sku && !name && amount == null && net == null && tax == null && rebate == null;
|
!sku && !name && amount == null && net == null && tax == null && rebate == null;
|
||||||
if (blank) continue;
|
if (blank) continue;
|
||||||
if ((!sku && !name) || amount == null || net == null || tax == null) {
|
if ((!sku && !name) || amount == null || net == null || tax == null) {
|
||||||
throw new Error('Jede Zeile braucht SKU oder Name, Amount, Net und Tax.');
|
throw new Error('Jede Zeile braucht SKU oder Name, Amount, Net und Taxrate.');
|
||||||
}
|
}
|
||||||
const row = { amount, net, tax };
|
const row = { amount, net, taxrate: tax };
|
||||||
if (sku) row.sku = sku;
|
if (sku) row.sku = sku;
|
||||||
if (name) row.name = name;
|
if (name) row.name = name;
|
||||||
if (rebate != null) row.rebate = rebate;
|
if (rebate != null) row.rebate = rebate;
|
||||||
|
|||||||
@@ -65,6 +65,8 @@ class LieferungNeuPage extends PureComponent {
|
|||||||
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.handleSaveTextEditor = this.handleSaveTextEditor.bind(this);
|
this.handleSaveTextEditor = this.handleSaveTextEditor.bind(this);
|
||||||
|
this.handleSkuBlur = this.handleSkuBlur.bind(this);
|
||||||
|
this.handleCreateArticleForLine = this.handleCreateArticleForLine.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
@@ -73,26 +75,32 @@ class LieferungNeuPage extends PureComponent {
|
|||||||
if (this.props.docId != null) {
|
if (this.props.docId != null) {
|
||||||
void this.loadEdit();
|
void this.loadEdit();
|
||||||
} else {
|
} else {
|
||||||
if (this.state.deliveryId != null || this.state.number != null) {
|
void this.loadPartiesOnly();
|
||||||
|
if (this.props.initialDelivery) {
|
||||||
|
deliveryEditorAutomat.actions.setDelivery(this.props.initialDelivery);
|
||||||
|
} else {
|
||||||
deliveryEditorAutomat.actions.resetDelivery();
|
deliveryEditorAutomat.actions.resetDelivery();
|
||||||
}
|
}
|
||||||
void this.loadPartiesOnly();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
componentDidUpdate(prevProps) {
|
||||||
if (this.props.docId !== prevProps.docId) {
|
if (prevProps.docId !== this.props.docId) {
|
||||||
if (this.props.docId != null) {
|
if (this.props.docId != null) {
|
||||||
void this.loadEdit();
|
void this.loadEdit();
|
||||||
} else {
|
} else {
|
||||||
deliveryEditorAutomat.actions.resetDelivery();
|
|
||||||
void this.loadPartiesOnly();
|
void this.loadPartiesOnly();
|
||||||
|
deliveryEditorAutomat.actions.resetDelivery();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
this._cancelled = true;
|
this._cancelled = true;
|
||||||
|
if (this._skuDebounceTimers) {
|
||||||
|
Object.values(this._skuDebounceTimers).forEach(clearTimeout);
|
||||||
|
this._skuDebounceTimers = null;
|
||||||
|
}
|
||||||
this.unsubscribe?.();
|
this.unsubscribe?.();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -207,6 +215,86 @@ class LieferungNeuPage extends PureComponent {
|
|||||||
...(field === 'net' ? { netprice: value } : {}),
|
...(field === 'net' ? { netprice: value } : {}),
|
||||||
...(field === 'tax' ? { taxrate: value } : {}),
|
...(field === 'tax' ? { taxrate: value } : {}),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (field === 'sku') {
|
||||||
|
if (!this._skuDebounceTimers) this._skuDebounceTimers = {};
|
||||||
|
if (this._skuDebounceTimers[key]) {
|
||||||
|
clearTimeout(this._skuDebounceTimers[key]);
|
||||||
|
}
|
||||||
|
this._skuDebounceTimers[key] = setTimeout(() => {
|
||||||
|
if (this._skuDebounceTimers) delete this._skuDebounceTimers[key];
|
||||||
|
deliveryEditorAutomat.actions.fetchSupplierArticleForSku(value, key);
|
||||||
|
}, 350);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleSkuBlur(key, value) {
|
||||||
|
if (this._skuDebounceTimers?.[key]) {
|
||||||
|
clearTimeout(this._skuDebounceTimers[key]);
|
||||||
|
delete this._skuDebounceTimers[key];
|
||||||
|
}
|
||||||
|
deliveryEditorAutomat.actions.fetchSupplierArticleForSku(value, key);
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleCreateArticleForLine(line) {
|
||||||
|
const { partyId } = this.state;
|
||||||
|
if (!partyId) {
|
||||||
|
enqueueSnackbar('Bitte wählen Sie zuerst einen Lieferanten aus.', { variant: 'warning' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const categoryName = line?.name;
|
||||||
|
if (!categoryName) {
|
||||||
|
enqueueSnackbar('Artikelname fehlt.', { variant: 'warning' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Get root category ID
|
||||||
|
const categoryState = this.context.getCategoryState();
|
||||||
|
if (!categoryState) {
|
||||||
|
enqueueSnackbar('Kategorie-Daten nicht verfügbar.', { variant: 'error' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { rootCategoryId } = categoryState;
|
||||||
|
if (!rootCategoryId) {
|
||||||
|
enqueueSnackbar('Root-Kategorie nicht gefunden.', { variant: 'error' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create article in root category
|
||||||
|
const articleRes = await this.context.article.create({
|
||||||
|
parentId: rootCategoryId,
|
||||||
|
name: categoryName,
|
||||||
|
});
|
||||||
|
|
||||||
|
requireObject(articleRes, 'POST /api/article');
|
||||||
|
requireObject(articleRes.article, 'article');
|
||||||
|
requireDefined(articleRes.article.sku, 'article.sku');
|
||||||
|
|
||||||
|
const internalSku = articleRes.article.sku;
|
||||||
|
const supplierSku = line.sku;
|
||||||
|
|
||||||
|
// Add supplier relationship with the supplier's SKU
|
||||||
|
await this.context.article.addSupplier(internalSku, {
|
||||||
|
entityId: partyId,
|
||||||
|
sku: supplierSku || undefined,
|
||||||
|
pu: line.pu ? Number(line.pu) : undefined,
|
||||||
|
stock: line.amount ? Number(line.amount) : undefined,
|
||||||
|
netPrice: line.netprice ? Number(line.netprice) : undefined,
|
||||||
|
taxRate: line.taxrate ? Number(line.taxrate) : undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Keep the supplier SKU in the delivery line, but refresh supplier article data
|
||||||
|
// using the supplier SKU (not the internal SKU)
|
||||||
|
deliveryEditorAutomat.actions.fetchSupplierArticleForSku(supplierSku, line.key);
|
||||||
|
|
||||||
|
enqueueSnackbar(`Artikel "${internalSku}" erstellt und Lieferanten-Artikel "${supplierSku}" zugeordnet.`, { variant: 'success' });
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Fehler beim Erstellen des Artikels:', err);
|
||||||
|
notifyError(err);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleAddLine() {
|
handleAddLine() {
|
||||||
@@ -268,39 +356,42 @@ class LieferungNeuPage extends PureComponent {
|
|||||||
async handlePdfRescan(attachment) {
|
async handlePdfRescan(attachment) {
|
||||||
if (!attachment?.file || this.state.pdfUploading) return;
|
if (!attachment?.file || this.state.pdfUploading) return;
|
||||||
|
|
||||||
// Refresh textfile upon rescan
|
// Refresh textfile upon rescan for PDF
|
||||||
try {
|
const isPdf = attachment.type === 'application/pdf' || attachment.name.toLowerCase().endsWith('.pdf');
|
||||||
const text = await extractPdfText(attachment.file, { layout: true });
|
if (isPdf) {
|
||||||
const txtName = attachment.name.replace(/\.pdf$/i, '') + '.txt';
|
try {
|
||||||
const txtFile = new File([text], txtName, { type: 'text/plain;charset=utf-8' });
|
const text = await extractPdfText(attachment.file, { layout: true });
|
||||||
|
const txtName = attachment.name.replace(/\.pdf$/i, '') + '.txt';
|
||||||
|
const txtFile = new File([text], txtName, { type: 'text/plain;charset=utf-8' });
|
||||||
|
|
||||||
const existingTxt = this.state.attachments?.find(
|
const existingTxt = this.state.attachments?.find(
|
||||||
(a) => a.parentPdfId === attachment.id || a.name === txtName,
|
(a) => a.parentPdfId === attachment.id || a.name === txtName,
|
||||||
);
|
);
|
||||||
if (existingTxt) {
|
if (existingTxt) {
|
||||||
deliveryEditorAutomat.actions.updateAttachment({
|
deliveryEditorAutomat.actions.updateAttachment({
|
||||||
id: existingTxt.id,
|
id: existingTxt.id,
|
||||||
file: txtFile,
|
file: txtFile,
|
||||||
size: txtFile.size,
|
size: txtFile.size,
|
||||||
text,
|
text,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
deliveryEditorAutomat.actions.addAttachment({
|
deliveryEditorAutomat.actions.addAttachment({
|
||||||
id: `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
id: `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
||||||
file: txtFile,
|
file: txtFile,
|
||||||
name: txtFile.name,
|
name: txtFile.name,
|
||||||
size: txtFile.size,
|
size: txtFile.size,
|
||||||
type: 'text/plain',
|
type: 'text/plain',
|
||||||
text,
|
text,
|
||||||
parentPdfId: attachment.id,
|
parentPdfId: attachment.id,
|
||||||
uploadedAt: new Date().toISOString(),
|
uploadedAt: new Date().toISOString(),
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
} catch (txtErr) {
|
||||||
|
console.error('Fehler beim Aktualisieren der Textdatei:', txtErr);
|
||||||
}
|
}
|
||||||
} catch (txtErr) {
|
|
||||||
console.error('Fehler beim Aktualisieren der Textdatei:', txtErr);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.runPdfParse(attachment.file);
|
await this.runParse(attachment.file);
|
||||||
}
|
}
|
||||||
|
|
||||||
handlePdfDelete(attachment) {
|
handlePdfDelete(attachment) {
|
||||||
@@ -308,7 +399,7 @@ class LieferungNeuPage extends PureComponent {
|
|||||||
deliveryEditorAutomat.actions.removeAttachment(attachment.id);
|
deliveryEditorAutomat.actions.removeAttachment(attachment.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
async runPdfParse(file) {
|
async runParse(file) {
|
||||||
this.setState({
|
this.setState({
|
||||||
pdfUploading: true,
|
pdfUploading: true,
|
||||||
pdfDialogOpen: true,
|
pdfDialogOpen: true,
|
||||||
@@ -327,10 +418,10 @@ class LieferungNeuPage extends PureComponent {
|
|||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const gen = this.context?.delivery?.parsePdfSse;
|
const parser = this.context?.delivery?.parseSse;
|
||||||
if (!gen) throw new Error('parsePdfSse nicht verfügbar.');
|
if (!parser) throw new Error('parseSse nicht verfügbar.');
|
||||||
|
|
||||||
for await (const { event, data } of this.context.delivery.parsePdfSse(file)) {
|
for await (const { event, data } of parser(file)) {
|
||||||
if (this._cancelled) return;
|
if (this._cancelled) return;
|
||||||
switch (event) {
|
switch (event) {
|
||||||
case 'status':
|
case 'status':
|
||||||
@@ -414,6 +505,13 @@ class LieferungNeuPage extends PureComponent {
|
|||||||
if (notes.length > 0) {
|
if (notes.length > 0) {
|
||||||
deliveryEditorAutomat.actions.setNotes(notes);
|
deliveryEditorAutomat.actions.setNotes(notes);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Supply whole article list to GET /supplier/{id}/articles
|
||||||
|
const effectiveSupplierId = supplierId ?? deliveryEditorAutomat.state.deliveryEntity ?? deliveryEditorAutomat.state.partyId;
|
||||||
|
const effectiveLines = (newLines && newLines.length > 0) ? newLines : deliveryEditorAutomat.state.lines;
|
||||||
|
if (effectiveSupplierId != null && effectiveLines && effectiveLines.length > 0) {
|
||||||
|
deliveryEditorAutomat.actions.fetchSupplierArticles(effectiveSupplierId, effectiveLines);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'error':
|
case 'error':
|
||||||
@@ -421,7 +519,7 @@ class LieferungNeuPage extends PureComponent {
|
|||||||
pdfDialogData: {
|
pdfDialogData: {
|
||||||
...prev.pdfDialogData,
|
...prev.pdfDialogData,
|
||||||
streaming: false,
|
streaming: false,
|
||||||
error: data.message ?? 'PDF-Analyse fehlgeschlagen.',
|
error: data.message ?? 'Analyse fehlgeschlagen.',
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
break;
|
break;
|
||||||
@@ -435,7 +533,7 @@ class LieferungNeuPage extends PureComponent {
|
|||||||
pdfDialogData: {
|
pdfDialogData: {
|
||||||
...prev.pdfDialogData,
|
...prev.pdfDialogData,
|
||||||
streaming: false,
|
streaming: false,
|
||||||
error: err.message ?? 'Netzwerkfehler bei PDF-Analyse.',
|
error: err.message ?? 'Netzwerkfehler bei Analyse.',
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
} finally {
|
} finally {
|
||||||
@@ -450,6 +548,14 @@ class LieferungNeuPage extends PureComponent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async runPdfParse(file) {
|
||||||
|
await this.runParse(file);
|
||||||
|
}
|
||||||
|
|
||||||
|
async runTxtParse(file) {
|
||||||
|
await this.runParse(file);
|
||||||
|
}
|
||||||
|
|
||||||
handleClosePdfDialog() {
|
handleClosePdfDialog() {
|
||||||
this.setState({ pdfDialogOpen: false });
|
this.setState({ pdfDialogOpen: false });
|
||||||
}
|
}
|
||||||
@@ -646,10 +752,16 @@ class LieferungNeuPage extends PureComponent {
|
|||||||
lines={lines}
|
lines={lines}
|
||||||
saving={saving}
|
saving={saving}
|
||||||
linesColor={cfg.linesColor}
|
linesColor={cfg.linesColor}
|
||||||
|
supplierArticlesMap={this.state.supplierArticlesMap}
|
||||||
|
supplierArticlesByLineKey={this.state.supplierArticlesByLineKey}
|
||||||
|
supplierArticlesLoading={this.state.supplierArticlesLoading}
|
||||||
|
loadingLineKeys={this.state.loadingLineKeys}
|
||||||
onLineChange={this.handleLineChange}
|
onLineChange={this.handleLineChange}
|
||||||
|
onSkuBlur={this.handleSkuBlur}
|
||||||
onAddLine={this.handleAddLine}
|
onAddLine={this.handleAddLine}
|
||||||
onRemoveLine={this.handleRemoveLine}
|
onRemoveLine={this.handleRemoveLine}
|
||||||
onSave={() => void this.handleSave()}
|
onSave={() => void this.handleSave()}
|
||||||
|
onCreateArticle={this.handleCreateArticleForLine}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user