Kundenportal Aufträge: Abschnitt im Reiter Rechnungen, Projekt unter der Rechnungsnummer
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
b53f9f67ed
commit
73c5446cf8
4 files changed
+95
-2
No files matched your search
+22
-1
@@ -584,6 +584,13 @@ function renderViewAsBanner(me) {
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------- Rechnungen --- */
|
||||
// @rechnungsnummer-anfang
|
||||
/** Rechnungsnummer mit dem Projekt darunter (Stripe-Metadaten projektName, Feld `projekt`); beides maskiert. */
|
||||
function invoiceNumberHtml(i) {
|
||||
const project = i.projekt ? `<span class="inv-project">${escapeHtml(i.projekt)}</span>` : ''
|
||||
return `${escapeHtml(i.number)}${project}`
|
||||
}
|
||||
// @rechnungsnummer-ende
|
||||
async function loadCustomerInvoices() {
|
||||
const body = document.getElementById('invoices-body')
|
||||
if (!body) return
|
||||
@@ -615,7 +622,7 @@ async function loadCustomerInvoices() {
|
||||
<tbody>
|
||||
${invoices.map((i) => `
|
||||
<tr class="${Number(i.balance) > 0 ? 'is-open' : 'is-paid'}">
|
||||
<td class="inv-no">${escapeHtml(i.number)}</td>
|
||||
<td class="inv-no">${invoiceNumberHtml(i)}</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>
|
||||
@@ -632,6 +639,19 @@ async function loadCustomerInvoices() {
|
||||
}
|
||||
}
|
||||
|
||||
// Abschnitt "Aufträge" im Reiter Rechnungen (public/auftrag-portal.js): Fehler lassen den Abschnitt weg und stören die Rechnungen nicht.
|
||||
function loadCustomerOrders() {
|
||||
return import('/auftrag-portal.js')
|
||||
.then((m) => m.initAuftraege({
|
||||
section: document.getElementById('orders-section'),
|
||||
body: document.getElementById('orders-body'),
|
||||
invoicesTitle: document.getElementById('invoices-title'),
|
||||
request: api,
|
||||
viewAs: chatState.viewAs,
|
||||
}))
|
||||
.catch(() => {})
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------- Aktivität --- */
|
||||
// Abschnitt "Was wir zuletzt erledigt haben" unter Projekte: je Auftrag eine Karte mit Status, Fortschrittsleiste
|
||||
// und Arbeiten (Bausteine der Projektdetails; Texte kommen aufbereitet vom Server). Zuerst die drei neuesten,
|
||||
@@ -2063,6 +2083,7 @@ async function initDashboardPage() {
|
||||
if (aboParam) window.history.replaceState(null, '', `/dashboard.html${window.location.hash}`)
|
||||
loadContracts({ afterCheckout: aboParam === 'success' })
|
||||
loadCustomerInvoices()
|
||||
loadCustomerOrders()
|
||||
activityScrollPending = currentHash() === 'fortschritt'
|
||||
loadCustomerActivity().then(scrollToActivity)
|
||||
|
||||
|
||||
+1
-1
@@ -149,7 +149,7 @@ body.ab-seite {
|
||||
.ab-portal .ab-details[open] > summary { margin-bottom: 4px; }
|
||||
.ab-portal .ab-knopf { border-radius: 999px; }
|
||||
.ab-rechnungen-titel { font-size: 20px; margin: 0 0 10px; }
|
||||
.inv-project { display: block; margin-top: 2px; font-size: 12px; font-weight: 400; color: var(--kb-muted, #6b6b70); overflow-wrap: anywhere; }
|
||||
.inv-project { display: block; margin-top: 2px; font-size: 12.5px; font-weight: 400; color: var(--kb-muted, rgba(255, 255, 255, 0.6)); overflow-wrap: anywhere; }
|
||||
|
||||
/* ------------------------------------------------------------------ Installierte App --- */
|
||||
.ab-app-back { display: none; }
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
<link rel="stylesheet" href="/termine.css">
|
||||
<link rel="stylesheet" href="/shop.css">
|
||||
<link rel="stylesheet" href="/zeiten.css">
|
||||
<link rel="stylesheet" href="/auftrag.css">
|
||||
</head>
|
||||
<body class="portal-page dashboard-page kb-page">
|
||||
<div class="dot-background" aria-hidden="true">
|
||||
@@ -222,6 +223,14 @@
|
||||
<h1>Rechnungen</h1>
|
||||
<p>Deine Rechnungen ansehen, bezahlen und herunterladen.</p>
|
||||
</header>
|
||||
<!-- Aufträge: Auftragsbestätigungen ansehen und bestätigen (auftrag-portal.js, von app.js loadCustomerOrders); erscheint nur,
|
||||
wenn Aufträge freigeschaltet sind und der Kunde welche hat -->
|
||||
<section class="ab-portal hidden" id="orders-section" aria-labelledby="orders-title">
|
||||
<h2 id="orders-title">Aufträge</h2>
|
||||
<p class="kb-note">Deine Auftragsbestätigungen. Offene Aufträge bestätigst du hier mit einem Klick.</p>
|
||||
<div id="orders-body"></div>
|
||||
</section>
|
||||
<h2 class="ab-rechnungen-titel hidden" id="invoices-title">Rechnungen</h2>
|
||||
<div class="kb-panel" id="invoices-body"><p class="kb-empty">Lade…</p></div>
|
||||
<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 & Rechnungen“.</p>
|
||||
</section>
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
import { test } from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import vm from 'node:vm'
|
||||
|
||||
const lies = (pfad) => readFileSync(new URL(`../${pfad}`, import.meta.url), 'utf8')
|
||||
const html = lies('public/dashboard.html')
|
||||
const app = lies('public/app.js')
|
||||
const css = lies('public/auftrag.css')
|
||||
|
||||
test('dashboard.html: auftrag.css nach zeiten.css; Abschnitt „Aufträge“ vor der Rechnungsliste, zunächst versteckt', () => {
|
||||
assert.match(html, /<link rel="stylesheet" href="\/zeiten\.css">\s*<link rel="stylesheet" href="\/auftrag\.css">/)
|
||||
const start = html.indexOf('id="view-invoices"')
|
||||
const ende = html.indexOf('</section>', html.indexOf('id="invoices-abo-note"'))
|
||||
const ansicht = html.slice(start, ende)
|
||||
assert.match(ansicht, /<section class="ab-portal hidden" id="orders-section" aria-labelledby="orders-title">\s*<h2 id="orders-title">Aufträge<\/h2>[\s\S]*<div id="orders-body"><\/div>\s*<\/section>\s*<h2 class="ab-rechnungen-titel hidden" id="invoices-title">Rechnungen<\/h2>\s*<div class="kb-panel" id="invoices-body">/)
|
||||
assert.match(ansicht, /<p class="kb-note">Deine Auftragsbestätigungen\. Offene Aufträge bestätigst du hier mit einem Klick\.<\/p>/, 'Portal: Du-Form')
|
||||
assert.ok(ansicht.indexOf('id="orders-section"') < ansicht.indexOf('id="invoices-body"'))
|
||||
assert.equal([...html.matchAll(/id="orders-section"/g)].length, 1)
|
||||
assert.match(ansicht, /<h1>Rechnungen<\/h1>/, 'Kopf und Hinweis des Reiters bleiben')
|
||||
})
|
||||
|
||||
test('app.js: loadCustomerOrders lädt auftrag-portal.js nach der Rechnungsliste, mit api und Ansichtsmodus; Fehler bleiben still', () => {
|
||||
const funktion = app.slice(app.indexOf('function loadCustomerOrders()'), app.indexOf('/* ---', app.indexOf('function loadCustomerOrders()')))
|
||||
assert.ok(funktion.length > 40, 'Funktion gefunden')
|
||||
assert.match(funktion, /import\('\/auftrag-portal\.js'\)/)
|
||||
assert.match(funktion, /m\.initAuftraege\(/)
|
||||
for (const teil of ["section: document.getElementById('orders-section')", "body: document.getElementById('orders-body')", "invoicesTitle: document.getElementById('invoices-title')", 'request: api', 'viewAs: chatState.viewAs']) {
|
||||
assert.ok(funktion.includes(teil), teil)
|
||||
}
|
||||
assert.match(funktion, /\.catch\(\(\) => \{\}\)/)
|
||||
const dashboard = app.slice(app.indexOf('async function initDashboardPage'))
|
||||
const aufruf = (name) => dashboard.search(new RegExp(`^\\s*${name}\\(\\)\\s*$`, 'm'))
|
||||
assert.ok(aufruf('loadCustomerInvoices') > 0 && aufruf('loadCustomerOrders') > aufruf('loadCustomerInvoices'), 'ein eigener Aufruf, nach der Rechnungsliste')
|
||||
assert.equal([...app.matchAll(/function loadCustomerOrders\(\)/g)].length, 1)
|
||||
})
|
||||
|
||||
test('invoiceNumberHtml: Projekt unter der Rechnungsnummer, alles maskiert', () => {
|
||||
const ausschnitt = (muster) => {
|
||||
const treffer = app.match(muster)
|
||||
assert.ok(treffer, `${muster} nicht in app.js`)
|
||||
return treffer[0]
|
||||
}
|
||||
const code = [
|
||||
ausschnitt(/function escapeHtml\(str\) \{[\s\S]*?\n\}/),
|
||||
ausschnitt(/\/\/ @rechnungsnummer-anfang[\s\S]*?\/\/ @rechnungsnummer-ende/),
|
||||
].join('\n')
|
||||
const { invoiceNumberHtml } = vm.runInNewContext(`${code}\n({ invoiceNumberHtml })`)
|
||||
assert.equal(invoiceNumberHtml({ number: 'WEBKLAR-0009', projekt: 'Website Anelia Wolf' }), 'WEBKLAR-0009<span class="inv-project">Website Anelia Wolf</span>')
|
||||
assert.equal(invoiceNumberHtml({ number: 'WEBKLAR-0009', projekt: '' }), 'WEBKLAR-0009')
|
||||
assert.equal(invoiceNumberHtml({ number: 'H96T-0004' }), 'H96T-0004')
|
||||
assert.equal(
|
||||
invoiceNumberHtml({ number: 'X', projekt: '<img src=x onerror=alert(1)> & "Bäckerei"' }),
|
||||
'X<span class="inv-project"><img src=x onerror=alert(1)> & "Bäckerei"</span>',
|
||||
)
|
||||
assert.match(app, /<td class="inv-no">\$\{invoiceNumberHtml\(i\)\}<\/td>/)
|
||||
assert.doesNotMatch(app, /\$\{i\.projekt\}/, 'der Projektname geht nie unmaskiert in eine Vorlage')
|
||||
})
|
||||
|
||||
test('auftrag.css: Projektname unter der Rechnungsnummer (Tabelle und Handy-Karte)', () => {
|
||||
assert.match(css, /\.inv-project\s*\{[^}]*display:\s*block/)
|
||||
assert.match(css, /\.inv-project\s*\{[^}]*overflow-wrap:\s*anywhere/)
|
||||
})
|
||||
Reference in new issue
Block a user