u
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
import { Automat } from 'react-automat';
|
import { Automat } from 'react-automat';
|
||||||
import { supplier as supplierApi } from '../api';
|
import { article as articleApi, supplier as supplierApi } from '../api';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {Object} DeliveryLine
|
* @typedef {Object} DeliveryLine
|
||||||
@@ -43,6 +43,7 @@ import { supplier as supplierApi } from '../api';
|
|||||||
* @property {Object<string, SupplierArticle|null>} supplierArticlesByLineKey Matched article per line.
|
* @property {Object<string, SupplierArticle|null>} supplierArticlesByLineKey Matched article per line.
|
||||||
* @property {boolean} supplierArticlesLoading Whether any supplier article request is pending.
|
* @property {boolean} supplierArticlesLoading Whether any supplier article request is pending.
|
||||||
* @property {Object<string, boolean>} loadingLineKeys Pending lookup state per line.
|
* @property {Object<string, boolean>} loadingLineKeys Pending lookup state per line.
|
||||||
|
* @property {Object<string, boolean>} busySupplierArticleLineKeys Lines with a pending unlink request.
|
||||||
* @property {string|null} supplierArticlesError Last supplier article error.
|
* @property {string|null} supplierArticlesError Last supplier article error.
|
||||||
* @property {string|null} unmatchedSupplierName Parsed supplier name without a matching entity.
|
* @property {string|null} unmatchedSupplierName Parsed supplier name without a matching entity.
|
||||||
*/
|
*/
|
||||||
@@ -130,6 +131,7 @@ export const initialDelivery = {
|
|||||||
supplierArticlesByLineKey: {},
|
supplierArticlesByLineKey: {},
|
||||||
supplierArticlesLoading: false,
|
supplierArticlesLoading: false,
|
||||||
loadingLineKeys: {},
|
loadingLineKeys: {},
|
||||||
|
busySupplierArticleLineKeys: {},
|
||||||
supplierArticlesError: null,
|
supplierArticlesError: null,
|
||||||
unmatchedSupplierName: 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<void>}
|
||||||
|
*/
|
||||||
|
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} */
|
/** @param {Object} attachment @returns {void} */
|
||||||
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)}`;
|
||||||
@@ -594,6 +660,7 @@ export const {
|
|||||||
removeLine,
|
removeLine,
|
||||||
fetchSupplierArticles,
|
fetchSupplierArticles,
|
||||||
fetchSupplierArticleForSku,
|
fetchSupplierArticleForSku,
|
||||||
|
unlinkSupplierArticle,
|
||||||
addAttachment,
|
addAttachment,
|
||||||
setAttachments,
|
setAttachments,
|
||||||
updateAttachment,
|
updateAttachment,
|
||||||
@@ -637,5 +704,8 @@ export const selectSupplierArticlesByLineKey = (state = deliveryEditorAutomat.st
|
|||||||
export const selectSupplierArticlesLoading = (state = deliveryEditorAutomat.state) => state.supplierArticlesLoading;
|
export const selectSupplierArticlesLoading = (state = deliveryEditorAutomat.state) => state.supplierArticlesLoading;
|
||||||
/** @param {DeliveryEditorState} [state] @returns {Object<string, boolean>} */
|
/** @param {DeliveryEditorState} [state] @returns {Object<string, boolean>} */
|
||||||
export const selectLoadingLineKeys = (state = deliveryEditorAutomat.state) => state.loadingLineKeys;
|
export const selectLoadingLineKeys = (state = deliveryEditorAutomat.state) => state.loadingLineKeys;
|
||||||
|
/** @param {DeliveryEditorState} [state] @returns {Object<string, boolean>} */
|
||||||
|
export const selectBusySupplierArticleLineKeys = (state = deliveryEditorAutomat.state) =>
|
||||||
|
state.busySupplierArticleLineKeys;
|
||||||
|
|
||||||
export default deliveryEditorAutomat;
|
export default deliveryEditorAutomat;
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ 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 Tooltip from '@mui/material/Tooltip';
|
||||||
import Typography from '@mui/material/Typography';
|
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 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 CheckCircleOutlinedIcon from '@mui/icons-material/CheckCircleOutlined';
|
||||||
import AddIcon from '@mui/icons-material/Add';
|
import AddIcon from '@mui/icons-material/Add';
|
||||||
import SearchIcon from '@mui/icons-material/Search';
|
import SearchIcon from '@mui/icons-material/Search';
|
||||||
|
import LinkOffOutlinedIcon from '@mui/icons-material/LinkOffOutlined';
|
||||||
import { LINE_COLUMNS } from './manifestConfig';
|
import { LINE_COLUMNS } from './manifestConfig';
|
||||||
import ArticlePhoto from '../article/ArticlePhoto';
|
import ArticlePhoto from '../article/ArticlePhoto';
|
||||||
import { formatPrice } from '../../utils/price';
|
import { formatPrice } from '../../utils/price';
|
||||||
@@ -27,6 +29,7 @@ class ManifestFormLine extends PureComponent {
|
|||||||
this.handleBlur = this.handleBlur.bind(this);
|
this.handleBlur = this.handleBlur.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);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleRemove() {
|
handleRemove() {
|
||||||
@@ -53,6 +56,13 @@ class ManifestFormLine extends PureComponent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleRemoveSupplierArticle() {
|
||||||
|
const { line, supplierArticle, onRemoveSupplierArticle } = this.props;
|
||||||
|
if (onRemoveSupplierArticle) {
|
||||||
|
onRemoveSupplierArticle(line, supplierArticle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
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;
|
||||||
@@ -226,6 +236,21 @@ class ManifestFormLine extends PureComponent {
|
|||||||
{taxRate != null ? ` (${taxRate}% MwSt)` : ''}
|
{taxRate != null ? ` (${taxRate}% MwSt)` : ''}
|
||||||
</Typography>
|
</Typography>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Remove supplier-article connection */}
|
||||||
|
{this.props.onRemoveSupplierArticle && supplierArticle.id != null && (
|
||||||
|
<span style={{ marginLeft: priceNet != null ? 0 : 'auto' }}>
|
||||||
|
<IconButton
|
||||||
|
size="small"
|
||||||
|
color="error"
|
||||||
|
aria-label="Lieferanten-Verknüpfung entfernen"
|
||||||
|
disabled={this.props.saving || this.props.supplierArticleBusy}
|
||||||
|
onClick={this.handleRemoveSupplierArticle}
|
||||||
|
>
|
||||||
|
<LinkOffOutlinedIcon fontSize="small" />
|
||||||
|
</IconButton>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,6 +31,8 @@ class ManifestLines extends PureComponent {
|
|||||||
onSkuBlur,
|
onSkuBlur,
|
||||||
onCreateArticle,
|
onCreateArticle,
|
||||||
onSearchArticle,
|
onSearchArticle,
|
||||||
|
onRemoveSupplierArticle,
|
||||||
|
busySupplierArticleLineKeys = {},
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -75,11 +77,13 @@ class ManifestLines extends PureComponent {
|
|||||||
saving={saving}
|
saving={saving}
|
||||||
supplierArticle={matched}
|
supplierArticle={matched}
|
||||||
loading={Boolean(isLineLoading)}
|
loading={Boolean(isLineLoading)}
|
||||||
|
supplierArticleBusy={Boolean(busySupplierArticleLineKeys[lineKey])}
|
||||||
onChange={onLineChange}
|
onChange={onLineChange}
|
||||||
onRemove={onRemoveLine}
|
onRemove={onRemoveLine}
|
||||||
onSkuBlur={onSkuBlur}
|
onSkuBlur={onSkuBlur}
|
||||||
onCreateArticle={onCreateArticle}
|
onCreateArticle={onCreateArticle}
|
||||||
onSearchArticle={onSearchArticle}
|
onSearchArticle={onSearchArticle}
|
||||||
|
onRemoveSupplierArticle={onRemoveSupplierArticle}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -85,6 +85,7 @@ class LieferungNeuPage extends PureComponent {
|
|||||||
this.handleOpenArticleSearchForLine = this.handleOpenArticleSearchForLine.bind(this);
|
this.handleOpenArticleSearchForLine = this.handleOpenArticleSearchForLine.bind(this);
|
||||||
this.handleCloseArticleSearch = this.handleCloseArticleSearch.bind(this);
|
this.handleCloseArticleSearch = this.handleCloseArticleSearch.bind(this);
|
||||||
this.handleAssignArticleForLine = this.handleAssignArticleForLine.bind(this);
|
this.handleAssignArticleForLine = this.handleAssignArticleForLine.bind(this);
|
||||||
|
this.handleRemoveSupplierArticle = this.handleRemoveSupplierArticle.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
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() {
|
handleAddLine() {
|
||||||
this.automat.actions.addLineitem();
|
this.automat.actions.addLineitem();
|
||||||
}
|
}
|
||||||
@@ -1111,6 +1132,8 @@ class LieferungNeuPage extends PureComponent {
|
|||||||
onSave={() => void this.handleSave()}
|
onSave={() => void this.handleSave()}
|
||||||
onCreateArticle={this.handleCreateArticleForLine}
|
onCreateArticle={this.handleCreateArticleForLine}
|
||||||
onSearchArticle={this.handleOpenArticleSearchForLine}
|
onSearchArticle={this.handleOpenArticleSearchForLine}
|
||||||
|
onRemoveSupplierArticle={this.handleRemoveSupplierArticle}
|
||||||
|
busySupplierArticleLineKeys={this.state.busySupplierArticleLineKeys}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user