-
Optional Add-ons
-
These can be arranged and paid separately after acceptance.
+
Accommodation
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Food
+
-
Registration fee: Select at least one week
-
Accommodation and food costs will be invoiced separately after acceptance.
+
Select at least one week
@@ -713,6 +777,28 @@
let currentStep = 1;
const totalSteps = 12;
const PRICE_PER_WEEK = 300;
+ const PROCESSING_FEE_PERCENT = 0.02;
+
+ // Accommodation prices per week (must match api/mollie.js)
+ const ACCOMMODATION_PRICES = {
+ 'ch-multi': 279.30,
+ 'ch-double': 356.30,
+ 'hh-single': 665.00,
+ 'hh-double-separate': 420.00,
+ 'hh-double-shared': 350.00,
+ 'hh-triple': 350.00,
+ 'hh-daybed': 280.00,
+ };
+
+ const ACCOMMODATION_LABELS = {
+ 'ch-multi': 'Commons Hub — Shared Room',
+ 'ch-double': 'Commons Hub — Double Room',
+ 'hh-single': 'Herrnhof Villa — Single Room',
+ 'hh-double-separate': 'Herrnhof Villa — Double (separate beds)',
+ 'hh-double-shared': 'Herrnhof Villa — Double (shared bed)',
+ 'hh-triple': 'Herrnhof Villa — Triple Room',
+ 'hh-daybed': 'Herrnhof Villa — Daybed / Extra Bed',
+ };
function updateProgress() {
const percent = Math.round(((currentStep - 1) / totalSteps) * 100);
@@ -815,19 +901,84 @@
card.classList.toggle('selected', cb.checked);
if (type === 'accommodation') {
- document.getElementById('accommodation-preference').style.display = cb.checked ? 'block' : 'none';
+ const opts = document.getElementById('accommodation-options');
+ opts.style.display = cb.checked ? 'block' : 'none';
+ if (!cb.checked) {
+ // Clear accommodation selection
+ document.getElementById('accommodation_type').value = '';
+ document.querySelectorAll('input[name="venue"]').forEach(r => { r.checked = false; r.closest('.week-card').classList.remove('selected'); });
+ document.querySelectorAll('input[name="room_type"]').forEach(r => { r.checked = false; r.closest('.week-card').classList.remove('selected'); });
+ document.getElementById('ch-rooms').style.display = 'none';
+ document.getElementById('hh-rooms').style.display = 'none';
+ }
}
+
+ // Always refresh summary (food toggle also affects it)
+ updatePriceSummary();
+ }
+
+ function selectVenue(venue) {
+ // Update radio + card styling
+ document.querySelectorAll('input[name="venue"]').forEach(r => {
+ r.checked = (r.value === venue);
+ r.closest('.week-card').classList.toggle('selected', r.checked);
+ });
+
+ // Show/hide room type sections
+ document.getElementById('ch-rooms').style.display = venue === 'ch' ? 'block' : 'none';
+ document.getElementById('hh-rooms').style.display = venue === 'hh' ? 'block' : 'none';
+
+ // Clear previous room selection
+ document.querySelectorAll('input[name="room_type"]').forEach(r => {
+ r.checked = false;
+ r.closest('.week-card').classList.remove('selected');
+ });
+ document.getElementById('accommodation_type').value = '';
+ updatePriceSummary();
+ }
+
+ function selectRoom(roomType) {
+ document.querySelectorAll('input[name="room_type"]').forEach(r => {
+ r.checked = (r.value === roomType);
+ r.closest('.week-card').classList.toggle('selected', r.checked);
+ });
+ document.getElementById('accommodation_type').value = roomType;
+ updatePriceSummary();
}
function updatePriceSummary() {
const weeksCount = document.querySelectorAll('input[name="weeks"]:checked').length;
const el = document.getElementById('price-calc');
+
if (weeksCount === 0) {
el.textContent = 'Select at least one week';
- } else {
- const total = weeksCount * PRICE_PER_WEEK;
- el.innerHTML = `€${PRICE_PER_WEEK} × ${weeksCount} week${weeksCount > 1 ? 's' : ''} = €${total.toLocaleString()}`;
+ return;
}
+
+ const registration = PRICE_PER_WEEK * weeksCount;
+ const accomType = document.getElementById('accommodation_type').value;
+ const accomPerWeek = accomType ? (ACCOMMODATION_PRICES[accomType] || 0) : 0;
+ const accommodation = accomPerWeek * weeksCount;
+ const subtotal = registration + accommodation;
+ const fee = subtotal * PROCESSING_FEE_PERCENT;
+ const total = subtotal + fee;
+
+ let html = `Registration: €${PRICE_PER_WEEK} × ${weeksCount} wk = €${registration.toFixed(2)}`;
+
+ if (accommodation > 0) {
+ const label = ACCOMMODATION_LABELS[accomType] || accomType;
+ html += `
Accommodation: ${label}
€${accomPerWeek.toFixed(2)} × ${weeksCount} wk = €${accommodation.toFixed(2)}`;
+ }
+
+ html += `
Processing fee (2%): €${fee.toFixed(2)}`;
+ html += `
Total: €${total.toFixed(2)}`;
+
+ const wantFood = document.getElementById('want_food').checked;
+ if (wantFood) {
+ html += `
Food: interest registered — details & costs coming soon.`;
+ }
+
+ el.innerHTML = html;
}
// Theme ranking drag & drop
@@ -902,7 +1053,8 @@
weeks: weeks,
attendance_type: weeks.length === 4 ? 'full' : 'partial',
need_accommodation: document.getElementById('need_accommodation').checked,
- accommodation_preference: document.getElementById('accom_type').value || null,
+ accommodation_type: document.getElementById('accommodation_type').value || null,
+ accommodation_preference: document.getElementById('accommodation_type').value || null,
want_food: document.getElementById('want_food').checked,
anything_else: form.anything_else.value,
privacy_policy_accepted: form.privacy_accepted.checked,
diff --git a/db/migration-003-accommodation.sql b/db/migration-003-accommodation.sql
new file mode 100644
index 0000000..390b20e
--- /dev/null
+++ b/db/migration-003-accommodation.sql
@@ -0,0 +1,5 @@
+-- Migration 003: Add accommodation_type column to applications table
+-- Stores the CCG-style accommodation selection (e.g. ch-multi, hh-single)
+-- Keeps existing accommodation_preference column for backward compatibility
+
+ALTER TABLE applications ADD COLUMN IF NOT EXISTS accommodation_type VARCHAR(50);
diff --git a/db/schema.sql b/db/schema.sql
index 1d4b122..9b00478 100644
--- a/db/schema.sql
+++ b/db/schema.sql
@@ -85,9 +85,10 @@ CREATE TABLE IF NOT EXISTS applications (
scholarship_reason TEXT,
contribution_amount VARCHAR(50), -- 'registration' (base fee) or legacy ticket type
- -- Add-ons (invoiced separately after acceptance)
+ -- Add-ons
need_accommodation BOOLEAN DEFAULT FALSE,
want_food BOOLEAN DEFAULT FALSE,
+ accommodation_type VARCHAR(50), -- CCG-style: ch-multi, ch-double, hh-single, etc.
-- Payment (Mollie)
mollie_payment_id VARCHAR(255),
diff --git a/server.js b/server.js
index ce49a0d..573ac6d 100644
--- a/server.js
+++ b/server.js
@@ -82,7 +82,8 @@ async function runMigrations() {
await pool.query(`
ALTER TABLE applications
ADD COLUMN IF NOT EXISTS need_accommodation BOOLEAN DEFAULT FALSE,
- ADD COLUMN IF NOT EXISTS want_food BOOLEAN DEFAULT FALSE
+ ADD COLUMN IF NOT EXISTS want_food BOOLEAN DEFAULT FALSE,
+ ADD COLUMN IF NOT EXISTS accommodation_type VARCHAR(50)
`);
// Rename resend_id → message_id in email_log (legacy column name)