319 lines
12 KiB
Python
319 lines
12 KiB
Python
from datetime import date, datetime, timedelta
|
|
from decimal import Decimal, InvalidOperation
|
|
from functools import wraps
|
|
|
|
from flask import Blueprint, render_template, request, redirect, url_for, flash, session, abort
|
|
from tenancy import huidige_school, per_school
|
|
from blueprints.auth import login_required
|
|
from services import personeel_service as P
|
|
|
|
personeel_bp = Blueprint('personeel', __name__)
|
|
|
|
_tables_ready = set() # scholen waarvoor de tabellen al gecontroleerd zijn
|
|
|
|
|
|
def _ensure_tables():
|
|
if huidige_school().code not in _tables_ready:
|
|
try:
|
|
P.ensure_tables()
|
|
_tables_ready.add(huidige_school().code)
|
|
except Exception as e:
|
|
flash(f"Kon de personeelstabellen niet aanmaken: {e}", "error")
|
|
|
|
|
|
def _me():
|
|
return (session.get('user') or '').lower(), session.get('naam') or session.get('user')
|
|
|
|
|
|
def rol_vereist(rol):
|
|
def deco(f):
|
|
@wraps(f)
|
|
def wrapped(*a, **kw):
|
|
if rol not in P.rollen(session):
|
|
flash("Je hebt geen toegang tot deze pagina.", "error")
|
|
return redirect(url_for('personeel.mijn'))
|
|
return f(*a, **kw)
|
|
return wrapped
|
|
return deco
|
|
|
|
|
|
@personeel_bp.context_processor
|
|
def _ctx():
|
|
r = P.rollen(session) if session.get('auth') else set()
|
|
return {'p_rollen': r, 'SOORTEN': P.SOORTEN, 'STATUS': P.STATUS, 'DAGDELEN': P.DAGDELEN,
|
|
'p_te_beoordelen': P.aantal_te_beoordelen() if 'directie' in r else 0}
|
|
|
|
|
|
def _link(aid):
|
|
return url_for('personeel.detail', aid=aid, _external=True)
|
|
|
|
|
|
def _mag_zien(item):
|
|
me, _ = _me()
|
|
r = P.rollen(session)
|
|
return (item['Email'] == me or 'secretariaat' in r
|
|
or ('directie' in r and P.SOORTEN[item['Soort']]['goedkeuring']))
|
|
|
|
|
|
def _laad(aid):
|
|
item = P.get(aid)
|
|
if not item:
|
|
abort(404)
|
|
if not _mag_zien(item):
|
|
flash("Je hebt geen toegang tot deze aanvraag.", "error")
|
|
return None
|
|
return item
|
|
|
|
|
|
#---------------------------------------------
|
|
# Mijn meldingen en aanvragen
|
|
#---------------------------------------------
|
|
@personeel_bp.route('/', methods=['GET'])
|
|
@login_required
|
|
def mijn():
|
|
_ensure_tables()
|
|
me, _ = _me()
|
|
try:
|
|
items = P.mijn(me)
|
|
except Exception as e:
|
|
items = []
|
|
flash(f"Kon je aanvragen niet ophalen: {e}", "error")
|
|
return render_template('personeel_mijn.html', items=items, vandaag=date.today())
|
|
|
|
|
|
#---------------------------------------------
|
|
# Nieuwe melding / aanvraag
|
|
#---------------------------------------------
|
|
@personeel_bp.route('/nieuw', methods=['GET', 'POST'])
|
|
@login_required
|
|
def nieuw():
|
|
_ensure_tables()
|
|
soort = request.values.get('soort', 'afwezigheid')
|
|
if soort not in P.SOORTEN:
|
|
soort = 'afwezigheid'
|
|
me, mijn_naam = _me()
|
|
is_secr = 'secretariaat' in P.rollen(session)
|
|
form = request.form
|
|
|
|
if request.method == 'POST':
|
|
fouten = []
|
|
try:
|
|
van = datetime.strptime(form.get('van', ''), '%Y-%m-%d').date()
|
|
tot = datetime.strptime(form.get('tot') or form.get('van', ''), '%Y-%m-%d').date()
|
|
if tot < van:
|
|
fouten.append("De einddatum ligt voor de begindatum.")
|
|
except ValueError:
|
|
van = tot = None
|
|
fouten.append("Kies een geldige datum.")
|
|
|
|
titel = (form.get('titel') or '').strip()
|
|
if soort == 'afwezigheid' and not titel:
|
|
titel = 'Afwezig'
|
|
if not titel:
|
|
fouten.append("Geef een titel op.")
|
|
|
|
dagdeel = form.get('dagdeel', 'dag')
|
|
if dagdeel not in P.DAGDELEN:
|
|
dagdeel = 'dag'
|
|
lesuren = (form.get('lesuren') or '').strip()
|
|
if dagdeel == 'uren' and not lesuren:
|
|
fouten.append("Vul in welke lesuren het gaat (bv. 3-5).")
|
|
|
|
klassen = form.getlist('klassen')
|
|
extra = [k.strip() for k in (form.get('klassen_extra') or '').split(',') if k.strip()]
|
|
klassen = list(dict.fromkeys(klassen + extra))
|
|
if soort == 'uitstap' and not klassen:
|
|
fouten.append("Kies welke klassen mee op uitstap gaan.")
|
|
|
|
kost = None
|
|
if (form.get('kost') or '').strip():
|
|
try:
|
|
kost = Decimal(form['kost'].replace(',', '.').replace('€', '').strip())
|
|
except InvalidOperation:
|
|
fouten.append("De kost is geen geldig bedrag.")
|
|
|
|
# Secretariaat kan melden namens een leerkracht
|
|
email, naam = me, mijn_naam
|
|
namens = (form.get('namens') or '').strip().lower()
|
|
if namens and namens != me:
|
|
if not is_secr:
|
|
fouten.append("Je kan enkel voor jezelf een melding maken.")
|
|
else:
|
|
email, naam = namens, namens
|
|
try:
|
|
from services.google_service import GoogleAdminService
|
|
u = GoogleAdminService().get_user_info(namens)
|
|
if not u:
|
|
fouten.append(f"{namens} bestaat niet in Google Workspace.")
|
|
else:
|
|
email = u['primaryEmail'].lower()
|
|
naam = (u.get('name') or {}).get('fullName') or email
|
|
except Exception:
|
|
pass
|
|
|
|
if fouten:
|
|
for f in fouten:
|
|
flash(f, "error")
|
|
else:
|
|
data = dict(soort=soort, email=email, naam=naam, titel=titel[:255],
|
|
omschrijving=(form.get('omschrijving') or '').strip() or None,
|
|
locatie=(form.get('locatie') or '').strip()[:255] or None,
|
|
van=van, tot=tot, dagdeel=dagdeel, lesuren=lesuren[:50] or None,
|
|
klassen=klassen, begeleiders=(form.get('begeleiders') or '').strip() or None, kost=kost)
|
|
aid = P.aanmaken(data, me, mijn_naam)
|
|
P.mail_nieuw(P.get(aid), _link(aid))
|
|
if P.SOORTEN[soort]['goedkeuring']:
|
|
flash("Je aanvraag is ingediend. De directie krijgt een melding; je ontvangt een mail zodra er een beslissing is.", "success")
|
|
else:
|
|
flash("De afwezigheid is gemeld aan het secretariaat.", "success")
|
|
return redirect(url_for('personeel.detail', aid=aid))
|
|
|
|
return render_template('personeel_nieuw.html', soort=soort, form=form, klassen=P.klassen(),
|
|
is_secr=is_secr, vandaag=date.today())
|
|
|
|
|
|
#---------------------------------------------
|
|
# Detail + acties
|
|
#---------------------------------------------
|
|
@personeel_bp.route('/<int:aid>', methods=['GET'])
|
|
@login_required
|
|
def detail(aid):
|
|
_ensure_tables()
|
|
item = _laad(aid)
|
|
if item is None:
|
|
return redirect(url_for('personeel.mijn'))
|
|
me, _ = _me()
|
|
return render_template('personeel_detail.html', item=item, reacties=P.reacties(aid),
|
|
is_eigenaar=item['Email'] == me, vandaag=date.today())
|
|
|
|
|
|
@personeel_bp.route('/<int:aid>/beslissing', methods=['POST'])
|
|
@login_required
|
|
@rol_vereist('directie')
|
|
def beslissing(aid):
|
|
item = _laad(aid)
|
|
if item is None:
|
|
return redirect(url_for('personeel.mijn'))
|
|
actie = request.form.get('actie')
|
|
tekst = (request.form.get('tekst') or '').strip()
|
|
if actie not in ('goedgekeurd', 'geweigerd', 'vraag'):
|
|
abort(400)
|
|
if not P.SOORTEN[item['Soort']]['goedkeuring'] or item['Status'] not in ('ingediend', 'vraag'):
|
|
flash("Over deze aanvraag kan geen beslissing (meer) genomen worden.", "error")
|
|
return redirect(url_for('personeel.detail', aid=aid))
|
|
if actie in ('geweigerd', 'vraag') and not tekst:
|
|
flash("Geef wat uitleg mee bij een weigering of vraag.", "error")
|
|
return redirect(url_for('personeel.detail', aid=aid))
|
|
me, naam = _me()
|
|
P.wijzig_status(aid, actie, me, naam, actie, tekst or None)
|
|
P.mail_beslissing(P.get(aid), actie, tekst, _link(aid))
|
|
flash({'goedgekeurd': "Aanvraag goedgekeurd.", 'geweigerd': "Aanvraag geweigerd.",
|
|
'vraag': "Je vraag is naar de leerkracht gestuurd."}[actie], "success")
|
|
nxt = request.form.get('volgende')
|
|
return redirect(url_for('personeel.beoordelen') if nxt else url_for('personeel.detail', aid=aid))
|
|
|
|
|
|
@personeel_bp.route('/<int:aid>/antwoord', methods=['POST'])
|
|
@login_required
|
|
def antwoord(aid):
|
|
item = _laad(aid)
|
|
if item is None:
|
|
return redirect(url_for('personeel.mijn'))
|
|
me, naam = _me()
|
|
tekst = (request.form.get('tekst') or '').strip()
|
|
if item['Email'] != me or item['Status'] != 'vraag' or not tekst:
|
|
flash("Antwoorden kan enkel op je eigen aanvraag met een openstaande vraag.", "error")
|
|
return redirect(url_for('personeel.detail', aid=aid))
|
|
P.wijzig_status(aid, 'ingediend', me, naam, 'antwoord', tekst)
|
|
P.mail_antwoord(P.get(aid), tekst, _link(aid))
|
|
flash("Je antwoord is verstuurd; de aanvraag wacht opnieuw op de directie.", "success")
|
|
return redirect(url_for('personeel.detail', aid=aid))
|
|
|
|
|
|
@personeel_bp.route('/<int:aid>/commentaar', methods=['POST'])
|
|
@login_required
|
|
def commentaar(aid):
|
|
item = _laad(aid)
|
|
if item is None:
|
|
return redirect(url_for('personeel.mijn'))
|
|
tekst = (request.form.get('tekst') or '').strip()
|
|
if tekst:
|
|
me, naam = _me()
|
|
P.commentaar(aid, me, naam, tekst)
|
|
return redirect(url_for('personeel.detail', aid=aid))
|
|
|
|
|
|
@personeel_bp.route('/<int:aid>/annuleren', methods=['POST'])
|
|
@login_required
|
|
def annuleren(aid):
|
|
item = _laad(aid)
|
|
if item is None:
|
|
return redirect(url_for('personeel.mijn'))
|
|
me, naam = _me()
|
|
if not (item['Email'] == me or 'secretariaat' in P.rollen(session)):
|
|
flash("Je kan deze aanvraag niet annuleren.", "error")
|
|
elif item['Status'] in ('geannuleerd', 'geweigerd'):
|
|
flash("Deze aanvraag is al afgesloten.", "error")
|
|
else:
|
|
P.mail_geannuleerd(item, _link(aid))
|
|
P.wijzig_status(aid, 'geannuleerd', me, naam, 'geannuleerd', (request.form.get('tekst') or '').strip() or None)
|
|
flash("Geannuleerd.", "success")
|
|
return redirect(url_for('personeel.detail', aid=aid))
|
|
|
|
|
|
@personeel_bp.route('/<int:aid>/vervanging', methods=['POST'])
|
|
@login_required
|
|
@rol_vereist('secretariaat')
|
|
def vervanging(aid):
|
|
item = _laad(aid)
|
|
if item is None:
|
|
return redirect(url_for('personeel.mijn'))
|
|
me, naam = _me()
|
|
P.vervanging(aid, request.form.get('ok') == '1', request.form.get('info'), me, naam)
|
|
terug = request.form.get('terug')
|
|
return redirect(terug if terug and terug.startswith('/') else url_for('personeel.detail', aid=aid))
|
|
|
|
|
|
#---------------------------------------------
|
|
# Directie: te beoordelen
|
|
#---------------------------------------------
|
|
@personeel_bp.route('/beoordelen', methods=['GET'])
|
|
@login_required
|
|
@rol_vereist('directie')
|
|
def beoordelen():
|
|
_ensure_tables()
|
|
return render_template('personeel_beoordelen.html', open_items=P.te_beoordelen(), recent=P.recent_beslist())
|
|
|
|
|
|
#---------------------------------------------
|
|
# Secretariaat: dagoverzicht
|
|
#---------------------------------------------
|
|
@personeel_bp.route('/dagoverzicht', methods=['GET'])
|
|
@login_required
|
|
@rol_vereist('secretariaat')
|
|
def dagoverzicht():
|
|
_ensure_tables()
|
|
try:
|
|
dag = datetime.strptime(request.args.get('datum', ''), '%Y-%m-%d').date()
|
|
except ValueError:
|
|
dag = date.today()
|
|
weergave = request.args.get('weergave', 'dag')
|
|
if weergave == 'week':
|
|
van = dag - timedelta(days=dag.weekday())
|
|
tot = van + timedelta(days=4)
|
|
stap = timedelta(days=7)
|
|
else:
|
|
weergave = 'dag'
|
|
if dag.weekday() >= 5: # weekend → volgende maandag
|
|
dag = dag + timedelta(days=7 - dag.weekday())
|
|
van = tot = dag
|
|
stap = timedelta(days=1)
|
|
vorige, volgende = van - stap, van + stap
|
|
if weergave == 'dag':
|
|
while vorige.weekday() >= 5:
|
|
vorige -= timedelta(days=1)
|
|
while volgende.weekday() >= 5:
|
|
volgende += timedelta(days=1)
|
|
return render_template('personeel_dagoverzicht.html', dagen=P.dagoverzicht(van, tot), weergave=weergave,
|
|
dag=dag, van=van, tot=tot, vorige=vorige, volgende=volgende, vandaag=date.today())
|