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 && (
}
onClick={this.handleSave}
disabled={loading || !isDirty}
+ sx={
+ isDirty
+ ? { bgcolor: 'success.main', color: '#fff', '&:hover': { bgcolor: 'success.dark' } }
+ : undefined
+ }
>
Speichern
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 {
: null}
+ sx={
+ canSave
+ ? { bgcolor: 'success.main', color: '#fff', '&:hover': { bgcolor: 'success.dark' } }
+ : undefined
+ }
>
Speichern
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 {
: }
onClick={onSave}
disabled={!isDirty || isSaving}
@@ -302,6 +302,11 @@ export default class VfsTextEditor extends PureComponent {
py: 0.4,
fontSize: '0.78rem',
fontWeight: 600,
+ ...(isDirty && {
+ bgcolor: 'success.main',
+ color: '#fff',
+ '&:hover': { bgcolor: 'success.dark' },
+ }),
}}
>
{isSaving ? 'Speichern…' : 'Speichern (Strg+S)'}
diff --git a/src/routes/kunde/_entityId.jsx b/src/routes/kunde/_entityId.jsx
index 77c6b86..c3f09e7 100644
--- a/src/routes/kunde/_entityId.jsx
+++ b/src/routes/kunde/_entityId.jsx
@@ -579,14 +579,28 @@ class KundeEditPage extends PureComponent {
onChange={(next) => this.setState({ firstName: next })}
/>
-
+ {(() => {
+ 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}