u
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user