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:
parent
5886e1b269
commit
c4a82e96fd
5 changed files with 66 additions and 18 deletions
|
|
@ -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():
|
||||||
|
|
|
||||||
|
|
@ -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 -->
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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 => {
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue