Compare commits
2
Commits
cc7e8b7593
...
9f6549e0e1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9f6549e0e1 | ||
|
|
8a53234eb6 |
No files matched your search
+19
@@ -0,0 +1,19 @@
|
|||||||
|
# Geheimen: NOOIT in git (wachtwoorden, sleutels, Google-serviceaccounts)
|
||||||
|
config.ini
|
||||||
|
geheimen/
|
||||||
|
import/
|
||||||
|
.env
|
||||||
|
*api_key*.json
|
||||||
|
|
||||||
|
# Python
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
venv/
|
||||||
|
.venv/
|
||||||
|
|
||||||
|
# Logboeken en tijdelijke bestanden
|
||||||
|
*.log
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
@@ -117,6 +117,7 @@ python beheer.py rechten jan sg-antwerpen alles | geen
|
|||||||
| `gopro` | GO! Pro: personeel van de school uit Informat, rollen en export (blok *Personeel*) |
|
| `gopro` | GO! Pro: personeel van de school uit Informat, rollen en export (blok *Personeel*) |
|
||||||
| `koppelingen` | Koppelingen van deze school |
|
| `koppelingen` | Koppelingen van deze school |
|
||||||
| `gebruikersbeheer` | Gebruikers & rechten van deze school |
|
| `gebruikersbeheer` | Gebruikers & rechten van deze school |
|
||||||
|
| `database_toestellen` | Beheer › Database: toestellen importeren uit Excel |
|
||||||
|
|
||||||
Een nieuw onderdeel voeg je toe in `rechten.py › ONDERDELEN`. Bij het opstarten komt het automatisch in de tabel `Onderdelen`.
|
Een nieuw onderdeel voeg je toe in `rechten.py › ONDERDELEN`. Bij het opstarten komt het automatisch in de tabel `Onderdelen`.
|
||||||
|
|
||||||
@@ -159,6 +160,9 @@ Wat PIVOT extra nodig heeft, staat in `sql/school.sql` gemarkeerd met `[+]`.
|
|||||||
`Geblokkeerd` (1 = geblokkeerd in Google Admin) wordt vanzelf toegevoegd; de knop in 💻 Alle toestellen en 🔒 bij Eigendom
|
`Geblokkeerd` (1 = geblokkeerd in Google Admin) wordt vanzelf toegevoegd; de knop in 💻 Alle toestellen en 🔒 bij Eigendom
|
||||||
houden hem bij, en "Chromebooks ophalen uit Google" neemt de status uit Google over.
|
houden hem bij, en "Chromebooks ophalen uit Google" neemt de status uit Google over.
|
||||||
`Schooleigen_ID` is het toestelnummer van de school; in de code heette dat vroeger `KAMXM_nummer`.
|
`Schooleigen_ID` is het toestelnummer van de school; in de code heette dat vroeger `KAMXM_nummer`.
|
||||||
|
Aanvullen kan ook in bulk via **⚙️ Beheer › 🗄️ Database › Toestellen importeren**: sjabloon downloaden, Excel uploaden,
|
||||||
|
voorbeeld nakijken en bevestigen. Toestellen worden herkend aan `Schooleigen_ID` (anders `Serienummer`); er wordt nooit
|
||||||
|
iets verwijderd en een lege cel wist niets (`services/toestellen_import.py`).
|
||||||
- **Signpost_DB**: één rij per toestel (`Toestel_ID` = sleutel). Het veld "Aankoopberdrag" heet `Aankoopbedrag`.
|
- **Signpost_DB**: één rij per toestel (`Toestel_ID` = sleutel). Het veld "Aankoopberdrag" heet `Aankoopbedrag`.
|
||||||
De synchronisatie met Signpost maakt een toestel aan als het serienummer nog niet bestaat.
|
De synchronisatie met Signpost maakt een toestel aan als het serienummer nog niet bestaat.
|
||||||
- **Opmerkingen** en **Signpost_herstellingen_DB** verwijzen naar `Toestellen.Toestel_ID`. Dat is hetzelfde nummer als
|
- **Opmerkingen** en **Signpost_herstellingen_DB** verwijzen naar `Toestellen.Toestel_ID`. Dat is hetzelfde nummer als
|
||||||
|
|||||||
@@ -152,6 +152,8 @@ def create_app():
|
|||||||
app.register_blueprint(verzendlijsten_bp, url_prefix='/google/verzendlijsten')
|
app.register_blueprint(verzendlijsten_bp, url_prefix='/google/verzendlijsten')
|
||||||
from blueprints.ipblokkade_bp import ipblokkade_bp
|
from blueprints.ipblokkade_bp import ipblokkade_bp
|
||||||
app.register_blueprint(ipblokkade_bp, url_prefix='/beheer/ip-blokkades')
|
app.register_blueprint(ipblokkade_bp, url_prefix='/beheer/ip-blokkades')
|
||||||
|
from blueprints.database_bp import database_bp
|
||||||
|
app.register_blueprint(database_bp, url_prefix='/beheer/database') # Beheer › Database (Excel-import)
|
||||||
|
|
||||||
#------------------------------------------------------
|
#------------------------------------------------------
|
||||||
# Welke school? (per request)
|
# Welke school? (per request)
|
||||||
|
|||||||
@@ -0,0 +1,100 @@
|
|||||||
|
"""
|
||||||
|
Beheer › Database: tabellen van de school aanvullen vanuit Excel.
|
||||||
|
|
||||||
|
Toestellen:
|
||||||
|
GET /beheer/database/toestellen → pagina (upload + sjabloon)
|
||||||
|
GET /beheer/database/toestellen/sjabloon.xlsx → sjabloon downloaden
|
||||||
|
POST /beheer/database/toestellen/controleren → Excel lezen en voorbeeld tonen (niets bewaard)
|
||||||
|
POST /beheer/database/toestellen/importeren → bevestigen: het voorbeeld uitvoeren
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
|
||||||
|
from flask import Blueprint, render_template, request, Response, redirect, url_for, flash, session
|
||||||
|
|
||||||
|
from blueprints.auth import login_required
|
||||||
|
from database import execute_query
|
||||||
|
from rechten import recht_vereist, log
|
||||||
|
from services import toestellen_import as ti
|
||||||
|
from tenancy import huidige_school
|
||||||
|
|
||||||
|
database_bp = Blueprint('database_beheer', __name__)
|
||||||
|
|
||||||
|
|
||||||
|
@database_bp.route('/toestellen', methods=['GET'])
|
||||||
|
@login_required
|
||||||
|
@recht_vereist('database_toestellen')
|
||||||
|
def toestellen():
|
||||||
|
try:
|
||||||
|
aantal = execute_query("SELECT COUNT(*) AS n FROM Toestellen")[0]['n']
|
||||||
|
except Exception:
|
||||||
|
aantal = None
|
||||||
|
return render_template('database_toestellen.html', plan=None, aantal=aantal, kolommen=ti.KOLOMMEN,
|
||||||
|
statussen=ti.STATUSSEN, max_rijen=ti.MAX_RIJEN)
|
||||||
|
|
||||||
|
|
||||||
|
@database_bp.route('/toestellen/sjabloon.xlsx', methods=['GET'])
|
||||||
|
@login_required
|
||||||
|
@recht_vereist('database_toestellen')
|
||||||
|
def toestellen_sjabloon():
|
||||||
|
school = huidige_school()
|
||||||
|
data = ti.maak_sjabloon(school.naam)
|
||||||
|
return Response(data, mimetype='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||||
|
headers={'Content-Disposition': f"attachment; filename=Sjabloon_toestellen_{school.code}.xlsx"})
|
||||||
|
|
||||||
|
|
||||||
|
@database_bp.route('/toestellen/controleren', methods=['POST'])
|
||||||
|
@login_required
|
||||||
|
@recht_vereist('database_toestellen')
|
||||||
|
def toestellen_controleren():
|
||||||
|
terug = redirect(url_for('database_beheer.toestellen'))
|
||||||
|
bestand = request.files.get('bestand')
|
||||||
|
if not bestand or not bestand.filename:
|
||||||
|
flash("Kies eerst een Excel-bestand.", "error")
|
||||||
|
return terug
|
||||||
|
if not bestand.filename.lower().endswith(('.xlsx', '.xlsm')):
|
||||||
|
flash("Enkel Excel-bestanden (.xlsx) kunnen ingelezen worden. Sla een .xls of .csv eerst op als .xlsx.", "error")
|
||||||
|
return terug
|
||||||
|
modus = 'overschrijven' if request.form.get('modus') == 'overschrijven' else 'aanvullen'
|
||||||
|
try:
|
||||||
|
rijen, onbekend = ti.lees_excel(bestand)
|
||||||
|
except ti.ImportFout as e:
|
||||||
|
flash(str(e), "error")
|
||||||
|
return terug
|
||||||
|
if not rijen:
|
||||||
|
flash("Het bestand bevat geen toestellen (enkel kolomkoppen of lege rijen).", "warning")
|
||||||
|
return terug
|
||||||
|
try:
|
||||||
|
plan = ti.maak_plan(rijen, modus)
|
||||||
|
except Exception as e:
|
||||||
|
flash(f"Controleren mislukt: {e}", "error")
|
||||||
|
return terug
|
||||||
|
return render_template('database_toestellen.html', plan=plan, bestandsnaam=bestand.filename, onbekend=onbekend,
|
||||||
|
rijen_json=json.dumps(rijen, ensure_ascii=False), kolommen=ti.KOLOMMEN,
|
||||||
|
statussen=ti.STATUSSEN, max_rijen=ti.MAX_RIJEN, aantal=None)
|
||||||
|
|
||||||
|
|
||||||
|
@database_bp.route('/toestellen/importeren', methods=['POST'])
|
||||||
|
@login_required
|
||||||
|
@recht_vereist('database_toestellen')
|
||||||
|
def toestellen_importeren():
|
||||||
|
terug = redirect(url_for('database_beheer.toestellen'))
|
||||||
|
modus = 'overschrijven' if request.form.get('modus') == 'overschrijven' else 'aanvullen'
|
||||||
|
try:
|
||||||
|
rijen = json.loads(request.form.get('rijen') or '[]')
|
||||||
|
if not isinstance(rijen, list) or len(rijen) > ti.MAX_RIJEN or not all(isinstance(r, dict) for r in rijen):
|
||||||
|
raise ValueError
|
||||||
|
except ValueError:
|
||||||
|
flash("De gegevens van het voorbeeld zijn ongeldig. Upload het bestand opnieuw.", "error")
|
||||||
|
return terug
|
||||||
|
try:
|
||||||
|
plan, nieuw, bijgewerkt = ti.voer_uit(rijen, modus, session.get('user'))
|
||||||
|
except Exception as e:
|
||||||
|
flash(f"Importeren mislukt, er is niets bewaard: {e}", "error")
|
||||||
|
return terug
|
||||||
|
t = plan['tellers']
|
||||||
|
log(f"Toestellen geïmporteerd uit Excel: {nieuw} nieuw, {bijgewerkt} bijgewerkt ({modus})")
|
||||||
|
ongewijzigd = f", {t['ongewijzigd']} ongewijzigd" if t['ongewijzigd'] else ""
|
||||||
|
flash(f"Import klaar: {nieuw} toestel(len) toegevoegd, {bijgewerkt} bijgewerkt{ongewijzigd}.", "success")
|
||||||
|
if t['fout']:
|
||||||
|
flash(f"{t['fout']} rij(en) met een fout werden overgeslagen.", "warning")
|
||||||
|
return terug
|
||||||
@@ -290,8 +290,11 @@ def verzameltabel():
|
|||||||
lijst = _lijst(request.args.get('lijst'))
|
lijst = _lijst(request.args.get('lijst'))
|
||||||
rijen = gp.verzameld_alle(soort=LIJSTEN[lijst])
|
rijen = gp.verzameld_alle(soort=LIJSTEN[lijst])
|
||||||
scholen = sorted({(r['School_code'], r['School_naam'] or r['School_code']) for r in rijen}, key=lambda x: x[1].lower())
|
scholen = sorted({(r['School_code'], r['School_naam'] or r['School_code']) for r in rijen}, key=lambda x: x[1].lower())
|
||||||
aantal = {k: (len(rijen) if k == lijst else len(gp.verzameld_alle(soort=v))) for k, v in LIJSTEN.items()}
|
per_soort = gp.aantallen()
|
||||||
return render_template("gopro_verzameld.html", rijen=rijen, scholen=scholen, rollen=gp.ROLLEN,
|
aantal = {k: (len(rijen) if k == lijst else per_soort.get(v, 0)) for k, v in LIJSTEN.items()}
|
||||||
|
from collections import Counter
|
||||||
|
per_school = Counter(r['School_code'] for r in rijen)
|
||||||
|
return render_template("gopro_verzameld.html", rijen=rijen, scholen=scholen, rollen=gp.ROLLEN, per_school=per_school,
|
||||||
huidige=huidige_school().code, lijst=lijst, rood=lijst == 'rood',
|
huidige=huidige_school().code, lijst=lijst, rood=lijst == 'rood',
|
||||||
verzonden=lijst == 'verzonden', aantal=aantal,
|
verzonden=lijst == 'verzonden', aantal=aantal,
|
||||||
aantal_groen=aantal['groen'], aantal_rood=aantal['rood'])
|
aantal_groen=aantal['groen'], aantal_rood=aantal['rood'])
|
||||||
|
|||||||
-42
@@ -1,42 +0,0 @@
|
|||||||
# =====================================================================
|
|
||||||
# PIVOT – algemene instellingen (gelden voor ALLE scholen)
|
|
||||||
#
|
|
||||||
# Alles wat per school verschilt (Smartschool, Informat, Google, Signpost,
|
|
||||||
# CO2, personeel, database van de school, ...) staat NIET meer hier, maar
|
|
||||||
# in het scholenregister: in PIVOT via 🏫 Scholen (superbeheerder) of
|
|
||||||
# 🔌 Koppelingen van deze school (beheerder van een school).
|
|
||||||
#
|
|
||||||
# Geheimen kan je ook als omgevingsvariabele meegeven i.p.v. hier:
|
|
||||||
# PIVOT_DB_PASSWORD, PIVOT_SSO_CLIENT_SECRET, FLASK_SECRET_KEY, PIVOT_SLEUTEL
|
|
||||||
# =====================================================================
|
|
||||||
|
|
||||||
[database]
|
|
||||||
# Server en standaardlogin: voor de centrale database én voor elke schooldatabase
|
|
||||||
# (per school te overschrijven in het scholenregister)
|
|
||||||
host = localhost
|
|
||||||
port = 3306
|
|
||||||
user = marijn
|
|
||||||
password =8778Zappa4861!
|
|
||||||
# Database met de lijst van scholen (wordt automatisch ingericht)
|
|
||||||
centrale_database = pivot
|
|
||||||
|
|
||||||
[google_sso]
|
|
||||||
# Gedeelde 'Inloggen met Google' voor alle scholen (OAuth-client van het type
|
|
||||||
# 'Webtoepassing', toestemmingsscherm 'Extern'). Leeg laten als elke school een
|
|
||||||
# eigen client gebruikt (in te stellen per school).
|
|
||||||
# Toegestane redirect-URI: https://<jouw-pivot-adres>/callback
|
|
||||||
client_id =
|
|
||||||
client_secret =
|
|
||||||
|
|
||||||
[algemeen]
|
|
||||||
app_version = 1.1.0
|
|
||||||
# Superbeheerders van de scholengroep (Google-accounts, komma-gescheiden):
|
|
||||||
# mogen scholen toevoegen/wijzigen en tussen scholen wisselen.
|
|
||||||
superbeheerders = ict@kamerksem.be
|
|
||||||
# Geheime sleutel voor sessies (liever via FLASK_SECRET_KEY)
|
|
||||||
secret_key =
|
|
||||||
debug = nee
|
|
||||||
# Elke bezochte pagina bewaren in de tabel Logs (ja/nee)
|
|
||||||
logboek = ja
|
|
||||||
# Aantal reverse proxies vóór PIVOT (nginx, IIS, firewall met https). 0 = PIVOT staat rechtstreeks op het net.
|
|
||||||
proxy_aantal = 0
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
100c148b256ee08843e482847b4a5d1f86797b380957c476ce6a29f1de2dd7cb
|
|
||||||
@@ -87,6 +87,7 @@ ONDERDELEN = [
|
|||||||
('bugs_beheren', 'Status van bugs wijzigen', 'Beheer'),
|
('bugs_beheren', 'Status van bugs wijzigen', 'Beheer'),
|
||||||
('nieuw_schooljaar', 'Nieuw schooljaar (accounts & toestellen)', 'Beheer'),
|
('nieuw_schooljaar', 'Nieuw schooljaar (accounts & toestellen)', 'Beheer'),
|
||||||
('ip_whitelist', 'IP-adressen toestaan voor deze school', 'Beheer'),
|
('ip_whitelist', 'IP-adressen toestaan voor deze school', 'Beheer'),
|
||||||
|
('database_toestellen', 'Database: toestellen importeren uit Excel', 'Beheer'),
|
||||||
# --- PIVOT ---
|
# --- PIVOT ---
|
||||||
('gebruikersbeheer', 'Gebruikers & rechten van deze school', 'PIVOT'),
|
('gebruikersbeheer', 'Gebruikers & rechten van deze school', 'PIVOT'),
|
||||||
('koppelingen', 'Koppelingen van deze school', 'PIVOT'),
|
('koppelingen', 'Koppelingen van deze school', 'PIVOT'),
|
||||||
|
|||||||
@@ -369,10 +369,14 @@ def schooljaar_periode(vandaag=None):
|
|||||||
|
|
||||||
|
|
||||||
_periode_gezet = {'dag': None}
|
_periode_gezet = {'dag': None}
|
||||||
|
_tabel_klaar = {'ok': False} # tabel/kolommen/index maar één keer per proces nakijken
|
||||||
|
|
||||||
|
|
||||||
def verzamel_ensure():
|
def verzamel_ensure():
|
||||||
from database import central_query
|
from database import central_query
|
||||||
|
if _tabel_klaar['ok']:
|
||||||
|
_periode_bijwerken()
|
||||||
|
return
|
||||||
central_query("""
|
central_query("""
|
||||||
CREATE TABLE IF NOT EXISTS GoPro_verzameltabel (
|
CREATE TABLE IF NOT EXISTS GoPro_verzameltabel (
|
||||||
ID INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
ID INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||||
@@ -395,7 +399,8 @@ def verzamel_ensure():
|
|||||||
Verzonden DATETIME NULL,
|
Verzonden DATETIME NULL,
|
||||||
Verzonden_door VARCHAR(255) NULL,
|
Verzonden_door VARCHAR(255) NULL,
|
||||||
UNIQUE KEY uq_gopro_verz (School_code, Person_ID),
|
UNIQUE KEY uq_gopro_verz (School_code, Person_ID),
|
||||||
KEY idx_gopro_verz_email (Email)
|
KEY idx_gopro_verz_email (Email),
|
||||||
|
KEY idx_gopro_verz_soort (Soort, School_naam)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci""", commit=True)
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci""", commit=True)
|
||||||
# Soort: 'groen' = gaat mee in de GO! Pro-CSV, 'genegeerd' = rode lijst (nooit meer tonen, niet exporteren),
|
# Soort: 'groen' = gaat mee in de GO! Pro-CSV, 'genegeerd' = rode lijst (nooit meer tonen, niet exporteren),
|
||||||
# 'verzonden' = al naar GO! Pro doorgestuurd (niet meer in de verzameltabel, nooit opnieuw toevoegen)
|
# 'verzonden' = al naar GO! Pro doorgestuurd (niet meer in de verzameltabel, nooit opnieuw toevoegen)
|
||||||
@@ -405,6 +410,15 @@ def verzamel_ensure():
|
|||||||
if 'verzonden' not in kolommen:
|
if 'verzonden' not in kolommen:
|
||||||
central_query("ALTER TABLE GoPro_verzameltabel ADD COLUMN Verzonden DATETIME NULL, "
|
central_query("ALTER TABLE GoPro_verzameltabel ADD COLUMN Verzonden DATETIME NULL, "
|
||||||
"ADD COLUMN Verzonden_door VARCHAR(255) NULL", commit=True)
|
"ADD COLUMN Verzonden_door VARCHAR(255) NULL", commit=True)
|
||||||
|
indexen = {r['Key_name'] for r in central_query("SHOW INDEX FROM GoPro_verzameltabel") or []}
|
||||||
|
if 'idx_gopro_verz_soort' not in indexen:
|
||||||
|
central_query("ALTER TABLE GoPro_verzameltabel ADD KEY idx_gopro_verz_soort (Soort, School_naam)", commit=True)
|
||||||
|
_tabel_klaar['ok'] = True
|
||||||
|
_periode_bijwerken()
|
||||||
|
|
||||||
|
|
||||||
|
def _periode_bijwerken():
|
||||||
|
from database import central_query
|
||||||
# Iedereen in de verzameltabel: periode = het lopende schooljaar (1 keer per dag nagekeken)
|
# Iedereen in de verzameltabel: periode = het lopende schooljaar (1 keer per dag nagekeken)
|
||||||
if _periode_gezet['dag'] != date.today():
|
if _periode_gezet['dag'] != date.today():
|
||||||
start, einde = schooljaar_periode()
|
start, einde = schooljaar_periode()
|
||||||
@@ -465,6 +479,14 @@ def verzameld_alle(school_code=None, soort='groen'):
|
|||||||
"ORDER BY School_naam, Achternaam, Voornaam", (soort,)) or []
|
"ORDER BY School_naam, Achternaam, Voornaam", (soort,)) or []
|
||||||
|
|
||||||
|
|
||||||
|
def aantallen():
|
||||||
|
"""{'groen': n, 'genegeerd': n, 'verzonden': n} in één query (in plaats van alle rijen op te halen)."""
|
||||||
|
from database import central_query
|
||||||
|
verzamel_ensure()
|
||||||
|
return {r['Soort']: int(r['n']) for r in
|
||||||
|
central_query("SELECT Soort, COUNT(*) AS n FROM GoPro_verzameltabel GROUP BY Soort") or []}
|
||||||
|
|
||||||
|
|
||||||
def markeer_verzonden(door=None, school_code=None):
|
def markeer_verzonden(door=None, school_code=None):
|
||||||
"""Alle personeelsleden in de verzameltabel (of enkel die van één school) als verzonden markeren.
|
"""Alle personeelsleden in de verzameltabel (of enkel die van één school) als verzonden markeren.
|
||||||
Ze verdwijnen uit de verzameltabel en worden nooit opnieuw toegevoegd. Geeft het aantal."""
|
Ze verdwijnen uit de verzameltabel en worden nooit opnieuw toegevoegd. Geeft het aantal."""
|
||||||
|
|||||||
@@ -0,0 +1,419 @@
|
|||||||
|
"""
|
||||||
|
Beheer › Database › Toestellen: de tabel Toestellen aanvullen vanuit een Excel-bestand.
|
||||||
|
|
||||||
|
Werkwijze (twee stappen, zodat er niets in de database komt zonder dat je het gezien hebt):
|
||||||
|
1. lees_excel() : het geüploade bestand → ruwe rijen (enkel tekst / ISO-datums)
|
||||||
|
2. maak_plan() : ruwe rijen → per rij 'nieuw', 'bijwerken', 'ongewijzigd' of 'fout' (niets wordt bewaard)
|
||||||
|
3. voer_uit() : het plan in één transactie uitvoeren (het plan wordt opnieuw gemaakt, nooit blind vertrouwd)
|
||||||
|
|
||||||
|
Een toestel wordt herkend aan het toestelnummer (Schooleigen_ID) en anders aan het serienummer.
|
||||||
|
Er wordt nooit iets verwijderd. Lege cellen wissen nooit een bestaande waarde.
|
||||||
|
"""
|
||||||
|
import io
|
||||||
|
from datetime import date, datetime
|
||||||
|
|
||||||
|
from database import execute_query, get_db_connection
|
||||||
|
from services import toestellen as tst
|
||||||
|
|
||||||
|
STATUSSEN = ('Beschikbaar', 'Uitgeleend', 'Defect')
|
||||||
|
MAX_RIJEN = 5000
|
||||||
|
|
||||||
|
# (sleutel, kolomkop in het sjabloon, max. lengte, uitleg, andere namen die ook herkend worden)
|
||||||
|
KOLOMMEN = [
|
||||||
|
('schooleigen_id', 'Toestelnummer', 50,
|
||||||
|
"Het eigen toestelnummer van de school (Schooleigen_ID). Uniek. Toestelnummer of serienummer is verplicht.",
|
||||||
|
('schooleigen_id', 'schooleigen id', 'kamxm_nummer', 'kamxm nummer', 'kamxm', 'nummer', 'toestel')),
|
||||||
|
('serienummer', 'Serienummer', 100,
|
||||||
|
"Serienummer van de fabrikant (nodig om het toestel in Google te blokkeren). "
|
||||||
|
"Toestelnummer of serienummer is verplicht.",
|
||||||
|
('serial', 'serial number', 'serienr', 'serie')),
|
||||||
|
('type', 'Type', 100, "Model van het toestel, bv. Lenovo 300e Gen 4.", ('model', 'toesteltype')),
|
||||||
|
('status', 'Status', None,
|
||||||
|
"Beschikbaar, Uitgeleend of Defect. Leeg bij een nieuw toestel = Beschikbaar.", ()),
|
||||||
|
('afschrijfdatum', 'Afschrijfdatum', None,
|
||||||
|
"Datum (dd-mm-jjjj) of een Excel-datum. Mag leeg.", ('afschrijving', 'afschrijf datum')),
|
||||||
|
('lokaal', 'Lokaal', None,
|
||||||
|
"Naam van een lokaal dat al in PIVOT bestaat (zie het tabblad Lokalen). Mag leeg.", ('lokaalnaam', 'lokaal_id')),
|
||||||
|
]
|
||||||
|
SLEUTELS = [k[0] for k in KOLOMMEN]
|
||||||
|
KOPPEN = {k[0]: k[1] for k in KOLOMMEN}
|
||||||
|
|
||||||
|
# Databasekolom per sleutel (lokaal → Lokaal_ID wordt apart opgezocht)
|
||||||
|
DB_KOLOM = {'schooleigen_id': 'Schooleigen_ID', 'serienummer': 'Serienummer', 'type': 'Type',
|
||||||
|
'status': 'Status', 'afschrijfdatum': 'Afschrijfdatum', 'lokaal': 'Lokaal_ID'}
|
||||||
|
|
||||||
|
|
||||||
|
class ImportFout(Exception):
|
||||||
|
"""Het bestand zelf kan niet gelezen worden (geen rijfout)."""
|
||||||
|
|
||||||
|
|
||||||
|
def _norm(tekst):
|
||||||
|
return ' '.join(str(tekst or '').strip().lower().replace('_', ' ').replace('-', ' ').split())
|
||||||
|
|
||||||
|
|
||||||
|
_HERKEN = {}
|
||||||
|
for _k, _kop, _m, _u, _alias in KOLOMMEN:
|
||||||
|
for _naam in (_k, _kop) + tuple(_alias):
|
||||||
|
_HERKEN[_norm(_naam)] = _k
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Sjabloon
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
def lokalen():
|
||||||
|
"""Alle lokalen van de huidige school: [{'Lokaal_ID', 'Naam', 'Campus'}]."""
|
||||||
|
return execute_query(
|
||||||
|
"SELECT l.Lokaal_ID, l.Naam, c.Campus_naam AS Campus FROM Lokalen l "
|
||||||
|
"LEFT JOIN Campus c ON c.Campus_ID = l.Campus_ID ORDER BY l.Naam") or []
|
||||||
|
|
||||||
|
|
||||||
|
def maak_sjabloon(schoolnaam=''):
|
||||||
|
"""Excel-sjabloon (bytes): tabblad Toestellen (in te vullen), Uitleg en Lokalen."""
|
||||||
|
from openpyxl import Workbook
|
||||||
|
from openpyxl.styles import Font, PatternFill, Alignment
|
||||||
|
from openpyxl.worksheet.datavalidation import DataValidation
|
||||||
|
|
||||||
|
wb = Workbook()
|
||||||
|
ws = wb.active
|
||||||
|
ws.title = 'Toestellen'
|
||||||
|
ws.append([k[1] for k in KOLOMMEN])
|
||||||
|
kop_vulling = PatternFill('solid', fgColor='1E6FA5')
|
||||||
|
for c in ws[1]:
|
||||||
|
c.font = Font(bold=True, color='FFFFFF')
|
||||||
|
c.fill = kop_vulling
|
||||||
|
c.alignment = Alignment(vertical='center')
|
||||||
|
breedtes = {'Toestelnummer': 16, 'Serienummer': 20, 'Type': 26, 'Status': 14, 'Afschrijfdatum': 16, 'Lokaal': 18}
|
||||||
|
for i, k in enumerate(KOLOMMEN):
|
||||||
|
letter = ws.cell(row=1, column=i + 1).column_letter
|
||||||
|
ws.column_dimensions[letter].width = breedtes.get(k[1], 16)
|
||||||
|
ws.freeze_panes = 'A2'
|
||||||
|
ws.row_dimensions[1].height = 22
|
||||||
|
|
||||||
|
kol = {k[0]: ws.cell(row=1, column=i + 1).column_letter for i, k in enumerate(KOLOMMEN)}
|
||||||
|
# Nummers als tekst (anders maakt Excel van 00123 → 123)
|
||||||
|
for sleutel in ('schooleigen_id', 'serienummer'):
|
||||||
|
for r in range(2, MAX_RIJEN + 2):
|
||||||
|
ws[f"{kol[sleutel]}{r}"].number_format = '@'
|
||||||
|
for r in range(2, MAX_RIJEN + 2):
|
||||||
|
ws[f"{kol['afschrijfdatum']}{r}"].number_format = 'DD-MM-YYYY'
|
||||||
|
|
||||||
|
dv_status = DataValidation(type='list', formula1='"' + ','.join(STATUSSEN) + '"', allow_blank=True,
|
||||||
|
showErrorMessage=True, errorTitle='Status',
|
||||||
|
error='Kies Beschikbaar, Uitgeleend of Defect (of laat leeg).')
|
||||||
|
dv_status.add(f"{kol['status']}2:{kol['status']}{MAX_RIJEN + 1}")
|
||||||
|
ws.add_data_validation(dv_status)
|
||||||
|
dv_datum = DataValidation(type='date', operator='greaterThan', formula1='1', allow_blank=True,
|
||||||
|
showErrorMessage=True, errorTitle='Afschrijfdatum',
|
||||||
|
error='Vul een datum in, bv. 31-08-2030.')
|
||||||
|
dv_datum.add(f"{kol['afschrijfdatum']}2:{kol['afschrijfdatum']}{MAX_RIJEN + 1}")
|
||||||
|
ws.add_data_validation(dv_datum)
|
||||||
|
|
||||||
|
# Lokalen van deze school (keuzelijst in de kolom Lokaal)
|
||||||
|
wl = wb.create_sheet('Lokalen')
|
||||||
|
wl.append(['Lokaal', 'Campus'])
|
||||||
|
for c in wl[1]:
|
||||||
|
c.font = Font(bold=True)
|
||||||
|
lijst = []
|
||||||
|
try:
|
||||||
|
lijst = lokalen()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
for l in lijst:
|
||||||
|
wl.append([l['Naam'], l.get('Campus') or ''])
|
||||||
|
wl.column_dimensions['A'].width = 24
|
||||||
|
wl.column_dimensions['B'].width = 28
|
||||||
|
if lijst:
|
||||||
|
dv_lokaal = DataValidation(type='list', formula1=f"=Lokalen!$A$2:$A${len(lijst) + 1}", allow_blank=True,
|
||||||
|
showErrorMessage=True, errorTitle='Lokaal',
|
||||||
|
error='Kies een lokaal uit de lijst (tabblad Lokalen) of laat leeg.')
|
||||||
|
dv_lokaal.add(f"{kol['lokaal']}2:{kol['lokaal']}{MAX_RIJEN + 1}")
|
||||||
|
ws.add_data_validation(dv_lokaal)
|
||||||
|
|
||||||
|
# Uitleg
|
||||||
|
wu = wb.create_sheet('Uitleg')
|
||||||
|
wu.column_dimensions['A'].width = 18
|
||||||
|
wu.column_dimensions['B'].width = 95
|
||||||
|
wu.append([f"Toestellen importeren in PIVOT{' – ' + schoolnaam if schoolnaam else ''}"])
|
||||||
|
wu['A1'].font = Font(bold=True, size=14)
|
||||||
|
regels = [
|
||||||
|
"Vul het tabblad 'Toestellen' in: één toestel per rij, vanaf rij 2. Laat de kolomkoppen in rij 1 staan.",
|
||||||
|
"Een toestel wordt herkend aan het toestelnummer, en anders aan het serienummer.",
|
||||||
|
"Bestaat het toestel nog niet, dan wordt het toegevoegd.",
|
||||||
|
"Bestaat het al, dan worden (afhankelijk van je keuze bij het uploaden) enkel lege velden aangevuld "
|
||||||
|
"of de ingevulde velden overschreven. Een lege cel wist nooit iets.",
|
||||||
|
"Er wordt nooit een toestel verwijderd. Je krijgt eerst een voorbeeld te zien voordat er iets bewaard wordt.",
|
||||||
|
f"Maximaal {MAX_RIJEN} rijen per bestand.",
|
||||||
|
]
|
||||||
|
for t in regels:
|
||||||
|
wu.append(['', t])
|
||||||
|
wu.append([])
|
||||||
|
wu.append(['Kolom', 'Uitleg'])
|
||||||
|
for c in wu[wu.max_row]:
|
||||||
|
c.font = Font(bold=True)
|
||||||
|
for _k, kop, maxlen, uitleg, _a in KOLOMMEN:
|
||||||
|
wu.append([kop, uitleg + (f" Max. {maxlen} tekens." if maxlen else '')])
|
||||||
|
for rij in wu.iter_rows(min_row=2):
|
||||||
|
for c in rij:
|
||||||
|
c.alignment = Alignment(wrap_text=True, vertical='top')
|
||||||
|
wu.append([])
|
||||||
|
wu.append(['Voorbeeld', 'Toestelnummer CB-0123 · Serienummer PF3ABC12 · Type Lenovo 300e Gen 4 · '
|
||||||
|
'Status Beschikbaar · Afschrijfdatum 31-08-2030 · Lokaal A101'])
|
||||||
|
|
||||||
|
wb.active = 0
|
||||||
|
buf = io.BytesIO()
|
||||||
|
wb.save(buf)
|
||||||
|
return buf.getvalue()
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Excel lezen
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
def _cel(waarde):
|
||||||
|
"""Celwaarde → tekst (datums als ISO 'jjjj-mm-dd', 123.0 → '123')."""
|
||||||
|
if waarde is None:
|
||||||
|
return ''
|
||||||
|
if isinstance(waarde, datetime):
|
||||||
|
return waarde.date().isoformat()
|
||||||
|
if isinstance(waarde, date):
|
||||||
|
return waarde.isoformat()
|
||||||
|
if isinstance(waarde, float) and waarde.is_integer():
|
||||||
|
return str(int(waarde))
|
||||||
|
return str(waarde).strip()
|
||||||
|
|
||||||
|
|
||||||
|
def lees_excel(bestand):
|
||||||
|
"""Leest een .xlsx (bestandsobject of bytes). Geeft (rijen, onbekende_kolommen).
|
||||||
|
rijen = [{'rij': <rijnummer in Excel>, 'schooleigen_id': '...', ...}] (enkel niet-lege rijen)."""
|
||||||
|
from openpyxl import load_workbook
|
||||||
|
data = bestand if isinstance(bestand, (bytes, bytearray)) else bestand.read()
|
||||||
|
if not data:
|
||||||
|
raise ImportFout("Het bestand is leeg.")
|
||||||
|
try:
|
||||||
|
wb = load_workbook(io.BytesIO(data), read_only=True, data_only=True)
|
||||||
|
except Exception:
|
||||||
|
raise ImportFout("Dit bestand kan niet gelezen worden. Gebruik een Excel-bestand (.xlsx), "
|
||||||
|
"bv. het sjabloon van deze pagina.")
|
||||||
|
try:
|
||||||
|
ws = wb['Toestellen'] if 'Toestellen' in wb.sheetnames else wb.worksheets[0]
|
||||||
|
rijen_iter = ws.iter_rows(values_only=True)
|
||||||
|
# Kopregel zoeken in de eerste 10 rijen: de eerste rij met een herkende kolom
|
||||||
|
kop_index, onbekend, rijnr = None, [], 0
|
||||||
|
for rij in rijen_iter:
|
||||||
|
rijnr += 1
|
||||||
|
gevonden = {i: _HERKEN[_norm(v)] for i, v in enumerate(rij or ()) if _norm(v) in _HERKEN}
|
||||||
|
if gevonden:
|
||||||
|
kop_index = {}
|
||||||
|
for i, sleutel in gevonden.items():
|
||||||
|
kop_index.setdefault(sleutel, i) # dubbele kolom: de eerste telt
|
||||||
|
onbekend = [_cel(v) for i, v in enumerate(rij) if _cel(v) and i not in gevonden]
|
||||||
|
break
|
||||||
|
if rijnr >= 10:
|
||||||
|
break
|
||||||
|
if not kop_index:
|
||||||
|
raise ImportFout("Geen kolomkoppen gevonden. Rij 1 moet de kolomnamen van het sjabloon bevatten "
|
||||||
|
f"({', '.join(KOPPEN.values())}).")
|
||||||
|
if 'schooleigen_id' not in kop_index and 'serienummer' not in kop_index:
|
||||||
|
raise ImportFout("De kolom Toestelnummer of Serienummer ontbreekt.")
|
||||||
|
|
||||||
|
uit = []
|
||||||
|
for rij in rijen_iter:
|
||||||
|
rijnr += 1
|
||||||
|
waarden = {s: _cel(rij[i]) if rij and i < len(rij) else '' for s, i in kop_index.items()}
|
||||||
|
if not any(waarden.values()):
|
||||||
|
continue
|
||||||
|
if len(uit) >= MAX_RIJEN:
|
||||||
|
raise ImportFout(f"Het bestand bevat meer dan {MAX_RIJEN} rijen. Splits het op in kleinere bestanden.")
|
||||||
|
uit.append({'rij': rijnr, **{s: waarden.get(s, '') for s in SLEUTELS}})
|
||||||
|
return uit, onbekend
|
||||||
|
finally:
|
||||||
|
wb.close()
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Plan maken (niets bewaren)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
def _datum_tekst(w):
|
||||||
|
"""'2030-08-31' / '31-08-2030' / '31/08/2030' → date, of ValueError."""
|
||||||
|
w = (w or '').strip()
|
||||||
|
if not w:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
return date.fromisoformat(w[:10]) if len(w) >= 10 and w[4] == '-' else tst.lees_datum(w)
|
||||||
|
except ValueError:
|
||||||
|
return tst.lees_datum(w)
|
||||||
|
|
||||||
|
|
||||||
|
def _toon(w):
|
||||||
|
if isinstance(w, date):
|
||||||
|
return w.strftime('%d-%m-%Y')
|
||||||
|
return '' if w is None else str(w)
|
||||||
|
|
||||||
|
|
||||||
|
def maak_plan(rijen, modus='aanvullen'):
|
||||||
|
"""Bepaalt per rij wat er zou gebeuren. modus: 'aanvullen' (enkel lege velden) of 'overschrijven'.
|
||||||
|
Geeft {'rijen': [...], 'tellers': {...}}; elke rij heeft 'actie', 'meldingen' en 'waarden' (voor de database)."""
|
||||||
|
overschrijven = modus == 'overschrijven'
|
||||||
|
bestaand = execute_query("SELECT Toestel_ID, Schooleigen_ID, Serienummer, Type, Status, Afschrijfdatum, Lokaal_ID "
|
||||||
|
"FROM Toestellen") or []
|
||||||
|
op_nummer = {(r['Schooleigen_ID'] or '').strip().upper(): r for r in bestaand if (r['Schooleigen_ID'] or '').strip()}
|
||||||
|
op_serie = {}
|
||||||
|
for r in bestaand:
|
||||||
|
s = (r['Serienummer'] or '').strip().upper()
|
||||||
|
if s:
|
||||||
|
op_serie.setdefault(s, r)
|
||||||
|
lok = {}
|
||||||
|
for l in lokalen():
|
||||||
|
lok.setdefault((l['Naam'] or '').strip().upper(), l)
|
||||||
|
lok_naam = {l['Lokaal_ID']: l['Naam'] for l in lok.values()}
|
||||||
|
|
||||||
|
gezien_nummer, gezien_serie = {}, {}
|
||||||
|
uit = []
|
||||||
|
tellers = {'nieuw': 0, 'bijwerken': 0, 'ongewijzigd': 0, 'fout': 0}
|
||||||
|
|
||||||
|
for invoer in rijen:
|
||||||
|
r = {'rij': invoer.get('rij'), 'invoer': {s: str(invoer.get(s) or '').strip() for s in SLEUTELS},
|
||||||
|
'meldingen': [], 'wijzigingen': [], 'actie': None, 'toestel_id': None, 'waarden': {}}
|
||||||
|
v = r['invoer']
|
||||||
|
fouten = []
|
||||||
|
|
||||||
|
# --- waarden controleren ---
|
||||||
|
nummer, serie = v['schooleigen_id'], v['serienummer']
|
||||||
|
if not nummer and not serie:
|
||||||
|
fouten.append("Toestelnummer of serienummer is verplicht.")
|
||||||
|
for s in ('schooleigen_id', 'serienummer', 'type'):
|
||||||
|
maxlen = next(k[2] for k in KOLOMMEN if k[0] == s)
|
||||||
|
if len(v[s]) > maxlen:
|
||||||
|
fouten.append(f"{KOPPEN[s]} is te lang (max. {maxlen} tekens).")
|
||||||
|
status = None
|
||||||
|
if v['status']:
|
||||||
|
status = next((x for x in STATUSSEN if x.lower() == v['status'].lower()), None)
|
||||||
|
if not status:
|
||||||
|
fouten.append(f"Status '{v['status']}' is ongeldig (kies {', '.join(STATUSSEN)}).")
|
||||||
|
afschrijf = None
|
||||||
|
if v['afschrijfdatum']:
|
||||||
|
try:
|
||||||
|
afschrijf = _datum_tekst(v['afschrijfdatum'])
|
||||||
|
except ValueError:
|
||||||
|
fouten.append(f"Afschrijfdatum '{v['afschrijfdatum']}' is geen geldige datum (gebruik dd-mm-jjjj).")
|
||||||
|
lokaal_id = None
|
||||||
|
if v['lokaal']:
|
||||||
|
l = lok.get(v['lokaal'].upper())
|
||||||
|
if l:
|
||||||
|
lokaal_id = l['Lokaal_ID']
|
||||||
|
else:
|
||||||
|
fouten.append(f"Lokaal '{v['lokaal']}' bestaat niet in PIVOT.")
|
||||||
|
|
||||||
|
# --- dubbels in het bestand (enkel t.o.v. eerdere rijen zonder fout) ---
|
||||||
|
if nummer and gezien_nummer.get(nummer.upper()):
|
||||||
|
fouten.append(f"Toestelnummer {nummer} staat al op rij {gezien_nummer[nummer.upper()]}.")
|
||||||
|
if serie and gezien_serie.get(serie.upper()):
|
||||||
|
fouten.append(f"Serienummer {serie} staat al op rij {gezien_serie[serie.upper()]}.")
|
||||||
|
|
||||||
|
# --- bestaand toestel? ---
|
||||||
|
t_nummer = op_nummer.get(nummer.upper()) if nummer else None
|
||||||
|
t_serie = op_serie.get(serie.upper()) if serie else None
|
||||||
|
toestel = t_nummer or t_serie
|
||||||
|
if t_nummer and t_serie and t_nummer['Toestel_ID'] != t_serie['Toestel_ID']:
|
||||||
|
fouten.append(f"Toestelnummer {nummer} en serienummer {serie} horen in PIVOT bij twee verschillende "
|
||||||
|
f"toestellen. Pas dit eerst aan in 💻 Alle toestellen.")
|
||||||
|
elif toestel:
|
||||||
|
db_nummer = (toestel['Schooleigen_ID'] or '').strip()
|
||||||
|
db_serie = (toestel['Serienummer'] or '').strip()
|
||||||
|
if t_nummer and serie and db_serie and db_serie.upper() != serie.upper():
|
||||||
|
fouten.append(f"Toestel {nummer} heeft in PIVOT serienummer {db_serie}, niet {serie}.")
|
||||||
|
if not t_nummer and nummer and db_nummer and db_nummer.upper() != nummer.upper():
|
||||||
|
fouten.append(f"Serienummer {serie} hoort in PIVOT bij toestel {db_nummer}, niet bij {nummer}.")
|
||||||
|
|
||||||
|
if fouten:
|
||||||
|
r['actie'] = 'fout'
|
||||||
|
r['meldingen'] = fouten
|
||||||
|
tellers['fout'] += 1
|
||||||
|
uit.append(r)
|
||||||
|
continue
|
||||||
|
|
||||||
|
if nummer:
|
||||||
|
gezien_nummer[nummer.upper()] = r['rij']
|
||||||
|
if serie:
|
||||||
|
gezien_serie[serie.upper()] = r['rij']
|
||||||
|
|
||||||
|
nieuw = {'schooleigen_id': nummer or None, 'serienummer': serie or None, 'type': v['type'] or None,
|
||||||
|
'status': status, 'afschrijfdatum': afschrijf, 'lokaal': lokaal_id}
|
||||||
|
|
||||||
|
if not toestel:
|
||||||
|
nieuw['status'] = status or 'Beschikbaar'
|
||||||
|
r['actie'] = 'nieuw'
|
||||||
|
r['waarden'] = {DB_KOLOM[s]: w for s, w in nieuw.items() if w is not None}
|
||||||
|
tellers['nieuw'] += 1
|
||||||
|
uit.append(r)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# --- bijwerken ---
|
||||||
|
r['toestel_id'] = toestel['Toestel_ID']
|
||||||
|
r['label'] = toestel['Schooleigen_ID'] or toestel['Serienummer'] or str(toestel['Toestel_ID'])
|
||||||
|
overgeslagen = []
|
||||||
|
for s, w in nieuw.items():
|
||||||
|
if w is None:
|
||||||
|
continue
|
||||||
|
kolom = DB_KOLOM[s]
|
||||||
|
huidig = toestel[kolom]
|
||||||
|
if isinstance(huidig, datetime):
|
||||||
|
huidig = huidig.date()
|
||||||
|
gelijk = (str(huidig).strip().upper() == str(w).strip().upper()) if huidig not in (None, '') else False
|
||||||
|
if gelijk:
|
||||||
|
continue
|
||||||
|
leeg = huidig is None or (isinstance(huidig, str) and not huidig.strip())
|
||||||
|
if s == 'status' and not overschrijven:
|
||||||
|
leeg = False # status heeft altijd een waarde: enkel wijzigen bij overschrijven
|
||||||
|
if leeg or overschrijven:
|
||||||
|
r['waarden'][kolom] = w
|
||||||
|
oud = lok_naam.get(huidig, huidig) if s == 'lokaal' else huidig
|
||||||
|
nw = lok_naam.get(w, w) if s == 'lokaal' else w
|
||||||
|
r['wijzigingen'].append(f"{KOPPEN[s]}: {_toon(oud) or '–'} → {_toon(nw)}")
|
||||||
|
else:
|
||||||
|
oud = lok_naam.get(huidig, huidig) if s == 'lokaal' else huidig
|
||||||
|
overgeslagen.append(f"{KOPPEN[s]} ({_toon(oud)})")
|
||||||
|
if overgeslagen:
|
||||||
|
r['meldingen'].append("Niet overschreven: " + ', '.join(overgeslagen) + '.')
|
||||||
|
if r['waarden']:
|
||||||
|
r['actie'] = 'bijwerken'
|
||||||
|
tellers['bijwerken'] += 1
|
||||||
|
else:
|
||||||
|
r['actie'] = 'ongewijzigd'
|
||||||
|
tellers['ongewijzigd'] += 1
|
||||||
|
uit.append(r)
|
||||||
|
|
||||||
|
return {'rijen': uit, 'tellers': tellers, 'modus': 'overschrijven' if overschrijven else 'aanvullen'}
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Uitvoeren
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
def voer_uit(rijen, modus, gebruiker=None):
|
||||||
|
"""Maakt het plan opnieuw en voert 'nieuw' en 'bijwerken' uit in één transactie.
|
||||||
|
Geeft (plan, aantal_nieuw, aantal_bijgewerkt). Bij een databasefout wordt niets bewaard."""
|
||||||
|
plan = maak_plan(rijen, modus)
|
||||||
|
conn = get_db_connection()
|
||||||
|
cur = conn.cursor()
|
||||||
|
nieuw = bijgewerkt = 0
|
||||||
|
try:
|
||||||
|
conn.start_transaction()
|
||||||
|
for r in plan['rijen']:
|
||||||
|
w = r['waarden']
|
||||||
|
if r['actie'] == 'nieuw':
|
||||||
|
kolommen = list(w.keys())
|
||||||
|
cur.execute(f"INSERT INTO Toestellen ({', '.join(kolommen)}) "
|
||||||
|
f"VALUES ({', '.join(['%s'] * len(kolommen))})", tuple(w[k] for k in kolommen))
|
||||||
|
nieuw += 1
|
||||||
|
elif r['actie'] == 'bijwerken':
|
||||||
|
kolommen = list(w.keys())
|
||||||
|
cur.execute(f"UPDATE Toestellen SET {', '.join(k + ' = %s' for k in kolommen)} WHERE Toestel_ID = %s",
|
||||||
|
tuple(w[k] for k in kolommen) + (r['toestel_id'],))
|
||||||
|
bijgewerkt += 1
|
||||||
|
conn.commit()
|
||||||
|
except Exception:
|
||||||
|
conn.rollback()
|
||||||
|
raise
|
||||||
|
finally:
|
||||||
|
cur.close()
|
||||||
|
conn.close()
|
||||||
|
return plan, nieuw, bijgewerkt
|
||||||
+2
-1
@@ -221,7 +221,8 @@ CREATE TABLE IF NOT EXISTS GoPro_verzameltabel (
|
|||||||
Verzonden DATETIME NULL,
|
Verzonden DATETIME NULL,
|
||||||
Verzonden_door VARCHAR(255) NULL,
|
Verzonden_door VARCHAR(255) NULL,
|
||||||
UNIQUE KEY uq_gopro_verz (School_code, Person_ID),
|
UNIQUE KEY uq_gopro_verz (School_code, Person_ID),
|
||||||
KEY idx_gopro_verz_email (Email)
|
KEY idx_gopro_verz_email (Email),
|
||||||
|
KEY idx_gopro_verz_soort (Soort, School_naam)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS Koppeling_tests (
|
CREATE TABLE IF NOT EXISTS Koppeling_tests (
|
||||||
|
|||||||
@@ -230,7 +230,8 @@ CREATE TABLE IF NOT EXISTS GoPro_verzameltabel (
|
|||||||
Verzonden DATETIME NULL,
|
Verzonden DATETIME NULL,
|
||||||
Verzonden_door VARCHAR(255) NULL,
|
Verzonden_door VARCHAR(255) NULL,
|
||||||
UNIQUE KEY uq_gopro_verz (School_code, Person_ID),
|
UNIQUE KEY uq_gopro_verz (School_code, Person_ID),
|
||||||
KEY idx_gopro_verz_email (Email)
|
KEY idx_gopro_verz_email (Email),
|
||||||
|
KEY idx_gopro_verz_soort (Soort, School_naam)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS Koppeling_tests (
|
CREATE TABLE IF NOT EXISTS Koppeling_tests (
|
||||||
|
|||||||
@@ -513,6 +513,10 @@ td.center, th.center { text-align: center; }
|
|||||||
.tile-links a::after { content: "›"; margin-left: auto; color: var(--border-strong); font-size: 18px; transition: transform .12s; }
|
.tile-links a::after { content: "›"; margin-left: auto; color: var(--border-strong); font-size: 18px; transition: transform .12s; }
|
||||||
.tile-links a:hover { background: var(--primary-light); color: var(--primary-color); }
|
.tile-links a:hover { background: var(--primary-light); color: var(--primary-color); }
|
||||||
.tile-links a:hover::after { color: var(--primary-color); transform: translateX(2px); }
|
.tile-links a:hover::after { color: var(--primary-color); transform: translateX(2px); }
|
||||||
|
/* Links-blok: icoontjes even breed (favicon of emoji), lange adressen afgekort met … */
|
||||||
|
.links-lijst .link-ico { flex: 0 0 18px; width: 18px; text-align: center; line-height: 1; font-size: 15px; }
|
||||||
|
.links-lijst .link-ico img { display: block; width: 16px; height: 16px; margin: 0 auto; border-radius: 3px; }
|
||||||
|
.links-lijst .link-tekst { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||||
|
|
||||||
.tile.tile-bug { border-top-color: var(--bug); }
|
.tile.tile-bug { border-top-color: var(--bug); }
|
||||||
.tile.tile-bug .tile-icon { background: var(--success-bg); }
|
.tile.tile-bug .tile-icon { background: var(--success-bg); }
|
||||||
|
|||||||
@@ -192,23 +192,25 @@
|
|||||||
{% if school %}
|
{% if school %}
|
||||||
{% set web_domein = (school.domein or '').strip().lower() %}
|
{% set web_domein = (school.domein or '').strip().lower() %}
|
||||||
{% set ss_url = ((school.waarden.get('SS_API_PLATFORM') or '').replace('https://', '').replace('http://', '').strip().strip('/')) %}
|
{% set ss_url = ((school.waarden.get('SS_API_PLATFORM') or '').replace('https://', '').replace('http://', '').strip().strip('/')) %}
|
||||||
{% if web_domein or ss_url %}
|
|
||||||
<section class="tile">
|
<section class="tile">
|
||||||
<div class="tile-head"><span class="tile-icon">🏫</span><h3>Links</h3></div>
|
<div class="tile-head"><span class="tile-icon">🏫</span><h3>Links</h3></div>
|
||||||
<ul class="tile-links">
|
<ul class="tile-links links-lijst">
|
||||||
|
{% macro ico(url, reserve) -%}
|
||||||
|
<span class="link-ico"><img src="{{ url }}" alt="" width="16" height="16" onerror="this.replaceWith(document.createTextNode({{ reserve|tojson|forceescape }}))"></span>
|
||||||
|
{%- endmacro %}
|
||||||
{% if web_domein %}
|
{% if web_domein %}
|
||||||
{% set web = web_domein if web_domein.startswith('www.') else 'www.' ~ web_domein %}
|
{% set web = web_domein if web_domein.startswith('www.') else 'www.' ~ web_domein %}
|
||||||
<li><a href="https://{{ web }}" target="_blank" rel="noopener">🌐 {{ web_domein }}</a></li>
|
<li><a href="https://{{ web }}" target="_blank" rel="noopener"><span class="link-ico">🌐</span><span class="link-tekst">{{ web_domein }}</span></a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if ss_url %}<li><a href="https://{{ ss_url }}" target="_blank" rel="noopener"><img src="https://{{ ss_url }}/favicon.ico" alt="" width="16" height="16"
|
{% if ss_url %}<li><a href="https://{{ ss_url }}" target="_blank" rel="noopener" title="{{ ss_url }}">{{ ico('https://' ~ ss_url ~ '/favicon.ico', '📘') }}<span class="link-tekst">{{ ss_url }}</span></a></li>{% endif %}
|
||||||
style="vertical-align:-3px;margin-right:6px;border-radius:3px" onerror="this.replaceWith(document.createTextNode('📘 '))">Smartschool</a></li>{% endif %}
|
<li><a href="https://start.informatsoftware.be/informat/informat.web/login.aspx?" target="_blank" rel="noopener">{{ ico('https://start.informatsoftware.be/favicon.ico', '📗') }}<span class="link-tekst">Informat</span></a></li>
|
||||||
|
<li><a href="https://pb-antwerpen.rvl.be/index.php" target="_blank" rel="noopener">{{ ico('https://pb-antwerpen.rvl.be/favicon.ico', '📙') }}<span class="link-tekst">Rivo</span></a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</section>
|
</section>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if mag('co2', 'koppelingen', 'gebruikersbeheer', 'nieuw_schooljaar', 'ip_whitelist') or is_admin or is_superbeheerder %}
|
{% if mag('co2', 'koppelingen', 'gebruikersbeheer', 'nieuw_schooljaar', 'ip_whitelist', 'database_toestellen') or is_admin or is_superbeheerder %}
|
||||||
<div class="dash-section-title admin">⚙️ Beheer</div>
|
<div class="dash-section-title admin">⚙️ Beheer</div>
|
||||||
|
|
||||||
<div class="tile-grid">
|
<div class="tile-grid">
|
||||||
@@ -252,6 +254,15 @@
|
|||||||
</ul>
|
</ul>
|
||||||
</section>
|
</section>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
{% if mag('database_toestellen') %}
|
||||||
|
<section class="tile tile-admin">
|
||||||
|
<div class="tile-head"><span class="tile-icon">🗄️</span><h3>Database</h3></div>
|
||||||
|
<ul class="tile-links">
|
||||||
|
<li><a href="{{ url_for('database_beheer.toestellen') }}">💻 Toestellen importeren (Excel)</a></li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@@ -0,0 +1,161 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Toestellen importeren{% endblock %}
|
||||||
|
{% block page_title %}Database › Toestellen{% endblock %}
|
||||||
|
{% block loader_text %}Bestand controleren...{% endblock %}
|
||||||
|
|
||||||
|
{% block extra_css %}
|
||||||
|
<style>
|
||||||
|
.stappen { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; align-items: start; }
|
||||||
|
.stappen > .card + .card { margin-top: 0; }
|
||||||
|
@media (max-width: 860px) { .stappen { grid-template-columns: 1fr; } }
|
||||||
|
.stap-nr { display: inline-grid; place-items: center; width: 26px; height: 26px; border-radius: 50%;
|
||||||
|
background: var(--primary-color); color: #fff; font-size: 14px; margin-right: 6px; }
|
||||||
|
.kolom-lijst { margin: 10px 0 0; padding: 0; list-style: none; font-size: 14px; }
|
||||||
|
.kolom-lijst li { padding: 6px 0; border-bottom: 1px solid var(--border); }
|
||||||
|
.kolom-lijst li:last-child { border-bottom: 0; }
|
||||||
|
.kolom-lijst strong { display: inline-block; min-width: 120px; }
|
||||||
|
.modus-keuze { display: grid; gap: 8px; margin: 4px 0 16px; }
|
||||||
|
.modus-keuze label { display: flex; gap: 10px; align-items: flex-start; padding: 10px 12px; border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius-sm); font-weight: 500; cursor: pointer; background: var(--surface-muted); }
|
||||||
|
.modus-keuze label:has(input:checked) { border-color: var(--primary-color); background: var(--primary-light); }
|
||||||
|
.modus-keuze input { margin-top: 2px; flex-shrink: 0; }
|
||||||
|
.modus-keuze small { display: block; color: var(--text-muted); font-weight: 400; }
|
||||||
|
.tellers { display: flex; flex-wrap: wrap; gap: 10px; margin: 6px 0 14px; }
|
||||||
|
.teller-knop { all: unset; cursor: pointer; display: inline-flex; gap: 8px; align-items: center; padding: 8px 14px;
|
||||||
|
border-radius: var(--radius); border: 1px solid var(--border); background: #fff; font-weight: 600; font-size: 14px; }
|
||||||
|
.teller-knop.actief { border-color: var(--primary-color); box-shadow: 0 0 0 2px rgba(30,111,165,.18); }
|
||||||
|
.teller-knop .n { font-size: 18px; }
|
||||||
|
tr.rij-fout td { background: #fff7f7; }
|
||||||
|
td.melding { font-size: 13px; color: var(--text-muted); min-width: 220px; }
|
||||||
|
td.melding .fout { color: var(--danger-text); }
|
||||||
|
td.melding .wijz { color: var(--text); }
|
||||||
|
.bevestig { display: flex; flex-wrap: wrap; gap: 10px; align-items: center; justify-content: space-between; margin-top: 16px; }
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
{% if not plan %}
|
||||||
|
<div class="stappen">
|
||||||
|
<div class="card">
|
||||||
|
<h2 class="card-title"><span class="stap-nr">1</span>Sjabloon downloaden</h2>
|
||||||
|
<p class="muted">Vul het sjabloon in met één toestel per rij. De kolom <em>Lokaal</em> heeft een keuzelijst met
|
||||||
|
de lokalen van {{ school.naam if school else 'deze school' }}.</p>
|
||||||
|
<a class="btn" href="{{ url_for('database_beheer.toestellen_sjabloon') }}">⬇️ Sjabloon downloaden (.xlsx)</a>
|
||||||
|
<ul class="kolom-lijst">
|
||||||
|
{% for k in kolommen %}
|
||||||
|
<li><strong>{{ k[1] }}</strong> <span class="muted">{{ k[3] }}</span></li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h2 class="card-title"><span class="stap-nr">2</span>Excel uploaden</h2>
|
||||||
|
<p class="muted">Je krijgt eerst een voorbeeld te zien. Er wordt pas iets bewaard als je bevestigt.
|
||||||
|
{% if aantal is not none %}Er staan nu <strong>{{ aantal }}</strong> toestellen in de database.{% endif %}</p>
|
||||||
|
<form method="POST" action="{{ url_for('database_beheer.toestellen_controleren') }}" enctype="multipart/form-data"
|
||||||
|
onsubmit="showLoader('Bestand controleren...')">
|
||||||
|
<div class="field">
|
||||||
|
<label for="bestand">Excel-bestand (.xlsx)</label>
|
||||||
|
<input type="file" id="bestand" name="bestand" accept=".xlsx,.xlsm,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" required>
|
||||||
|
<div class="field-hint">Max. {{ max_rijen }} rijen. Het tabblad <em>Toestellen</em> wordt gelezen (of anders het eerste tabblad).</div>
|
||||||
|
</div>
|
||||||
|
<label>Toestellen die al bestaan</label>
|
||||||
|
<div class="modus-keuze">
|
||||||
|
<label><input type="radio" name="modus" value="aanvullen" checked>
|
||||||
|
<span>Enkel lege velden aanvullen <small>Wat al ingevuld is in PIVOT blijft staan. (Aanbevolen)</small></span></label>
|
||||||
|
<label><input type="radio" name="modus" value="overschrijven">
|
||||||
|
<span>Ingevulde cellen overschrijven <small>Wat in Excel staat, vervangt de waarde in PIVOT (ook de status). Lege cellen wissen niets.</small></span></label>
|
||||||
|
</div>
|
||||||
|
<button type="submit">🔎 Controleren</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% else %}
|
||||||
|
{% set t = plan.tellers %}
|
||||||
|
<div class="card">
|
||||||
|
<div class="page-head">
|
||||||
|
<h2>🔎 Voorbeeld: {{ bestandsnaam }}</h2>
|
||||||
|
<a class="btn btn-light" href="{{ url_for('database_beheer.toestellen') }}">↩️ Ander bestand</a>
|
||||||
|
</div>
|
||||||
|
<p class="muted" style="margin-top:-8px">Nog niets bewaard. Bestaande toestellen:
|
||||||
|
<strong>{{ 'ingevulde cellen overschrijven' if plan.modus == 'overschrijven' else 'enkel lege velden aanvullen' }}</strong>.</p>
|
||||||
|
{% if onbekend %}
|
||||||
|
<div class="alert alert-warning">Deze kolommen worden genegeerd: {{ onbekend|join(', ') }}.</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="tellers" role="group" aria-label="Filter">
|
||||||
|
<button type="button" class="teller-knop actief" data-filter="">Alle <span class="n">{{ plan.rijen|length }}</span></button>
|
||||||
|
<button type="button" class="teller-knop" data-filter="nieuw"><span class="badge badge-success">➕ Nieuw</span> <span class="n">{{ t.nieuw }}</span></button>
|
||||||
|
<button type="button" class="teller-knop" data-filter="bijwerken"><span class="badge badge-info">✏️ Bijwerken</span> <span class="n">{{ t.bijwerken }}</span></button>
|
||||||
|
<button type="button" class="teller-knop" data-filter="ongewijzigd"><span class="badge">= Ongewijzigd</span> <span class="n">{{ t.ongewijzigd }}</span></button>
|
||||||
|
<button type="button" class="teller-knop" data-filter="fout"><span class="badge badge-danger">⚠️ Fout</span> <span class="n">{{ t.fout }}</span></button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="table-wrap">
|
||||||
|
<table id="voorbeeld">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Rij</th><th>Actie</th>
|
||||||
|
{% for k in kolommen %}<th>{{ k[1] }}</th>{% endfor %}
|
||||||
|
<th>Wat gebeurt er</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for r in plan.rijen %}
|
||||||
|
<tr data-actie="{{ r.actie }}" class="{{ 'rij-fout' if r.actie == 'fout' }}">
|
||||||
|
<td class="muted">{{ r.rij }}</td>
|
||||||
|
<td>
|
||||||
|
{% if r.actie == 'nieuw' %}<span class="badge badge-success">➕ Nieuw</span>
|
||||||
|
{% elif r.actie == 'bijwerken' %}<span class="badge badge-info">✏️ Bijwerken</span>
|
||||||
|
{% elif r.actie == 'ongewijzigd' %}<span class="badge">= Ongewijzigd</span>
|
||||||
|
{% else %}<span class="badge badge-danger">⚠️ Fout</span>{% endif %}
|
||||||
|
</td>
|
||||||
|
{% for k in kolommen %}<td class="{{ 'mono' if k[0] in ('schooleigen_id', 'serienummer') }}">{{ r.invoer[k[0]] }}</td>{% endfor %}
|
||||||
|
<td class="melding">
|
||||||
|
{% if r.actie == 'fout' %}{% for m in r.meldingen %}<div class="fout">{{ m }}</div>{% endfor %}
|
||||||
|
{% else %}
|
||||||
|
{% if r.actie == 'nieuw' %}<div class="wijz">Wordt toegevoegd{% if not r.invoer.status %} (status Beschikbaar){% endif %}.</div>{% endif %}
|
||||||
|
{% if r.label %}<div>Toestel {{ r.label }}</div>{% endif %}
|
||||||
|
{% for w in r.wijzigingen %}<div class="wijz">{{ w }}</div>{% endfor %}
|
||||||
|
{% for m in r.meldingen %}<div>{{ m }}</div>{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% set te_doen = t.nieuw + t.bijwerken %}
|
||||||
|
<form method="POST" action="{{ url_for('database_beheer.toestellen_importeren') }}" class="bevestig"
|
||||||
|
onsubmit="return confirm({{ ('Nu ' ~ t.nieuw ~ ' toestel(len) toevoegen en ' ~ t.bijwerken ~ ' bijwerken?' ~ (' De ' ~ t.fout ~ ' rij(en) met een fout worden overgeslagen.' if t.fout else ''))|tojson|forceescape }}) && (showLoader('Importeren...'), true)">
|
||||||
|
<input type="hidden" name="modus" value="{{ plan.modus }}">
|
||||||
|
<input type="hidden" name="rijen" value="{{ rijen_json }}">
|
||||||
|
<span class="muted">
|
||||||
|
{% if te_doen %}{{ t.nieuw }} nieuw en {{ t.bijwerken }} bij te werken toestel(len).
|
||||||
|
{% if t.fout %}Rijen met een fout worden overgeslagen; verbeter ze in Excel en upload opnieuw.{% endif %}
|
||||||
|
{% else %}Er is niets te importeren.{% endif %}
|
||||||
|
</span>
|
||||||
|
<span style="display:flex;gap:8px">
|
||||||
|
<a class="btn btn-light" href="{{ url_for('database_beheer.toestellen') }}">Annuleren</a>
|
||||||
|
<button type="submit" class="btn-success" {{ 'disabled' if not te_doen }}>✅ Importeren</button>
|
||||||
|
</span>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block scripts %}
|
||||||
|
<script>
|
||||||
|
// Voorbeeld filteren op actie (nieuw / bijwerken / ongewijzigd / fout)
|
||||||
|
document.querySelectorAll('.teller-knop').forEach(knop => knop.addEventListener('click', () => {
|
||||||
|
document.querySelectorAll('.teller-knop').forEach(k => k.classList.toggle('actief', k === knop));
|
||||||
|
const f = knop.dataset.filter;
|
||||||
|
document.querySelectorAll('#voorbeeld tbody tr').forEach(tr => {
|
||||||
|
tr.style.display = !f || tr.dataset.actie === f ? '' : 'none';
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
@@ -77,7 +77,7 @@
|
|||||||
{% if scholen %}
|
{% if scholen %}
|
||||||
<div class="per-school">
|
<div class="per-school">
|
||||||
{% for code, naam in scholen %}
|
{% for code, naam in scholen %}
|
||||||
<button type="button" class="chip" data-school="{{ code }}" aria-pressed="false" title="Enkel {{ naam }} tonen (nog eens klikken = alle scholen)">{{ naam }} <b>{{ rijen|selectattr('School_code', 'equalto', code)|list|length }}</b></button>
|
<button type="button" class="chip" data-school="{{ code }}" aria-pressed="false" title="Enkel {{ naam }} tonen (nog eens klikken = alle scholen)">{{ naam }} <b>{{ per_school[code] }}</b></button>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Informat-velden{% endblock %}
|
||||||
|
{% block page_title %}Informat-velden{% endblock %}
|
||||||
|
{% block loader_text %}Leerlingenlijst ophalen bij Informat...{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<a href="{{ url_for('leerlingenaantal.leerlingenaantal_page') }}" style="display:inline-block;margin-bottom:14px">← Leerlingenaantallen</a>
|
||||||
|
<div class="card">
|
||||||
|
<div class="page-head"><h2>🔎 Welke velden geeft Informat terug?</h2></div>
|
||||||
|
<p class="muted">Haalt de leerlingenlijst op zoals bij het tellen en toont per veld hoeveel leerlingen een waarde hebben.
|
||||||
|
Bij velden met weinig verschillende waarden zie je die waarden en hun aantal: zo vind je het veld dat de campus aangeeft.
|
||||||
|
Namen en andere persoonsgegevens worden niet getoond.</p>
|
||||||
|
<form method="GET" class="search-bar" onsubmit="showLoader()">
|
||||||
|
<select name="school" required>
|
||||||
|
<option value="">Kies een school…</option>
|
||||||
|
{% for s in scholen %}<option value="{{ s.code }}" {% if school and s.code == school.code %}selected{% endif %}>{{ s.naam }}</option>{% endfor %}
|
||||||
|
</select>
|
||||||
|
<button type="submit">🔎 Velden ophalen</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{% if fout %}<div class="alert alert-danger">{{ fout }}</div>{% endif %}
|
||||||
|
|
||||||
|
{% if velden is not none %}
|
||||||
|
<p><strong>{{ totaal }}</strong> leerlingen ontvangen van Informat voor {{ school.naam }}.</p>
|
||||||
|
<div class="table-wrap">
|
||||||
|
<table>
|
||||||
|
<thead><tr><th>Veld</th><th class="text-right">Gevuld bij</th><th class="text-right">Verschillende waarden</th><th>Waarden (aantal leerlingen)</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
{% for v in velden %}
|
||||||
|
<tr {% if v.waarden|length > 1 %}style="background:var(--primary-light)"{% endif %}>
|
||||||
|
<td class="mono">{{ v.pad }}</td>
|
||||||
|
<td class="text-right">{{ v.gevuld }}</td>
|
||||||
|
<td class="text-right">{{ v.verschillend }}</td>
|
||||||
|
<td class="small">
|
||||||
|
{% if v.gevoelig %}<span class="muted">persoonsgegeven – niet getoond</span>
|
||||||
|
{% elif v.waarden %}{% for w, n in v.waarden %}<code>{{ w }}</code> {{ n }}{% if not loop.last %} · {% endif %}{% endfor %}
|
||||||
|
{% else %}<span class="muted">te veel verschillende waarden</span>{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<p class="field-hint">Gevonden? Vul het veld in bij 🔌 Koppelingen › Informat › <em>Campusveld in de leerlingenlijst</em>
|
||||||
|
en bij <em>Campussen</em> de waarden met een naam, bv. <code>1=Campus Kapellen, 3=Campus Stabroek</code>.</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
Reference in new issue
Block a user