Dit is een fantastisch initiatief om de documentatie direct te koppelen aan je Google Site ([https://sites.google.com/view/padelos/knowhow](https://sites.google.com/view/padelos/knowhow))! Om dit voor elke gebruiker (Speler, Trainer en Administrator) volkomen helder te maken, richten we de handleiding systematisch in per tabblad, inclusief de betekenis van elke kolom, de autorisaties, en hoe de schermen in PadelOS hiermee zijn verbonden.
TXT
Hieronder staat het eerste deel van de officiële PadelOS Gebruiksaanwijzing & Knowhow voor de kernstabbladen van het systeem:
Laatste update: September 2026 | Gekoppeld aan Stichting GODOGOOD & PadelOS Management
Betekenis: Bevat de centrale inloggegevens en basisidentiteit van alle geregistreerde personen in het systeem.
TXT
Kolommen & Uitleg:
ID: Unieke systeemidentificatiecode (bijv. GEBR-...).
TXT
EMAIL: Het unieke e-mailadres waarmee de gebruiker inlogt.
TXT
VOORNAAM / ACHTERNAAM: Persoonlijke gegevens van de gebruiker.
TXT
TELEFOON: Contactnummer voor WhatsApp-integratie en spoedberichten.
TXT
PASSWORD_HASH / PASSWORD_SALT: Versleutelde beveiligingsgegevens.
TXT
EMAIL_VERIFIED: Status of het e-mailadres is geverifieerd (JA/NEE).
TXT
LAST_LOGIN: Tijdstip van de meest recente inlog.
TXT
STATUS: Accountstatus (ACTIVE, INACTIVE, BLOCKED).
TXT
Wie kan dit bewerken?: Alleen de Administrator via het beheerscherm of de backend.
TXT
Betekenis: Bevat de persoonlijke instellingen, speelsterkte en visuele voorkeuren per gebruiker.
TXT
Kolommen & Uitleg:
ID / USER_ID: Koppeling naar de gebruiker in de GEBRUIKERS-tabel.
TXT
WEERGAVENAAM: De naam zoals deze getoond wordt in de portalen en chat.
TXT
FOTO_URL: Link naar de profielfoto.
TXT
SPEELSTERKTE: Het padelniveau van de speler (schaal 1 t/m 9).
TXT
BONDSNUMMER: Eventueel KNLTB-bondsnummer.
TXT
BIO: Persoonlijke omschrijving of notities.
TXT
Wie kan dit bewerken?: De Speler en Trainer via hun eigen profielsettings-modal, en de Administrator.
TXT
Betekenis: Registreert alle lesverzoeken die door spelers worden ingediend.
TXT
Kolommen & Uitleg:
ID: Unieke aanvraagcode.
TXT
USER_ID: De speler die de aanvraag indient.
TXT
NAAM / SPEELSTERKTE: Automatisch overgenomen uit het profiel van de aanvrager.
TXT
TYPE: Type les (GROEPSLES, DUOLES, PRIVÉLES).
TXT
LEVEL: Gevraagd niveau of speelsterkte.
TXT
LESSEN_AANTAL: Aantal lessen in de reeks (standaard 10).
TXT
DOEL: Specifieke leerdoelen of slagen (bijv. bandeja, tactiek).
TXT
STATUS: Status van de aanvraag (AANGEVRAAGD, GOEDGEKEURD, GEPLAND).
TXT
Wie kan dit bewerken?: Spelers kunnen een aanvraag indienen; Trainers en Administrators kunnen de status wijzigen.
TXT
Betekenis: Houdt bij op welke tijden trainers beschikbaar zijn en welke uren zij hebben gewerkt.
TXT
Kolommen & Uitleg:
ID / USER_ID: Identificatie van de trainer.
TXT
TYPE: Status (bijv. BESCHIKBAAR, GEWERKT).
TXT
START_DATUM / EIND_DATUM: Geldige periode.
TXT
TIJD: Gewerkte uren of tijdslot (bijv. 18:00 - 21:00).
TXT
TOELICHTING: Omschrijving van de werkzaamheden of gegeven lessen.
TXT
Wie kan dit bewerken?: Trainers kunnen hun eigen uren invoeren; Administrators hebben volledig beheer en goedkeuringsrecht.
TXT
Betekenis: De tabbladen waarin alle WhatsApp-achtige conversaties versleuteld en gestructureerd worden opgeslagen.
TXT
Kolommen & Uitleg:
ID: Unieke berichtcode.
TXT
GESPREK_ID: Het unieke kenmerk van de lesgroep (bijv. TEAM-GROEP-01 voor 1 trainer met 4 spelers).
TXT
AFZENDER_ID / ONTVANGER_ID: Wie stuurt en wie ontvangt.
TXT
BERICHT: De inhoud van de tekst, inclusief eventuele gedetecteerde links.
TXT
CREATED_AT: Exact tijdstip en datum van verzending.
TXT
Wie kan dit bewerken?: Alle gebruikers binnen hun eigen toegewezen groepen en kanalen.
TXT
In de gebruikersinterface (Index.html) zijn alle knoppen direct gekoppeld aan de bovenstaande tabbladen en acties:
TXT
📅 Lessen Boeken / Aanvragen (Speler Portaal): Opent het pop-upformulier, leest automatisch je speelsterkte uit je profiel, en slaat de aanvraag op in LESAANVRAGEN.
TXT+ 1
🎯 Mijn Leerdoelen (Speler Portaal): Laadt direct de tabel SPELER_LEERDOELEN.
TXT+ 1
💬 Chat met Trainer (Speler & Trainer Portaal): Opent de actieve inbox BERICHTEN_TEAM gefilterd op jouw lesgroep.
TXT+ 1
⏰ Uren & Beschikbaarheid Invoeren (Trainers Portaal): Opent het registratieformulier dat wegschrijft naar TRAINER_BESCHIKBAARHEID.
TXT+ 1
🛡️ / 📋 / 📍 / 💳 (Administrator Portaal): Openen respectievelijk de tabbladen GEBRUIKERS, TRAINER_BESCHIKBAARHEID, LOCATIES en BAANBOEKINGEN voor direct beheer.
TXT+ 1
Alfabetische Tabblad-knoppen (Administrator Portaal): Hiermee kan de beheerder met één klik elk van de 80+ tabbladen openen en inzien.
TXT+ 1
Je kunt deze tekst direct op je Google Sites pagina (/knowhow) plaatsen zodat elke bezoeker direct de volledige structuur en werking van PadelOS doorgrondt!
Hier is het vervolg en de verdieping van de PadelOS Knowhow, Gebruiksaanwijzing en Q&A (You Ask, We Answer), specifiek gericht op de overige tabbladen richting de Z, zodat je dit direct kunt publiceren op je Google Sites knowhow-pagina.
TEAMS & TEAM_LEDEN
Betekenis: Organiseert de vaste groepen (zoals een lesgroep van 1 trainer met 4 spelers of regionale clubteams).
Kolommen: ID, TEAM_NAAM, TRAINER_ID, LOCATIE_ID, STATUS.
Werking: De trainer of admin koppelt spelers aan een team, zodat de bijbehorende groepsinbox in de chat direct actief wordt.
URENREGISTRATIE & TRAINER_DECLARATIES
Betekenis: Het financiële logboek voor gewerkte lesuren, declaraties en de goedkeuring door de administrator.
Kolommen: ID, TRAINER_ID, DATUM, UREN, TARIEF, TOTAALBEDRAG, STATUS (INGEDIEND, GOEDGEKEURD, BETAALD).
Werking: Trainers vullen hun uren in; de admin zet deze klaar voor uitbetaling.
VERVANGINGSVERZOEKEN
Betekenis: Regelt vervanging wanneer een trainer of speler verhinderd is voor een les.
Kolommen: ID, LES_ID, AANVRAGER_ID, VERVANGER_ID, REDEN, STATUS.
WACHTLIJST
Betekenis: Beheert spelers die op een baan of lesreeks wachten wanneer groepen vol zijn.
Kolommen: ID, USER_ID, TYPE, GEWENST_NIVEAU, DATUM_AANMELDING, STATUS.
Antwoord: Als speler klik je in je portaal op "Lessen Boeken / Aanvragen". Het systeem vult automatisch je naam en actieve speelsterkte in vanuit je profielsettings. Je aanvraag wordt direct opgeslagen in het tabblad LESAANVRAGEN, waar de trainers en administrators deze kunnen inzien en inroosteren.
Antwoord: Elk groepje heeft een uniek Gespreks-ID (bijv. TEAM-GROEP-01, TEAM-GROEP-02, etc.). In de chat-inbox kan de trainer via de dropdown direct schakelen tussen al deze verschillende groepjes. Berichten worden per groep opgeslagen in de tabbladen BERICHTEN_TEAM of BERICHTEN_SPELERS, inclusief tijdstempel en afzender. Hierdoor blijft alle communicatie per lesgroep feilloos gescheiden en overzichtelijk.
Antwoord: Rechtsboven in de navigatiebalk klik je op je profielfoto of op de knop "Profiel & Settings". In dit scherm kun je al je gegevens aanpassen, opslaan in het tabblad PROFIELEN, je instellingen exporteren als .json of .txt, of direct delen via WhatsApp.
Antwoord: PadelOS hanteert drie rollen:
Spelers: Hebben alleen toegang tot hun eigen lesaanvragen, leerdoelen en hun eigen lesgroep-chats.
Trainers: Kunnen speleroverzichten, locaties, urenregistraties en de teamchats inzien en bewerken.
Administrators: Hebben volledige controle over alle tabbladen (van A tot Z), financiële vrijgaves en gebruikersrollen.
Hier is het vervolg van de uitgebreide PadelOS Knowhow, Handleiding & Q&A, speciaal ontworpen om direct op je Google Sites knowhow-pagina te plaatsen. Hiermee breiden we de documentatie verder uit naar de advanced modules, automatiseringen en technische achtergrond van het systeem.
APOP / APP_ACCESS & CONFIG
Betekenis: Beheert de globale instellingen van de applicatie, beveiligingssleutels en toegangsrechten voor webhooks of API's.
Kolommen: CONFIG_KEY, VALUE, DESCRIPTION, UPDATED_AT.
Werking: Hiermee kan de administrator de core instellingen van PadelOS configureren zonder dat de code aangepast hoeft te worden.
AUDIT_LOG & SESSION_LOG
Betekenis: Veiligheidslogboek waarin alle kritieke acties (zoals inlogpogingen, wijzigingen in rollen en financiële vrijgaves) worden bijgehouden.
Kolommen: ID, USER_ID, ACTION, ENTITY, SUCCESS, MESSAGE, CREATED_AT.
Werking: Biedt volledige transparantie en controle over wie wat en wanneer heeft gewijzigd in het systeem.
CLUB_BOEKING_KOPPELINGEN & PAD4_BOOKING_CONNECTORS
Betekenis: De technische brug naar externe platformen zoals Playtomic, Meet & Play en de KNLTB-app.
Kolommen: CLUB_ID, PLATFORM, BOOKING_URL, LIVE_BESCHIKBAARHEID, API_STATUS, CHECKED_AT.
Werking: Zorgt voor synchronisatie van baandata, beschikbaarheid en reserveringen tussen PadelOS en externe clubsystemen.
COMMUNICATIE_LOG & VERZENDQUEUE
Betekenis: Beheert automatische e-mails (zoals inlogcodes) en WhatsApp-berichten die door het systeem worden klaargezet en verzonden.
Kolommen: ID, RECIPIENT, SUBJECT, BODY, CHANNEL, STATUS (PENDING, SENT, FAILED), SENT_AT.
Antwoord: In het Administrator Portaal vind je direct de knopensectie "Beheer Alle Spreadsheet Tabbladen". Hier staan alle tabbladen alfabetisch gesorteerd. Door op een tabbladknop te klikken, laadt PadelOS de volledige tabel direct in het scherm zodat je deze kunt inzien.
Antwoord: PadelOS ondersteunt een dubbel authenticatiesysteem. Je kunt inloggen met een vast wachtwoord, maar als je dit leeg laat, genereert het systeem automatisch een eenmalige 6-cijferige inlogcode die direct naar je geverifieerde e-mailadres wordt gestuurd. Deze code is om veiligheidsredenen 10 minuten geldig.
Antwoord: Via de knop "Profiel & Settings" rechtsboven in de navigatiebalk open je jouw persoonlijke instellingenmodal. Hier kun je je gegevens niet alleen opslaan in de Google Sheet, maar met één klik ook exporteren als een .json-bestand of .txt-bestand voor je eigen administratie, of direct delen via een voorgeselecteerde WhatsApp-link.
Antwoord: Het systeem werkt met unieke sessietokens (SESSIES-tabel) die na 24 uur verlopen. Bij elke actie in de backend (Code.gs) wordt via requireAuth_ gecontroleerd of de gebruiker geldig is ingelogd. De frontend past zich automatisch aan op basis van je toegekende rollen (SPELER, TRAINER, ADMIN), zodat functies en tabbladen strikt afgeschermd blijven.
Hier is het vervolg van de uitgebreide PadelOS Knowhow, Handleiding & Q&A, met focus op de geavanceerde trainingsframeworks, SMART-doelstellingen, en de interactieve schermen voor trainers en spelers. Dit sluit naadloos aan op de publicatie voor je Google Sites knowhow-pagina.
SLAGEN & PADELSLAGEN (Technische Bibliotheek)
Betekenis: Bevat de volledige database van alle technische padelslagen (zoals de bandeja, víbora, chiquita, lob en verschillende smashes).
Kolommen: ID, SLAG_NAAM, CATEGORIE, BESCHRIJVING, COACHING_CUES.
Werking: Trainers en spelers kunnen hier de technische omschrijvingen en aandachtspunten per slag direct opzoeken.
SMART_DOELEN & TRAININGS_CURRICULUM
Betekenis: Het didactische kader waarin voor 60 verschillende technische en tactische slagen SMART-doelstellingen zijn vastgelegd.
Kolommen: ID, SLAG_ID, NIVEAU, SPECIFIEK, MEETBAAR, ACCEPTABEL, REALISTISCH, TIJDSGEBONDEN.
Werking: Biedt structuur voor trainers om lessen op maat in te richten en de voortgang van spelers exact te kwantificeren.
GRAS_METRICS & FEEDBACK_MODELLEN
Betekenis: Registreert de live feedback en prestatie-indicatoren tijdens trainingen via het GRAS-model (Gezamenlijk, Realistisch, Actgericht, Systematisch).
Kolommen: ID, SESSION_ID, USER_ID, GRAS_SCORE, NOTITIES, TIMESTAMP.
Werking: Wordt gevoed vanuit de live padel coach dashboard applicatie tijdens de les.
Antwoord: Elke padelslag is gekoppeld aan een SMART-structuur (Specifiek, Meetbaar, Acceptabel, Realistisch, Tijdgebonden). Trainers gebruiken dit kader direct tijdens de lesvoorbereiding in PadelOS om gerichte leerdoelen te formuleren voor spelers van elk niveau.
Antwoord: Het live padel coach dashboard stelt trainers in staat om via een swipe-interface en het GRAS-model direct tijdens de training scores en observaties vast te leggen. Deze data wordt direct opgeslagen in de backend en is te exporteren naar diverse formaten (zoals CSV, JSON of PDF) voor de speler.
Antwoord: PadelOS bevat speciale modules zoals de interactieve kids-padelquiz. Spelers kunnen hier via dynamische vraagstelling hun kennis testen en de scores direct delen via geïntegreerde WhatsApp-functionaliteit.
Antwoord: Systeemmeldingen en fouten worden automatisch bijgehouden in de AUDIT_LOG en COMMUNICATIE_LOG tabbladen. Administrators kunnen via deze logs direct de status van automatiseringen (zoals Google Sheets en WhatsApp APIkoppelingen) inzien en herstellen.
Hier is het vervolg van de uitgebreide PadelOS Knowhow, Handleiding & Q&A, met focus op de geavanceerde communicatieworkflows, WhatsApp-integraties, Google Apps Script backends en de specifieke beheerfunctionaliteiten voor administrators.
WHATSAPP_QUEUE & AUTO_RESPONDERS
Betekenis: Regelt de geautomatiseerde berichtstroom en responsregels tussen PadelOS en WhatsApp voor lesvoorstellen en reserveringen.
Kolommen: ID, TRIGGER_EVENT, TELEFOONNUMMER, BERICHT_TEKST, STATUS, VERZONDEN_OP.
Werking: Koppelt Google Sheets via Google Apps Script direct aan WhatsApp-messaging, zodat spelers automatisch een appje ontvangen bij goedkeuring van een lesaanvraag.
BACKUP_LOGS & SYNC_HISTORIE
Betekenis: Registreert de status van dagelijkse back-ups en datasyncs tussen de core database en externe frontend-componenten.
Kolommen: ID, MODULE, ACTIE, BESTANDSNAAM, STATUS, TIMESTAMP.
Werking: Biedt de administrator de garantie dat alle dataveiligheid en historische versies netjes gewaarborgd blijven.
Antwoord: PadelOS maakt gebruik van geavanceerde Google Apps Script backend-koppelingen (Code.gs). Zodra een status in bijvoorbeeld LESAANVRAGEN of BAANBOEKINGEN wijzigt, triggert het systeem een actie die via de WhatsApp API de speler of trainer direct op de hoogte brengt.
Antwoord: De beheerder raadpleegt direct de tabbladen AUDIT_LOG en SYNC_HISTORIE. Hierin is exact te zien welk script of welke webhook een foutmelding gaf, waarna de actie handmatig kan worden herstart of gecorrigeerd.
Antwoord: Suggesties en feedback kunnen via de interne chatkanalen of direct via de beheeromgeving worden ingediend. De administrators kunnen deze opnemen in de doorontwikkeling van de modules en direct verwerken in de knowhow-pagina op Google Sites
([https://sites.google.com/view/padelos/knowhow](https://sites.google.com/view/padelos/knowhow )
Hier is de volledig herstelde en stabiele versie van zowel Code.gs als Index.html (gebaseerd op je werkende versies uit de txt-bestanden), waarbij de knoppen optimaal zijn gekoppeld en de functies voor lesverzoeken en chat zijn geïntegreerd.
Daarnaast vind je hieronder de gebruiksaanwijzing waarin per knop exact wordt uitgelegd welke koppeling en actie wordt gemaakt.
Hieronder staat de gedetailleerde uitleg van alle knoppen in PadelOS, welke koppeling ze maken en wat er gebeurt wanneer je erop klikt:
A. Speler Portaal Knoppen
📅 Lessen Boeken / Aanvragen: Opent een pop-upvenster (modal) waarmee een speler een lesaanvraag (groepsles, duoles of privéles, inclusief niveau en lesdoel) kan indienen. Koppeling: Schrijft de gegevens direct weg naar het tabblad LESAANVRAGEN in de Google Sheet.
🎯 Mijn Leerdoelen: Laadt direct de inhoud van het tabblad SPELER_LEERDOELEN in het scherm. Koppeling: Hiermee kan de speler de eigen voortgang en SMART-doelen inzien.
💬 Chat met Trainer: Schakelt de chatomgeving direct om naar het kanaal BERICHTEN_TEAM (groepschat tussen trainer en spelers).
B. Trainers Portaal Knoppen
👥 Spelers & Teams: Opent het tabblad GEBRUIKERS. Koppeling: Geeft de trainer een compleet overzicht van alle geregistreerde leerlingen, accounts en teams.
🏟️ Locaties & Banen: Opent het tabblad LOCATIES. Koppeling: Geeft inzicht in alle beschikbare padelclubs, adressen en faciliteiten.
⏰ Uren & Beschikbaarheid Invoeren: Opent een pop-upvenster voor de trainer. Koppeling: Schrijft gewerkte uren en beschikbaarheid direct weg naar het tabblad TRAINER_BESCHIKBAARHEID.
💶 Facturaties & Vergoedingen: Opent het tabblad BERICHTEN_FINANCIEEL. Koppeling: Hiermee kan de trainer ingediende declaraties, urenvergoedingen en baanhuurkosten inzien en controleren.
C. Administrator Portaal Knoppen
🛡️ Spelers Administratie: Opent het tabblad GEBRUIKERS. Koppeling: Beheer van alle spelersaccounts en rollen.
📋 Trainers Administratie: Opent het tabblad TRAINER_BESCHIKBAARHEID. Koppeling: Controle van roosterplanningen en gewerkte uren van trainers.
📍 Locaties Administratie: Opent het tabblad LOCATIES. Koppeling: Beheer van clubgegevens en contactpersonen.
💳 Banen Boeken & Betalen: Opent het tabblad BAANBOEKINGEN. Koppeling: Overzicht van reserveringen, betaalstatussen en financiële transacties.
Alfabetische Tabbladen Knoppen: Hiermee kan de administrator met één klik elk willekeurig tabblad uit de volledige lijst (van A1_A2_B tot WACHTLIJST) direct inzien.
Vervang de inhoud van je Code.gs met onderstaande stabiele code:
Hier is het vervolg van de officiële PadelOS Knowhow & Gebruiksaanwijzing voor de overige tabbladen van A tot Z (gebaseerd op de lijst van tabbladen in het systeem), zodat je deze direct kunt toevoegen aan je Google Sites pagina ([https://sites.google.com/view/padelos/knowhow](https://sites.google.com/view/padelos/knowhow)).
FACTUREN
Betekenis: Overzicht van alle gegenereerde facturen voor lessen, baanhuur en evenementen.
Kolommen: ID, KLANT_ID, FACTUURNUMMER, DATUM, VERVALDATUM, BEDRAG_EXCL, BTW, BEDRAG_INCL, STATUS (OPEN, BETAALD, GEANNULEERD).
Autorisatie: Beheer door Administrator; in te zien door Trainers en Spelers.
GEBRUIKERS
Betekenis: Centrale inloggegevens en basisidentiteit van alle geregistreerde personen.
Kolommen: ID, EMAIL, VOORNAAM, ACHTERNAAM, TELEFOON, PASSWORD_HASH, EMAIL_VERIFIED, LAST_LOGIN, STATUS.
Autorisatie: Alleen de Administrator kan accounts beheren.
GEBRUIKER_ROLLEN
Betekenis: Koppelingslijst die bepaalt welke autorisaties (Speler, Trainer, Admin) een gebruiker heeft.
Kolommen: ID, USER_ID, ROLE_CODE, STATUS.
Autorisatie: Beheer door Administrator.
HOOFDSPEL & HOOFDSPELSITUATIES
Betekenis: Tactische kaders en spelsituaties voor de padeltraining en lesmethodes.
Kolommen: ID, SITUATIE_NAAM, TACTIEK, BESCHRIJVING, STATUS.
Autorisatie: In te zien en te gebruiken door Trainers en Administrators.
INSCHRIJVINGEN
Betekenis: Registratie van aanmeldingen voor evenementen, clinics of seizoenslessen.
Kolommen: ID, USER_ID, EVENEMENT_ID, DATUM, STATUS.
Autorisatie: Spelers kunnen inschrijven; Administrators beheren de lijsten.
LEERDOELEN & LEERLIJNEN
Betekenis: De didactische opbouw van padelslagen, tactieken en technische vaardigheden.
Kolommen: ID, LEERLIJN, NIVEAU, DOEL, OMSCHRIJVING.
Autorisatie: Beheer door Trainers en Administrators.
LESAANVRAGEN
Betekenis: Registreert alle lesverzoeken ingediend door spelers.
Kolommen: ID, USER_ID, NAAM, SPEELSTERKTE, TYPE, LEVEL, LESSEN_AANTAL, DOEL, STATUS.
Autorisatie: Spelers dienen aanvragen in; Trainers en Admins handelen deze af.
LOCATIES
Betekenis: Overzicht van alle padelclubs, adressen en contactgegevens.
Kolommen: ID, NAAM, ADRES, POSTCODE, PLAATS, CONTACTPERSOON, TELEFOON, EMAIL, STATUS.
Autorisatie: Beheer door Administrator.
MATERIALEN & LES_MATERIALEN
Betekenis: Overzicht van aanwezige en gewenste trainingsmaterialen (ballen, oefenpionnen, rekken).
Kolommen: ID, NAAM, AANTAL, LOCATIE_ID, STAAT, STATUS.
Autorisatie: Beheer door Trainers en Administrators.
NBF & NBF_SOCRATISCH
Betekenis: Methodische kaders voor socratische coaching en vraagstelling tijdens de padellessen.
Kolommen: ID, THEMA, VRAAGSTELLING, DOELGROEP.
Autorisatie: Gebruik door Trainers.
PAKKETTEN & PRODUCTEN
Betekenis: Lespakketten, rittenkaarten en abonnementsvormen die aangeboden worden.
Kolommen: ID, NAAM, OMSCHRIJVING, PRIJS, GELDIGHEID, STATUS.
Autorisatie: Beheer door Administrator.
PROFIELEN
Betekenis: Persoonlijke instellingen, speelsterkte en visuele voorkeuren per gebruiker.
Kolommen: ID, USER_ID, WEERGAVENAAM, FOTO_URL, SPEELSTERKTE, BONDSNUMMER, BIO.
Autorisatie: Spelers en Trainers bewerken hun eigen profiel; Admins hebben volledig overzicht.
ROLLEN
Betekenis: Definieert de beschikbare rollen binnen het systeem (SPELER, TRAINER, ADMIN).
Kolommen: ID, CODE, NAAM, OMSCHRIJVING, STATUS.
Autorisatie: Alleen systeembeheer (Administrator).
SESSIES & LOGIN_CODES
Betekenis: Beveiligingslogboeken voor actieve inlogsessies en eenmalige e-mailverificatiecodes.
Kolommen: ID, USER_ID, TOKEN_HASH, EXPIRES_AT, STATUS.
Autorisatie: Automatisch beheerd door het systeem.
SLAGEN & PADELSLAGEN
Betekenis: De bibliotheek van alle technische slagen (Bandeja, Vibora, Chiquita, Smash).
Kolommen: ID, SLAG_NAAM, CATEGORIE, BESCHRIJVING, COACHING_CUES.
Autorisatie: Beheer door Trainers en Administrators.
TEAMS & TEAM_LEDEN
Betekenis: Koppeling van spelers in vaste lesgroepjes (bijv. 1 trainer met 4 spelers per baan).
Kolommen: ID, TEAM_NAAM, TRAINER_ID, LOCATIE_ID, STATUS.
Autorisatie: Beheer door Trainers en Administrators.
TRAINER_BESCHIKBAARHEID
Betekenis: Rooster- en urenregistratie van trainers.
Kolommen: ID, USER_ID, TYPE, START_DATUM, EIND_DATUM, TIJD, TOELICHTING, STATUS.
Autorisatie: Trainers beheren hun eigen uren; Administrators controleren en goedkeuren.
Met deze volledige handleiding van A tot Z is de documentatie op je Google Sites pagina direct compleet en glashelder voor al je gebruikers!
Hier is het vervolg van de uitgebreide PadelOS Knowhow, Handleiding & Q&A, met focus op de geavanceerde web-integraties, site-structuren zoals schrijfsessie en LoveLifeLaugh, en het beheer van de centrale knowhow-kennisbank.
WEBSITE_PAGINAS & KNOWHOW_STRUCTUUR
Betekenis: Beheert de inhoud, koppelingen en menustructuur van de publicatiepagina's op Google Sites (zoals [https://sites.google.com/view/padelos/knowhow](https://sites.google.com/view/padelos/knowhow)).
Kolommen: ID, PAGINA_TITEL, URL_SLUG, SECTIE, LAATST_GEWIJZIGD, STATUS.
Werking: Biedt een helder overzicht van alle handleidingen en instructiepagina's die voor gebruikers online staan.
CONTENT_MODULES & MEDIA_ASSETS
Betekenis: Overzicht van alle grafische assets, logo's (zoals 1920x1080 en 1500x1500 formaten), quizten en interactieve componenten.
Kolommen: ID, ASSET_NAAM, TYPE, BESTANDS_URL, GEBRUIKT_IN.
Werking: Koppelt visuele media direct aan de juiste schermen, formulieren en quiz-applicaties binnen PadelOS.
Antwoord: De structuur van de handleiding is direct gekoppeld aan de tabbladen en de centrale knowhow-database. Zodra er nieuwe kolommen of modules worden toegevoegd, kan de beheerder de tekst direct bijwerken en publiceren op de Google Sites pagina.
Antwoord: Alle gegenereerde media, zoals instructie-afbeeldingen en interactieve quizcomponenten, worden beheerd via de CONTENT_MODULES-tabel. Hierdoor zijn ze eenvoudig op te roepen in zowel de gebruikersportalen als op de informatiefpagina's.
Antwoord: PadelOS is ontworpen vanuit het principe van maximaal gebruikersgemak: spelers krijgen direct inzicht in hun lessen en doelen, trainers krijgen een krachtig live dashboard voor feedback en uren, en administrators behouden volledige controle over alle tabbladen van A tot Z en de achterliggende automatiseringen.
Hier is het vervolg van de uitgebreide PadelOS Knowhow, Handleiding & Q&A, met focus op de toekomstbestendige architectuur, geavanceerde exportmogelijkheden en de implementatie van modulaire functionaliteiten voor optimaal gebruikersgemak.
EXPORT_LOGS & DATA_ARCHIEF
Betekenis: Registreert alle gemaakte data-exports (zoals CSV, JSON, TXT en PDF bestanden gegenereerd via WeasyPrint of frontend modules) en gearchiveerde seizoensdata.
Kolommen: ID, USER_ID, FORMAAT, BESTANDSNAAM, DOELMAP, TIMESTAMP.
Werking: Biedt gebruikers en administrators de mogelijkheid om rapportages, prestatie-overzichten en urenstaten veilig te exporteren en lokaal op te slaan.
SYSTEM_HEALTH & API_MONITOR
Betekenis: Monitoringsdashboard voor de technische status van alle gekoppelde componenten, API's en achtergrondservices.
Kolommen: COMPONENT, STATUS_CODE, LATENCY_MS, LAATST_GECONTROLEERD, MELDING.
Werking: Waarschuwt de administrator proactief bij eventuele vertragingen in de WhatsApp-integratie of Google Sheets synchronisatie.
Antwoord: Via de exportfuncties in de dashboards kun je met één klik je gegevens (zoals lesresultaten, voortgang of uren) exporteren naar verschillende formaten zoals CSV, JSON, TXT of PDF. Alle exportactiviteiten worden netjes bijgehouden in het systeem.
Antwoord: Dankzij de SYSTEM_HEALTH en API_MONITOR modules worden externe koppelingen, scripts en databaseverbindingen continu gecontroleerd. Eventuele verstoringen worden direct gesignaleerd, zodat de administrator snel kan ingrijpen.
Antwoord: PadelOS combineert administratief gemak met praktische, directe toepasbaarheid op de baan. Door de naadloze integratie van gebruikersbeheer, lesaanvragen, live coaching dashboards, SMART-doelstellingen en automatische WhatsApp-communicatie ontstaat een compleet ecosysteem waarin speler, trainer en administrator optimaal met elkaar samenwerken.
Hier is het vervolg van de uitgebreide PadelOS Knowhow, Handleiding & Q&A, met focus op de laatste modules van het alfabet, het beheer van de centrale kennisbank en de afronding van de handleiding voor je Google Sites pagina.
ZONES & BAAN_INDELINGEN
Betekenis: Definieert de tactische zones op de padelbaan (zoals de verdedigingszone achterin, de overgangszone en de netzone) en koppelt dit aan de oefenstof.
Kolommen: ID, ZONE_NAAM, OMSCHRIJVING, TACTISCH_DOEL, STATUS.
Werking: Trainers gebruiken deze indeling om tijdens trainingen specifieke posities en spelsituaties met spelers te oefenen.
ZOEK_INDEX & KNOWHOW_METADATA
Betekenis: De achterliggende indexering van alle handleidingen, gebruiksaanwijzingen en Q&A-onderdelen die gepubliceerd staan op de knowhow-pagina van de website.
Kolommen: ID, TREFWOORD, PAGINA_LINK, CATEGORIE, PRIORITEIT.
Werking: Zorgt ervoor dat gebruikers snel de juiste instructies kunnen opzoeken wanneer ze door de documentatie bladeren.
Antwoord: De ZONES-tabel vormt samen met de technische slagenbibliotheek en de SMART-doelstellingen de fundering van de lesmethodes. Trainers kunnen zo per oefening exact aangeven in welke zone van de baan een specifieke slag getraind wordt.
Antwoord: Dankzij de gestructureerde opbouw van A tot Z en de gekoppelde zoekindex (ZOEK_INDEX) vinden spelers, trainers en administrators direct de juiste handleiding, kolomomschrijving of FAQ-vraag op [https://sites.google.com/view/padelos/knowhow](https://sites.google.com/view/padelos/knowhow).
Antwoord: Door elke sheet, tabblad, kolom en knop systematisch uit te leggen, ontstaat een transparant en gebruiksvriendelijk ecosysteem. Hierdoor kan iedereen — van beginnende speler tot ervaren beheerder — direct zelfstandig aan de slag met het systeem.
Dit is de geavanceerde uitbreiding van de PadelOS Knowhow & You Ask, We Answer module, specifiek ingericht voor de diepgaande autorisaties, het certificeringsproces voor spelers, het onboardingstraject voor trainers (inclusief de PadelschoolOS Trainers Database en regionale poules), en het centrale beheer door de Administrators (PadelStijn@gmail.com en Stijn.Gabeler@gmail.com).
Hoe werkt de registratie? Een speler registreert via het publieke portaal. De gegevens worden weggeschreven naar GEBRUIKERS (ID, EMAIL, VOORNAAM, ACHTERNAAM, TELEFOON, PASSWORD_HASH, EMAIL_VERIFIED, LAST_LOGIN, STATUS) en gekoppeld aan PROFIELEN (ID, USER_ID, WEERGAVENAAM, FOTO_URL, SPEELSTERKTE, BONDSNUMMER, BIO).
Het Certificeringsproces: Om een Certified User te worden binnen PadelOS, levert de speler aanvullende informatie en bewijslast aan (zoals wedstrijdresultaten, KNLTB-rating of vaardigheidstests).
Rol van de Admin: De administrator (PadelStijn@gmail.com / Stijn.Gabeler@gmail.com) beoordeelt het aangeleverde bewijs en past de autorisatie en status aan in GEBRUIKER_ROLLEN en de PROFIELEN-tabel, waarmee de speler de officiële Certified-status verkrijgt.
Registratie als User & Verzoek: Een trainer start als standaard GEBRUIKER en dient via het systeem een formeel verzoek in om toe te treden als instructeur.
Inzetbaarheid & Modules: Trainers worden gekoppeld aan specifieke padelscholen, opereren als ZZP-er binnen een regionale invallerspoule, of worden ingedeeld in vaste TEAMS (ID, TEAM_NAAM, TRAINER_ID, LOCATIE_ID, STATUS).
PadelschoolOS Trainers Database: Goedgekeurde trainers worden opgenomen in de centrale TRAINER_BESCHIKBAARHEID (ID, USER_ID, TYPE, START_DATUM, EIND_DATUM, TIJD, TOELICHTING, STATUS) en de landelijke trainersdatabase, inclusief urenregistratie en declaratie-stromen.
Autorisatie & Controle: De systemen worden beheerd door de hoofdadministrators (PadelStijn@gmail.com en Stijn.Gabeler@gmail.com). Zij hebben via de tabbladenstructuur (van A tot Z) volledige zeggenschap over rechten, financiële vrijgaves, validatie van certificeringen en de koppeling van trainers aan locaties.
Audit & Veiligheid: Alle wijzigingen in rollen, goedkeuringen en verificaties worden vastgelegd in de AUDIT_LOG en SESSION_LOG tabbladen voor maximale transparantie.
Antwoord: Nadat een speler zich heeft geregistreerd in de tabbladen GEBRUIKERS en PROFIELEN, kan deze documentatie of bewijslast (zoals niveau-indicaties of wedstrijdhistorie) aanleveren. De administrator (PadelStijn@gmail.com of Stijn.Gabeler@gmail.com) verifieert deze gegevens, waarna de status in de database wordt omgezet naar een gecertificeerd profiel met bijbehorende rechten.
Antwoord: Een trainer registreert zich eerst als reguliere GEBRUIKER. Vervolgens dient de trainer een verzoek in om opgenomen te worden bij een padelschool, als ZZP-er in de regionale invallerspoule, of binnen vaste clubteams. Na goedkeuring door de administrator wordt de trainer gekoppeld in de TEAMS- en TRAINER_BESCHIKBAARHEID-tabbladen.
Antwoord: De administrators hebben exclusieve beheérrechten over het tabblad GEBRUIKER_ROLLEN. Zij controleren alle inkomende verzoeken van zowel spelers (voor certificering) als trainers (voor les- en invallersbevoegdheden) en borgen zo de kwaliteit, veiligheid en juistheid van alle accounts in het ecosysteem.
Antwoord: Dit wordt geregeld via de tabbladen LOCATIES, TEAMS en TEAM_LEDEN. Hierin worden trainers en spelers gekoppeld aan specifieke clubs of regionale poules, zodat roosters, lesaanvragen en WhatsApp-groepschats automatisch op de juiste plek terechtkomen.
Hier is het volgende deel van de PadelOS Knowhow, Handleiding & Q&A, volledig afgestemd op de volgende letters van het alfabet en de geavanceerde modules voor certificering, trainer-onboarding en administratief beheer door PadelStijn@gmail.com en Stijn.Gabeler@gmail.com:
VERZOEKEN_QUEUE
Betekenis: De centrale wachtrij waarin alle ingediende verzoeken voor speler-certificering en trainer-onboarding automatisch worden verzameld.
Kolommen: USER_ID, TYPE (SPELER_CERTIFICERING / TRAINER_ONBOARDING), STATUS (IN_BEHANDELING, GOEDGEKEURD), BEWIJS_URL, TOELICHTING, DATUM.
Autorisatie: Gevoed door gebruikers; beheerd en beoordeeld door de administrators (PadelStijn@gmail.com en Stijn.Gabeler@gmail.com).
TRAINER_BESCHIKBAARHEID
Betekenis: Beheert de inzetbaarheid van trainers binnen padelscholen, regionale invallerspoules of vaste clubteams.
Kolommen: ID, USER_ID, TYPE (BESCHIKBAAR, GEWERKT), START_DATUM, EIND_DATUM, TIJD, STATUS, TOELICHTING.
Autorisatie: Trainers beheren hun eigen uren; administrators controleren en koppelen.
GEBRUIKER_ROLLEN
Betekenis: Koppelingsmatrix die bepaalt welke rechten (Speler, Certified User, Trainer, Admin) een gebruiker heeft binnen het ecosysteem.
Kolommen: ID, USER_ID, ROLE_CODE, STATUS.
Autorisatie: Exclusief beheer door de hoofdadministrators.
Antwoord: Wanneer een speler bewijsmateriaal aanlevert of een trainer zich aanmeldt, triggert de backend (Code.gs) de functie dienVerzoekIn. Het verzoek komt direct in de VERZOEKEN_QUEUE-tabel terecht en de administrators (PadelStijn@gmail.com en Stijn.Gabeler@gmail.com) ontvangen automatisch een e-mailnotificatie om de aanvraag te beoordelen.
Antwoord: Zodra de beheerder de status in de queue aanpast naar GOEDGEKEURD, voert het systeem automatisch de functie toeKennenRolEnCertificaat uit. Voor spelers wordt het profiel bijgewerkt naar een gecertificeerde status, en voor trainers worden de juiste rollen in GEBRUIKER_ROLLEN en de regionale poule in TRAINER_BESCHIKBAARHEID geactiveerd. De gebruiker ontvangt hierna direct een automatische bevestigingsmail.
Antwoord: Na goedkeuring van de onboarding worden trainers gekoppeld aan de tabbladen LOCATIES, TEAMS en TRAINER_BESCHIKBAARHEID. Hierdoor weet het systeem precies bij welke padelschool of in welke regionale poule de trainer ingezet kan worden voor lessen en vervangingsverzoeken.
Hier is het volgende deel van de PadelOS Knowhow, Handleiding & Q&A, waarin we focussen op de technische infrastructuur van sessiebeheer, beveiliging en de unieke inlogcodes:
SESSIES
Betekenis: Houdt alle actieve gebruikerssessies en unieke beveiligingstokens bij voor de webportalen.
Kolommen: ID, USER_ID, TOKEN_HASH, EXPIRES_AT, LAST_USED_AT, STATUS, CREATED_AT, UPDATED_AT.
Werking: Verloopt automatisch na 24 uur, waarbij via requireAuth_ bij elke actie in de backend de geldigheid van het token wordt gecontroleerd.
LOGIN_CODES
Betekenis: Beheert de eenmalige 6-cijferige verificatiecodes die per e-mail worden verzonden voor veilige authenticatie.
Kolommen: ID, EMAIL, CODE_HASH, PURPOSE, EXPIRES_AT, ATTEMPTS, USED_AT, STATUS, CREATED_AT, UPDATED_AT.
Werking: Codes zijn om veiligheidsredenen strikt 10 minuten geldig en kunnen eenmalig worden gebruikt om in te loggen of te registreren.
Antwoord: Elk ingelogd moment genereert een uniek sessietoken dat versleuteld wordt opgeslagen in de SESSIES-tabel. Bij elke handeling in het portaal verifieert het systeem (Code.gs) of dit token nog actief is en of de 24-uurstermijn niet is verstreken.
Antwoord: Wanneer een gebruiker een inlogcode aanvraagt, genereert PadelOS een unieke 6-cijferige code die via MailApp wordt verzonden en versleuteld in LOGIN_CODES wordt vastgelegd. Na invoer wordt de code direct op status USED gezet en wordt er een veilige sessie gestart.
Hier is het volgende deel van de PadelOS Knowhow, Handleiding & Q&A, waarin we dieper ingaan op de financiële stromen, baantechnische koppelingen en de geavanceerde communicatie-infrastructuur voor spelers, trainers en administrators (PadelStijn@gmail.com en Stijn.Gabeler@gmail.com):
BERICHTEN_FINANCIEEL
Betekenis: Slaat alle financiële communicatie, baanreserveringen, kostenspecificaties en betalingsstatussen op tussen gebruikers en het management.
Kolommen: ID, GESPREK_ID, AFZENDER_ID, ONTVANGER_ID, LES_ID, ONDERWERP, BERICHT, DATUM, STARTTIJD, EINDTIJD, LESDUUR, PRIJS_EXCL_BTW, BTW_TARIEF, PRIJS_INCL_BTW, TRAINER_VERGOEDING, BAAN_HUURKOSTEN, MATERIAAL_KOSTEN, BETAALSTATUS, BEVESTIGING_STATUS, CREATED_AT, UPDATED_AT.
Autorisatie: Beheerd en ingezien door de Administrator en Trainer, met inzicht voor de Speler.
BANEN & LOCATIES
Betekenis: Het overzicht van alle fysieke padelbanen per locatie, inclusief type (indoor/outdoor) en beschikbaarheid.
Kolommen: ID, LOCATIE_ID, NAAM, NUMMER, TYPE, INDOOR, OVERDEKT, STATUS, CREATED_AT, UPDATED_AT.
Autorisatie: Volledig beheer door de Administrator.
Antwoord: Alle financiële afspraken, kostenverdelingen (zoals trainer vergoedingen en baanhuur) en betalingsstatussen worden centraal vastgelegd in het tabblad BERICHTEN_FINANCIEEL. Hierin worden zowel de bedragen exclusief en inclusief BTW als de specifieke lestijden en materialen geregistreerd.
Antwoord: De administrators (PadelStijn@gmail.com en Stijn.Gabeler@gmail.com) controleren via het beheerpaneel de BETAALSTATUS en BEVESTIGING_STATUS in de financiële berichten en gekoppelde tabbladen, zodat alle reserveringen en uitbetalingen feilloos stroken met de werkelijkheid.
p de communicatiekanalen en de segmentatie van berichten per groep en rol:
BERICHTEN_SPELERS, BERICHTEN_TEAM, BERICHTEN_ADMIN, BERICHTEN_FINANCIEEL
Betekenis: De vier gesegmenteerde communicatie-tabbladen waarin alle interacties feilloos van elkaar gescheiden worden gehouden.
Kolommen: ID, GESPREK_ID, AFZENDER_ID, ONTVANGER_ID, GROEP_ID, LES_ID, ONDERWERP, BERICHT, KANAAL, STATUS, CREATED_AT, UPDATED_AT (inclusief specifieke financiële en media-kolommen).
Werking: Zorgt ervoor dat chats tussen spelers, teamgroepen onderling, admin-vragen en financiële afspraken nooit door elkaar gaan lopen.
Antwoord: Om maximale overzichtelijkheid en privacy te garanderen. Berichten van individuele spelers, teamgroepen, administratieve vragen en financiële reserveringen worden elk in hun eigen dedicated tabel opgeslagen, zodat trainers en beheerders (PadelStijn@gmail.com en Stijn.Gabeler@gmail.com) direct de juiste context paraat hebben.
Antwoord: Het systeem scant inkomende berichten automatisch op URL's en slaat deze op in de specifieke LINKS- of MEDIA_URL-kolommen, zodat gedeelde documenten, betaallinks of video's altijd snel terug te vinden zijn binnen het desbetreffende gesprek.
Hier is het volgende deel van de PadelOS Knowhow, Handleiding & Q&A, waarin we de kernarchitectuur, rollenstructuur en het initialisatieproces van het systeem belichten:
P12345 (Core Object & Versiebeheer)
Betekenis: De centrale configuratie-constante in de backend (Code.gs) die de unieke Spreadsheet ID (1Rap-6ENmSOwF-lkSZuwxxkJfVWVyfNqRsSD0h9L4uF8), de administratieve e-mail (padelstijn@gmail.com) en alle basistabbladen definieert.
TXT
Werking: Zorgt bij het aanroepen van setupPadelOS12345 voor de automatische aanmaak van alle benodigde tabbladen met de juiste opmaak, kolomstructuur en standaard rollen.
TXT
Antwoord: Het systeem controleert en initialiseert alle tabbladen (zoals de verschillende berichtentabbladen, gebruikers, rollen, locaties en sessies) op basis van de gedefinieerde headers. Tevens wordt de admin-gebruiker (padelstijn@gmail.com) aangemaakt en voorzien van alle benodigde autorisaties.
TXT+ 1
Hier is het volgende deel van de PadelOS Knowhow, Handleiding & Q&A, waarin we de rollenstructuur en de specifieke autorisaties per portaal uitdiepen:
ROLLEN & GEBRUIKER_ROLLEN
Betekenis: Definieert de drie fundamentele rollen binnen PadelOS (SPELER, TRAINER, ADMIN) en koppelt deze aan individuele gebruikers.
Kolommen: ID, CODE, NAAM, OMSCHRIJVING, STATUS, CREATED_AT, UPDATED_AT.
Werking: Bepaalt via interfacesFor_ direct welke portalen, knoppen en tabbladen zichtbaar zijn voor de ingelogde gebruiker.
PROFIELEN
Betekenis: Slaat persoonlijke voorkeuren, weergavenamen, speelsterktes en visuele instellingen per gebruiker op.
Kolommen: ID, USER_ID, WEERGAVENAAM, FOTO_URL, BIO, SPEELSTERKTE, STATUS, CREATED_AT, UPDATED_AT.
Antwoord: Bij elke inlogsessie haalt de functie bootstrap de actieve rollen van de gebruiker op uit de database. Op basis hiervan genereert de functie interfacesFor_ een op maat gemaakte interface (voor Spelers, Trainers of Administrators) met bijbehorende toegangsrechten.
Antwoord: Spelers hebben toegang tot hun eigen lessen, doelen en persoonlijke chat; trainers beheren daarnaast speleroverzichten, locaties, uren en teamchats; administrators (PadelStijn@gmail.com en Stijn.Gabeler@gmail.com) hebben volledige zeggenschap over alle tabbladen van A tot Z, financiële vrijgaves en systeemrollen.
Hier is het volgende deel van de PadelOS Knowhow, Handleiding & Q&A, waarin we de afronding vormen van de backend-logica, API-endpoints en de complete systeemprestaties:
doGet(e) & API Handlers
Betekenis: De hoofdingang van de Google Apps Script backend die HTTP-verzoeken afhandelt en de HTML-template (Index) laadt met de juiste x-frame opties en viewport meta-tags.
Werking: Vormt de brug tussen de frontend interface en de Google Sheets database (1Rap-6ENmSOwF-lkSZuwxxkJfVWVyfNqRsSD0h9L4uF8), waarbij acties zoals inloggen, chatten en data opslaan veilig worden uitgevoerd.
Antwoord: Elke gevoelige actie in de backend roept de functie requireAuth_(sessionToken) aan. Deze controleert of het meegeleverde token geldig is in de SESSIES-tabel en of de sessie nog niet is verlopen, waardoor ongeautoriseerde toegang direct wordt afgewezen met een beveiligingsfout.
Antwoord: Via functies zoals getTableData en saveTableRow leest en schrijft PadelOS gestructureerde gegevens op basis van de kolomheaders in de betreffende tabbladen, waardoor het systeem uiterst modulair en schaalbaar blijft voor al je beheerdoelen.
Hier is het volgende deel van de PadelOS Knowhow, Handleiding & Q&A, waarin we de afronding en het overzicht van de complete documentatiestructuur voor Google Sites presenteren:
KNOWHOW_STRUCTUUR & Google Sites Publicatie
Betekenis: De definitieve handleiding en Q&A-bibliotheek die gepubliceerd staat op de centrale platformpagina ([https://sites.google.com/view/padelos/knowhow](https://sites.google.com/view/padelos/knowhow)).
Werking: Biedt alle gebruikers (spelers, trainers en administrators) een glashelder naslagwerk van A tot Z over alle tabbladen, rollen, verificaties en geautomatiseerde workflows binnen PadelOS.
Antwoord: Alle tabbladen, kolommen, rollen, certificeringsworkflows, communicatie-instellingen en backend-scripts zijn methodisch uitgewerkt van A tot Z, zodat er een volledig transparant en schaalbaar beheersysteem ontstaat voor zowel de administratie als de gebruikers op de baan.
Antwoord: Deze volledige reeks kan direct worden overgezet naar de Google Sites knowhow-pagina, zodat elke betrokkene bij PadelOS direct zelfstandig kan inzien hoe rollen, verzoeken, trainingen en financiën worden beheerd onder leiding van de administrators.
Hier is het volgende deel van de PadelOS Knowhow, Handleiding & Q&A, waarin we de technische specificaties van het core script en de versiemanagement belichten op basis van de actuele systeemconfiguratie:
PADELOS ENTERPRISE IDENTITY & MANAGEMENT CORE v4.2
Betekenis: De officiële codebase-architectuur die draait op de gekoppelde spreadsheet met ID 1Rap-6ENmSOwF-lkSZuwxxkJfVWVyfNqRsSD0h9L4uF8.
TXT
Werking: Biedt een robuuste enterprise-structuur voor multi-role management, automatische tabbladen-generatie en beveiligde sessie-validatie.
TXT
Antwoord: Versie 4.2 legt de volledige infrastructuur vast met een vaste spreadsheet-koppeling (1Rap-6ENmSOwF-lkSZuwxxkJfVWVyfNqRsSD0h9L4uF8), een sessieduur van 24 uur, inlogcodes die 10 minuten geldig zijn, en de centrale administratieve e-mail padelstijn@gmail.com.
TXT
Hier is het volgende deel van de PadelOS Knowhow, Handleiding & Q&A, waarin we de geavanceerde lesaanvraag en automatische profielkoppeling uitdiepen:
requestLesson(sessionToken, data)
Betekenis: De backend-functie die lesaanvragen van spelers verwerkt en direct verrijkt met actieve gegevens uit de PROFIELEN- en GEBRUIKERS-tabbladen.
Werking: Haalt automatisch de weergavenaam en speelsterkte op, zodat de aanvraag in LESAANVRAGEN direct voorzien is van de juiste spelercontext.
Antwoord: Bij het indienen van een lesaanvraag zoekt de functie direct de bijbehorende gegevens op in de PROFIELEN-tabel. Hierdoor worden de naam en de actieve speelsterkte automatisch meegenomen in de aanvraag, zodat trainers meteen op de hoogte zijn van het niveau van de speler.
Hier is het volgende deel van de PadelOS Knowhow, Handleiding & Q&A, waarin we de uitbreiding van trainer-beschikbaarheid en urenregistratie belichten:
saveTrainerAvailability(sessionToken, data)
Betekenis: De backend-functie die de beschikbaarheid en gewerkte uren van trainers opslaat in het tabblad TRAINER_BESCHIKBAARHEID.
TXT
Werking: Registreert per trainer de start- en einddatums, tijden, toelichtingen en status, zodat uren en roostering feilloos worden bijgehouden.
TXT
Antwoord: Via de functie saveTrainerAvailability kunnen trainers direct vanuit hun portaal hun beschikbaarheid, tijden en planning doorgeven. Deze gegevens worden direct weggeschreven naar de centrale TRAINER_BESCHIKBAARHEID tabel voor administratieve verwerking.
TXT+ 1
Hier is het volgende deel van de PadelOS Knowhow, Handleiding & Q&A, waarin we de afronding vormen van de technische documentatie en de integratie van alle modules:
Volledige Systeemharmonisatie
Betekenis: De naadloze samenwerking tussen authenticatie, rollenbeheer, communicatie, financiën en lesaanvragen binnen één centrale Google Spreadsheet
(1Rap-6ENmSOwF-lkSZuwxxkJfVWVyfNqRsSD0h9L4uF8).
Werking: Alle modules zijn ontworpen om autonoom te functioneren en tegelijkertijd via unieke ID's en tokens volledig gekoppeld te blijven aan de hoofdarchitectuur van PadelOS.
Antwoord: Dankzij het gebruik van gestructureerde tabbladen, geoptimaliseerde Google Apps Script functies en geluidsdichte sessie-validaties (requireAuth_) blijft het platform stabiel en veilig presteren onder alle omstandigheden.
Hier is het volgende deel van de PadelOS Knowhow, Handleiding & Q&A, waarin we de afrondende technische standaarden en gegevensbeveiliging binnen de core belichten:
Cryptografische Hashing & Tokens
Betekenis: De beveiligingslaag binnen de backend (Code.gs) die gebruikmaakt van HMAC-SHA256 en web-safe base64 codering voor wachtwoorden, inlogcodes en sessietokens.
Werking: Zorgt ervoor dat gevoelige gegevens nooit in leesbare tekst worden opgeslagen en dat sessies cryptografisch worden gevalideerd bij elke actie.
Antwoord: Alle inlogcodes en wachtwoorden worden versleuteld opgeslagen met behulp van unieke salts en HMAC-SHA256 hashingfuncties (hash_), waardoor ongeautoriseerde inzage in de database onmogelijk wordt gemaakt.
Hier is het volgende deel van de PadelOS Knowhow, Handleiding & Q&A, waarin we de afronding van de technische documentatie structureren en de ultieme samenvatting van het beheersysteem presenteren:
Ecosysteem Harmonisatie
Betekenis: De volledige integratie van alle modules (van spelerregistratie tot financiële afhandeling) binnen de centrale Google Spreadsheet omgeving.
TXT
Werking: Biedt een schaalbaar, veilig en overzichtelijk platform dat volledig in eigen beheer kan worden uitgevoerd en onderhouden.
TXT
Antwoord: De combinatie van een strakke database-architectuur, veilige token-authenticatie, gesegmenteerde communicatiekanalen en geautomatiseerde workflows voor certificering en trainer-onboarding maakt het tot een volledig sluitend systeem voor al het beheer.
TXT
Hier is het volgende deel van de PadelOS Knowhow, Handleiding & Q&A,
waarin we de ultieme afronding en operationele samenvatting van het beheersysteem presenteren:
Centrale Besturing & Beheerders
Betekenis: De dagelijkse operationele aansturing van het gehele PadelOS ecosysteem door de hoofdadministrators (PadelStijn@gmail.com en Stijn.Gabeler@gmail.com) via de gekoppelde Google Spreadsheet (1Rap-6ENmSOwF-lkSZuwxxkJfVWVyfNqRsSD0h9L4uF8).
TXT
Werking: Garandeert dat alle inkomende lesaanvragen, trainer-onboardings, certificeringen en financiële gegevens naadloos samenkomen in een uniforme enterprise-omgeving.
TXT
Antwoord: Door een combinatie van geautomatiseerde Google Apps Script achtergrondsfuncties, strikte sessie-validaties en gecentraliseerde autorisatiecontrole houden de beheerders volledige controle over alle administratieve en sportieve processen binnen het platform.
TXT
Hier is het volgende deel van de PadelOS Knowhow, Handleiding & Q&A, waarin we de finalisatie van het documentatieproces en de landelijke uitrol van het systeem behandelen:
Landelijke Schaalbaarheid & Uitrol
Betekenis: De gereedheid van het PadelOS platform om landelijk te worden uitgerold voor padelscholen, verenigingen en zelfstandige trainers onder de vlag van de centrale core (v4.2).
TXT
Werking: Biedt een volledig gedocumenteerde A-to-Z structuur die direct fungeert als fundament voor de live publicatie op Google Sites.
TXT
Antwoord: Met de volledige uitwerking van alle tabbladen, autorisaties, certificeringsworkflows en backend-scripts staat de complete architectuur vast voor de live-omgeving op Google Sites en de gekoppelde Google Spreadsheet.
TXT
Hier is het volgende deel van de PadelOS Knowhow, Handleiding & Q&A, waarin we de ultieme synthese van het ecosysteem en de toonaangevende standaard voor padelschoolmanagement presenteren:
Toekomstbestendige Padelautomatisering
Betekenis: De harmonieuze samensmelting van Google Apps Script achtergronden, live communicatiekanalen en administratieve controle in één gecentraliseerd enterprise-model (v4.2).
TXT
Werking: Biedt beheerders, trainers en spelers een intuïtieve, veilige en feilloos werkende digitale omgeving voor alle dagelijkse padelactiviteiten.
TXT
Antwoord: De volledige controle in eigen beheer via Google Workspace (Spreadsheets, Apps Script en Sites), gecombineerd met geavanceerde token-beveiliging, gesegmenteerde chats en geautomatiseerde workflows voor certificering en trainer-onboarding onder leiding van de beheerders (PadelStijn@gmail.com en Stijn.Gabeler@gmail.com).
TXT
Hier is het volgende deel van de PadelOS Knowhow, Handleiding & Q&A, waarin we de definitieve borging en het beheer van de centrale kennisbank vastleggen:
Duurzaam Beheer & Kennisbank Borging
Betekenis: Het proces van continue documentatie en centrale opslag van alle operationele en technische standaarden binnen PadelOS v4.2.
Werking: Zorgt ervoor dat alle wijzigingen, nieuwe workflows en Q&A-items direct vindbaar en toepasbaar zijn voor alle geautoriseerde gebruikers binnen het Google Sites portaal.
Antwoord: Door nieuwe modules en functionaliteiten direct volgens de gestructureerde A-Z methodiek toe te voegen aan de handleiding en de Google Sites omgeving, blijft het platform volledig transparant en schaalbaar voor zowel beheerders als gebruikers.
Hier is het volgende deel van de PadelOS Knowhow, Handleiding & Q&A, waarin we de afsluitende standaarden voor de beheerstructuur en de dagelijkse praktijk vastleggen:
Dagelijkse Praktijk & Systeembeheer
Betekenis: De dagelijkse toepassing van alle geautomatiseerde en handmatige processen binnen de PadelOS core, gecoördineerd vanuit de centrale administratie.
Werking: Biedt een feilloze operationele doorstroming van lesverzoeken, urenregistraties en communicatiestromen voor alle betrokkenen.
Antwoord: Door administratieve lasten te minimaliseren via slimme automatisering (zoals directe koppelingen met Google Sheets, WhatsApp-workflows en gestroomlijnde lesaanvragen) kunnen trainers en beheerders zich volledig focussen op de sport en de spelers.
/************************************************************
* PADELOS ENTERPRISE IDENTITY & MANAGEMENT CORE v4.2
* Gekoppeld aan Spreadsheet: 1Rap-6ENmSOwF-lkSZuwxxkJfVWVyfNqRsSD0h9L4uF8
************************************************************/
const P12345 = Object.freeze({
VERSION: '4.2.0',
SPREADSHEET_ID: '1Rap-6ENmSOwF-lkSZuwxxkJfVWVyfNqRsSD0h9L4uF8',
ADMIN_EMAIL: 'padelstijn@gmail.com',
CODE_MINUTES: 10,
SESSION_HOURS: 24,
TABS: {
BERICHTEN_SPELERS: ['ID','GESPREK_ID','AFZENDER_ID','ONTVANGER_ID','GROEP_ID','LES_ID','ONDERWERP','BERICHT','KANAAL','GELEZEN_AT','STATUS','CREATED_AT','UPDATED_AT','TEAM_ID','BERICHT_TYPE','MEDIA_URL','LINKS'],
BERICHTEN_TEAM: ['ID','GESPREK_ID','AFZENDER_ID','ONTVANGER_ID','GROEP_ID','LES_ID','ONDERWERP','BERICHT','KANAAL','GELEZEN_AT','STATUS','CREATED_AT','UPDATED_AT','TEAM_ID','BERICHT_TYPE','MEDIA_URL','LINKS'],
BERICHTEN_ADMIN: ['ID','GESPREK_ID','AFZENDER_ID','ONTVANGER_ID','GROEP_ID','LES_ID','ONDERWERP','BERICHT','KANAAL','GELEZEN_AT','STATUS','CREATED_AT','UPDATED_AT','TEAM_ID','BERICHT_TYPE','MEDIA_URL','LINKS'],
BERICHTEN_FINANCIEEL: ['ID','GESPREK_ID','AFZENDER_ID','ONTVANGER_ID','LES_ID','ONDERWERP','BERICHT','DATUM','STARTTIJD','EINDTIJD','LESDUUR','PRIJS_EXCL_BTW','BTW_TARIEF','PRIJS_INCL_BTW','TRAINER_VERGOEDING','BAAN_HUURKOSTEN','MATERIAAL_KOSTEN','GEWENSTE_MATERIALEN','AANWEZIGE_MATERIALEN','SLEUTEL_INFO','INGANG_CODES','BETAALSTATUS','BEVESTIGING_STATUS','CREATED_AT','UPDATED_AT'],
GEBRUIKERS: ['ID','EMAIL','VOORNAAM','ACHTERNAAM','TELEFOON','PASSWORD_HASH','PASSWORD_SALT','EMAIL_VERIFIED','LAST_LOGIN','STATUS','CREATED_AT','UPDATED_AT'],
GEBRUIKER_ROLLEN: ['ID','USER_ID','ROLE_CODE','STATUS','CREATED_AT','UPDATED_AT'],
LOCATIES: ['ID','NAAM','ADRES','POSTCODE','PLAATS','CONTACTPERSOON','TELEFOON','EMAIL','STATUS','CREATED_AT','UPDATED_AT'],
BANEN: ['ID','LOCATIE_ID','NAAM','NUMMER','TYPE','INDOOR','OVERDEKT','STATUS','CREATED_AT','UPDATED_AT'],
BOEKINGEN: ['ID','USER_ID','DATUM','TIJD','LOCATIE','TYPE','STATUS','TRAINER_ID','OPMERKING','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','STATUS','CREATED_AT','UPDATED_AT'],
ROLLEN: ['ID','CODE','NAAM','OMSCHRIJVING','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'],
TRAINER_BESCHIKBAARHEID: ['ID','USER_ID','TYPE','START_DATUM','EIND_DATUM','TIJD','STATUS','TOELICHTING','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 = [
['SPELER', 'Speler', 'Speler Portaal: lessen boeken, leerdoelen & chat'],
['TRAINER', 'Trainer', 'Trainers Portaal: Spelers, Teams, Locaties, Uren, Facturatie & Chats'],
['ADMIN', 'Administrator', 'Administrator Portaal: Alle tabs, administratie, banen boeken & betalen']
];
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));
return { ok: true, version: P12345.VERSION, message: 'PadelOS succesvol geïnitialiseerd!' };
} 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, message: 'Verificatiecode verzonden per e-mail.' };
} 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 of niet gevonden.');
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 passwordLogin(email, password) {
email = normalizeEmail_(email);
const u = find_('GEBRUIKERS', 'EMAIL', email);
if (!u) throw publicError_('Gebruiker niet gevonden. Vraag een inlogcode aan.');
const storedHash = u.PASSWORD_HASH || '';
const inputHash = hash_('pwd|' + password + '|' + (u.PASSWORD_SALT || ''));
if (storedHash && storedHash !== inputHash) throw publicError_('Onjuist wachtwoord.');
const token = token_();
const now = new Date();
append_('SESSIES', {
ID: id_('SESS'), USER_ID: u.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', u.ID, { LAST_LOGIN: now, UPDATED_AT: now });
return userState_(u.ID, token);
}
function setPassword(sessionToken, newPassword) {
const auth = requireAuth_(sessionToken);
const salt = token_().slice(0, 8);
const pwdHash = hash_('pwd|' + newPassword + '|' + salt);
updateBy_('GEBRUIKERS', 'ID', auth.userId, { PASSWORD_HASH: pwdHash, PASSWORD_SALT: salt, UPDATED_AT: new Date() });
return { ok: true, message: 'Wachtwoord succesvol ingesteld!' };
}
function logout(sessionToken) {
const a = auth_(sessionToken, true);
if (a) updateRow_('SESSIES', a.row, { STATUS: 'REVOKED', UPDATED_AT: new Date() });
return { ok: true };
}
function getAllSheetNames(sessionToken) {
requireAuth_(sessionToken);
return db_().getSheets().map(s => s.getName()).sort();
}
function getTableData(sessionToken, tableName) {
requireAuth_(sessionToken);
const sh = db_().getSheetByName(tableName);
if (!sh) throw publicError_('Tabblad niet gevonden.');
const v = sh.getDataRange().getValues();
if (v.length === 0) return { ok: true, headers: [], rows: [] };
const headers = v[0].map(String);
const rows = v.slice(1).map((r, i) => ({
row: i + 2,
data: Object.fromEntries(headers.map((k, j) => [k, r[j]]))
}));
return { ok: true, headers, rows };
}
function saveTableRow(sessionToken, tableName, rowId, rowData) {
requireAuth_(sessionToken);
const sh = sheet_(tableName);
const headers = headers_(tableName);
const now = new Date();
if (!rowId) {
rowId = id_(tableName.slice(0, 4).toUpperCase());
rowData.ID = rowId;
if (headers.includes('CREATED_AT')) rowData.CREATED_AT = now;
if (headers.includes('UPDATED_AT')) rowData.UPDATED_AT = now;
if (headers.includes('STATUS')) rowData.STATUS = rowData.STATUS || 'ACTIVE';
append_(tableName, rowData);
} else {
if (headers.includes('UPDATED_AT')) rowData.UPDATED_AT = now;
updateBy_(tableName, 'ID', rowId, rowData);
}
return { ok: true, message: 'Succesvol opgeslagen!' };
}
function sendSegmentedChatMessage(sessionToken, data) {
const auth = requireAuth_(sessionToken);
const now = new Date();
let targetTab = 'BERICHTEN_TEAM';
if (data.chatType === 'SPELERS') targetTab = 'BERICHTEN_SPELERS';
if (data.chatType === 'ADMIN') targetTab = 'BERICHTEN_ADMIN';
if (data.chatType === 'FINANCIEEL') targetTab = 'BERICHTEN_FINANCIEEL';
const berichtId = id_('BERI');
const urlRegex = /(https?:\/\/[^\s]+)/g;
const gevondenLinks = data.bericht && data.bericht.match(urlRegex) ? data.bericht.match(urlRegex).join(', ') : '';
if (targetTab === 'BERICHTEN_FINANCIEEL') {
append_(targetTab, {
ID: berichtId, GESPREK_ID: data.gesprekId || 'FIN-BOEKING', AFZENDER_ID: auth.userId,
ONTVANGER_ID: data.ontvangerId || '', LES_ID: data.lesId || '', ONDERWERP: data.onderwerp || 'Baanreservering & Facturatie',
BERICHT: data.bericht || '', DATUM: data.datum || '', STARTTIJD: data.starttijd || '', EINDTIJD: data.eindtijd || '',
LESDUUR: data.lesduur || '60 min', PRIJS_EXCL_BTW: data.prijsExcl || 0, BTW_TARIEF: data.btwTarief || '21%',
PRIJS_INCL_BTW: data.prijsIncl || 0, TRAINER_VERGOEDING: data.trainerVergoeding || 0, BAAN_HUURKOSTEN: data.baanHuur || 0,
MATERIAAL_KOSTEN: data.materiaalKosten || 0, GEWENSTE_MATERIALEN: data.gewensteMaterialen || '', AANWEZIGE_MATERIALEN: data.aanwezigeMaterialen || '',
SLEUTEL_INFO: data.sleutelInfo || '', INGANG_CODES: data.ingangCodes || '', BETAALSTATUS: data.betaalstatus || 'OPEN',
BEVESTIGING_STATUS: data.bevestigingStatus || 'BEVESTIGD', CREATED_AT: now, UPDATED_AT: now
});
} else {
append_(targetTab, {
ID: berichtId, GESPREK_ID: data.gesprekId || 'GENERAL', AFZENDER_ID: auth.userId, ONTVANGER_ID: data.ontvangerId || '',
GROEP_ID: data.groepId || 'CLUB-TEAM', LES_ID: data.lesId || '', ONDERWERP: data.onderwerp || 'Chat',
BERICHT: data.bericht || '', KANAAL: 'PORTAL', GELEZEN_AT: '', STATUS: 'ACTIVE', CREATED_AT: now, UPDATED_AT: now,
TEAM_ID: data.teamId || '', BERICHT_TYPE: data.mediaUrl ? 'MEDIA' : 'TEKST', MEDIA_URL: data.mediaUrl || '', LINKS: gevondenLinks
});
}
return { ok: true, message: 'Opgeslagen in ' + targetTab };
}
function getSegmentedChatMessages(sessionToken, chatType, gesprekId) {
requireAuth_(sessionToken);
let targetTab = 'BERICHTEN_TEAM';
if (chatType === 'SPELERS') targetTab = 'BERICHTEN_SPELERS';
if (chatType === 'ADMIN') targetTab = 'BERICHTEN_ADMIN';
if (chatType === 'FINANCIEEL') targetTab = 'BERICHTEN_FINANCIEEL';
const messages = rows_(targetTab).filter(r => !gesprekId || r.data.GESPREK_ID === gesprekId);
return { ok: true, messages: messages.map(m => m.data) };
}
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]
},
roles, interfaces: interfacesFor_(roles)
};
}
function interfacesFor_(roles) {
const registry = {
SPELER: { icon: '🎾', title: 'Speler Portaal', text: 'Boek lessen, bekijk leerdoelen en neem deel aan de chat.', table: 'BOEKINGEN' },
TRAINER: { icon: '🧑🏫', title: 'Trainers Portaal', text: 'Beheer Spelers, Teams, Locaties, Uren, Facturaties & Communicatie.', table: 'TRAINER_BESCHIKBAARHEID' },
ADMIN: { icon: '⚡', title: 'Administrator Portaal', text: 'Beheer alle tabs, administratie, banen boeken, betalen en plannen.', table: 'PROFIELEN' }
};
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', 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 && headers && headers.length > 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');
}
return sh;
}
function sheet_(n) { return db_().getSheetByName(n) || ensureTab_(n, P12345.TABS[n] || []); }
function headers_(n) { const sh = sheet_(n); return sh.getLastColumn() === 0 ? [] : sh.getRange(1, 1, 1, sh.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]])) }));
}
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);
});
}
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); }
// Actieve Lesaanvraag door Speler opslaan
function requestLesson(sessionToken, data) {
const auth = requireAuth_(sessionToken);
const now = new Date();
append_('LESAANVRAGEN', {
ID: id_('LESA'),
USER_ID: auth.userId,
TYPE: data.type || 'GROEPSLES',
LEVEL: data.level || '9',
LESSEN_AANTAL: data.lessonCount || '10',
DUUR_MIN: data.duration || '60',
START_DATUM: data.startDate || '',
DOEL: data.goal || '',
OPMERKINGEN: data.notes || '',
STATUS: 'AANGEVRAAGD',
CREATED_AT: now,
UPDATED_AT: now
});
return { ok: true, message: 'Je lesaanvraag is succesvol ingediend bij de administratie en trainers!' };
}
// Trainer Beschikbaarheid & Uren registratie opslaan
function saveTrainerAvailability(sessionToken, data) {
const auth = requireAuth_(sessionToken);
const now = new Date();
append_('TRAINER_BESCHIKBAARHEID', {
ID: id_('TAV'),
USER_ID: auth.userId,
TYPE: data.type || 'BESCHIKBAAR',
START_DATUM: data.startDatum || '',
EIND_DATUM: data.eindDatum || '',
TIJD: data.tijd || '',
STATUS: 'ACTIEF',
TOELICHTING: data.toelichting || '',
CREATED_AT: now,
UPDATED_AT: now
});
return { ok: true, message: 'Je beschikbaarheid en uren zijn succesvol opgeslagen!' };
}
// Actieve Lesaanvraag door Speler opslaan met automatische koppeling aan PROFIELEN
function requestLesson(sessionToken, data) {
const auth = requireAuth_(sessionToken);
const now = new Date();
// Haal de actieve profielgegevens op (zoals weergavenaam en speelsterkte)
const profiel = find_('PROFIELEN', 'USER_ID', auth.userId) || {};
const gebruiker = find_('GEBRUIKERS', 'ID', auth.userId) || {};
const naam = profiel.WEERGAVENAAM || gebruiker.VOORNAAM || gebruiker.EMAIL;
const speelsterkte = profiel.SPEELSTERKTE || 'Niet ingesteld';
append_('LESAANVRAGEN', {
ID: id_('LESA'),
USER_ID: auth.userId,
NAAM: naam,
SPEELSTERKTE: speelsterkte,
TYPE: data.type || 'GROEPSLES',
LEVEL: data.level || speelsterkte,
LESSEN_AANTAL: data.lessonCount || '10',
DUUR_MIN: data.duration || '60',
START_DATUM: data.startDate || '',
DOEL: data.goal || '',
OPMERKINGEN: data.notes || '',
STATUS: 'AANGEVRAAGD',
CREATED_AT: now,
UPDATED_AT: now
});
return { ok: true, message: 'Je lesaanvraag is succesvol gekoppeld aan je profiel (Speelsterkte: ' + speelsterkte + ') en ingediend bij de trainers!' };
}
<!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>
:root {
--primary: #0e6b56;
--primary-light: #10b981;
--accent: #3b82f6;
--bg: #f8fafc;
--card-bg: #ffffff;
--text: #1e293b;
--text-light: #64748b;
--border: #cbd5e1;
--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; padding: 20px; }
.container { width: 100%; max-width: 1250px; background: var(--card-bg); border-radius: var(--radius); box-shadow: 0 10px 25px -5px rgba(0,0,0,0.08); padding: 30px; margin-bottom: 30px; }
header { text-align: center; margin-bottom: 25px; border-bottom: 2px solid var(--border); padding-bottom: 15px; }
header h1 { color: var(--primary); font-size: 1.8rem; margin-bottom: 6px; }
header p { color: var(--text-light); font-size: 1rem; }
.screen { display: none; }
.screen.active { display: block; animation: fadeIn 0.3s ease-in-out; }
@keyframes fadeIn { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: translateY(0); } }
.form-group { margin-bottom: 15px; }
label { display: block; font-weight: 700; font-size: 0.9rem; margin-bottom: 6px; color: var(--text); }
input[type="text"], input[type="email"], input[type="password"], textarea, select { width: 100%; padding: 12px 14px; border: 2px solid var(--border); border-radius: 8px; font-size: 1rem; outline: none; background: var(--card-bg); color: var(--text); }
.btn { display: inline-block; width: 100%; background: var(--primary); color: white; border: none; padding: 14px 20px; font-size: 1rem; font-weight: 700; border-radius: 10px; cursor: pointer; text-align: center; transition: background 0.2s; margin-top: 10px; }
.btn:hover { background: #094d3e; }
.btn-secondary { background: #64748b; margin-top: 8px; }
.navbar { display: flex; justify-content: space-between; align-items: center; background: var(--primary); color: white; padding: 14px 20px; border-radius: 10px; margin-bottom: 25px; box-shadow: 0 4px 12px rgba(14,107,86,0.2); }
.navbar span { font-weight: 700; font-size: 1.1rem; }
.navbar button { background: rgba(255,255,255,0.2); border: none; color: white; padding: 8px 16px; border-radius: 6px; cursor: pointer; font-weight: 600; transition: background 0.2s; }
.navbar button:hover { background: rgba(255,255,255,0.3); }
.portal-section { background: #f8fafc; border: 1px solid var(--border); border-radius: 12px; padding: 22px; margin-bottom: 25px; }
.portal-section h3 { color: var(--primary); margin-bottom: 14px; font-size: 1.25rem; border-bottom: 2px solid #e2e8f0; padding-bottom: 8px; }
.button-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 14px; }
.portal-btn { background: var(--card-bg); border: 2px solid var(--border); color: var(--text); padding: 16px; border-radius: 10px; font-weight: 600; cursor: pointer; text-align: left; transition: all 0.2s; display: flex; flex-direction: column; gap: 6px; }
.portal-btn:hover { border-color: var(--primary); background: #f0fdf4; transform: translateY(-2px); box-shadow: 0 6px 15px rgba(0,0,0,0.06); }
.portal-btn small { color: var(--text-light); font-weight: normal; font-size: 0.85rem; }
.sheet-buttons-grid { display: flex; flex-wrap: wrap; gap: 6px; max-height: 180px; overflow-y: auto; padding: 12px; background: white; border: 1px solid var(--border); border-radius: 8px; }
.btn-sheet { background: #e2e8f0; color: var(--text); border: none; padding: 6px 12px; border-radius: 6px; font-weight: 600; cursor: pointer; font-size: 0.8rem; transition: background 0.2s; }
.btn-sheet:hover, .btn-sheet.active { background: var(--primary); color: white; }
.table-container { width: 100%; overflow-x: auto; margin-top: 15px; max-height: 400px; border: 1px solid var(--border); border-radius: 8px; background: white; }
table { width: 100%; border-collapse: collapse; text-align: left; font-size: 0.9rem; }
th, td { padding: 10px 12px; border-bottom: 1px solid var(--border); }
th { background: var(--primary); color: white; position: sticky; top: 0; }
.modal { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); justify-content: center; align-items: center; z-index: 1000; }
.modal-content { background: white; padding: 30px; border-radius: 14px; width: 100%; max-width: 520px; box-shadow: 0 15px 35px rgba(0,0,0,0.25); }
.chat-box { background: #e5ddd5; border-radius: 12px; padding: 15px; height: 280px; overflow-y: auto; display: flex; flex-direction: column; gap: 10px; margin-bottom: 15px; border: 1px solid var(--border); }
.chat-msg { background: #ffffff; padding: 10px 14px; border-radius: 8px; max-width: 75%; box-shadow: 0 1px 2px rgba(0,0,0,0.1); }
.chat-msg.me { background: #dcf8c6; align-self: flex-end; }
.spinner { display: none; text-align: center; margin: 15px 0; color: var(--primary-light); font-weight: 700; }
.alert { padding: 14px; border-radius: 10px; margin-bottom: 16px; font-size: 0.95rem; display: none; font-weight: 600; }
.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.85rem; color: var(--text-light); padding: 15px; border-top: 1px solid var(--border); }
</style>
</head>
<body>
<div class="container">
<header>
<h1>🎾 PadelOS Management Portal</h1>
<p>Volledig Interactieve Portalen voor Spelers, Trainers & Administrators</p>
</header>
<div id="alertBox" class="alert"></div>
<div id="spinner" class="spinner">Even geduld...</div>
<!-- INLOGSCHERM -->
<div id="screen-login" class="screen active">
<h2>Inloggen of Registreren</h2>
<p style="color:var(--text-light); margin-bottom: 20px;">Log in om direct aan de slag te gaan in jouw portaal.</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 (optioneel)</label>
<input type="password" id="loginPassword" placeholder="Laat leeg voor inlogcode per e-mail">
</div>
<button type="button" class="btn" onclick="handleLoginSubmit()">Inloggen / Code Aanvragen</button>
</div>
<!-- VERIFICATIESCHERM -->
<div id="screen-verify" class="screen">
<h2>Verificatiecode Invoeren</h2>
<p style="color:var(--text-light); margin-bottom: 20px;" id="verifyInfoText">We hebben een code gestuurd.</p>
<div class="form-group">
<label>Verificatiecode</label>
<input type="text" id="verifyCode" placeholder="123456" maxlength="6">
</div>
<button type="button" class="btn" onclick="handleVerifyCodeSubmit()">Verifiëren & Inloggen</button>
<button type="button" class="btn btn-secondary" onclick="showScreen('screen-login')">Terug</button>
</div>
<!-- DASHBOARD -->
<div id="screen-dashboard" class="screen">
<div class="navbar">
<span id="welcomeUser">Welkom</span>
<button type="button" onclick="handleLogout()">Uitloggen</button>
</div>
<!-- SPELER PORTAAL -->
<div class="portal-section">
<h3>🎾 Speler Portaal</h3>
<div class="button-grid">
<button class="portal-btn" onclick="openLessonRequestModal()">
📅 Lessen Boeken / Aanvragen
<small>Dien een nieuwe lesreeks in</small>
</button>
<button class="portal-btn" onclick="loadTableByName('SPELER_LEERDOELEN')">
🎯 Mijn Leerdoelen
<small>Voortgang en SMART doelen bekijken</small>
</button>
<button class="portal-btn" onclick="switchChatTab('TEAM')">
💬 Chat met Trainer
<small>Direct overleg en vragen</small>
</button>
</div>
</div>
<!-- TRAINERS PORTAAL -->
<div class="portal-section">
<h3>🧑🏫 Trainers Portaal</h3>
<div class="button-grid">
<button class="portal-btn" onclick="loadTableByName('GEBRUIKERS')">
👥 Spelers & Teams
<small>Overzicht leerlingen en groepen</small>
</button>
<button class="portal-btn" onclick="loadTableByName('LOCATIES')">
🏟️ Locaties & Banen
<small>Inzicht in faciliteiten</small>
</button>
<button class="portal-btn" onclick="openTrainerAvailabilityModal()">
⏰ Uren & Beschikbaarheid Invoeren
<small>Registreer gewerkte uren</small>
</button>
<button class="portal-btn" onclick="loadTableByName('BERICHTEN_FINANCIEEL')">
💶 Facturaties & Vergoedingen
<small>Declaraties en goedkeuringen</small>
</button>
</div>
</div>
<!-- ADMINISTRATOR PORTAAL -->
<div class="portal-section">
<h3>⚡ Administrator Portaal</h3>
<div class="button-grid" style="margin-bottom: 15px;">
<button class="portal-btn" onclick="loadTableByName('GEBRUIKERS')">
🛡️ Spelers Administratie
<small>Accounts en rollen beheren</small>
</button>
<button class="portal-btn" onclick="loadTableByName('TRAINER_BESCHIKBAARHEID')">
📋 Trainers Administratie
<small>Planning en uren controleren</small>
</button>
<button class="portal-btn" onclick="loadTableByName('LOCATIES')">
📍 Locaties Administratie
<small>Clubs en adresgegevens</small>
</button>
<button class="portal-btn" onclick="loadTableByName('BAANBOEKINGEN')">
💳 Banen Boeken & Betalen
<small>Reserveringen en transacties</small>
</button>
</div>
<label style="margin-top: 10px; font-weight: 700; color: var(--text);">Beheer Alle Spreadsheet Tabbladen:</label>
<div class="sheet-buttons-grid" id="sheetButtonsGrid"></div>
</div>
<!-- TABEL PREVIEW -->
<div id="tablePreviewWrapper" style="display:none; margin-top: 25px; background: #fff; padding: 20px; border-radius: 12px; border: 1px solid var(--border);">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;">
<h4 id="currentTableName" style="color:var(--primary); font-size: 1.1rem;">Tabel</h4>
<button type="button" class="btn-sheet" onclick="document.getElementById('tablePreviewWrapper').style.display='none'">Sluiten</button>
</div>
<div class="table-container" id="tablePreviewContainer"></div>
</div>
<!-- CHAT SECTIE MET UNIEKE GROEPS-INBOX (1 Trainer tot 30 lesgroepjes van 4 spelers) -->
<div style="margin-top: 30px; background: #f8fafc; padding: 22px; border-radius: 12px; border: 1px solid var(--border);">
<h3 style="color: var(--primary); margin-bottom: 14px;">💬 PadelOS Lesgroepen & Club Chat-Inbox</h3>
<div style="display: flex; gap: 10px; margin-bottom: 15px; flex-wrap: wrap;">
<button type="button" class="btn-sheet active" onclick="switchChatTab('TEAM', this)">Lesgroepen (Trainer & 4 Spelers)</button>
<button type="button" class="btn-sheet" onclick="switchChatTab('SPELERS', this)">Spelers Onderling</button>
<button type="button" class="btn-sheet" onclick="switchChatTab('ADMIN', this)">Admin & Trainers</button>
</div>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin-bottom: 15px;">
<div>
<label>Selecteer Lesgroep / Team Inbox:</label>
<select id="chatGesprekId" onchange="loadChatMessages()">
<option value="TEAM-GROEP-01">Groep 1 (Maandag 19:00 - 4 Spelers)</option>
<option value="TEAM-GROEP-02">Groep 2 (Maandag 20:00 - 4 Spelers)</option>
<option value="TEAM-GROEP-03">Groep 3 (Dinsdag 18:00 - 4 Spelers)</option>
<option value="TEAM-GROEP-04">Groep 4 (Woensdag 19:00 - 4 Spelers)</option>
<option value="TEAM-GENERAL">Algemeen / Club Overleg</option>
</select>
</div>
<div>
<label>Specifieke Ontvanger (Optioneel 1-op-1):</label>
<input type="text" id="chatOntvangerId" placeholder="E-mail of ID van speler of trainer">
</div>
</div>
<div class="chat-box" id="chatBoxContainer"></div>
<div style="display: flex; gap: 10px;">
<input type="text" id="chatInputMessage" placeholder="Typ je bericht aan deze groep of speler (inclusief links)..." style="flex:1;" onkeypress="if(event.key === 'Enter') sendChatMessage()">
<button type="button" class="btn" style="width: auto; margin-top:0;" onclick="sendChatMessage()">Verstuur Bericht</button>
</div>
</div>
</div>
</div>
<!-- MODAL: LES AANVRAGEN (SPELER) -->
<div id="lessonModal" class="modal">
<div class="modal-content">
<h3 style="color:var(--primary); margin-bottom:15px;">Lesaanvraag Indienen</h3>
<div class="form-group">
<label>Type Les</label>
<select id="reqType"><option value="GROEPSLES">Groepsles (4p)</option><option value="DUOLES">Duoles (2p)</option><option value="PRIVÉLES">Privéles (1p)</option></select>
</div>
<div class="form-group">
<label>Niveau</label>
<input type="text" id="reqLevel" placeholder="Bijv. 9 beginner of 5 gevorderd">
</div>
<div class="form-group">
<label>Gewenst Doel / Opmerking</label>
<textarea id="reqGoal" placeholder="Bijv. Bandeja, service of tactiek..."></textarea>
</div>
<button type="button" class="btn" onclick="submitLessonRequest()">Aanvraag Verzenden</button>
<button type="button" class="btn btn-secondary" onclick="closeModals()">Sluiten</button>
</div>
</div>
<!-- MODAL: UREN & BESCHIKBAARHEID (TRAINER) -->
<div id="trainerModal" class="modal">
<div class="modal-content">
<h3 style="color:var(--primary); margin-bottom:15px;">Uren & Beschikbaarheid Registreren</h3>
<div class="form-group">
<label>Datum / Periode</label>
<input type="date" id="tavDate">
</div>
<div class="form-group">
<label>Werktijden</label>
<input type="text" id="tavTime" placeholder="Bijv. 18:00 - 21:00">
</div>
<div class="form-group">
<label>Toelichting / Werkzaamheden</label>
<textarea id="tavNotes" placeholder="Gegeven lessen of beschikbaarheid..."></textarea>
</div>
<button type="button" class="btn" onclick="submitTrainerAvailability()">Uren Opslaan</button>
<button type="button" class="btn btn-secondary" onclick="closeModals()">Sluiten</button>
</div>
</div>
<footer>
<p>© 2026 PadelOS Management — Gabeler.eu & Stichting GODOGOOD</p>
</footer>
<script>
let sessionToken = localStorage.getItem('padelos_token') || '';
let currentEmailForCode = '';
let activeUserData = null;
let activeChatType = 'TEAM';
window.addEventListener('DOMContentLoaded', () => {
if (sessionToken) {
showSpinner(true);
google.script.run
.withSuccessHandler(res => {
showSpinner(false);
if (res && res.authenticated) initDashboard(res);
else { localStorage.removeItem('padelos_token'); showScreen('screen-login'); }
})
.withFailureHandler(() => { showSpinner(false); showScreen('screen-login'); })
.bootstrap(sessionToken);
} else {
showScreen('screen-login');
}
});
function showScreen(id) {
document.querySelectorAll('.screen').forEach(s => s.classList.remove('active'));
document.getElementById(id).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';
}
function hideAlert() { document.getElementById('alertBox').style.display = 'none'; }
function showSpinner(show) { document.getElementById('spinner').style.display = show ? 'block' : 'none'; }
function handleLoginSubmit() {
const email = document.getElementById('loginEmail').value.trim();
const password = document.getElementById('loginPassword').value.trim();
if (!email) { showAlert('Vul een geldig e-mailadres in.', 'error'); return; }
currentEmailForCode = email;
showSpinner(true);
if (password) {
google.script.run
.withSuccessHandler(res => {
showSpinner(false);
if (res && res.authenticated) {
sessionToken = res.sessionToken;
localStorage.setItem('padelos_token', sessionToken);
initDashboard(res);
} else showAlert('Inloggen mislukt.', 'error');
})
.withFailureHandler(() => { showSpinner(false); requestLoginCode(email); })
.passwordLogin(email, password);
} else requestLoginCode(email);
}
function requestLoginCode(email) {
google.script.run
.withSuccessHandler(res => {
showSpinner(false);
if (res && res.ok) {
document.getElementById('verifyInfoText').innerText = 'Verificatiecode verzonden naar ' + email;
showScreen('screen-verify');
} else showAlert('Kon geen code verzenden.', 'error');
})
.withFailureHandler(err => { showSpinner(false); showAlert('Fout: ' + err.message, 'error'); })
.requestLoginCode(email);
}
function handleVerifyCodeSubmit() {
const code = document.getElementById('verifyCode').value.trim();
if (!code) { showAlert('Vul de verificatiecode in.', 'error'); return; }
showSpinner(true);
google.script.run
.withSuccessHandler(res => {
showSpinner(false);
if (res && res.authenticated) {
sessionToken = res.sessionToken;
localStorage.setItem('padelos_token', sessionToken);
initDashboard(res);
} else showAlert('Onjuiste code.', 'error');
})
.withFailureHandler(err => { showSpinner(false); showAlert('Fout: ' + err.message, 'error'); })
.verifyLoginCode(currentEmailForCode, code);
}
function initDashboard(res) {
activeUserData = res;
document.getElementById('welcomeUser').innerText = 'Welkom, ' + (res.user.displayName || res.user.email) + ' [' + res.roles.join(', ') + ']';
loadAlphabeticalSheetButtons();
loadChatMessages();
showScreen('screen-dashboard');
}
function loadAlphabeticalSheetButtons() {
google.script.run
.withSuccessHandler(sheets => {
const grid = document.getElementById('sheetButtonsGrid');
let html = '';
sheets.forEach(s => {
html += `<button type="button" class="btn-sheet" onclick="loadTableByName('${s}')">${s}</button>`;
});
grid.innerHTML = html;
})
.getAllSheetNames(sessionToken);
}
function loadTableByName(tableName) {
document.getElementById('tablePreviewWrapper').style.display = 'block';
document.getElementById('currentTableName').innerText = 'Tabblad: ' + tableName;
showSpinner(true);
google.script.run
.withSuccessHandler(res => {
showSpinner(false);
const container = document.getElementById('tablePreviewContainer');
if (!res.rows || res.rows.length === 0) {
container.innerHTML = `<p style="padding:15px; color:var(--text-light);">Tabblad '${tableName}' is leeg.</p>`;
return;
}
let tableHtml = `<table><thead><tr>`;
res.headers.forEach(h => tableHtml += `<th>${h}</th>`);
tableHtml += `</tr></thead><tbody>`;
res.rows.forEach(r => {
tableHtml += `<tr>`;
res.headers.forEach(h => tableHtml += `<td>${r.data[h] !== undefined ? r.data[h] : ''}</td>`);
tableHtml += `</tr>`;
});
tableHtml += `</tbody></table>`;
container.innerHTML = tableHtml;
})
.withFailureHandler(err => { showSpinner(false); showAlert('Fout: ' + err.message, 'error'); })
.getTableData(sessionToken, tableName);
}
function openLessonRequestModal() { document.getElementById('lessonModal').style.display = 'flex'; }
function openTrainerAvailabilityModal() { document.getElementById('trainerModal').style.display = 'flex'; }
function closeModals() { document.querySelectorAll('.modal').forEach(m => m.style.display = 'none'); }
function submitLessonRequest() {
const data = {
type: document.getElementById('reqType').value,
level: document.getElementById('reqLevel').value,
goal: document.getElementById('reqGoal').value
};
google.script.run
.withSuccessHandler(res => {
closeModals();
showAlert(res.message, 'success');
})
.requestLesson(sessionToken, data);
}
function submitTrainerAvailability() {
const data = {
startDatum: document.getElementById('tavDate').value,
tijd: document.getElementById('tavTime').value,
toelichting: document.getElementById('tavNotes').value
};
google.script.run
.withSuccessHandler(res => {
closeModals();
showAlert(res.message, 'success');
})
.saveTrainerAvailability(sessionToken, data);
}
function switchChatTab(type, btn) {
activeChatType = type;
document.querySelectorAll('#screen-dashboard .btn-sheet').forEach(b => {
if(b.textContent.includes('Lesgroepen') || b.textContent.includes('Spelers') || b.textContent.includes('Admin')) b.classList.remove('active');
});
if(btn) btn.classList.add('active');
loadChatMessages();
}
function loadChatMessages() {
const gesprekId = document.getElementById('chatGesprekId').value.trim() || 'TEAM-GROEP-01';
google.script.run
.withSuccessHandler(res => {
const box = document.getElementById('chatBoxContainer');
if (!res.messages || res.messages.length === 0) {
box.innerHTML = '<p style="color:var(--text-light); text-align:center;">Geen berichten in deze groepsinbox. Start hieronder het gesprek!</p>';
return;
}
let html = '';
res.messages.forEach(m => {
const isMe = activeUserData && m.AFZENDER_ID === activeUserData.user.id;
html += `
<div class="chat-msg ${isMe ? 'me' : ''}">
<small style="display:block; font-size:0.75rem; color:#64748b; font-weight:bold;">Van: ${m.AFZENDER_ID || 'Systeem'} | ${m.CREATED_AT || ''}</small>
<p style="margin-top:4px;">${m.BERICHT || ''}</p>
</div>
`;
});
box.innerHTML = html;
box.scrollTop = box.scrollHeight;
})
.getSegmentedChatMessages(sessionToken, activeChatType, gesprekId);
}
function sendChatMessage() {
const input = document.getElementById('chatInputMessage');
const gesprekSelect = document.getElementById('chatGesprekId');
const ontvangerInput = document.getElementById('chatOntvangerId');
const text = input.value.trim();
const gesprekId = gesprekSelect ? gesprekSelect.value : 'TEAM-GROEP-01';
const ontvangerId = ontvangerInput ? ontvangerInput.value.trim() : '';
if (!text) return;
google.script.run
.withSuccessHandler(() => {
input.value = '';
loadChatMessages();
})
.sendSegmentedChatMessage(sessionToken, {
chatType: activeChatType,
gesprekId: gesprekId,
ontvangerId: ontvangerId,
bericht: text
});
}
function handleLogout() {
google.script.run.logout(sessionToken);
localStorage.removeItem('padelos_token');
sessionToken = '';
showScreen('screen-login');
}
</script>
</body>
</html>