UX: Rechnungs-Edit-Modal status-bewusst + Storno-Button, SW by-v1075

- _openNeueRechnungModal akzeptiert jetzt status+invoice_number; je
  nach Status anderes Verhalten:
  • draft   → bearbeitbar, Stornieren + Abbrechen + Speichern
  • sent    → readonly Banner, Stornieren + Schließen
  • paid    → readonly Banner, Stornieren + Schließen
  • neu     → Abbrechen + Erstellen (kein Storno-Slot)
- Footer-Layout: Stornieren links, Abbrechen/Speichern rechts
  (justify-content:space-between → symmetrisch)
- Stornieren öffnet den existierenden _openStornoModal; das Edit-
  Modal wird durch UI.modal.open() automatisch geschlossen
- Submit-Handler ignoriert locked-Status (Schutz auch wenn Button
  irgendwie sichtbar wäre)
- Upgrades-Tab + Rechnungen-Tab geben jetzt inv.status+invoice_number
  beim Öffnen mit; Reload-Callback aus Upgrades-Tab rendert den Tab
  neu, damit nach Stornierung der gelbe Button zurück auf orange geht
This commit is contained in:
rene 2026-05-26 13:59:40 +02:00
parent 5886e1b269
commit c4a82e96fd
5 changed files with 66 additions and 18 deletions

View file

@ -3715,7 +3715,7 @@ window.Page_admin = (() => {
try {
const inv = await API.get(`/admin/invoices/${btn.dataset.invoiceId}`);
_openNeueRechnungModal(() => {
_tab = 'rechnungen';
// Nach Speichern/Stornieren: zurück auf Upgrades-Tab, damit der Button neu rendert
_renderTab();
}, {
recipient_name: inv.recipient_name,
@ -3725,7 +3725,7 @@ window.Page_admin = (() => {
discount_pct: inv.discount_pct || 0,
notes: inv.notes || '',
items: inv.items.map(it => ({ description: it.description, quantity: it.quantity, unit_price: it.unit_price })),
}, inv.id);
}, inv.id, inv.status, inv.invoice_number);
} catch (e) {
UI.toast.error(e.message || 'Rechnung konnte nicht geladen werden.');
}
@ -3923,7 +3923,7 @@ window.Page_admin = (() => {
discount_pct: inv.discount_pct || 0,
notes: inv.notes || '',
items: inv.items.map(it => ({ description: it.description, quantity: it.quantity, unit_price: it.unit_price })),
}, inv.id);
}, inv.id, inv.status, inv.invoice_number);
});
});
@ -3943,16 +3943,31 @@ window.Page_admin = (() => {
});
}
function _openNeueRechnungModal(reload, prefill = null, invoiceId = null) {
function _openNeueRechnungModal(reload, prefill = null, invoiceId = null, status = null, invoiceNumber = null) {
const id = `inv-new-${Date.now()}`;
const p = prefill || {};
const isEdit = !!invoiceId;
const isEdit = !!invoiceId;
const isLocked = isEdit && status && status !== 'draft'; // sent / paid / cancelled → nicht änderbar
const canCancel = isEdit && status !== 'cancelled'; // alles außer schon storniert
const lockedBanner = isLocked ? (() => {
const map = {
sent: ['#fff8f0', '#f0a060', '#c05000', 'Diese Rechnung wurde bereits versendet — Inhalt nicht mehr änderbar. Korrekturen nur per Stornierung.'],
paid: ['#f0fdf4', '#86efac', '#15803d', 'Diese Rechnung ist bezahlt — Inhalt nicht änderbar.'],
cancelled: ['#fef2f2', '#fca5a5', '#b91c1c', 'Diese Rechnung wurde storniert — Inhalt nicht änderbar.'],
};
const [bg, br, fg, msg] = map[status] || ['#f5f5f5', '#ccc', '#444', `Status: ${status}`];
return `<div style="padding:var(--space-2) var(--space-3);border-radius:var(--radius-md);
background:${bg};border:1px solid ${br};
font-size:var(--text-xs);color:${fg};line-height:1.6">${msg}</div>`;
})() : '';
UI.modal.open({
title: `${UI.icon('receipt')} ${isEdit ? 'Rechnung bearbeiten' : 'Neue Rechnung erstellen'}`,
title: `${UI.icon('receipt')} ${isEdit ? (isLocked ? 'Rechnung ansehen' : 'Rechnung bearbeiten') : 'Neue Rechnung erstellen'}`,
body: `
<form id="${id}" style="display:flex;flex-direction:column;gap:var(--space-3)">
${lockedBanner}
${!isEdit && !p.recipient_name ? `
<div style="padding:var(--space-2) var(--space-3);border-radius:var(--radius-md);
background:#fff8f0;border:1px solid #f0a060;
@ -4031,11 +4046,43 @@ window.Page_admin = (() => {
</form>
`,
footer: `
<button class="btn btn-secondary" data-modal-close>Abbrechen</button>
<button class="btn btn-primary" form="${id}" type="submit">${UI.icon('receipt')} ${isEdit ? 'Änderungen speichern' : 'Rechnung erstellen'}</button>
<div style="display:flex;align-items:center;justify-content:space-between;gap:var(--space-2);width:100%;flex-wrap:wrap">
<div>
${canCancel ? `
<button class="btn btn-ghost" id="${id}-cancel-invoice"
style="color:var(--c-danger);border:1px solid var(--c-danger)">
${UI.icon('x-circle')} Stornieren
</button>` : ''}
</div>
<div style="display:flex;gap:var(--space-2)">
<button class="btn btn-secondary" data-modal-close>${isLocked ? 'Schließen' : 'Abbrechen'}</button>
${!isLocked ? `<button class="btn btn-primary" form="${id}" type="submit">
${UI.icon('receipt')} ${isEdit ? 'Änderungen speichern' : 'Rechnung erstellen'}
</button>` : ''}
</div>
</div>
`,
});
// Bei gesperrten Rechnungen (sent/paid/cancelled) alle Eingaben & Action-Buttons readonly
if (isLocked) {
setTimeout(() => {
const form = document.getElementById(id);
if (!form) return;
form.querySelectorAll('input, textarea').forEach(inp => { inp.disabled = true; });
// "+ Position hinzufügen" und Item-Lösch-Buttons verstecken
const addBtn = document.getElementById(`${id}-add-item`);
if (addBtn) addBtn.style.display = 'none';
form.querySelectorAll('.inv-item-remove').forEach(b => { b.style.display = 'none'; });
}, 0);
}
// Stornieren — schließt dieses Modal, öffnet den Storno-Dialog
document.getElementById(`${id}-cancel-invoice`)?.addEventListener('click', () => {
// _openStornoModal ruft intern UI.modal.open() → schließt dieses Modal automatisch
_openStornoModal(invoiceId, invoiceNumber || `#${invoiceId}`, reload);
});
// Items-Container und Hilfsfunktionen
const itemsContainer = document.getElementById(`${id}-items`);
const previewEl = document.getElementById(`${id}-preview`);
@ -4100,6 +4147,7 @@ window.Page_admin = (() => {
// Form Submit
document.getElementById(id)?.addEventListener('submit', async e => {
e.preventDefault();
if (isLocked) return; // gesperrte Rechnung — Submit ignorieren (Button ist eh ausgeblendet)
const fd = new FormData(e.target);
const items = [];
itemsContainer.querySelectorAll('.adm-inv-item-row').forEach(row => {