Rechnungen: Stripe-Rechnungen im Kundenportal anzeigen (Abo, Funktionen, Projektrechnungen)

Der Tab "Rechnungen" las nur InvoiceNinja. Seit dem Finanzen-Livegang laufen Rechnungen
aber ueber Stripe; die bezahlte Abo-Rechnung eines Kunden fehlte deshalb im Portal.

- /api/invoices fuehrt Stripe (alle Stripe-Kunden des Kunden: Abo-Spiegel und Suche nach
  metadata.webklarCustomerId) und InvoiceNinja zusammen, neueste zuerst; Entwuerfe und
  0-EUR-Rechnungen bleiben verborgen. Faellt eine Quelle aus, kommt der Rest mit incomplete.
- /api/invoices/:id/pdf laedt Stripe-PDFs frisch ueber den Server, nur fuer eigene Rechnungen
  (sonst 404) und nur von *.stripe.com.
- Seite: Status-Farben fuer Bezahlt/Offen/Ueberfaellig/Storniert, "Ansehen" statt
  "Ansehen / Zahlen" bei bezahlten Rechnungen, Hinweis bei unvollstaendiger Liste; der
  Abo-Hinweis verweist nur noch fuer die Zahlungsart auf "Abo verwalten & Rechnungen".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Opus 5.5 committed 2026-10-02 09:33:22 +00:00
1 parent df8b952b81
commit ff7fe35c06
6 files changed
+606 -34

No files matched your search

+16 -12
View File
@@ -588,41 +588,45 @@ async function loadCustomerInvoices() {
const body = document.getElementById('invoices-body')
if (!body) return
try {
// Stripe (Abo, Funktionen, Projektrechnungen) und InvoiceNinja (Altbestand), neueste zuerst
const data = await api('/api/invoices')
const invoices = data.invoices || []
// Zaehler in der Seitenleiste: Rechnungen mit offenem Betrag
const openCount = (data.invoices || []).filter((i) => Number(i.balance) > 0).length
const openCount = invoices.filter((i) => Number(i.balance) > 0).length
const badge = document.getElementById('invoices-badge')
if (badge) {
badge.textContent = openCount
badge.classList.toggle('hidden', !openCount)
}
if (!data.linked) {
body.innerHTML = '<p class="muted">Für dein Konto sind noch keine Rechnungen hinterlegt.</p>'
return
}
if (!data.invoices.length) {
body.innerHTML = '<p class="muted">Es liegen noch keine Rechnungen vor.</p>'
// Eine der Quellen hat nicht geantwortet: sagen, statt Rechnungen still wegzulassen
const gap = data.incomplete
? '<p class="kb-note">Deine Rechnungen konnten gerade nicht vollständig geladen werden. Bitte versuche es später noch einmal.</p>'
: ''
if (!invoices.length) {
body.innerHTML = gap || '<p class="muted">Es liegen noch keine Rechnungen vor.</p>'
return
}
const pill = (i) => (i.overdue ? 'pill-red' : i.paid ? 'pill-ok' : i.cancelled ? 'pill-muted' : 'pill-warn')
const hasPdf = (i) => i.hasPdf !== false
// Klassen je Spalte: in der App-Ansicht wird jede Zeile zur Karte (kundenbereich.css)
body.innerHTML = `
<div class="kb-table-wrap"><table class="simple-table kb-invoices">
<thead><tr><th>Nr.</th><th>Datum</th><th>Betrag</th><th>Offen</th><th>Status</th><th></th></tr></thead>
<tbody>
${data.invoices.map((i) => `
${invoices.map((i) => `
<tr class="${Number(i.balance) > 0 ? 'is-open' : 'is-paid'}">
<td class="inv-no">${escapeHtml(i.number)}</td>
<td class="inv-date">${fmtDateShort(i.date)}</td>
<td class="inv-amount">${fmtEuro(i.amount)}</td>
<td class="inv-open">${fmtEuro(i.balance)}</td>
<td class="inv-status"><span class="pill ${i.overdue ? 'pill-red' : i.statusId === 4 ? 'pill-ok' : 'pill-warn'}">${escapeHtml(i.status)}</span></td>
<td class="inv-status"><span class="pill ${pill(i)}">${escapeHtml(i.status)}</span></td>
<td class="inv-actions" style="white-space:nowrap">
${i.portalUrl ? `<a class="btn-link" href="${escapeAttr(i.portalUrl)}" target="_blank" rel="noreferrer">Ansehen / Zahlen</a><span class="inv-sep" aria-hidden="true"> · </span>` : ''}
<a class="btn-link" href="/api/invoices/${escapeAttr(i.id)}/pdf">PDF</a>
${i.portalUrl ? `<a class="btn-link" href="${escapeAttr(i.portalUrl)}" target="_blank" rel="noreferrer">${Number(i.balance) > 0 ? 'Ansehen / Zahlen' : 'Ansehen'}</a>${hasPdf(i) ? '<span class="inv-sep" aria-hidden="true"> · </span>' : ''}` : ''}
${hasPdf(i) ? `<a class="btn-link" href="/api/invoices/${encodeURIComponent(i.id)}/pdf">PDF</a>` : ''}
</td>
</tr>`).join('')}
</tbody>
</table></div>`
</table></div>${gap}`
} catch (err) {
body.innerHTML = `<p class="muted">Rechnungen konnten nicht geladen werden: ${escapeHtml(err.message)}</p>`
}
+1 -1
View File
@@ -192,7 +192,7 @@
<p>Deine Rechnungen ansehen, bezahlen und herunterladen.</p>
</header>
<div class="kb-panel" id="invoices-body"><p class="kb-empty">Lade…</p></div>
<p class="kb-note hidden" id="invoices-abo-note">Rechnungen zum Wartungs-Abo findest du über dein <button type="button" class="kb-link" data-open-account>Profilbild</button> oben rechts unter „Abo verwalten &amp; Rechnungen“.</p>
<p class="kb-note hidden" id="invoices-abo-note">Die Zahlungsart für dein Abo änderst du über dein <button type="button" class="kb-link" data-open-account>Profilbild</button> oben rechts unter „Abo verwalten &amp; Rechnungen“.</p>
</section>
<!-- Chat -->