Kundenansicht neu P2: Zeile Rechnung offen
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
b77c3291c5
commit
3c44aaea77
2 files changed
+58
No files matched your search
@@ -0,0 +1,36 @@
|
||||
/* Kundenansicht neu (P2): Zeile "Rechnung offen" für Website und Funktionen. Offen heißt wie überall Number(balance) > 0 (V3).
|
||||
Fällt der Abruf aus, fehlt nur diese Zeile. */
|
||||
|
||||
import { api } from './api.js'
|
||||
import { h } from './dom.js'
|
||||
import { fmtEuro, safeUrl, zuAnsicht } from './ansicht-hilfen.js'
|
||||
|
||||
/** Rechnungen mit offenem Betrag, in der Reihenfolge der Antwort von GET /api/invoices. */
|
||||
export function offeneRechnungen(antwort) {
|
||||
const liste = Array.isArray(antwort?.invoices) ? antwort.invoices : []
|
||||
return liste.filter((r) => Number(r?.balance) > 0)
|
||||
}
|
||||
|
||||
/** Lädt GET /api/invoices und liefert die offenen Rechnungen; jeder Fehler ergibt eine leere Liste. */
|
||||
export async function ladeOffeneRechnungen(apiFn = api) {
|
||||
try {
|
||||
return offeneRechnungen(await apiFn('/api/invoices'))
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
/** Die Zeile für die offenen Rechnungen; ohne offene Rechnung null. Eine Rechnung mit sicherem Link führt dorthin, sonst zu den Rechnungen. */
|
||||
export function rechnungOffenZeile(offen) {
|
||||
if (!Array.isArray(offen) || !offen.length) return null
|
||||
const summe = Math.round(offen.reduce((s, r) => s + (Number(r.balance) || 0), 0) * 100) / 100
|
||||
const einzel = offen.length === 1 ? offen[0] : null
|
||||
const url = einzel ? safeUrl(einzel.portalUrl) : ''
|
||||
const link = url
|
||||
? h('a', { href: url, target: '_blank', rel: 'noopener noreferrer' }, 'Zahlen')
|
||||
: h('a', zuAnsicht('invoices'), 'Zahlen')
|
||||
const teile = einzel
|
||||
? ['Rechnung offen', einzel.number, fmtEuro(summe)]
|
||||
: [`${offen.length} Rechnungen offen`, fmtEuro(summe)]
|
||||
return h('p', { class: 'kn-rechnung-offen' }, `${teile.filter(Boolean).join(' · ')} · `, link)
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { test } from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { offeneRechnungen, ladeOffeneRechnungen, rechnungOffenZeile } from '../public/neu/rechnung-offen.js'
|
||||
import { mit, mitDom, rechnung } from './support/knDaten.js'
|
||||
|
||||
const NB = ' '
|
||||
test('eine offene Rechnung: Text und Link auf die Rechnung', () => mitDom((fx) => {
|
||||
const p = rechnungOffenZeile([rechnung({ number: 'RE-2026-0042', balance: 50, portalUrl: 'https://invoice.stripe.com/i/x' })])
|
||||
assert.equal(p.textContent, `Rechnung offen · RE-2026-0042 · 50,00${NB}€ · Zahlen`)
|
||||
const a = fx.finde(p, 'a'); assert.equal(a.getAttribute('href'), 'https://invoice.stripe.com/i/x'); assert.equal(a.getAttribute('rel'), 'noopener noreferrer')
|
||||
}))
|
||||
test('mehrere offene: Summe, Link über die Brücke zu #rechnungen; unsicherer Link fällt darauf zurück', () => mitDom((fx) => {
|
||||
const zwei = [rechnung({ balance: 100 }), rechnung({ balance: 50 })]
|
||||
assert.equal(rechnungOffenZeile(zwei).textContent, `2 Rechnungen offen · 150,00${NB}€ · Zahlen`)
|
||||
const a = fx.finde(rechnungOffenZeile(zwei), 'a'); assert.deepEqual([a.getAttribute('href'), a.getAttribute('data-view')], ['#rechnungen', 'invoices'])
|
||||
assert.equal(fx.finde(rechnungOffenZeile([rechnung({ balance: 5, portalUrl: 'javascript:alert(1)' })]), 'a').getAttribute('href'), '#rechnungen')
|
||||
assert.equal(rechnungOffenZeile([]), null)
|
||||
}))
|
||||
test('offeneRechnungen filtert nach Betrag, ladeOffeneRechnungen schluckt Fehler', mit({ 'GET /api/invoices': () => ({ status: 500, body: { error: 'x' } }) }, async () => {
|
||||
assert.deepEqual(offeneRechnungen({ invoices: [rechnung({ balance: 0 }), rechnung({ id: 'b', balance: 1 })] }).map((r) => r.id), ['b'])
|
||||
assert.deepEqual(await ladeOffeneRechnungen(), [])
|
||||
}))
|
||||
Reference in new issue
Block a user