eindelijk weer eens een push

This commit is contained in:
allard
2026-05-31 16:07:30 +02:00
parent 01cff8e165
commit ff21c258e0
2747 changed files with 302316 additions and 131101 deletions
+4
View File
@@ -0,0 +1,4 @@
FROM couchdb
COPY serveradmin.ini /opt/couchdb/etc/local.d/
COPY allard.txt /opt/couchdb/etc/
+27
View File
@@ -0,0 +1,27 @@
Step 1
======
kubectl apply -f couchdb.yaml
step 2
======
kubectl port-forward pod/couchdb-0 5984:5984 -n couchdb
dan in andere terminal:
curl -X PUT http://admin:Couchdb01@localhost:5984/_users
curl -X PUT http://admin:Couchdb01@localhost:5984/_replicator
curl -X PUT http://admin:Couchdb01@localhost:5984/_global_changes
{"ok":true}
{"ok":true}
{"ok":true}
verify:
curl http://admin:Couchdb01@localhost:5984/_all_dbs
check cluster status:
curl -s -u admin:Couchdb01 https://couchdb-prod.allarddcs.nl/_membership
+13
View File
@@ -0,0 +1,13 @@
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install --omit=dev
COPY server.js ./
COPY public/ ./public/
EXPOSE 3000
CMD ["node", "server.js"]
+84
View File
@@ -0,0 +1,84 @@
---
# Secret CouchDB credentials
apiVersion: v1
kind: Secret
metadata:
name: couchdb-credentials
namespace: couchdb
type: Opaque
stringData:
username: admin
password: Couchdb01
url: http://admin:Couchdb01@couchdb.couchdb.svc.cluster.local:5984
---
# Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
name: aangifte
namespace: couchdb
labels:
app: aangifte
spec:
replicas: 1
selector:
matchLabels:
app: aangifte
template:
metadata:
labels:
app: aangifte
spec:
containers:
- name: aangifte
image: allardkrings/aangifte
imagePullPolicy: Always
ports:
- containerPort: 8080
env:
- name: COUCHDB_URL
valueFrom:
secretKeyRef:
name: couchdb-credentials
key: url
resources:
requests:
memory: "128Mi"
cpu: "100m"
limits:
memory: "256Mi"
cpu: "250m"
---
# Service
apiVersion: v1
kind: Service
metadata:
name: aangifte
namespace: couchdb
spec:
selector:
app: aangifte
ports:
- port: 8080
targetPort: 8080
---
# IngressRoute
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: aangifte
namespace: couchdb
spec:
entryPoints:
- websecure
routes:
- match: Host(`aangifte-prod.allarddcs.nl`)
kind: Rule
services:
- name: aangifte
port: 8080
tls:
certResolver: letsencrypt
File diff suppressed because it is too large Load Diff
+13
View File
@@ -0,0 +1,13 @@
{
"name": "aangifte-app",
"version": "1.0.0",
"description": "Belastingdienst aangifte app with CouchDB backend",
"main": "server.js",
"scripts": {
"start": "node server.js"
},
"dependencies": {
"express": "^4.18.2",
"http-proxy-middleware": "^3.0.0"
}
}
+585
View File
@@ -0,0 +1,585 @@
<!DOCTYPE html>
<html lang="nl">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Mijn Belastingdienst Aangifte Inkomstenbelasting 2024</title>
<link href="https://fonts.googleapis.com/css2?family=Source+Sans+3:wght@300;400;600;700&display=swap" rel="stylesheet">
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: 'Source Sans 3', Arial, sans-serif; font-size: 14px; color: #1a1a1a; background: #fff; }
.top-logo-bar { border-bottom: 1px solid #ccc; padding: 10px 20px; display: flex; justify-content: center; align-items: center; }
.logo-placeholder { display: flex; align-items: center; gap: 10px; }
.logo-shield { width: 44px; height: 44px; background: #154273; border-radius: 2px; display: flex; align-items: center; justify-content: center; }
.logo-shield svg { width: 28px; height: 28px; }
.logo-site-name { font-size: 15px; color: #333; font-weight: 400; }
.main-header { background: #c8daea; padding: 12px 24px; display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid #a8c0d8; }
.main-header .app-title { font-size: 20px; font-weight: 700; color: #1a1a1a; }
.main-header .user-name { font-size: 18px; font-weight: 700; color: #1a1a1a; }
.app-body { display: flex; min-height: calc(100vh - 120px); }
.sidebar { width: 280px; min-width: 280px; border-right: 1px solid #ddd; padding: 20px 0; background: #fff; overflow-y: auto; }
.sidebar-title { font-size: 14px; font-weight: 700; color: #1a1a1a; padding: 0 16px 12px 16px; border-bottom: 1px solid #eee; margin-bottom: 8px; }
.nav-tree { list-style: none; }
.nav-item { display: flex; align-items: center; gap: 6px; padding: 5px 16px; font-size: 13px; color: #1a1a1a; cursor: pointer; line-height: 1.4; }
.nav-item:hover { background: #e8f0f8; }
.nav-item.active { background: #dce8f5; font-weight: 600; color: #154273; }
.nav-item.section-header { font-weight: 600; padding-top: 8px; }
.nav-sub { list-style: none; padding-left: 24px; }
.nav-sub .nav-item { font-weight: 400; }
.main-content { flex: 1; padding: 28px 36px; max-width: 820px; }
.content-title { font-size: 18px; font-weight: 700; margin-bottom: 4px; }
.content-subtitle { font-size: 16px; font-weight: 700; margin-bottom: 20px; color: #1a1a1a; }
.employer-block { border: 1px solid #ccc; border-radius: 3px; margin-bottom: 24px; background: #fff; }
.employer-header { background: #f5f5f5; border-bottom: 1px solid #ccc; padding: 10px 16px; display: flex; align-items: center; justify-content: space-between; cursor: pointer; user-select: none; }
.employer-header-left { display: flex; align-items: center; gap: 8px; font-weight: 700; font-size: 14px; }
.collapse-icon { color: #154273; font-size: 12px; font-weight: 700; }
.btn-verwijderen { background: none; border: none; color: #154273; font-size: 13px; cursor: pointer; display: flex; align-items: center; gap: 5px; font-family: inherit; text-decoration: underline; }
.btn-verwijderen:hover { color: #0f2f52; }
.employer-body { padding: 20px; }
.form-row { display: flex; align-items: flex-start; margin-bottom: 16px; gap: 16px; }
.form-label-col { min-width: 200px; font-size: 14px; padding-top: 8px; display: flex; align-items: center; gap: 5px; flex-shrink: 0; }
.form-value-col { flex: 1; }
.help-btn { display: inline-flex; align-items: center; justify-content: center; width: 18px; height: 18px; border: 1px solid #154273; color: #154273; border-radius: 2px; font-size: 11px; font-weight: 700; cursor: pointer; flex-shrink: 0; background: white; font-family: inherit; }
.tooltip { display: none; position: absolute; background: #154273; color: white; padding: 8px 12px; border-radius: 3px; font-size: 12px; max-width: 240px; line-height: 1.5; z-index: 100; box-shadow: 0 2px 8px rgba(0,0,0,0.25); }
.tooltip::before { content: ''; position: absolute; top: -6px; left: 12px; border: 6px solid transparent; border-bottom-color: #154273; border-top: none; }
.help-container { position: relative; display: inline-block; }
.help-container:hover .tooltip { display: block; top: 100%; margin-top: 4px; left: 0; }
.bd-text-input { width: 100%; max-width: 320px; border: 1px solid #aaa; border-radius: 2px; padding: 6px 10px; font-size: 14px; font-family: inherit; background: #eaf3fb; color: #1a1a1a; }
.bd-text-input:focus { outline: none; border-color: #154273; box-shadow: 0 0 0 2px rgba(21,66,115,0.2); }
.bd-text-input[readonly] { background: #f0f0f0; color: #555; }
.money-row { display: flex; gap: 24px; margin-bottom: 16px; align-items: flex-start; }
.money-col { display: flex; flex-direction: column; gap: 4px; }
.money-label { font-size: 14px; display: flex; align-items: center; gap: 5px; margin-bottom: 4px; }
.money-input-wrap { display: flex; align-items: center; border: 1px solid #aaa; border-radius: 2px; background: #eaf3fb; overflow: hidden; width: 130px; }
.money-input-wrap .euro-sign { padding: 6px 8px; background: #eaf3fb; font-size: 14px; color: #333; border-right: 1px solid #aaa; flex-shrink: 0; }
.money-input-wrap input { border: none; background: #eaf3fb; padding: 6px 8px; font-size: 14px; font-family: inherit; width: 100%; text-align: right; }
.money-input-wrap input:focus { outline: none; }
.sub-section-title { font-size: 14px; font-weight: 700; margin: 20px 0 12px 0; }
.radio-row { display: flex; align-items: center; margin-top: 4px; }
.radio-option { display: flex; align-items: center; gap: 5px; padding: 5px 12px 5px 0; cursor: pointer; font-size: 14px; }
.radio-option input[type="radio"] { accent-color: #154273; width: 15px; height: 15px; }
.btn-toevoegen { background: none; border: 1px solid #154273; color: #154273; padding: 6px 16px; font-size: 13px; font-family: inherit; border-radius: 2px; cursor: pointer; margin-top: 8px; display: flex; align-items: center; gap: 6px; }
.btn-toevoegen:hover { background: #e8f0f8; }
.bottom-nav { display: flex; gap: 12px; margin-top: 32px; padding-top: 20px; border-top: 1px solid #ddd; align-items: center; }
.btn-primary { background: #154273; color: white; border: none; padding: 9px 24px; font-size: 14px; font-weight: 600; font-family: inherit; border-radius: 2px; cursor: pointer; }
.btn-primary:hover { background: #1a5296; }
.btn-secondary { background: white; color: #154273; border: 1px solid #154273; padding: 8px 20px; font-size: 14px; font-family: inherit; border-radius: 2px; cursor: pointer; }
.btn-secondary:hover { background: #e8f0f8; }
.pension-block { border: 1px solid #ccc; border-radius: 3px; margin-bottom: 16px; background: #fff; }
.pension-header { background: #f5f5f5; border-bottom: 1px solid #ccc; padding: 9px 16px; display: flex; align-items: center; justify-content: space-between; cursor: pointer; font-weight: 700; font-size: 13px; }
.section-tabs { display: none; }
.section-tabs.active-section { display: block; }
.info-text { font-size: 13px; color: #555; margin-bottom: 16px; line-height: 1.6; }
.status-bar { font-size: 12px; color: #555; display: flex; align-items: center; gap: 8px; }
.status-dot { width: 8px; height: 8px; border-radius: 50%; background: #ccc; flex-shrink: 0; }
.status-dot.saved { background: #39870c; }
.status-dot.saving { background: #e17000; animation: pulse 1s infinite; }
.status-dot.error { background: #d52b1e; }
@keyframes pulse { 0%,100% { opacity:1; } 50% { opacity:0.3; } }
.toast { position: fixed; bottom: 24px; right: 24px; padding: 12px 20px; border-radius: 3px; font-size: 13px; font-weight: 600; box-shadow: 0 4px 16px rgba(0,0,0,0.2); transform: translateY(60px); opacity: 0; transition: all 0.3s ease; z-index: 1000; }
.toast.show { transform: translateY(0); opacity: 1; }
.toast.success { background: #39870c; color: white; }
.toast.error { background: #d52b1e; color: white; }
@media (max-width: 768px) { .sidebar { display: none; } .main-content { padding: 16px; } .money-row { flex-wrap: wrap; } }
</style>
</head>
<body>
<div class="top-logo-bar">
<div class="logo-placeholder">
<div class="logo-shield">
<svg viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M14 2L4 7v7c0 5.5 4.3 10.7 10 12 5.7-1.3 10-6.5 10-12V7L14 2z" fill="white" opacity="0.9"/>
<text x="9" y="19" font-size="12" font-weight="bold" fill="#154273" font-family="Arial">BD</text>
</svg>
</div>
<span class="logo-site-name">Belastingdienst</span>
</div>
</div>
<div class="main-header">
<span class="app-title">Mijn Belastingdienst</span>
<span class="user-name">A C KRINGS</span>
</div>
<div class="app-body">
<nav class="sidebar">
<div class="sidebar-title">Aangifte Inkomstenbelasting 2024</div>
<ul class="nav-tree">
<li><span class="nav-item" onclick="showSection('intro')">Introductie</span></li>
<li><span class="nav-item" onclick="showSection('persoonlijk')">Persoonlijke gegevens A C KRINGS</span></li>
<li><span class="nav-item" onclick="showSection('partner')">Partner</span></li>
<li><span class="nav-item" onclick="showSection('vooraf')">Vooraf ingevulde gegevens</span></li>
<li><span class="nav-item" onclick="showSection('partner-gegevens')">Persoonlijke gegevens partner</span></li>
<li><span class="nav-item" onclick="showSection('kinderen')">Persoonlijke gegevens kinderen</span></li>
<li><span class="nav-item" onclick="showSection('onderneming')">Onderneming</span></li>
<li>
<span class="nav-item section-header" onclick="toggleNav('nav-inkomsten')">
<span id="chev-nav-inkomsten"></span> Inkomsten
</span>
<ul class="nav-sub" id="nav-inkomsten">
<li><span class="nav-item active" onclick="showSection('loondienst')">Inkomsten uit loondienst</span></li>
<li><span class="nav-item" onclick="showSection('pensioen')">Pensioen en andere uitkeringen</span></li>
<li><span class="nav-item" onclick="showSection('overige-inkomsten')">Overige inkomsten</span></li>
<li><span class="nav-item" onclick="showSection('beschikbaar')">Inkomsten uit beschikbaar gestelde bezittingen</span></li>
<li><span class="nav-item" onclick="showSection('partneralimentatie')">Partneralimentatie</span></li>
</ul>
</li>
<li><span class="nav-item" onclick="showSection('woningen')">Woningen en andere onroerende zaken</span></li>
<li>
<span class="nav-item section-header" onclick="toggleNav('nav-bank')">
<span id="chev-nav-bank"></span> Bankrekeningen en andere bezittingen
</span>
<ul class="nav-sub" id="nav-bank" style="display:none">
<li><span class="nav-item" onclick="showSection('bank')">Bank- en spaarrekeningen</span></li>
<li><span class="nav-item" onclick="showSection('aanmerkelijk')">Aanmerkelijk belang</span></li>
<li><span class="nav-item" onclick="showSection('beleggingen')">Beleggingen</span></li>
<li><span class="nav-item" onclick="showSection('kapitaalverzekeringen')">Kapitaalverzekeringen</span></li>
<li><span class="nav-item" onclick="showSection('bouwdepots')">Bouwdepots</span></li>
<li><span class="nav-item" onclick="showSection('andere-bezittingen')">Andere bezittingen</span></li>
</ul>
</li>
<li>
<span class="nav-item section-header" onclick="toggleNav('nav-hypotheken')">
<span id="chev-nav-hypotheken"></span> Hypotheken en andere schulden
</span>
<ul class="nav-sub" id="nav-hypotheken">
<li><span class="nav-item" onclick="showSection('hypotheken')">Hypotheken en leningen woning</span></li>
</ul>
</li>
<li>
<span class="nav-item section-header" onclick="toggleNav('nav-uitgaven')">
<span id="chev-nav-uitgaven"></span> Uitgaven
</span>
<ul class="nav-sub" id="nav-uitgaven" style="display:none">
<li><span class="nav-item" onclick="showSection('lijfrente')">Uitgaven voor lijfrente</span></li>
<li><span class="nav-item" onclick="showSection('zorgkosten')">Zorgkosten</span></li>
<li><span class="nav-item" onclick="showSection('giften')">Giften</span></li>
</ul>
</li>
</ul>
</nav>
<main class="main-content">
<!-- LOONDIENST -->
<div id="section-loondienst" class="section-tabs active-section">
<div class="content-title">Inkomsten uit loondienst</div>
<div class="content-subtitle">A C KRINGS</div>
<div class="employer-block" id="employer-0">
<div class="employer-header" onclick="toggleEmployer(0)">
<div class="employer-header-left">
<span class="collapse-icon" id="emp-icon-0"></span>
Werkgever: MINISTERIE VAN FINANCIEN
</div>
<button class="btn-verwijderen" onclick="event.stopPropagation()">
<span>🗑</span> verwijderen
</button>
</div>
<div class="employer-body" id="emp-body-0">
<div class="form-row">
<div class="form-label-col">Soort inkomsten: <div class="help-container"><button class="help-btn">?</button><div class="tooltip">Het soort inkomsten bepaalt hoe de loonheffing wordt berekend.</div></div></div>
<div class="form-value-col"><input class="bd-text-input" type="text" value="Loon binnen Nederland" readonly></div>
</div>
<div class="form-row">
<div class="form-label-col">Werkgever: <div class="help-container"><button class="help-btn">?</button><div class="tooltip">De naam van uw werkgever zoals vermeld op uw jaaropgave.</div></div></div>
<div class="form-value-col"><input class="bd-text-input" type="text" id="wg0-naam" value="MINISTERIE VAN FINANCIEN"></div>
</div>
<div class="money-row">
<div class="money-col">
<div class="money-label">Loon: <div class="help-container"><button class="help-btn">?</button><div class="tooltip">Brutoloon van uw jaaropgave (rubriek 1).</div></div></div>
<div class="money-input-wrap"><span class="euro-sign"></span><input type="number" id="wg0-loon" value="118231"></div>
</div>
<div class="money-col">
<div class="money-label">Loonheffing: <div class="help-container"><button class="help-btn">?</button><div class="tooltip">Ingehouden loonheffing (rubriek 2).</div></div></div>
<div class="money-input-wrap"><span class="euro-sign"></span><input type="number" id="wg0-loonheffing" value="41992"></div>
</div>
<div class="money-col">
<div class="money-label">Arbeidskorting: <div class="help-container"><button class="help-btn">?</button><div class="tooltip">Arbeidskorting verrekend door werkgever (rubriek 5).</div></div></div>
<div class="money-input-wrap"><span class="euro-sign"></span><input type="number" id="wg0-arbeidskorting" value="869"></div>
</div>
</div>
<div class="form-row">
<div class="form-label-col" style="align-self:flex-start;padding-top:4px">
Andere inkomsten bij deze werkgever waarop geen loonheffing wordt ingehouden (zoals fooien):
<div class="help-container" style="margin-left:4px"><button class="help-btn">?</button><div class="tooltip">Bijv. fooien of andere inkomsten die niet op de loonstrook staan.</div></div>
</div>
<div class="form-value-col" style="padding-top:4px">
<div class="money-input-wrap"><span class="euro-sign"></span><input type="number" id="wg0-andere" value="0"></div>
</div>
</div>
<div class="sub-section-title">Reiskosten MINISTERIE VAN FINANCIEN</div>
<div class="form-row">
<div class="form-label-col">Reisde U met openbaar vervoer naar het werk?: <div class="help-container"><button class="help-btn">?</button><div class="tooltip">U kunt reiskosten aftrekken als u met OV reisde zonder volledige vergoeding.</div></div></div>
<div class="form-value-col">
<div class="radio-row">
<label class="radio-option"><input type="radio" name="reiskosten-0" value="ja"> ja</label>
<label class="radio-option"><input type="radio" name="reiskosten-0" value="nee" checked> nee</label>
</div>
</div>
</div>
</div>
</div>
<button class="btn-toevoegen"> Werkgever toevoegen</button>
<div class="bottom-nav">
<button class="btn-primary" onclick="opslaanEnVolgende('pensioen')">Volgende ▶</button>
<button class="btn-secondary" onclick="showSection('vooraf')">◀ Vorige</button>
<button class="btn-secondary" onclick="opslaan()">💾 Opslaan</button>
<div class="status-bar"><div class="status-dot" id="statusDot"></div><span id="statusText">Niet opgeslagen</span></div>
</div>
</div>
<!-- PENSIOEN -->
<div id="section-pensioen" class="section-tabs">
<div class="content-title">Pensioen en andere uitkeringen</div>
<div class="content-subtitle">A C KRINGS</div>
<p class="info-text">Controleer de bedragen aan de hand van uw jaaropgaven.</p>
<div class="pension-block">
<div class="pension-header" onclick="togglePension(this)">
<span>∧ Nationale Nederlanden € 646</span>
<button class="btn-verwijderen" onclick="event.stopPropagation()">🗑 verwijderen</button>
</div>
<div class="employer-body">
<div class="money-row">
<div class="money-col"><div class="money-label">Loon:</div><div class="money-input-wrap"><span class="euro-sign"></span><input type="number" id="p0-loon" value="646"></div></div>
<div class="money-col"><div class="money-label">Loonheffing:</div><div class="money-input-wrap"><span class="euro-sign"></span><input type="number" id="p0-heffing" value="114"></div></div>
</div>
</div>
</div>
<div class="pension-block">
<div class="pension-header" onclick="togglePension(this)">
<span>∧ ABP € 45.926</span>
<button class="btn-verwijderen" onclick="event.stopPropagation()">🗑 verwijderen</button>
</div>
<div class="employer-body">
<div class="money-row">
<div class="money-col"><div class="money-label">Loon:</div><div class="money-input-wrap"><span class="euro-sign"></span><input type="number" id="p1-loon" value="45926"></div></div>
<div class="money-col"><div class="money-label">Loonheffing:</div><div class="money-input-wrap"><span class="euro-sign"></span><input type="number" id="p1-heffing" value="10149"></div></div>
</div>
</div>
</div>
<div class="pension-block">
<div class="pension-header" onclick="togglePension(this)">
<span>∧ Nationale Nederlanden € 472</span>
<button class="btn-verwijderen" onclick="event.stopPropagation()">🗑 verwijderen</button>
</div>
<div class="employer-body">
<div class="money-row">
<div class="money-col"><div class="money-label">Loon:</div><div class="money-input-wrap"><span class="euro-sign"></span><input type="number" id="p2-loon" value="472"></div></div>
<div class="money-col"><div class="money-label">Loonheffing:</div><div class="money-input-wrap"><span class="euro-sign"></span><input type="number" id="p2-heffing" value="82"></div></div>
</div>
</div>
</div>
<div class="pension-block">
<div class="pension-header" onclick="togglePension(this)">
<span>∧ UWV € 13.336</span>
<button class="btn-verwijderen" onclick="event.stopPropagation()">🗑 verwijderen</button>
</div>
<div class="employer-body">
<div class="money-row">
<div class="money-col"><div class="money-label">Loon:</div><div class="money-input-wrap"><span class="euro-sign"></span><input type="number" id="p3-loon" value="13336"></div></div>
<div class="money-col"><div class="money-label">Loonheffing:</div><div class="money-input-wrap"><span class="euro-sign"></span><input type="number" id="p3-heffing" value="2538"></div></div>
</div>
</div>
</div>
<button class="btn-toevoegen"> Uitkering toevoegen</button>
<div class="bottom-nav">
<button class="btn-primary" onclick="opslaanEnVolgende('woningen')">Volgende ▶</button>
<button class="btn-secondary" onclick="showSection('loondienst')">◀ Vorige</button>
<button class="btn-secondary" onclick="opslaan()">💾 Opslaan</button>
<div class="status-bar"><div class="status-dot" id="statusDot2"></div><span id="statusText2">Niet opgeslagen</span></div>
</div>
</div>
<!-- WONINGEN -->
<div id="section-woningen" class="section-tabs">
<div class="content-title">Woningen en andere onroerende zaken</div>
<div class="content-subtitle">A C KRINGS</div>
<div class="employer-block">
<div class="employer-header">
<div class="employer-header-left">∧ Warnsveld Postcode 7231GH nr. 42</div>
<button class="btn-verwijderen">🗑 verwijderen</button>
</div>
<div class="employer-body">
<div class="form-row">
<div class="form-label-col">Situatie begin jaar:</div>
<div class="form-value-col"><input class="bd-text-input" type="text" value="woning: Hoofdverblijf" readonly></div>
</div>
<div class="form-row">
<div class="form-label-col">WOZ-waarde:</div>
<div class="form-value-col"><div class="money-input-wrap"><span class="euro-sign"></span><input type="number" id="woz-waarde" value="528000"></div></div>
</div>
<div class="form-row">
<div class="form-label-col">Verhuurd in 2024?</div>
<div class="form-value-col"><div class="radio-row">
<label class="radio-option"><input type="radio" name="verhuurd" value="ja"> ja</label>
<label class="radio-option"><input type="radio" name="verhuurd" value="nee" checked> nee</label>
</div></div>
</div>
<div class="form-row">
<div class="form-label-col">Erfpachtcanon betaald?</div>
<div class="form-value-col"><div class="radio-row">
<label class="radio-option"><input type="radio" name="erfpacht" value="ja"> ja</label>
<label class="radio-option"><input type="radio" name="erfpacht" value="nee" checked> nee</label>
</div></div>
</div>
</div>
</div>
<div class="bottom-nav">
<button class="btn-primary" onclick="opslaanEnVolgende('hypotheken')">Volgende ▶</button>
<button class="btn-secondary" onclick="showSection('pensioen')">◀ Vorige</button>
<button class="btn-secondary" onclick="opslaan()">💾 Opslaan</button>
</div>
</div>
<!-- HYPOTHEKEN -->
<div id="section-hypotheken" class="section-tabs">
<div class="content-title">Hypotheken en andere schulden</div>
<div class="content-subtitle">A C KRINGS</div>
<div class="employer-block">
<div class="employer-header">
<div class="employer-header-left">∧ BijBouwe nr. 290396101</div>
<button class="btn-verwijderen">🗑 verwijderen</button>
</div>
<div class="employer-body">
<div class="money-row">
<div class="money-col"><div class="money-label">Schuld 01-01-2024:</div><div class="money-input-wrap"><span class="euro-sign"></span><input type="number" value="217500"></div></div>
<div class="money-col"><div class="money-label">Schuld 31-12-2024:</div><div class="money-input-wrap"><span class="euro-sign"></span><input type="number" value="217500"></div></div>
<div class="money-col"><div class="money-label">Rente betaald:</div><div class="money-input-wrap"><span class="euro-sign"></span><input type="number" value="3546"></div></div>
</div>
<div class="form-row">
<div class="form-label-col">Schuld voor eigen woning?</div>
<div class="form-value-col"><div class="radio-row">
<label class="radio-option"><input type="radio" name="woning-schuld" value="ja" checked> ja</label>
<label class="radio-option"><input type="radio" name="woning-schuld" value="nee"> nee</label>
</div></div>
</div>
</div>
</div>
<div class="employer-block">
<div class="employer-header">
<div class="employer-header-left">∧ BijBouwe nr. 290396101 (aflossing)</div>
<button class="btn-verwijderen">🗑 verwijderen</button>
</div>
<div class="employer-body">
<div class="money-row">
<div class="money-col"><div class="money-label">Schuld 01-01-2024:</div><div class="money-input-wrap"><span class="euro-sign"></span><input type="number" value="88431"></div></div>
<div class="money-col"><div class="money-label">Schuld 31-12-2024:</div><div class="money-input-wrap"><span class="euro-sign"></span><input type="number" value="85263"></div></div>
<div class="money-col"><div class="money-label">Rente betaald:</div><div class="money-input-wrap"><span class="euro-sign"></span><input type="number" value="1423"></div></div>
</div>
</div>
</div>
<button class="btn-toevoegen"> Lening toevoegen</button>
<div class="bottom-nav">
<button class="btn-primary" onclick="opslaanEnVolgende('intro')">Opslaan &amp; afronden ▶</button>
<button class="btn-secondary" onclick="showSection('woningen')">◀ Vorige</button>
<button class="btn-secondary" onclick="opslaan()">💾 Opslaan</button>
</div>
</div>
<!-- Placeholders -->
<div id="section-intro" class="section-tabs"><div class="content-title">Introductie</div><p class="info-text" style="margin-top:12px">Welkom bij de aangifte Inkomstenbelasting 2024. Uw gegevens worden opgeslagen in CouchDB.</p><div class="bottom-nav"><button class="btn-primary" onclick="showSection('persoonlijk')">Volgende ▶</button></div></div>
<div id="section-persoonlijk" class="section-tabs"><div class="content-title">Persoonlijke gegevens A C KRINGS</div><div style="margin-top:12px"><div class="form-row"><div class="form-label-col">Naam:</div><div class="form-value-col"><input class="bd-text-input" type="text" value="A C KRINGS"></div></div><div class="form-row"><div class="form-label-col">Geboortedatum:</div><div class="form-value-col"><input class="bd-text-input" type="date" value="1956-05-22"></div></div><div class="form-row"><div class="form-label-col">Burgerservicenummer:</div><div class="form-value-col"><input class="bd-text-input" type="text" value="4444444444"></div></div></div><div class="bottom-nav"><button class="btn-primary" onclick="showSection('partner')">Volgende ▶</button></div></div>
<div id="section-partner" class="section-tabs"><div class="content-title">Partner</div><div style="margin-top:12px"><div class="form-row"><div class="form-label-col">Naam partner:</div><div class="form-value-col"><input class="bd-text-input" type="text" value="H M A VAN DER POL"></div></div><div class="form-row"><div class="form-label-col">Geboortedatum partner:</div><div class="form-value-col"><input class="bd-text-input" type="date" value="1960-05-15"></div></div><div class="form-row"><div class="form-label-col">Fiscale partner?</div><div class="form-value-col"><div class="radio-row"><label class="radio-option"><input type="radio" name="fiscaal" value="ja" checked> ja</label><label class="radio-option"><input type="radio" name="fiscaal" value="nee"> nee</label></div></div></div></div><div class="bottom-nav"><button class="btn-primary" onclick="showSection('loondienst')">Volgende ▶</button></div></div>
<div id="section-vooraf" class="section-tabs"><div class="content-title">Vooraf ingevulde gegevens</div><p class="info-text" style="margin-top:12px">Gegevens zijn vooraf ingevuld vanuit uw werkgever(s) en pensioenuitvoerder(s). Controleer zorgvuldig.</p><div class="bottom-nav"><button class="btn-primary" onclick="showSection('loondienst')">Naar inkomsten ▶</button></div></div>
<div id="section-partner-gegevens" class="section-tabs"><div class="content-title">Persoonlijke gegevens partner</div><p class="info-text" style="margin-top:12px">H M A VAN DER POL BSN: 3333333333</p></div>
<div id="section-kinderen" class="section-tabs"><div class="content-title">Persoonlijke gegevens kinderen</div><p class="info-text" style="margin-top:12px">2 kinderen: A M KRINGS (2005) en A C KRINGS (2006).</p></div>
<div id="section-onderneming" class="section-tabs"><div class="content-title">Onderneming</div><p class="info-text" style="margin-top:12px">Ondernemer IB: nee. Geldverstrekker: ja.</p></div>
<div id="section-overige-inkomsten" class="section-tabs"><div class="content-title">Overige inkomsten</div><p class="info-text" style="margin-top:12px">Geen overige inkomsten opgegeven.</p></div>
<div id="section-beschikbaar" class="section-tabs"><div class="content-title">Inkomsten uit beschikbaar gestelde bezittingen</div></div>
<div id="section-partneralimentatie" class="section-tabs"><div class="content-title">Partneralimentatie</div></div>
<div id="section-bank" class="section-tabs"><div class="content-title">Bank- en spaarrekeningen</div></div>
<div id="section-aanmerkelijk" class="section-tabs"><div class="content-title">Aanmerkelijk belang</div></div>
<div id="section-beleggingen" class="section-tabs"><div class="content-title">Beleggingen</div></div>
<div id="section-kapitaalverzekeringen" class="section-tabs"><div class="content-title">Kapitaalverzekeringen</div></div>
<div id="section-bouwdepots" class="section-tabs"><div class="content-title">Bouwdepots</div></div>
<div id="section-andere-bezittingen" class="section-tabs"><div class="content-title">Andere bezittingen</div></div>
<div id="section-lijfrente" class="section-tabs"><div class="content-title">Uitgaven voor lijfrente</div></div>
<div id="section-zorgkosten" class="section-tabs"><div class="content-title">Zorgkosten</div></div>
<div id="section-giften" class="section-tabs"><div class="content-title">Giften</div></div>
</main>
</div>
<div class="toast" id="toast"></div>
<script>
const DB = '/api/couchdb/aangifte';
const DOC_ID = 'aangifte-2024-allard';
let currentRev = null;
async function ensureDb() {
await fetch(`${DB}`, { method: 'PUT' });
}
function collectData() {
return {
_id: DOC_ID,
login_naam: 'allard',
jaar_aangifte: 2024,
naam: 'A C KRINGS',
geboortedatum: '1956-05-22',
burgerservicenummer: 4444444444,
naam_partner: 'H M A VAN DER POL',
geboortedatum_partner: '1960-05-15',
burgerservicenummer_partner: 3333333333,
inkomstenUitLoondienst: [{
werkgever: val('wg0-naam'),
soortInkomsten: 'loon',
loon: num('wg0-loon'),
loonheffing: num('wg0-loonheffing'),
arbeidskorting: num('wg0-arbeidskorting'),
andereInkomsten: num('wg0-andere') || null,
reiskosten: document.querySelector('input[name="reiskosten-0"]:checked')?.value === 'ja'
}],
pensioenEnAndereUitkeringen: [
{ verzekeringsmaatschappij: 'Nationale Nederlanden', loon: num('p0-loon'), loonheffing: num('p0-heffing') },
{ verzekeringsmaatschappij: 'ABP', loon: num('p1-loon'), loonheffing: num('p1-heffing') },
{ verzekeringsmaatschappij: 'Nationale Nederlanden', loon: num('p2-loon'), loonheffing: num('p2-heffing') },
{ verzekeringsmaatschappij: 'UWV', loon: num('p3-loon'), loonheffing: num('p3-heffing') },
],
woningen: [{
postcode: '7231GH', huisnummer: 42, woonplaats: 'Warnsveld',
woz_waarde: num('woz-waarde'),
verhuurd: document.querySelector('input[name="verhuurd"]:checked')?.value === 'ja',
erfpachtcanon: document.querySelector('input[name="erfpacht"]:checked')?.value === 'ja',
situatie_begin_jaar: 'woning: Hoofdverblijf'
}],
updated_at: new Date().toISOString()
};
}
async function opslaan() {
setStatus('saving', 'Opslaan...');
try {
await ensureDb();
const doc = collectData();
if (currentRev) doc._rev = currentRev;
const res = await fetch(`${DB}/${DOC_ID}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(doc)
});
const json = await res.json();
if (res.ok) {
currentRev = json.rev;
setStatus('saved', 'Opgeslagen ✓');
showToast('✓ Opgeslagen in CouchDB', 'success');
} else {
throw new Error(json.reason || res.statusText);
}
} catch (e) {
setStatus('error', 'Fout bij opslaan');
showToast('✗ Fout: ' + e.message, 'error');
}
}
async function laadDocument() {
try {
const res = await fetch(`${DB}/${DOC_ID}`);
if (res.ok) {
const doc = await res.json();
currentRev = doc._rev;
if (doc.inkomstenUitLoondienst?.[0]) {
const w = doc.inkomstenUitLoondienst[0];
set('wg0-naam', w.werkgever);
set('wg0-loon', w.loon);
set('wg0-loonheffing', w.loonheffing);
set('wg0-arbeidskorting', w.arbeidskorting);
set('wg0-andere', w.andereInkomsten || 0);
}
const p = doc.pensioenEnAndereUitkeringen || [];
if (p[0]) { set('p0-loon', p[0].loon); set('p0-heffing', p[0].loonheffing); }
if (p[1]) { set('p1-loon', p[1].loon); set('p1-heffing', p[1].loonheffing); }
if (p[2]) { set('p2-loon', p[2].loon); set('p2-heffing', p[2].loonheffing); }
if (p[3]) { set('p3-loon', p[3].loon); set('p3-heffing', p[3].loonheffing); }
if (doc.woningen?.[0]) set('woz-waarde', doc.woningen[0].woz_waarde);
setStatus('saved', 'Geladen uit CouchDB ✓');
}
} catch (e) {
console.warn('Geen bestaand document, nieuw document wordt aangemaakt bij opslaan.');
}
}
function val(id) { return document.getElementById(id)?.value || ''; }
function num(id) { return parseInt(document.getElementById(id)?.value) || 0; }
function set(id, v) { const el = document.getElementById(id); if (el) el.value = v; }
function setStatus(state, text) {
['statusDot','statusDot2'].forEach(id => {
const d = document.getElementById(id);
if (d) d.className = 'status-dot ' + state;
});
['statusText','statusText2'].forEach(id => {
const el = document.getElementById(id);
if (el) el.textContent = text;
});
}
function showToast(msg, type) {
const t = document.getElementById('toast');
t.textContent = msg;
t.className = 'toast ' + type + ' show';
setTimeout(() => t.classList.remove('show'), 3000);
}
function showSection(id) {
document.querySelectorAll('.section-tabs').forEach(s => s.classList.remove('active-section'));
const el = document.getElementById('section-' + id);
if (el) el.classList.add('active-section');
document.querySelectorAll('.nav-item').forEach(n => {
n.classList.toggle('active', !!(n.getAttribute('onclick') && n.getAttribute('onclick').includes("'" + id + "'")));
});
}
async function opslaanEnVolgende(next) {
await opslaan();
showSection(next);
}
function toggleNav(id) {
const el = document.getElementById(id);
const chev = document.getElementById('chev-' + id);
const hidden = el.style.display === 'none';
el.style.display = hidden ? 'block' : 'none';
if (chev) chev.textContent = hidden ? '▼' : '▲';
}
function toggleEmployer(idx) {
const body = document.getElementById('emp-body-' + idx);
const icon = document.getElementById('emp-icon-' + idx);
const hidden = body.style.display === 'none';
body.style.display = hidden ? 'block' : 'none';
icon.textContent = hidden ? '∧' : '';
}
function togglePension(header) {
const body = header.nextElementSibling;
const span = header.querySelector('span');
const hidden = body.style.display === 'none';
body.style.display = hidden ? 'block' : 'none';
span.textContent = span.textContent.replace(hidden ? '' : '∧', hidden ? '∧' : '');
}
laadDocument();
</script>
</body>
</html>
+33
View File
@@ -0,0 +1,33 @@
const express = require('express');
const { createProxyMiddleware } = require('http-proxy-middleware');
const path = require('path');
const app = express();
const PORT = 3000;
const COUCHDB_URL = process.env.COUCHDB_URL || 'http://admin:Couchdb01@couchdb.couchdb.svc.cluster.local:5984';
// Proxy /api/couchdb → CouchDB
app.use('/api/couchdb', createProxyMiddleware({
target: COUCHDB_URL,
changeOrigin: true,
pathRewrite: { '^/api/couchdb': '' },
on: {
error: (err, req, res) => {
console.error('CouchDB proxy error:', err.message);
res.status(502).json({ error: 'CouchDB unreachable', detail: err.message });
}
}
}));
// Serve static files
app.use(express.static(path.join(__dirname, 'public')));
app.get('*', (req, res) => {
res.sendFile(path.join(__dirname, 'public', 'index.html'));
});
app.listen(PORT, () => {
console.log(`Aangifte app running on port ${PORT}`);
console.log(`CouchDB proxy → ${COUCHDB_URL}`);
});
+1
View File
@@ -0,0 +1 @@
allard
+10
View File
@@ -0,0 +1,10 @@
FROM golang:1.22-alpine AS builder
WORKDIR /app
COPY main.go .
RUN go mod init aangifte && go build -o aangifte .
FROM alpine:3.19
WORKDIR /app
COPY --from=builder /app/aangifte .
EXPOSE 8080
CMD ["./aangifte"]
+2
View File
@@ -0,0 +1,2 @@
docker build -t allardkrings/aangifte:latest .
docker push allardkrings/aangifte:latest
+68
View File
@@ -0,0 +1,68 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: aangifte
namespace: couchdb
spec:
replicas: 1
selector:
matchLabels:
app: aangifte
template:
metadata:
labels:
app: aangifte
spec:
containers:
- name: aangifte
image: allardkrings/aangifte:latest
imagePullPolicy: Always
ports:
- containerPort: 8080
env:
- name: COUCHDB_URL
value: "http://couchdb-lb.couchdb.svc.cluster.local:5984"
- name: COUCHDB_USER
value: "admin"
- name: COUCHDB_PASS
value: "Couchdb01"
- name: COUCHDB_DB
value: "aangiftes"
- name: LISTEN_ADDR
value: ":8080"
resources:
requests:
memory: "64Mi"
cpu: "50m"
limits:
memory: "128Mi"
cpu: "200m"
---
apiVersion: v1
kind: Service
metadata:
name: aangifte
namespace: couchdb
spec:
selector:
app: aangifte
ports:
- port: 8080
targetPort: 8080
---
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: aangifte-ingress
namespace: couchdb
spec:
entryPoints:
- websecure
routes:
- match: Host(`aangifte-dev.allarddcs.nl`)
kind: Rule
services:
- name: aangifte
port: 8080
tls:
certResolver: letsencrypt
BIN
View File
Binary file not shown.
File diff suppressed because it is too large Load Diff
+415
View File
@@ -0,0 +1,415 @@
package main
import (
"bytes"
"encoding/json"
"fmt"
"html/template"
"io"
"log"
"net/http"
"os"
"strings"
"time"
)
// Config
var (
couchdbURL = getEnv("COUCHDB_URL", "http://10.152.183.59:5984")
couchdbUser = getEnv("COUCHDB_USER", "admin")
couchdbPass = getEnv("COUCHDB_PASS", "Couchdb01")
dbName = getEnv("COUCHDB_DB", "aangiftes")
listenAddr = getEnv("LISTEN_ADDR", ":8080")
)
func getEnv(key, def string) string {
if v := os.Getenv(key); v != "" {
return v
}
return def
}
// Data structures
type CouchDate struct {
time.Time
}
func (c *CouchDate) UnmarshalJSON(b []byte) error {
s := strings.Trim(string(b), `"`)
if s == "null" || s == "" {
return nil
}
t, err := time.Parse(time.RFC3339, s)
if err != nil {
return nil
}
c.Time = t
return nil
}
func (c CouchDate) MarshalJSON() ([]byte, error) {
if c.Time.IsZero() {
return []byte("null"), nil
}
return []byte(`"` + c.Time.Format(time.RFC3339) + `"`), nil
}
func (c CouchDate) String() string {
if c.Time.IsZero() {
return ""
}
return c.Time.Format("2006-01-02")
}
type Huisgenoot struct {
ID string `json:"_id,omitempty"`
Naam string `json:"naam"`
DatumVan CouchDate `json:"datumVan"`
DatumTotEnMet CouchDate `json:"datumTotEnMet"`
Samenlevingscontract bool `json:"samenlevingscontract"`
Pensioenregeling bool `json:"pensioenregeling"`
EigenaarWoning bool `json:"eigenaarwoning"`
MinderjarigKind bool `json:"minderjarigkind"`
FiscalePartner bool `json:"fiscalePartner"`
}
type Kind struct {
ID string `json:"_id,omitempty"`
Naam string `json:"naam"`
Geboortedatum CouchDate `json:"geboortedatum"`
Burgerservicenummer int64 `json:"burgerservicenummer"`
}
type InkomenLoondienst struct {
ID string `json:"_id,omitempty"`
Werkgever string `json:"werkgever"`
SoortInkomsten string `json:"soortInkomsten"`
Loon float64 `json:"loon"`
Loonheffing float64 `json:"loonheffing"`
Arbeidskorting float64 `json:"arbeidskorting"`
AndereInkomsten *float64 `json:"andereInkomsten"`
Reiskosten bool `json:"reiskosten"`
}
type PensioenUitkering struct {
ID string `json:"_id,omitempty"`
Land string `json:"land"`
Verzekeringsmaatschappij string `json:"verzekeringsmaatschappij"`
Loon float64 `json:"loon"`
Loonheffing float64 `json:"loonheffing"`
Kosten *float64 `json:"kosten"`
}
type Woning struct {
ID string `json:"_id,omitempty"`
Land string `json:"land"`
Postcode string `json:"postcode"`
Huisnummer int `json:"huisnummer"`
Woonplaats string `json:"woonplaats"`
Eigenaar bool `json:"eigenaar"`
AndereEigenaar bool `json:"andere_eigenaar"`
Eigendom bool `json:"eigendom"`
VeranderdEigendomsdeelLoopJaar bool `json:"veranderd_eigendomsdeel_loop_jaar"`
SituatieBeginJaar string `json:"situatie_begin_jaar"`
SituatieRestJaar bool `json:"situatie_rest_jaar"`
WozWaarde float64 `json:"woz_waarde"`
Verhuurd bool `json:"verhuurd"`
Erfpachtcanon bool `json:"erfpachtcanon"`
}
type Schuld struct {
ID string `json:"_id,omitempty"`
BankOfGeldverstrekker string `json:"bank_of_geldverstrekker"`
Nummer string `json:"nummer"`
SchuldVoorWoningJaNee string `json:"schuld_voor_woning_ja_nee"`
Jaar string `json:"jaar"`
Schuld0101 float64 `json:"schuld_0101"`
Schuld3112 float64 `json:"schuld_3112"`
Rente float64 `json:"rente"`
SchuldVanBeidenJaNee string `json:"schuld_van_beiden_ja_nee"`
LeningVoorWoningJaNee string `json:"lening_voor_woning_ja_nee"`
LeningGeheelVoorWoningJaNee string `json:"lening_geheel_voor_woning_ja_nee"`
}
type Aangifte struct {
ID string `json:"_id,omitempty"`
Rev string `json:"_rev,omitempty"`
LoginNaam string `json:"login_naam"`
Wachtwoord string `json:"wachtwoord"`
JaarAangifte int `json:"jaar_aangifte"`
JaarVoorJaarAangifte int `json:"jaar_voor_jaar_aangifte"`
Naam string `json:"naam"`
Geboortedatum CouchDate `json:"geboortedatum"`
Burgerservicenummer int64 `json:"burgerservicenummer"`
Telefoonnummer *string `json:"telefoonnummer"`
NummerBelastingconsulent *string `json:"nummer_belastingconsulent"`
Echtgenoot bool `json:"echtgenoot"`
Huisgenoot bool `json:"huisgenoot"`
SamenIngeschrevenJaNee string `json:"samen_ingeschreven_ja_nee"`
FiscalePartnerJaNee string `json:"fiscale_partner_ja_nee"`
AndereHuisgenootJaNee string `json:"andere_huisgenoot_ja_nee"`
NaamPartner string `json:"naam_partner"`
GeboortedatumPartner CouchDate `json:"geboortedatum_partner"`
BurgerservicenummerPartner int64 `json:"burgerservicenummer_partner"`
TelefoonnummerPartner *string `json:"telefoonnummer_partner"`
NummerBelastingconsulentPartner *string `json:"nummer_belastingconsulent_partner"`
Huisgenoten []Huisgenoot `json:"huisgenoten"`
Kinderen []Kind `json:"kinderen"`
InkomstenUitLoondienst []InkomenLoondienst `json:"inkomstenUitLoondienst"`
PensioenEnAndereUitkeringen []PensioenUitkering `json:"pensioenEnAndereUitkeringen"`
OndernemingJaNee string `json:"onderneming_ja_nee"`
OndernenerIBJaNee string `json:"ondernemer_ib_ja_nee"`
GeldverstrekkerJaNee string `json:"geldverstrekker_ja_nee"`
PartnerOndernenerIBJaNee string `json:"partner_ondernemer_ib_ja_nee"`
PartnerGeldverstrekkerJaNee string `json:"partner_geldverstrekker_ja_nee"`
Woningen []Woning `json:"woningen"`
SchuldenJaNee string `json:"schulden_ja_nee"`
Schulden []Schuld `json:"schulden"`
BankEnSpaarrekeningen bool `json:"bank_en_spaarrekeningen_ja_nee"`
GroeneSpaartegoeden bool `json:"groene_spaartegoeden_ja_nee"`
Beleggingen bool `json:"beleggingen_ja_nee"`
GroeneBeleggingen bool `json:"groene_beleggingen_ja_nee"`
AanmerkelijkBelang bool `json:"aanmerkelijk_belang_ja_nee"`
Kapitaalverzekeringen bool `json:"kapitaalverzekeringen_ja_nee"`
Bouwdepots bool `json:"bouwdepots_ja_nee"`
ContantGeld bool `json:"contant_geld_ja_nee"`
UitgeleendGeld bool `json:"uitgeleend_geld_ja_nee"`
RechtenOpPeriodiekUitkeringen bool `json:"rechten_op_periodieke_uitkeringen_ja_nee"`
OverigeBezittingen bool `json:"overige_bezittingen_ja_nee"`
WaardeHogerDanJaNee string `json:"waarde_meer_dan_ja_nee"`
LijfrenteJaNee bool `json:"lijfrente_ja_nee"`
InkomensvoorzieningenJaNee bool `json:"inkomensvoorzieningen_ja_nee"`
ZorgkostenJaNee bool `json:"zorgkosten_ja_nee"`
GiftenJaNee bool `json:"giften_ja_nee"`
StudiefinancieringJaNee bool `json:"studiefinanciering_ja_nee"`
AlimentatieJaNee bool `json:"alimentatie_ja_nee"`
GehandicapteJaNee bool `json:"gehandicapte_ja_nee"`
VoorJaarAangifteJaNee bool `json:"voor_jaar_aangifte_ja_nee"`
DatumIngediend *CouchDate `json:"datum_ingediend,omitempty"`
}
// mapCheckboxes returns the mapping of labels to struct field names
func mapCheckboxes(a *Aangifte) map[string]string {
return map[string]string{
"Bank- en spaarrekeningen": "bank_en_spaarrekeningen_ja_nee",
"Groene spaartegoeden": "groene_spaartegoeden_ja_nee",
"Beleggingen": "beleggingen_ja_nee",
"Groene beleggingen": "groene_beleggingen_ja_nee",
"Aanmerkelijk belang": "aanmerkelijk_belang_ja_nee",
"Kapitaalverzekeringen": "kapitaalverzekeringen_ja_nee",
"Bouwdepots": "bouwdepots_ja_nee",
"Contant geld": "contant_geld_ja_nee",
"Uitgeleend geld": "uitgeleend_geld_ja_nee",
"Rechten op periodieke uitkeringen": "rechten_op_periodieke_uitkeringen_ja_nee",
"Overige bezittingen": "overige_bezittingen_ja_nee",
"Lijfrente": "lijfrente_ja_nee",
"Inkomensvoorzieningen": "inkomensvoorzieningen_ja_nee",
"Zorgkosten": "zorgkosten_ja_nee",
"Giften": "giften_ja_nee",
"Studiefinanciering": "studiefinanciering_ja_nee",
"Alimentatie": "alimentatie_ja_nee",
"Gehandicapte": "gehandicapte_ja_nee",
"Voor jaar aangifte": "voor_jaar_aangifte_ja_nee",
}
}
// template setup with the fix
var tmpl = template.Must(template.New("").Funcs(template.FuncMap{
"add": func(a, b int) int { return a + b },
"deref": func(p *string) string {
if p == nil {
return ""
}
return *p
},
"mapCheckboxes": mapCheckboxes, // 👈 register the function
}).Parse(allTemplates))
// handler for login page
func handleLogin(w http.ResponseWriter, r *http.Request) {
if r.Method == "GET" {
tmpl.ExecuteTemplate(w, "login", map[string]interface{}{})
return
}
bsn := strToInt64(r.FormValue("bsn"))
wachtwoord := r.FormValue("wachtwoord")
aangifte, err := findByBSN(bsn)
if err != nil {
tmpl.ExecuteTemplate(w, "login", map[string]interface{}{
"Error": "Fout bij ophalen gegevens.",
})
return
}
if aangifte != nil && aangifte.Wachtwoord != wachtwoord {
tmpl.ExecuteTemplate(w, "login", map[string]interface{}{
"Error": "Onjuist BSN of wachtwoord.",
})
return
}
if aangifte == nil {
aangifte = &Aangifte{
Burgerservicenummer: bsn,
Wachtwoord: wachtwoord,
JaarAangifte: time.Now().Year() - 1,
JaarVoorJaarAangifte: time.Now().Year() - 2,
}
}
sid := fmt.Sprintf("%d-%d", bsn, time.Now().UnixNano())
sessions[sid] = &Session{
BSN: bsn,
Aangifte: aangifte,
}
http.SetCookie(w, &http.Cookie{
Name: "session",
Value: sid,
Path: "/",
})
http.Redirect(w, r, "/aangifte", http.StatusSeeOther)
}
// handleAangifte shows the main form page
func handleAangifte(w http.ResponseWriter, r *http.Request) {
sess, ok := getSession(r)
if !ok {
http.Redirect(w, r, "/login", http.StatusSeeOther)
return
}
tmpl.ExecuteTemplate(w, "aangifte", map[string]interface{}{
"Aangifte": sess.Aangifte,
})
}
// handleSave updates the Aangifte struct from POST form data
func handleSave(w http.ResponseWriter, r *http.Request) {
sess, ok := getSession(r)
if !ok {
http.Redirect(w, r, "/login", http.StatusSeeOther)
return
}
if err := r.ParseForm(); err != nil {
http.Error(w, "Kon formulier niet parsen", http.StatusBadRequest)
return
}
// update all fields
checks := mapCheckboxes(sess.Aangifte)
for label, field := range checks {
val := r.FormValue(label)
b := val == "on"
setBoolField(sess.Aangifte, field, b)
}
// optional: update jaar fields if present
if jaar := r.FormValue("jaar_aangifte"); jaar != "" {
if y, err := strconv.Atoi(jaar); err == nil {
sess.Aangifte.JaarAangifte = y
}
}
if jaarVoor := r.FormValue("jaar_voor_jaar_aangifte"); jaarVoor != "" {
if y, err := strconv.Atoi(jaarVoor); err == nil {
sess.Aangifte.JaarVoorJaarAangifte = y
}
}
// store/save to your DB if needed; here we just update session
http.Redirect(w, r, "/aangifte", http.StatusSeeOther)
}
// handleLogout deletes the session and redirects to login
func handleLogout(w http.ResponseWriter, r *http.Request) {
cookie, err := r.Cookie("session")
if err == nil {
delete(sessions, cookie.Value)
cookie.MaxAge = -1
http.SetCookie(w, cookie)
}
http.Redirect(w, r, "/login", http.StatusSeeOther)
}
// helper to set boolean field via reflection
func setBoolField(a *Aangifte, fieldName string, val bool) {
v := reflect.ValueOf(a).Elem().FieldByName(fieldName)
if v.IsValid() && v.Kind() == reflect.Bool {
v.SetBool(val)
}
}
// getSession retrieves session from cookie
func getSession(r *http.Request) (*Session, bool) {
cookie, err := r.Cookie("session")
if err != nil {
return nil, false
}
sess, ok := sessions[cookie.Value]
return sess, ok
}
// allTemplates holds embedded templates (login + aangifte)
var allTemplates = `
{{define "login"}}
<!DOCTYPE html>
<html>
<head><title>Login</title></head>
<body>
<h1>Login</h1>
<form method="POST" action="/login">
<label>Username: <input type="text" name="username"></label><br>
<label>Password: <input type="password" name="password"></label><br>
<button type="submit">Login</button>
</form>
</body>
</html>
{{end}}
{{define "aangifte"}}
<!DOCTYPE html>
<html>
<head><title>Aangifte</title></head>
<body>
<h1>Aangifte Form</h1>
<form method="POST" action="/save">
<label>Jaar Aangifte: <input type="text" name="jaar_aangifte" value="{{.Aangifte.JaarAangifte}}"></label><br>
<label>Jaar Voor Jaar Aangifte: <input type="text" name="jaar_voor_jaar_aangifte" value="{{.Aangifte.JaarVoorJaarAangifte}}"></label><br>
{{range $i, $h := .Aangifte.Huisgenoten}}
<h3>Huisgenoot {{$i}}</h3>
<label>Naam: <input type="text" name="huisgenoot_{{$i}}_naam" value="{{$h.Naam}}"></label><br>
<label>Partner: <input type="checkbox" name="huisgenoot_{{$i}}_partner" {{if $h.Partner}}checked{{end}}></label><br>
<label>Aangifte: <input type="checkbox" name="huisgenoot_{{$i}}_aangifte" {{if $h.Aangifte}}checked{{end}}></label><br>
{{end}}
<button type="submit">Opslaan</button>
</form>
<br>
<a href="/logout">Logout</a>
</body>
</html>
{{end}}
`
func main() {
// parse templates
tmpl = template.Must(template.New("all").Parse(allTemplates))
// initialize sessions map
sessions = make(map[string]*Session)
// routes
http.HandleFunc("/login", handleLogin)
http.HandleFunc("/aangifte", handleAangifte)
http.HandleFunc("/save", handleSave)
http.HandleFunc("/logout", handleLogout)
fmt.Println("Server gestart op :8080")
err := http.ListenAndServe(":8080", nil)
if err != nil {
log.Fatal(err)
}
}
+105
View File
@@ -0,0 +1,105 @@
apiVersion: v1
kind: Service
metadata:
name: couchdb
namespace: couchdb
labels:
app: couchdb
spec:
ports:
- port: 5984
name: http
clusterIP: None
selector:
app: couchdb
---
apiVersion: v1
kind: Service
metadata:
name: couchdb-lb
namespace: couchdb
labels:
app: couchdb
spec:
ports:
- port: 5984
name: http
selector:
app: couchdb
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: couchdb
namespace: couchdb
spec:
serviceName: couchdb
replicas: 3
selector:
matchLabels:
app: couchdb
template:
metadata:
labels:
app: couchdb
spec:
containers:
- name: couchdb
image: couchdb:latest
imagePullPolicy: Always
ports:
- containerPort: 5984
env:
- name: POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
- name: NODENAME
value: "$(POD_NAME).couchdb.couchdb.svc.cluster.local"
- name: COUCHDB_UUID
valueFrom:
fieldRef:
fieldPath: metadata.uid
- name: ERL_FLAGS
value: "-setcookie couchdb_cookie"
- name: COUCHDB_USER
value: "admin"
- name: COUCHDB_PASSWORD
value: "Couchdb01"
volumeMounts:
- name: couchdb
mountPath: /opt/couchdb/data
volumeClaimTemplates:
- metadata:
name: couchdb
spec:
accessModes: ["ReadWriteOnce"]
storageClassName: ""
resources:
requests:
storage: 2Gi
selector:
matchLabels:
pv: couchdb-data
---
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: couchdb-ingress
namespace: couchdb
spec:
entryPoints:
- websecure
routes:
- match: Host(`couchdb-prod.allarddcs.nl`)
kind: Rule
services:
- name: couchdb-lb
port: 5984
namespace: couchdb
sticky:
cookie:
name: couchdb_sticky
tls:
certResolver: letsencrypt
+89
View File
@@ -0,0 +1,89 @@
apiVersion: v1
kind: PersistentVolume
metadata:
name: couchdb-pv-0
spec:
accessModes:
- ReadWriteOnce
capacity:
storage: 2Gi
nfs:
path: /mnt/nfs_share/prod/couchdb/data/couchdb-0
server: 192.168.2.111
persistentVolumeReclaimPolicy: Retain
volumeMode: Filesystem
---
apiVersion: v1
kind: PersistentVolume
metadata:
name: couchdb-pv-1
spec:
accessModes:
- ReadWriteOnce
capacity:
storage: 2Gi
nfs:
path: /mnt/nfs_share/prod/couchdb/data/couchdb-1
server: 192.168.2.111
persistentVolumeReclaimPolicy: Retain
volumeMode: Filesystem
---
apiVersion: v1
kind: PersistentVolume
metadata:
name: couchdb-pv-2
spec:
accessModes:
- ReadWriteOnce
capacity:
storage: 2Gi
nfs:
path: /mnt/nfs_share/prod/couchdb/data/couchdb-2
server: 192.168.2.111
persistentVolumeReclaimPolicy: Retain
volumeMode: Filesystem
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: couchdb-couchdb-0
namespace: couchdb
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 2Gi
storageClassName: ""
volumeMode: Filesystem
volumeName: couchdb-pv-0
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: couchdb-couchdb-1
namespace: couchdb
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 2Gi
storageClassName: ""
volumeMode: Filesystem
volumeName: couchdb-pv-1
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: couchdb-couchdb-2
namespace: couchdb
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 2Gi
storageClassName: ""
volumeMode: Filesystem
volumeName: couchdb-pv-2
+100
View File
@@ -0,0 +1,100 @@
; CouchDB Configuration Settings
; Custom settings should be made in this file. They will override settings
; in default.ini, but unlike changes made to default.ini, this file won't be
; overwritten on server upgrade.
[couchdb]
;max_document_size = 4294967296 ; bytes
;os_process_timeout = 5000
[couch_peruser]
; If enabled, couch_peruser ensures that a private per-user database
; exists for each document in _users. These databases are writable only
; by the corresponding user. Databases are in the following form:
; userdb-{hex encoded username}
;enable = true
; If set to true and a user is deleted, the respective database gets
; deleted as well.
;delete_dbs = true
; Set a default q value for peruser-created databases that is different from
; cluster / q
;q = 1
[chttpd]
;port = 5984
;bind_address = 127.0.0.1
; Options for the MochiWeb HTTP server.
;server_options = [{backlog, 128}, {acceptor_pool_size, 16}]
; For more socket options, consult Erlang's module 'inet' man page.
;socket_options = [{sndbuf, 262144}, {nodelay, true}]
[httpd]
; NOTE that this only configures the "backend" node-local port, not the
; "frontend" clustered port. You probably don't want to change anything in
; this section.
; Uncomment next line to trigger basic-auth popup on unauthorized requests.
;WWW-Authenticate = Basic realm="administrator"
; Uncomment next line to set the configuration modification whitelist. Only
; whitelisted values may be changed via the /_config URLs. To allow the admin
; to change this value over HTTP, remember to include {httpd,config_whitelist}
; itself. Excluding it from the list would require editing this file to update
; the whitelist.
;config_whitelist = [{httpd,config_whitelist}, {log,level}, {etc,etc}]
[ssl]
;enable = true
;cert_file = /full/path/to/server_cert.pem
;key_file = /full/path/to/server_key.pem
;password = somepassword
; set to true to validate peer certificates
;verify_ssl_certificates = false
; Set to true to fail if the client does not send a certificate. Only used if verify_ssl_certificates is true.
;fail_if_no_peer_cert = false
; Path to file containing PEM encoded CA certificates (trusted
; certificates used for verifying a peer certificate). May be omitted if
; you do not want to verify the peer.
;cacert_file = /full/path/to/cacertf
; The verification fun (optional) if not specified, the default
; verification fun will be used.
;verify_fun = {Module, VerifyFun}
; maximum peer certificate depth
;ssl_certificate_max_depth = 1
; Reject renegotiations that do not live up to RFC 5746.
;secure_renegotiate = true
; The cipher suites that should be supported.
; Can be specified in erlang format "{ecdhe_ecdsa,aes_128_cbc,sha256}"
; or in OpenSSL format "ECDHE-ECDSA-AES128-SHA256".
;ciphers = ["ECDHE-ECDSA-AES128-SHA256", "ECDHE-ECDSA-AES128-SHA"]
; The SSL/TLS versions to support
;tls_versions = [tlsv1, 'tlsv1.1', 'tlsv1.2']
; To enable Virtual Hosts in CouchDB, add a vhost = path directive. All requests to
; the Virtual Host will be redirected to the path. In the example below all requests
; to http://example.com/ are redirected to /database.
; If you run CouchDB on a specific port, include the port number in the vhost:
; example.com:5984 = /database
[vhosts]
;example.com = /database/
; To create an admin account uncomment the '[admins]' section below and add a
; line in the format 'username = password'. When you next start CouchDB, it
; will change the password to a hash (so that your passwords don't linger
; around in plain-text files). You can add more admin accounts with more
; 'username = password' lines. Don't forget to restart CouchDB after
; changing this.
[admins]
;admin = mysecretpassword