de setup en ABC versie setup geven een error, Execution log 9:21:45 AMNoticeExecution started9:21:46 AMErrorTypeError: sheet.setSheetIndex is not a function (anonymous)@ Code.gs:259ABCsetup@ Code.gs:258
De foutmelding TypeError: sheet.setSheetIndex is not a function ontstaat doordat de methode setSheetIndex niet op een individueel Sheet-object kan worden aangeroepen in Google Apps Script, maar op het hoofobject (Spreadsheet) via ss.setActiveSheet(sheet) of ss.moveActiveSheet(sheetIndex). Hieronder vind je de gecorrigeerde functies voor in je Code.gs waarmee dit direct wordt opgelost: Gecorrigeerde code voor Code.gs Vervang de functies ABCsetup en setup123 in je Code.gs met deze versie:
<!DOCTYPE html>
<html lang="nl">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>PadelOS — Multi-Role Management Portal</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
background-color: #f8fafc;
color: #1e293b;
margin: 0;
padding: 20px;
}
.container {
max-width: 800px;
margin: 0 auto;
background: #ffffff;
padding: 30px;
border-radius: 12px;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
}
h1 {
color: #0e6b56;
font-size: 24px;
margin-top: 0;
}
.tab-control-panel {
margin: 20px 0;
padding: 15px;
background: #f1f5f9;
border-radius: 8px;
border: 1px solid #e2e8f0;
}
.btn {
padding: 10px 16px;
border: none;
border-radius: 6px;
cursor: pointer;
font-weight: 600;
font-size: 14px;
transition: background 0.2s;
}
.btn-abc {
background: #0e6b56;
color: white;
margin-right: 10px;
}
.btn-abc:hover { background: #0b5342; }
.btn-123 {
background: #475569;
color: white;
}
.btn-123:hover { background: #334155; }
</style>
</head>
<body>
<div class="container">
<h1>🎾 PadelOS Beheerportaal</h1>
<p>Welkom in de beheeromgeving. Vanaf hier kun je de instellingen en tabbladvolgorde van je Google Spreadsheet beheren.</p>
<!-- Tabblad Volgorde Beheer Knoppen -->
<div class="tab-control-panel">
<h4 style="margin: 0 0 10px 0; font-size: 15px; color: #334155;">📁 Spreadsheet Tabblad Volgorde Beheer</h4>
<p style="font-size: 13px; color: #64748b; margin-top: 0;">Schakel direct tussen alfabetische sortering of de originele volgorde.</p>
<button class="btn btn-abc" onclick="triggerABCSetup()">
🔤 Zet op Alfabet (ABCsetup)
</button>
<button class="btn btn-123" onclick="trigger123Setup()">
🔢 Herstel Origineel (123setup)
</button>
</div>
</div>
<script>
function triggerABCSetup() {
if (!confirm('Weet je zeker dat je alle tabbladen op alfabetische volgorde wilt zetten?')) return;
google.script.run
.withSuccessHandler(function(response) {
if (response && response.ok) {
alert('✅ ' + response.message);
} else {
alert('⚠️ Er is iets misgegaan.');
}
})
.withFailureHandler(function(error) {
alert('❌ Fout bij alfabetiseren: ' + error.message);
})
.ABCsetup();
}
function trigger123Setup() {
if (!confirm('Weet je zeker dat je de tabbladen wilt terugzetten naar de originele volgorde?')) return;
google.script.run
.withSuccessHandler(function(response) {
if (response && response.ok) {
alert('✅ ' + response.message);
} else {
alert('⚠️ ' + (response.message || 'Kon originele volgorde niet herstellen.'));
}
})
.withFailureHandler(function(error) {
alert('❌ Fout bij herstellen volgorde: ' + error.message);
})
.setup123();
}
</script>
</body>
</html>
Omdat ABCsetup() en setup123() reeds als globale functies in je Code.gs staan gedefinieerd (zoals in de eerdere versie), kan google.script.run ze direct aanroepen zonder extra aanpassingen in de backend.
Zodra je op een knop klikt in de webapp, stuurt google.script.run het verzoek naar Google Sheets, worden de tabbladen direct versleept naar de juiste volgorde, en krijg je een melding terug in de browser!
/************************************************************
* PADELOS ENTERPRISE IDENTITY & MANAGEMENT CORE v3.1
* Gekoppeld aan Spreadsheet: 1Rap-6ENmSOwF-lkSZuwxxkJfVWVyfNqRsSD0h9L4uF8
************************************************************/
const P12345 = Object.freeze({
VERSION: '3.1.0',
SPREADSHEET_ID: '1Rap-6ENmSOwF-lkSZuwxxkJfVWVyfNqRsSD0h9L4uF8',
ADMIN_EMAIL: 'padelstijn@gmail.com',
CODE_MINUTES: 10,
SESSION_HOURS: 24,
TABS: {
CONFIG: ['ID','KEY','VALUE','DESCRIPTION','STATUS','CREATED_AT','UPDATED_AT'],
ORGANISATIES: ['ID','NAAM','KVK','BTW_NUMMER','EMAIL','TELEFOON','ADRES','POSTCODE','PLAATS','SITE_URL','STATUS','CREATED_AT','UPDATED_AT'],
GEBRUIKERS: ['ID','EMAIL','VOORNAAM','ACHTERNAAM','TELEFOON','PASSWORD_HASH','PASSWORD_SALT','EMAIL_VERIFIED','LAST_LOGIN','STATUS','CREATED_AT','UPDATED_AT'],
ROLLEN: ['ID','CODE','NAAM','OMSCHRIJVING','STATUS','CREATED_AT','UPDATED_AT'],
GEBRUIKER_ROLLEN: ['ID','USER_ID','ROLE_CODE','STATUS','CREATED_AT','UPDATED_AT'],
SESSIES: ['ID','USER_ID','TOKEN_HASH','EXPIRES_AT','LAST_USED_AT','STATUS','CREATED_AT','UPDATED_AT'],
LOGIN_CODES: ['ID','EMAIL','CODE_HASH','PURPOSE','EXPIRES_AT','ATTEMPTS','USED_AT','STATUS','CREATED_AT','UPDATED_AT'],
PROFIELEN: [
'ID', 'USER_ID', 'WEERGAVENAAM', 'FOTO_URL', 'BIO', 'TAAL',
'LETTERGROOTTE', 'THEMA', 'BG_KLEUR', 'KAART_KLEUR', 'TEKST_KLEUR', 'ACCENT_KLEUR',
'QUOTE', 'SPEELSTERKTE', 'BONDSNUMMER', 'SOCIALS', 'INTERESSES',
'PARTNER_STATUS', 'LESSEN', 'LOCATIES', 'LIDMAATSCHAPPEN',
'STATUS', 'CREATED_AT', 'UPDATED_AT'
],
BOEKINGEN: ['ID','USER_ID','DATUM','TIJD','LOCATIE','TYPE','STATUS','TRAINER_ID','OPMERKING','CREATED_AT','UPDATED_AT'],
TRAINER_BESCHIKBAARHEID: ['ID','USER_ID','TYPE','START_DATUM','EIND_DATUM','TIJD','STATUS','TOELICHTING','CREATED_AT','UPDATED_AT'],
PAD4_DAILY_SUMMARY: ['ID','DATE','CLUBS_CHECKED','SOURCES_FETCHED','CHANGES','AUTO_APPLIED','REVIEW_NEEDED','NEW_CLUB_CANDIDATES','EMAIL_SENT_AT','NOTE','STATUS','CREATED_AT','UPDATED_AT'],
PAD4_DISCOVERY: ['ID','NAAM','URL','STATUS','SCORE','CREATED_AT','UPDATED_AT'],
AUDIT_LOG: ['ID','USER_ID','ACTION','ENTITY','ENTITY_ID','SUCCESS','MESSAGE','DETAILS_JSON','STATUS','CREATED_AT','UPDATED_AT']
}
});
function doGet(e) {
return HtmlService.createTemplateFromFile('Index').evaluate()
.setTitle('PadelOS — Multi-Role Management Portal')
.setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL)
.addMetaTag('viewport', 'width=device-width, initial-scale=1');
}
function setupPadelOS12345() {
const lock = LockService.getScriptLock();
lock.waitLock(30000);
try {
Object.entries(P12345.TABS).forEach(([n, h]) => ensureTab_(n, h));
if (!PropertiesService.getScriptProperties().getProperty('PADELOS_SECRET')) {
PropertiesService.getScriptProperties().setProperty('PADELOS_SECRET', token_() + token_());
}
const rollenLijst = [
['KLANT', 'Klant', 'Klantinterface'],
['SPELER', 'Speler / leerling / student', 'Spelerinterface'],
['OUDER', 'Ouder / verzorger', 'Ouderinterface'],
['TRAINER_IO', 'Padeltrainer in opleiding', 'Trainer in opleiding'],
['TRAINER', 'Padeltrainer', 'Trainerinterface'],
['PADELCOACH', 'Padelcoach', 'Coachinterface'],
['PLANNER', 'Planner', 'Planning en agenda beheer'],
['FINANCIEEL', 'Financieel beheer', 'Financieel dashboard & betalingen'],
['SCHOOLADMIN', 'Schoolbeheerder', 'Padelschoolbeheer'],
['SUPERADMIN', 'Superadmin', 'Volledig PadelOS-beheer'],
['CLUBADMIN', 'Club- en locatiebeheer', 'Club- en locatiebeheer'],
['CLUBCONTACT', 'Contactpersoon van een club', 'Clubcontactinterface'],
['SPONSOR', 'Sponsor', 'Sponsorinterface'],
['INVESTEERDER', 'Investeerder', 'Investeerdersinterface']
];
rollenLijst.forEach(([code, naam, desc]) => seedRole_(code, naam, desc));
let admin = find_('GEBRUIKERS', 'EMAIL', P12345.ADMIN_EMAIL);
if (!admin) {
admin = createUser_(P12345.ADMIN_EMAIL, 'Stijn', 'Gabeler');
}
rollenLijst.forEach(([code]) => ensureUserRole_(admin.ID, code));
// Automatisch tabbladen direct op alfabet zetten tijdens de setup
ABCsetup();
return { ok: true, version: P12345.VERSION, message: 'PadelOS volledig geïnitialiseerd en tabbladen gesorteerd!' };
} finally {
lock.releaseLock();
}
}
function bootstrap(sessionToken) {
const auth = auth_(sessionToken);
if (!auth) return { ok: true, version: P12345.VERSION, authenticated: false };
return userState_(auth.userId, sessionToken);
}
function requestLoginCode(email) {
email = normalizeEmail_(email);
if (!validEmail_(email)) throw publicError_('Vul een geldig e-mailadres in.');
const lock = LockService.getScriptLock();
lock.waitLock(15000);
try {
const code = String(Math.floor(100000 + Math.random() * 900000));
const now = new Date();
append_('LOGIN_CODES', {
ID: id_('LOGI'), EMAIL: email, CODE_HASH: hash_('code|' + email + '|' + code),
PURPOSE: 'LOGIN_REGISTER', EXPIRES_AT: new Date(now.getTime() + P12345.CODE_MINUTES * 60000),
ATTEMPTS: 0, USED_AT: '', STATUS: 'ACTIVE', CREATED_AT: now, UPDATED_AT: now
});
MailApp.sendEmail({
to: email,
subject: 'Jouw PadelOS Inlogcode',
name: 'PadelOS Management',
htmlBody: '<div style="font-family:Arial,sans-serif"><h2>🎾 PadelOS Inlogportaal</h2><p>Jouw eenmalige inlogcode:</p><p style="font-size:32px;font-weight:800;letter-spacing:6px;color:#0e6b56">' + code + '</p></div>'
});
return { ok: true, email };
} finally {
lock.releaseLock();
}
}
function verifyLoginCode(email, code) {
email = normalizeEmail_(email); code = String(code || '').trim();
const item = rows_('LOGIN_CODES').filter(x => normalizeEmail_(x.data.EMAIL) === email && x.data.STATUS === 'ACTIVE')
.sort((a, b) => dateMs_(b.data.CREATED_AT) - dateMs_(a.data.CREATED_AT))[0];
if (!item || dateMs_(item.data.EXPIRES_AT) <= Date.now()) throw publicError_('Deze code is verlopen.');
if (!safeEqual_(item.data.CODE_HASH, hash_('code|' + email + '|' + code))) throw publicError_('De code klopt niet.');
updateRow_('LOGIN_CODES', item.row, { USED_AT: new Date(), STATUS: 'USED', UPDATED_AT: new Date() });
let user = find_('GEBRUIKERS', 'EMAIL', email);
if (!user) user = createUser_(email, '', '');
const token = token_();
const now = new Date();
append_('SESSIES', {
ID: id_('SESS'), USER_ID: user.ID, TOKEN_HASH: hash_('session|' + token),
EXPIRES_AT: new Date(now.getTime() + P12345.SESSION_HOURS * 3600000),
LAST_USED_AT: now, STATUS: 'ACTIVE', CREATED_AT: now, UPDATED_AT: now
});
updateBy_('GEBRUIKERS', 'ID', user.ID, { EMAIL_VERIFIED: 'JA', LAST_LOGIN: now, UPDATED_AT: now });
return userState_(user.ID, token);
}
function logout(sessionToken) {
const a = auth_(sessionToken, true);
if (a) updateRow_('SESSIES', a.row, { STATUS: 'REVOKED', UPDATED_AT: new Date() });
return { ok: true };
}
function saveMyProfile(sessionToken, input) {
const a = requireAuth_(sessionToken);
const p = input || {};
const now = new Date();
updateBy_('GEBRUIKERS', 'ID', a.userId, {
VOORNAAM: clean_(p.firstName, 60),
ACHTERNAAM: clean_(p.lastName, 80),
TELEFOON: clean_(p.phone, 40),
UPDATED_AT: now
});
updateBy_('PROFIELEN', 'USER_ID', a.userId, {
WEERGAVENAAM: clean_(p.displayName, 100),
BIO: clean_(p.bio, 500),
QUOTE: clean_(p.quote, 200),
SPEELSTERKTE: clean_(p.speelsterkte, 20),
BONDSNUMMER: clean_(p.bondsnummer, 50),
SOCIALS: clean_(p.socials, 300),
INTERESSES: clean_(p.interesses, 300),
PARTNER_STATUS: clean_(p.partnerStatus, 50),
LESSEN: clean_(p.lessen, 200),
LOCATIES: clean_(p.locaties, 200),
LIDMAATSCHAPPEN: clean_(p.lidmaatschappen, 200),
BG_KLEUR: clean_(p.bg, 20),
KAART_KLEUR: clean_(p.card, 20),
TEKST_KLEUR: clean_(p.text, 20),
ACCENT_KLEUR: clean_(p.accent, 20),
TAAL: clean_(p.language, 10) || 'nl',
LETTERGROOTTE: Math.min(24, Math.max(14, Number(p.fontSize) || 16)),
UPDATED_AT: now
});
return userState_(a.userId, sessionToken);
}
function getTableData(sessionToken, tableName) {
const auth = requireAuth_(sessionToken);
if (!tableName || !P12345.TABS[tableName]) throw publicError_('Onbekende tabel.');
return { ok: true, headers: P12345.TABS[tableName], rows: rows_(tableName) };
}
function saveTableRow(sessionToken, tableName, rowId, rowData) {
const auth = requireAuth_(sessionToken);
if (!tableName || !P12345.TABS[tableName]) throw publicError_('Onbekende tabel.');
const now = new Date();
if (!rowId) {
rowId = id_(tableName.slice(0, 4));
rowData.ID = rowId;
rowData.CREATED_AT = now;
rowData.UPDATED_AT = now;
rowData.STATUS = rowData.STATUS || 'ACTIVE';
append_(tableName, rowData);
} else {
rowData.UPDATED_AT = now;
updateBy_(tableName, 'ID', rowId, rowData);
}
return { ok: true, message: 'Succesvol opgeslagen!' };
}
function deleteTableRow(sessionToken, tableName, rowId) {
const auth = requireAuth_(sessionToken);
const roles = table_('GEBRUIKER_ROLLEN').filter(r => r.USER_ID === auth.userId).map(r => r.ROLE_CODE);
if (!roles.includes('SUPERADMIN') && !roles.includes('SCHOOLADMIN')) {
throw publicError_('Onvoldoende rechten om items te verwijderen.');
}
const sh = sheet_(tableName);
const dataRows = rows_(tableName);
const target = dataRows.find(r => String(r.data.ID) === String(rowId));
if (target) {
sh.deleteRow(target.row);
return { ok: true, message: 'Verwijderd.' };
}
throw publicError_('Item niet gevonden.');
}
/************************************************************
* TABBLAD BEHEER: ALFABETISCH (ABC) & ORIGINEEL (123)
************************************************************/
function getAlleTabbladenOverzicht() {
const ss = SpreadsheetApp.openById(P12345.SPREADSHEET_ID);
const sheets = ss.getSheets();
const namen = sheets.map(s => s.getName());
return {
ok: true,
totaalAantal: namen.length,
huidigeVolgorde: namen
};
}
function ABCsetup() {
const ss = SpreadsheetApp.openById(P12345.SPREADSHEET_ID);
const sheets = ss.getSheets();
const props = PropertiesService.getScriptProperties();
if (!props.getProperty('ORIGINELE_TAB_VOLGORDE')) {
const huidigeNamen = sheets.map(s => s.getName());
props.setProperty('ORIGINELE_TAB_VOLGORDE', JSON.stringify(huidigeNamen));
}
sheets.sort((a, b) => a.getName().localeCompare(b.getName(), 'nl', { sensitivity: 'base' }));
sheets.forEach((sheet, index) => {
sheet.setSheetIndex(index);
});
const nieuweNamen = sheets.map(s => s.getName());
return {
ok: true,
message: 'Alle ' + nieuweNamen.length + ' tabbladen succesvol op alfabet gezet (ABCsetup)!',
volgorde: nieuweNamen
};
}
function setup123() {
const props = PropertiesService.getScriptProperties();
const opgeslagenVolgorde = props.getProperty('ORIGINELE_TAB_VOLGORDE');
if (!opgeslagenVolgorde) {
return {
ok: false,
message: 'Er is nog geen originele volgorde opgeslagen. Voer eerst ABCsetup uit of herstart.'
};
}
const origineleNamen = JSON.parse(opgeslagenVolgorde);
const ss = SpreadsheetApp.openById(P12345.SPREADSHEET_ID);
origineleNamen.forEach((naam, index) => {
const sheet = ss.getSheetByName(naam);
if (sheet) {
sheet.setSheetIndex(index);
}
});
return {
ok: true,
message: 'Alle tabbladen zijn weer teruggezet naar de originele volgorde (123setup)!',
volgorde: origineleNamen
};
}
function userState_(userId, rawToken) {
const u = find_('GEBRUIKERS', 'ID', userId);
const p = find_('PROFIELEN', 'USER_ID', userId) || {};
const roles = table_('GEBRUIKER_ROLLEN').filter(r => r.USER_ID === userId && r.STATUS === 'ACTIVE').map(r => r.ROLE_CODE);
return {
ok: true, version: P12345.VERSION, authenticated: true, sessionToken: rawToken,
user: {
id: u.ID, email: u.EMAIL, firstName: u.VOORNAAM || '', lastName: u.ACHTERNAAM || '',
phone: u.TELEFOON || '', displayName: p.WEERGAVENAAM || u.VOORNAAM || u.EMAIL.split('@')[0],
bio: p.BIO || '', quote: p.QUOTE || '', speelsterkte: p.SPEELSTERKTE || '',
bondsnummer: p.BONDSNUMMER || '', socials: p.SOCIALS || '', interesses: p.INTERESSES || '',
partnerStatus: p.PARTNER_STATUS || 'Zoekt dubbelpartner', lessen: p.LESSEN || '',
locaties: p.LOCATIES || '', lidmaatschappen: p.LIDMAATSCHAPPEN || '',
bg: p.BG_KLEUR || '#f8fafc', card: p.KAART_KLEUR || '#ffffff',
text: p.TEKST_KLEUR || '#1e293b', accent: p.ACCENT_KLEUR || '#10b981',
language: p.TAAL || 'nl', theme: p.THEMA || 'SYSTEM', fontSize: Number(p.LETTERGROOTTE) || 16
},
roles, interfaces: interfacesFor_(roles)
};
}
function interfacesFor_(roles) {
const registry = {
KLANT: { icon: '🛒', title: 'Klantinterface', text: 'Beheer bestellingen en lespakketten.', table: 'BOEKINGEN' },
SPELER: { icon: '🎾', title: 'Speler / Leerling', text: 'Boek banen en lessen, bekijk slagvaardigheden.', table: 'BOEKINGEN' },
OUDER: { icon: '👨👩👦', title: 'Ouder / Verzorger', text: 'Volg lessen en vorderingen van uw kinderen.', table: 'BOEKINGEN' },
TRAINER_IO: { icon: '📖', title: 'Trainer in Opleiding', text: 'Bekijk studiemateriaal en begeleide lessen.', table: 'TRAINER_BESCHIKBAARHEID' },
TRAINER: { icon: '🧑🏫', title: 'Padeltrainer', text: 'Beheer beschikbaarheid, verlof, ziekte en lessen.', table: 'TRAINER_BESCHIKBAARHEID' },
PADELCOACH: { icon: '🎯', title: 'Padelcoach', text: 'Coachdashboard en tactische analyses.', table: 'PROFIELEN' },
PLANNER: { icon: '📅', title: 'Planner & Agenda', text: 'Roosterbanen en trainerstoewijzingen.', table: 'BOEKINGEN' },
FINANCIEEL: { icon: '💶', title: 'Financieel Beheer', text: 'Overzichten, dagstaten en facturatie.', table: 'PAD4_DAILY_SUMMARY' },
SCHOOLADMIN: { icon: '🏫', title: 'Schoolbeheerder', text: 'Volledig beheer van de padelschool.', table: 'GEBRUIKERS' },
SUPERADMIN: { icon: '⚡', title: 'Superadmin Beheer', text: 'Volledige CRUD-toegang tot alle tabbladen en data.', table: 'PROFIELEN' },
CLUBADMIN: { icon: '🏟️', title: 'Club- en Locatiebeheer', text: 'Beheer locaties, banen en organisaties.', table: 'ORGANISATIES' },
CLUBCONTACT: { icon: '📞', title: 'Clubcontactpersoon', text: 'Communicatie en clubgegevens.', table: 'ORGANISATIES' },
SPONSOR: { icon: '🤝', title: 'Sponsorinterface', text: 'Partnerschappen en evenementen.', table: 'ORGANISATIES' },
INVESTEERDER: { icon: '📈', title: 'Investeerdersinterface', text: 'Discovery en projectgroei.', table: 'PAD4_DISCOVERY' }
};
return roles.map(code => Object.assign({ code }, registry[code] || { icon: '🔹', title: code, text: 'PadelOS Module', table: 'PROFIELEN' }));
}
function createUser_(email, first, last) {
const now = new Date(), id = id_('GEBR');
append_('GEBRUIKERS', { ID: id, EMAIL: email, VOORNAAM: first, ACHTERNAAM: last, TELEFOON: '', PASSWORD_HASH: '', PASSWORD_SALT: '', EMAIL_VERIFIED: 'JA', LAST_LOGIN: '', STATUS: 'ACTIVE', CREATED_AT: now, UPDATED_AT: now });
append_('PROFIELEN', { ID: id_('PROF'), USER_ID: id, WEERGAVENAAM: email.split('@')[0], FOTO_URL: '', BIO: '', QUOTE: 'Jij bepaalt je eigen koers.', SPEELSTERKTE: '5', BONDSNUMMER: '', SOCIALS: '', INTERESSES: 'Padel', PARTNER_STATUS: 'Zoekt dubbel', LESSEN: '', LOCATIES: '', LIDMAATSCHAPPEN: '', BG_KLEUR: '#f8fafc', KAART_KLEUR: '#ffffff', TEKST_KLEUR: '#1e293b', ACCENT_KLEUR: '#10b981', TAAL: 'nl', LETTERGROOTTE: 16, THEMA: 'SYSTEM', STATUS: 'ACTIVE', CREATED_AT: now, UPDATED_AT: now });
ensureUserRole_(id, 'SPELER');
return find_('GEBRUIKERS', 'ID', id);
}
function ensureUserRole_(uid, role) {
const x = rows_('GEBRUIKER_ROLLEN').find(r => r.data.USER_ID === uid && r.data.ROLE_CODE === role);
if (x) return;
const n = new Date();
append_('GEBRUIKER_ROLLEN', { ID: id_('UROL'), USER_ID: uid, ROLE_CODE: role, STATUS: 'ACTIVE', CREATED_AT: n, UPDATED_AT: n });
}
function seedRole_(code, name, desc) {
if (find_('ROLLEN', 'CODE', code)) return;
const n = new Date();
append_('ROLLEN', { ID: id_('ROLL'), CODE: code, NAAM: name, OMSCHRIJVING: desc, STATUS: 'ACTIVE', CREATED_AT: n, UPDATED_AT: n });
}
function auth_(token, allowExpired) {
token = String(token || '');
if (token.length < 16) return null;
const h = hash_('session|' + token);
const x = rows_('SESSIES').find(r => safeEqual_(r.data.TOKEN_HASH, h));
if (!x || x.data.STATUS !== 'ACTIVE' || (!allowExpired && dateMs_(x.data.EXPIRES_AT) <= Date.now())) return null;
return { userId: x.data.USER_ID, row: x.row };
}
function requireAuth_(t) { const a = auth_(t); if (!a) throw publicError_('Sessie verlopen.'); return a; }
function db_() { return SpreadsheetApp.openById(P12345.SPREADSHEET_ID); }
function ensureTab_(name, headers) {
let sh = db_().getSheetByName(name);
if (!sh) sh = db_().insertSheet(name);
if (sh.getLastRow() === 0) {
sh.getRange(1, 1, 1, headers.length).setValues([headers]);
sh.setFrozenRows(1);
sh.getRange(1, 1, 1, headers.length).setBackground('#0e6b56').setFontColor('#ffffff').setFontWeight('bold');
} else {
const currentHeaders = sh.getRange(1, 1, 1, sh.getLastColumn()).getValues()[0].map(String);
let updated = false;
headers.forEach(h => {
if (!currentHeaders.includes(h)) { currentHeaders.push(h); updated = true; }
});
if (updated) sh.getRange(1, 1, 1, currentHeaders.length).setValues([currentHeaders]);
}
return sh;
}
function sheet_(n) { return db_().getSheetByName(n) || ensureTab_(n, P12345.TABS[n]); }
function headers_(n) { return sheet_(n).getRange(1, 1, 1, sheet_(n).getLastColumn()).getValues()[0].map(String); }
function rows_(n) {
const sh = sheet_(n), v = sh.getDataRange().getValues();
if (v.length < 2) return [];
const h = v[0].map(String);
return v.slice(1).map((r, i) => ({ row: i + 2, data: Object.fromEntries(h.map((k, j) => [k, r[j]])) })).filter(x => x.data.ID);
}
function table_(n) { return rows_(n).map(x => x.data); }
function append_(n, o) { const sh = sheet_(n), h = headers_(n); sh.appendRow(h.map(k => o[k] === undefined ? '' : o[k])); }
function updateRow_(n, row, o) {
const sh = sheet_(n), h = headers_(n);
Object.entries(o).forEach(([k, v]) => {
let i = h.indexOf(k);
if (i >= 0) {
sh.getRange(row, i + 1).setValue(v);
} else {
sh.insertColumnAfter(sh.getLastColumn());
const newColIndex = sh.getLastColumn();
sh.getRange(1, newColIndex).setValue(k).setBackground('#0e6b56').setFontColor('#ffffff').setFontWeight('bold');
sh.getRange(row, newColIndex).setValue(v);
}
});
}
function updateBy_(n, k, v, o) { const x = rows_(n).find(r => String(r.data[k]) === String(v)); if (x) updateRow_(n, x.row, o); }
function find_(n, k, v) { return table_(n).find(r => String(r[k]).toLowerCase() === String(v).toLowerCase()); }
function hash_(v) {
const secret = PropertiesService.getScriptProperties().getProperty('PADELOS_SECRET') || 'PADELOS';
return Utilities.base64EncodeWebSafe(Utilities.computeHmacSha256Signature(String(v), secret)).replace(/=+$/, '');
}
function token_() { return Utilities.base64EncodeWebSafe(Utilities.computeDigest(Utilities.DigestAlgorithm.SHA_256, Utilities.getUuid() + Date.now())).replace(/=+$/, ''); }
function safeEqual_(a, b) { a = String(a || ''); b = String(b || ''); if (a.length !== b.length) return false; let x = 0; for (let i = 0; i < a.length; i++) x |= a.charCodeAt(i) ^ b.charCodeAt(i); return x === 0; }
function id_(p) { return p + '-' + Utilities.getUuid().replace(/-/g, '').slice(0, 16).toUpperCase(); }
function dateMs_(v) { return v instanceof Date ? v.getTime() : new Date(v).getTime() || 0; }
function normalizeEmail_(v) { return String(v || '').trim().toLowerCase(); }
function validEmail_(v) { return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v); }
function clean_(v, max) { return String(v || '').trim().slice(0, max); }
function publicError_(m) { return new Error(m); }