diff --git a/src/components/category/CategoryNameDialog.jsx b/src/components/category/CategoryNameDialog.jsx index 4bde4cb..8e5d045 100644 --- a/src/components/category/CategoryNameDialog.jsx +++ b/src/components/category/CategoryNameDialog.jsx @@ -113,8 +113,9 @@ class CategoryNameDialog extends Component { const { open, mode = 'create' } = this.props; const { mode: formMode, name, bulkText, busy, error } = this.state; const isBulk = formMode === BULK_MODE; - const canBulk = mode === 'create'; - const canSubmitSingle = name.trim() !== ''; + const canSubmitSingle = + name.trim() !== '' && + (mode !== 'edit' || name.trim() !== (this.props.initialName ?? '').trim()); const bulkNames = parseBulkNames(bulkText); const canSubmitBulk = bulkNames.length > 0; @@ -157,7 +158,13 @@ class CategoryNameDialog extends Component { diff --git a/src/components/category/CategoryTree.jsx b/src/components/category/CategoryTree.jsx index b1bebae..0d43f96 100644 --- a/src/components/category/CategoryTree.jsx +++ b/src/components/category/CategoryTree.jsx @@ -749,13 +749,27 @@ export default class CategoryTree extends PureComponent { > Abbrechen - + {(() => { + const canSave = + Boolean(renameValue.trim()) && + !isSubmitting && + renameValue.trim() !== (renameTarget?.name ?? '').trim(); + return ( + + ); + })()} diff --git a/src/components/manifest/ManifestHeader.jsx b/src/components/manifest/ManifestHeader.jsx index f7862da..2b60184 100644 --- a/src/components/manifest/ManifestHeader.jsx +++ b/src/components/manifest/ManifestHeader.jsx @@ -272,6 +272,11 @@ class ManifestHeader extends PureComponent { } = this.props; const hasArticles = this.hasArticles(); + const isEdit = docId != null; + const hasAttachments = Array.isArray(attachments) && attachments.length > 0; + const canSave = isEdit + ? Boolean(isDirty) + : Boolean(hasArticles || hasAttachments); // Delivery: number is server-allocated/read-only; the caller reference // lives in the editable externalNumber field. @@ -447,20 +452,33 @@ class ManifestHeader extends PureComponent { {isDirty && onRestore && ( diff --git a/src/components/settings/CompanySettingsPanel.jsx b/src/components/settings/CompanySettingsPanel.jsx index f3747f8..d0be55b 100644 --- a/src/components/settings/CompanySettingsPanel.jsx +++ b/src/components/settings/CompanySettingsPanel.jsx @@ -52,11 +52,17 @@ export default class CompanySettingsPanel extends Component { syncFormFromCompany(companyRow) { if (!companyRow) return; - this.setState({ + this._initial = { name: companyRow.name ?? '', iso639: companyRow.iso639 ?? 'de', taxrate: companyRow.taxrate != null ? String(companyRow.taxrate) : '', 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', }); } @@ -100,6 +106,12 @@ export default class CompanySettingsPanel extends Component { taxrate: parsedTaxrate, reducedtaxrate: parsedReducedtaxrate, }); + this._initial = { + name: name.trim(), + iso639, + taxrate: String(parsedTaxrate), + reducedtaxrate: String(parsedReducedtaxrate), + }; enqueueSnackbar('Unternehmen gespeichert.', { variant: 'success' }); } catch (err) { this.setState({ error: backendErrorFrom(err) }); @@ -125,7 +137,14 @@ export default class CompanySettingsPanel extends Component { const parsedTaxrate = parseTaxRate(taxrate); 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 = + isDirty && name.trim() !== '' && ISO639_OPTIONS.includes(iso639) && parsedTaxrate != null && @@ -218,8 +237,14 @@ export default class CompanySettingsPanel extends Component { diff --git a/src/components/vfs/VfsTextEditor.jsx b/src/components/vfs/VfsTextEditor.jsx index e36ddef..7a46ec6 100644 --- a/src/components/vfs/VfsTextEditor.jsx +++ b/src/components/vfs/VfsTextEditor.jsx @@ -292,7 +292,7 @@ export default class VfsTextEditor extends PureComponent { + {(() => { + const canSaveEntity = + Boolean(name.trim()) && + ((name.trim() !== (entity?.name ?? '').trim()) || + (firstName.trim() !== (entity?.firstName ?? '').trim())); + return ( + + ); + })()} @@ -600,6 +614,10 @@ class KundeEditPage extends PureComponent { ) : ( addresses.map((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; return ( @@ -626,9 +644,15 @@ class KundeEditPage extends PureComponent { @@ -674,6 +698,9 @@ class KundeEditPage extends PureComponent { contactType: contact.contactType, 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; return ( diff --git a/src/routes/kunde/neu.jsx b/src/routes/kunde/neu.jsx index 729abb9..e7e3ef4 100644 --- a/src/routes/kunde/neu.jsx +++ b/src/routes/kunde/neu.jsx @@ -178,8 +178,14 @@ class KundeNeuPage extends PureComponent { diff --git a/src/routes/lieferung/neu.jsx b/src/routes/lieferung/neu.jsx index 0090711..94cb989 100644 --- a/src/routes/lieferung/neu.jsx +++ b/src/routes/lieferung/neu.jsx @@ -214,6 +214,7 @@ class LieferungNeuPage extends PureComponent { this.unsubscribe = this.automat.subscribe(this); this.setState({ ...this.automat.state }); this._initialLines = null; + this._initialDoc = null; if (this.props.docId != null) { void this.loadEdit(); } else { @@ -296,11 +297,21 @@ class LieferungNeuPage extends PureComponent { if (doc.externalNumber != null) this.automat.actions.setExternalNumber(String(doc.externalNumber)); this.automat.actions.setLineitems(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 const apiFiles = doc.files ?? []; const attachments = await restoreAttachmentsFromApi(apiFiles, this.state.attachments); this.automat.actions.setAttachments(attachments); + if (this._initialDoc) { + this._initialDoc.attachments = attachments; + } let ledgerSummary = null; try { @@ -1186,6 +1197,14 @@ class LieferungNeuPage extends PureComponent { const attachments = await restoreAttachmentsFromApi(apiFiles, this.state.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' }); this.setState({ saving: false }); } @@ -1259,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 ( <> @@ -1279,6 +1319,7 @@ class LieferungNeuPage extends PureComponent { onPdfCreate={this.handleCreatePdf} attachments={attachments} lines={lines} + isDirty={isDirty} onPdfUpload={this.handlePdfUpload} onPdfRescan={this.handlePdfRescan} onPdfDelete={this.handlePdfDelete}