function g20VerifyCode(email, code) {
try {
var ss = getActiveSpreadsheet_();
ensureSheetsExist_(ss);
var sheet = ss.getSheetByName('G20_Users');
var data = sheet.getDataRange().getValues();
email = email.trim().toLowerCase();
var cleanInputCode = String(code).trim();
for (var i = 1; i < data.length; i++) {
if (data[i][2] && data[i][2].toString().toLowerCase() === email) {
var storedCode = String(data[i][4]).trim();
if (storedCode === cleanInputCode) {
sheet.getRange(i + 1, 6).setValue('ACTIVE');
return { success: true, message: 'Account succesvol geverifieerd! Je kunt nu inloggen.' };
} else {
return { success: false, message: 'Onjuiste mailcode. Ingevuld: ' + cleanInputCode + ' vs Opgeslagen: ' + storedCode };
}
}
}
return { success: false, message: 'E-mailadres niet gevonden.' };
} catch(e) {
return { success: false, message: e.toString() };
}
}
Index
<!DOCTYPE html>
<html lang="nl">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>G20 — Dagelijks Geluksbesef | Coaching by Gabeler.eu</title>
<style>
:root {
--primary: #1e3a8a;
--primary-light: #3b82f6;
--accent: #10b981;
--bg: #f8fafc;
--card-bg: #ffffff;
--text: #1e293b;
--text-light: #64748b;
--border: #e2e8f0;
--radius: 12px;
}
* { box-sizing: border-box; margin: 0; padding: 0; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; }
body { background-color: var(--bg); color: var(--text); min-height: 100vh; display: flex; flex-direction: column; align-items: center; justify-content: flex-start; padding: 20px; }
.container { width: 100%; max-width: 800px; background: var(--card-bg); border-radius: var(--radius); box-shadow: 0 10px 25px -5px rgba(0,0,0,0.05), 0 8px 10px -6px rgba(0,0,0,0.05); padding: 30px; margin-top: 20px; margin-bottom: 40px; }
header { text-align: center; margin-bottom: 30px; border-bottom: 2px solid var(--border); padding-bottom: 20px; }
header h1 { color: var(--primary); font-size: 1.8rem; margin-bottom: 5px; }
header p { color: var(--text-light); font-size: 0.95rem; }
.screen { display: none; }
.screen.active { display: block; animation: fadeIn 0.3s ease-in-out; }
@keyframes fadeIn { from { opacity: 0; transform: translateY(5px); } to { opacity: 1; transform: translateY(0); } }
h2 { color: var(--primary); font-size: 1.4rem; margin-bottom: 15px; }
p.sub { color: var(--text-light); font-size: 0.9rem; margin-bottom: 20px; }
.form-group { margin-bottom: 15px; }
label { display: block; font-weight: 600; font-size: 0.85rem; margin-bottom: 5px; color: var(--text); }
input, select, textarea { width: 100%; padding: 12px; border: 1px solid var(--border); border-radius: 8px; font-size: 0.95rem; outline: none; transition: border-color 0.2s; }
input:focus, select:focus, textarea:focus { border-color: var(--primary-light); }
.btn { display: inline-block; width: 100%; background: var(--primary); color: white; border: none; padding: 12px 20px; font-size: 1rem; font-weight: 600; border-radius: 8px; cursor: pointer; text-align: center; transition: background 0.2s; margin-top: 10px; }
.btn:hover { background: #1d4ed8; }
.btn-secondary { background: #64748b; }
.btn-secondary:hover { background: #475569; }
.btn-accent { background: var(--accent); }
.btn-accent:hover { background: #059669; }
.link-text { text-align: center; margin-top: 15px; font-size: 0.9rem; color: var(--text-light); }
.link-text a { color: var(--primary-light); text-decoration: none; font-weight: 600; cursor: pointer; }
.link-text a:hover { text-decoration: underline; }
/* Dashboard & Nav */
.navbar { display: flex; justify-content: space-between; align-items: center; background: var(--primary); color: white; padding: 12px 20px; border-radius: 8px; margin-bottom: 25px; }
.navbar span { font-weight: 600; font-size: 0.9rem; }
.navbar button { background: rgba(255,255,255,0.2); border: none; color: white; padding: 6px 12px; border-radius: 6px; cursor: pointer; font-size: 0.85rem; }
.navbar button:hover { background: rgba(255,255,255,0.3); }
.menu-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 15px; margin-bottom: 25px; }
.menu-card { background: var(--bg); border: 1px solid var(--border); padding: 20px; border-radius: 8px; text-align: center; cursor: pointer; transition: all 0.2s; }
.menu-card:hover { border-color: var(--primary-light); transform: translateY(-2px); box-shadow: 0 4px 12px rgba(0,0,0,0.05); }
.menu-card h3 { color: var(--primary); font-size: 1.1rem; margin-bottom: 5px; }
.menu-card p { font-size: 0.85rem; color: var(--text-light); }
/* G20 Form Fields */
.g20-item { background: var(--bg); border: 1px solid var(--border); border-radius: 8px; padding: 15px; margin-bottom: 15px; }
.g20-item-header { display: flex; align-items: center; margin-bottom: 8px; }
.g20-emoji { font-size: 1.4rem; margin-right: 10px; }
.g20-title { font-weight: 600; font-size: 1rem; color: var(--primary); }
.g20-question { font-size: 0.9rem; color: var(--text); margin-bottom: 10px; font-style: italic; }
.spinner { display: none; text-align: center; margin: 15px 0; color: var(--primary-light); font-weight: 600; }
.alert { padding: 12px; border-radius: 8px; margin-bottom: 15px; font-size: 0.9rem; display: none; }
.alert-success { background: #d1fae5; color: #065f46; border: 1px solid #a7f3d0; }
.alert-error { background: #fee2e2; color: #991b1b; border: 1px solid #fecaca; }
footer { text-align: center; margin-top: auto; font-size: 0.8rem; color: var(--text-light); padding: 20px; }
</style>
</head>
<body>
<div class="container">
<header>
<h1>G20 — Dagelijks Geluksbesef</h1>
<p>Coaching by Gabeler.eu | Coach Stijn Gabeler</p>
</header>
<div id="alertBox" class="alert"></div>
<div id="spinner" class="spinner">Even geduld, bezig met verwerken...</div>
<!-- SCHERM 1: LOGIN -->
<div id="screen-login" class="screen active">
<h2>Inloggen</h2>
<p class="sub">Log in met je e-mailadres en gekozen wachtwoord.</p>
<div class="form-group">
<label>E-mailadres</label>
<input type="email" id="loginEmail" placeholder="jouw@email.nl">
</div>
<div class="form-group">
<label>Wachtwoord</label>
<input type="password" id="loginPassword" placeholder="••••••••">
</div>
<button class="btn" onclick="handleLogin()">Inloggen</button>
<div class="link-text">
Nog geen account? <a onclick="showScreen('screen-register-req')">Registreer hier (Gratis voorproefje)</a><br>
<a onclick="showScreen('screen-forgot')" style="margin-top: 8px; display: inline-block;">Wachtwoord vergeten?</a>
</div>
</div>
<!-- SCHERM 2A: REGISTRATIE AANVRAAG -->
<div id="screen-register-req" class="screen">
<h2>Account Registreren</h2>
<p class="sub">Vul je gegevens in om je gratis voorproefje te starten. Je ontvangt een verificatiecode per e-mail.</p>
<div class="form-group">
<label>Volledige Naam</label>
<input type="text" id="regName" placeholder="Stijn Gabeler">
</div>
<div class="form-group">
<label>E-mailadres</label>
<input type="email" id="regEmail" placeholder="jouw@email.nl">
</div>
<div class="form-group">
<label>Kies een Wachtwoord</label>
<input type="password" id="regPassword" placeholder="••••••••">
</div>
<button class="btn" onclick="handleRegisterRequest()">Verstuur Verificatiecode</button>
<div class="link-text">
Al een account? <a onclick="showScreen('screen-login')">Direct inloggen</a>
</div>
</div>
<!-- SCHERM 2B: REGISTRATIE VERIFICATIE -->
<div id="screen-register-verify" class="screen">
<h2>E-mail Verifiëren</h2>
<p class="sub">We hebben een 6-cijferige verificatiecode gestuurd naar je e-mailadres. Vul deze hieronder in.</p>
<div class="form-group">
<label>Verificatiecode (6 cijfers)</label>
<input type="text" id="verifyCodeInput" placeholder="123456" maxlength="6">
</div>
<button class="btn btn-accent" onclick="handleVerifyCode()">Account Activeren</button>
<div class="link-text">
<a onclick="showScreen('screen-login')">Terug naar inloggen</a>
</div>
</div>
<!-- SCHERM 3: WACHTWOORD HERSTELLEN -->
<div id="screen-forgot" class="screen">
<h2>Wachtwoord Herstellen</h2>
<p class="sub">Vul je e-mailadres in om een herstelcode te ontvangen.</p>
<div class="form-group">
<label>E-mailadres</label>
<input type="email" id="forgotEmail" placeholder="jouw@email.nl">
</div>
<button class="btn" onclick="handleRequestForgot()">Stuur Herstelcode</button>
<div id="resetSection" style="display:none; margin-top:20px; border-top: 1px solid var(--border); padding-top: 15px;">
<div class="form-group">
<label>Herstelcode</label>
<input type="text" id="resetCode" placeholder="123456">
</div>
<div class="form-group">
<label>Nieuw Wachtwoord</label>
<input type="password" id="newPassword" placeholder="••••••••">
</div>
<button class="btn btn-accent" onclick="handleExecuteReset()">Wachtwoord Opslaan</button>
</div>
<div class="link-text" style="margin-top: 15px;">
<a onclick="showScreen('screen-login')">Terug naar inloggen</a>
</div>
</div>
<!-- SCHERM 4: HOOFDDASHBOARD -->
<div id="screen-dashboard" class="screen">
<div class="navbar">
<span id="welcomeUser">Welkom</span>
<button onclick="handleLogout()">Uitloggen</button>
</div>
<div class="menu-grid">
<div class="menu-card" onclick="openG20Form()">
<h3>📅 Dagelijks G20</h3>
<p>Vul je dagboek en de 20 onderdelen in.</p>
</div>
<div class="menu-card" onclick="loadCards()">
<h3>🃏 Coachingkaarten</h3>
<p>Bekijk inzichten en mindfulness kaarten.</p>
</div>
<div class="menu-card" onclick="openProfile()">
<h3>👤 Mijn Profiel</h3>
<p>Beheer je quote, thema en instellingen.</p>
</div>
<div class="menu-card" onclick="openExport()">
<h3>📥 Data Exporteren</h3>
<p>Download je data als JSON, CSV of TXT.</p>
</div>
</div>
<div style="text-align: center; margin-top: 20px;">
<a href="https://wa.me/" target="_blank" style="color: var(--accent); font-weight: 600; text-decoration: none;">💬 Contact Coach Stijn via WhatsApp</a>
</div>
</div>
<!-- SCHERM 5: G20 DAGBOEK FORMULIER -->
<div id="screen-g20form" class="screen">
<div class="navbar">
<span>G20 Dagboek & Vragen</span>
<button onclick="showScreen('screen-dashboard')">Terug naar Menu</button>
</div>
<div class="form-group">
<label>Datum</label>
<input type="date" id="g20Date">
</div>
<div id="g20QuestionsContainer"></div>
<div class="form-group">
<label>Algemeen Inzicht van de dag</label>
<textarea id="g20Inzicht" rows="3" placeholder="Wat viel je vandaag op?"></textarea>
</div>
<div class="form-group">
<label>Het mooiste moment</label>
<input type="text" id="g20Moment" placeholder="Waar werd je blij van?">
</div>
<div class="form-group">
<label>Stap voor morgen</label>
<input type="text" id="g20Stap" placeholder="Wat neem je mee?">
</div>
<button class="btn btn-accent" onclick="saveG20Data()">Opslaan in Spreadsheet</button>
</div>
<!-- SCHERM 6: COACHINGKAARTEN -->
<div id="screen-cards" class="screen">
<div class="navbar">
<span>Coachingkaarten</span>
<button onclick="showScreen('screen-dashboard')">Terug naar Menu</button>
</div>
<div id="cardsContainer"></div>
</div>
<!-- SCHERM 7: PROFIEL -->
<div id="screen-profile" class="screen">
<div class="navbar">
<span>Mijn Profiel</span>
<button onclick="showScreen('screen-dashboard')">Terug naar Menu</button>
</div>
<div class="form-group">
<label>Naam</label>
<input type="text" id="profileName">
</div>
<div class="form-group">
<label>E-mailadres (niet wijzigbaar)</label>
<input type="email" id="profileEmail" disabled style="background:#f1f5f9;">
</div>
<div class="form-group">
<label>Persoonlijke Quote</label>
<input type="text" id="profileQuote" placeholder="Jij bepaalt je eigen koers.">
</div>
<div class="form-group">
<label>Favoriete Thema</label>
<input type="text" id="profileTheme" placeholder="Bijv. Verstilling of Geluksbesef">
</div>
<button class="btn" onclick="saveProfileData()">Profiel Opslaan</button>
</div>
<!-- SCHERM 8: EXPORT -->
<div id="screen-export" class="screen">
<div class="navbar">
<span>Data Exporteren</span>
<button onclick="showScreen('screen-dashboard')">Terug naar Menu</button>
</div>
<p class="sub">Kies je gewenste exportformaat om je dagboekgegevens direct op te slaan of te bekijken.</p>
<div style="display: flex; gap: 10px; margin-bottom: 20px;">
<button class="btn" onclick="executeExport('json')">Exporteer JSON</button>
<button class="btn btn-accent" onclick="executeExport('csv')">Exporteer CSV</button>
<button class="btn btn-secondary" onclick="executeExport('txt')">Exporteer TXT</button>
</div>
<div class="form-group">
<textarea id="exportOutput" rows="10" readonly placeholder="Export resultaat verschijnt hier..." style="font-family: monospace; font-size: 0.85rem;"></textarea>
</div>
</div>
</div>
<footer>
© 2026 Gabeler.eu Coaching — Alle rechten voorbehouden.
</footer>
<script>
let currentUser = null;
let cachedQuestions = [];
let pendingRegEmail = '';
function showScreen(screenId) {
document.querySelectorAll('.screen').forEach(s => s.classList.remove('active'));
document.getElementById(screenId).classList.add('active');
hideAlert();
}
function showAlert(msg, isError = false) {
const box = document.getElementById('alertBox');
box.innerText = msg;
box.className = 'alert ' + (isError ? 'alert-error' : 'alert-success');
box.style.display = 'block';
window.scrollTo({ top: 0, behavior: 'smooth' });
}
function hideAlert() {
document.getElementById('alertBox').style.display = 'none';
}
function setSpinner(show) {
document.getElementById('spinner').style.display = show ? 'block' : 'none';
}
// --- AUTH LOGICA ---
function handleLogin() {
const email = document.getElementById('loginEmail').value;
const pass = document.getElementById('loginPassword').value;
if (!email || !pass) {
showAlert('Vul zowel e-mailadres als wachtwoord in.', true);
return;
}
setSpinner(true);
google.script.run
.withSuccessHandler(res => {
setSpinner(false);
if (res.success) {
currentUser = res.user;
if (currentUser.status !== 'ACTIVE') {
showAlert('Je account is nog niet geverifieerd met de mailcode.', true);
pendingRegEmail = email;
showScreen('screen-register-verify');
return;
}
document.getElementById('welcomeUser').innerText = 'Welkom, ' + currentUser.name;
showScreen('screen-dashboard');
} else {
showAlert(res.message, true);
}
})
.withFailureHandler(err => {
setSpinner(false);
showAlert('Fout bij inloggen: ' + err.message, true);
})
.g20Login(email, pass);
}
function handleRegisterRequest() {
const name = document.getElementById('regName').value;
const email = document.getElementById('regEmail').value;
const pass = document.getElementById('regPassword').value;
if (!name || !email || !pass) {
showAlert('Vul alle velden in voor registratie.', true);
return;
}
pendingRegEmail = email;
setSpinner(true);
google.script.run
.withSuccessHandler(res => {
setSpinner(false);
if (res.success) {
showAlert(res.message);
showScreen('screen-register-verify');
} else {
showAlert(res.message, true);
}
})
.withFailureHandler(err => {
setSpinner(false);
showAlert('Fout: ' + err.message, true);
})
.g20RequestRegistrationCode(name, email, pass);
}
function handleVerifyCode() {
const code = document.getElementById('verifyCodeInput').value;
if (!code) {
showAlert('Vul de verificatiecode in.', true);
return;
}
setSpinner(true);
google.script.run
.withSuccessHandler(res => {
setSpinner(false);
if (res.success) {
showAlert(res.message);
showScreen('screen-login');
} else {
showAlert(res.message, true);
}
})
.withFailureHandler(err => {
setSpinner(false);
showAlert('Fout: ' + err.message, true);
})
.g20VerifyCode(pendingRegEmail, code);
}
function handleRequestForgot() {
const email = document.getElementById('forgotEmail').value;
if (!email) {
showAlert('Vul je e-mailadres in.', true);
return;
}
setSpinner(true);
google.script.run
.withSuccessHandler(res => {
setSpinner(false);
if (res.success) {
showAlert(res.message);
document.getElementById('resetSection').style.display = 'block';
} else {
showAlert(res.message, true);
}
})
.withFailureHandler(err => {
setSpinner(false);
showAlert('Fout: ' + err.message, true);
})
.g20RequestPasswordReset(email);
}
function handleExecuteReset() {
const email = document.getElementById('forgotEmail').value;
const code = document.getElementById('resetCode').value;
const newPass = document.getElementById('newPassword').value;
if (!code || !newPass) {
showAlert('Vul de herstelcode en het nieuwe wachtwoord in.', true);
return;
}
setSpinner(true);
google.script.run
.withSuccessHandler(res => {
setSpinner(false);
if (res.success) {
showAlert(res.message);
showScreen('screen-login');
} else {
showAlert(res.message, true);
}
})
.withFailureHandler(err => {
setSpinner(false);
showAlert('Fout: ' + err.message, true);
})
.g20ResetPassword(email, code, newPass);
}
function handleLogout() {
currentUser = null;
showScreen('screen-login');
}
// --- G20 DAGBOEK & VRAGEN ---
function openG20Form() {
document.getElementById('g20Date').valueAsDate = new Date();
setSpinner(true);
google.script.run
.withSuccessHandler(res => {
setSpinner(false);
if (res.success) {
cachedQuestions = res.questions;
renderG20Form(res.questions);
loadExistingDaily();
showScreen('screen-g20form');
} else {
showAlert('Kon vragen niet laden: ' + res.message, true);
}
})
.withFailureHandler(err => {
setSpinner(false);
showAlert('Fout: ' + err.message, true);
})
.g20GetQuestions();
}
function renderG20Form(questions) {
const container = document.getElementById('g20QuestionsContainer');
let html = '';
questions.forEach(q => {
html += `
<div class="g20-item">
<div class="g20-item-header">
<span class="g20-emoji">${q.emoji}</span>
<span class="g20-title">${q.number}. ${q.title}</span>
</div>
<div class="g20-question">${q.question}</div>
<input type="text" id="g_answer_${q.number}" placeholder="Jouw antwoord of ervaring...">
</div>
`;
});
container.innerHTML = html;
}
function loadExistingDaily() {
const datum = document.getElementById('g20Date').value;
google.script.run
.withSuccessHandler(res => {
if (res.success && res.data) {
document.getElementById('g20Inzicht').value = res.data.Inzicht || '';
document.getElementById('g20Moment').value = res.data.MooisteMoment || '';
document.getElementById('g20Stap').value = res.data.MorgenStap || '';
cachedQuestions.forEach(q => {
const field = document.getElementById('g_answer_' + q.number);
if (field) field.value = res.data['G' + q.number] || '';
});
}
})
.g20GetDaily(currentUser.userId, datum);
}
function saveG20Data() {
const payload = {
UserID: currentUser.userId,
Datum: document.getElementById('g20Date').value,
Inzicht: document.getElementById('g20Inzicht').value,
MooisteMoment: document.getElementById('g20Moment').value,
MorgenStap: document.getElementById('g20Stap').value
};
cachedQuestions.forEach(q => {
const field = document.getElementById('g_answer_' + q.number);
payload['G' + q.number] = field ? field.value : '';
});
setSpinner(true);
google.script.run
.withSuccessHandler(res => {
setSpinner(false);
if (res.success) {
showAlert(res.message);
} else {
showAlert(res.message, true);
}
})
.withFailureHandler(err => {
setSpinner(false);
showAlert('Fout bij opslaan: ' + err.message, true);
})
.g20SaveDaily(payload);
}
// --- KAARTEN ---
function loadCards() {
setSpinner(true);
google.script.run
.withSuccessHandler(res => {
setSpinner(false);
if (res.success) {
const container = document.getElementById('cardsContainer');
let html = '';
res.cards.forEach(c => {
html += `
<div class="g20-item">
<div class="g20-title">${c.title} <span style="font-size:0.75rem; color:var(--text-light);">(${c.category})</span></div>
<p style="margin-top:5px; font-size:0.9rem;">${c.description}</p>
</div>
`;
});
container.innerHTML = html;
showScreen('screen-cards');
} else {
showAlert(res.message, true);
}
})
.g20GetCards();
}
// --- PROFIEL ---
function openProfile() {
setSpinner(true);
google.script.run
.withSuccessHandler(res => {
setSpinner(false);
if (res.success) {
document.getElementById('profileName').value = res.profile.name;
document.getElementById('profileEmail').value = res.profile.email;
document.getElementById('profileQuote').value = res.profile.quote;
document.getElementById('profileTheme').value = res.profile.favoriteTheme;
showScreen('screen-profile');
} else {
showAlert(res.message, true);
}
})
.g20GetProfile(currentUser.userId);
}
function saveProfileData() {
const profileData = {
name: document.getElementById('profileName').value,
quote: document.getElementById('profileQuote').value,
favoriteTheme: document.getElementById('profileTheme').value
};
setSpinner(true);
google.script.run
.withSuccessHandler(res => {
setSpinner(false);
if (res.success) {
showAlert(res.message);
currentUser.name = profileData.name;
document.getElementById('welcomeUser').innerText = 'Welkom, ' + currentUser.name;
} else {
showAlert(res.message, true);
}
})
.g20SaveProfile(currentUser.userId, profileData);
}
// --- EXPORT ---
function openExport() {
document.getElementById('exportOutput').value = '';
showScreen('screen-export');
}
function executeExport(format) {
setSpinner(true);
google.script.run
.withSuccessHandler(res => {
setSpinner(false);
if (res.success) {
document.getElementById('exportOutput').value = res.data;
showAlert('Export succesvol gegenereerd!');
} else {
showAlert(res.message, true);
}
})
.g20ExportData(currentUser.userId, format);
}
</script>
</body>
</html>
Code
function doGet(e) {
return HtmlService.createHtmlOutputFromFile('Index')
.setTitle('G20 — Dagelijks Geluksbesef | Coaching by Gabeler.eu')
.addMetaTag('viewport', 'width=device-width, initial-scale=1.0');
}
function getActiveSpreadsheet_() {
try {
var ss = SpreadsheetApp.getActiveSpreadsheet();
if (ss) return ss;
} catch(e) {}
return SpreadsheetApp.openById('19C7zYtV7YZ1s-1ZJl_z3E2p_ZMnZTFwvJGNK7R39D60');
}
function setupG20() {
try {
var ss = getActiveSpreadsheet_();
ensureSheetsExist_(ss);
return { success: true, message: 'Alle G20 tabbladen en structuren zijn succesvol aangemaakt/gecontroleerd.' };
} catch(e) {
return { success: false, message: 'Fout bij setupG20: ' + e.toString() };
}
}
function testG20() {
return {
success: true,
app: 'G20 — Dagelijks Geluksbesef',
coach: 'Coach Stijn Gabeler',
message: 'G20 Code.gs werkt correct.'
};
}
function testG20Spreadsheet() {
try {
var ss = getActiveSpreadsheet_();
var name = ss.getName();
return { success: true, message: 'Spreadsheet bereikbaar: ' + name };
} catch(e) {
return { success: false, message: 'Spreadsheet niet bereikbaar: ' + e.toString() };
}
}
function testG20Questions() {
var res = g20GetQuestions();
return res;
}
function ensureSheetsExist_(ss) {
var required = {
'G20_Users': ['UserID', 'Name', 'Email', 'PasswordHash', 'MailCode', 'Status', 'CreatedAt', 'LastLogin', 'Quote', 'FavoriteTheme'],
'G20_Daily': ['UserID', 'Datum', 'Inzicht', 'MooisteMoment', 'MorgenStap'],
'G20_Questions': ['Number', 'Code', 'Title', 'Emoji', 'Question', 'Reflection'],
'G20_Cards': ['CardID', 'Title', 'Category', 'Description'],
'G20_Favorites': ['UserID', 'ItemType', 'ItemID', 'AddedAt']
};
for (var name in required) {
var sheet = ss.getSheetByName(name);
if (!sheet) {
sheet = ss.insertSheet(name);
var headers = required[name];
if (name === 'G20_Daily') {
for (var i = 1; i <= 20; i++) headers.push('G' + i);
}
sheet.appendRow(headers);
if (name === 'G20_Questions') {
seedQuestions_(sheet);
}
if (name === 'G20_Cards') {
seedCards_(sheet);
}
}
}
}
function seedQuestions_(sheet) {
var defaultQuestions = [
[1, "GELUKSBESEF", "Geluksbesef", "😊", "Waar werd je vandaag gelukkig, blij of dankbaar van?", "Geluk wordt zichtbaar wanneer je er bewust aandacht aan geeft."],
[2, "GEVOELENS", "Gevoelens", "❤️", "Welke gevoelens waren vandaag aanwezig?", "Gevoelens mogen eerst worden opgemerkt voordat je er iets mee hoeft te doen."],
[3, "GEDROOMD", "Gedroomd", "💭", "Waar heb je vandaag over gedroomd of gefantaseerd?", "Een droom kan iets vertellen over wat belangrijk voor je is."],
[4, "GEMIS", "Gemis", "🫶", "Wat of wie heb je vandaag gemist?", "Gemis kan zichtbaar maken wat waardevol voor je is."],
[5, "GEWENST", "Gewenst", "🌱", "Wat had je vandaag graag anders, beter of mooier gewild?", "Een wens kan een richting aanwijzen."],
[6, "GEHOOPT", "Gehoopt", "✨", "Waar hoopte je vandaag op?", "Hoop geeft ruimte aan mogelijkheden."],
[7, "GEDACHTEN", "Gedachten", "🧠", "Welke gedachte bleef vandaag bij je hangen?", "Aandacht voor gedachten geeft ruimte om bewust te kiezen."],
[8, "GEDRAG", "Gedrag", "🚶", "Hoe heb je vandaag gehandeld?", "Gedrag laat zien hoe gedachten en waarden in de praktijk komen."],
[9, "GEZEGD", "Gezegd", "💬", "Wat heb je vandaag gezegd dat belangrijk voor je was?", "Woorden kunnen verbinden, duidelijkheid geven en veranderen."],
[10, "GEDAAN", "Gedaan", "✅", "Wat heb je vandaag gedaan waar je bewust bij wilt stilstaan?", "Ook kleine acties kunnen betekenisvol zijn."],
[11, "GESCHREVEN", "Geschreven", "✍️", "Wat heb je vandaag geschreven?", "Schrijven helpt gedachten zichtbaar te maken."],
[12, "GELEZEN", "Gelezen", "📖", "Wat heb je vandaag gelezen dat je raakte of iets leerde?", "Wat je leest kan nieuwe perspectieven openen."],
[13, "GELEERD", "Geleerd", "🎓", "Wat heb je vandaag geleerd?", "Iedere dag bevat momenten waarop je iets nieuws kunt ontdekken."],
[14, "GEWETEN", "Geweten", "🧭", "Wat wist je vandaag diep vanbinnen al?", "Innerlijk weten kan een waardevol signaal zijn."],
[15, "GEHOORD", "Gehoord", "👂", "Wat heb je vandaag gehoord dat je bijblijft?", "Luisteren kan net zo waardevol zijn als spreken."],
[16, "GEZIEN", "Gezien", "👀", "Wat heb je vandaag bewust gezien?", "Aandacht verandert wat je opmerkt."],
[17, "GEPROEFD", "Geproefd", "😋", "Wat heb je vandaag geproefd en bewust beleefd?", "Zintuiglijke aandacht brengt je terug naar het moment."],
[18, "GEROKEN", "Geroken", "🌸", "Welke geur heb je vandaag bewust opgemerkt?", "Geur kan herinneringen en gevoelens oproepen."],
[19, "GEDACHT_OVER", "Gedacht over", "🔎", "Waar heb je vandaag langer of dieper over nagedacht?", "Reflectie ontstaat wanneer je even vertraagt."],
[20, "GELUKSGETAL", "Geluksgetal", "🔢", "Welk getal heeft vandaag betekenis voor jou?", "Een getal kan een persoonlijk anker voor je dag worden."]
];
sheet.getRange(2, 1, defaultQuestions.length, 6).setValues(defaultQuestions);
}
function seedCards_(sheet) {
var cards = [
["CARD-01", "Verstilling", "Mindfulness", "Neem een moment om stil te staan bij je ademhaling."],
["CARD-02", "Grenzen", "Balans", "Nee zeggen tegen een ander is ja zeggen tegen jezelf."],
["CARD-03", "Dankbaarheid", "Geluksbesef", "Tel je zegeningen en merk op wat er al is."]
];
sheet.getRange(2, 1, cards.length, 4).setValues(cards);
}
function hashPassword_(pass) {
var rawHash = Utilities.computeDigest(Utilities.DigestAlgorithm.SHA_256, pass, Utilities.Charset.UTF_8);
var txt = '';
for (var i = 0; i < rawHash.length; i++) {
var byte = rawHash[i];
if (byte < 0) byte += 256;
var bStr = byte.toString(16);
if (bStr.length == 1) bStr = '0' + bStr;
txt += bStr;
}
return txt;
}
function g20GetQuestions() {
try {
var ss = getActiveSpreadsheet_();
ensureSheetsExist_(ss);
var sheet = ss.getSheetByName('G20_Questions');
var data = sheet.getDataRange().getValues();
var questions = [];
for (var i = 1; i < data.length; i++) {
if (data[i][0]) {
questions.push({
number: data[i][0],
code: data[i][1],
title: data[i][2],
emoji: data[i][3],
question: data[i][4],
reflection: data[i][5]
});
}
}
return { success: true, questions: questions };
} catch(e) {
return { success: false, message: e.toString() };
}
}
function g20RequestRegistrationCode(name, email, password) {
try {
var ss = getActiveSpreadsheet_();
ensureSheetsExist_(ss);
var sheet = ss.getSheetByName('G20_Users');
var data = sheet.getDataRange().getValues();
email = email.trim().toLowerCase();
for (var i = 1; i < data.length; i++) {
if (data[i][2] && data[i][2].toString().toLowerCase() === email) {
if (data[i][5] === 'ACTIVE') {
return { success: false, message: 'Dit e-mailadres is al geregistreerd. Log direct in.' };
}
}
}
var userId = 'G20-' + Utilities.getUuid().substring(0, 8).toUpperCase();
var hash = hashPassword_(password);
var mailCode = Math.floor(100000 + Math.random() * 900000).toString();
MailApp.sendEmail(
email,
'Jouw G20 Verificatiecode — Coaching by Gabeler.eu',
'Hi ' + name + ',\n\nBedankt voor je registratie bij G20 — Dagelijks Geluksbesef.\nJouw verificatiecode is: ' + mailCode + '\n\nMet vriendelijke groet,\nCoach Stijn Gabeler'
);
sheet.appendRow([userId, name, email, hash, mailCode, 'PENDING_VERIFICATION', new Date(), new Date(), '', '']);
return { success: true, message: 'Registratie gestart! Er is een verificatiecode verzonden naar ' + email, userId: userId };
} catch(e) {
return { success: false, message: 'Fout bij registratie: ' + e.toString() };
}
}
function g20VerifyCode(email, code) {
try {
var ss = getActiveSpreadsheet_();
ensureSheetsExist_(ss);
var sheet = ss.getSheetByName('G20_Users');
var data = sheet.getDataRange().getValues();
email = email.trim().toLowerCase();
for (var i = 1; i < data.length; i++) {
if (data[i][2] && data[i][2].toString().toLowerCase() === email) {
if (String(data[i][4]) === String(code).trim()) {
sheet.getRange(i + 1, 6).setValue('ACTIVE');
return { success: true, message: 'Account succesvol geverifieerd! Je kunt nu inloggen.' };
} else {
return { success: false, message: 'Onjuiste mailcode.' };
}
}
}
return { success: false, message: 'E-mailadres niet gevonden.' };
} catch(e) {
return { success: false, message: e.toString() };
}
}
function g20Login(email, password) {
try {
var ss = getActiveSpreadsheet_();
ensureSheetsExist_(ss);
var sheet = ss.getSheetByName('G20_Users');
var data = sheet.getDataRange().getValues();
email = email.trim().toLowerCase();
var hash = hashPassword_(password);
for (var i = 1; i < data.length; i++) {
if (data[i][2] && data[i][2].toString().toLowerCase() === email) {
if (data[i][3] === hash) {
sheet.getRange(i + 1, 8).setValue(new Date());
return {
success: true,
user: { userId: data[i][0], name: data[i][1], email: data[i][2], status: data[i][5], quote: data[i][8] || '', favoriteTheme: data[i][9] || '' }
};
} else {
return { success: false, message: 'Onjuist wachtwoord.' };
}
}
}
return { success: false, message: 'Gebruiker niet gevonden.' };
} catch(e) {
return { success: false, message: e.toString() };
}
}
function g20RequestPasswordReset(email) {
try {
var ss = getActiveSpreadsheet_();
ensureSheetsExist_(ss);
var sheet = ss.getSheetByName('G20_Users');
var data = sheet.getDataRange().getValues();
email = email.trim().toLowerCase();
for (var i = 1; i < data.length; i++) {
if (data[i][2] && data[i][2].toString().toLowerCase() === email) {
var resetCode = Math.floor(100000 + Math.random() * 900000).toString();
sheet.getRange(i + 1, 5).setValue(resetCode);
MailApp.sendEmail(
email,
'Wachtwoord Herstelcode — G20 Coaching',
'Hi ' + data[i][1] + ',\n\nJe hebt een nieuw wachtwoord aangevraagd. Jouw herstelcode is: ' + resetCode + '\n\nCoach Stijn Gabeler'
);
return { success: true, message: 'Herstelcode verzonden naar je e-mailadres.' };
}
}
return { success: false, message: 'E-mailadres niet gevonden.' };
} catch(e) {
return { success: false, message: e.toString() };
}
}
function g20ResetPassword(email, code, newPassword) {
try {
var ss = getActiveSpreadsheet_();
ensureSheetsExist_(ss);
var sheet = ss.getSheetByName('G20_Users');
var data = sheet.getDataRange().getValues();
email = email.trim().toLowerCase();
for (var i = 1; i < data.length; i++) {
if (data[i][2] && data[i][2].toString().toLowerCase() === email) {
if (String(data[i][4]) === String(code).trim()) {
var newHash = hashPassword_(newPassword);
sheet.getRange(i + 1, 4).setValue(newHash);
sheet.getRange(i + 1, 5).setValue(''); // Clear code
return { success: true, message: 'Wachtwoord succesvol gewijzigd!' };
} else {
return { success: false, message: 'Onjuiste herstelcode.' };
}
}
}
return { success: false, message: 'E-mailadres niet gevonden.' };
} catch(e) {
return { success: false, message: e.toString() };
}
}
function g20SaveDaily(payload) {
try {
var ss = getActiveSpreadsheet_();
ensureSheetsExist_(ss);
var sheet = ss.getSheetByName('G20_Daily');
var data = sheet.getDataRange().getValues();
var rowIndex = -1;
for (var i = 1; i < data.length; i++) {
if (data[i][0] === payload.UserID && String(data[i][1]).substring(0, 10) === String(payload.Datum).substring(0, 10)) {
rowIndex = i + 1;
break;
}
}
var rowValues = [payload.UserID, payload.Datum, payload.Inzicht || '', payload.MooisteMoment || '', payload.MorgenStap || ''];
for (var j = 1; j <= 20; j++) {
rowValues.push(payload['G' + j] || '');
}
if (rowIndex > -1) {
sheet.getRange(rowIndex, 1, 1, rowValues.length).setValues([rowValues]);
} else {
sheet.appendRow(rowValues);
}
return { success: true, message: 'Opgeslagen in spreadsheet!' };
} catch(e) {
return { success: false, message: e.toString() };
}
}
function g20GetDaily(userId, datum) {
try {
var ss = getActiveSpreadsheet_();
ensureSheetsExist_(ss);
var sheet = ss.getSheetByName('G20_Daily');
var data = sheet.getDataRange().getValues();
if (data.length <= 1) return { success: true, data: null };
var headers = data[0];
for (var i = 1; i < data.length; i++) {
if (data[i][0] === userId && String(data[i][1]).substring(0, 10) === String(datum).substring(0, 10)) {
var record = { Inzicht: data[i][2], MooisteMoment: data[i][3], MorgenStap: data[i][4] };
for (var j = 5; j < headers.length; j++) {
record[headers[j]] = data[i][j];
}
return { success: true, data: record };
}
}
return { success: true, data: null };
} catch(e) {
return { success: false, message: e.toString() };
}
}
function g20GetCards() {
try {
var ss = getActiveSpreadsheet_();
ensureSheetsExist_(ss);
var sheet = ss.getSheetByName('G20_Cards');
var data = sheet.getDataRange().getValues();
var cards = [];
for (var i = 1; i < data.length; i++) {
if (data[i][0]) {
cards.push({ cardId: data[i][0], title: data[i][1], category: data[i][2], description: data[i][3] });
}
}
return { success: true, cards: cards };
} catch(e) {
return { success: false, message: e.toString() };
}
}
function g20GetProfile(userId) {
try {
var ss = getActiveSpreadsheet_();
ensureSheetsExist_(ss);
var sheet = ss.getSheetByName('G20_Users');
var data = sheet.getDataRange().getValues();
for (var i = 1; i < data.length; i++) {
if (data[i][0] === userId) {
return {
success: true,
profile: { userId: data[i][0], name: data[i][1], email: data[i][2], quote: data[i][8] || '', favoriteTheme: data[i][9] || '' }
};
}
}
return { success: false, message: 'Profiel niet gevonden.' };
} catch(e) {
return { success: false, message: e.toString() };
}
}
function g20SaveProfile(userId, profileData) {
try {
var ss = getActiveSpreadsheet_();
ensureSheetsExist_(ss);
var sheet = ss.getSheetByName('G20_Users');
var data = sheet.getDataRange().getValues();
for (var i = 1; i < data.length; i++) {
if (data[i][0] === userId) {
if (profileData.name) sheet.getRange(i + 1, 2).setValue(profileData.name);
if (profileData.quote !== undefined) sheet.getRange(i + 1, 9).setValue(profileData.quote);
if (profileData.favoriteTheme !== undefined) sheet.getRange(i + 1, 10).setValue(profileData.favoriteTheme);
return { success: true, message: 'Profiel bijgewerkt!' };
}
}
return { success: false, message: 'Gebruiker niet gevonden.' };
} catch(e) {
return { success: false, message: e.toString() };
}
}
function g20GetHistory(userId) {
try {
var ss = getActiveSpreadsheet_();
ensureSheetsExist_(ss);
var sheet = ss.getSheetByName('G20_Daily');
var data = sheet.getDataRange().getValues();
var history = [];
for (var i = 1; i < data.length; i++) {
if (data[i][0] === userId) {
history.push({ datum: String(data[i][1]).substring(0, 10), inzicht: data[i][2], moment: data[i][3] });
}
}
return { success: true, history: history };
} catch(e) {
return { success: false, message: e.toString() };
}
}
function g20ExportData(userId, format) {
try {
var ss = getActiveSpreadsheet_();
ensureSheetsExist_(ss);
var sheet = ss.getSheetByName('G20_Daily');
var data = sheet.getDataRange().getValues();
var headers = data[0];
var userRows = [];
for (var i = 1; i < data.length; i++) {
if (data[i][0] === userId) {
var obj = {};
for (var j = 0; j < headers.length; j++) {
obj[headers[j]] = data[i][j];
}
userRows.push(obj);
}
}
if (format === 'json') {
return { success: true, format: 'json', data: JSON.stringify(userRows, null, 2) };
} else if (format === 'csv') {
var csv = headers.join(',') + '\n';
userRows.forEach(function(row) {
var line = headers.map(function(h) { return '"' + (row[h] || '') + '"'; }).join(',');
csv += line + '\n';
});
return { success: true, format: 'csv', data: csv };
} else {
var txt = 'G20 EXPORT — COACHING BY GABELER.EU\n\n';
userRows.forEach(function(row) {
txt += 'Datum: ' + row.Datum + '\nInzicht: ' + row.Inzicht + '\nMooiste moment: ' + row.MooisteMoment + '\n---\n';
});
return { success: true, format: 'txt', data: txt };
}
} catch(e) {
return { success: false, message: e.toString() };
}
}