diff --git a/CHANGELOG.md b/CHANGELOG.md index 87100d8..3cbf235 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ## [Unreleased] +### Added — Opkrævningsoverblik Project +- Consolidated municipal charges overview prototype for Aarhus Kommune citizens +- Interactive mock with login, dashboard, status indicator, charge list, filtering, and 5-year history chart + ### Added - "Last edited" dates on home page project cards, sourced from git commit timestamps - Custom HomeFeatures component replacing default VitePress feature cards diff --git a/docs/.vitepress/sidebar.mts b/docs/.vitepress/sidebar.mts index d45fc7e..f277088 100644 --- a/docs/.vitepress/sidebar.mts +++ b/docs/.vitepress/sidebar.mts @@ -78,6 +78,16 @@ const bookAarhus: DefaultTheme.SidebarItem[] = [ }, ] +const opkraevningsoverblik: DefaultTheme.SidebarItem[] = [ + { + text: 'Opkrævningsoverblik', + items: [ + { text: 'Overview', link: '/projects/opkraevningsoverblik/' }, + { text: 'Interactive Mocks', link: '/projects/opkraevningsoverblik/mocks' }, + ], + }, +] + export function sidebar(): DefaultTheme.Sidebar { return { '/projects/climate-nudging/': climateNudging, @@ -87,6 +97,7 @@ export function sidebar(): DefaultTheme.Sidebar { '/projects/wcag-contrast-checker/': wcagContrastChecker, '/projects/deltag-aarhus-timeline/': deltagAarhusTimeline, '/projects/book-aarhus/': bookAarhus, + '/projects/opkraevningsoverblik/': opkraevningsoverblik, } } diff --git a/docs/index.md b/docs/index.md index 68d075b..b8cb18a 100644 --- a/docs/index.md +++ b/docs/index.md @@ -34,4 +34,8 @@ features: details: Prototype af en responsiv bookingapplikation til lokaler og faciliteter i Aarhus Kommune med søgning, filtrering og favoritter. link: /projects/book-aarhus/ linkText: View project + - title: Opkrævningsoverblik + details: Samlet overblik over kommunale opkrævninger med statusindikator, opkrævningsliste, filtrering og 5-års historisk udvikling. + link: /projects/opkraevningsoverblik/ + linkText: View project --- diff --git a/docs/projects/opkraevningsoverblik/index.md b/docs/projects/opkraevningsoverblik/index.md new file mode 100644 index 0000000..fe462d1 --- /dev/null +++ b/docs/projects/opkraevningsoverblik/index.md @@ -0,0 +1,68 @@ +**Project:** Opkrævningsoverblik · **Status:** Prototype · **Date:** February 2026 + +# Opkrævningsoverblik + +**Samlet overblik over kommunale opkrævninger for borgere i Aarhus Kommune.** + +--- + +## Baggrund + +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. + +## 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. + +**Succeskriterium:** Borgeren kan danne sig et overblik over sin samlede status og eventuelle restancer på under ét minut — uden vejledning. + +--- + +## Hvad prototypen viser + +### Login og testbrugere + +Prototypen inkluderer tre testbrugere med forskellige statuser: +- **Anders** — alle opkrævninger betalt, ingen restancer +- **Maria** — kommende forfaldsdatoer inden for 30 dage +- **Lars** — ubetalte opkrævninger der har overskredet forfaldsdato + +### Dashboard med statusindikator + +En visuel statusindikator der giver borgeren øjeblikkeligt overblik: +- **Alt i orden** (grøn) — alle opkrævninger er betalt +- **Kommende forfald** (gul) — opkrævninger forfalder inden for 30 dage +- **Restance** (rød) — ubetalte opkrævninger der har overskredet forfaldsdato + +Indikatoren bruger kombination af farve, ikon og tekst for tilgængelighed. + +### Opkrævningsliste + +- Alle aktive opkrævninger med beløb, forfaldsdato og status +- Filtrering efter ydelsestype (ejendomsskat, daginstitution, renovation m.fl.) +- Samlet restancebeløb +- Detaljevisning for den enkelte opkrævning + +### Historisk udvikling (5 år) + +- Linjediagram (Chart.js) der viser opkrævet vs. betalt beløb over tid +- Mulighed for at se udviklingen per ydelsestype +- Tydelig markering af perioder med restancer + +--- + +## Krav + +- WCAG 2.1 Level AA tilgængelighed +- Responsivt design (desktop, tablet, mobil) +- NemLog-in autentifikation +- Nær-realtid datahentning +- Overblikket dækker kun kommunale opkrævninger — ikke regionale eller nationale + +--- + +## Interaktiv prototype + +Åbn prototypen ↗ + +Vælg en testbruger for at se forskellige statuser (alt i orden, kommende forfald, restance). diff --git a/docs/projects/opkraevningsoverblik/mocks.md b/docs/projects/opkraevningsoverblik/mocks.md new file mode 100644 index 0000000..e512f10 --- /dev/null +++ b/docs/projects/opkraevningsoverblik/mocks.md @@ -0,0 +1,8 @@ +**Project:** Opkrævningsoverblik + +# Interaktive Mocks + +--- + +**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. diff --git a/docs/public/projects/opkraevningsoverblik/mocks/app.js b/docs/public/projects/opkraevningsoverblik/mocks/app.js new file mode 100644 index 0000000..5271d47 --- /dev/null +++ b/docs/public/projects/opkraevningsoverblik/mocks/app.js @@ -0,0 +1,548 @@ +/* ========================================================================== + Opkraevningsoverblik — Mockup Data & Logic + Pure vanilla JS, no dependencies (except Chart.js via CDN) + ========================================================================== */ + +// -- Helpers ------------------------------------------------------------------ + +function formatDkk(amount) { + return amount.toLocaleString('da-DK') + ' kr.'; +} + +function formatDate(dateStr) { + const d = new Date(dateStr); + return d.toLocaleDateString('da-DK', { day: '2-digit', month: '2-digit', year: 'numeric' }); +} + +function formatDateLong(dateStr) { + const d = new Date(dateStr); + return d.toLocaleDateString('da-DK', { day: 'numeric', month: 'long', year: 'numeric' }); +} + +// -- Data --------------------------------------------------------------------- + +const chargeTypes = { + ejendomsskat: { name: 'Ejendomsskat', slug: 'ejendomsskat', color: '#1565C0' }, + daginstitution: { name: 'Daginstitution', slug: 'daginstitution', color: '#6A1B9A' }, + renovation: { name: 'Renovation', slug: 'renovation', color: '#2E7D32' }, + rottebekaempelse: { name: 'Rottebekaempelse', slug: 'rottebekaempelse', color: '#D84315' }, +}; + +const statusConfig = { + betalt: { label: 'Betalt', color: '#0B6E4F', bg: '#E8F5E9' }, + ubetalt: { label: 'Ubetalt', color: '#B8860B', bg: '#FFF8E1' }, + forfalden:{ label: 'Forfalden',color: '#C62828', bg: '#FFEBEE' }, + kommende: { label: 'Kommende', color: '#546E7A', bg: '#ECEFF1' }, +}; + +const overallStatusConfig = { + ok: { label: 'Alt i orden', desc: 'Alle opkraevninger er betalt', color: '#0B6E4F', bg: '#E8F5E9', icon: '\u2713' }, + upcoming: { label: 'Kommende forfald', desc: 'Du har opkraevninger der forfalder inden 30 dage', color: '#B8860B', bg: '#FFF8E1', icon: '\u23F1' }, + overdue: { label: 'Udestaende', desc: 'Du har ubetalte opkraevninger', color: '#C62828', bg: '#FFEBEE', icon: '!' }, +}; + +const users = { + anders: { + name: 'Anders Hansen', + email: 'borger@aarhus.test', + overallStatus: 'ok', + 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' }, + { id: 3, type: 'renovation', amount: 1850, due: '2025-03-15', status: 'betalt', period: 'Q1 2025', paidAt: '2025-03-15' }, + { id: 4, type: 'daginstitution', amount: 3200, due: '2025-03-01', status: 'betalt', period: 'Mar 2025', paidAt: '2025-03-01' }, + { id: 5, type: 'rottebekaempelse', amount: 500, due: '2025-01-15', status: 'betalt', period: 'Q1 2025', paidAt: '2025-01-15' }, + { 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 }, + ], + history: [ + { year: 2021, charged: 48680, paid: 48680, arrears: 0 }, + { year: 2022, charged: 51890, paid: 50290, arrears: 1600 }, + { year: 2023, charged: 53600, paid: 53600, arrears: 0 }, + { year: 2024, charged: 55310, paid: 55310, arrears: 0 }, + { year: 2025, charged: 56720, paid: 56720, arrears: 0 }, + ], + }, + maria: { + name: 'Maria Jensen', + email: 'borger2@aarhus.test', + overallStatus: 'upcoming', + 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' }, + { id: 11, type: 'renovation', amount: 1850, due: '2025-03-15', status: 'betalt', period: 'Q1 2025', paidAt: '2025-03-15' }, + { id: 12, type: 'rottebekaempelse', amount: 500, due: '2025-01-15', status: 'betalt', period: 'Q1 2025', paidAt: '2025-01-15' }, + { id: 13, type: 'ejendomsskat', amount: 12400, due: '2025-06-01', status: 'ubetalt', period: 'Q2 2025', paidAt: null }, + { id: 14, type: 'daginstitution', amount: 3200, due: '2025-06-15', status: 'ubetalt', period: 'Q2 2025', paidAt: null }, + { id: 15, type: 'rottebekaempelse', amount: 500, due: '2026-01-15', status: 'ubetalt', period: 'Q1 2026', paidAt: null }, + ], + history: [ + { year: 2021, charged: 45480, paid: 45480, arrears: 0 }, + { year: 2022, charged: 47690, paid: 47690, arrears: 0 }, + { year: 2023, charged: 50300, paid: 50300, arrears: 0 }, + { year: 2024, charged: 51710, paid: 51710, arrears: 0 }, + { year: 2025, charged: 53900, paid: 38300, arrears: 15600 }, + ], + }, + lars: { + name: 'Lars Nielsen', + email: 'borger3@aarhus.test', + overallStatus: 'overdue', + 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 }, + { id: 18, type: 'renovation', amount: 1850, due: '2025-03-15', status: 'forfalden', period: 'Q1 2025', paidAt: null }, + { id: 19, type: 'rottebekaempelse', amount: 500, due: '2025-01-15', status: 'forfalden', period: 'Q1 2025', paidAt: null }, + { 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 }, + ], + history: [ + { year: 2021, charged: 46980, paid: 44480, arrears: 2500 }, + { year: 2022, charged: 49390, paid: 46290, arrears: 3100 }, + { year: 2023, charged: 51200, paid: 51200, arrears: 0 }, + { year: 2024, charged: 52910, paid: 49810, arrears: 3100 }, + { year: 2025, charged: 54600, paid: 37200, arrears: 17400 }, + ], + }, +}; + +// -- State -------------------------------------------------------------------- + +let currentUser = null; +let currentFilter = 'alle'; +let currentTab = 'oversigt'; +let historyChart = null; + +// -- Rendering ---------------------------------------------------------------- + +function render() { + const app = document.getElementById('app'); + if (!currentUser) { + app.innerHTML = renderLogin(); + bindLogin(); + } else { + app.innerHTML = renderDashboard(); + bindDashboard(); + } +} + +function renderLogin() { + return ` + + +
+
+ +
+
+ + `; +} + +function renderDashboard() { + const user = users[currentUser]; + const status = overallStatusConfig[user.overallStatus]; + const outstanding = user.charges + .filter(c => c.status === 'ubetalt' || c.status === 'forfalden') + .reduce((sum, c) => sum + c.amount, 0); + + return ` + + +
+
+ ${renderStatusBanner(status, outstanding)} +
+
+ + +
+
+ ${renderFilterBar()} + ${renderChargeList()} +
+
+ ${renderHistory()} +
+
+
+ +
+ + `; +} + +function renderStatusBanner(status, outstanding) { + return ` +
+ +
+
${status.label}
+
${status.desc}
+
+ ${outstanding > 0 ? ` +
+
Samlet udestaende
+
${formatDkk(outstanding)}
+
+ ` : ''} +
+ `; +} + +function renderFilterBar() { + const types = Object.values(chargeTypes); + const allActive = currentFilter === 'alle'; + + let html = '
'; + html += ``; + + for (const t of types) { + const active = currentFilter === t.slug; + const activeStyle = active ? `border-color: ${t.color}; background: ${t.color}; color: #fff;` : ''; + html += ``; + } + + html += '
'; + return html; +} + +function renderChargeList() { + const user = users[currentUser]; + let charges = user.charges; + if (currentFilter !== 'alle') { + charges = charges.filter(c => c.type === currentFilter); + } + + if (charges.length === 0) { + return '

Ingen opkraevninger fundet.

'; + } + + let html = '
'; + for (const c of charges) { + const type = chargeTypes[c.type]; + const badge = statusConfig[c.status]; + html += ` + +
+
${type.name}
+
${c.period} · Forfald: ${formatDate(c.due)}
+
+
${formatDkk(c.amount)}
+ ${badge.label} +
+ `; + } + html += '
'; + return html; +} + +function renderHistory() { + const user = users[currentUser]; + const history = user.history; + const arrearsEntries = history.filter(h => h.arrears > 0); + + let arrearsHtml; + if (arrearsEntries.length === 0) { + arrearsHtml = 'Ingen restance i perioden'; + } else { + arrearsHtml = arrearsEntries.map(h => ` +
+ ${h.year} + Restance: ${formatDkk(h.arrears)} +
+ `).join(''); + } + + let tableRows = history.map(h => { + const arrearsClass = h.arrears > 0 ? 'text-danger font-semibold' : 'text-success'; + const arrearsVal = h.arrears > 0 ? formatDkk(h.arrears) : '\u2014'; + return ` + + ${h.year} + ${formatDkk(h.charged)} + ${formatDkk(h.paid)} + ${arrearsVal} + + `; + }).join(''); + + return ` +
+

Udvikling over 5 ar

+

Opkraevet vs. betalt belob pr. ar

+ +
+

Perioder med restance

+
${arrearsHtml}
+
+
+ Vis som tabel (tilgaengeligt alternativ) + + + + + + + + + + + ${tableRows} +
Betalingshistorik over de seneste 5 ar
ArOpkraevetBetaltRestance
+
+
+ `; +} + +function renderChargeDetail(chargeId) { + const user = users[currentUser]; + const charge = user.charges.find(c => c.id === chargeId); + if (!charge) return ''; + + const type = chargeTypes[charge.type]; + const badge = statusConfig[charge.status]; + + let paidRow = ''; + if (charge.paidAt) { + paidRow = ` +
+
Betalt
+
${formatDateLong(charge.paidAt)}
+
+ `; + } + + return ` +
+ +
+
+

${type.name}

+ ${badge.label} +
+
+
+
Belob
+
${formatDkk(charge.amount)}
+
+
+
Periode
+
${charge.period}
+
+
+
Forfaldsdato
+
${formatDateLong(charge.due)}
+
+ ${paidRow} +
+
+
+ `; +} + +// -- Chart -------------------------------------------------------------------- + +function initChart() { + const canvas = document.getElementById('history-chart'); + if (!canvas) return; + + if (historyChart) { + historyChart.destroy(); + historyChart = null; + } + + const user = users[currentUser]; + const labels = user.history.map(h => String(h.year)); + const charged = user.history.map(h => h.charged); + const paid = user.history.map(h => h.paid); + + historyChart = new Chart(canvas, { + type: 'bar', + data: { + labels: labels, + datasets: [ + { + label: 'Opkraevet', + backgroundColor: '#003C50', + borderRadius: 4, + data: charged, + }, + { + label: 'Betalt', + backgroundColor: '#43A047', + borderRadius: 4, + data: paid, + }, + ], + }, + options: { + responsive: true, + maintainAspectRatio: false, + plugins: { + legend: { + position: 'bottom', + labels: { + usePointStyle: true, + pointStyle: 'circle', + padding: 20, + }, + }, + }, + scales: { + x: { + grid: { display: false }, + }, + y: { + grid: { color: '#eee' }, + border: { display: false }, + ticks: { + callback: function(value) { return (value / 1000) + 'k'; }, + }, + }, + }, + }, + }); +} + +// -- Event Binding ------------------------------------------------------------ + +function bindLogin() { + document.getElementById('login-btn').addEventListener('click', function() { + const select = document.getElementById('user-select'); + currentUser = select.value; + currentFilter = 'alle'; + currentTab = 'oversigt'; + render(); + }); +} + +function bindDashboard() { + // Logout + document.getElementById('logout-btn').addEventListener('click', function() { + currentUser = null; + currentFilter = 'alle'; + currentTab = 'oversigt'; + render(); + }); + + // Tabs + document.querySelectorAll('.tabs__tab').forEach(function(tab) { + tab.addEventListener('click', function() { + currentTab = this.dataset.tab; + render(); + }); + }); + + // Filters + document.querySelectorAll('.filter-bar__btn').forEach(function(btn) { + btn.addEventListener('click', function() { + currentFilter = this.dataset.filter; + render(); + }); + }); + + // Charge cards -> detail view + document.querySelectorAll('.charge-card').forEach(function(card) { + card.addEventListener('click', function(e) { + e.preventDefault(); + const id = parseInt(this.dataset.chargeId, 10); + showDetail(id); + }); + }); + + // Init chart if history tab is active + if (currentTab === 'historik') { + initChart(); + } +} + +function showDetail(chargeId) { + const dashboardView = document.getElementById('dashboard-view'); + const detailView = document.getElementById('detail-view'); + + dashboardView.style.display = 'none'; + detailView.style.display = 'block'; + detailView.innerHTML = renderChargeDetail(chargeId); + + document.getElementById('back-btn').addEventListener('click', function() { + detailView.style.display = 'none'; + detailView.innerHTML = ''; + dashboardView.style.display = 'block'; + }); +} + +// -- Init --------------------------------------------------------------------- + +document.addEventListener('DOMContentLoaded', render); diff --git a/docs/public/projects/opkraevningsoverblik/mocks/index.html b/docs/public/projects/opkraevningsoverblik/mocks/index.html new file mode 100644 index 0000000..b1788d2 --- /dev/null +++ b/docs/public/projects/opkraevningsoverblik/mocks/index.html @@ -0,0 +1,15 @@ + + + + + + Mine opkraevninger — Aarhus Kommune (mockup) + + + + + +
+ + + diff --git a/docs/public/projects/opkraevningsoverblik/mocks/style.css b/docs/public/projects/opkraevningsoverblik/mocks/style.css new file mode 100644 index 0000000..ef6d7c8 --- /dev/null +++ b/docs/public/projects/opkraevningsoverblik/mocks/style.css @@ -0,0 +1,806 @@ +/* ========================================================================== + Aarhus Kommune — Opkraevningsoverblik Mockup + CSS Custom Properties + BEM naming + WCAG 2.1 AA + ========================================================================== */ + +@import url('https://fonts.googleapis.com/css2?family=Source+Sans+3:wght@400;500;600;700;800&display=swap'); + +:root { + --color-primary: #003C50; + --color-primary-light: #F5F5F0; + --color-success: #0B6E4F; + --color-success-bg: #E8F5E9; + --color-warning: #B8860B; + --color-warning-bg: #FFF8E1; + --color-danger: #C62828; + --color-danger-bg: #FFEBEE; + --color-muted: #546E7A; + --color-muted-bg: #ECEFF1; + --color-text: #222; + --color-text-secondary: #666; + --color-text-light: #888; + --color-border: #ddd; + --color-bg: #F5F5F0; + --color-white: #fff; + --font-family: 'Source Sans 3', 'Segoe UI', system-ui, -apple-system, sans-serif; + --max-width: 960px; + --radius: 10px; + --radius-lg: 12px; + --shadow: 0 1px 3px rgba(0, 0, 0, 0.06); + --focus-ring: 0 0 0 3px rgba(0, 60, 80, 0.4); +} + +/* Reset */ +*, +*::before, +*::after { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +body { + font-family: var(--font-family); + background: var(--color-bg); + color: var(--color-text); + line-height: 1.5; + min-height: 100vh; + display: flex; + flex-direction: column; +} + +/* ========================================================================== + Accessibility + ========================================================================== */ + +.sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; +} + +.skip-link { + position: absolute; + top: -100%; + left: 16px; + z-index: 1000; + padding: 12px 24px; + background: var(--color-primary); + color: var(--color-white); + text-decoration: none; + border-radius: 0 0 var(--radius) var(--radius); + font-weight: 600; +} + +.skip-link:focus { + top: 0; +} + +/* Focus indicators */ +:focus-visible { + outline: 3px solid var(--color-primary); + outline-offset: 2px; +} + +a:focus-visible, +button:focus-visible, +input:focus-visible { + box-shadow: var(--focus-ring); + outline: none; +} + +/* ========================================================================== + Header + ========================================================================== */ + +.header { + background: var(--color-primary); + color: var(--color-white); +} + +.header__inner { + max-width: var(--max-width); + margin: 0 auto; + padding: 20px 24px; + display: flex; + align-items: center; + gap: 16px; +} + +.header__logo { + width: 40px; + height: 40px; + border-radius: 50%; + background: var(--color-white); + display: flex; + align-items: center; + justify-content: center; + font-weight: 800; + color: var(--color-primary); + font-size: 18px; + flex-shrink: 0; +} + +.header__kommune { + font-size: 12px; + letter-spacing: 1.5px; + text-transform: uppercase; + opacity: 0.7; + margin-bottom: 2px; +} + +.header__title { + font-size: 20px; + font-weight: 600; + letter-spacing: -0.3px; +} + +.header__user { + margin-left: auto; + display: flex; + align-items: center; + gap: 12px; + font-size: 14px; +} + +.header__user-name { + opacity: 0.8; +} + +.header__logout { + color: var(--color-white); + text-decoration: underline; + text-underline-offset: 3px; + cursor: pointer; + background: none; + border: none; + font-size: 14px; + font-family: inherit; +} + +.header__logout:hover { + opacity: 0.8; +} + +/* ========================================================================== + Main content + ========================================================================== */ + +.main { + max-width: var(--max-width); + margin: 0 auto; + padding: 28px 24px 60px; + width: 100%; + flex: 1; +} + +/* ========================================================================== + Footer + ========================================================================== */ + +.footer { + background: var(--color-primary); + color: var(--color-white); + padding: 20px 24px; + margin-top: auto; +} + +.footer__inner { + max-width: var(--max-width); + margin: 0 auto; + font-size: 14px; + opacity: 0.7; +} + +/* ========================================================================== + Status Banner + ========================================================================== */ + +.status-banner { + border: 2px solid; + border-radius: var(--radius-lg); + padding: 24px 28px; + display: flex; + align-items: center; + gap: 20px; + margin-bottom: 28px; +} + +.status-banner__icon { + width: 56px; + height: 56px; + border-radius: 50%; + color: var(--color-white); + display: flex; + align-items: center; + justify-content: center; + font-size: 28px; + font-weight: 700; + flex-shrink: 0; +} + +.status-banner__content { + flex: 1; +} + +.status-banner__label { + font-size: 22px; + font-weight: 700; + margin-bottom: 4px; +} + +.status-banner__desc { + font-size: 15px; + color: #333; +} + +.status-banner__amount { + text-align: right; + flex-shrink: 0; +} + +.status-banner__amount-label { + font-size: 13px; + color: var(--color-text-secondary); + margin-bottom: 2px; +} + +.status-banner__amount-value { + font-size: 28px; + font-weight: 700; + letter-spacing: -0.5px; +} + +/* ========================================================================== + Tabs + ========================================================================== */ + +.tabs__list { + display: flex; + gap: 0; + margin-bottom: 24px; + border-bottom: 2px solid var(--color-border); +} + +.tabs__tab { + padding: 12px 24px; + font-size: 15px; + font-weight: 400; + color: var(--color-text-secondary); + background: none; + border: none; + border-bottom: 3px solid transparent; + cursor: pointer; + margin-bottom: -2px; + transition: all 0.15s; + font-family: inherit; +} + +.tabs__tab--active { + font-weight: 700; + color: var(--color-primary); + border-bottom-color: var(--color-primary); +} + +.tabs__tab:hover { + color: var(--color-primary); +} + +.tabs__panel--hidden { + display: none; +} + +/* ========================================================================== + Filter Bar + ========================================================================== */ + +.filter-bar { + display: flex; + gap: 8px; + margin-bottom: 20px; + flex-wrap: wrap; +} + +.filter-bar__btn { + padding: 8px 16px; + border-radius: 20px; + font-size: 14px; + border: 2px solid #ccc; + background: var(--color-white); + color: #333; + cursor: pointer; + font-weight: 500; + text-decoration: none; + display: inline-block; + transition: all 0.15s; + font-family: inherit; +} + +.filter-bar__btn:hover { + border-color: var(--color-primary); +} + +.filter-bar__btn--active { + border-color: var(--color-primary); + background: var(--color-primary); + color: var(--color-white); +} + +/* ========================================================================== + Charge List & Cards + ========================================================================== */ + +.charge-list { + display: flex; + flex-direction: column; + gap: 10px; +} + +.charge-list__empty { + text-align: center; + padding: 40px; + color: var(--color-text-secondary); +} + +.charge-card { + background: var(--color-white); + border-radius: var(--radius); + padding: 18px 22px; + display: flex; + align-items: center; + gap: 16px; + box-shadow: var(--shadow); + border-left: 4px solid; + text-decoration: none; + color: inherit; + transition: box-shadow 0.15s; + cursor: pointer; +} + +.charge-card:hover { + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); +} + +.charge-card__info { + flex: 1; +} + +.charge-card__type { + font-weight: 600; + font-size: 16px; + color: var(--color-text); + margin-bottom: 4px; +} + +.charge-card__meta { + font-size: 13px; + color: var(--color-text-light); +} + +.charge-card__amount { + font-size: 18px; + font-weight: 600; + color: var(--color-text); + margin-right: 12px; + white-space: nowrap; +} + +/* ========================================================================== + Charge Badge + ========================================================================== */ + +.charge-badge { + padding: 5px 14px; + border-radius: 16px; + font-size: 13px; + font-weight: 600; + white-space: nowrap; + display: inline-block; +} + +.charge-badge--large { + padding: 8px 20px; + font-size: 15px; +} + +/* ========================================================================== + History + ========================================================================== */ + +.history { + background: var(--color-white); + border-radius: var(--radius-lg); + padding: 28px; + box-shadow: var(--shadow); +} + +.history__title { + font-size: 18px; + font-weight: 700; + color: var(--color-text); + margin: 0 0 4px; +} + +.history__subtitle { + font-size: 14px; + color: var(--color-text-light); + margin: 0 0 24px; +} + +.history__chart { + margin-bottom: 24px; + height: 320px; +} + +.history__arrears { + margin-top: 24px; + border-top: 1px solid #eee; + padding-top: 20px; +} + +.history__arrears-title { + font-size: 15px; + font-weight: 600; + color: var(--color-text); + margin: 0 0 12px; +} + +.history__arrears-list { + display: flex; + gap: 12px; + flex-wrap: wrap; +} + +.arrears-badge { + background: var(--color-danger-bg); + border: 1px solid #FFCDD2; + border-radius: 8px; + padding: 10px 16px; + display: flex; + align-items: center; + gap: 10px; +} + +.arrears-badge__year { + font-weight: 600; + color: var(--color-danger); +} + +.arrears-badge__amount { + font-size: 14px; + color: var(--color-text-secondary); +} + +.history__no-arrears { + color: var(--color-success); + font-weight: 500; +} + +.history__table-toggle { + margin-top: 24px; +} + +.history__table-toggle summary { + font-size: 14px; + color: var(--color-primary); + cursor: pointer; + font-weight: 500; +} + +/* ========================================================================== + History Table + ========================================================================== */ + +.history-table { + width: 100%; + margin-top: 12px; + border-collapse: collapse; + font-size: 14px; +} + +.history-table thead tr { + border-bottom: 2px solid var(--color-border); +} + +.history-table th { + text-align: left; + padding: 8px 12px; + color: #444; + font-weight: 600; +} + +.history-table td { + padding: 8px 12px; +} + +.history-table tbody tr { + border-bottom: 1px solid #eee; +} + +/* ========================================================================== + Charge Detail + ========================================================================== */ + +.charge-detail { + max-width: 640px; +} + +.charge-detail__back { + display: inline-block; + margin-bottom: 20px; + color: var(--color-primary); + text-decoration: none; + font-weight: 500; + font-size: 14px; + cursor: pointer; + background: none; + border: none; + font-family: inherit; +} + +.charge-detail__back:hover { + text-decoration: underline; +} + +.charge-detail__card { + background: var(--color-white); + border-radius: var(--radius-lg); + padding: 28px; + box-shadow: var(--shadow); + border-left: 4px solid; +} + +.charge-detail__header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 24px; +} + +.charge-detail__title { + font-size: 24px; + font-weight: 700; +} + +.charge-detail__info { + display: flex; + flex-direction: column; + gap: 0; +} + +.charge-detail__row { + display: flex; + justify-content: space-between; + padding: 12px 0; + border-bottom: 1px solid #eee; +} + +.charge-detail__row:last-child { + border-bottom: none; +} + +.charge-detail__row dt { + color: var(--color-text-secondary); + font-weight: 500; +} + +.charge-detail__row dd { + font-weight: 600; +} + +.charge-detail__amount { + font-size: 20px; + color: var(--color-text); +} + +/* ========================================================================== + Login + ========================================================================== */ + +.login { + display: flex; + justify-content: center; + align-items: center; + min-height: 60vh; + padding: 40px 24px; +} + +.login__card { + background: var(--color-white); + border-radius: var(--radius-lg); + padding: 40px; + box-shadow: var(--shadow); + max-width: 420px; + width: 100%; +} + +.login__title { + font-size: 24px; + font-weight: 700; + margin-bottom: 8px; +} + +.login__subtitle { + font-size: 14px; + color: var(--color-text-secondary); + margin-bottom: 24px; +} + +.login__form { + display: flex; + flex-direction: column; + gap: 16px; +} + +.login__hint { + margin-top: 24px; + font-size: 13px; + color: var(--color-text-secondary); +} + +.login__hint summary { + cursor: pointer; + color: var(--color-primary); + font-weight: 500; +} + +.login__test-users { + list-style: none; + margin: 8px 0; +} + +.login__test-users li { + padding: 4px 0; +} + +.login__test-users code { + background: #f0f0f0; + padding: 2px 6px; + border-radius: 4px; + font-size: 13px; +} + +/* ========================================================================== + Form elements + ========================================================================== */ + +.form-group { + display: flex; + flex-direction: column; + gap: 6px; +} + +.form-label { + font-size: 14px; + font-weight: 600; + color: var(--color-text); +} + +.form-input { + padding: 10px 14px; + border: 2px solid var(--color-border); + border-radius: 8px; + font-size: 16px; + font-family: inherit; + transition: border-color 0.15s; +} + +.form-input:focus { + border-color: var(--color-primary); +} + +/* ========================================================================== + Buttons + ========================================================================== */ + +.btn { + padding: 12px 24px; + border-radius: 8px; + font-size: 16px; + font-weight: 600; + border: none; + cursor: pointer; + font-family: inherit; + text-align: center; + text-decoration: none; + display: inline-block; + transition: opacity 0.15s; +} + +.btn:hover { + opacity: 0.9; +} + +.btn--primary { + background: var(--color-primary); + color: var(--color-white); +} + +.btn--full { + width: 100%; +} + +/* ========================================================================== + Utility classes + ========================================================================== */ + +.text-right { text-align: right; } +.text-danger { color: var(--color-danger); } +.text-success { color: var(--color-success); } +.font-medium { font-weight: 500; } +.font-semibold { font-weight: 600; } + +/* ========================================================================== + Responsive + ========================================================================== */ + +@media (max-width: 768px) { + .header__inner { + flex-wrap: wrap; + } + + .header__user { + width: 100%; + justify-content: flex-end; + margin-top: 8px; + } + + .status-banner { + flex-direction: column; + text-align: center; + gap: 16px; + padding: 20px; + } + + .status-banner__amount { + text-align: center; + } + + .charge-card { + flex-wrap: wrap; + } + + .charge-card__amount { + margin-right: 0; + } + + .charge-detail__header { + flex-direction: column; + align-items: flex-start; + gap: 12px; + } +} + +@media (max-width: 480px) { + .main { + padding: 16px; + } + + .tabs__tab { + padding: 10px 16px; + font-size: 14px; + } + + .filter-bar__btn { + padding: 6px 12px; + font-size: 13px; + } + + .status-banner__label { + font-size: 18px; + } + + .status-banner__amount-value { + font-size: 22px; + } + + .login__card { + padding: 24px; + } +}