From 313ead77f439df03db0d8506b5573d591ae9b925 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 6 Oct 2026 09:11:05 +0000 Subject: [PATCH] Rechtliches: Anzeige-Texte der Vertragsliste als Lib mit Test Co-Authored-By: Claude Sonnet 5.5 --- src/lib/legalView.js | 53 +++++++++++++++++++++++++++++++++++ tests/unit/legalView.test.mjs | 43 ++++++++++++++++++++++++++++ 2 files changed, 96 insertions(+) create mode 100644 src/lib/legalView.js create mode 100644 tests/unit/legalView.test.mjs diff --git a/src/lib/legalView.js b/src/lib/legalView.js new file mode 100644 index 0000000..e51aaa6 --- /dev/null +++ b/src/lib/legalView.js @@ -0,0 +1,53 @@ +// Anzeige-Texte der Vertragsliste im Bereich Rechtliches (reine Funktionen, ohne React). +// Die Rechenlogik der Fristen steht in legal.js; hier nur Beschriftung, Badge und Daempfung. +import { deadlineTone, formatDate } from './legal' + +const euro = new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }) + +export function costLabel(c) { + if (c.cost === null || c.cost === undefined) return '–' + return `${euro.format(c.cost)}/${c.costInterval === 'year' ? 'Jahr' : 'Monat'}` +} + +export function termLabel(c) { + if (c.termMonths === null || c.termMonths === undefined) return '–' + if (c.termMonths === 0) return 'jederzeit kündbar' + const renew = c.renewalMonths ? `, dann je ${c.renewalMonths} Monate` : ', endet danach' + return `${c.termMonths} Monate${renew}` +} + +export function noticeLabel(c) { + if (c.noticeValue === null || c.noticeValue === undefined) return '–' + return `${c.noticeValue} ${c.noticeUnit === 'days' ? 'Tage' : 'Monate'}` +} + +export function deadlineLabel(r) { + switch (r.state) { + case 'active': return `kündbar bis ${formatDate(r.noticeBy)} (Periode endet ${formatDate(r.periodEnd)})` + case 'anytime': return 'jederzeit kündbar' + case 'ends': return `endet am ${formatDate(r.periodEnd)}` + case 'ended': return `beendet seit ${formatDate(r.periodEnd)}` + case 'cancelled': return r.periodEnd ? `gekündigt zum ${formatDate(r.periodEnd)}` : 'gekündigt' + default: return 'Fristen fehlen' + } +} + +/** + * Darstellung der Frist-Spalte: text (voller Satz), lead und rest (Aufteilung, wenn ein + * Badge gesetzt ist, sonst lead = text und rest leer), badge ('danger' bei rot, 'warn' bei + * orange, sonst null) und muted (Vertrag beendet, gekuendigt oder unvollstaendig). + */ +export function deadlineView(result, today) { + const tone = deadlineTone(result, today) + const text = deadlineLabel(result) + const badge = tone === 'red' ? 'danger' : tone === 'orange' ? 'warn' : null + const muted = tone === 'grey' || tone === 'missing' + if (!badge) return { text, lead: text, rest: '', badge, muted } + return { + text, + lead: `kündbar bis ${formatDate(result.noticeBy)}`, + rest: `(Periode endet ${formatDate(result.periodEnd)})`, + badge, + muted, + } +} diff --git a/tests/unit/legalView.test.mjs b/tests/unit/legalView.test.mjs new file mode 100644 index 0000000..cbaf926 --- /dev/null +++ b/tests/unit/legalView.test.mjs @@ -0,0 +1,43 @@ +import test from 'node:test' +import assert from 'node:assert/strict' +import { loadModule } from './load.mjs' + +const { costLabel, termLabel, noticeLabel, deadlineView } = await loadModule('src/lib/legalView.js') +const NB = ' ' +const today = '2026-09-30' +const active = (noticeBy, periodEnd = '2027-03-12') => ({ state: 'active', noticeBy, periodEnd }) +const v = (r) => deadlineView(r, today) + +test('Vertragsliste: Kosten, Laufzeit, Frist wie bisher', () => { + assert.equal(costLabel({ cost: null }), '–') + assert.equal(costLabel({ cost: 12.5, costInterval: 'month' }), `12,50${NB}€/Monat`) + assert.equal(costLabel({ cost: 120, costInterval: 'year' }), `120,00${NB}€/Jahr`) + assert.equal(termLabel({}), '–') + assert.equal(termLabel({ termMonths: 0 }), 'jederzeit kündbar') + assert.equal(termLabel({ termMonths: 12, renewalMonths: 12 }), '12 Monate, dann je 12 Monate') + assert.equal(termLabel({ termMonths: 24, renewalMonths: 0 }), '24 Monate, endet danach') + assert.equal(noticeLabel({}), '–') + assert.equal(noticeLabel({ noticeValue: 1, noticeUnit: 'months' }), '1 Monate') + assert.equal(noticeLabel({ noticeValue: 14, noticeUnit: 'days' }), '14 Tage') +}) + +test('deadlineView: Badge nur bei rot und orange, Text wörtlich wie bisher', () => { + assert.deepEqual(v(active('2026-10-15')), { + text: 'kündbar bis 15.10.2026 (Periode endet 12.03.2027)', + lead: 'kündbar bis 15.10.2026', rest: '(Periode endet 12.03.2027)', badge: 'danger', muted: false, + }) + assert.equal(v(active('2026-10-30')).badge, 'danger') // 30 Tage + assert.equal(v(active('2026-10-31')).badge, 'warn') // 31 Tage + assert.equal(v(active('2026-12-29')).badge, 'warn') // 90 Tage + const normal = v(active('2026-12-30')) // 91 Tage + assert.deepEqual([normal.badge, normal.lead === normal.text, normal.rest], [null, true, '']) +}) + +test('deadlineView: Text und Dämpfung der übrigen Zustände', () => { + assert.deepEqual([v({ state: 'anytime' }).text, v({ state: 'anytime' }).muted], ['jederzeit kündbar', false]) + assert.equal(v({ state: 'ends', periodEnd: '2027-01-31' }).text, 'endet am 31.01.2027') + assert.deepEqual([v({ state: 'ended', periodEnd: '2026-01-31' }).text, v({ state: 'ended', periodEnd: '2026-01-31' }).muted], ['beendet seit 31.01.2026', true]) + assert.equal(v({ state: 'cancelled', periodEnd: '2027-01-31' }).text, 'gekündigt zum 31.01.2027') + assert.equal(v({ state: 'cancelled', periodEnd: null }).text, 'gekündigt') + assert.deepEqual([v({ state: 'incomplete' }).text, v({ state: 'incomplete' }).muted], ['Fristen fehlen', true]) +})