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