Initial commit
This commit is contained in:
commit
536ff43ce4
158 files changed
+30355
No files matched your search
@@ -0,0 +1,318 @@
|
||||
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())
|
||||
Reference in new issue
Block a user