Compare commits

..

3 Commits

Author SHA1 Message Date
seb
94f385e753 u 2026-10-18 19:36:56 +02:00
seb
587170ce9b u 2026-10-18 19:32:31 +02:00
seb
199bf869a6 u 2026-10-18 19:10:43 +02:00
13 changed files with 339 additions and 48 deletions

View File

@@ -113,8 +113,9 @@ class CategoryNameDialog extends Component {
const { open, mode = 'create' } = this.props; const { open, mode = 'create' } = this.props;
const { mode: formMode, name, bulkText, busy, error } = this.state; const { mode: formMode, name, bulkText, busy, error } = this.state;
const isBulk = formMode === BULK_MODE; const isBulk = formMode === BULK_MODE;
const canBulk = mode === 'create'; const canSubmitSingle =
const canSubmitSingle = name.trim() !== ''; name.trim() !== '' &&
(mode !== 'edit' || name.trim() !== (this.props.initialName ?? '').trim());
const bulkNames = parseBulkNames(bulkText); const bulkNames = parseBulkNames(bulkText);
const canSubmitBulk = bulkNames.length > 0; const canSubmitBulk = bulkNames.length > 0;
@@ -157,7 +158,13 @@ class CategoryNameDialog extends Component {
<Button <Button
type="submit" type="submit"
variant="contained" variant="contained"
color={canSubmitSingle ? 'success' : 'inherit'}
disabled={busy || !canSubmitSingle} disabled={busy || !canSubmitSingle}
sx={
canSubmitSingle
? { bgcolor: 'success.main', color: '#fff', '&:hover': { bgcolor: 'success.dark' } }
: undefined
}
> >
{SUBMIT_LABELS[mode]} {SUBMIT_LABELS[mode]}
</Button> </Button>

View File

@@ -749,13 +749,27 @@ export default class CategoryTree extends PureComponent {
> >
Abbrechen Abbrechen
</Button> </Button>
<Button {(() => {
onClick={this.handleConfirmRename} const canSave =
variant="contained" Boolean(renameValue.trim()) &&
disabled={!renameValue.trim() || isSubmitting} !isSubmitting &&
> renameValue.trim() !== (renameTarget?.name ?? '').trim();
Speichern return (
</Button> <Button
onClick={this.handleConfirmRename}
variant="contained"
color={canSave ? 'success' : 'inherit'}
disabled={!canSave}
sx={
canSave
? { bgcolor: 'success.main', color: '#fff', '&:hover': { bgcolor: 'success.dark' } }
: undefined
}
>
Speichern
</Button>
);
})()}
</DialogActions> </DialogActions>
</AppDialog> </AppDialog>

View File

@@ -17,6 +17,7 @@ import OrderDocumentsTiles from './OrderDocumentsTiles';
import OrderShipmentDialog from './OrderShipmentDialog'; import OrderShipmentDialog from './OrderShipmentDialog';
import artikelAutomat, { getArticle } from '../../automats/artikelAutomat'; import artikelAutomat, { getArticle } from '../../automats/artikelAutomat';
import entityAutomat from '../../automats/entityAutomat'; import entityAutomat from '../../automats/entityAutomat';
import customergroupAutomat from '../../automats/customergroupAutomat';
import supplierAutomat from '../../automats/supplierAutomat'; import supplierAutomat from '../../automats/supplierAutomat';
import orderAutomat from '../../automats/orderAutomat'; import orderAutomat from '../../automats/orderAutomat';
import companyAutomat, { selectCompanyTaxrate } from '../../automats/companyAutomat'; import companyAutomat, { selectCompanyTaxrate } from '../../automats/companyAutomat';
@@ -33,6 +34,7 @@ import {
manifestConfig, manifestConfig,
apiFor, apiFor,
parseNum, parseNum,
filterNonSupplierEntities,
} from './manifestConfig'; } from './manifestConfig';
import { ensureDocumentNumber, getNextDocumentNumber } from '../../utils/documentNumber'; import { ensureDocumentNumber, getNextDocumentNumber } from '../../utils/documentNumber';
import { import {
@@ -44,12 +46,15 @@ class ManifestForm extends PureComponent {
constructor(props) { constructor(props) {
super(props); super(props);
this.cfg = manifestConfig(props.kind); this.cfg = manifestConfig(props.kind);
const groups = customergroupAutomat.state.items ?? [];
const initialParties = props.kind === 'delivery' const initialParties = props.kind === 'delivery'
? (supplierAutomat.state.status === 'ready' ? (supplierAutomat.state.suppliers ?? []) : []) ? (supplierAutomat.state.status === 'ready' ? (supplierAutomat.state.suppliers ?? []) : [])
: (entityAutomat.state.status === 'ready' ? (entityAutomat.state.items ?? []) : []); : (entityAutomat.state.status === 'ready'
? filterNonSupplierEntities(entityAutomat.state.items ?? [], groups)
: []);
const partiesReady = props.kind === 'delivery' const partiesReady = props.kind === 'delivery'
? supplierAutomat.state.status === 'ready' ? supplierAutomat.state.status === 'ready'
: entityAutomat.state.status === 'ready'; : (entityAutomat.state.status === 'ready' && customergroupAutomat.state.status === 'ready');
this.draftAutomat = props.mode === 'create' ? getManifestDraftAutomat(props.kind) : null; this.draftAutomat = props.mode === 'create' ? getManifestDraftAutomat(props.kind) : null;
this.editAutomat = props.mode === 'edit' ? getManifestEditAutomat(props.kind) : null; this.editAutomat = props.mode === 'edit' ? getManifestEditAutomat(props.kind) : null;
@@ -150,10 +155,25 @@ class ManifestForm extends PureComponent {
partiesLoading: state.status === 'loading', partiesLoading: state.status === 'loading',
})); }));
} else { } else {
this.unsubParties = entityAutomat.subscribe(this, (state) => ({ const updateParties = () => {
parties: state.items ?? [], const entities = entityAutomat.state.items ?? [];
partiesLoading: state.status === 'loading', const groups = customergroupAutomat.state.items ?? [];
})); this.setState({
parties: filterNonSupplierEntities(entities, groups),
partiesLoading:
entityAutomat.state.status === 'loading' ||
customergroupAutomat.state.status === 'loading',
});
};
this.unsubParties = entityAutomat.subscribe(this, () => {
updateParties();
return null;
});
this.unsubGroups = customergroupAutomat.subscribe(this, () => {
updateParties();
return null;
});
void customergroupAutomat.actions.ensureLoaded();
} }
if (companyAutomat.state.status === 'idle') { if (companyAutomat.state.status === 'idle') {
@@ -231,6 +251,38 @@ class ManifestForm extends PureComponent {
} }
componentDidUpdate(prevProps, prevState) { componentDidUpdate(prevProps, prevState) {
if (prevProps.kind !== this.props.kind) {
this.cfg = manifestConfig(this.props.kind);
this.unsubParties?.();
this.unsubGroups?.();
if (this.props.kind === 'delivery') {
this.unsubParties = supplierAutomat.subscribe(this, (state) => ({
parties: state.status === 'ready' ? (state.suppliers ?? []) : [],
partiesLoading: state.status === 'loading',
}));
} else {
const updateParties = () => {
const entities = entityAutomat.state.items ?? [];
const groups = customergroupAutomat.state.items ?? [];
this.setState({
parties: filterNonSupplierEntities(entities, groups),
partiesLoading:
entityAutomat.state.status === 'loading' ||
customergroupAutomat.state.status === 'loading',
});
};
this.unsubParties = entityAutomat.subscribe(this, () => {
updateParties();
return null;
});
this.unsubGroups = customergroupAutomat.subscribe(this, () => {
updateParties();
return null;
});
void customergroupAutomat.actions.ensureLoaded();
}
}
if ( if (
this.props.mode === 'edit' && this.props.mode === 'edit' &&
prevProps.docId !== this.props.docId prevProps.docId !== this.props.docId
@@ -291,6 +343,7 @@ class ManifestForm extends PureComponent {
this.unsubDraft?.(); this.unsubDraft?.();
this.unsubEdit?.(); this.unsubEdit?.();
this.unsubParties?.(); this.unsubParties?.();
this.unsubGroups?.();
this.unsubCompany?.(); this.unsubCompany?.();
if (this._parseAbortController) { if (this._parseAbortController) {
this._parseAbortController.abort(); this._parseAbortController.abort();
@@ -1095,6 +1148,13 @@ class ManifestForm extends PureComponent {
if (this.state.saving) return false; if (this.state.saving) return false;
if (mode === 'edit' && docId == null) return false; if (mode === 'edit' && docId == null) return false;
if (cfg.kind === 'order' && (this.state.partyId == null || this.state.partyId === '')) {
this.setState({ partyError: true });
notifyErrorMessage('Für eine Bestellung ist ein Kunde erforderlich.');
this.headerRef.current?.focusPartyInput?.();
return false;
}
const isEdit = mode === 'edit'; const isEdit = mode === 'edit';
let body; let body;
try { try {

View File

@@ -16,7 +16,12 @@ import { enqueueSnackbar } from 'notistack';
import RestoreIcon from '@mui/icons-material/Restore'; import RestoreIcon from '@mui/icons-material/Restore';
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,
getLowestSortNonSupplierCustomerGroup,
} from './manifestConfig';
import { notifyError, notifyErrorMessage } from '../../utils/notify'; import { notifyError, notifyErrorMessage } from '../../utils/notify';
import entityAutomat from '../../automats/entityAutomat'; import entityAutomat from '../../automats/entityAutomat';
import customergroupAutomat from '../../automats/customergroupAutomat'; import customergroupAutomat from '../../automats/customergroupAutomat';
@@ -29,7 +34,11 @@ class ManifestHeader extends PureComponent {
constructor(props) { constructor(props) {
super(props); super(props);
const selected = const selected =
(props.parties || []).find((row) => row.id === props.partyId) ?? null; (props.parties || []).find((row) => row.id === props.partyId) ??
(props.kind === 'delivery'
? (supplierAutomat.state.suppliers || []).find((row) => row.id === props.partyId)
: (entityAutomat.state.items || []).find((row) => row.id === props.partyId)) ??
null;
this.state = { this.state = {
partyInput: selected ? partyDisplayName(props.kind, selected) : '', partyInput: selected ? partyDisplayName(props.kind, selected) : '',
creatingParty: false, creatingParty: false,
@@ -48,10 +57,13 @@ class ManifestHeader extends PureComponent {
} }
componentDidUpdate(prevProps) { componentDidUpdate(prevProps) {
if (prevProps.partyId !== this.props.partyId) { if (prevProps.partyId !== this.props.partyId || prevProps.parties !== this.props.parties) {
const selected = const selected =
(this.props.parties || []).find((row) => row.id === this.props.partyId) ?? (this.props.parties || []).find((row) => row.id === this.props.partyId) ??
(this.state.createdParty?.id === this.props.partyId ? this.state.createdParty : null); (this.state.createdParty?.id === this.props.partyId ? this.state.createdParty : null) ??
(this.props.kind === 'delivery'
? (supplierAutomat.state.suppliers || []).find((row) => row.id === this.props.partyId)
: (entityAutomat.state.items || []).find((row) => row.id === this.props.partyId));
if (selected) { if (selected) {
this.setState({ partyInput: partyDisplayName(this.props.kind, selected) }); this.setState({ partyInput: partyDisplayName(this.props.kind, selected) });
} else if (this.props.partyId == null) { } else if (this.props.partyId == null) {
@@ -137,8 +149,8 @@ class ManifestHeader extends PureComponent {
} else { } else {
await customergroupAutomat.actions.ensureLoaded(); await customergroupAutomat.actions.ensureLoaded();
const groups = customergroupAutomat.state.items ?? []; const groups = customergroupAutomat.state.items ?? [];
const defaultGroup = groups.find((g) => g.editable !== false) ?? groups[0]; const targetGroup = getLowestSortNonSupplierCustomerGroup(groups) ?? groups[0];
if (!defaultGroup) { if (!targetGroup) {
throw new Error('Keine Kundengruppe vorhanden.'); throw new Error('Keine Kundengruppe vorhanden.');
} }
@@ -156,7 +168,7 @@ class ManifestHeader extends PureComponent {
} }
const body = { const body = {
customergroupId: defaultGroup.id, customergroupId: targetGroup.id,
name, name,
}; };
if (firstName) body.firstName = firstName; if (firstName) body.firstName = firstName;
@@ -260,6 +272,13 @@ class ManifestHeader extends PureComponent {
} = this.props; } = this.props;
const hasArticles = this.hasArticles(); const hasArticles = this.hasArticles();
const isEdit = docId != null;
const hasAttachments = Array.isArray(attachments) && attachments.length > 0;
const hasParty = partyId != null && partyId !== '';
const partyValid = kind !== 'order' || hasParty;
const canSave =
partyValid &&
(isEdit ? Boolean(isDirty) : Boolean(hasArticles || hasAttachments));
// Delivery: number is server-allocated/read-only; the caller reference // Delivery: number is server-allocated/read-only; the caller reference
// lives in the editable externalNumber field. // lives in the editable externalNumber field.
@@ -358,8 +377,8 @@ class ManifestHeader extends PureComponent {
{...params} {...params}
inputRef={this.partyInputRef} inputRef={this.partyInputRef}
label={dirtyFields?.partyId ? `${partyLabel} *` : partyLabel} label={dirtyFields?.partyId ? `${partyLabel} *` : partyLabel}
required={kind === 'order'}
error={isPartyEmpty} error={isPartyEmpty}
helperText={Boolean(partyError) && isPartyEmpty ? 'Kunde für Bestellung erforderlich' : undefined}
onKeyDown={this.handlePartyKeyDown} onKeyDown={this.handlePartyKeyDown}
slotProps={{ slotProps={{
...params.slotProps, ...params.slotProps,
@@ -435,20 +454,33 @@ class ManifestHeader extends PureComponent {
<Stack spacing={1.5} sx={{ pt: 0.5 }}> <Stack spacing={1.5} sx={{ pt: 0.5 }}>
<Button <Button
variant="contained" variant="contained"
color={isDirty ? 'warning' : 'primary'} color={canSave ? 'success' : 'inherit'}
disabled={saving || orderCreating} disabled={Boolean(saving || orderCreating || !canSave)}
onClick={onSave} onClick={onSave}
sx={{ sx={{
height: 40, height: 40,
fontWeight: 700, fontWeight: 700,
fontSize: '0.875rem', fontSize: '0.875rem',
whiteSpace: 'nowrap', whiteSpace: 'nowrap',
...(isDirty && { ...(canSave && {
boxShadow: (theme) => `0 0 0 2px ${theme.palette.warning.main}`, bgcolor: 'success.main',
color: '#fff',
'&:hover': {
bgcolor: 'success.dark',
},
}),
...(!canSave && {
bgcolor: 'action.disabledBackground',
color: 'text.disabled',
boxShadow: 'none',
'&.Mui-disabled': {
bgcolor: 'action.disabledBackground',
color: 'text.disabled',
},
}), }),
}} }}
> >
{saving ? 'Speichern…' : isDirty ? 'Speichern *' : 'Speichern'} {saving ? 'Speichern…' : 'Speichern'}
</Button> </Button>
{isDirty && onRestore && ( {isDirty && onRestore && (
<Button <Button

View File

@@ -362,9 +362,15 @@ export class TextEditorPanel extends PureComponent {
<Button <Button
size="small" size="small"
variant="contained" variant="contained"
color={isDirty ? 'success' : 'inherit'}
startIcon={<SaveOutlinedIcon />} startIcon={<SaveOutlinedIcon />}
onClick={this.handleSave} onClick={this.handleSave}
disabled={loading || !isDirty} disabled={loading || !isDirty}
sx={
isDirty
? { bgcolor: 'success.main', color: '#fff', '&:hover': { bgcolor: 'success.dark' } }
: undefined
}
> >
Speichern Speichern
</Button> </Button>

View File

@@ -33,4 +33,6 @@ export {
restoreAttachmentsFromApi, restoreAttachmentsFromApi,
dateInputValue, dateInputValue,
todayInputValue, todayInputValue,
filterNonSupplierEntities,
getLowestSortNonSupplierCustomerGroup,
} from './manifestConfig'; } from './manifestConfig';

View File

@@ -23,6 +23,10 @@ import {
/** Groups that are not sales customers (filter client-side). */ /** Groups that are not sales customers (filter client-side). */
export const EXCLUDED_CUSTOMER_GROUP_NAMES = new Set(['Lieferanten', 'POS']); export const EXCLUDED_CUSTOMER_GROUP_NAMES = new Set(['Lieferanten', 'POS']);
export function isSupplierCustomerGroup(group) {
return String(group?.name ?? '').trim().toLowerCase() === 'lieferanten';
}
export function entityDisplayName(entity) { export function entityDisplayName(entity) {
if (!entity) return ''; if (!entity) return '';
const fullName = [entity.firstName, entity.name].filter(Boolean).join(' '); const fullName = [entity.firstName, entity.name].filter(Boolean).join(' ');
@@ -37,6 +41,31 @@ export function sortEntities(entities) {
}); });
} }
/**
* Customers = entities whose customergroup is not Lieferanten.
*/
export function filterNonSupplierEntities(entities, groups) {
const supplierIds = new Set(
(groups || [])
.filter(isSupplierCustomerGroup)
.map((g) => g.id),
);
return sortEntities(
(entities || []).filter((e) => !supplierIds.has(e.customergroupId)),
);
}
/**
* Returns the customer group with the lowest sort number that is not Lieferanten.
*/
export function getLowestSortNonSupplierCustomerGroup(groups) {
const nonSuppliers = (groups || []).filter((g) => !isSupplierCustomerGroup(g));
if (nonSuppliers.length === 0) return null;
return [...nonSuppliers].sort(
(a, b) => (Number(a.sort) || 0) - (Number(b.sort) || 0),
)[0];
}
/** /**
* Customers = entities whose customergroup.name is not Lieferanten/POS. * Customers = entities whose customergroup.name is not Lieferanten/POS.
*/ */
@@ -498,6 +527,8 @@ export async function buildManifestBody(
if (!Number.isNaN(pid)) { if (!Number.isNaN(pid)) {
body[partyField] = pid; body[partyField] = pid;
} }
} else if (kind === 'order') {
throw new Error('Für eine Bestellung ist ein Kunde erforderlich.');
} else if (isEdit) { } else if (isEdit) {
body[partyField] = null; body[partyField] = null;
} }
@@ -588,8 +619,13 @@ export async function loadParties(cfgOrKind) {
const list = await supplierAutomat.actions.load(); const list = await supplierAutomat.actions.load();
return sortSuppliers(list); return sortSuppliers(list);
} }
const entities = await entityAutomat.actions.ensureLoaded(); const [entities, groups] = await Promise.all([
return entities ?? entityAutomat.state.items ?? []; entityAutomat.actions.ensureLoaded(),
customergroupAutomat.actions.ensureLoaded(),
]);
const entityList = entities ?? entityAutomat.state.items ?? [];
const groupList = groups ?? customergroupAutomat.state.items ?? [];
return filterNonSupplierEntities(entityList, groupList);
} }
export function partyDisplayName(cfgOrKind, party) { export function partyDisplayName(cfgOrKind, party) {

View File

@@ -52,11 +52,17 @@ export default class CompanySettingsPanel extends Component {
syncFormFromCompany(companyRow) { syncFormFromCompany(companyRow) {
if (!companyRow) return; if (!companyRow) return;
this.setState({ this._initial = {
name: companyRow.name ?? '', name: companyRow.name ?? '',
iso639: companyRow.iso639 ?? 'de', iso639: companyRow.iso639 ?? 'de',
taxrate: companyRow.taxrate != null ? String(companyRow.taxrate) : '', taxrate: companyRow.taxrate != null ? String(companyRow.taxrate) : '',
reducedtaxrate: companyRow.reducedtaxrate != null ? String(companyRow.reducedtaxrate) : '', reducedtaxrate: companyRow.reducedtaxrate != null ? String(companyRow.reducedtaxrate) : '',
};
this.setState({
name: this._initial.name,
iso639: this._initial.iso639,
taxrate: this._initial.taxrate,
reducedtaxrate: this._initial.reducedtaxrate,
status: 'ready', status: 'ready',
}); });
} }
@@ -100,6 +106,12 @@ export default class CompanySettingsPanel extends Component {
taxrate: parsedTaxrate, taxrate: parsedTaxrate,
reducedtaxrate: parsedReducedtaxrate, reducedtaxrate: parsedReducedtaxrate,
}); });
this._initial = {
name: name.trim(),
iso639,
taxrate: String(parsedTaxrate),
reducedtaxrate: String(parsedReducedtaxrate),
};
enqueueSnackbar('Unternehmen gespeichert.', { variant: 'success' }); enqueueSnackbar('Unternehmen gespeichert.', { variant: 'success' });
} catch (err) { } catch (err) {
this.setState({ error: backendErrorFrom(err) }); this.setState({ error: backendErrorFrom(err) });
@@ -125,7 +137,14 @@ export default class CompanySettingsPanel extends Component {
const parsedTaxrate = parseTaxRate(taxrate); const parsedTaxrate = parseTaxRate(taxrate);
const parsedReducedtaxrate = parseTaxRate(reducedtaxrate); const parsedReducedtaxrate = parseTaxRate(reducedtaxrate);
const isDirty =
Boolean(this._initial) &&
(name.trim() !== this._initial.name.trim() ||
iso639 !== this._initial.iso639 ||
taxrate.trim() !== this._initial.taxrate.trim() ||
reducedtaxrate.trim() !== this._initial.reducedtaxrate.trim());
const canSave = const canSave =
isDirty &&
name.trim() !== '' && name.trim() !== '' &&
ISO639_OPTIONS.includes(iso639) && ISO639_OPTIONS.includes(iso639) &&
parsedTaxrate != null && parsedTaxrate != null &&
@@ -218,8 +237,14 @@ export default class CompanySettingsPanel extends Component {
<Button <Button
type="submit" type="submit"
variant="contained" variant="contained"
color={canSave ? 'success' : 'inherit'}
disabled={busy || !canSave} disabled={busy || !canSave}
startIcon={busy ? <CircularProgress size={16} color="inherit" /> : null} startIcon={busy ? <CircularProgress size={16} color="inherit" /> : null}
sx={
canSave
? { bgcolor: 'success.main', color: '#fff', '&:hover': { bgcolor: 'success.dark' } }
: undefined
}
> >
Speichern Speichern
</Button> </Button>

View File

@@ -292,7 +292,7 @@ export default class VfsTextEditor extends PureComponent {
<Button <Button
size="small" size="small"
variant="contained" variant="contained"
color={isDirty ? 'primary' : 'inherit'} color={isDirty ? 'success' : 'inherit'}
startIcon={isSaving ? <CircularProgress size={14} color="inherit" /> : <SaveRoundedIcon />} startIcon={isSaving ? <CircularProgress size={14} color="inherit" /> : <SaveRoundedIcon />}
onClick={onSave} onClick={onSave}
disabled={!isDirty || isSaving} disabled={!isDirty || isSaving}
@@ -302,6 +302,11 @@ export default class VfsTextEditor extends PureComponent {
py: 0.4, py: 0.4,
fontSize: '0.78rem', fontSize: '0.78rem',
fontWeight: 600, fontWeight: 600,
...(isDirty && {
bgcolor: 'success.main',
color: '#fff',
'&:hover': { bgcolor: 'success.dark' },
}),
}} }}
> >
{isSaving ? 'Speichern…' : 'Speichern (Strg+S)'} {isSaving ? 'Speichern…' : 'Speichern (Strg+S)'}

View File

@@ -14,6 +14,7 @@ import { backendErrorFrom } from '../utils/backendError';
import { notifyError } from '../utils/notify'; import { notifyError } from '../utils/notify';
import entityAutomat from '../automats/entityAutomat'; import entityAutomat from '../automats/entityAutomat';
import customergroupAutomat from '../automats/customergroupAutomat'; import customergroupAutomat from '../automats/customergroupAutomat';
import { getLowestSortNonSupplierCustomerGroup } from '../components/manifest/manifestConfig';
import { Kunde } from '../components/kunde'; import { Kunde } from '../components/kunde';
import { BLUE_PANEL } from '../colors'; import { BLUE_PANEL } from '../colors';
@@ -70,7 +71,7 @@ class KundeSearchField extends PureComponent {
try { try {
await customergroupAutomat.actions.ensureLoaded(); await customergroupAutomat.actions.ensureLoaded();
const groups = customergroupAutomat.state.items ?? []; const groups = customergroupAutomat.state.items ?? [];
const defaultGroup = groups.find((g) => g.editable !== false) ?? groups[0]; const defaultGroup = getLowestSortNonSupplierCustomerGroup(groups) ?? groups[0];
if (!defaultGroup) { if (!defaultGroup) {
throw new Error('Keine Kundengruppe vorhanden.'); throw new Error('Keine Kundengruppe vorhanden.');
} }

View File

@@ -579,14 +579,28 @@ class KundeEditPage extends PureComponent {
onChange={(next) => this.setState({ firstName: next })} onChange={(next) => this.setState({ firstName: next })}
/> />
<Box sx={{ display: 'flex', justifyContent: 'flex-end' }}> <Box sx={{ display: 'flex', justifyContent: 'flex-end' }}>
<Button {(() => {
variant="contained" const canSaveEntity =
size="small" Boolean(name.trim()) &&
disabled={savingEntity || !name.trim()} ((name.trim() !== (entity?.name ?? '').trim()) ||
onClick={() => void this.handleSaveEntity()} (firstName.trim() !== (entity?.firstName ?? '').trim()));
> return (
{savingEntity ? 'Speichern…' : 'Speichern'} <Button
</Button> variant="contained"
size="small"
color={canSaveEntity ? 'success' : 'inherit'}
disabled={savingEntity || !canSaveEntity}
onClick={() => void this.handleSaveEntity()}
sx={
canSaveEntity
? { bgcolor: 'success.main', color: '#fff', '&:hover': { bgcolor: 'success.dark' } }
: undefined
}
>
{savingEntity ? 'Speichern…' : 'Speichern'}
</Button>
);
})()}
</Box> </Box>
</Stack> </Stack>
</PanelTile> </PanelTile>
@@ -600,6 +614,10 @@ class KundeEditPage extends PureComponent {
) : ( ) : (
addresses.map((address) => { addresses.map((address) => {
const draft = addressDrafts[address.id] ?? addressDraftFrom(address); const draft = addressDrafts[address.id] ?? addressDraftFrom(address);
const orig = addressDraftFrom(address);
const isAddressDirty = Object.keys(orig).some(
(k) => String(draft[k] ?? '').trim() !== String(orig[k] ?? '').trim(),
);
const busy = busyAddressKey === address.id; const busy = busyAddressKey === address.id;
return ( return (
<Box key={address.id}> <Box key={address.id}>
@@ -626,9 +644,15 @@ class KundeEditPage extends PureComponent {
</IconButton> </IconButton>
<Button <Button
size="small" size="small"
variant="outlined" variant={isAddressDirty ? 'contained' : 'outlined'}
disabled={addressBusy} color={isAddressDirty ? 'success' : 'inherit'}
disabled={addressBusy || !isAddressDirty}
onClick={() => void this.handleSaveAddress(address)} onClick={() => void this.handleSaveAddress(address)}
sx={
isAddressDirty
? { bgcolor: 'success.main', color: '#fff', '&:hover': { bgcolor: 'success.dark' } }
: undefined
}
> >
{busy ? '…' : 'Speichern'} {busy ? '…' : 'Speichern'}
</Button> </Button>
@@ -674,6 +698,9 @@ class KundeEditPage extends PureComponent {
contactType: contact.contactType, contactType: contact.contactType,
contact: contact.contact, contact: contact.contact,
}; };
const isContactDirty =
String(draft.contact ?? '').trim() !== String(contact.contact ?? '').trim() ||
String(draft.contactType ?? '').trim() !== String(contact.contactType ?? '').trim();
const busy = busyContactKey === contact.id; const busy = busyContactKey === contact.id;
return ( return (
<Stack <Stack
@@ -713,9 +740,15 @@ class KundeEditPage extends PureComponent {
</IconButton> </IconButton>
<Button <Button
size="small" size="small"
variant="outlined" variant={isContactDirty ? 'contained' : 'outlined'}
disabled={contactBusy} color={isContactDirty ? 'success' : 'inherit'}
disabled={contactBusy || !isContactDirty}
onClick={() => void this.handleSaveContact(contact)} onClick={() => void this.handleSaveContact(contact)}
sx={
isContactDirty
? { bgcolor: 'success.main', color: '#fff', '&:hover': { bgcolor: 'success.dark' } }
: undefined
}
> >
{busy ? '…' : 'Speichern'} {busy ? '…' : 'Speichern'}
</Button> </Button>

View File

@@ -11,6 +11,7 @@ import { PanelTile, PanelTileGrid } from 'dashgui/panels';
import { entity as entityApi } from '../../api'; import { entity as entityApi } from '../../api';
import entityAutomat from '../../automats/entityAutomat'; import entityAutomat from '../../automats/entityAutomat';
import customergroupAutomat from '../../automats/customergroupAutomat'; import customergroupAutomat from '../../automats/customergroupAutomat';
import { getLowestSortNonSupplierCustomerGroup } from '../../components/manifest/manifestConfig';
import { router } from '../../appRouter'; import { router } from '../../appRouter';
import { requireDefined, requireObject } from '../../utils/apiShape'; import { requireDefined, requireObject } from '../../utils/apiShape';
import { notifyError } from '../../utils/notify'; import { notifyError } from '../../utils/notify';
@@ -45,9 +46,10 @@ class KundeNeuPage extends PureComponent {
constructor(props) { constructor(props) {
super(props); super(props);
const groups = customergroupAutomat.state.items ?? []; const groups = customergroupAutomat.state.items ?? [];
const defaultGroup = getLowestSortNonSupplierCustomerGroup(groups) ?? groups[0];
const prefill = getInitialPrefill(props); const prefill = getInitialPrefill(props);
this.state = { this.state = {
customergroupId: groups.length > 0 ? groups[0].id : null, customergroupId: defaultGroup ? defaultGroup.id : null,
number: '', number: '',
name: prefill.name, name: prefill.name,
firstName: prefill.firstName, firstName: prefill.firstName,
@@ -59,6 +61,14 @@ class KundeNeuPage extends PureComponent {
componentDidMount() { componentDidMount() {
void this.loadNumber(); void this.loadNumber();
if (this.state.customergroupId == null) {
void customergroupAutomat.actions.ensureLoaded().then((groups) => {
const defaultGroup = getLowestSortNonSupplierCustomerGroup(groups) ?? groups?.[0];
if (defaultGroup && this.state.customergroupId == null) {
this.setState({ customergroupId: defaultGroup.id });
}
});
}
} }
async loadNumber() { async loadNumber() {
@@ -168,8 +178,14 @@ class KundeNeuPage extends PureComponent {
<Button <Button
variant="contained" variant="contained"
size="small" size="small"
disabled={!canSave} color={canSave ? 'success' : 'inherit'}
disabled={!canSave || saving}
onClick={() => void this.handleSave()} onClick={() => void this.handleSave()}
sx={
canSave
? { bgcolor: 'success.main', color: '#fff', '&:hover': { bgcolor: 'success.dark' } }
: undefined
}
> >
{saving ? 'Speichern…' : 'Speichern'} {saving ? 'Speichern…' : 'Speichern'}
</Button> </Button>

View File

@@ -37,7 +37,8 @@ import {
ArticleSearchDialog, ArticleSearchDialog,
BookDeliveryDialog, BookDeliveryDialog,
} from '../../components/manifest'; } from '../../components/manifest';
import { loadParties, apiFor, resolveLineSkus } from '../../components/manifest/manifestConfig'; import { loadParties, apiFor, resolveLineSkus, sortSuppliers } from '../../components/manifest/manifestConfig';
import supplierAutomat from '../../automats/supplierAutomat';
import warehouseAutomat from '../../automats/warehouseAutomat'; import warehouseAutomat from '../../automats/warehouseAutomat';
import { article as articleApi, delivery as deliveryApi } from '../../api'; import { article as articleApi, delivery as deliveryApi } from '../../api';
import { getCategoryState } from '../../automats/categoryAutomat'; import { getCategoryState } from '../../automats/categoryAutomat';
@@ -108,6 +109,12 @@ class LieferungNeuPage extends PureComponent {
super(props); super(props);
this.cfg = manifestConfig('delivery'); this.cfg = manifestConfig('delivery');
this.automat = getDeliveryEditorAutomat(props.docId); this.automat = getDeliveryEditorAutomat(props.docId);
if (
supplierAutomat.state.status === 'ready' &&
(!this.automat.state.deliveryEntities || this.automat.state.deliveryEntities.length === 0)
) {
this.automat.actions.setDeliveryEntities(sortSuppliers(supplierAutomat.state.suppliers));
}
const initialDate = this.automat.state.date || (props.docId == null ? todayInputValue() : null); const initialDate = this.automat.state.date || (props.docId == null ? todayInputValue() : null);
if (props.docId == null && !this.automat.state.date) { if (props.docId == null && !this.automat.state.date) {
this.automat.actions.setDate(initialDate); this.automat.actions.setDate(initialDate);
@@ -181,6 +188,11 @@ class LieferungNeuPage extends PureComponent {
this._warehouseUnsub = warehouseAutomat.subscribe(this, (state) => ({ this._warehouseUnsub = warehouseAutomat.subscribe(this, (state) => ({
warehouses: state.warehouses, warehouses: state.warehouses,
})); }));
this._supplierUnsub = supplierAutomat.subscribe(this, (state) => {
if (state.status === 'ready' && Array.isArray(state.suppliers)) {
this.automat.actions.setDeliveryEntities(sortSuppliers(state.suppliers));
}
});
void warehouseAutomat.actions.load().catch(() => { }); void warehouseAutomat.actions.load().catch(() => { });
this._cancelled = false; this._cancelled = false;
if (this.props.docId != null) { if (this.props.docId != null) {
@@ -202,6 +214,7 @@ class LieferungNeuPage extends PureComponent {
this.unsubscribe = this.automat.subscribe(this); this.unsubscribe = this.automat.subscribe(this);
this.setState({ ...this.automat.state }); this.setState({ ...this.automat.state });
this._initialLines = null; this._initialLines = null;
this._initialDoc = null;
if (this.props.docId != null) { if (this.props.docId != null) {
void this.loadEdit(); void this.loadEdit();
} else { } else {
@@ -225,6 +238,7 @@ class LieferungNeuPage extends PureComponent {
this.unsubscribe?.(); this.unsubscribe?.();
this._settingsUnsub?.(); this._settingsUnsub?.();
this._warehouseUnsub?.(); this._warehouseUnsub?.();
this._supplierUnsub?.();
} }
async loadPartiesOnly() { async loadPartiesOnly() {
@@ -283,11 +297,21 @@ class LieferungNeuPage extends PureComponent {
if (doc.externalNumber != null) this.automat.actions.setExternalNumber(String(doc.externalNumber)); if (doc.externalNumber != null) this.automat.actions.setExternalNumber(String(doc.externalNumber));
this.automat.actions.setLineitems(lines); this.automat.actions.setLineitems(lines);
this._initialLines = lines; this._initialLines = lines;
this._initialDoc = {
date: dateInputValue(doc.date),
partyId: doc[cfg.partyField] ?? null,
externalNumber: doc.externalNumber != null ? String(doc.externalNumber) : '',
lines,
attachments: [],
};
// Restore attachments with thumbnails // Restore attachments with thumbnails
const apiFiles = doc.files ?? []; const apiFiles = doc.files ?? [];
const attachments = await restoreAttachmentsFromApi(apiFiles, this.state.attachments); const attachments = await restoreAttachmentsFromApi(apiFiles, this.state.attachments);
this.automat.actions.setAttachments(attachments); this.automat.actions.setAttachments(attachments);
if (this._initialDoc) {
this._initialDoc.attachments = attachments;
}
let ledgerSummary = null; let ledgerSummary = null;
try { try {
@@ -1173,6 +1197,14 @@ class LieferungNeuPage extends PureComponent {
const attachments = await restoreAttachmentsFromApi(apiFiles, this.state.attachments); const attachments = await restoreAttachmentsFromApi(apiFiles, this.state.attachments);
this.automat.actions.setAttachments(attachments); this.automat.actions.setAttachments(attachments);
this._initialDoc = {
date: dateInputValue(doc.date),
partyId: doc[cfg.partyField] ?? null,
externalNumber: doc.externalNumber != null ? String(doc.externalNumber) : '',
lines,
attachments,
};
enqueueSnackbar(cfg.savedMsg, { variant: 'success' }); enqueueSnackbar(cfg.savedMsg, { variant: 'success' });
this.setState({ saving: false }); this.setState({ saving: false });
} }
@@ -1246,6 +1278,27 @@ class LieferungNeuPage extends PureComponent {
} }
} }
const isEdit = this.props.docId != null;
let isDirty = false;
if (isEdit && this._initialDoc) {
const dateChanged = String(date ?? '').trim() !== String(this._initialDoc.date ?? '').trim();
const partyChanged = (partyId ?? null) !== (this._initialDoc.partyId ?? null);
const extChanged = String(externalNumber ?? '').trim() !== String(this._initialDoc.externalNumber ?? '').trim();
const linesChanged = !areLinesEqual(lines, this._initialDoc.lines);
const currentAtts = attachments || [];
const baseAtts = this._initialDoc.attachments || [];
let attsChanged = currentAtts.length !== baseAtts.length;
if (!attsChanged) {
for (const a of currentAtts) {
if (!a.isPersisted || a.isDirty) {
attsChanged = true;
break;
}
}
}
isDirty = dateChanged || partyChanged || extChanged || linesChanged || attsChanged;
}
return ( return (
<> <>
<PanelTileGrid> <PanelTileGrid>
@@ -1266,6 +1319,7 @@ class LieferungNeuPage extends PureComponent {
onPdfCreate={this.handleCreatePdf} onPdfCreate={this.handleCreatePdf}
attachments={attachments} attachments={attachments}
lines={lines} lines={lines}
isDirty={isDirty}
onPdfUpload={this.handlePdfUpload} onPdfUpload={this.handlePdfUpload}
onPdfRescan={this.handlePdfRescan} onPdfRescan={this.handlePdfRescan}
onPdfDelete={this.handlePdfDelete} onPdfDelete={this.handlePdfDelete}