änderung der übergabe der vertragsnummer

This commit is contained in:
Cay 2026-02-25 19:06:26 +00:00
parent 3bee2c54df
commit 795b8a66ce
2 changed files with 427 additions and 285 deletions

View File

@ -1,62 +1,76 @@
const express = require('express'); const express = require("express");
const Database = require('better-sqlite3'); const Database = require("better-sqlite3");
const validateSepa = require('../utils/sepaValidator'); const validateSepa = require("../utils/sepaValidator");
const { encrypt } = require('../utils/crypto'); const { encrypt } = require("../utils/crypto");
const generateVertragsnummer = require('../utils/vertragsnummer'); const generateVertragsnummer = require("../utils/vertragsnummer");
const createContractPdf = require('../utils/contractPdf'); const createContractPdf = require("../utils/contractPdf");
const sendContractMail = require('../utils/sendContractMail'); const sendContractMail = require("../utils/sendContractMail");
const sendAdminMail = require('../utils/sendAdminMail'); const sendAdminMail = require("../utils/sendAdminMail");
const router = express.Router(); const router = express.Router();
const db = new Database('plusfit.db'); const db = new Database("plusfit.db");
/* ========================= /* =========================
Helper Helper
========================= */ ========================= */
function loadActiveContracts() { function loadActiveContracts() {
return db.prepare(` return db
.prepare(
`
SELECT * SELECT *
FROM vertragsarten FROM vertragsarten
WHERE aktiv = 1 WHERE aktiv = 1
ORDER BY betrag ASC ORDER BY betrag ASC
`).all(); `,
)
.all();
} }
/* ========================= /* =========================
GET /register GET /register
========================= */ ========================= */
router.get('/', (req, res) => { router.get("/", (req, res) => {
const vertragId = req.query.vertrag || null; const vertragId = req.query.vertrag || null;
return res.render('register', { const vertraege = loadActiveContracts();
vertragsarten: loadActiveContracts(),
selectedVertrag: vertragId, // Prüfen ob Vertrag existiert
formData: {} const selected = vertraege.find((v) => v.id == vertragId);
return res.render("register", {
vertragsarten: vertraege,
selectedVertrag: selected ? selected.id : null,
selectedVertragData: selected || null,
formData: {},
}); });
}); });
/* ========================= /* =========================
POST /register/create POST /register/create
========================= */ ========================= */
router.post('/create', async (req, res) => { router.post("/create", async (req, res) => {
const u = req.body; const u = req.body;
const vertragsarten = loadActiveContracts(); const vertragsarten = loadActiveContracts();
/* ========================= /* =========================
Vertragsdaten laden Vertragsdaten laden
========================= */ ========================= */
const contractData = db.prepare(` const contractData = db
.prepare(
`
SELECT name, laufzeit, betrag SELECT name, laufzeit, betrag
FROM vertragsarten FROM vertragsarten
WHERE id = ? AND aktiv = 1 WHERE id = ? AND aktiv = 1
`).get(u.vertragsvariante); `,
)
.get(u.vertragsvariante);
if (!contractData) { if (!contractData) {
return res.render('register', { return res.render("register", {
vertragsarten, vertragsarten,
selectedVertrag: u.vertragsvariante, selectedVertrag: u.vertragsvariante,
error: 'Vertragsdaten konnten nicht geladen werden.', error: "Vertragsdaten konnten nicht geladen werden.",
formData: u formData: u,
}); });
} }
@ -64,11 +78,11 @@ router.post('/create', async (req, res) => {
Pflicht-Zustimmungen Pflicht-Zustimmungen
========================= */ ========================= */
if (!u.agreeConsent || !u.agreeAgb || !u.agreeSepa) { if (!u.agreeConsent || !u.agreeAgb || !u.agreeSepa) {
return res.render('register', { return res.render("register", {
vertragsarten, vertragsarten,
selectedVertrag: u.vertragsvariante, selectedVertrag: u.vertragsvariante,
error: 'Bitte bestätige alle rechtlichen Hinweise, um fortzufahren.', error: "Bitte bestätige alle rechtlichen Hinweise, um fortzufahren.",
formData: u formData: u,
}); });
} }
@ -78,32 +92,36 @@ router.post('/create', async (req, res) => {
const sepaError = validateSepa({ const sepaError = validateSepa({
ibanValue: u.iban, ibanValue: u.iban,
bic: u.bic, bic: u.bic,
mandatsreferenz: u.mandatsreferenz mandatsreferenz: u.mandatsreferenz,
}); });
if (sepaError) { if (sepaError) {
return res.render('register', { return res.render("register", {
vertragsarten, vertragsarten,
selectedVertrag: u.vertragsvariante, selectedVertrag: u.vertragsvariante,
error: sepaError, error: sepaError,
formData: u formData: u,
}); });
} }
/* ========================= /* =========================
Logische Prüfungen Logische Prüfungen
========================= */ ========================= */
const mandatsExists = db.prepare(` const mandatsExists = db
.prepare(
`
SELECT id FROM users SELECT id FROM users
WHERE mandatsreferenz = ? WHERE mandatsreferenz = ?
`).get(u.mandatsreferenz); `,
)
.get(u.mandatsreferenz);
if (mandatsExists) { if (mandatsExists) {
return res.render('register', { return res.render("register", {
vertragsarten, vertragsarten,
selectedVertrag: u.vertragsvariante, selectedVertrag: u.vertragsvariante,
error: 'Diese Mandatsreferenz ist bereits vergeben.', error: "Diese Mandatsreferenz ist bereits vergeben.",
formData: u formData: u,
}); });
} }
@ -114,12 +132,14 @@ router.post('/create', async (req, res) => {
const zustimmungsDatum = new Date().toISOString(); const zustimmungsDatum = new Date().toISOString();
const zustimmungsIp = ( const zustimmungsIp = (
req.headers['x-forwarded-for'] || req.headers["x-forwarded-for"] ||
req.socket?.remoteAddress || req.socket?.remoteAddress ||
'' ""
).split(',')[0].trim(); )
.split(",")[0]
.trim();
const vertragsversion = 'v1.0'; const vertragsversion = "v1.0";
const widerrufBis = new Date(); const widerrufBis = new Date();
widerrufBis.setDate(widerrufBis.getDate() + 14); widerrufBis.setDate(widerrufBis.getDate() + 14);
@ -133,7 +153,8 @@ router.post('/create', async (req, res) => {
/* ========================= /* =========================
SPEICHERN SPEICHERN
========================= */ ========================= */
db.prepare(` db.prepare(
`
INSERT INTO users ( INSERT INTO users (
vertragsnummer, vertragsnummer,
vertragsvariante, vertragsvariante,
@ -165,13 +186,21 @@ router.post('/create', async (req, res) => {
?,?, ?,?,
0 0
) )
`).run( `,
).run(
vertragsnummer, vertragsnummer,
u.vertragsvariante, u.vertragsvariante,
u.vorname, u.nachname, u.vorname,
u.strasse, u.hausnummer, u.plz, u.ort, u.land, u.nachname,
u.mobil, u.telefon, u.email, u.strasse,
u.hausnummer,
u.plz,
u.ort,
u.land,
u.mobil,
u.telefon,
u.email,
u.kontoinhaber, u.kontoinhaber,
ibanEncrypted, ibanEncrypted,
@ -186,7 +215,7 @@ router.post('/create', async (req, res) => {
vertragsversion, vertragsversion,
widerrufBis.toISOString(), widerrufBis.toISOString(),
'aktiv' "aktiv",
); );
/* ========================= /* =========================
@ -200,7 +229,7 @@ router.post('/create', async (req, res) => {
laufzeit: contractData.laufzeit, laufzeit: contractData.laufzeit,
betrag: contractData.betrag, betrag: contractData.betrag,
datum: zustimmungsDatum, datum: zustimmungsDatum,
ip: zustimmungsIp ip: zustimmungsIp,
}); });
/* ========================= /* =========================
@ -213,7 +242,7 @@ router.post('/create', async (req, res) => {
vertragName: contractData.name, vertragName: contractData.name,
betrag: contractData.betrag, betrag: contractData.betrag,
datum: zustimmungsDatum, datum: zustimmungsDatum,
pdfPath pdfPath,
}); });
await sendAdminMail({ await sendAdminMail({
@ -224,13 +253,13 @@ await sendAdminMail({
vertragName: contractData.name, vertragName: contractData.name,
betrag: contractData.betrag, betrag: contractData.betrag,
datum: zustimmungsDatum, datum: zustimmungsDatum,
ip: zustimmungsIp ip: zustimmungsIp,
}); });
/* ========================= /* =========================
ERFOLG ERFOLG
========================= */ ========================= */
return res.render('registerSuccess', { vertragsnummer }); return res.render("registerSuccess", { vertragsnummer });
}); });
module.exports = router; module.exports = router;

View File

@ -1,40 +1,35 @@
<%- include('partials/header') %> <%- include('partials/header') %>
<style> <style>
.step { display: none; } .step{display:none}
.step.active { display: block; } .step.active{display:block}
.container-form { .container-form{max-width:700px;margin:auto}
max-width: 700px;
margin: auto;
}
.progress { .progress{height:10px;margin-bottom:30px}
height: 10px;
margin-bottom: 30px;
}
.step-nav{ .step-nav{
display:flex; display:flex;
justify-content:space-between; justify-content:space-between;
font-size:14px; font-size:14px;
margin-bottom: 20px; margin-bottom:20px
} }
.step-nav span.active{ .step-nav span.active{
font-weight:bold; font-weight:bold;
color: #3b3be3; color:#3b3be3
} }
@media(max-width:768px){ @media(max-width:768px){
h4{font-size:18px;} h4{font-size:18px}
button{width:100%;} button{width:100%}
} }
</style> </style>
<div class="container-form"> <div class="container-form">
<% if (typeof error !== 'undefined') { %> <% if(error){ %>
<div class="alert alert-danger"> <div class="alert alert-danger">
⚠️ <%= error %> ⚠️ <%= error %>
</div> </div>
@ -42,6 +37,8 @@
<h3 class="text-center mb-4">Mitglied werden</h3> <h3 class="text-center mb-4">Mitglied werden</h3>
<!-- Progress -->
<div class="progress"> <div class="progress">
<div id="progressBar" <div id="progressBar"
class="progress-bar bg-primary" class="progress-bar bg-primary"
@ -49,13 +46,19 @@
</div> </div>
</div> </div>
<!-- Nav -->
<div class="step-nav text-center mb-3"> <div class="step-nav text-center mb-3">
<span id="nav1">Daten</span> <span id="nav1">Daten</span>
<span id="nav2">Bank</span> <span id="nav2">Bank</span>
<span id="nav3">Prüfen</span> <span id="nav3">Prüfen</span>
</div> </div>
<form method="POST" action="/register/create" id="registerForm">
<form method="POST"
action="/register/create"
id="registerForm">
<!-- ================= STEP 1 ================= --> <!-- ================= STEP 1 ================= -->
<div class="step" id="step1"> <div class="step" id="step1">
@ -72,8 +75,10 @@
required> required>
<input name="mobil" class="form-control mb-2" placeholder="Mobilnummer" required> <input name="mobil" class="form-control mb-2" placeholder="Mobilnummer" required>
<input type="email" name="email" class="form-control mb-2" placeholder="E-Mail" required> <input type="email" name="email" class="form-control mb-2" placeholder="E-Mail" required>
<h5 class="mt-3">Adresse</h5> <h5 class="mt-3">Adresse</h5>
<input name="strasse" class="form-control mb-2" placeholder="Straße" required> <input name="strasse" class="form-control mb-2" placeholder="Straße" required>
@ -82,12 +87,17 @@
<input name="ort" class="form-control mb-2" placeholder="Ort" required> <input name="ort" class="form-control mb-2" placeholder="Ort" required>
<input name="land" class="form-control mb-2" value="Deutschland"> <input name="land" class="form-control mb-2" value="Deutschland">
<button type="button" class="btn btn-primary mt-3" onclick="nextStep(2)">
<button type="button"
class="btn btn-primary mt-3"
onclick="nextStep(2)">
Weiter → Weiter →
</button> </button>
</div> </div>
<!-- ================= STEP 2 ================= --> <!-- ================= STEP 2 ================= -->
<div class="step" id="step2"> <div class="step" id="step2">
@ -98,29 +108,40 @@ Weiter →
<input name="iban" class="form-control mb-2" placeholder="IBAN" required> <input name="iban" class="form-control mb-2" placeholder="IBAN" required>
<input name="bic" class="form-control mb-2" placeholder="BIC" required> <input name="bic" class="form-control mb-2" placeholder="BIC" required>
<div class="form-check mt-2"> <div class="form-check mt-2">
<input class="form-check-input" <input class="form-check-input"
type="checkbox" type="checkbox"
name="agreeSepa" name="agreeSepa"
value="on" value="on"
required> required>
<label class="form-check-label"> <label class="form-check-label">
SEPA-Mandat erteilen SEPA-Mandat erteilen
</label> </label>
</div> </div>
<div class="d-flex justify-content-between mt-4"> <div class="d-flex justify-content-between mt-4">
<button type="button" class="btn btn-secondary" onclick="nextStep(1)">
<button type="button"
class="btn btn-secondary"
onclick="nextStep(1)">
← Zurück ← Zurück
</button> </button>
<button type="button" class="btn btn-primary" onclick="nextStep(3)"> <button type="button"
class="btn btn-primary"
onclick="nextStep(3)">
Weiter → Weiter →
</button> </button>
</div> </div>
</div> </div>
<!-- ================= STEP 3 ================= --> <!-- ================= STEP 3 ================= -->
<div class="step" id="step3"> <div class="step" id="step3">
@ -130,65 +151,103 @@ Weiter →
<div id="summaryBox"></div> <div id="summaryBox"></div>
</div> </div>
<!-- Vertrag hidden -->
<input type="hidden"
name="vertragsvariante"
id="vertragHidden"
value="<%= selectedVertrag || '' %>">
<h5>Vertrag</h5> <h5>Vertrag</h5>
<select name="vertragsvariante" class="form-select mb-3" required> <div class="alert alert-info">
<option value="">Bitte wählen</option>
<% vertragsarten.forEach(v => { %> <b>
<option value="<%= v.id %>"> <% if(selectedVertragData){ %>
<%= v.name %> <%= v.betrag.toFixed(2) %> € <%= selectedVertragData.name %>
</option> <%= selectedVertragData.betrag.toFixed(2) %> € / Monat
<% }) %> <% }else{ %>
</select> Kein Vertrag gewählt
<% } %>
</b>
</div>
<h5>Rechtliches</h5> <h5>Rechtliches</h5>
<!-- Eltern -->
<div class="form-check mb-2 d-none" id="consentBox"> <div class="form-check mb-2 d-none" id="consentBox">
<input class="form-check-input" <input class="form-check-input"
type="checkbox" type="checkbox"
name="agreeConsent" name="agreeConsent"
id="agreeConsent" id="agreeConsent"
value="on"> value="on">
<label class="form-check-label"> <label class="form-check-label">
Einverständnis der Erziehungsberechtigten Einverständnis der Erziehungsberechtigten
</label> </label>
</div> </div>
<!-- AGB -->
<div class="form-check mb-3"> <div class="form-check mb-3">
<input class="form-check-input" <input class="form-check-input"
type="checkbox" type="checkbox"
name="agreeAgb" name="agreeAgb"
value="on" value="on"
required> required>
<label class="form-check-label"> <label class="form-check-label">
<a href="https://plusfit24.de/wp-content/uploads/2022/11/AG_PlusFit24.pdf" target="_blank" rel="noopener noreferrer"> <a href="https://plusfit24.de/wp-content/uploads/2022/11/AG_PlusFit24.pdf"
target="_blank">
AGB gelesen AGB gelesen
</a> </a>
</label> </label>
</div> </div>
<div class="d-flex justify-content-between mt-4"> <div class="d-flex justify-content-between mt-4">
<button type="button" class="btn btn-secondary" onclick="nextStep(2)">
<button type="button"
class="btn btn-secondary"
onclick="nextStep(2)">
← Zurück ← Zurück
</button> </button>
<button type="submit" class="btn btn-success btn-lg"> <button type="submit"
class="btn btn-success btn-lg">
Abschließen Abschließen
</button> </button>
</div> </div>
</div> </div>
</form> </form>
</div> </div>
<script> <script>
window.fromServerError = <%= typeof errorStep !== 'undefined' ? 'true' : 'false' %>; window.fromServerError =
let currentStep = <%= typeof errorStep !== 'undefined' ? errorStep : 1 %>; <%= typeof errorStep !== 'undefined' ? 'true' : 'false' %>;
let currentStep =
<%= typeof errorStep !== 'undefined' ? errorStep : 1 %>;
const form = document.getElementById('registerForm'); const form = document.getElementById('registerForm');
function nextStep(step){ function nextStep(step){
if(step > currentStep && !window.fromServerError){ if(step > currentStep && !window.fromServerError){
@ -214,25 +273,35 @@ function nextStep(step){
} }
} }
function validateStep(step){ function validateStep(step){
const container = document.getElementById('step'+step);
const fields = container.querySelectorAll('input,select'); const container =
document.getElementById('step'+step);
const fields =
container.querySelectorAll('input,select');
for(let field of fields){ for(let field of fields){
if(!field.checkValidity()){ if(!field.checkValidity()){
field.reportValidity(); field.reportValidity();
return false; return false;
} }
} }
return true; return true;
} }
function updateProgress(){ function updateProgress(){
document.getElementById('progressBar') document.getElementById('progressBar')
.style.width = (currentStep*33)+'%'; .style.width = (currentStep*33)+'%';
} }
function updateNav(){ function updateNav(){
document.querySelectorAll('.step-nav span') document.querySelectorAll('.step-nav span')
.forEach(n=>n.classList.remove('active')); .forEach(n=>n.classList.remove('active'));
@ -240,56 +309,85 @@ function updateNav(){
.classList.add('active'); .classList.add('active');
} }
function isUnder18(){ function isUnder18(){
const bday = document.getElementById('geburtsdatum').value;
const bday =
document.getElementById('geburtsdatum').value;
if(!bday) return false; if(!bday) return false;
const birth = new Date(bday); const birth = new Date(bday);
const today = new Date(); const today = new Date();
let age = today.getFullYear()-birth.getFullYear(); let age = today.getFullYear()-birth.getFullYear();
if(today.getMonth()<birth.getMonth() || if(today.getMonth()<birth.getMonth() ||
(today.getMonth()===birth.getMonth() && (today.getMonth()===birth.getMonth() &&
today.getDate()<birth.getDate())){ today.getDate()<birth.getDate())){
age--; age--;
} }
return age<18; return age<18;
} }
function checkAgeAndConsent(){ function checkAgeAndConsent(){
const box = document.getElementById('consentBox');
const checkbox = document.getElementById('agreeConsent'); const box =
document.getElementById('consentBox');
const checkbox =
document.getElementById('agreeConsent');
if(isUnder18()){ if(isUnder18()){
box.classList.remove('d-none'); box.classList.remove('d-none');
checkbox.required=true; checkbox.required=true;
}else{ }else{
box.classList.add('d-none'); box.classList.add('d-none');
checkbox.required=false; checkbox.required=false;
checkbox.checked=true; checkbox.checked=true;
} }
} }
form.addEventListener('input',saveForm); form.addEventListener('input',saveForm);
function saveForm(){ function saveForm(){
const data={}; const data={};
Array.from(form.elements).forEach(el=>{ Array.from(form.elements).forEach(el=>{
if(!el.name) return; if(!el.name) return;
if(el.type==='checkbox'){ if(el.type==='checkbox'){
data[el.name]=el.checked?'on':''; data[el.name]=el.checked?'on':'';
}else{ }else{
data[el.name]=el.value; data[el.name]=el.value;
} }
}); });
localStorage.setItem('registerData', JSON.stringify(data));
localStorage.setItem(
'registerData',
JSON.stringify(data)
);
} }
function loadForm(){ function loadForm(){
const data = JSON.parse(localStorage.getItem('registerData'));
const data =
JSON.parse(localStorage.getItem('registerData'));
if(!data) return; if(!data) return;
Object.keys(data).forEach(key=>{ Object.keys(data).forEach(key=>{
const field=form.elements[key]; const field=form.elements[key];
if(!field) return; if(!field) return;
@ -301,27 +399,42 @@ function loadForm(){
}); });
} }
function buildSummary(){ function buildSummary(){
const data = JSON.parse(localStorage.getItem('registerData'));
const data =
JSON.parse(localStorage.getItem('registerData'));
if(!data) return; if(!data) return;
document.getElementById('summaryBox').innerHTML = ` document.getElementById('summaryBox')
.innerHTML=`
<b>Name:</b> ${data.vorname} ${data.nachname}<br> <b>Name:</b> ${data.vorname} ${data.nachname}<br>
<b>Geburtsdatum:</b> ${data.geburtsdatum}<br> <b>Geburtsdatum:</b> ${data.geburtsdatum}<br>
<b>Email:</b> ${data.email}<br> <b>Email:</b> ${data.email}<br>
<b>Mobil:</b> ${data.mobil}<br> <b>Mobil:</b> ${data.mobil}<br>
<b>Adresse:</b> ${data.strasse} ${data.hausnummer}, ${data.plz} ${data.ort}<br> <b>Adresse:</b>
${data.strasse} ${data.hausnummer},
${data.plz} ${data.ort}<br>
<b>IBAN:</b> ${data.iban} <b>IBAN:</b> ${data.iban}
`; `;
} }
form.addEventListener('submit',()=>{ form.addEventListener('submit',()=>{
localStorage.removeItem('registerData'); localStorage.removeItem('registerData');
}); });
loadForm(); loadForm();
setTimeout(()=>{ nextStep(currentStep); },100);
setTimeout(()=>{
nextStep(currentStep);
},100);
</script> </script>
<%- include('partials/footer') %> <%- include('partials/footer') %>