Feat: Rechnungsadresse — Profil, Upgrade-Modal Hinweis, Rechnung-erstellen-Button in Upgrade-Cards (SW by-v967)
This commit is contained in:
parent
95b70d5119
commit
0a466ef6ce
9 changed files with 89 additions and 20 deletions
|
|
@ -2444,6 +2444,12 @@ def _migrate(conn_factory):
|
|||
except Exception as e:
|
||||
logger.warning(f"Migration invoices: {e}")
|
||||
|
||||
try:
|
||||
conn.execute("ALTER TABLE users ADD COLUMN billing_address TEXT")
|
||||
logger.info("Migration: billing_address bereit.")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def _seed_help_articles(conn):
|
||||
"""Befüllt help_articles mit Starter-FAQs — nur wenn die Tabelle noch leer ist."""
|
||||
|
|
|
|||
|
|
@ -408,7 +408,7 @@ async def serve_media(path: str, request: _Request):
|
|||
raise _HE(404, "Nicht gefunden.")
|
||||
return _media_response(filepath)
|
||||
|
||||
APP_VER = "966" # muss mit APP_VER in app.js übereinstimmen
|
||||
APP_VER = "967" # muss mit APP_VER in app.js übereinstimmen
|
||||
|
||||
@app.get("/.well-known/assetlinks.json")
|
||||
async def assetlinks():
|
||||
|
|
|
|||
|
|
@ -1137,7 +1137,7 @@ async def list_upgrade_requests(user=Depends(require_admin)):
|
|||
with db() as conn:
|
||||
rows = conn.execute("""
|
||||
SELECT r.id, r.user_id, r.tier, r.message, r.created_at, r.fulfilled_at,
|
||||
u.name, u.email
|
||||
u.name, u.email, u.billing_address
|
||||
FROM upgrade_requests r
|
||||
JOIN users u ON u.id = r.user_id
|
||||
ORDER BY r.fulfilled_at IS NOT NULL, r.created_at DESC
|
||||
|
|
|
|||
|
|
@ -241,7 +241,8 @@ async def me(user=Depends(get_current_user)):
|
|||
is_founder, is_partner, founder_number, is_founder_pending,
|
||||
notes_ki_enabled, gassi_stunde_push,
|
||||
preferred_theme, subscription_tier,
|
||||
subscription_expires_at, subscription_cancelled_at, needs_dog_selection
|
||||
subscription_expires_at, subscription_cancelled_at, needs_dog_selection,
|
||||
billing_address
|
||||
FROM users WHERE id=?""",
|
||||
(user["id"],)
|
||||
).fetchone()
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ class ProfileUpdate(BaseModel):
|
|||
notes_ki_enabled: Optional[int] = None
|
||||
gassi_stunde_push: Optional[int] = None
|
||||
preferred_theme: Optional[str] = None
|
||||
billing_address: Optional[str] = None
|
||||
|
||||
|
||||
def _load_user(user_id: int) -> dict:
|
||||
|
|
@ -35,7 +36,7 @@ def _load_user(user_id: int) -> dict:
|
|||
row = conn.execute(
|
||||
"""SELECT id, name, real_name, email, rolle, is_premium, email_verified,
|
||||
bio, wohnort, erfahrung, social_link,
|
||||
profil_sichtbarkeit, avatar_url, created_at
|
||||
profil_sichtbarkeit, avatar_url, created_at, billing_address
|
||||
FROM users WHERE id=?""",
|
||||
(user_id,)
|
||||
).fetchone()
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
Router, State-Management, Navigation, Initialisierung.
|
||||
============================================================ */
|
||||
|
||||
const APP_VER = '966'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
|
||||
const APP_VER = '967'; // ← 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
|
||||
|
|
|
|||
|
|
@ -3540,12 +3540,22 @@ window.Page_admin = (() => {
|
|||
</div>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn adm-fulfill-btn" data-id="${r.id}" data-name="${_esc(r.name)}" data-tier="${r.tier}"
|
||||
style="width:100%;margin-top:var(--space-3);background:#16a34a;color:#fff;border:none;
|
||||
padding:var(--space-2) var(--space-3);border-radius:var(--radius-md);
|
||||
cursor:pointer;font-size:var(--text-sm);font-weight:600">
|
||||
✓ Freischalten
|
||||
</button>
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:var(--space-2);margin-top:var(--space-3)">
|
||||
<button class="btn adm-invoice-btn"
|
||||
data-name="${_esc(r.name)}" data-email="${_esc(r.email)}"
|
||||
data-tier="${r.tier}" data-address="${_esc(r.billing_address || '')}"
|
||||
style="background:#e67e22;color:#fff;border:none;
|
||||
padding:var(--space-2) var(--space-3);border-radius:var(--radius-md);
|
||||
cursor:pointer;font-size:var(--text-sm);font-weight:600">
|
||||
${UI.icon('receipt')} Rechnung erstellen
|
||||
</button>
|
||||
<button class="btn adm-fulfill-btn" data-id="${r.id}" data-name="${_esc(r.name)}" data-tier="${r.tier}"
|
||||
style="background:#16a34a;color:#fff;border:none;
|
||||
padding:var(--space-2) var(--space-3);border-radius:var(--radius-md);
|
||||
cursor:pointer;font-size:var(--text-sm);font-weight:600">
|
||||
✓ Freischalten
|
||||
</button>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
// Erledigte als kompakte Tabellenzeilen
|
||||
|
|
@ -3610,6 +3620,31 @@ window.Page_admin = (() => {
|
|||
}
|
||||
});
|
||||
});
|
||||
|
||||
// "Rechnung erstellen" — öffnet Invoice-Modal mit vorbefüllten Nutzerdaten
|
||||
const TIER_ITEMS = {
|
||||
pro: { description: 'Ban Yaro Pro Jahresabo', unit_price: 29.00 },
|
||||
breeder: { description: 'Ban Yaro Züchter Jahresabo', unit_price: 49.00 },
|
||||
};
|
||||
const _year = new Date().getFullYear();
|
||||
const _period = `01.01.${_year} – 31.12.${_year}`;
|
||||
|
||||
el.querySelectorAll('.adm-invoice-btn').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
const { name, email, tier, address } = btn.dataset;
|
||||
const tierItem = TIER_ITEMS[tier] || { description: 'Ban Yaro Abo', unit_price: 0 };
|
||||
_openNeueRechnungModal(() => {
|
||||
_tab = 'rechnungen';
|
||||
_renderTab();
|
||||
}, {
|
||||
recipient_name: name,
|
||||
recipient_email: email,
|
||||
recipient_address: address || '',
|
||||
service_period: _period,
|
||||
items: [{ description: tierItem.description, quantity: 1, unit_price: tierItem.unit_price }],
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
|
|
@ -3789,8 +3824,9 @@ window.Page_admin = (() => {
|
|||
});
|
||||
}
|
||||
|
||||
function _openNeueRechnungModal(reload) {
|
||||
function _openNeueRechnungModal(reload, prefill = null) {
|
||||
const id = `inv-new-${Date.now()}`;
|
||||
const p = prefill || {};
|
||||
|
||||
UI.modal.open({
|
||||
title: `${UI.icon('receipt')} Neue Rechnung erstellen`,
|
||||
|
|
@ -3801,25 +3837,32 @@ window.Page_admin = (() => {
|
|||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:var(--space-3)">
|
||||
<div>
|
||||
<label class="form-label" style="font-size:var(--text-xs)">Empfänger Name *</label>
|
||||
<input class="form-control" name="recipient_name" type="text" required placeholder="Max Muster">
|
||||
<input class="form-control" name="recipient_name" type="text" required
|
||||
placeholder="Max Muster" value="${_esc(p.recipient_name || '')}">
|
||||
</div>
|
||||
<div>
|
||||
<label class="form-label" style="font-size:var(--text-xs)">E-Mail</label>
|
||||
<input class="form-control" name="recipient_email" type="email" placeholder="max@example.com">
|
||||
<input class="form-control" name="recipient_email" type="email"
|
||||
placeholder="max@example.com" value="${_esc(p.recipient_email || '')}">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="form-label" style="font-size:var(--text-xs)">Adresse <span style="color:var(--c-text-muted)">(optional)</span></label>
|
||||
<label class="form-label" style="font-size:var(--text-xs)">Adresse
|
||||
${p.recipient_name && !p.recipient_address
|
||||
? `<span style="color:var(--c-warning);font-size:10px"> ⚠ Nutzer hat keine Rechnungsadresse hinterlegt</span>`
|
||||
: '<span style="color:var(--c-text-muted)">(optional)</span>'}
|
||||
</label>
|
||||
<textarea class="form-control" name="recipient_address" rows="2"
|
||||
placeholder="Musterstr. 1 12345 Berlin"
|
||||
style="resize:vertical;font-family:inherit"></textarea>
|
||||
style="resize:vertical;font-family:inherit">${_esc(p.recipient_address || '')}</textarea>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="form-label" style="font-size:var(--text-xs)">Leistungszeitraum <span style="color:var(--c-text-muted)">(optional)</span></label>
|
||||
<input class="form-control" name="service_period" type="text"
|
||||
placeholder="01.01.2026 – 31.12.2026">
|
||||
placeholder="01.01.2026 – 31.12.2026"
|
||||
value="${_esc(p.service_period || '')}">
|
||||
</div>
|
||||
|
||||
<!-- Positionen -->
|
||||
|
|
@ -3915,8 +3958,12 @@ window.Page_admin = (() => {
|
|||
`;
|
||||
}
|
||||
|
||||
// Erste Position hinzufügen
|
||||
_addItem('Ban Yaro Pro Jahresabo', 1, 29.00);
|
||||
// Erste Position — aus Prefill oder Standard
|
||||
if (p.items && p.items.length) {
|
||||
p.items.forEach(it => _addItem(it.description, it.quantity ?? 1, it.unit_price ?? 0));
|
||||
} else {
|
||||
_addItem('Ban Yaro Pro Jahresabo', 1, 29.00);
|
||||
}
|
||||
|
||||
// Weitere Position
|
||||
document.getElementById(`${id}-add-item`)?.addEventListener('click', () => _addItem());
|
||||
|
|
|
|||
|
|
@ -307,6 +307,12 @@ window.Page_settings = (() => {
|
|||
Wir schalten deinen Account manuell frei — innerhalb von 24 Stunden.
|
||||
Wir melden uns mit den Zahlungsdetails per E-Mail.
|
||||
</div>
|
||||
${!_appState.user?.billing_address ? `
|
||||
<div style="padding:var(--space-2) var(--space-3);border-radius:var(--radius-md);
|
||||
background:#fff8f0;border:1px solid #f0a060;
|
||||
font-size:var(--text-xs);color:#c05000;line-height:1.6;margin-top:var(--space-2)">
|
||||
💡 Tipp: Trag deine <strong>Rechnungsadresse</strong> im Profil ein — dann können wir die Rechnung vollständig ausstellen.
|
||||
</div>` : ''}
|
||||
${breederForm}
|
||||
</div>`,
|
||||
footer: `
|
||||
|
|
@ -1135,6 +1141,13 @@ window.Page_settings = (() => {
|
|||
value="${_esc(u.social_link || '')}"
|
||||
style="${inputStyle}">
|
||||
</div>
|
||||
<div style="border-top:1px solid var(--c-border);padding-top:var(--space-3);margin-top:var(--space-1)">
|
||||
<label style="display:block;font-size:var(--text-sm);font-weight:600;margin-bottom:2px">Rechnungsadresse</label>
|
||||
<div style="font-size:var(--text-xs);color:var(--c-text-muted);margin-bottom:var(--space-2)">Wird auf Rechnungen gedruckt. Straße in Zeile 1, PLZ + Ort in Zeile 2.</div>
|
||||
<textarea name="billing_address" rows="2" maxlength="200"
|
||||
placeholder="Musterstraße 1 12345 Berlin"
|
||||
style="${inputStyle};resize:vertical;font-family:inherit">${_esc(u.billing_address || '')}</textarea>
|
||||
</div>
|
||||
<div>
|
||||
<label style="display:block;font-size:var(--text-sm);font-weight:600;margin-bottom:var(--space-1)">Profil-Sichtbarkeit</label>
|
||||
<select name="profil_sichtbarkeit" style="${inputStyle}">${sichtbarkeitOpts}</select>
|
||||
|
|
@ -1161,6 +1174,7 @@ window.Page_settings = (() => {
|
|||
erfahrung: fd.erfahrung || '',
|
||||
social_link: fd.social_link || '',
|
||||
profil_sichtbarkeit: fd.profil_sichtbarkeit || 'public',
|
||||
billing_address: fd.billing_address || '',
|
||||
});
|
||||
Object.assign(_appState.user, updated);
|
||||
UI.modal.close?.();
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
Offline-Cache + Push Notifications + Tile-Cache
|
||||
============================================================ */
|
||||
|
||||
const CACHE_VERSION = 'by-v966';
|
||||
const CACHE_VERSION = 'by-v967';
|
||||
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