diff --git a/src/components/manifest/ManifestForm.jsx b/src/components/manifest/ManifestForm.jsx index d95ff1d..9cbb803 100644 --- a/src/components/manifest/ManifestForm.jsx +++ b/src/components/manifest/ManifestForm.jsx @@ -16,6 +16,7 @@ import ArticleSearchDialog from './ArticleSearchDialog'; import artikelAutomat, { getArticle } from '../../automats/artikelAutomat'; import entityAutomat from '../../automats/entityAutomat'; import supplierAutomat from '../../automats/supplierAutomat'; +import companyAutomat, { selectCompanyTaxrate } from '../../automats/companyAutomat'; import { netFromGross, getArticleGross } from '../../utils/price'; import { buildManifestBody, @@ -95,6 +96,35 @@ class ManifestForm extends PureComponent { })); } + if (companyAutomat.state.status === 'idle') { + void companyAutomat.actions.load(); + } + this.unsubCompany = companyAutomat.subscribe(this, (state) => { + const taxrate = state.current?.taxrate; + if (taxrate != null) { + this.setState((prev) => { + if ( + prev.lines.length === 1 && + !prev.lines[0].sku && + !prev.lines[0].name && + !prev.lines[0].net && + prev.lines[0].tax !== String(taxrate) + ) { + return { + lines: [ + { + ...prev.lines[0], + tax: String(taxrate), + taxrate: String(taxrate), + }, + ], + }; + } + return null; + }); + } + }); + if (this.props.mode === 'edit') void this.loadEdit(); else void this.loadPartiesOnly(); } @@ -111,6 +141,7 @@ class ManifestForm extends PureComponent { componentWillUnmount() { this._cancelled = true; this.unsubParties?.(); + this.unsubCompany?.(); if (this._parseAbortController) { this._parseAbortController.abort(); this._parseAbortController = null; @@ -323,12 +354,21 @@ class ManifestForm extends PureComponent { const g = Math.round((netNum * (1 + (taxNum ?? 0) / 100) + Number.EPSILON) * 100) / 100; grossVal = String(g); } + const normTax = selectCompanyTaxrate() != null ? String(selectCompanyTaxrate()) : '19'; + const finalTax = + art.taxrate != null + ? String(art.taxrate) + : art.tax != null + ? String(art.tax) + : l.tax || normTax; return { ...l, + amount: l.amount || '1', sku: art.sku || l.sku, name: art.name || l.name || '', - tax: l.tax || taxVal, - taxrate: l.taxrate || taxVal, + tax: finalTax, + taxrate: finalTax, + rebate: l.rebate || '0', net: l.net || netVal, netprice: l.netprice || netVal, gross: l.gross || grossVal, @@ -348,6 +388,7 @@ class ManifestForm extends PureComponent { handleAddArticleLine(article) { if (!article) return; + const normTax = selectCompanyTaxrate() != null ? String(selectCompanyTaxrate()) : '19'; const netVal = article.price != null ? String(article.price) @@ -361,7 +402,7 @@ class ManifestForm extends PureComponent { ? String(article.taxrate) : article.tax != null ? String(article.tax) - : ''; + : normTax; const netNum = parseNum(netVal); const taxNum = parseNum(taxVal); const artGross = getArticleGross(article, taxNum ?? 0); @@ -383,7 +424,7 @@ class ManifestForm extends PureComponent { net: netVal, netprice: netVal, gross: grossVal, - rebate: '', + rebate: '0', pu: '1', unit: article.unit || 'Stück', }; @@ -420,6 +461,7 @@ class ManifestForm extends PureComponent { }); return; } + const normTax = selectCompanyTaxrate() != null ? String(selectCompanyTaxrate()) : '19'; const netVal = article.price != null ? String(article.price) @@ -433,7 +475,7 @@ class ManifestForm extends PureComponent { ? String(article.taxrate) : article.tax != null ? String(article.tax) - : ''; + : normTax; const netNum = parseNum(netVal); const taxNum = parseNum(taxVal); const artGross = getArticleGross(article, taxNum ?? 0); @@ -450,10 +492,12 @@ class ManifestForm extends PureComponent { if (l.key !== targetLine.key) return l; return { ...l, + amount: l.amount || '1', sku: article.sku || l.sku, name: article.name || l.name, - tax: taxVal || l.tax, - taxrate: taxVal || l.taxrate, + tax: taxVal || l.tax || normTax, + taxrate: taxVal || l.taxrate || normTax, + rebate: l.rebate || '0', net: netVal || l.net, netprice: netVal || l.netprice, gross: grossVal || l.gross, diff --git a/src/components/manifest/ManifestHeader.jsx b/src/components/manifest/ManifestHeader.jsx index 42221b3..6170090 100644 --- a/src/components/manifest/ManifestHeader.jsx +++ b/src/components/manifest/ManifestHeader.jsx @@ -11,10 +11,14 @@ import Inventory2OutlinedIcon from '@mui/icons-material/Inventory2Outlined'; import CheckCircleOutlineOutlinedIcon from '@mui/icons-material/CheckCircleOutlineOutlined'; import SingleLineTextEdit from 'dashgui/SingleLineTextEdit'; import { PanelTile } from 'dashgui/panels'; +import { enqueueSnackbar } from 'notistack'; import FileDropzone from '../FileDropzone'; import AttachmentList from './AttachmentList'; import { hasArticleLines, manifestConfig, partyDisplayName } from './manifestConfig'; -import { notifyErrorMessage } from '../../utils/notify'; +import { notifyError, notifyErrorMessage } from '../../utils/notify'; +import entityAutomat from '../../automats/entityAutomat'; +import customergroupAutomat from '../../automats/customergroupAutomat'; +import supplierAutomat from '../../automats/supplierAutomat'; import { SURFACE_RAISED, } from '../../colors'; @@ -22,17 +26,148 @@ import { class ManifestHeader extends PureComponent { constructor(props) { super(props); + const selected = + (props.parties || []).find((row) => row.id === props.partyId) ?? null; + this.state = { + partyInput: selected ? partyDisplayName(props.kind, selected) : '', + creatingParty: false, + createdParty: null, + }; this.fileInputRef = createRef(); this.handlePartyChange = this.handlePartyChange.bind(this); + this.handlePartyInputChange = this.handlePartyInputChange.bind(this); + this.handlePartyKeyDown = this.handlePartyKeyDown.bind(this); + this.handleCreateParty = this.handleCreateParty.bind(this); this.handleUploadClick = this.handleUploadClick.bind(this); this.handleFileChange = this.handleFileChange.bind(this); this.handleFileDrop = this.handleFileDrop.bind(this); } + componentDidUpdate(prevProps) { + if (prevProps.partyId !== this.props.partyId) { + const selected = + (this.props.parties || []).find((row) => row.id === this.props.partyId) ?? + (this.state.createdParty?.id === this.props.partyId ? this.state.createdParty : null); + if (selected) { + this.setState({ partyInput: partyDisplayName(this.props.kind, selected) }); + } else if (this.props.partyId == null) { + this.setState({ partyInput: '', createdParty: null }); + } + } + } + handlePartyChange(_event, option) { + this.setState({ + createdParty: null, + partyInput: option ? partyDisplayName(this.props.kind, option) : '', + }); this.props.onPartyChange(option?.id ?? null); } + handlePartyInputChange(_event, value, reason) { + if (reason === 'reset') { + const selected = + (this.props.parties || []).find((row) => row.id === this.props.partyId) ?? + this.state.createdParty; + this.setState({ + partyInput: selected ? partyDisplayName(this.props.kind, selected) : value, + }); + return; + } + this.setState({ partyInput: value }); + } + + handlePartyKeyDown(event) { + if (event.key !== 'Enter') return; + if (event.defaultPrevented) return; + + const activeDescendant = event.target?.getAttribute?.('aria-activedescendant'); + if (activeDescendant) return; + + const raw = this.state.partyInput?.trim(); + if (!raw || this.state.creatingParty) return; + + const { parties, kind } = this.props; + const lower = raw.toLowerCase(); + const existing = (parties || []).find((p) => { + const label = partyDisplayName(kind, p).toLowerCase(); + return label === lower || String(p.name ?? '').toLowerCase() === lower; + }); + + if (existing) { + event.preventDefault(); + event.stopPropagation(); + this.setState({ + createdParty: null, + partyInput: partyDisplayName(kind, existing), + }); + this.props.onPartyChange(existing.id); + event.target?.blur?.(); + } else { + event.preventDefault(); + event.stopPropagation(); + void this.handleCreateParty(raw, event.target); + } + } + + async handleCreateParty(rawInput, targetEl) { + const raw = String(rawInput ?? this.state.partyInput ?? '').trim(); + if (!raw || this.state.creatingParty) return; + + this.setState({ creatingParty: true }); + try { + const { kind, onPartyChange } = this.props; + if (kind === 'delivery') { + const row = await supplierAutomat.actions.create({ name: raw }); + const displayName = row?.name || raw; + enqueueSnackbar(`Lieferant „${displayName}“ angelegt.`, { variant: 'success' }); + this.setState({ creatingParty: false, createdParty: row, partyInput: displayName }); + onPartyChange?.(row.id); + } else { + await customergroupAutomat.actions.ensureLoaded(); + const groups = customergroupAutomat.state.items ?? []; + const defaultGroup = groups.find((g) => g.editable !== false) ?? groups[0]; + if (!defaultGroup) { + throw new Error('Keine Kundengruppe vorhanden.'); + } + + let name = raw; + let firstName = null; + + if (raw.includes(',')) { + const parts = raw.split(',').map((p) => p.trim()); + name = parts[0]; + firstName = parts.slice(1).join(' ') || null; + } else if (raw.includes(' ') && !/gmbh|ag|kg|ug|inc|ltd|co\./i.test(raw)) { + const parts = raw.split(' '); + firstName = parts[0]; + name = parts.slice(1).join(' '); + } + + const body = { + customergroupId: defaultGroup.id, + name, + }; + if (firstName) body.firstName = firstName; + + const row = await entityAutomat.actions.create(body); + const displayName = [row.firstName, row.name].filter(Boolean).join(' ') || row.name || raw; + enqueueSnackbar(`Kunde „${displayName}“ angelegt.`, { variant: 'success' }); + this.setState({ creatingParty: false, createdParty: row, partyInput: displayName }); + onPartyChange?.(row.id); + } + + if (targetEl?.blur) { + targetEl.blur(); + } else if (document.activeElement?.blur) { + document.activeElement.blur(); + } + } catch (err) { + this.setState({ creatingParty: false }); + notifyError(err); + } + } + hasArticles() { return Boolean( this.props.hasArticles ?? @@ -111,7 +246,8 @@ class ManifestHeader extends PureComponent { const useExternalNumber = Boolean(cfg.externalNumber); const selected = - (parties || []).find((row) => row.id === partyId) ?? null; + (parties || []).find((row) => row.id === partyId) ?? + (this.state.createdParty?.id === partyId ? this.state.createdParty : null); const isDateEmpty = !date || String(date).trim() === ''; const isPartyEmpty = partyId == null || partyId === ''; @@ -147,13 +283,44 @@ class ManifestHeader extends PureComponent { /> + + Wird angelegt… + + ) : ( + { + e.preventDefault(); + const raw = this.state.partyInput?.trim(); + if (raw) void this.handleCreateParty(raw); + }} + sx={{ + cursor: 'pointer', + display: 'block', + width: '100%', + userSelect: 'none', + fontWeight: 600, + color: 'primary.main', + }} + > + Enter zum anlegen + + ) + } disabled={saving || pdfUploading} getOptionLabel={(option) => partyDisplayName(kind, option)} isOptionEqualToValue={(option, value) => option?.id === value?.id} onChange={this.handlePartyChange} + onInputChange={this.handlePartyInputChange} + onKeyDown={this.handlePartyKeyDown} sx={{ flex: { xs: '1 1 100%', sm: '2 1 200px' }, minWidth: { sm: 160 }, @@ -163,6 +330,7 @@ class ManifestHeader extends PureComponent { {...params} label={partyLabel} error={isPartyEmpty} + onKeyDown={this.handlePartyKeyDown} slotProps={{ ...params.slotProps, htmlInput: { diff --git a/src/components/manifest/manifestConfig.js b/src/components/manifest/manifestConfig.js index bfbdfde..c9d3931 100644 --- a/src/components/manifest/manifestConfig.js +++ b/src/components/manifest/manifestConfig.js @@ -568,6 +568,7 @@ export async function loadParties(cfgOrKind) { export function partyDisplayName(cfgOrKind, party) { if (!party) return ''; + if (typeof party === 'string') return party; const kind = typeof cfgOrKind === 'string' ? cfgOrKind : cfgOrKind?.kind; if (kind === 'delivery') return String(party.name ?? ''); return entityDisplayName(party);