diff --git a/CHANGELOG.md b/CHANGELOG.md index e494e39..1efaadf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ## [Unreleased] +### Added — Opkrævningsoverblik BBR-beregner +- New "Beregner" tab in the Opkrævningsoverblik prototype that calculates four BBR-driven municipal fees: rottebekæmpelse (with the >250 m² rate split), renovation, skorstensfejer, and ejendomsskat (grundskyld) +- Editable mock-BBR data (grundareal, grundværdi, bygninger med type/m²/ildsteder) per test user with per-field reset and live recalculation +- "Hvad-hvis" scenario chips: add carport (30 m²), install brændeovn, expand bolig (+25 m²), reset all +- "Se beregning →" link from each charge detail view that switches to the calculator and highlights the relevant card +- New `skorstensfejer` charge type added to the chargeTypes registry and to Anders/Lars test data + ### Added — Design System (v1) - Opt-in design system at `docs/public/design-system/v1/` — plain CSS + tokens using ITK Dev teal/cyan brand palette - Tokens for colors, typography (Inter), spacing, radii, shadows, transitions diff --git a/docs/projects/opkraevningsoverblik/index.md b/docs/projects/opkraevningsoverblik/index.md index fe462d1..080baad 100644 --- a/docs/projects/opkraevningsoverblik/index.md +++ b/docs/projects/opkraevningsoverblik/index.md @@ -10,10 +10,14 @@ Borgere i Aarhus Kommune modtager opkrævninger for en række kommunale ydelser — ejendomsskat, daginstitution, renovation m.fl. Disse oplysninger er i dag spredt over flere systemer og kanaler, hvilket gør det svært for borgere at få et samlet overblik over hvad de skylder, hvad der er betalt, og hvad der er på vej. +Borgerservice har desuden et behov for at borgeren kan se *hvordan* gebyrerne er beregnet — ikke blot beløbet. Det giver gennemsigtighed og gør det muligt at opdage fejl i fx BBR-registreringen. + ## Formål Formålet er at skabe ét samlet overblik hvor borgere hurtigt og intuitivt kan se alle deres kommunale opkrævninger — både den aktuelle status og udviklingen over tid. Overblikket skal skabe gennemsigtighed og gøre det nemt at forstå sin økonomiske situation i forhold til kommunen. +Hertil kommer en **beregningsfunktion** der ud fra BBR-træk viser hvordan kommunale bidrag fremkommer, og som lader borgeren simulere hvordan ændringer (tilbygning, ny brændeovn osv.) påvirker beløbet. + **Succeskriterium:** Borgeren kan danne sig et overblik over sin samlede status og eventuelle restancer på under ét minut — uden vejledning. --- @@ -49,6 +53,26 @@ Indikatoren bruger kombination af farve, ikon og tekst for tilgængelighed. - Mulighed for at se udviklingen per ydelsestype - Tydelig markering af perioder med restancer +### Beregningsfunktion (BBR) + +En ny "Beregner"-fane viser hvordan fire kommunale bidrag fremkommer, baseret på automatisk hentede BBR-oplysninger: + +- **Rottebekæmpelse** — fast gebyr pr. ejendom + sats pr. bebygget m² (halveret sats over 250 m²) +- **Renovation** — grundgebyr + tillæg pr. boligbygning afhængig af type +- **Skorstensfejer** — fast beløb pr. registreret ildsted +- **Ejendomsskat (grundskyld)** — promille af grundværdi + +Hver beregning vises som et kort med totalbeløb og en linje-for-linje opdeling af hvordan beløbet er fremkommet. + +**BBR-felterne er redigerbare**, så borgeren kan: +- Rette en forkert BBR-registrering og se hvordan opkrævningen ændres +- Simulere "hvad-hvis"-scenarier (fx tilbygning, ny brændeovn, ændret grundværdi) — totalen og delta-en mod den nuværende BBR-værdi opdateres live +- Nulstille ændringer pr. felt eller for hele BBR-panelet + +::: warning Demo-takster +Satserne i prototypen er fiktive demo-værdier baseret på offentligt tilgængelige niveauer for 2025/2026. Den endelige opkrævning afgøres af kommunens takstblad og BBR-registreringen. +::: + --- ## Krav @@ -65,4 +89,4 @@ Indikatoren bruger kombination af farve, ikon og tekst for tilgængelighed. Åbn prototypen ↗ -Vælg en testbruger for at se forskellige statuser (alt i orden, kommende forfald, restance). +Vælg en testbruger for at se forskellige statuser (alt i orden, kommende forfald, restance). Brug fanen "Beregner" for at se hvordan BBR-baserede gebyrer beregnes — og leg med felterne for at se hvad-hvis-effekter live. diff --git a/docs/projects/opkraevningsoverblik/mocks.md b/docs/projects/opkraevningsoverblik/mocks.md index e512f10..ac55295 100644 --- a/docs/projects/opkraevningsoverblik/mocks.md +++ b/docs/projects/opkraevningsoverblik/mocks.md @@ -5,4 +5,4 @@ --- **Opkrævningsoverblik — Kommunale opkrævninger ↗** -Samlet overblik over kommunale opkrævninger med statusindikator, opkrævningsliste, filtrering, detaljevisning og 5-års historisk udvikling. Tre testbrugere med forskellige statuser. +Samlet overblik over kommunale opkrævninger med statusindikator, opkrævningsliste, filtrering, detaljevisning og 5-års historisk udvikling. Inkluderer en BBR-baseret **beregner** for rottebekæmpelse, renovation, skorstensfejer og ejendomsskat — med editerbare BBR-felter og hvad-hvis-scenarier. Tre testbrugere med forskellige statuser. diff --git a/docs/public/projects/opkraevningsoverblik/mocks/app.js b/docs/public/projects/opkraevningsoverblik/mocks/app.js index 5271d47..68f353e 100644 --- a/docs/public/projects/opkraevningsoverblik/mocks/app.js +++ b/docs/public/projects/opkraevningsoverblik/mocks/app.js @@ -26,8 +26,37 @@ const chargeTypes = { daginstitution: { name: 'Daginstitution', slug: 'daginstitution', color: '#6A1B9A' }, renovation: { name: 'Renovation', slug: 'renovation', color: '#2E7D32' }, rottebekaempelse: { name: 'Rottebekaempelse', slug: 'rottebekaempelse', color: '#D84315' }, + skorstensfejer: { name: 'Skorstensfejer', slug: 'skorstensfejer', color: '#5D4037' }, }; +// Mock BBR-takster (demo-niveauer baseret på offentligt tilgængelige satser 2025/2026) +const bbrRates = { + rotte: { + fast: 75, // kr. pr. ejendom + satsLav: 0.73, // kr. pr. m² (0–250) + satsHoej: 0.36, // kr. pr. m² (>250) + knaek: 250, // m² + }, + renovation: { + grundgebyr: 1200, // kr. pr. ejendom + boligtypeTillaeg: { + beboelse: 600, + raekkehus: 400, + lejlighed: 300, + carport: 0, + udhus: 0, + }, + }, + skorsten: { + prIldsted: 350, // kr. pr. ildsted pr. år + }, + ejendomsskat: { + promille: 22.5, // grundskyldspromille + }, +}; + +const bygningstyper = ['beboelse', 'raekkehus', 'lejlighed', 'carport', 'udhus']; + const statusConfig = { betalt: { label: 'Betalt', color: '#0B6E4F', bg: '#E8F5E9' }, ubetalt: { label: 'Ubetalt', color: '#B8860B', bg: '#FFF8E1' }, @@ -46,6 +75,16 @@ const users = { name: 'Anders Hansen', email: 'borger@aarhus.test', overallStatus: 'ok', + bbr: { + ejendomsnummer: '751-123456', + adresse: 'Eksempelvej 12, 8000 Aarhus C', + grundareal: 720, + grundvaerdi: 1850000, + bygninger: [ + { id: 1, type: 'beboelse', bebyggetAreal: 142, ildsteder: 1 }, + { id: 2, type: 'carport', bebyggetAreal: 28, ildsteder: 0 }, + ], + }, charges: [ { id: 1, type: 'ejendomsskat', amount: 12400, due: '2025-03-01', status: 'betalt', period: 'Q1 2025', paidAt: '2025-03-01' }, { id: 2, type: 'daginstitution', amount: 3200, due: '2025-02-01', status: 'betalt', period: 'Feb 2025', paidAt: '2025-02-01' }, @@ -55,6 +94,7 @@ const users = { { id: 6, type: 'ejendomsskat', amount: 12400, due: '2025-06-01', status: 'kommende', period: 'Q2 2025', paidAt: null }, { id: 7, type: 'renovation', amount: 1850, due: '2025-06-15', status: 'kommende', period: 'Q2 2025', paidAt: null }, { id: 8, type: 'rottebekaempelse', amount: 500, due: '2026-01-15', status: 'kommende', period: 'Q1 2026', paidAt: null }, + { id: 23, type: 'skorstensfejer', amount: 350, due: '2025-04-15', status: 'betalt', period: '2025', paidAt: '2025-04-15' }, ], history: [ { year: 2021, charged: 48680, paid: 48680, arrears: 0 }, @@ -68,6 +108,15 @@ const users = { name: 'Maria Jensen', email: 'borger2@aarhus.test', overallStatus: 'upcoming', + bbr: { + ejendomsnummer: '751-654321', + adresse: 'Nørrevej 45B, 8210 Aarhus V', + grundareal: 320, + grundvaerdi: 1200000, + bygninger: [ + { id: 1, type: 'raekkehus', bebyggetAreal: 95, ildsteder: 0 }, + ], + }, charges: [ { id: 9, type: 'ejendomsskat', amount: 12400, due: '2025-03-01', status: 'betalt', period: 'Q1 2025', paidAt: '2025-03-01' }, { id: 10, type: 'daginstitution', amount: 3200, due: '2025-02-01', status: 'betalt', period: 'Feb 2025', paidAt: '2025-02-01' }, @@ -89,6 +138,16 @@ const users = { name: 'Lars Nielsen', email: 'borger3@aarhus.test', overallStatus: 'overdue', + bbr: { + ejendomsnummer: '751-789012', + adresse: 'Søndergade 88, 8000 Aarhus C', + grundareal: 1100, + grundvaerdi: 2400000, + bygninger: [ + { id: 1, type: 'beboelse', bebyggetAreal: 280, ildsteder: 2 }, + { id: 2, type: 'udhus', bebyggetAreal: 40, ildsteder: 0 }, + ], + }, charges: [ { id: 16, type: 'ejendomsskat', amount: 12400, due: '2025-03-01', status: 'betalt', period: 'Q1 2025', paidAt: '2025-03-01' }, { id: 17, type: 'daginstitution', amount: 3200, due: '2025-02-01', status: 'forfalden', period: 'Feb 2025', paidAt: null }, @@ -97,6 +156,7 @@ const users = { { id: 20, type: 'ejendomsskat', amount: 12400, due: '2025-06-01', status: 'ubetalt', period: 'Q2 2025', paidAt: null }, { id: 21, type: 'daginstitution', amount: 3200, due: '2025-06-15', status: 'kommende', period: 'Q2 2025', paidAt: null }, { id: 22, type: 'rottebekaempelse', amount: 500, due: '2026-01-15', status: 'kommende', period: 'Q1 2026', paidAt: null }, + { id: 24, type: 'skorstensfejer', amount: 700, due: '2025-04-15', status: 'forfalden', period: '2025', paidAt: null }, ], history: [ { year: 2021, charged: 46980, paid: 44480, arrears: 2500 }, @@ -108,12 +168,127 @@ const users = { }, }; +// -- Calculators (BBR-baserede beregninger) ----------------------------------- + +function bygningstypeLabel(t) { + return ({ + beboelse: 'Beboelse', + raekkehus: 'Rækkehus', + lejlighed: 'Lejlighed', + carport: 'Carport', + udhus: 'Udhus / skur', + })[t] || t; +} + +function round2(n) { + return Math.round(n * 100) / 100; +} + +const calculators = { + // Rottebekæmpelse: fast gebyr + sats pr. m² (knækket ved 250 m² pr. bygning) + rottebekaempelse(bbr) { + const r = bbrRates.rotte; + const lines = [ + { label: 'Fast gebyr (pr. ejendom)', amount: r.fast }, + ]; + let total = r.fast; + for (const b of bbr.bygninger) { + const lavM2 = Math.min(b.bebyggetAreal, r.knaek); + const hoejM2 = Math.max(0, b.bebyggetAreal - r.knaek); + const lavBel = round2(lavM2 * r.satsLav); + total += lavBel; + lines.push({ + label: `${bygningstypeLabel(b.type)} — 0–${r.knaek} m²`, + amount: lavBel, + note: `${lavM2} m² × ${r.satsLav.toString().replace('.', ',')} kr.`, + }); + if (hoejM2 > 0) { + const hoejBel = round2(hoejM2 * r.satsHoej); + total += hoejBel; + lines.push({ + label: `${bygningstypeLabel(b.type)} — over ${r.knaek} m²`, + amount: hoejBel, + note: `${hoejM2} m² × ${r.satsHoej.toString().replace('.', ',')} kr. (halveret sats)`, + }); + } + } + return { total: round2(total), lines }; + }, + + // Renovation: grundgebyr + tillæg pr. boligbygning + renovation(bbr) { + const r = bbrRates.renovation; + const lines = [ + { label: 'Grundgebyr (pr. ejendom)', amount: r.grundgebyr }, + ]; + let total = r.grundgebyr; + for (const b of bbr.bygninger) { + const tillaeg = r.boligtypeTillaeg[b.type] || 0; + if (tillaeg > 0) { + total += tillaeg; + lines.push({ + label: `Tillæg — ${bygningstypeLabel(b.type)}`, + amount: tillaeg, + }); + } + } + return { total, lines }; + }, + + // Skorstensfejer: pr. ildsted + skorstensfejer(bbr) { + const r = bbrRates.skorsten; + const lines = []; + let total = 0; + let antalIldsteder = 0; + for (const b of bbr.bygninger) { + antalIldsteder += b.ildsteder; + } + if (antalIldsteder === 0) { + lines.push({ label: 'Ingen ildsteder registreret', amount: 0 }); + return { total: 0, lines }; + } + total = antalIldsteder * r.prIldsted; + lines.push({ + label: `${antalIldsteder} ildsted${antalIldsteder === 1 ? '' : 'er'}`, + amount: total, + note: `${antalIldsteder} × ${r.prIldsted} kr.`, + }); + return { total, lines }; + }, + + // Ejendomsskat (grundskyld): promille af grundværdi + ejendomsskat(bbr) { + const r = bbrRates.ejendomsskat; + const total = round2(bbr.grundvaerdi * r.promille / 1000); + return { + total, + lines: [ + { + label: 'Grundskyld', + amount: total, + note: `${bbr.grundvaerdi.toLocaleString('da-DK')} kr. × ${r.promille.toString().replace('.', ',')} ‰`, + }, + ], + }; + }, +}; + +const calculatorOrder = [ + { key: 'rottebekaempelse', fn: 'rottebekaempelse', title: 'Rottebekæmpelse' }, + { key: 'renovation', fn: 'renovation', title: 'Renovation' }, + { key: 'skorstensfejer', fn: 'skorstensfejer', title: 'Skorstensfejer' }, + { key: 'ejendomsskat', fn: 'ejendomsskat', title: 'Ejendomsskat (grundskyld)' }, +]; + // -- State -------------------------------------------------------------------- let currentUser = null; let currentFilter = 'alle'; let currentTab = 'oversigt'; let historyChart = null; +let bbrEdit = null; // arbejdskopi af BBR-data for nuværende bruger (redigerbar) +let scrollToCalcKey = null; // hvis sat, scroller Beregner-fanen til det kort efter render // -- Rendering ---------------------------------------------------------------- @@ -210,6 +385,9 @@ function renderDashboard() { +
${renderHistory()}
+
+ ${renderCalculatorTab()} +
@@ -365,6 +548,209 @@ function renderHistory() { `; } +// -- Beregner-fane ------------------------------------------------------------ + +function cloneBbr(bbr) { + return { + ejendomsnummer: bbr.ejendomsnummer, + adresse: bbr.adresse, + grundareal: bbr.grundareal, + grundvaerdi: bbr.grundvaerdi, + bygninger: bbr.bygninger.map(b => ({ ...b })), + }; +} + +function ensureBbrEdit() { + if (!bbrEdit) { + bbrEdit = cloneBbr(users[currentUser].bbr); + } + return bbrEdit; +} + +function bbrFieldChanged(originalVal, currentVal) { + return originalVal !== currentVal; +} + +function renderBbrPanel() { + const orig = users[currentUser].bbr; + const edit = ensureBbrEdit(); + const grundChanged = bbrFieldChanged(orig.grundareal, edit.grundareal); + const vaerdiChanged = bbrFieldChanged(orig.grundvaerdi, edit.grundvaerdi); + + let bygningerHtml = ''; + edit.bygninger.forEach((b, i) => { + const o = orig.bygninger[i] || { type: '', bebyggetAreal: 0, ildsteder: 0 }; + const arealChanged = bbrFieldChanged(o.bebyggetAreal, b.bebyggetAreal); + const ildChanged = bbrFieldChanged(o.ildsteder, b.ildsteder); + const typeChanged = bbrFieldChanged(o.type, b.type); + const typeOptions = bygningstyper.map(t => + `` + ).join(''); + bygningerHtml += ` +
+
+ Bygning ${i + 1} +
+
+ + + ${typeChanged ? `` : ''} +
+
+ + + ${arealChanged ? `` : ''} +
+
+ + + ${ildChanged ? `` : ''} +
+
+ `; + }); + + return ` +
+
+
+

Ejendomsoplysninger

+
${edit.adresse} · Ejendomsnr. ${edit.ejendomsnummer}
+
+ Hentet fra BBR +
+ +
+
+ + + ${grundChanged ? `` : ''} +
+
+ + + ${vaerdiChanged ? `` : ''} +
+
+ +

Bygninger

+
${bygningerHtml}
+ +
+ Hvad hvis: + + + + +
+
+ `; +} + +function renderCalculatorCards() { + const orig = users[currentUser].bbr; + const edit = ensureBbrEdit(); + + let html = '
'; + for (const c of calculatorOrder) { + const type = chargeTypes[c.key]; + const result = calculators[c.fn](edit); + const origResult = calculators[c.fn](orig); + const delta = round2(result.total - origResult.total); + const deltaSign = delta > 0 ? '+' : ''; + const deltaClass = delta > 0 ? 'calc-card__delta--up' : (delta < 0 ? 'calc-card__delta--down' : ''); + + const linesHtml = result.lines.map(l => ` +
  • +
    + ${l.label} + ${l.note ? `${l.note}` : ''} +
    + ${formatDkk(l.amount)} +
  • + `).join(''); + + html += ` +
    +
    +

    ${c.title}

    + +
    +
    + ${formatDkk(result.total)} + pr. år +
    + ${delta !== 0 ? ` +
    + ${deltaSign}${formatDkk(delta)} ift. BBR (${formatDkk(origResult.total)}) +
    + ` : ''} + +
    + `; + } + html += '
    '; + return html; +} + +function renderCalculatorTab() { + return ` +
    +
    +

    Beregn dine kommunale gebyrer

    +

    + Gebyrerne nedenfor er beregnet automatisk ud fra dine BBR-oplysninger. Du kan justere felterne i panelet for at se hvordan ændringer (fx en tilbygning, en ny brændeovn eller en ændret grundværdi) påvirker beløbet. +

    +

    + OBS: Satserne er fiktive demo-værdier baseret på offentligt tilgængelige niveauer for 2025/2026. Den endelige opkrævning afgøres af kommunens takstblad og BBR-registreringen. +

    +
    + ${renderBbrPanel()} + ${renderCalculatorCards()} +
    + `; +} + +// -- "Hvad hvis"-scenarier --------------------------------------------------- + +function applyWhatIf(scenario) { + const edit = ensureBbrEdit(); + if (scenario === 'addCarport') { + const nextId = (Math.max(0, ...edit.bygninger.map(b => b.id)) || 0) + 1; + edit.bygninger.push({ id: nextId, type: 'carport', bebyggetAreal: 30, ildsteder: 0 }); + } else if (scenario === 'addBraendeovn') { + const beboelse = edit.bygninger.find(b => b.type === 'beboelse' || b.type === 'raekkehus' || b.type === 'lejlighed'); + if (beboelse) { + beboelse.ildsteder += 1; + } + } else if (scenario === 'udvidBolig') { + const beboelse = edit.bygninger.find(b => b.type === 'beboelse' || b.type === 'raekkehus'); + if (beboelse) { + beboelse.bebyggetAreal += 25; + } + } else if (scenario === 'resetAll') { + bbrEdit = cloneBbr(users[currentUser].bbr); + } +} + +function resetBbrField(field) { + const orig = users[currentUser].bbr; + const edit = ensureBbrEdit(); + if (field === 'grundareal') edit.grundareal = orig.grundareal; + if (field === 'grundvaerdi') edit.grundvaerdi = orig.grundvaerdi; +} + +function resetBbrBygningField(idx, field) { + const orig = users[currentUser].bbr.bygninger[idx]; + const edit = ensureBbrEdit().bygninger[idx]; + if (!orig || !edit) return; + if (field === 'type') edit.type = orig.type; + if (field === 'areal') edit.bebyggetAreal = orig.bebyggetAreal; + if (field === 'ild') edit.ildsteder = orig.ildsteder; +} + function renderChargeDetail(chargeId) { const user = users[currentUser]; const charge = user.charges.find(c => c.id === chargeId); @@ -383,6 +769,16 @@ function renderChargeDetail(chargeId) { `; } + const calcKey = calcKeyForChargeType(charge.type); + const calcLink = calcKey ? ` + + ` : ''; + return `
    @@ -407,6 +803,7 @@ function renderChargeDetail(chargeId) {
    ${paidRow} + ${calcLink} `; @@ -484,6 +881,7 @@ function bindLogin() { currentUser = select.value; currentFilter = 'alle'; currentTab = 'oversigt'; + bbrEdit = null; render(); }); } @@ -494,6 +892,7 @@ function bindDashboard() { currentUser = null; currentFilter = 'alle'; currentTab = 'oversigt'; + bbrEdit = null; render(); }); @@ -526,6 +925,188 @@ function bindDashboard() { if (currentTab === 'historik') { initChart(); } + + // Beregner-fanen + if (currentTab === 'beregner') { + bindCalculator(); + + if (scrollToCalcKey) { + const target = document.getElementById('calc-' + scrollToCalcKey); + if (target) { + target.scrollIntoView({ behavior: 'smooth', block: 'center' }); + target.classList.add('calc-card--highlight'); + setTimeout(() => target.classList.remove('calc-card--highlight'), 1600); + } + scrollToCalcKey = null; + } + } +} + +// Mappe charge-type -> calculator-kort +function calcKeyForChargeType(chargeType) { + if (calculators[chargeType]) return chargeType; + return null; +} + +// Opdater kun beregningskort + nulstil-markeringer uden at rerender hele DOM'en +// (så input-feltet beholder fokus mens man taster). +function refreshCalculatorCards() { + const grid = document.querySelector('.calc-grid'); + if (grid) { + const wrapper = document.createElement('div'); + wrapper.innerHTML = renderCalculatorCards(); + const newGrid = wrapper.firstElementChild; + grid.replaceWith(newGrid); + } + // Opdater "ændret"-markering og nulstil-knapper på BBR-felterne + refreshBbrFieldStates(); +} + +function refreshBbrFieldStates() { + const orig = users[currentUser].bbr; + const edit = bbrEdit; + if (!edit) return; + + const setFieldState = (selector, changed, resetAttr, resetTitle) => { + const input = document.querySelector(selector); + if (!input) return; + const wrapper = input.closest('.bbr-field'); + if (!wrapper) return; + wrapper.classList.toggle('bbr-field--changed', changed); + const existingReset = wrapper.querySelector('.bbr-field__reset'); + if (changed && !existingReset) { + const btn = document.createElement('button'); + btn.className = 'bbr-field__reset'; + btn.title = resetTitle; + btn.textContent = '↺'; + for (const [k, v] of Object.entries(resetAttr)) { + btn.dataset[k] = v; + } + btn.addEventListener('click', handleResetClick); + wrapper.appendChild(btn); + } else if (!changed && existingReset) { + existingReset.remove(); + } + }; + + setFieldState( + '[data-bbr-grundareal]', + bbrFieldChanged(orig.grundareal, edit.grundareal), + { bbrReset: 'grundareal' }, + `Nulstil til BBR-værdi (${orig.grundareal})`, + ); + setFieldState( + '[data-bbr-grundvaerdi]', + bbrFieldChanged(orig.grundvaerdi, edit.grundvaerdi), + { bbrReset: 'grundvaerdi' }, + `Nulstil til BBR-værdi (${orig.grundvaerdi.toLocaleString('da-DK')})`, + ); + edit.bygninger.forEach((b, i) => { + const o = orig.bygninger[i] || { type: '', bebyggetAreal: 0, ildsteder: 0 }; + setFieldState( + `[data-bbr-bygning-areal="${i}"]`, + bbrFieldChanged(o.bebyggetAreal, b.bebyggetAreal), + { bbrResetBygning: String(i), bbrResetField: 'areal' }, + `Nulstil til BBR-værdi (${o.bebyggetAreal})`, + ); + setFieldState( + `[data-bbr-bygning-ild="${i}"]`, + bbrFieldChanged(o.ildsteder, b.ildsteder), + { bbrResetBygning: String(i), bbrResetField: 'ild' }, + `Nulstil til BBR-værdi (${o.ildsteder})`, + ); + setFieldState( + `[data-bbr-bygning-type="${i}"]`, + bbrFieldChanged(o.type, b.type), + { bbrResetBygning: String(i), bbrResetField: 'type' }, + `Nulstil til BBR-værdi`, + ); + }); +} + +function handleResetClick(ev) { + const btn = ev.currentTarget; + if (btn.dataset.bbrReset) { + resetBbrField(btn.dataset.bbrReset); + } else if (btn.dataset.bbrResetBygning) { + resetBbrBygningField(parseInt(btn.dataset.bbrResetBygning, 10), btn.dataset.bbrResetField); + } + // Synk DOM'ens input-værdier med ny state + syncBbrInputsFromEdit(); + refreshCalculatorCards(); +} + +function syncBbrInputsFromEdit() { + if (!bbrEdit) return; + const ga = document.querySelector('[data-bbr-grundareal]'); + if (ga) ga.value = bbrEdit.grundareal; + const gv = document.querySelector('[data-bbr-grundvaerdi]'); + if (gv) gv.value = bbrEdit.grundvaerdi; + bbrEdit.bygninger.forEach((b, i) => { + const a = document.querySelector(`[data-bbr-bygning-areal="${i}"]`); + if (a) a.value = b.bebyggetAreal; + const il = document.querySelector(`[data-bbr-bygning-ild="${i}"]`); + if (il) il.value = b.ildsteder; + const ty = document.querySelector(`[data-bbr-bygning-type="${i}"]`); + if (ty) ty.value = b.type; + }); +} + +function bindCalculator() { + const panel = document.getElementById('panel-beregner'); + if (!panel) return; + + // Top-level felter (live opdatering uden full re-render → bevarer fokus) + const grundareal = panel.querySelector('[data-bbr-grundareal]'); + if (grundareal) { + grundareal.addEventListener('input', function() { + ensureBbrEdit().grundareal = parseInt(this.value, 10) || 0; + refreshCalculatorCards(); + }); + } + const grundvaerdi = panel.querySelector('[data-bbr-grundvaerdi]'); + if (grundvaerdi) { + grundvaerdi.addEventListener('input', function() { + ensureBbrEdit().grundvaerdi = parseInt(this.value, 10) || 0; + refreshCalculatorCards(); + }); + } + + // Bygnings-felter + panel.querySelectorAll('[data-bbr-bygning-areal]').forEach(function(el) { + el.addEventListener('input', function() { + const i = parseInt(this.dataset.bbrBygningAreal, 10); + ensureBbrEdit().bygninger[i].bebyggetAreal = parseInt(this.value, 10) || 0; + refreshCalculatorCards(); + }); + }); + panel.querySelectorAll('[data-bbr-bygning-ild]').forEach(function(el) { + el.addEventListener('input', function() { + const i = parseInt(this.dataset.bbrBygningIld, 10); + ensureBbrEdit().bygninger[i].ildsteder = parseInt(this.value, 10) || 0; + refreshCalculatorCards(); + }); + }); + panel.querySelectorAll('[data-bbr-bygning-type]').forEach(function(el) { + el.addEventListener('change', function() { + const i = parseInt(this.dataset.bbrBygningType, 10); + ensureBbrEdit().bygninger[i].type = this.value; + refreshCalculatorCards(); + }); + }); + + // Nulstil-knapper + panel.querySelectorAll('.bbr-field__reset').forEach(function(btn) { + btn.addEventListener('click', handleResetClick); + }); + + // Hvad-hvis-chips → fuld re-render (kan ændre antallet af bygninger) + panel.querySelectorAll('[data-whatif]').forEach(function(btn) { + btn.addEventListener('click', function() { + applyWhatIf(this.dataset.whatif); + render(); + }); + }); } function showDetail(chargeId) { @@ -541,6 +1122,18 @@ function showDetail(chargeId) { detailView.innerHTML = ''; dashboardView.style.display = 'block'; }); + + const calcBtn = document.getElementById('open-calc-btn'); + if (calcBtn) { + calcBtn.addEventListener('click', function() { + scrollToCalcKey = this.dataset.calcKey; + currentTab = 'beregner'; + detailView.style.display = 'none'; + detailView.innerHTML = ''; + dashboardView.style.display = 'block'; + render(); + }); + } } // -- Init --------------------------------------------------------------------- diff --git a/docs/public/projects/opkraevningsoverblik/mocks/index.html b/docs/public/projects/opkraevningsoverblik/mocks/index.html index b1788d2..1122419 100644 --- a/docs/public/projects/opkraevningsoverblik/mocks/index.html +++ b/docs/public/projects/opkraevningsoverblik/mocks/index.html @@ -9,6 +9,7 @@ +
    Dette er en mock-up, ikke det rigtige eller endelige produkt.
    diff --git a/docs/public/projects/opkraevningsoverblik/mocks/style.css b/docs/public/projects/opkraevningsoverblik/mocks/style.css index ef6d7c8..b03cfef 100644 --- a/docs/public/projects/opkraevningsoverblik/mocks/style.css +++ b/docs/public/projects/opkraevningsoverblik/mocks/style.css @@ -47,6 +47,22 @@ body { min-height: 100vh; display: flex; flex-direction: column; + padding-top: 32px; +} + +.mock-banner { + position: fixed; + top: 0; + left: 0; + right: 0; + background: #fef9e7; + color: #666; + text-align: center; + padding: 8px 16px; + font-size: 12px; + z-index: 1000; + letter-spacing: 0.3px; + border-bottom: 1px solid #f0e6c0; } /* ========================================================================== @@ -722,6 +738,13 @@ input:focus-visible { color: var(--color-white); } +.btn--secondary { + background: var(--color-white); + color: var(--color-primary); + border: 2px solid var(--color-primary); + padding: 10px 22px; +} + .btn--full { width: 100%; } @@ -804,3 +827,405 @@ input:focus-visible { padding: 24px; } } + +/* ========================================================================== + Beregner-fane (BBR-baseret) + ========================================================================== */ + +.calculator { + display: flex; + flex-direction: column; + gap: 24px; +} + +.calculator__intro { + background: var(--color-white); + border-radius: var(--radius-lg); + padding: 20px 24px; + box-shadow: var(--shadow); +} + +.calculator__title { + font-size: 22px; + font-weight: 700; + color: var(--color-primary); + margin-bottom: 8px; +} + +.calculator__desc { + color: var(--color-text-secondary); + margin-bottom: 12px; +} + +.calculator__disclaimer { + font-size: 13px; + color: var(--color-text-light); + background: var(--color-warning-bg); + padding: 10px 14px; + border-radius: 6px; + border-left: 3px solid var(--color-warning); +} + +.calculator__disclaimer strong { + color: var(--color-warning); +} + +/* BBR-panel ----------------------------------------------------------------- */ + +.bbr-panel { + background: var(--color-white); + border-radius: var(--radius-lg); + padding: 20px 24px; + box-shadow: var(--shadow); +} + +.bbr-panel__header { + display: flex; + justify-content: space-between; + align-items: flex-start; + gap: 16px; + margin-bottom: 20px; + flex-wrap: wrap; +} + +.bbr-panel__title { + font-size: 18px; + font-weight: 700; + color: var(--color-primary); + margin-bottom: 4px; +} + +.bbr-panel__meta { + color: var(--color-text-secondary); + font-size: 14px; +} + +.bbr-badge { + display: inline-block; + background: #E1F2F7; + color: var(--color-primary); + font-size: 12px; + font-weight: 600; + padding: 4px 10px; + border-radius: 12px; + border: 1px solid #B5DCE7; + white-space: nowrap; +} + +.bbr-panel__subtitle { + font-size: 14px; + font-weight: 600; + color: var(--color-text-secondary); + text-transform: uppercase; + letter-spacing: 0.04em; + margin-bottom: 10px; +} + +.bbr-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); + gap: 14px; + margin-bottom: 20px; +} + +.bbr-bygninger { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); + gap: 14px; + margin-bottom: 20px; +} + +.bbr-bygning { + background: var(--color-bg); + border: 1px solid var(--color-border); + border-radius: 8px; + padding: 12px 14px; +} + +.bbr-bygning__header { + margin-bottom: 8px; +} + +.bbr-bygning__num { + font-size: 12px; + font-weight: 700; + color: var(--color-text-light); + text-transform: uppercase; + letter-spacing: 0.04em; +} + +.bbr-bygning .bbr-field + .bbr-field { + margin-top: 8px; +} + +.bbr-field { + position: relative; + display: flex; + flex-direction: column; + gap: 4px; +} + +.bbr-field label { + font-size: 13px; + font-weight: 600; + color: var(--color-text-secondary); +} + +.bbr-field input, +.bbr-field select { + padding: 8px 10px; + border: 2px solid var(--color-border); + border-radius: 6px; + font-family: inherit; + font-size: 15px; + background: var(--color-white); + color: var(--color-text); + transition: border-color 0.15s, box-shadow 0.15s; +} + +.bbr-field input:focus, +.bbr-field select:focus { + outline: none; + border-color: var(--color-primary); + box-shadow: var(--focus-ring); +} + +.bbr-field--changed input, +.bbr-field--changed select { + border-color: var(--color-warning); + background: #FFFEF5; +} + +.bbr-field--changed::before { + content: 'Ændret'; + position: absolute; + top: 0; + right: 0; + font-size: 10px; + font-weight: 700; + color: var(--color-warning); + text-transform: uppercase; + letter-spacing: 0.05em; +} + +.bbr-field__reset { + position: absolute; + right: 6px; + bottom: 6px; + background: transparent; + border: none; + cursor: pointer; + color: var(--color-warning); + font-size: 16px; + line-height: 1; + padding: 4px 6px; + border-radius: 4px; +} + +.bbr-field__reset:hover { + background: var(--color-warning-bg); +} + +.bbr-field__reset:focus-visible { + outline: 2px solid var(--color-warning); +} + +/* Hvad-hvis-chips ----------------------------------------------------------- */ + +.bbr-whatif { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 8px; + padding-top: 16px; + border-top: 1px solid var(--color-border); +} + +.bbr-whatif__label { + font-size: 13px; + font-weight: 700; + color: var(--color-text-secondary); + text-transform: uppercase; + letter-spacing: 0.04em; + margin-right: 4px; +} + +.bbr-chip { + background: var(--color-primary-light); + color: var(--color-primary); + border: 1px solid var(--color-border); + border-radius: 16px; + padding: 6px 14px; + font-size: 13px; + font-weight: 600; + cursor: pointer; + font-family: inherit; + transition: background 0.15s, border-color 0.15s; +} + +.bbr-chip:hover { + background: #E1F2F7; + border-color: var(--color-primary); +} + +.bbr-chip:focus-visible { + outline: 2px solid var(--color-primary); + outline-offset: 2px; +} + +.bbr-chip--reset { + background: transparent; + color: var(--color-text-secondary); + margin-left: auto; +} + +/* Beregningskort ------------------------------------------------------------ */ + +.calc-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); + gap: 16px; +} + +.calc-card { + background: var(--color-white); + border-radius: var(--radius-lg); + border-top: 4px solid var(--color-muted); + padding: 18px 20px; + box-shadow: var(--shadow); + display: flex; + flex-direction: column; + transition: box-shadow 0.3s; +} + +.calc-card--highlight { + box-shadow: 0 0 0 3px var(--color-primary), var(--shadow); +} + +.calc-card__header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 8px; +} + +.calc-card__title { + font-size: 15px; + font-weight: 700; + color: var(--color-text); + margin: 0; +} + +.calc-card__dot { + width: 12px; + height: 12px; + border-radius: 50%; +} + +.calc-card__total { + display: flex; + align-items: baseline; + gap: 8px; + margin-bottom: 8px; +} + +.calc-card__total-value { + font-size: 28px; + font-weight: 800; + color: var(--color-primary); +} + +.calc-card__total-period { + font-size: 13px; + color: var(--color-text-light); +} + +.calc-card__delta { + display: inline-block; + font-size: 13px; + font-weight: 600; + padding: 4px 10px; + border-radius: 12px; + background: var(--color-muted-bg); + color: var(--color-text-secondary); + margin-bottom: 12px; + align-self: flex-start; +} + +.calc-card__delta--up { + background: var(--color-warning-bg); + color: var(--color-warning); +} + +.calc-card__delta--down { + background: var(--color-success-bg); + color: var(--color-success); +} + +.calc-card__lines { + list-style: none; + margin: 0; + padding: 12px 0 0; + border-top: 1px dashed var(--color-border); + display: flex; + flex-direction: column; + gap: 8px; +} + +.calc-card__line { + display: flex; + justify-content: space-between; + align-items: flex-start; + gap: 12px; + font-size: 14px; +} + +.calc-card__line-label { + display: flex; + flex-direction: column; + color: var(--color-text); +} + +.calc-card__line-note { + font-size: 12px; + color: var(--color-text-light); + margin-top: 2px; +} + +.calc-card__line-amount { + font-weight: 600; + color: var(--color-text); + white-space: nowrap; +} + +/* "Se beregning"-link i charge-detail -------------------------------------- */ + +.charge-detail__calc-link { + display: flex; + align-items: center; + gap: 12px; + flex-wrap: wrap; + margin-top: 20px; + padding-top: 20px; + border-top: 1px solid var(--color-border); +} + +.charge-detail__calc-hint { + font-size: 13px; + color: var(--color-text-secondary); +} + +@media (max-width: 480px) { + .calc-grid { + grid-template-columns: 1fr; + } + + .bbr-grid, + .bbr-bygninger { + grid-template-columns: 1fr; + } + + .calc-card__total-value { + font-size: 24px; + } +}