This commit is contained in:
seb
2026-10-13 22:50:36 +02:00
parent bf7ff349f3
commit e2bf08d340
3 changed files with 223 additions and 10 deletions

View File

@@ -16,6 +16,7 @@ import ArticleSearchDialog from './ArticleSearchDialog';
import artikelAutomat, { getArticle } from '../../automats/artikelAutomat'; import artikelAutomat, { getArticle } from '../../automats/artikelAutomat';
import entityAutomat from '../../automats/entityAutomat'; import entityAutomat from '../../automats/entityAutomat';
import supplierAutomat from '../../automats/supplierAutomat'; import supplierAutomat from '../../automats/supplierAutomat';
import companyAutomat, { selectCompanyTaxrate } from '../../automats/companyAutomat';
import { netFromGross, getArticleGross } from '../../utils/price'; import { netFromGross, getArticleGross } from '../../utils/price';
import { import {
buildManifestBody, 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(); if (this.props.mode === 'edit') void this.loadEdit();
else void this.loadPartiesOnly(); else void this.loadPartiesOnly();
} }
@@ -111,6 +141,7 @@ class ManifestForm extends PureComponent {
componentWillUnmount() { componentWillUnmount() {
this._cancelled = true; this._cancelled = true;
this.unsubParties?.(); this.unsubParties?.();
this.unsubCompany?.();
if (this._parseAbortController) { if (this._parseAbortController) {
this._parseAbortController.abort(); this._parseAbortController.abort();
this._parseAbortController = null; this._parseAbortController = null;
@@ -323,12 +354,21 @@ class ManifestForm extends PureComponent {
const g = Math.round((netNum * (1 + (taxNum ?? 0) / 100) + Number.EPSILON) * 100) / 100; const g = Math.round((netNum * (1 + (taxNum ?? 0) / 100) + Number.EPSILON) * 100) / 100;
grossVal = String(g); 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 { return {
...l, ...l,
amount: l.amount || '1',
sku: art.sku || l.sku, sku: art.sku || l.sku,
name: art.name || l.name || '', name: art.name || l.name || '',
tax: l.tax || taxVal, tax: finalTax,
taxrate: l.taxrate || taxVal, taxrate: finalTax,
rebate: l.rebate || '0',
net: l.net || netVal, net: l.net || netVal,
netprice: l.netprice || netVal, netprice: l.netprice || netVal,
gross: l.gross || grossVal, gross: l.gross || grossVal,
@@ -348,6 +388,7 @@ class ManifestForm extends PureComponent {
handleAddArticleLine(article) { handleAddArticleLine(article) {
if (!article) return; if (!article) return;
const normTax = selectCompanyTaxrate() != null ? String(selectCompanyTaxrate()) : '19';
const netVal = const netVal =
article.price != null article.price != null
? String(article.price) ? String(article.price)
@@ -361,7 +402,7 @@ class ManifestForm extends PureComponent {
? String(article.taxrate) ? String(article.taxrate)
: article.tax != null : article.tax != null
? String(article.tax) ? String(article.tax)
: ''; : normTax;
const netNum = parseNum(netVal); const netNum = parseNum(netVal);
const taxNum = parseNum(taxVal); const taxNum = parseNum(taxVal);
const artGross = getArticleGross(article, taxNum ?? 0); const artGross = getArticleGross(article, taxNum ?? 0);
@@ -383,7 +424,7 @@ class ManifestForm extends PureComponent {
net: netVal, net: netVal,
netprice: netVal, netprice: netVal,
gross: grossVal, gross: grossVal,
rebate: '', rebate: '0',
pu: '1', pu: '1',
unit: article.unit || 'Stück', unit: article.unit || 'Stück',
}; };
@@ -420,6 +461,7 @@ class ManifestForm extends PureComponent {
}); });
return; return;
} }
const normTax = selectCompanyTaxrate() != null ? String(selectCompanyTaxrate()) : '19';
const netVal = const netVal =
article.price != null article.price != null
? String(article.price) ? String(article.price)
@@ -433,7 +475,7 @@ class ManifestForm extends PureComponent {
? String(article.taxrate) ? String(article.taxrate)
: article.tax != null : article.tax != null
? String(article.tax) ? String(article.tax)
: ''; : normTax;
const netNum = parseNum(netVal); const netNum = parseNum(netVal);
const taxNum = parseNum(taxVal); const taxNum = parseNum(taxVal);
const artGross = getArticleGross(article, taxNum ?? 0); const artGross = getArticleGross(article, taxNum ?? 0);
@@ -450,10 +492,12 @@ class ManifestForm extends PureComponent {
if (l.key !== targetLine.key) return l; if (l.key !== targetLine.key) return l;
return { return {
...l, ...l,
amount: l.amount || '1',
sku: article.sku || l.sku, sku: article.sku || l.sku,
name: article.name || l.name, name: article.name || l.name,
tax: taxVal || l.tax, tax: taxVal || l.tax || normTax,
taxrate: taxVal || l.taxrate, taxrate: taxVal || l.taxrate || normTax,
rebate: l.rebate || '0',
net: netVal || l.net, net: netVal || l.net,
netprice: netVal || l.netprice, netprice: netVal || l.netprice,
gross: grossVal || l.gross, gross: grossVal || l.gross,

View File

@@ -11,10 +11,14 @@ import Inventory2OutlinedIcon from '@mui/icons-material/Inventory2Outlined';
import CheckCircleOutlineOutlinedIcon from '@mui/icons-material/CheckCircleOutlineOutlined'; import CheckCircleOutlineOutlinedIcon from '@mui/icons-material/CheckCircleOutlineOutlined';
import SingleLineTextEdit from 'dashgui/SingleLineTextEdit'; import SingleLineTextEdit from 'dashgui/SingleLineTextEdit';
import { PanelTile } from 'dashgui/panels'; import { PanelTile } from 'dashgui/panels';
import { enqueueSnackbar } from 'notistack';
import FileDropzone from '../FileDropzone'; import FileDropzone from '../FileDropzone';
import AttachmentList from './AttachmentList'; import AttachmentList from './AttachmentList';
import { hasArticleLines, manifestConfig, partyDisplayName } from './manifestConfig'; 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 { import {
SURFACE_RAISED, SURFACE_RAISED,
} from '../../colors'; } from '../../colors';
@@ -22,17 +26,148 @@ import {
class ManifestHeader extends PureComponent { class ManifestHeader extends PureComponent {
constructor(props) { constructor(props) {
super(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.fileInputRef = createRef();
this.handlePartyChange = this.handlePartyChange.bind(this); 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.handleUploadClick = this.handleUploadClick.bind(this);
this.handleFileChange = this.handleFileChange.bind(this); this.handleFileChange = this.handleFileChange.bind(this);
this.handleFileDrop = this.handleFileDrop.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) { handlePartyChange(_event, option) {
this.setState({
createdParty: null,
partyInput: option ? partyDisplayName(this.props.kind, option) : '',
});
this.props.onPartyChange(option?.id ?? null); 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() { hasArticles() {
return Boolean( return Boolean(
this.props.hasArticles ?? this.props.hasArticles ??
@@ -111,7 +246,8 @@ class ManifestHeader extends PureComponent {
const useExternalNumber = Boolean(cfg.externalNumber); const useExternalNumber = Boolean(cfg.externalNumber);
const selected = 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 isDateEmpty = !date || String(date).trim() === '';
const isPartyEmpty = partyId == null || partyId === ''; const isPartyEmpty = partyId == null || partyId === '';
@@ -147,13 +283,44 @@ class ManifestHeader extends PureComponent {
/> />
<Autocomplete <Autocomplete
size="small" size="small"
autoHighlight
options={parties || []} options={parties || []}
value={selected} value={selected}
loading={Boolean(partiesLoading)} loading={Boolean(partiesLoading) || this.state.creatingParty}
loadingText={this.state.creatingParty ? 'Wird angelegt…' : 'Lädt…'}
noOptionsText={
this.state.creatingParty ? (
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, py: 0.5 }}>
<CircularProgress size={16} />
<span>Wird angelegt…</span>
</Box>
) : (
<Box
component="span"
onMouseDown={(e) => {
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
</Box>
)
}
disabled={saving || pdfUploading} disabled={saving || pdfUploading}
getOptionLabel={(option) => partyDisplayName(kind, option)} getOptionLabel={(option) => partyDisplayName(kind, option)}
isOptionEqualToValue={(option, value) => option?.id === value?.id} isOptionEqualToValue={(option, value) => option?.id === value?.id}
onChange={this.handlePartyChange} onChange={this.handlePartyChange}
onInputChange={this.handlePartyInputChange}
onKeyDown={this.handlePartyKeyDown}
sx={{ sx={{
flex: { xs: '1 1 100%', sm: '2 1 200px' }, flex: { xs: '1 1 100%', sm: '2 1 200px' },
minWidth: { sm: 160 }, minWidth: { sm: 160 },
@@ -163,6 +330,7 @@ class ManifestHeader extends PureComponent {
{...params} {...params}
label={partyLabel} label={partyLabel}
error={isPartyEmpty} error={isPartyEmpty}
onKeyDown={this.handlePartyKeyDown}
slotProps={{ slotProps={{
...params.slotProps, ...params.slotProps,
htmlInput: { htmlInput: {

View File

@@ -568,6 +568,7 @@ export async function loadParties(cfgOrKind) {
export function partyDisplayName(cfgOrKind, party) { export function partyDisplayName(cfgOrKind, party) {
if (!party) return ''; if (!party) return '';
if (typeof party === 'string') return party;
const kind = typeof cfgOrKind === 'string' ? cfgOrKind : cfgOrKind?.kind; const kind = typeof cfgOrKind === 'string' ? cfgOrKind : cfgOrKind?.kind;
if (kind === 'delivery') return String(party.name ?? ''); if (kind === 'delivery') return String(party.name ?? '');
return entityDisplayName(party); return entityDisplayName(party);