From 98080f0514fc2cc78c4ecc3aaa5999a63f85d203 Mon Sep 17 00:00:00 2001 From: seb Date: Mon, 21 Sep 2026 07:43:15 +0200 Subject: [PATCH] u --- src/automats/deliveryEditorAutomat.js | 72 +++++++++++++++++++- src/components/manifest/ManifestFormLine.jsx | 25 +++++++ src/components/manifest/ManifestLines.jsx | 4 ++ src/pages/lieferung/neu.jsx | 23 +++++++ 4 files changed, 123 insertions(+), 1 deletion(-) diff --git a/src/automats/deliveryEditorAutomat.js b/src/automats/deliveryEditorAutomat.js index 0293f36..4af990c 100644 --- a/src/automats/deliveryEditorAutomat.js +++ b/src/automats/deliveryEditorAutomat.js @@ -1,5 +1,5 @@ import { Automat } from 'react-automat'; -import { supplier as supplierApi } from '../api'; +import { article as articleApi, supplier as supplierApi } from '../api'; /** * @typedef {Object} DeliveryLine @@ -43,6 +43,7 @@ import { supplier as supplierApi } from '../api'; * @property {Object} supplierArticlesByLineKey Matched article per line. * @property {boolean} supplierArticlesLoading Whether any supplier article request is pending. * @property {Object} loadingLineKeys Pending lookup state per line. + * @property {Object} busySupplierArticleLineKeys Lines with a pending unlink request. * @property {string|null} supplierArticlesError Last supplier article error. * @property {string|null} unmatchedSupplierName Parsed supplier name without a matching entity. */ @@ -130,6 +131,7 @@ export const initialDelivery = { supplierArticlesByLineKey: {}, supplierArticlesLoading: false, loadingLineKeys: {}, + busySupplierArticleLineKeys: {}, supplierArticlesError: null, unmatchedSupplierName: null, }; @@ -482,6 +484,70 @@ export function createDeliveryEditorAutomat(options = {}) { } }, + /** + * Removes the supplier-article assignment behind a matched line by + * `DELETE /api/article/{sku}/supplier/{id}`, then re-resolves the line. + * + * @param {string|number} internalSku Internal article SKU. + * @param {string|number} assignmentId Article-supplier assignment ID. + * @param {string|number} [lineKey] Editor line key showing the match. + * @returns {Promise} + */ + async unlinkSupplierArticle(internalSku, assignmentId, lineKey) { + const entityId = automat.state.deliveryEntity ?? automat.state.partyId; + const sku = internalSku != null ? String(internalSku).trim() : ''; + if (!sku || assignmentId == null) { + throw new Error('Artikelsku und Zuordnungs-ID sind erforderlich.'); + } + + if (lineKey != null) { + automat.setState({ + busySupplierArticleLineKeys: { + ...(automat.state.busySupplierArticleLineKeys || {}), + [lineKey]: true, + }, + }); + } + + try { + await articleApi.deleteSupplier(sku, assignmentId); + + const clearedMap = { ...(automat.state.supplierArticlesMap || {}) }; + for (const [mapKey, item] of Object.entries(clearedMap)) { + if (item != null && String(item.id) === String(assignmentId)) { + delete clearedMap[mapKey]; + } + } + + const nextList = (automat.state.supplierArticles || []).filter( + (item) => item != null && String(item.id) !== String(assignmentId), + ); + + const byLineKey = { ...(automat.state.supplierArticlesByLineKey || {}) }; + if (lineKey != null) byLineKey[lineKey] = null; + + automat.setState({ + supplierArticles: nextList, + supplierArticlesMap: clearedMap, + supplierArticlesByLineKey: byLineKey, + }); + + if (lineKey != null && entityId) { + const line = (automat.state.lines || []).find((l) => l.key === lineKey); + const rawSku = line?.sku != null ? String(line.sku).trim() : ''; + if (rawSku) { + await automat.actions.fetchSupplierArticleForSku(rawSku, lineKey); + } + } + } finally { + if (lineKey != null) { + const nextBusy = { ...(automat.state.busySupplierArticleLineKeys || {}) }; + delete nextBusy[lineKey]; + automat.setState({ busySupplierArticleLineKeys: nextBusy }); + } + } + }, + /** @param {Object} attachment @returns {void} */ addAttachment(attachment) { const id = attachment.id ?? `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`; @@ -594,6 +660,7 @@ export const { removeLine, fetchSupplierArticles, fetchSupplierArticleForSku, + unlinkSupplierArticle, addAttachment, setAttachments, updateAttachment, @@ -637,5 +704,8 @@ export const selectSupplierArticlesByLineKey = (state = deliveryEditorAutomat.st export const selectSupplierArticlesLoading = (state = deliveryEditorAutomat.state) => state.supplierArticlesLoading; /** @param {DeliveryEditorState} [state] @returns {Object} */ export const selectLoadingLineKeys = (state = deliveryEditorAutomat.state) => state.loadingLineKeys; +/** @param {DeliveryEditorState} [state] @returns {Object} */ +export const selectBusySupplierArticleLineKeys = (state = deliveryEditorAutomat.state) => + state.busySupplierArticleLineKeys; export default deliveryEditorAutomat; diff --git a/src/components/manifest/ManifestFormLine.jsx b/src/components/manifest/ManifestFormLine.jsx index 54ef881..372b0c6 100644 --- a/src/components/manifest/ManifestFormLine.jsx +++ b/src/components/manifest/ManifestFormLine.jsx @@ -8,6 +8,7 @@ import IconButton from '@mui/material/IconButton'; import TableCell from '@mui/material/TableCell'; import TableRow from '@mui/material/TableRow'; import TextField from '@mui/material/TextField'; +import Tooltip from '@mui/material/Tooltip'; import Typography from '@mui/material/Typography'; import DeleteOutlinedIcon from '@mui/icons-material/DeleteOutlined'; import HelpOutlineIcon from '@mui/icons-material/HelpOutlineOutlined'; @@ -15,6 +16,7 @@ import Inventory2OutlinedIcon from '@mui/icons-material/Inventory2Outlined'; import CheckCircleOutlinedIcon from '@mui/icons-material/CheckCircleOutlined'; import AddIcon from '@mui/icons-material/Add'; import SearchIcon from '@mui/icons-material/Search'; +import LinkOffOutlinedIcon from '@mui/icons-material/LinkOffOutlined'; import { LINE_COLUMNS } from './manifestConfig'; import ArticlePhoto from '../article/ArticlePhoto'; import { formatPrice } from '../../utils/price'; @@ -27,6 +29,7 @@ class ManifestFormLine extends PureComponent { this.handleBlur = this.handleBlur.bind(this); this.handleCreateArticle = this.handleCreateArticle.bind(this); this.handleSearchArticle = this.handleSearchArticle.bind(this); + this.handleRemoveSupplierArticle = this.handleRemoveSupplierArticle.bind(this); } handleRemove() { @@ -53,6 +56,13 @@ class ManifestFormLine extends PureComponent { } } + handleRemoveSupplierArticle() { + const { line, supplierArticle, onRemoveSupplierArticle } = this.props; + if (onRemoveSupplierArticle) { + onRemoveSupplierArticle(line, supplierArticle); + } + } + renderField(column) { const { id, align, type } = column; const { line, saving, onChange } = this.props; @@ -226,6 +236,21 @@ class ManifestFormLine extends PureComponent { {taxRate != null ? ` (${taxRate}% MwSt)` : ''} )} + + {/* Remove supplier-article connection */} + {this.props.onRemoveSupplierArticle && supplierArticle.id != null && ( + + + + + + )} ); } diff --git a/src/components/manifest/ManifestLines.jsx b/src/components/manifest/ManifestLines.jsx index b6f1912..75698e2 100644 --- a/src/components/manifest/ManifestLines.jsx +++ b/src/components/manifest/ManifestLines.jsx @@ -31,6 +31,8 @@ class ManifestLines extends PureComponent { onSkuBlur, onCreateArticle, onSearchArticle, + onRemoveSupplierArticle, + busySupplierArticleLineKeys = {}, } = this.props; return ( @@ -75,11 +77,13 @@ class ManifestLines extends PureComponent { saving={saving} supplierArticle={matched} loading={Boolean(isLineLoading)} + supplierArticleBusy={Boolean(busySupplierArticleLineKeys[lineKey])} onChange={onLineChange} onRemove={onRemoveLine} onSkuBlur={onSkuBlur} onCreateArticle={onCreateArticle} onSearchArticle={onSearchArticle} + onRemoveSupplierArticle={onRemoveSupplierArticle} /> ); })} diff --git a/src/pages/lieferung/neu.jsx b/src/pages/lieferung/neu.jsx index 977fe7c..8c725a2 100644 --- a/src/pages/lieferung/neu.jsx +++ b/src/pages/lieferung/neu.jsx @@ -85,6 +85,7 @@ class LieferungNeuPage extends PureComponent { this.handleOpenArticleSearchForLine = this.handleOpenArticleSearchForLine.bind(this); this.handleCloseArticleSearch = this.handleCloseArticleSearch.bind(this); this.handleAssignArticleForLine = this.handleAssignArticleForLine.bind(this); + this.handleRemoveSupplierArticle = this.handleRemoveSupplierArticle.bind(this); } componentDidMount() { @@ -403,6 +404,26 @@ class LieferungNeuPage extends PureComponent { } } + async handleRemoveSupplierArticle(line, supplierArticle) { + const internalSku = supplierArticle?.article?.sku; + const assignmentId = supplierArticle?.id; + if (!internalSku || assignmentId == null) { + enqueueSnackbar('Verknüpfung konnte nicht ermittelt werden.', { variant: 'warning' }); + return; + } + + try { + await this.automat.actions.unlinkSupplierArticle(internalSku, assignmentId, line?.key); + enqueueSnackbar( + `Verknüpfung zu Artikel "${internalSku}" wurde entfernt.`, + { variant: 'success' }, + ); + } catch (err) { + console.error('Fehler beim Lösen der Lieferanten-Verknüpfung:', err); + notifyError(err); + } + } + handleAddLine() { this.automat.actions.addLineitem(); } @@ -1111,6 +1132,8 @@ class LieferungNeuPage extends PureComponent { onSave={() => void this.handleSave()} onCreateArticle={this.handleCreateArticleForLine} onSearchArticle={this.handleOpenArticleSearchForLine} + onRemoveSupplierArticle={this.handleRemoveSupplierArticle} + busySupplierArticleLineKeys={this.state.busySupplierArticleLineKeys} /> )}