230 lines
10 KiB
Python
230 lines
10 KiB
Python
"""Internaat: lijst van alle internen van de actieve school (Informat Internaat WEB API)."""
|
||
import csv
|
||
import io
|
||
import time
|
||
from datetime import date
|
||
|
||
from flask import Blueprint, render_template, request, Response, redirect, url_for, flash
|
||
|
||
from blueprints.auth import login_required
|
||
from rechten import recht_vereist
|
||
from services.facturatie_service import schooljaren
|
||
from services.internaat_service import InternaatService, InternaatFout
|
||
from tenancy import huidige_school
|
||
|
||
internaat_bp = Blueprint('internaat', __name__)
|
||
|
||
|
||
def _keuzes():
|
||
jaren = schooljaren()
|
||
schooljaar = request.args.get('schooljaar') if request.args.get('schooljaar') in jaren else jaren[0]
|
||
try:
|
||
peildatum = date.fromisoformat(request.args.get('peildatum') or '')
|
||
except ValueError:
|
||
peildatum = date.today()
|
||
return schooljaar, jaren, peildatum
|
||
|
||
|
||
@internaat_bp.route("/internen", methods=["GET"])
|
||
@login_required
|
||
@recht_vereist('internen')
|
||
def internen():
|
||
school = huidige_school()
|
||
schooljaar, jaren, peildatum = _keuzes()
|
||
rijen, fout, opgehaald = [], None, None
|
||
if not school.heeft('INFORMAT_ID', 'INFORMAT_SECRET'):
|
||
fout = "Informat is nog niet ingesteld voor deze school (🔌 Koppelingen › Informat)."
|
||
else:
|
||
try:
|
||
rijen, t = InternaatService().internen(schooljaar, peildatum, vernieuw=bool(request.args.get('vernieuw')))
|
||
opgehaald = time.strftime('%d-%m-%Y %H:%M', time.localtime(t))
|
||
except InternaatFout as e:
|
||
fout = str(e)
|
||
except Exception as e:
|
||
fout = f"Internen ophalen mislukt: {e}"
|
||
internaten = sorted({r['internaat'] for r in rijen})
|
||
return render_template("internen.html", rijen=rijen, fout=fout, opgehaald=opgehaald, schooljaar=schooljaar,
|
||
jaren=jaren, peildatum=peildatum, internaten=internaten, school_naam=school.naam)
|
||
|
||
|
||
@internaat_bp.route("/internen/smartschool.json", methods=["POST"])
|
||
@login_required
|
||
@recht_vereist('internen')
|
||
def internen_smartschool():
|
||
"""Welke internen bestaan als gebruiker in Smartschool? {pid: gebruikersnaam | null}"""
|
||
from flask import jsonify
|
||
from services.internaat_service import smartschool_status
|
||
personen = (request.get_json(silent=True) or {}).get('personen') or []
|
||
try:
|
||
return jsonify(ok=True, status=smartschool_status(personen[:2000]))
|
||
except InternaatFout as e:
|
||
return jsonify(ok=False, fout=str(e))
|
||
except Exception as e:
|
||
return jsonify(ok=False, fout=f"Smartschool kon niet bevraagd worden: {e}")
|
||
|
||
|
||
@internaat_bp.route("/internen/export.csv", methods=["GET"])
|
||
@login_required
|
||
@recht_vereist('internen')
|
||
def export():
|
||
schooljaar, _, peildatum = _keuzes()
|
||
try:
|
||
rijen, _ = InternaatService().internen(schooljaar, peildatum)
|
||
except Exception as e:
|
||
flash(str(e), "error")
|
||
return redirect(url_for('internaat.internen', schooljaar=schooljaar))
|
||
internaat = request.args.get('internaat') or ''
|
||
zoek = (request.args.get('zoek') or '').strip().lower()
|
||
if internaat:
|
||
rijen = [r for r in rijen if r['internaat'] == internaat]
|
||
if zoek:
|
||
rijen = [r for r in rijen if zoek in ' '.join(str(r[k]) for k in ('naam', 'voornaam', 'stamnummer', 'kamer', 'vestiging', 'gemeente')).lower()]
|
||
datum = lambda d: d.strftime('%d/%m/%Y') if d else ''
|
||
buf = io.StringIO()
|
||
w = csv.writer(buf, delimiter=';')
|
||
w.writerow(['Naam', 'Voornaam', 'Geboortedatum', 'Geslacht', 'Stamnummer', 'Internaat', 'Vestiging', 'Kamer',
|
||
'Ingeschreven van', 'Ingeschreven tot', 'Financiering', 'Woonplaats', 'Contact 1', 'Gsm 1', 'E-mail 1',
|
||
'Contact 2', 'Gsm 2', 'E-mail 2', 'Smartschool'])
|
||
ss_cache = huidige_school().cache.get('ss_internen') or {}
|
||
def ss(r):
|
||
hit = ss_cache.get((str(r['id']), r['voornaam'], r['naam']))
|
||
return '' if not hit else (hit[1] or 'niet gevonden')
|
||
for r in rijen:
|
||
o = (r['ouders'] + [{}, {}])[:2]
|
||
w.writerow([r['naam'], r['voornaam'], datum(r['geboortedatum']), r['geslacht'], r['stamnummer'], r['internaat'],
|
||
r['vestiging'], r['kamer'], datum(r['begin']), datum(r['einde']), r['financiering'], r['gemeente']]
|
||
+ sum(([f"{x.get('naam', '')} ({x['type']})" if x.get('type') else x.get('naam', ''), x.get('gsm', ''),
|
||
x.get('email', '')] for x in o), []) + [ss(r)])
|
||
return Response('' + buf.getvalue(), mimetype='text/csv; charset=utf-8', headers={
|
||
'Content-Disposition': f"attachment; filename=Internen_{huidige_school().code}_{peildatum:%Y%m%d}.csv"})
|
||
|
||
|
||
# ---------------------------------------------------------------------
|
||
# Aanwezigheden (register per week: ochtend en avond)
|
||
# ---------------------------------------------------------------------
|
||
from datetime import timedelta
|
||
from flask import jsonify, session
|
||
from services import aanwezigheid_service as aw
|
||
|
||
|
||
def _week():
|
||
try:
|
||
d = date.fromisoformat(request.args.get('week') or '')
|
||
except ValueError:
|
||
d = date.today()
|
||
return aw.maandag_van(d)
|
||
|
||
|
||
def _internen_van_week(maandag):
|
||
"""Internen die in de week ingeschreven zijn (peildatum: vandaag als die in de week valt, anders woensdag)."""
|
||
vandaag = date.today()
|
||
peildatum = vandaag if maandag <= vandaag <= maandag + timedelta(days=6) else maandag + timedelta(days=2)
|
||
rijen, _ = InternaatService().internen(aw.schooljaar_van(peildatum), peildatum)
|
||
meerdere = len({r['internaat'] for r in rijen}) > 1
|
||
for r in rijen:
|
||
r['groep'] = aw.groep_van(r, meerdere)
|
||
r['pid'] = str(r['id'])
|
||
return rijen
|
||
|
||
|
||
@internaat_bp.route("/aanwezigheden", methods=["GET"])
|
||
@login_required
|
||
@recht_vereist('internaat_aanwezigheid')
|
||
def aanwezigheden():
|
||
school = huidige_school()
|
||
maandag = _week()
|
||
rijen, fout, data = [], None, {}
|
||
try:
|
||
rijen = _internen_van_week(maandag)
|
||
data = aw.week(maandag)
|
||
except InternaatFout as e:
|
||
fout = str(e)
|
||
except Exception as e:
|
||
fout = f"Internen ophalen mislukt: {e}"
|
||
groepen = {}
|
||
for r in sorted(rijen, key=lambda r: (r['groep'].lower(), r['naam'].lower(), r['voornaam'].lower())):
|
||
groepen.setdefault(r['groep'], []).append(r)
|
||
dagen = [maandag + timedelta(days=i) for i in range(7)]
|
||
return render_template("aanwezigheden.html", groepen=groepen, data=data, dagen=dagen, maandag=maandag,
|
||
fout=fout, school_naam=school.naam, vandaag=date.today(), dagnamen=aw.DAGEN,
|
||
dagnamen_voluit=aw.DAGEN_VOLUIT, aantal=len(rijen),
|
||
vorige=maandag - timedelta(days=7), volgende=maandag + timedelta(days=7),
|
||
weeknr=maandag.isocalendar()[1])
|
||
|
||
|
||
@internaat_bp.route("/aanwezigheden/zet", methods=["POST"])
|
||
@login_required
|
||
@recht_vereist('internaat_aanwezigheid')
|
||
def aanwezigheid_zet():
|
||
d = request.get_json(silent=True) or {}
|
||
try:
|
||
datum = date.fromisoformat(str(d.get('datum') or ''))
|
||
aw.zet(datum, str(d.get('persoon') or '')[:64], d.get('moment'), d.get('status'), d.get('naam'),
|
||
session.get('naam') or session.get('user'))
|
||
except ValueError as e:
|
||
return jsonify(ok=False, fout=str(e) or 'Ongeldige gegevens.'), 400
|
||
except Exception as e:
|
||
return jsonify(ok=False, fout=f"Bewaren mislukt: {e}"), 500
|
||
return jsonify(ok=True)
|
||
|
||
|
||
@internaat_bp.route("/aanwezigheden/export.xlsx", methods=["GET"])
|
||
@login_required
|
||
@recht_vereist('internaat_aanwezigheid')
|
||
def aanwezigheden_export():
|
||
"""Excel van de week, met dezelfde filters als op het scherm (?groep=&zoek=)."""
|
||
from openpyxl import Workbook
|
||
from openpyxl.styles import Font, PatternFill, Alignment
|
||
maandag = _week()
|
||
try:
|
||
rijen = _internen_van_week(maandag)
|
||
data = aw.week(maandag)
|
||
except Exception as e:
|
||
flash(str(e), "error")
|
||
return redirect(url_for('internaat.aanwezigheden', week=maandag.isoformat()))
|
||
groep, zoek = request.args.get('groep') or '', (request.args.get('zoek') or '').strip().lower()
|
||
rijen = [r for r in rijen if (not groep or r['groep'] == groep)
|
||
and (not zoek or zoek in f"{r['naam']} {r['voornaam']}".lower())]
|
||
rijen.sort(key=lambda r: (r['groep'].lower(), r['naam'].lower(), r['voornaam'].lower()))
|
||
labels = {'aanwezig': 'Aanwezig', 'afwezig': 'Afwezig'}
|
||
kleur = {'aanwezig': PatternFill('solid', fgColor='D7EDDF'), 'afwezig': PatternFill('solid', fgColor='F6D5CF')}
|
||
wb = Workbook()
|
||
ws = wb.active
|
||
ws.title = f"Week {maandag.isocalendar()[1]}"
|
||
kop = ['Naam', 'Voornaam', 'Groep', 'Kamer']
|
||
for i, dag in enumerate(aw.DAGEN_VOLUIT):
|
||
d = maandag + timedelta(days=i)
|
||
kop += [f"{dag} {d:%d/%m} ochtend", f"{dag} {d:%d/%m} avond"]
|
||
ws.append(kop)
|
||
for c in ws[1]:
|
||
c.font = Font(bold=True)
|
||
c.alignment = Alignment(wrap_text=True, vertical='top')
|
||
vorige_groep = None
|
||
for r in rijen:
|
||
if r['groep'] != vorige_groep:
|
||
ws.append([f"Groep: {r['groep']}"])
|
||
ws.cell(ws.max_row, 1).font = Font(bold=True)
|
||
vorige_groep = r['groep']
|
||
rij = [r['naam'], r['voornaam'], r['groep'], r['kamer']]
|
||
for i in range(7):
|
||
for m in aw.MOMENTEN:
|
||
rij.append(labels.get(data.get(f"{r['pid']}:{i}:{m}"), ''))
|
||
ws.append(rij)
|
||
for c in ws[ws.max_row][4:]:
|
||
for k, f in kleur.items():
|
||
if c.value == labels[k]:
|
||
c.fill = f
|
||
ws.column_dimensions['A'].width = 22
|
||
ws.column_dimensions['B'].width = 16
|
||
ws.column_dimensions['C'].width = 16
|
||
ws.column_dimensions['D'].width = 22
|
||
from openpyxl.utils import get_column_letter
|
||
for i in range(5, 5 + 14):
|
||
ws.column_dimensions[get_column_letter(i)].width = 13
|
||
ws.freeze_panes = 'C2'
|
||
buf = io.BytesIO()
|
||
wb.save(buf)
|
||
return Response(buf.getvalue(), mimetype='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||
headers={'Content-Disposition': f"attachment; filename=Aanwezigheden_{huidige_school().code}_"
|
||
f"{maandag:%Y}-W{maandag.isocalendar()[1]:02d}.xlsx"})
|