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

@ -410,7 +410,7 @@ async def serve_media(path: str, request: _Request):
raise _HE(404, "Nicht gefunden.") raise _HE(404, "Nicht gefunden.")
return _media_response(filepath) return _media_response(filepath)
APP_VER = "1074" # muss mit APP_VER in app.js übereinstimmen APP_VER = "1075" # muss mit APP_VER in app.js übereinstimmen
@app.get("/.well-known/assetlinks.json") @app.get("/.well-known/assetlinks.json")
async def assetlinks(): async def assetlinks():

View file

@ -101,9 +101,9 @@
</script> </script>
<!-- CSS: Reihenfolge ist wichtig — ?v= zwingt Browser zur Neuladung --> <!-- CSS: Reihenfolge ist wichtig — ?v= zwingt Browser zur Neuladung -->
<link rel="stylesheet" href="/css/design-system.css?v=1074"> <link rel="stylesheet" href="/css/design-system.css?v=1075">
<link rel="stylesheet" href="/css/layout.css?v=1074"> <link rel="stylesheet" href="/css/layout.css?v=1075">
<link rel="stylesheet" href="/css/components.css?v=1074"> <link rel="stylesheet" href="/css/components.css?v=1075">
</head> </head>
<body> <body>
@ -616,10 +616,10 @@
<div id="modal-container"></div> <div id="modal-container"></div>
<!-- JS: Reihenfolge ist wichtig — erst Basis, dann Features --> <!-- JS: Reihenfolge ist wichtig — erst Basis, dann Features -->
<script src="/js/api.js?v=1074"></script> <script src="/js/api.js?v=1075"></script>
<script src="/js/ui.js?v=1074"></script> <script src="/js/ui.js?v=1075"></script>
<script src="/js/app.js?v=1074"></script> <script src="/js/app.js?v=1075"></script>
<script src="/js/worlds.js?v=1074"></script> <script src="/js/worlds.js?v=1075"></script>
<!-- Feature-Seiten werden lazy geladen --> <!-- Feature-Seiten werden lazy geladen -->

View file

@ -3,7 +3,7 @@
Router, State-Management, Navigation, Initialisierung. Router, State-Management, Navigation, Initialisierung.
============================================================ */ ============================================================ */
const APP_VER = '1074'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen const APP_VER = '1075'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
const APP_VERSION = '1.6.0'; // ← semantische Version, wird bei make release gesetzt const APP_VERSION = '1.6.0'; // ← semantische Version, wird bei make release gesetzt
const IS_STAGING = location.hostname === 'staging.banyaro.app'; const IS_STAGING = location.hostname === 'staging.banyaro.app';
// Cache-Bust-Parameter nach Update-Reload sofort entfernen. // Cache-Bust-Parameter nach Update-Reload sofort entfernen.

View file

@ -3715,7 +3715,7 @@ window.Page_admin = (() => {
try { try {
const inv = await API.get(`/admin/invoices/${btn.dataset.invoiceId}`); const inv = await API.get(`/admin/invoices/${btn.dataset.invoiceId}`);
_openNeueRechnungModal(() => { _openNeueRechnungModal(() => {
_tab = 'rechnungen'; // Nach Speichern/Stornieren: zurück auf Upgrades-Tab, damit der Button neu rendert
_renderTab(); _renderTab();
}, { }, {
recipient_name: inv.recipient_name, recipient_name: inv.recipient_name,
@ -3725,7 +3725,7 @@ window.Page_admin = (() => {
discount_pct: inv.discount_pct || 0, discount_pct: inv.discount_pct || 0,
notes: inv.notes || '', notes: inv.notes || '',
items: inv.items.map(it => ({ description: it.description, quantity: it.quantity, unit_price: it.unit_price })), 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) { } catch (e) {
UI.toast.error(e.message || 'Rechnung konnte nicht geladen werden.'); UI.toast.error(e.message || 'Rechnung konnte nicht geladen werden.');
} }
@ -3923,7 +3923,7 @@ window.Page_admin = (() => {
discount_pct: inv.discount_pct || 0, discount_pct: inv.discount_pct || 0,
notes: inv.notes || '', notes: inv.notes || '',
items: inv.items.map(it => ({ description: it.description, quantity: it.quantity, unit_price: it.unit_price })), 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 id = `inv-new-${Date.now()}`;
const p = prefill || {}; 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({ 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: ` body: `
<form id="${id}" style="display:flex;flex-direction:column;gap:var(--space-3)"> <form id="${id}" style="display:flex;flex-direction:column;gap:var(--space-3)">
${lockedBanner}
${!isEdit && !p.recipient_name ? ` ${!isEdit && !p.recipient_name ? `
<div style="padding:var(--space-2) var(--space-3);border-radius:var(--radius-md); <div style="padding:var(--space-2) var(--space-3);border-radius:var(--radius-md);
background:#fff8f0;border:1px solid #f0a060; background:#fff8f0;border:1px solid #f0a060;
@ -4031,11 +4046,43 @@ window.Page_admin = (() => {
</form> </form>
`, `,
footer: ` footer: `
<button class="btn btn-secondary" data-modal-close>Abbrechen</button> <div style="display:flex;align-items:center;justify-content:space-between;gap:var(--space-2);width:100%;flex-wrap:wrap">
<button class="btn btn-primary" form="${id}" type="submit">${UI.icon('receipt')} ${isEdit ? 'Änderungen speichern' : 'Rechnung erstellen'}</button> <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 // Items-Container und Hilfsfunktionen
const itemsContainer = document.getElementById(`${id}-items`); const itemsContainer = document.getElementById(`${id}-items`);
const previewEl = document.getElementById(`${id}-preview`); const previewEl = document.getElementById(`${id}-preview`);
@ -4100,6 +4147,7 @@ window.Page_admin = (() => {
// Form Submit // Form Submit
document.getElementById(id)?.addEventListener('submit', async e => { document.getElementById(id)?.addEventListener('submit', async e => {
e.preventDefault(); e.preventDefault();
if (isLocked) return; // gesperrte Rechnung — Submit ignorieren (Button ist eh ausgeblendet)
const fd = new FormData(e.target); const fd = new FormData(e.target);
const items = []; const items = [];
itemsContainer.querySelectorAll('.adm-inv-item-row').forEach(row => { itemsContainer.querySelectorAll('.adm-inv-item-row').forEach(row => {

View file

@ -4,7 +4,7 @@
============================================================ */ ============================================================ */
// ← EINZIGE Stelle für die Version — STATIC_ASSETS und CACHE_VERSION leiten sich ab // ← EINZIGE Stelle für die Version — STATIC_ASSETS und CACHE_VERSION leiten sich ab
const VER = '1074'; const VER = '1075';
const CACHE_VERSION = `by-v${VER}`; const CACHE_VERSION = `by-v${VER}`;
const CACHE_STATIC = `${CACHE_VERSION}-static`; const CACHE_STATIC = `${CACHE_VERSION}-static`;
const CACHE_TILES = 'ban-yaro-tiles-v1'; // bleibt über SW-Updates erhalten const CACHE_TILES = 'ban-yaro-tiles-v1'; // bleibt über SW-Updates erhalten