""" Personeel: afwezigheden van leerkrachten, nascholingen en uitstappen. Rollen (config.ini βΊ [personeel]): - leerkracht : iedereen in personeel_ous die met Google inlogt (PIVOT-groep 5). Ziet en beheert enkel zijn eigen meldingen/aanvragen. - directie : e-mailadressen in 'directie' β keurt nascholingen en uitstappen goed. - secretariaat : e-mailadressen in 'secretariaat' β dagoverzicht, vervangingen, melden namens een leerkracht. PIVOT-gebruikers met het recht 'personeel_secretariaat' / 'personeel_directie' in deze school krijgen die rol ook. Administrators hebben de secretariaatsrechten, en ook de directierechten zolang er nog geen directie is ingesteld. Soorten: - afwezigheid : geen goedkeuring nodig, meteen zichtbaar voor het secretariaat - nascholing : goedkeuring door de directie - uitstap : goedkeuring door de directie (klassen + begeleiders zijn weg) """ import json import sys import threading import time from datetime import date, timedelta from config import Config from database import execute_query, get_db_connection from tenancy import huidige_school, met_school LEERKRACHT_GROEP = 5 SOORTEN = { 'afwezigheid': {'label': 'Afwezigheid', 'icoon': 'π€', 'goedkeuring': False}, 'nascholing': {'label': 'Nascholing', 'icoon': 'π', 'goedkeuring': True}, 'uitstap': {'label': 'Uitstap', 'icoon': 'π', 'goedkeuring': True}, } STATUS = { 'gemeld': ('Gemeld', 'info'), 'ingediend': ('Wacht op directie', 'warning'), 'vraag': ('Vraag van directie', 'warning'), 'goedgekeurd': ('Goedgekeurd', 'success'), 'geweigerd': ('Geweigerd', 'danger'), 'geannuleerd': ('Geannuleerd', ''), } DAGDELEN = {'dag': 'Hele dag', 'vm': 'Voormiddag', 'nm': 'Namiddag', 'uren': 'Bepaalde lesuren'} # Statussen die in het dagoverzicht meetellen als "is weg" ACTIEF = ('gemeld', 'goedgekeurd') def _lijst(waarde): return [x.strip().lower() for x in (waarde or '').replace(';', ',').split(',') if x.strip()] # ---------------- rollen ---------------- def directie(): return _lijst(Config.PERSONEEL_DIRECTIE) def secretariaat(): return _lijst(Config.PERSONEEL_SECRETARIAAT) def rollen(sess): """Rollen in de personeelsmodule: via de e-maillijsten van de school of via de PIVOT-rechten 'personeel_directie' / 'personeel_secretariaat' (Gebruikers & rechten).""" from rechten import heeft_recht, is_admin email = (sess.get('user') or '').lower() admin = is_admin() r = {'leerkracht'} if email in directie() or (heeft_recht('personeel_directie') and (not admin or not directie())): r.add('directie') if email in secretariaat() or heeft_recht('personeel_secretariaat'): r.add('secretariaat') return r _ou_cache = {} def is_personeelslid(email): """True als het Google-account in een van de personeels-OU's zit (cache 10 min).""" email = (email or '').lower() school = huidige_school() if email.rsplit('@', 1)[-1] not in school.domeinen: return False sleutel = (school.code, email) hit = _ou_cache.get(sleutel) if hit and time.time() - hit[0] < 600: return hit[1] ok = False try: from services.google_service import GoogleAdminService u = GoogleAdminService().get_user_info(email) or {} ou = u.get('orgUnitPath', '') ok = (not u.get('suspended')) and any(ou == p or ou.startswith(p.rstrip('/') + '/') for p in [x.strip() for x in Config.PERSONEEL_OUS.split(',') if x.strip()]) except Exception as e: sys.stderr.write(f"Personeel: OU-controle mislukt voor {email}: {e}\n") _ou_cache[sleutel] = (time.time(), ok) return ok # ---------------- klassen uit Smartschool ---------------- def klassen(): """OfficiΓ«le klassen uit Smartschool (cache 6 uur, per school). Lege lijst als het niet lukt.""" _klassen_cache = huidige_school().object('klassen_cache', lambda: {'t': 0, 'lijst': []}) if _klassen_cache['lijst'] and time.time() - _klassen_cache['t'] < 6 * 3600: return _klassen_cache['lijst'] try: from services.smartschool_api import SmartschoolService ss = SmartschoolService(platform=Config.SS_API_PLATFORM, webservice_pwd=Config.SS_API_PW) raw = ss.client.service.getClassListJson(Config.SS_API_PW) data = json.loads(raw) if isinstance(raw, str) else raw if isinstance(data, dict): data = list(data.values()) namen = [] for k in data or []: if not isinstance(k, dict): continue officieel = k.get('isKlas', k.get('isOfficial', True)) if str(officieel).lower() in ('false', '0', 'none'): continue naam = (k.get('name') or k.get('code') or '').strip() if naam: namen.append(naam) lijst = sorted(set(namen), key=lambda n: (len(n) > 3, n)) if lijst: _klassen_cache.update(t=time.time(), lijst=lijst) return lijst except Exception as e: sys.stderr.write(f"Personeel: klassenlijst uit Smartschool mislukt: {e}\n") return _klassen_cache['lijst'] # ---------------- database ---------------- def ensure_tables(): execute_query(""" CREATE TABLE IF NOT EXISTS personeel_aanvragen ( ID INT AUTO_INCREMENT PRIMARY KEY, Soort VARCHAR(20) NOT NULL, Status VARCHAR(20) NOT NULL, Email VARCHAR(255) NOT NULL, Naam VARCHAR(255) NULL, Ingediend_door VARCHAR(255) NULL, Titel VARCHAR(255) NOT NULL, Omschrijving TEXT NULL, Locatie VARCHAR(255) NULL, Van_datum DATE NOT NULL, Tot_datum DATE NOT NULL, Dagdeel VARCHAR(10) NOT NULL DEFAULT 'dag', Lesuren VARCHAR(50) NULL, Klassen TEXT NULL, Begeleiders TEXT NULL, Kost DECIMAL(10,2) NULL, Vervanging_ok TINYINT(1) NOT NULL DEFAULT 0, Vervanging_info VARCHAR(255) NULL, Aangemaakt DATETIME NOT NULL, Bijgewerkt DATETIME NOT NULL, INDEX idx_status (Status), INDEX idx_datum (Van_datum, Tot_datum), INDEX idx_email (Email) )""", commit=True) execute_query(""" CREATE TABLE IF NOT EXISTS personeel_reacties ( ID INT AUTO_INCREMENT PRIMARY KEY, Aanvraag_ID INT NOT NULL, Tijdstip DATETIME NOT NULL, Email VARCHAR(255) NULL, Naam VARCHAR(255) NULL, Actie VARCHAR(30) NOT NULL, Tekst TEXT NULL, INDEX idx_aanvraag (Aanvraag_ID) )""", commit=True) def _reactie(aanvraag_id, email, naam, actie, tekst=None): execute_query("INSERT INTO personeel_reacties (Aanvraag_ID, Tijdstip, Email, Naam, Actie, Tekst) " "VALUES (%s, NOW(), %s, %s, %s, %s)", (aanvraag_id, email, naam, actie, tekst), commit=True) def aanmaken(data, door_email, door_naam): soort = data['soort'] status = 'ingediend' if SOORTEN[soort]['goedkeuring'] else 'gemeld' conn = get_db_connection() cur = conn.cursor() try: cur.execute(""" INSERT INTO personeel_aanvragen (Soort, Status, Email, Naam, Ingediend_door, Titel, Omschrijving, Locatie, Van_datum, Tot_datum, Dagdeel, Lesuren, Klassen, Begeleiders, Kost, Aangemaakt, Bijgewerkt) VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,NOW(),NOW())""", (soort, status, data['email'], data['naam'], door_email if door_email != data['email'] else None, data['titel'], data.get('omschrijving'), data.get('locatie'), data['van'], data['tot'], data['dagdeel'], data.get('lesuren'), ', '.join(data.get('klassen') or []), data.get('begeleiders'), data.get('kost'))) conn.commit() aid = cur.lastrowid finally: cur.close() conn.close() tekst = None if door_email == data['email'] else f"Gemeld door {door_naam or door_email}" _reactie(aid, door_email, door_naam, 'gemeld' if status == 'gemeld' else 'ingediend', tekst) return aid def get(aid): rows = execute_query("SELECT * FROM personeel_aanvragen WHERE ID = %s", (aid,)) return rows[0] if rows else None def reacties(aid): return execute_query( "SELECT Tijdstip, Email, Naam, Actie, Tekst FROM personeel_reacties WHERE Aanvraag_ID = %s ORDER BY Tijdstip, ID", (aid,)) def wijzig_status(aid, nieuwe_status, door_email, door_naam, actie, tekst=None): execute_query("UPDATE personeel_aanvragen SET Status = %s, Bijgewerkt = NOW() WHERE ID = %s", (nieuwe_status, aid), commit=True) _reactie(aid, door_email, door_naam, actie, tekst) def commentaar(aid, door_email, door_naam, tekst): execute_query("UPDATE personeel_aanvragen SET Bijgewerkt = NOW() WHERE ID = %s", (aid,), commit=True) _reactie(aid, door_email, door_naam, 'commentaar', tekst) def vervanging(aid, ok, info, door_email, door_naam): execute_query("UPDATE personeel_aanvragen SET Vervanging_ok = %s, Vervanging_info = %s, Bijgewerkt = NOW() " "WHERE ID = %s", (1 if ok else 0, (info or '')[:255] or None, aid), commit=True) _reactie(aid, door_email, door_naam, 'vervanging', ('Vervanging geregeld' if ok else 'Vervanging nog niet geregeld') + (f": {info}" if info else '')) def mijn(email): return execute_query("SELECT * FROM personeel_aanvragen WHERE Email = %s ORDER BY Van_datum DESC, ID DESC LIMIT 200", (email,)) def te_beoordelen(): return execute_query("SELECT * FROM personeel_aanvragen WHERE Status IN ('ingediend','vraag') " "ORDER BY Status = 'vraag', Van_datum, ID") def recent_beslist(limit=20): return execute_query("SELECT * FROM personeel_aanvragen WHERE Status IN ('goedgekeurd','geweigerd') " "AND Soort <> 'afwezigheid' ORDER BY Bijgewerkt DESC LIMIT %s", (int(limit),)) def periode(van, tot, statussen=ACTIEF): ph = ','.join(['%s'] * len(statussen)) return execute_query( f"SELECT * FROM personeel_aanvragen WHERE Status IN ({ph}) AND Van_datum <= %s AND Tot_datum >= %s " "ORDER BY Van_datum, Naam", tuple(statussen) + (tot, van)) def aantal_te_beoordelen(): try: return execute_query("SELECT COUNT(*) AS n FROM personeel_aanvragen WHERE Status = 'ingediend'")[0]['n'] except Exception: return 0 # ---------------- dagoverzicht ---------------- def dagoverzicht(van, tot): """Per dag: wie is weg en welke klassen zijn weg (enkel gemeld/goedgekeurd), plus wat nog wacht.""" items = periode(van, tot) wachtend = periode(van, tot, ('ingediend', 'vraag')) dagen = [] d = van while d <= tot: if d.weekday() < 5: vandaag = [i for i in items if i['Van_datum'] <= d <= i['Tot_datum']] personen, klassen_weg = [], [] for i in vandaag: personen.append({'naam': i['Naam'] or i['Email'], 'email': i['Email'], 'item': i, 'rol': None}) if i['Soort'] == 'uitstap': for b in [x.strip() for x in (i['Begeleiders'] or '').split(',') if x.strip()]: personen.append({'naam': b, 'email': None, 'item': i, 'rol': 'begeleider'}) for k in [x.strip() for x in (i['Klassen'] or '').split(',') if x.strip()]: klassen_weg.append({'klas': k, 'item': i}) dagen.append({ 'datum': d, 'personen': personen, 'klassen': sorted(klassen_weg, key=lambda k: k['klas']), 'wachtend': [w for w in wachtend if w['Van_datum'] <= d <= w['Tot_datum']], 'open_vervanging': sum(1 for i in vandaag if i['Klassen'] and i['Soort'] != 'uitstap' and not i['Vervanging_ok']), }) d += timedelta(days=1) return dagen # ---------------- e-mail ---------------- def _mail_async(ontvangers, onderwerp, html): ontvangers = sorted({o for o in ontvangers if o and '@' in o}) if not Config.PERSONEEL_MAILS or not ontvangers: return def verstuur(): try: from services.google_service import GoogleAdminService svc = GoogleAdminService() for o in ontvangers: ok, msg = svc.send_custom_mail(o, onderwerp, html) if not ok: sys.stderr.write(f"Personeel: mail naar {o} mislukt: {msg}\n") except Exception as e: sys.stderr.write(f"Personeel: mail mislukt: {e}\n") threading.Thread(target=met_school(verstuur), daemon=True).start() def _mailtekst(item, intro, link, extra=None): soort = SOORTEN[item['Soort']] periode_txt = item['Van_datum'].strftime('%d/%m/%Y') if item['Tot_datum'] != item['Van_datum']: periode_txt += ' β ' + item['Tot_datum'].strftime('%d/%m/%Y') dagdeel = DAGDELEN.get(item['Dagdeel'], '') if item['Dagdeel'] == 'uren' and item['Lesuren']: dagdeel += f" ({item['Lesuren']})" rijen = [('Wie', item['Naam'] or item['Email']), ('Wat', f"{soort['icoon']} {soort['label']}: {item['Titel']}"), ('Wanneer', f"{periode_txt}, {dagdeel}")] if item['Klassen']: rijen.append(('Klassen', item['Klassen'])) if item['Begeleiders']: rijen.append(('Begeleiders', item['Begeleiders'])) if item['Kost']: rijen.append(('Kost', f"β¬ {item['Kost']}")) tabel = ''.join(f"
{extra}
" if extra else '' return (f"") def secretariaat_adressen(): return _lijst(Config.PERSONEEL_SECRETARIAAT_MAIL) or secretariaat() def mail_nieuw(item, link): soort = SOORTEN[item['Soort']] if soort['goedkeuring']: _mail_async(directie(), f"Nieuwe aanvraag {soort['label'].lower()}: {item['Naam']} β {item['Titel']}", _mailtekst(item, f"Er is een nieuwe aanvraag voor een {soort['label'].lower()} die je goedkeuring vraagt.", link)) else: _mail_async(secretariaat_adressen(), f"Afwezig: {item['Naam']} ({item['Van_datum']:%d/%m})", _mailtekst(item, "Er is een afwezigheid gemeld.", link, item['Omschrijving'])) def mail_beslissing(item, beslissing, tekst, link): soort = SOORTEN[item['Soort']] woorden = {'goedgekeurd': 'is goedgekeurd β ', 'geweigerd': 'is geweigerd β', 'vraag': 'heeft een vraag van de directie β'} _mail_async([item['Email']], f"Je aanvraag {soort['label'].lower()} {woorden[beslissing]}", _mailtekst(item, f"Je aanvraag {item['Titel']} {woorden[beslissing]}.", link, f"Commentaar: {tekst}" if tekst else None)) if beslissing == 'goedgekeurd': _mail_async(secretariaat_adressen(), f"Goedgekeurd: {soort['label'].lower()} {item['Naam']} ({item['Van_datum']:%d/%m})", _mailtekst(item, f"Een {soort['label'].lower()} is goedgekeurd en staat nu in het dagoverzicht.", link)) def mail_antwoord(item, tekst, link): _mail_async(directie(), f"Antwoord van {item['Naam']}: {item['Titel']}", _mailtekst(item, "De leerkracht heeft geantwoord op je vraag; de aanvraag wacht opnieuw op je beslissing.", link, tekst)) def mail_geannuleerd(item, link): ontvangers = secretariaat_adressen() if item['Status'] in ('gemeld', 'goedgekeurd') else [] if SOORTEN[item['Soort']]['goedkeuring']: ontvangers = ontvangers + directie() _mail_async(ontvangers, f"Geannuleerd: {SOORTEN[item['Soort']]['label'].lower()} {item['Naam']} ({item['Van_datum']:%d/%m})", _mailtekst(item, "Deze melding/aanvraag is geannuleerd.", link))