Feat: Entwurf bearbeiten (PATCH), erneut senden; SW by-v968
This commit is contained in:
parent
a2d089bce4
commit
b14a251bdc
5 changed files with 96 additions and 11 deletions
|
|
@ -408,7 +408,7 @@ async def serve_media(path: str, request: _Request):
|
|||
raise _HE(404, "Nicht gefunden.")
|
||||
return _media_response(filepath)
|
||||
|
||||
APP_VER = "967" # muss mit APP_VER in app.js übereinstimmen
|
||||
APP_VER = "968" # muss mit APP_VER in app.js übereinstimmen
|
||||
|
||||
@app.get("/.well-known/assetlinks.json")
|
||||
async def assetlinks():
|
||||
|
|
|
|||
|
|
@ -444,6 +444,58 @@ def get_invoice(invoice_id: int, admin=Depends(require_admin)):
|
|||
return result
|
||||
|
||||
|
||||
@router.patch("/{invoice_id}")
|
||||
def update_invoice(invoice_id: int, data: InvoiceCreate, admin=Depends(require_admin)):
|
||||
with db() as conn:
|
||||
row = conn.execute("SELECT * FROM invoices WHERE id=?", (invoice_id,)).fetchone()
|
||||
if not row:
|
||||
raise HTTPException(404, "Rechnung nicht gefunden.")
|
||||
if row["status"] != "draft":
|
||||
raise HTTPException(400, "Nur Entwürfe können bearbeitet werden.")
|
||||
if not data.items:
|
||||
raise HTTPException(400, "Mindestens eine Position erforderlich.")
|
||||
|
||||
KLEINUNTERNEHMER = os.getenv("KLEINUNTERNEHMER", "true").lower() == "true"
|
||||
TAX_RATE = 0.0 if KLEINUNTERNEHMER else float(os.getenv("RECHNUNG_MWST", "19"))
|
||||
|
||||
amount_net = round(sum(i.quantity * i.unit_price for i in data.items), 2)
|
||||
discount_pct = data.discount_pct or 0.0
|
||||
discount_amount = round(amount_net * discount_pct / 100, 2)
|
||||
amount_after_discount = round(amount_net - discount_amount, 2)
|
||||
tax_amount = round(amount_after_discount * TAX_RATE / 100, 2)
|
||||
amount_gross = round(amount_after_discount + tax_amount, 2)
|
||||
description = data.items[0].description if len(data.items) == 1 else f"{len(data.items)} Positionen"
|
||||
|
||||
conn.execute("""
|
||||
UPDATE invoices SET
|
||||
recipient_name=?, recipient_email=?, recipient_address=?,
|
||||
description=?, service_period=?,
|
||||
amount_net=?, discount_pct=?, discount_amount=?,
|
||||
amount_after_discount=?, tax_rate=?, tax_amount=?, amount_gross=?,
|
||||
notes=?
|
||||
WHERE id=?
|
||||
""", (
|
||||
data.recipient_name, data.recipient_email, data.recipient_address,
|
||||
description, data.service_period,
|
||||
amount_net, discount_pct, discount_amount,
|
||||
amount_after_discount, TAX_RATE, tax_amount, amount_gross,
|
||||
data.notes, invoice_id,
|
||||
))
|
||||
conn.execute("DELETE FROM invoice_items WHERE invoice_id=?", (invoice_id,))
|
||||
for item in data.items:
|
||||
total = round(item.quantity * item.unit_price, 2)
|
||||
conn.execute(
|
||||
"INSERT INTO invoice_items (invoice_id, description, quantity, unit_price, total) VALUES (?,?,?,?,?)",
|
||||
(invoice_id, item.description, item.quantity, item.unit_price, total)
|
||||
)
|
||||
row = conn.execute("SELECT * FROM invoices WHERE id=?", (invoice_id,)).fetchone()
|
||||
items = _fetch_items(conn, invoice_id)
|
||||
|
||||
result = _row_to_dict(row)
|
||||
result["items"] = items
|
||||
return result
|
||||
|
||||
|
||||
@router.post("", status_code=201)
|
||||
def create_invoice(data: InvoiceCreate, admin=Depends(require_admin)):
|
||||
if not data.items:
|
||||
|
|
@ -501,6 +553,8 @@ async def send_invoice(invoice_id: int, admin=Depends(require_admin)):
|
|||
raise HTTPException(404, "Rechnung nicht gefunden.")
|
||||
if row["status"] == "cancelled":
|
||||
raise HTTPException(400, "Stornierte Rechnung kann nicht gesendet werden.")
|
||||
if row["status"] == "paid":
|
||||
raise HTTPException(400, "Bezahlte Rechnung kann nicht erneut gesendet werden.")
|
||||
items = _fetch_items(conn, invoice_id)
|
||||
|
||||
invoice = _row_to_dict(row)
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
Router, State-Management, Navigation, Initialisierung.
|
||||
============================================================ */
|
||||
|
||||
const APP_VER = '967'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
|
||||
const APP_VER = '968'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
|
||||
const APP_VERSION = '1.6.0'; // ← semantische Version, wird bei make release gesetzt
|
||||
const IS_STAGING = location.hostname === 'staging.banyaro.app';
|
||||
// Cache-Bust-Parameter nach Update-Reload sofort entfernen
|
||||
|
|
|
|||
|
|
@ -3726,10 +3726,19 @@ window.Page_admin = (() => {
|
|||
const rows = invoices.map((inv, i) => {
|
||||
const actions = [];
|
||||
if (inv.status === 'draft') {
|
||||
actions.push(`<button class="btn btn-sm btn-ghost adm-inv-edit" data-id="${inv.id}" title="Bearbeiten">
|
||||
${UI.icon('pencil')} Bearbeiten
|
||||
</button>`);
|
||||
actions.push(`<button class="btn btn-sm btn-primary adm-inv-send" data-id="${inv.id}" data-num="${_esc(inv.invoice_number)}" title="Senden">
|
||||
${UI.icon('paper-plane-tilt')} Senden
|
||||
</button>`);
|
||||
}
|
||||
if (inv.status === 'sent') {
|
||||
actions.push(`<button class="btn btn-sm btn-ghost adm-inv-send" data-id="${inv.id}" data-num="${_esc(inv.invoice_number)}" title="Erneut senden"
|
||||
style="color:var(--c-text-muted)">
|
||||
${UI.icon('paper-plane-tilt')} Erneut senden
|
||||
</button>`);
|
||||
}
|
||||
if (inv.status === 'sent') {
|
||||
actions.push(`<button class="btn btn-sm btn-secondary adm-inv-pay" data-id="${inv.id}" data-amount="${inv.amount_gross}" title="Als bezahlt markieren">
|
||||
${UI.icon('check-circle')} Bezahlt
|
||||
|
|
@ -3808,6 +3817,22 @@ window.Page_admin = (() => {
|
|||
});
|
||||
});
|
||||
|
||||
// Entwurf bearbeiten
|
||||
el.querySelectorAll('.adm-inv-edit').forEach(btn => {
|
||||
btn.addEventListener('click', async () => {
|
||||
const inv = await API.get(`/admin/invoices/${btn.dataset.id}`);
|
||||
_openNeueRechnungModal(reload, {
|
||||
recipient_name: inv.recipient_name,
|
||||
recipient_email: inv.recipient_email,
|
||||
recipient_address: inv.recipient_address || '',
|
||||
service_period: inv.service_period || '',
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
// Als bezahlt markieren
|
||||
el.querySelectorAll('.adm-inv-pay').forEach(btn => {
|
||||
btn.addEventListener('click', () => _openBezahltModal(btn.dataset.id, Number(btn.dataset.amount), reload));
|
||||
|
|
@ -3824,12 +3849,13 @@ window.Page_admin = (() => {
|
|||
});
|
||||
}
|
||||
|
||||
function _openNeueRechnungModal(reload, prefill = null) {
|
||||
function _openNeueRechnungModal(reload, prefill = null, invoiceId = null) {
|
||||
const id = `inv-new-${Date.now()}`;
|
||||
const p = prefill || {};
|
||||
const isEdit = !!invoiceId;
|
||||
|
||||
UI.modal.open({
|
||||
title: `${UI.icon('receipt')} Neue Rechnung erstellen`,
|
||||
title: `${UI.icon('receipt')} ${isEdit ? 'Rechnung bearbeiten' : 'Neue Rechnung erstellen'}`,
|
||||
body: `
|
||||
<form id="${id}" style="display:flex;flex-direction:column;gap:var(--space-3)">
|
||||
|
||||
|
|
@ -3904,7 +3930,7 @@ window.Page_admin = (() => {
|
|||
`,
|
||||
footer: `
|
||||
<button class="btn btn-secondary" data-modal-close>Abbrechen</button>
|
||||
<button class="btn btn-primary" form="${id}" type="submit">${UI.icon('receipt')} Rechnung erstellen</button>
|
||||
<button class="btn btn-primary" form="${id}" type="submit">${UI.icon('receipt')} ${isEdit ? 'Änderungen speichern' : 'Rechnung erstellen'}</button>
|
||||
`,
|
||||
});
|
||||
|
||||
|
|
@ -3988,7 +4014,7 @@ window.Page_admin = (() => {
|
|||
if (submitBtn) submitBtn.disabled = true;
|
||||
|
||||
try {
|
||||
await API.post('/admin/invoices', {
|
||||
const payload = {
|
||||
recipient_name: fd.get('recipient_name'),
|
||||
recipient_email: fd.get('recipient_email') || null,
|
||||
recipient_address: fd.get('recipient_address') || null,
|
||||
|
|
@ -3996,9 +4022,14 @@ window.Page_admin = (() => {
|
|||
discount_pct: parseFloat(fd.get('discount_pct')) || 0,
|
||||
notes: fd.get('notes') || null,
|
||||
items,
|
||||
});
|
||||
};
|
||||
if (isEdit) {
|
||||
await API.patch(`/admin/invoices/${invoiceId}`, payload);
|
||||
} else {
|
||||
await API.post('/admin/invoices', payload);
|
||||
}
|
||||
UI.modal.close();
|
||||
UI.toast.success('Rechnung erstellt.');
|
||||
UI.toast.success(isEdit ? 'Rechnung gespeichert.' : 'Rechnung erstellt.');
|
||||
reload();
|
||||
} catch (err) {
|
||||
UI.toast.error(err.message || 'Fehler beim Erstellen.');
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
Offline-Cache + Push Notifications + Tile-Cache
|
||||
============================================================ */
|
||||
|
||||
const CACHE_VERSION = 'by-v967';
|
||||
const CACHE_VERSION = 'by-v968';
|
||||
const CACHE_STATIC = `${CACHE_VERSION}-static`;
|
||||
const CACHE_TILES = 'ban-yaro-tiles-v1'; // bleibt über SW-Updates erhalten
|
||||
const CACHE_API = 'ban-yaro-api-v1'; // API-Response-Cache
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue