Code
/************************************************************
* G20 — DAGELIJKS GELUKSBESEF
* NextLevel Coaching by Gabeler.eu
*
* VOLLEDIGE CODE.GS
*
* Deze versie is afgestemd op de huidige Index.html:
*
* g20SaveDaily(payload)
*
* Payload:
* UserID
* Datum
* G1_Geluksbesef
* G2_Gevoelens
* ...
* G20_Geluksgetal
* Inzicht
* MooisteMoment
* MorgenStap
*
* Spreadsheet:
* 1cl2uFtN05D6Yw78R9QtpiVMBjvXGINmMdI01A3Fhons
************************************************************/
/************************************************************
* CONFIGURATIE
************************************************************/
const SS_ID = '1cl2uFtN05D6Yw78R9QtpiVMBjvXGINmMdI01A3Fhons';
const DAILY_SHEET_NAME = 'G20_Daily';
const USERS_SHEET_NAME = 'G20_Users';
const QUESTIONS_SHEET_NAME = 'G20_Questions';
const CARDS_SHEET_NAME = 'G20_Cards';
const CODES_SHEET_NAME = 'G20_Codes';
const FAVORITES_SHEET_NAME = 'G20_Favorites';
/************************************************************
* DE 20 G-VELDEN
*
* Dit zijn EXACT de namen die de huidige Index gebruikt.
************************************************************/
const G20_KEYS_ = [
'G1_Geluksbesef',
'G2_Gevoelens',
'G3_Gedroomd',
'G4_Gemis',
'G5_Gewenst',
'G6_Gehoopt',
'G7_Gedachten',
'G8_Gedrag',
'G9_Gezegd',
'G10_Gedaan',
'G11_Geschreven',
'G12_Gelezen',
'G13_Geleerd',
'G14_Geweten',
'G15_Gehoord',
'G16_Gezien',
'G17_Geproefd',
'G18_Geroken',
'G19_GedachtOver',
'G20_Geluksgetal'
];
/************************************************************
* ALLE KOLOMMEN VAN G20_Daily
************************************************************/
function getDailyHeaders_() {
return [
'RecordID',
'UserID',
'Datum'
].concat(G20_KEYS_).concat([
'Inzicht',
'MooisteMoment',
'MorgenStap',
'CreatedAt',
'UpdatedAt'
]);
}
/************************************************************
* WEBAPP
************************************************************/
function doGet(e) {
return HtmlService
.createHtmlOutputFromFile('Index')
.setTitle(
'G20 — Dagelijks Geluksbesef | NextLevel Coaching by Gabeler.eu'
)
.addMetaTag(
'viewport',
'width=device-width, initial-scale=1.0'
);
}
/************************************************************
* SPREADSHEET OPENEN
************************************************************/
function getActiveSpreadsheet_() {
try {
var ss = SpreadsheetApp.getActiveSpreadsheet();
if (ss) {
return ss;
}
} catch (e) {
// Webapp-context kan geen actieve spreadsheet hebben.
}
return SpreadsheetApp.openById(SS_ID);
}
/************************************************************
* HEADER NORMALISEREN
*
* Hiermee worden bijvoorbeeld:
*
* "G1_Geluksbesef "
* " G1_Geluksbesef"
* "G1_Geluksbesef"
*
* als dezelfde header behandeld.
*
* Ook worden onzichtbare BOM/spaties verwijderd.
************************************************************/
function normalizeHeader_(value) {
if (value === null || value === undefined) {
return '';
}
return String(value)
.replace(/\uFEFF/g, '')
.replace(/\u00A0/g, ' ')
.trim();
}
/************************************************************
* HEADER-MAP MAKEN
*
* Geeft terug:
*
* {
* G1_Geluksbesef: 4,
* G2_Gevoelens: 5,
* ...
* }
*
* Kolomnummers zijn 1-based voor SpreadsheetApp.
************************************************************/
function getHeaderMap_(sheet) {
var lastColumn = sheet.getLastColumn();
if (lastColumn < 1) {
return {};
}
var headers = sheet
.getRange(1, 1, 1, lastColumn)
.getValues()[0];
var map = {};
for (var i = 0; i < headers.length; i++) {
var header = normalizeHeader_(headers[i]);
if (!header) {
continue;
}
map[header] = i + 1;
}
return map;
}
/************************************************************
* G20_Daily STRUCTUUR CONTROLEREN
*
* BELANGRIJK:
* We verwijderen GEEN bestaande kolommen.
*
* We voegen ontbrekende kolommen toe.
*
* Bestaande headers met spaties worden gecorrigeerd.
************************************************************/
function ensureDailySheet_() {
var ss = getActiveSpreadsheet_();
var sheet = ss.getSheetByName(DAILY_SHEET_NAME);
if (!sheet) {
sheet = ss.insertSheet(DAILY_SHEET_NAME);
var headers = getDailyHeaders_();
sheet
.getRange(1, 1, 1, headers.length)
.setValues([headers]);
formatDailyHeader_(sheet);
return sheet;
}
/**********************************************************
* Als het blad helemaal leeg is.
**********************************************************/
if (sheet.getLastColumn() === 0) {
var emptyHeaders = getDailyHeaders_();
sheet
.getRange(1, 1, 1, emptyHeaders.length)
.setValues([emptyHeaders]);
formatDailyHeader_(sheet);
return sheet;
}
/**********************************************************
* Bestaande headers lezen.
**********************************************************/
var lastColumn = sheet.getLastColumn();
var existingHeaders = sheet
.getRange(1, 1, 1, lastColumn)
.getValues()[0];
/**********************************************************
* Headers normaliseren.
**********************************************************/
for (var i = 0; i < existingHeaders.length; i++) {
var normalized = normalizeHeader_(existingHeaders[i]);
if (normalized !== existingHeaders[i]) {
sheet
.getRange(1, i + 1)
.setValue(normalized);
existingHeaders[i] = normalized;
}
}
/**********************************************************
* Ontbrekende gewenste headers toevoegen.
**********************************************************/
var required = getDailyHeaders_();
var currentMap = {};
for (var j = 0; j < existingHeaders.length; j++) {
var h = normalizeHeader_(existingHeaders[j]);
if (h) {
currentMap[h] = j + 1;
}
}
var missing = [];
for (var k = 0; k < required.length; k++) {
if (!currentMap[required[k]]) {
missing.push(required[k]);
}
}
if (missing.length > 0) {
var startColumn = sheet.getLastColumn() + 1;
sheet
.getRange(
1,
startColumn,
1,
missing.length
)
.setValues([missing]);
}
formatDailyHeader_(sheet);
return sheet;
}
/************************************************************
* HEADER OPMAAK
************************************************************/
function formatDailyHeader_(sheet) {
try {
var lastColumn = sheet.getLastColumn();
if (lastColumn < 1) {
return;
}
sheet
.getRange(1, 1, 1, lastColumn)
.setFontWeight('bold')
.setWrap(true);
sheet.setFrozenRows(1);
} catch (e) {
// Opmaak mag opslag nooit blokkeren.
}
}
/************************************************************
* SETUP ALLE TABBLADEN
************************************************************/
function setupG20() {
try {
var ss = getActiveSpreadsheet_();
ensureSheetsExist_(ss);
return {
success: true,
message:
'Alle G20 tabbladen en kolommen zijn gecontroleerd.'
};
} catch (e) {
return {
success: false,
message:
'Fout bij setupG20: ' + e.toString()
};
}
}
/************************************************************
* TABBLADEN AANMAKEN
************************************************************/
function ensureSheetsExist_(ss) {
var required = {
'G20_Users': [
'UserID',
'Naam',
'Email',
'PasswordHash',
'Status',
'CreatedAt',
'LastLogin',
'ProfileQuote',
'FavoriteTheme',
'Phone',
'Bio'
],
'G20_Daily':
getDailyHeaders_(),
'G20_Questions': [
'Nummer',
'Code',
'Titel',
'Emoji',
'Vraag',
'Reflectie'
],
'G20_Cards': [
'CardID',
'G20',
'Titel',
'Vraag',
'Reflectie',
'Emoji',
'Active'
],
'G20_Codes': [
'CodeID',
'Email',
'Code',
'Purpose',
'CreatedAt',
'ExpiresAt',
'Used'
],
'G20_Favorites': [
'FavoriteID',
'UserID',
'Type',
'ReferenceID',
'Title',
'G20',
'Note',
'CreatedAt'
]
};
for (var name in required) {
var sheet = ss.getSheetByName(name);
if (!sheet) {
sheet = ss.insertSheet(name);
sheet
.getRange(
1,
1,
1,
required[name].length
)
.setValues([required[name]]);
sheet
.getRange(
1,
1,
1,
required[name].length
)
.setFontWeight('bold');
sheet.setFrozenRows(1);
if (name === QUESTIONS_SHEET_NAME) {
seedQuestions_(sheet);
}
} else {
ensureSheetHeaders_(sheet, required[name]);
}
}
/**********************************************************
* Extra controle voor G20_Daily
**********************************************************/
ensureDailySheet_();
}
/************************************************************
* GENERIEKE SHEET HEADERS CONTROLEREN
************************************************************/
function ensureSheetHeaders_(sheet, requiredHeaders) {
if (sheet.getLastColumn() === 0) {
sheet
.getRange(
1,
1,
1,
requiredHeaders.length
)
.setValues([requiredHeaders]);
return;
}
var lastColumn = sheet.getLastColumn();
var existing = sheet
.getRange(
1,
1,
1,
lastColumn
)
.getValues()[0];
for (var i = 0; i < existing.length; i++) {
var normalized = normalizeHeader_(existing[i]);
if (normalized !== existing[i]) {
sheet
.getRange(1, i + 1)
.setValue(normalized);
existing[i] = normalized;
}
}
var existingMap = {};
for (var j = 0; j < existing.length; j++) {
var h = normalizeHeader_(existing[j]);
if (h) {
existingMap[h] = true;
}
}
var missing = [];
for (var k = 0; k < requiredHeaders.length; k++) {
if (!existingMap[requiredHeaders[k]]) {
missing.push(requiredHeaders[k]);
}
}
if (missing.length > 0) {
sheet
.getRange(
1,
sheet.getLastColumn() + 1,
1,
missing.length
)
.setValues([missing]);
}
}
/************************************************************
* TEST
************************************************************/
function testG20() {
return {
success: true,
app:
'G20 — Dagelijks Geluksbesef',
coach:
'Coach Stijn Gabeler',
spreadsheetId:
SS_ID,
dailyHeaders:
getDailyHeaders_(),
message:
'NextLevel Code.gs werkt correct.'
};
}
/************************************************************
* TEST SPECIFIEK OPSLAAN
*
* Deze functie kan vanuit Apps Script worden uitgevoerd
* om te controleren of alle 20 velden daadwerkelijk
* naar de juiste kolommen worden geschreven.
************************************************************/
function testG20DailyStorage() {
var payload = {
UserID: 'TEST_USER',
Datum:
Utilities.formatDate(
new Date(),
Session.getScriptTimeZone(),
'yyyy-MM-dd'
),
G1_Geluksbesef: 'TEST G1',
G2_Gevoelens: 'TEST G2',
G3_Gedroomd: 'TEST G3',
G4_Gemis: 'TEST G4',
G5_Gewenst: 'TEST G5',
G6_Gehoopt: 'TEST G6',
G7_Gedachten: 'TEST G7',
G8_Gedrag: 'TEST G8',
G9_Gezegd: 'TEST G9',
G10_Gedaan: 'TEST G10',
G11_Geschreven: 'TEST G11',
G12_Gelezen: 'TEST G12',
G13_Geleerd: 'TEST G13',
G14_Geweten: 'TEST G14',
G15_Gehoord: 'TEST G15',
G16_Gezien: 'TEST G16',
G17_Geproefd: 'TEST G17',
G18_Geroken: 'TEST G18',
G19_GedachtOver: 'TEST G19',
G20_Geluksgetal: '20',
Inzicht: 'TEST INZICHT',
MooisteMoment: 'TEST MOOISTE MOMENT',
MorgenStap: 'TEST MORGEN STAP'
};
return g20SaveDaily(payload);
}
/************************************************************
* G20 DAILY OPSLAAN
*
* BELANGRIJKSTE FUNCTIE
*
* De huidige Index roept exact deze functie aan:
*
* g20SaveDaily(payload)
*
* De opslag gebeurt KOLOM VOOR KOLOM op basis van
* de HEADERNAAM.
*
* Daardoor maakt de fysieke kolompositie niet uit.
************************************************************/
function g20SaveDaily(payload) {
try {
/********************************************************
* Payload controleren
********************************************************/
if (!payload || typeof payload !== 'object') {
return {
success: false,
message:
'Geen geldige G20-gegevens ontvangen.'
};
}
/********************************************************
* Spreadsheet / sheet
********************************************************/
var sheet = ensureDailySheet_();
/********************************************************
* Headers opnieuw ophalen
********************************************************/
var lastColumn = sheet.getLastColumn();
var headers = sheet
.getRange(
1,
1,
1,
lastColumn
)
.getValues()[0];
/********************************************************
* Headers normaliseren
********************************************************/
for (var h = 0; h < headers.length; h++) {
headers[h] =
normalizeHeader_(headers[h]);
}
/********************************************************
* Header map
*
* header -> kolomnummer
********************************************************/
var headerMap = {};
for (var i = 0; i < headers.length; i++) {
if (headers[i]) {
headerMap[headers[i]] =
i + 1;
}
}
/********************************************************
* Controleer alle 20 G-kolommen
********************************************************/
var missingG = [];
for (var g = 0; g < G20_KEYS_.length; g++) {
if (!headerMap[G20_KEYS_[g]]) {
missingG.push(G20_KEYS_[g]);
}
}
/********************************************************
* Als er iets ontbreekt: toevoegen.
********************************************************/
if (missingG.length > 0) {
var startColumn =
sheet.getLastColumn() + 1;
sheet
.getRange(
1,
startColumn,
1,
missingG.length
)
.setValues([missingG]);
/******************************************************
* Header map opnieuw maken.
******************************************************/
lastColumn =
sheet.getLastColumn();
headers =
sheet
.getRange(
1,
1,
1,
lastColumn
)
.getValues()[0];
headerMap = {};
for (var hm = 0; hm < headers.length; hm++) {
var normalizedHeader =
normalizeHeader_(headers[hm]);
if (normalizedHeader) {
headerMap[normalizedHeader] =
hm + 1;
}
}
}
/********************************************************
* Datum
********************************************************/
var targetDateStr = '';
if (
payload.Datum !== undefined &&
payload.Datum !== null &&
payload.Datum !== ''
) {
targetDateStr =
String(payload.Datum)
.substring(0, 10);
} else {
targetDateStr =
Utilities.formatDate(
new Date(),
Session.getScriptTimeZone(),
'yyyy-MM-dd'
);
}
/********************************************************
* UserID
********************************************************/
var userId =
payload.UserID !== undefined &&
payload.UserID !== null
? String(payload.UserID)
: '';
/********************************************************
* Bestaand record zoeken
*
* Unieke combinatie:
*
* UserID + Datum
********************************************************/
var data =
sheet
.getDataRange()
.getValues();
var targetRow = -1;
var userColumn =
headerMap['UserID'];
var dateColumn =
headerMap['Datum'];
if (
data.length > 1 &&
userColumn &&
dateColumn
) {
for (
var r = 1;
r < data.length;
r++
) {
var rowUser =
data[r][userColumn - 1];
var rowDate =
data[r][dateColumn - 1];
var rowUserString =
rowUser === null ||
rowUser === undefined
? ''
: String(rowUser);
var rowDateString =
normalizeDate_(rowDate);
if (
rowUserString === userId &&
rowDateString === targetDateStr
) {
targetRow =
r + 1;
break;
}
}
}
/********************************************************
* RecordID
********************************************************/
var recordId = '';
var createdAt = null;
if (targetRow > 0) {
var existingRecordIdColumn =
headerMap['RecordID'];
var existingCreatedColumn =
headerMap['CreatedAt'];
if (existingRecordIdColumn) {
recordId =
sheet
.getRange(
targetRow,
existingRecordIdColumn
)
.getValue();
}
if (existingCreatedColumn) {
createdAt =
sheet
.getRange(
targetRow,
existingCreatedColumn
)
.getValue();
}
}
if (!recordId) {
recordId =
'REC_' +
new Date().getTime() +
'_' +
Math.floor(
Math.random() * 10000
);
}
if (!createdAt) {
createdAt =
new Date();
}
/********************************************************
* NIEUWE / BESTAANDE RIJ
*
* We maken een array ter grootte van ALLE kolommen.
********************************************************/
var rowData =
new Array(headers.length);
/********************************************************
* Eerst bestaande waarden behouden bij update.
********************************************************/
if (targetRow > 0) {
var existingRow =
sheet
.getRange(
targetRow,
1,
1,
headers.length
)
.getValues()[0];
for (
var copy = 0;
copy < existingRow.length;
copy++
) {
rowData[copy] =
existingRow[copy];
}
}
/********************************************************
* RecordID
********************************************************/
setColumnValue_(
rowData,
headerMap,
'RecordID',
recordId
);
/********************************************************
* UserID
********************************************************/
setColumnValue_(
rowData,
headerMap,
'UserID',
userId
);
/********************************************************
* Datum
********************************************************/
setColumnValue_(
rowData,
headerMap,
'Datum',
targetDateStr
);
/********************************************************
* ALLE 20 G-VELDEN
*
* Hier gebeurt de daadwerkelijke opslag.
********************************************************/
for (
var gIndex = 0;
gIndex < G20_KEYS_.length;
gIndex++
) {
var gKey =
G20_KEYS_[gIndex];
var gValue =
getPayloadValue_(
payload,
gKey
);
setColumnValue_(
rowData,
headerMap,
gKey,
gValue
);
}
/********************************************************
* Inzicht
********************************************************/
setColumnValue_(
rowData,
headerMap,
'Inzicht',
getPayloadValue_(
payload,
'Inzicht'
)
);
/********************************************************
* MooisteMoment
********************************************************/
setColumnValue_(
rowData,
headerMap,
'MooisteMoment',
getPayloadValue_(
payload,
'MooisteMoment'
)
);
/********************************************************
* MorgenStap
********************************************************/
setColumnValue_(
rowData,
headerMap,
'MorgenStap',
getPayloadValue_(
payload,
'MorgenStap'
)
);
/********************************************************
* CreatedAt
********************************************************/
setColumnValue_(
rowData,
headerMap,
'CreatedAt',
createdAt
);
/********************************************************
* UpdatedAt
********************************************************/
setColumnValue_(
rowData,
headerMap,
'UpdatedAt',
new Date()
);
/********************************************************
* Lege arraywaarden voorkomen
********************************************************/
for (
var empty = 0;
empty < rowData.length;
empty++
) {
if (
rowData[empty] === undefined
) {
rowData[empty] = '';
}
}
/********************************************************
* OPSLAAN
********************************************************/
if (targetRow > 0) {
sheet
.getRange(
targetRow,
1,
1,
rowData.length
)
.setValues([rowData]);
} else {
sheet
.getRange(
sheet.getLastRow() + 1,
1,
1,
rowData.length
)
.setValues([rowData]);
}
/********************************************************
* Controleer na schrijven
********************************************************/
SpreadsheetApp.flush();
/********************************************************
* Geef terug welke G-velden daadwerkelijk zijn
* ontvangen.
*
* Dit is ook handig voor debugging.
********************************************************/
var received = {};
for (
var check = 0;
check < G20_KEYS_.length;
check++
) {
var checkKey =
G20_KEYS_[check];
received[checkKey] =
getPayloadValue_(
payload,
checkKey
);
}
return {
success: true,
message:
'Alle 20 G-velden en de overige dagvelden zijn opgeslagen.',
recordId:
recordId,
row:
targetRow > 0
? targetRow
: sheet.getLastRow(),
received:
received
};
} catch (e) {
return {
success: false,
message:
'Fout bij opslaan G20: ' +
e.toString()
};
}
}
/************************************************************
* PAYLOAD WAARDE OPHALEN
*
* Normaal:
*
* payload.G1_Geluksbesef
*
* Maar deze functie accepteert ook eenvoudige varianten.
************************************************************/
function getPayloadValue_(payload, key) {
if (
payload[key] !== undefined &&
payload[key] !== null
) {
return payload[key];
}
/**********************************************************
* Alternatieve schrijfwijzen voor extra robuustheid.
**********************************************************/
var alternatives = [];
if (
key.indexOf('G') === 0 &&
key.indexOf('_') !== -1
) {
var numberPart =
key.substring(
1,
key.indexOf('_')
);
alternatives.push(
'G' + numberPart
);
alternatives.push(
'g' + numberPart
);
}
for (
var i = 0;
i < alternatives.length;
i++
) {
if (
payload[alternatives[i]] !== undefined &&
payload[alternatives[i]] !== null
) {
return payload[alternatives[i]];
}
}
return '';
}
/************************************************************
* WAARDE IN JUISTE KOLOM ZETTEN
************************************************************/
function setColumnValue_(
rowData,
headerMap,
header,
value
) {
var column =
headerMap[header];
if (!column) {
return;
}
rowData[column - 1] =
value === null ||
value === undefined
? ''
: value;
}
/************************************************************
* DATUM NORMALISEREN
************************************************************/
function normalizeDate_(value) {
if (
value instanceof Date &&
!isNaN(value.getTime())
) {
return Utilities.formatDate(
value,
Session.getScriptTimeZone(),
'yyyy-MM-dd'
);
}
if (
value === null ||
value === undefined ||
value === ''
) {
return '';
}
var str =
String(value).trim();
if (!str) {
return '';
}
/**********************************************************
* ISO datum.
**********************************************************/
if (
/^\d{4}-\d{2}-\d{2}/.test(str)
) {
return str.substring(0, 10);
}
/**********************************************************
* Nederlandse datum dd-mm-yyyy.
**********************************************************/
var dutch =
str.match(
/^(\d{1,2})-(\d{1,2})-(\d{4})$/
);
if (dutch) {
return (
dutch[3] +
'-' +
('0' + dutch[2]).slice(-2) +
'-' +
('0' + dutch[1]).slice(-2)
);
}
return str.substring(0, 10);
}
/************************************************************
* G20 DAILY OPHALEN
************************************************************/
function g20GetDaily(
userId,
datum
) {
try {
var sheet =
ensureDailySheet_();
var data =
sheet
.getDataRange()
.getValues();
if (data.length <= 1) {
return {
success: false,
data: null
};
}
var headers = data[0];
var headerMap = {};
for (
var h = 0;
h < headers.length;
h++
) {
var normalized =
normalizeHeader_(headers[h]);
if (normalized) {
headerMap[normalized] =
h;
}
}
var userIndex =
headerMap['UserID'];
var dateIndex =
headerMap['Datum'];
if (
userIndex === undefined ||
dateIndex === undefined
) {
return {
success: false,
message:
'UserID of Datum kolom ontbreekt.'
};
}
var targetDate =
normalizeDate_(datum);
for (
var i = 1;
i < data.length;
i++
) {
var rowUser =
data[i][userIndex];
var rowDate =
data[i][dateIndex];
if (
String(rowUser) === String(userId) &&
normalizeDate_(rowDate) === targetDate
) {
var record = {};
for (
var c = 0;
c < headers.length;
c++
) {
var key =
normalizeHeader_(headers[c]);
if (key) {
var value =
data[i][c];
/**********************************************
* Date-objecten veilig naar de browser.
**********************************************/
if (
value instanceof Date &&
!isNaN(value.getTime())
) {
record[key] =
value.toISOString();
} else {
record[key] =
value;
}
}
}
return {
success: true,
data:
record
};
}
}
return {
success: false,
data: null
};
} catch (e) {
return {
success: false,
message:
'Fout bij ophalen dagboek: ' +
e.toString()
};
}
}
/************************************************************
* G20 GESCHIEDENIS
************************************************************/
function g20GetUserHistory(userId) {
try {
var sheet =
ensureDailySheet_();
var data =
sheet
.getDataRange()
.getValues();
if (data.length <= 1) {
return {
success: true,
history: []
};
}
var headers =
data[0];
var userIndex =
-1;
for (
var h = 0;
h < headers.length;
h++
) {
if (
normalizeHeader_(headers[h]) ===
'UserID'
) {
userIndex = h;
break;
}
}
if (userIndex === -1) {
return {
success: true,
history: []
};
}
var history = [];
for (
var i = 1;
i < data.length;
i++
) {
if (
String(data[i][userIndex]) ===
String(userId)
) {
var rowObj = {};
for (
var c = 0;
c < headers.length;
c++
) {
var key =
normalizeHeader_(headers[c]);
if (!key) {
continue;
}
var value =
data[i][c];
if (
value instanceof Date &&
!isNaN(value.getTime())
) {
rowObj[key] =
value.toISOString();
} else {
rowObj[key] =
value;
}
}
history.push(rowObj);
}
}
return {
success: true,
history:
history
};
} catch (e) {
return {
success: false,
message:
'Fout bij ophalen geschiedenis: ' +
e.toString()
};
}
}
/************************************************************
* G20 TITELS
************************************************************/
function getGTitle(num) {
var titles = [
'',
'Geluksbesef',
'Gevoelens',
'Gedroomd',
'Gemis',
'Gewenst',
'Gehoopt',
'Gedachten',
'Gedrag',
'Gezegd',
'Gedaan',
'Geschreven',
'Gelezen',
'Geleerd',
'Geweten',
'Gehoord',
'Gezien',
'Geproefd',
'Geroken',
'GedachtOver',
'Geluksgetal'
];
return titles[num] || '';
}
/************************************************************
* DEFAULT VRAGEN
************************************************************/
function getDefaultQuestions_() {
return [
{
number: 1,
code: 'G1_Geluksbesef',
title: 'Geluksbesef',
emoji: '😊',
question:
'Waar werd je vandaag gelukkig, blij of dankbaar van?',
reflection:
'Geluk wordt zichtbaar wanneer je er bewust aandacht aan geeft.'
},
{
number: 2,
code: 'G2_Gevoelens',
title: 'Gevoelens',
emoji: '❤️',
question:
'Welke gevoelens waren vandaag aanwezig?',
reflection:
'Gevoelens mogen eerst worden opgemerkt.'
},
{
number: 3,
code: 'G3_Gedroomd',
title: 'Gedroomd',
emoji: '💭',
question:
'Waar heb je vandaag over gedroomd of gefantaseerd?',
reflection:
'Een droom vertelt wat belangrijk is.'
},
{
number: 4,
code: 'G4_Gemis',
title: 'Gemis',
emoji: '🫶',
question:
'Wat of wie heb je vandaag gemist?',
reflection:
'Gemis maakt waardes zichtbaar.'
},
{
number: 5,
code: 'G5_Gewenst',
title: 'Gewenst',
emoji: '🌱',
question:
'Wat had je vandaag graag anders of beter gewild?',
reflection:
'Een wens wijst een richting aan.'
},
{
number: 6,
code: 'G6_Gehoopt',
title: 'Gehoopt',
emoji: '✨',
question:
'Waar hoopte je vandaag op?',
reflection:
'Hoop geeft ruimte voor de toekomst.'
},
{
number: 7,
code: 'G7_Gedachten',
title: 'Gedachten',
emoji: '🔍',
question:
'Welke gedachten beheersden vandaag je hoofd?',
reflection:
'Gedachten zijn feiten noch bevelen.'
},
{
number: 8,
code: 'G8_Gedrag',
title: 'Gedrag',
emoji: '☕',
question:
'Hoe handelde je vandaag; welk gedrag liet je zien?',
reflection:
'Je gedrag volgt je intentie.'
},
{
number: 9,
code: 'G9_Gezegd',
title: 'Gezegd',
emoji: '🎁',
question:
'Wat heb je vandaag uitgesproken of gedeeld?',
reflection:
'Woorden hebben impact.'
},
{
number: 10,
code: 'G10_Gedaan',
title: 'Gedaan',
emoji: '🤝',
question:
'Welke concrete daden heb je vandaag verricht?',
reflection:
'Daden spreken dieper dan intenties.'
},
{
number: 11,
code: 'G11_Geschreven',
title: 'Geschreven',
emoji: '🏆',
question:
'Wat heb je vandaag opgeschreven of vastgelegd?',
reflection:
'Schrijven structureert je binnenwereld.'
},
{
number: 12,
code: 'G12_Gelezen',
title: 'Gelezen',
emoji: '📚',
question:
'Wat heb je vandaag gelezen of tot je genomen?',
reflection:
'Inspiratie voedt je blik.'
},
{
number: 13,
code: 'G13_Geleerd',
title: 'Geleerd',
emoji: '🚀',
question:
'Wat heb je vandaag geleerd of ingezien?',
reflection:
'Groei ontstaat door reflectie.'
},
{
number: 14,
code: 'G14_Geweten',
title: 'Geweten',
emoji: '😄',
question:
'Wat zei je geweten vandaag over je keuzes?',
reflection:
'Je geweten is je zuivere kompas.'
},
{
number: 15,
code: 'G15_Gehoord',
title: 'Gehoord',
emoji: '🧘',
question:
'Wat heb je vandaag opgevangen of gehoord?',
reflection:
'Luisteren brengt verbinding.'
},
{
number: 16,
code: 'G16_Gezien',
title: 'Gezien',
emoji: '🎉',
question:
'Wat is je vandaag opgevallen of scherp gezien?',
reflection:
'Aandacht maakt details zichtbaar.'
},
{
number: 17,
code: 'G17_Geproefd',
title: 'Geproefd',
emoji: '🌧️',
question:
'Waar heb je van geproefd, letterlijk of figuurlijk?',
reflection:
'Ervaringen kleuren je dag.'
},
{
number: 18,
code: 'G18_Geroken',
title: 'Geroken',
emoji: '⏳',
question:
'Welke geuren of indrukken kwamen binnen?',
reflection:
'Geur brengt je bij het moment.'
},
{
number: 19,
code: 'G19_GedachtOver',
title: 'GedachtOver',
emoji: '⭐',
question:
'Waar heb je vandaag diep over nagedacht?',
reflection:
'Overpeinzing brengt helderheid.'
},
{
number: 20,
code: 'G20_Geluksgetal',
title: 'Geluksgetal',
emoji: '🌊',
question:
'Wat was jouw persoonlijke geluksgetal of score vandaag?',
reflection:
'Cijfers geven structuur aan beleving.'
}
];
}
/************************************************************
* VRAGEN OPHALEN
************************************************************/
function g20GetQuestions() {
try {
var ss =
getActiveSpreadsheet_();
var sheet =
ss.getSheetByName(
QUESTIONS_SHEET_NAME
);
if (
!sheet ||
sheet.getLastRow() <= 1
) {
return {
success: true,
questions:
getDefaultQuestions_()
};
}
var data =
sheet
.getDataRange()
.getValues();
var questions = [];
for (
var i = 1;
i < data.length;
i++
) {
if (!data[i][0]) {
continue;
}
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]
});
}
if (questions.length === 0) {
questions =
getDefaultQuestions_();
}
return {
success: true,
questions:
questions
};
} catch (e) {
return {
success: true,
questions:
getDefaultQuestions_()
};
}
}
/************************************************************
* VRAGEN INITIALISEREN
************************************************************/
function seedQuestions_(sheet) {
var questions =
getDefaultQuestions_();
for (
var i = 0;
i < questions.length;
i++
) {
var q =
questions[i];
sheet.appendRow([
q.number,
q.code,
q.title,
q.emoji,
q.question,
q.reflection
]);
}
}
/************************************************************
* KAARTEN
************************************************************/
function g20GetCards() {
try {
var ss =
getActiveSpreadsheet_();
var sheet =
ss.getSheetByName(
CARDS_SHEET_NAME
);
if (!sheet) {
return {
success: true,
cards: []
};
}
var data =
sheet
.getDataRange()
.getValues();
var cards = [];
for (
var i = 1;
i < data.length;
i++
) {
if (!data[i][0]) {
continue;
}
cards.push({
cardId:
data[i][0],
category:
data[i][1],
title:
data[i][2],
description:
data[i][4] ||
data[i][3]
});
}
return {
success: true,
cards:
cards
};
} catch (e) {
return {
success: false,
message:
e.toString()
};
}
}
/************************************************************
* PROFIEL OPSLAAN
************************************************************/
function g20SaveProfile(
userId,
profileData
) {
try {
var ss =
getActiveSpreadsheet_();
var sheet =
ss.getSheetByName(
USERS_SHEET_NAME
);
if (!sheet) {
return {
success: false,
message:
'Gebruikersblad niet gevonden.'
};
}
var data =
sheet
.getDataRange()
.getValues();
for (
var i = 1;
i < data.length;
i++
) {
if (
String(data[i][0]) ===
String(userId)
) {
sheet
.getRange(i + 1, 2)
.setValue(
profileData.name || ''
);
sheet
.getRange(i + 1, 8)
.setValue(
profileData.quote || ''
);
sheet
.getRange(i + 1, 9)
.setValue(
profileData.favoriteTheme || ''
);
if (
profileData.phone !== undefined
) {
sheet
.getRange(i + 1, 10)
.setValue(
profileData.phone
);
}
if (
profileData.bio !== undefined
) {
sheet
.getRange(i + 1, 11)
.setValue(
profileData.bio
);
}
return {
success: true,
message:
'Profiel opgeslagen!'
};
}
}
return {
success: false,
message:
'Gebruiker niet gevonden.'
};
} catch (e) {
return {
success: false,
message:
'Fout bij opslaan profiel: ' +
e.toString()
};
}
}
/************************************************************
* LOGIN
************************************************************/
function g20Login(
email,
password
) {
try {
var ss =
getActiveSpreadsheet_();
var sheet =
ss.getSheetByName(
USERS_SHEET_NAME
);
if (!sheet) {
ensureSheetsExist_(ss);
sheet =
ss.getSheetByName(
USERS_SHEET_NAME
);
}
var data =
sheet
.getDataRange()
.getValues();
var inputHash =
hashCode_(password);
var oldHash =
oldHashCode_(password);
for (
var i = 1;
i < data.length;
i++
) {
var rowEmail =
data[i][2]
? String(data[i][2])
.trim()
.toLowerCase()
: '';
if (
rowEmail ===
String(email)
.trim()
.toLowerCase()
) {
var storedHash =
data[i][3]
? String(data[i][3]).trim()
: '';
if (
storedHash === inputHash ||
storedHash === oldHash ||
storedHash === String(password)
) {
/**************************************************
* Upgrade oud wachtwoord.
**************************************************/
if (
storedHash !== inputHash
) {
sheet
.getRange(i + 1, 4)
.setValue(inputHash);
}
/**************************************************
* LastLogin.
**************************************************/
sheet
.getRange(i + 1, 7)
.setValue(new Date());
return {
success: true,
user: {
userId:
data[i][0],
name:
data[i][1],
email:
data[i][2],
status:
data[i][4] ||
'ACTIVE',
quote:
data[i][7] ||
'',
favoriteTheme:
data[i][8] ||
''
},
message:
'Succesvol ingelogd.'
};
}
return {
success: false,
message:
'Onjuist wachtwoord.'
};
}
}
return {
success: false,
message:
'Geen account gevonden met dit e-mailadres.'
};
} catch (e) {
return {
success: false,
message:
'Fout bij inloggen: ' +
e.toString()
};
}
}
/************************************************************
* REGISTRATIE
************************************************************/
function g20RequestRegistrationCode(
name,
email,
password
) {
try {
var ss =
getActiveSpreadsheet_();
ensureSheetsExist_(ss);
var usersSheet =
ss.getSheetByName(
USERS_SHEET_NAME
);
var codesSheet =
ss.getSheetByName(
CODES_SHEET_NAME
);
var data =
usersSheet
.getDataRange()
.getValues();
for (
var i = 1;
i < data.length;
i++
) {
if (
data[i][2] &&
String(data[i][2])
.trim()
.toLowerCase() ===
String(email)
.trim()
.toLowerCase()
) {
return {
success: false,
message:
'Dit e-mailadres is al geregistreerd.'
};
}
}
var userId =
'U_' +
new Date().getTime();
var hash =
hashCode_(password);
var mailCode =
Math.floor(
100000 +
Math.random() * 900000
).toString();
usersSheet.appendRow([
userId,
name,
String(email).trim(),
hash,
'PENDING_VERIFICATION',
new Date(),
'',
'Jij bepaalt je eigen koers.',
'Geluksbesef',
'',
''
]);
if (codesSheet) {
var expiresAt =
new Date(
new Date().getTime() +
15 * 60000
);
codesSheet.appendRow([
'CODE_' +
new Date().getTime(),
String(email).trim(),
mailCode,
'register',
new Date(),
expiresAt,
'FALSE'
]);
}
try {
MailApp.sendEmail({
to: email,
subject:
'Jouw verificatiecode — G20 NextLevel',
body:
'Beste ' +
name +
',\n\n' +
'Welkom bij G20 NextLevel.\n\n' +
'Jouw 6-cijferige verificatiecode is:\n\n' +
mailCode +
'\n\n' +
'Deze code is 15 minuten geldig.\n\n' +
'Coach Stijn Gabeler'
});
} catch (mailError) {
console.log(
'E-mail fout: ' +
mailError.toString()
);
}
return {
success: true,
message:
'Verificatiecode is succesvol verzonden naar je e-mailadres.'
};
} catch (e) {
return {
success: false,
message:
'Fout bij registratie: ' +
e.toString()
};
}
}
/************************************************************
* VERIFICATIECODE
************************************************************/
function g20VerifyCode(
email,
code
) {
try {
var ss =
getActiveSpreadsheet_();
var codesSheet =
ss.getSheetByName(
CODES_SHEET_NAME
);
var usersSheet =
ss.getSheetByName(
USERS_SHEET_NAME
);
if (
!codesSheet ||
!usersSheet
) {
return {
success: false,
message:
'Registratiesysteem niet geïnitialiseerd.'
};
}
var codesData =
codesSheet
.getDataRange()
.getValues();
var verified = false;
for (
var i = 1;
i < codesData.length;
i++
) {
var codeEmail =
codesData[i][1]
? String(codesData[i][1])
.trim()
.toLowerCase()
: '';
var storedCode =
codesData[i][2]
? String(codesData[i][2]).trim()
: '';
var purpose =
String(codesData[i][3] || '')
.trim();
var used =
String(codesData[i][6] || '')
.trim()
.toUpperCase();
if (
codeEmail ===
String(email)
.trim()
.toLowerCase() &&
storedCode ===
String(code).trim() &&
purpose ===
'register' &&
used !==
'TRUE'
) {
var expiresAt =
codesData[i][5];
if (
expiresAt instanceof Date &&
expiresAt.getTime() <
new Date().getTime()
) {
return {
success: false,
message:
'Deze verificatiecode is verlopen.'
};
}
codesSheet
.getRange(i + 1, 7)
.setValue('TRUE');
verified = true;
break;
}
}
if (!verified) {
return {
success: false,
message:
'Onjuiste of verlopen verificatiecode.'
};
}
var usersData =
usersSheet
.getDataRange()
.getValues();
for (
var j = 1;
j < usersData.length;
j++
) {
if (
usersData[j][2] &&
String(usersData[j][2])
.trim()
.toLowerCase() ===
String(email)
.trim()
.toLowerCase()
) {
usersSheet
.getRange(j + 1, 5)
.setValue('ACTIVE');
return {
success: true,
message:
'Account succesvol geactiveerd! Je kunt nu inloggen.'
};
}
}
return {
success: false,
message:
'Gebruiker niet gevonden.'
};
} catch (e) {
return {
success: false,
message:
'Fout bij verifiëren: ' +
e.toString()
};
}
}
/************************************************************
* WACHTWOORD RESET AANVRAGEN
************************************************************/
function g20RequestPasswordReset(
) {
try {
var ss =
getActiveSpreadsheet_();
var usersSheet =
ss.getSheetByName(
USERS_SHEET_NAME
);
var codesSheet =
ss.getSheetByName(
CODES_SHEET_NAME
);
if (!usersSheet) {
return {
success: false,
message:
'Gebruikerssysteem niet geïnitialiseerd.'
};
}
var data =
usersSheet
.getDataRange()
.getValues();
var found = false;
for (
var i = 1;
i < data.length;
i++
) {
if (
data[i][2] &&
String(data[i][2])
.trim()
.toLowerCase() ===
String(email)
.trim()
.toLowerCase()
) {
found = true;
break;
}
}
if (!found) {
return {
success: false,
message:
'Dit e-mailadres is niet bekend.'
};
}
var resetCode =
Math.floor(
100000 +
Math.random() * 900000
).toString();
var expiresAt =
new Date(
new Date().getTime() +
15 * 60000
);
if (codesSheet) {
codesSheet.appendRow([
'RESET_' +
new Date().getTime(),
String(email).trim(),
resetCode,
'reset',
new Date(),
expiresAt,
'FALSE'
]);
}
try {
MailApp.sendEmail({
to: email,
subject:
'Wachtwoord herstellen — G20 NextLevel',
body:
'Je herstelcode voor G20 NextLevel is:\n\n' +
resetCode +
'\n\n' +
'Deze code is 15 minuten geldig.\n\n' +
'Gebruik deze code in de app om een nieuw wachtwoord in te stellen.\n\n' +
'Coach Stijn Gabeler'
});
} catch (mailError) {
console.log(
'Mail reset fout: ' +
mailError.toString()
);
}
return {
success: true,
message:
'Herstelcode is succesvol verzonden naar je e-mailadres.'
};
} catch (e) {
return {
success: false,
message:
'Fout bij herstel aanvragen: ' +
e.toString()
};
}
}
/************************************************************
* WACHTWOORD RESETTEN
************************************************************/
function g20ResetPassword(
email,
code,
newPassword
) {
try {
var ss =
getActiveSpreadsheet_();
var codesSheet =
ss.getSheetByName(
CODES_SHEET_NAME
);
var usersSheet =
ss.getSheetByName(
USERS_SHEET_NAME
);
if (
!codesSheet ||
!usersSheet
) {
return {
success: false,
message:
'Wachtwoordsysteem niet geïnitialiseerd.'
};
}
var cData =
codesSheet
.getDataRange()
.getValues();
var codeValid = false;
for (
var i = 1;
i < cData.length;
i++
) {
var codeEmail =
cData[i][1]
? String(cData[i][1])
.trim()
.toLowerCase()
: '';
var storedCode =
cData[i][2]
? String(cData[i][2]).trim()
: '';
var purpose =
String(cData[i][3] || '')
.trim();
var used =
String(cData[i][6] || '')
.trim()
.toUpperCase();
if (
codeEmail ===
String(email)
.trim()
.toLowerCase() &&
storedCode ===
String(code).trim() &&
purpose ===
'reset' &&
used !==
'TRUE'
) {
var expiresAt =
cData[i][5];
if (
expiresAt instanceof Date &&
expiresAt.getTime() <
new Date().getTime()
) {
return {
success: false,
message:
'Deze herstelcode is verlopen.'
};
}
codesSheet
.getRange(i + 1, 7)
.setValue('TRUE');
codeValid = true;
break;
}
}
if (!codeValid) {
return {
success: false,
message:
'Onjuiste of verlopen herstelcode.'
};
}
var uData =
usersSheet
.getDataRange()
.getValues();
var newHash =
hashCode_(newPassword);
for (
var j = 1;
j < uData.length;
j++
) {
if (
uData[j][2] &&
String(uData[j][2])
.trim()
.toLowerCase() ===
String(email)
.trim()
.toLowerCase()
) {
usersSheet
.getRange(j + 1, 4)
.setValue(newHash);
usersSheet
.getRange(j + 1, 5)
.setValue('ACTIVE');
return {
success: true,
message:
'Wachtwoord succesvol gewijzigd! Je kunt nu direct inloggen.'
};
}
}
return {
success: false,
message:
'E-mailadres niet gevonden in de gebruikerslijst.'
};
} catch (e) {
return {
success: false,
message:
'Fout bij resetten wachtwoord: ' +
e.toString()
};
}
}
/************************************************************
* SHA-256
************************************************************/
function hashCode_(str) {
if (
str === null ||
str === undefined ||
str === ''
) {
return '';
}
var rawHash =
Utilities.computeDigest(
Utilities.DigestAlgorithm.SHA_256,
String(str),
Utilities.Charset.UTF_8
);
var output = '';
for (
var i = 0;
i < rawHash.length;
i++
) {
var byteValue =
rawHash[i];
if (byteValue < 0) {
byteValue += 256;
}
var byteString =
byteValue.toString(16);
if (
byteString.length === 1
) {
byteString =
'0' +
byteString;
}
output +=
byteString;
}
return output;
}
/************************************************************
* OUDE HASH
************************************************************/
function oldHashCode_(str) {
var hash = 0;
if (
!str ||
str.length === 0
) {
return hash.toString();
}
for (
var i = 0;
i < str.length;
i++
) {
var char =
str.charCodeAt(i);
hash =
((hash << 5) - hash) +
char;
hash =
hash & hash;
}
return hash.toString();
}
/************************************************************
* EINDE CODE.GS
************************************************************/
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 | NextLevel 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; }
.header-links { margin-top: 10px; font-size: 0.9rem; }
.header-links a { color: var(--primary-light); text-decoration: none; font-weight: 600; margin: 0 8px; }
.header-links a:hover { text-decoration: underline; }
.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); }
/* Wachtwoord wrapper met oogje */
.password-wrapper { position: relative; display: flex; align-items: center; }
.password-wrapper input { padding-right: 45px; }
.toggle-password { position: absolute; right: 12px; background: none; border: none; cursor: pointer; font-size: 1.2rem; color: var(--text-light); padding: 4px; }
.toggle-password:hover { color: var(--primary); }
.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; }
/* Navbar & Menu */
.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; }
/* Overzicht / Geschiedenis */
.history-card { background: var(--bg); border: 1px solid var(--border); border-radius: 8px; padding: 15px; margin-bottom: 12px; display: flex; justify-content: space-between; align-items: center; }
.history-info h4 { color: var(--primary); font-size: 1rem; margin-bottom: 4px; }
.history-info p { font-size: 0.85rem; color: var(--text-light); }
.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; }
footer a { color: var(--primary-light); text-decoration: none; }
footer a:hover { text-decoration: underline; }
</style>
</head>
<body>
<div class="container">
<header>
<h1>G20 — Dagelijks Geluksbesef</h1>
<p>Coaching by Gabeler.eu | Coach Stijn Gabeler</p>
<div class="header-links">
<a href="https://sites.google.com/view/gabeler" target="_blank">Gabeler.eu</a> |
<a href="https://sites.google.com/view/coachstijn" target="_blank">Coach Stijn</a>
</div>
</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>
<div class="password-wrapper">
<input type="password" id="loginPassword" placeholder="••••••••">
<button type="button" class="toggle-password" onclick="togglePasswordVisibility('loginPassword', this)" title="Toon/verberg wachtwoord">👁️</button>
</div>
</div>
<button type="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>
<div class="password-wrapper">
<input type="password" id="regPassword" placeholder="••••••••">
<button type="button" class="toggle-password" onclick="togglePasswordVisibility('regPassword', this)" title="Toon/verberg wachtwoord">👁️</button>
</div>
</div>
<button type="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 type="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 type="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 (6 cijfers)</label>
<input type="text" id="resetCode" placeholder="123456" maxlength="6">
</div>
<div class="form-group">
<label>Nieuw Wachtwoord</label>
<div class="password-wrapper">
<input type="password" id="newPassword" placeholder="••••••••">
<button type="button" class="toggle-password" onclick="togglePasswordVisibility('newPassword', this)" title="Toon/verberg wachtwoord">👁️</button>
</div>
</div>
<button type="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 type="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="openHistory()">
<h3>📊 Mijn Dagen & Overzicht</h3>
<p>Bekijk al je eerdere invullingen en analyses.</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>
</div>
<!-- SCHERM 5: G20 DAGELIJKS FORMULIER -->
<div id="screen-g20-form" class="screen">
<div class="navbar">
<span>G20 Dagboek invullen</span>
<button type="button" onclick="showScreen('screen-dashboard')">Terug</button>
</div>
<div class="form-group">
<label>Selecteer Datum</label>
<input type="date" id="dailyDate" onchange="loadDailyForDate()">
</div>
<div class="form-group">
<label>Algemeen Inzicht van de dag</label>
<textarea id="dailyInzicht" rows="3" placeholder="Wat is je belangrijkste inzicht vandaag?"></textarea>
</div>
<div class="form-group">
<label>Mooiste Moment</label>
<input type="text" id="dailyMooiste" placeholder="Wat was het mooiste moment?">
</div>
<div class="form-group">
<label>Stap voor Morgen</label>
<input type="text" id="dailyStap" placeholder="Welke stap zet je morgen?">
</div>
<hr style="margin: 25px 0; border:0; border-top:1px solid var(--border);">
<h3>De 20 G's van Bewustwording</h3>
<p class="sub">Vul hieronder jouw persoonlijke reflecties in op de 20 G-thema's.</p>
<div id="g20QuestionsContainer"></div>
<button type="button" class="btn btn-accent" onclick="saveG20Daily(event)">Dagboek Opslaan</button>
<button type="button" class="btn btn-secondary" onclick="showScreen('screen-dashboard')" style="margin-top: 10px;">Terug naar Menu</button>
</div>
<!-- SCHERM 6: GESCHIEDENIS & ANALYSE -->
<div id="screen-history" class="screen">
<div class="navbar">
<span>Mijn Ingevulde Dagen</span>
<button type="button" onclick="showScreen('screen-dashboard')">Terug</button>
</div>
<p class="sub">Hier zie je een overzicht van al jouw opgeslagen dagen. Klik op 'Bewerken' om een dag in te zien of bij te werken.</p>
<div id="historyListContainer"></div>
<button type="button" class="btn btn-secondary" onclick="showScreen('screen-dashboard')">Terug naar Menu</button>
</div>
<!-- SCHERM 7: KAARTEN -->
<div id="screen-cards" class="screen">
<div class="navbar">
<span>Coachingkaarten</span>
<button type="button" onclick="showScreen('screen-dashboard')">Terug</button>
</div>
<div id="cardsContainer"></div>
<button type="button" class="btn btn-secondary" onclick="showScreen('screen-dashboard')">Terug naar Menu</button>
</div>
<!-- SCHERM 8: PROFIEL -->
<div id="screen-profile" class="screen">
<div class="navbar">
<span>Mijn Profiel</span>
<button type="button" onclick="showScreen('screen-dashboard')">Terug</button>
</div>
<div class="form-group">
<label>Naam</label>
<input type="text" id="profileName">
</div>
<div class="form-group">
<label>E-mailadres</label>
<input type="email" id="profileEmail" disabled>
</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>Favoriet Thema</label>
<input type="text" id="profileTheme" placeholder="Geluksbesef">
</div>
<button type="button" class="btn btn-accent" onclick="saveProfile()">Profiel Opslaan</button>
<button type="button" class="btn btn-secondary" onclick="showScreen('screen-dashboard')">Terug naar Menu</button>
</div>
<footer>
<p>© 2026 Coaching by Gabeler.eu — Stijn Gabeler | <a href="https://sites.google.com/view/gabeler" target="_blank">Bezoek Gabeler.eu</a></p>
</footer>
</div>
<script>
let currentUser = null;
let cachedQuestions = [];
let tempRegEmail = '';
let tempForgotEmail = '';
function showScreen(screenId) {
document.querySelectorAll('.screen').forEach(s => s.classList.remove('active'));
document.getElementById(screenId).classList.add('active');
hideAlert();
}
function showAlert(msg, type = 'success') {
const box = document.getElementById('alertBox');
box.className = 'alert ' + (type === 'success' ? 'alert-success' : 'alert-error');
box.innerText = msg;
box.style.display = 'block';
window.scrollTo({ top: 0, behavior: 'smooth' });
}
function hideAlert() {
document.getElementById('alertBox').style.display = 'none';
}
function showSpinner(show) {
document.getElementById('spinner').style.display = show ? 'block' : 'none';
}
function togglePasswordVisibility(inputId, btn) {
const input = document.getElementById(inputId);
if (input.type === 'password') {
input.type = 'text';
btn.innerText = '🙈';
} else {
input.type = 'password';
btn.innerText = '👁️';
}
}
// --- AUTH LOGICA ---
function handleLogin() {
const email = document.getElementById('loginEmail').value.trim();
const password = document.getElementById('loginPassword').value.trim();
if (!email || !password) {
showAlert('Vul zowel e-mailadres als wachtwoord in.', 'error');
return;
}
showSpinner(true);
google.script.run
.withSuccessHandler(res => {
showSpinner(false);
if (res.success) {
currentUser = res.user;
document.getElementById('welcomeUser').innerText = 'Welkom, ' + currentUser.name;
showScreen('screen-dashboard');
} else {
showAlert(res.message, 'error');
}
})
.withFailureHandler(err => {
showSpinner(false);
showAlert('Technische fout: ' + err.message, 'error');
})
.g20Login(email, password);
}
function handleLogout() {
currentUser = null;
document.getElementById('loginEmail').value = '';
document.getElementById('loginPassword').value = '';
showScreen('screen-login');
}
function handleRegisterRequest() {
const name = document.getElementById('regName').value.trim();
const email = document.getElementById('regEmail').value.trim();
const password = document.getElementById('regPassword').value.trim();
if (!name || !email || !password) {
showAlert('Vul alle velden in om te registreren.', 'error');
return;
}
tempRegEmail = email;
showSpinner(true);
google.script.run
.withSuccessHandler(res => {
showSpinner(false);
showAlert(res.message, res.success ? 'success' : 'error');
if (res.success) {
showScreen('screen-register-verify');
}
})
.withFailureHandler(err => {
showSpinner(false);
showAlert('Fout bij registratie: ' + err.message, 'error');
})
.g20RequestRegistrationCode(name, email, password);
}
function handleVerifyCode() {
const code = document.getElementById('verifyCodeInput').value.trim();
if (!code) {
showAlert('Vul de verificatiecode in.', 'error');
return;
}
showSpinner(true);
google.script.run
.withSuccessHandler(res => {
showSpinner(false);
showAlert(res.message, res.success ? 'success' : 'error');
if (res.success) {
showScreen('screen-login');
}
})
.withFailureHandler(err => {
showSpinner(false);
showAlert('Fout bij verifiëren: ' + err.message, 'error');
})
.g20VerifyCode(tempRegEmail, code);
}
function handleRequestForgot() {
const email = document.getElementById('forgotEmail').value.trim();
if (!email) {
showAlert('Vul je e-mailadres in.', 'error');
return;
}
tempForgotEmail = email;
showSpinner(true);
google.script.run
.withSuccessHandler(res => {
showSpinner(false);
showAlert(res.message, res.success ? 'success' : 'error');
if (res.success) {
document.getElementById('resetSection').style.display = 'block';
}
})
.withFailureHandler(err => {
showSpinner(false);
showAlert('Fout: ' + err.message, 'error');
})
.g20RequestPasswordReset(email);
}
function handleExecuteReset() {
const code = document.getElementById('resetCode').value.trim();
const newPassword = document.getElementById('newPassword').value.trim();
if (!code || !newPassword) {
showAlert('Vul de herstelcode en je nieuwe wachtwoord in.', 'error');
return;
}
showSpinner(true);
google.script.run
.withSuccessHandler(res => {
showSpinner(false);
showAlert(res.message, res.success ? 'success' : 'error');
if (res.success) {
showScreen('screen-login');
}
})
.withFailureHandler(err => {
showSpinner(false);
showAlert('Fout: ' + err.message, 'error');
})
.g20ResetPassword(tempForgotEmail, code, newPassword);
}
// --- G20 FORMULIER & VRAGEN (GEÜPDATET) ---
function openG20Form() {
const today = new Date().toISOString().split('T')[0];
document.getElementById('dailyDate').value = today;
showScreen('screen-g20-form');
loadQuestionsAndData(today);
}
function loadDailyForDate() {
const datum = document.getElementById('dailyDate').value;
loadQuestionsAndData(datum);
}
function renderQuestions(questions) {
const container = document.getElementById('g20QuestionsContainer');
if (!container) {
console.error('g20QuestionsContainer niet gevonden.');
return;
}
const canonicalCodes = [
'G1_Geluksbesef', 'G2_Gevoelens', 'G3_Gedroomd', 'G4_Gemis', 'G5_Gewenst',
'G6_Gehoopt', 'G7_Gedachten', 'G8_Gedrag', 'G9_Gezegd', 'G10_Gedaan',
'G11_Geschreven', 'G12_Gelezen', 'G13_Geleerd', 'G14_Geweten', 'G15_Gehoord',
'G16_Gezien', 'G17_Geproefd', 'G18_Geroken', 'G19_GedachtOver', 'G20_Geluksgetal'
];
let html = '';
for (let i = 0; i < 20; i++) {
const q = questions[i] || {};
const canonicalCode = canonicalCodes[i];
html += `
<div class="g20-item">
<div class="g20-item-header">
<span class="g20-emoji">${q.emoji || ''}</span>
<span class="g20-title">${i + 1}. ${q.title || getFallbackGTitle_(i + 1)}</span>
</div>
<div class="g20-question">${q.question || ''}</div>
<div class="form-group" style="margin-bottom:0;">
<textarea
id="input_${canonicalCode}"
data-g20-code="${canonicalCode}"
data-g20-number="${i + 1}"
rows="2"
autocomplete="off"
placeholder="Jouw antwoord..."
></textarea>
</div>
</div>
`;
}
container.innerHTML = html;
}
function getFallbackGTitle_(number) {
const titles = [
'', 'Geluksbesef', 'Gevoelens', 'Gedroomd', 'Gemis', 'Gewenst',
'Gehoopt', 'Gedachten', 'Gedrag', 'Gezegd', 'Gedaan',
'Geschreven', 'Gelezen', 'Geleerd', 'Geweten', 'Gehoord',
'Gezien', 'Geproefd', 'Geroken', 'Gedacht over', 'Geluksgetal'
];
return titles[number] || '';
}
function loadQuestionsAndData(datum) {
if (!currentUser || !currentUser.userId) {
showAlert('Je bent niet ingelogd.', 'error');
return;
}
if (!datum) {
showAlert('Selecteer eerst een datum.', 'error');
return;
}
const canonicalCodes = [
'G1_Geluksbesef', 'G2_Gevoelens', 'G3_Gedroomd', 'G4_Gemis', 'G5_Gewenst',
'G6_Gehoopt', 'G7_Gedachten', 'G8_Gedrag', 'G9_Gezegd', 'G10_Gedaan',
'G11_Geschreven', 'G12_Gelezen', 'G13_Geleerd', 'G14_Geweten', 'G15_Gehoord',
'G16_Gezien', 'G17_Geproefd', 'G18_Geroken', 'G19_GedachtOver', 'G20_Geluksgetal'
];
showSpinner(true);
google.script.run
.withSuccessHandler(function(qRes) {
if (!qRes || !qRes.success) {
showSpinner(false);
showAlert('Kon de 20 G-vragen niet laden.', 'error');
return;
}
cachedQuestions = qRes.questions || [];
renderQuestions(cachedQuestions);
google.script.run
.withSuccessHandler(function(dRes) {
// Eerst ALLES leegmaken
document.getElementById('dailyInzicht').value = '';
document.getElementById('dailyMooiste').value = '';
document.getElementById('dailyStap').value = '';
canonicalCodes.forEach(function(code) {
const field = document.getElementById('input_' + code);
if (field) {
field.value = '';
}
});
// Daarna eventueel opgeslagen gegevens terugzetten
if (dRes && dRes.success && dRes.data) {
document.getElementById('dailyInzicht').value = dRes.data.Inzicht || '';
document.getElementById('dailyMooiste').value = dRes.data.MooisteMoment || '';
document.getElementById('dailyStap').value = dRes.data.MorgenStap || '';
canonicalCodes.forEach(function(code) {
const field = document.getElementById('input_' + code);
if (!field) return;
if (dRes.data[code] !== undefined && dRes.data[code] !== null) {
field.value = dRes.data[code];
}
});
}
showSpinner(false);
})
.withFailureHandler(function(err) {
showSpinner(false);
showAlert('Fout bij ophalen dagboek: ' + (err && err.message ? err.message : err), 'error');
})
.g20GetDaily(currentUser.userId, datum);
})
.withFailureHandler(function(err) {
showSpinner(false);
showAlert('Fout bij ophalen vragen: ' + (err && err.message ? err.message : err), 'error');
})
.g20GetQuestions();
}
function saveG20Daily(event) {
if (event) {
event.preventDefault();
event.stopPropagation();
}
if (!currentUser || !currentUser.userId) {
showAlert('Je bent niet ingelogd.', 'error');
return false;
}
const dateElement = document.getElementById('dailyDate');
const datum = dateElement ? dateElement.value : '';
if (!datum) {
showAlert('Selecteer een datum.', 'error');
return false;
}
const gCodes = [
'G1_Geluksbesef', 'G2_Gevoelens', 'G3_Gedroomd', 'G4_Gemis', 'G5_Gewenst',
'G6_Gehoopt', 'G7_Gedachten', 'G8_Gedrag', 'G9_Gezegd', 'G10_Gedaan',
'G11_Geschreven', 'G12_Gelezen', 'G13_Geleerd', 'G14_Geweten', 'G15_Gehoord',
'G16_Gezien', 'G17_Geproefd', 'G18_Geroken', 'G19_GedachtOver', 'G20_Geluksgetal'
];
const payload = {
UserID: String(currentUser.userId || ''),
Datum: datum,
Inzicht: document.getElementById('dailyInzicht') ? document.getElementById('dailyInzicht').value : '',
MooisteMoment: document.getElementById('dailyMooiste') ? document.getElementById('dailyMooiste').value : '',
MorgenStap: document.getElementById('dailyStap') ? document.getElementById('dailyStap').value : ''
};
gCodes.forEach(function(code) {
const field = document.getElementById('input_' + code);
if (field) {
payload[code] = field.value !== undefined && field.value !== null ? String(field.value) : '';
} else {
payload[code] = '';
}
});
showSpinner(true);
google.script.run
.withSuccessHandler(function(res) {
showSpinner(false);
if (res && res.success) {
showAlert('Alle 20 G-velden en de overige dagvelden zijn opgeslagen.', 'success');
} else {
showAlert(res && res.message ? res.message : 'Opslaan is niet gelukt.', 'error');
}
})
.withFailureHandler(function(err) {
showSpinner(false);
showAlert('Fout bij opslaan: ' + (err && err.message ? err.message : err), 'error');
})
.g20SaveDaily(payload);
return false;
}
// --- GESCHIEDENIS ---
function openHistory() {
showScreen('screen-history');
showSpinner(true);
google.script.run
.withSuccessHandler(res => {
showSpinner(false);
const container = document.getElementById('historyListContainer');
if (!res.success || !res.history || res.history.length === 0) {
container.innerHTML = '<p class="sub">Je hebt nog geen dagen ingevuld.</p>';
return;
}
let html = '';
res.history.sort((a, b) => new Date(b.Datum) - new Date(a.Datum)).forEach(row => {
let formattedDate = row.Datum;
try { formattedDate = new Date(row.Datum).toISOString().split('T')[0]; } catch(e){}
html += `
<div class="history-card">
<div class="history-info">
<h4>📅 Datum: ${formattedDate}</h4>
<p><strong>Inzicht:</strong> ${row.Inzicht ? row.Inzicht.substring(0, 60) + '...' : 'Geen inzicht ingevuld'}</p>
</div>
<button type="button" class="btn" style="width: auto; padding: 8px 15px; margin-top:0;" onclick="editDay('${formattedDate}')">Bewerken</button>
</div>
`;
});
container.innerHTML = html;
})
.withFailureHandler(err => {
showSpinner(false);
showAlert('Fout bij ophalen geschiedenis: ' + err.message, 'error');
})
.g20GetUserHistory(currentUser.userId);
}
function editDay(datum) {
showScreen('screen-g20-form');
document.getElementById('dailyDate').value = datum;
loadQuestionsAndData(datum);
}
// --- KAARTEN & PROFIEL ---
function loadCards() {
showScreen('screen-cards');
showSpinner(true);
google.script.run
.withSuccessHandler(res => {
showSpinner(false);
const container = document.getElementById('cardsContainer');
if (!res.success || !res.cards) {
container.innerHTML = '<p class="sub">Geen kaarten beschikbaar.</p>';
return;
}
let html = '';
res.cards.forEach(c => {
html += `
<div class="g20-item">
<div class="g20-title">${c.title} <span style="font-size:0.8rem; color:var(--text-light);">(${c.category})</span></div>
<p style="margin-top:8px; font-size:0.9rem;">${c.description}</p>
</div>
`;
});
container.innerHTML = html;
})
.withFailureHandler(err => {
showSpinner(false);
showAlert('Fout: ' + err.message, 'error');
})
.g20GetCards();
}
function openProfile() {
showScreen('screen-profile');
document.getElementById('profileEmail').value = currentUser.email;
document.getElementById('profileName').value = currentUser.name;
document.getElementById('profileQuote').value = currentUser.quote || '';
document.getElementById('profileTheme').value = currentUser.favoriteTheme || '';
}
function saveProfile() {
const profileData = {
name: document.getElementById('profileName').value,
quote: document.getElementById('profileQuote').value,
favoriteTheme: document.getElementById('profileTheme').value
};
showSpinner(true);
google.script.run
.withSuccessHandler(res => {
showSpinner(false);
showAlert(res.message, res.success ? 'success' : 'error');
if (res.success) {
currentUser.name = profileData.name;
document.getElementById('welcomeUser').innerText = 'Welkom, ' + currentUser.name;
}
})
.withFailureHandler(err => {
showSpinner(false);
showAlert('Fout bij opslaan profiel: ' + err.message, 'error');
})
.g20SaveProfile(currentUser.userId, profileData);
}
</script>
</body>
</html>