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 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,

View File

@@ -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 {
/>
<Autocomplete
size="small"
autoHighlight
options={parties || []}
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}
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: {

View File

@@ -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);