Author SHA1 Message Date
marijnandClaude Opus 5.5 c541ccc9a8 config.ini uit git halen, config.example.ini toevoegen (1.1.2)
config.ini bevat wachtwoorden en stond al in .gitignore, maar was
toch in git opgenomen. Het bestand blijft lokaal bestaan; nieuwe
installaties vertrekken van config.example.ini.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 20:17:58 +02:00
marijnandClaude Opus 5.5 6e78b2e790 Versienummer automatisch verhogen bij elke commit (1.1.1)
- VERSION bevat het versienummer, config.py leest het daaruit
  (werkt ook in Docker, waar config.ini niet meegaat)
- verhoog_versie.py: laatste cijfer +1 tot 99, dan het middelste;
  het middelste ook tot 99, dan het eerste
- CLAUDE.md: Claude verhoogt de versie voor elke commit

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 20:17:32 +02:00
marijn 51b016d8fb Merge pull request 'Logo leesbaar maken in het donkere thema' (#5) from logo_donker_thema into main
Reviewed-on: #5
2026-10-14 18:04:23 +00:00
marijnandClaude Opus 5.5 eb07a9afef Logo leesbaar maken in het donkere thema
Het donkerblauwe logo op de inlogpagina viel weg tegen de donkere
achtergrond. In het donkere thema staat het nu op een lichte plaat.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 20:01:09 +02:00
marijn beaa5b2e5c Merge pull request 'Volledige schoolnaam tonen in de header' (#4) from schoolnaam_volledig into main
Reviewed-on: #4
2026-10-14 16:24:26 +00:00
marinostrusandClaude Opus 5.5 8620dfb916 Volledige schoolnaam tonen in de header
De schoolnaam werd afgekapt op 200px. Nu wordt ze volledig getoond
en enkel nog ingekort met … als er geen plaats meer is.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 18:23:51 +02:00
marijn 91a5b8a7f3 Merge pull request 'Donker thema toevoegen via gebruikersmenu rechtsboven' (#3) from donker_thema into main
Reviewed-on: #3
2026-10-14 16:11:54 +00:00
marinostrusandClaude Opus 5.5 8b7fa3f716 Donker thema toevoegen via gebruikersmenu rechtsboven
Klik op je naam rechtsboven om een menu te openen met een schakelaar
voor het donkere thema. De keuze wordt per browser onthouden.

- global.css: donkere kleurvariabelen, alleen op scherm (afdrukken blijft licht)
- base.html: gebruikersmenu met themaschakelaar
- vaste witte achtergronden en pastelkleuren op enkele pagina's
  volgen nu het thema

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 18:08:30 +02:00
marijn 684db2ec82 Upload files to "static" 2026-10-14 14:39:51 +00:00
marijn 857f9c8f8b Update static/pivot_logo_old.png 2026-10-14 14:38:57 +00:00
marijn a85eff883e Merge pull request 'mijn eerste aanpassing' (#2) from import_eigendom into main
Reviewed-on: #2
2026-10-14 13:28:05 +00:00
marijn 45d0b7f04a Merge pull request 'qr-codes genereren op basis van database tables' (#1) from QR_genereren_vanuit_database into main
Reviewed-on: #1
2026-10-14 13:23:59 +00:00
Kristof 2b4e8de681 qr-codes genereren op basis van database tables 2026-10-14 15:13:40 +02:00
19 changed files with 435 additions and 70 deletions

No files matched your search

+18
View File
@@ -0,0 +1,18 @@
# PIVOT – afspraken voor Claude
## Versienummer bij elke commit
Het versienummer van PIVOT staat in het bestand `VERSION` (onderaan elke pagina: "PIVOT – Versie x.y.z").
**Vóór elke commit** het versienummer één stap verhogen en `VERSION` mee in de commit opnemen:
```bash
python verhoog_versie.py
git add VERSION
```
- Eén verhoging per commit, niet per gewijzigd bestand.
- Regels: het laatste cijfer telt op tot 99, daarna gaat het middelste cijfer omhoog (`1.1.99` → `1.2.0`); het middelste cijfer gaat ook tot 99 (`1.99.99` → `2.0.0`). Het eerste cijfer heeft geen maximum.
- Nooit `VERSION` met de hand aanpassen of nummers overslaan.
- Geeft een merge of pull een conflict in `VERSION` (twee branches verhoogden tegelijk)? Neem het **hoogste** van de twee nummers en voer daarna `python verhoog_versie.py` nog één keer uit.
- Vermeld het nieuwe nummer in de commitboodschap, bv. `Logo leesbaar maken in het donkere thema (1.1.2)`.
+1
View File
@@ -0,0 +1 @@
1.1.2
+148 -45
View File
@@ -1,31 +1,68 @@
import io import io
import re
import zipfile import zipfile
import qrcode import qrcode
import pandas as pd import pandas as pd
from flask import Blueprint, render_template, request, send_file, flash, redirect, url_for from flask import Blueprint, render_template, request, send_file, flash, redirect, url_for
from PIL import Image, ImageDraw, ImageFont from PIL import Image, ImageDraw, ImageFont
from blueprints.auth import login_required from blueprints.auth import login_required
from rechten import recht_vereist # Zorg dat dit pad klopt naar je auth_utils.py from database import execute_query
from rechten import recht_vereist, heeft_recht
# definieren blueprint # definieren blueprint
qr_code_bp = Blueprint('qr_generator', __name__, template_folder='templates') qr_code_bp = Blueprint('qr_generator', __name__, template_folder='templates')
def generate_qr_images(excel_file, show_top, show_bottom): def _lettertype():
# Overslaan van de header rij
df = pd.read_excel(excel_file, header=0)
zip_buffer = io.BytesIO()
try: try:
# Try loading your local or system Arial font # Try loading your local or system Arial font
font = ImageFont.truetype("arial.ttf", 20) return ImageFont.truetype("arial.ttf", 20)
except IOError: except IOError:
try: try:
return ImageFont.load_default_font(size=20)
font = ImageFont.load_default_font(size=20)
except AttributeError: except AttributeError:
# Absolute fallback if Pillow is outdated, but textbbox WILL fail if this hits # Absolute fallback if Pillow is outdated, but textbbox WILL fail if this hits
font = ImageFont.load_default() return ImageFont.load_default()
def _qr_png(qr_text, top_text, bottom_text, font):
"""Eén QR-code als PNG (bytes), met optioneel tekst erboven en eronder."""
qr = qrcode.QRCode(error_correction=qrcode.constants.ERROR_CORRECT_H)
qr.add_data(qr_text)
qr.make(fit=True)
qr_img = qr.make_image(fill_color="black", back_color="white").convert("RGB")
qr_width, qr_height = qr_img.size
padding = 40
total_height = qr_height + (2 * padding)
canvas = Image.new("RGB", (qr_width + 40, total_height + 40), "white")
draw = ImageDraw.Draw(canvas)
qr_x = (canvas.width - qr_width) // 2
qr_y = padding
canvas.paste(qr_img, (qr_x, qr_y))
if top_text:
bbox = draw.textbbox((0, 0), top_text, font=font)
text_w = bbox[2] - bbox[0]
draw.text(((canvas.width - text_w) / 2, 5), top_text, fill="black", font=font)
if bottom_text:
bbox = draw.textbbox((0, 0), bottom_text, font=font)
text_w = bbox[2] - bbox[0]
draw.text(((canvas.width - text_w) / 2, qr_y + qr_height + 5), bottom_text, fill="black", font=font)
img_io = io.BytesIO()
canvas.save(img_io, 'PNG')
return img_io.getvalue()
def generate_qr_images(excel_file, show_top, show_bottom):
# Overslaan van de header rij
df = pd.read_excel(excel_file, header=0)
zip_buffer = io.BytesIO()
font = _lettertype()
with zipfile.ZipFile(zip_buffer, "a", zipfile.ZIP_DEFLATED) as zip_file: with zipfile.ZipFile(zip_buffer, "a", zipfile.ZIP_DEFLATED) as zip_file:
for index, row in df.iterrows(): for index, row in df.iterrows():
@@ -33,50 +70,68 @@ def generate_qr_images(excel_file, show_top, show_bottom):
qr_text = str(row.iloc[0]) if not pd.isna(row.iloc[0]) else "" qr_text = str(row.iloc[0]) if not pd.isna(row.iloc[0]) else ""
top_text = str(row.iloc[1]) if not pd.isna(row.iloc[1]) else "" top_text = str(row.iloc[1]) if not pd.isna(row.iloc[1]) else ""
bottom_text = str(row.iloc[2]) if not pd.isna(row.iloc[2]) else "" bottom_text = str(row.iloc[2]) if not pd.isna(row.iloc[2]) else ""
if qr_text.strip() == "": if qr_text.strip() == "":
continue continue
qr = qrcode.QRCode(error_correction=qrcode.constants.ERROR_CORRECT_H)
qr.add_data(qr_text)
qr.make(fit=True)
qr_img = qr.make_image(fill_color="black", back_color="white").convert("RGB")
qr_width, qr_height = qr_img.size
padding = 40
total_height = qr_height + (2 * padding)
canvas = Image.new("RGB", (qr_width + 40, total_height + 40), "white")
draw = ImageDraw.Draw(canvas)
qr_x = (canvas.width - qr_width) // 2
qr_y = padding
canvas.paste(qr_img, (qr_x, qr_y))
if show_top and top_text:
bbox = draw.textbbox((0, 0), top_text, font=font)
text_w = bbox[2] - bbox[0]
draw.text(((canvas.width - text_w) / 2, 5), top_text, fill="black", font=font)
if show_bottom and bottom_text: png = _qr_png(qr_text, top_text if show_top else "", bottom_text if show_bottom else "", font)
bbox = draw.textbbox((0, 0), bottom_text, font=font) zip_file.writestr(f"qr_{index+1}.png", png)
text_w = bbox[2] - bbox[0]
draw.text(((canvas.width - text_w) / 2, qr_y + qr_height + 5), bottom_text, fill="black", font=font)
img_io = io.BytesIO()
canvas.save(img_io, 'PNG')
img_io.seek(0)
zip_file.writestr(f"qr_{index+1}.png", img_io.getvalue())
zip_buffer.seek(0) zip_buffer.seek(0)
return zip_buffer return zip_buffer
# ---------------------------------------------------------------------------
# QR-codes uit de database (tabel Toestellen + Signpost_DB)
# ---------------------------------------------------------------------------
# sleutel → (naam op het scherm, kolom in de query)
DB_VELDEN = {
'toestelnummer': ('Toestelnummer (Schooleigen_ID)', 't.Schooleigen_ID'),
'serienummer': ('Serienummer', 't.Serienummer'),
'spb': ('SPB-nummer (Signpost)', 'sp.Signpostnummer'),
'type': ('Type', 't.Type'),
}
QR_VELDEN = ('toestelnummer', 'serienummer', 'spb') # wat in de QR-code zelf mag
STATUSSEN = ('Beschikbaar', 'Uitgeleend', 'Defect')
MAX_DB_CODES = 5000
def _bestandsnaam(tekst, gebruikt):
"""Veilige, unieke bestandsnaam op basis van de QR-inhoud."""
naam = re.sub(r'[^A-Za-z0-9._-]+', '_', tekst).strip('._') or 'qr'
kandidaat, i = naam, 2
while kandidaat.lower() in gebruikt:
kandidaat, i = f"{naam}_{i}", i + 1
gebruikt.add(kandidaat.lower())
return f"{kandidaat}.png"
def toestellen_voor_qr(qr_veld, status='', zoek='', nummers=None):
"""Toestellen met een ingevulde waarde voor qr_veld, optioneel gefilterd. Geeft rijen met alle DB_VELDEN."""
kolommen = ', '.join(f"{kol} AS {sleutel}" for sleutel, (_n, kol) in DB_VELDEN.items())
qr_kol = DB_VELDEN[qr_veld][1]
sql = (f"SELECT {kolommen} FROM Toestellen t LEFT JOIN Signpost_DB sp ON sp.Toestel_ID = t.Toestel_ID "
f"WHERE {qr_kol} IS NOT NULL AND TRIM({qr_kol}) <> ''")
params = []
if status in STATUSSEN:
sql += " AND t.Status = %s"
params.append(status)
if zoek:
sql += " AND CONCAT_WS(' ', t.Schooleigen_ID, t.Serienummer, sp.Signpostnummer, t.Type) LIKE %s"
params.append(f"%{zoek}%")
if nummers:
sql += f" AND {qr_kol} IN ({', '.join(['%s'] * len(nummers))})"
params.extend(nummers)
sql += f" ORDER BY {qr_kol}"
return execute_query(sql, tuple(params)) or []
@qr_code_bp.route('/') @qr_code_bp.route('/')
@login_required @login_required
@recht_vereist('qr_codes') @recht_vereist('qr_codes')
def qr_generator(): def qr_generator():
return render_template('qr_code.html', db_toegang=heeft_recht('toestellen_beheren'), db_velden=DB_VELDEN,
return render_template('qr_code.html') qr_velden=QR_VELDEN, statussen=STATUSSEN, max_db_codes=MAX_DB_CODES)
@qr_code_bp.route('/download-template') @qr_code_bp.route('/download-template')
@login_required @login_required
@@ -122,4 +177,52 @@ def upload_file():
flash(f"Fout bij verwerken: {str(e)}", "danger") flash(f"Fout bij verwerken: {str(e)}", "danger")
return redirect(url_for('qr_generator.qr_generator')) return redirect(url_for('qr_generator.qr_generator'))
return redirect(url_for('qr_generator.qr_generator')) return redirect(url_for('qr_generator.qr_generator'))
@qr_code_bp.route('/database', methods=['POST'])
@login_required
@recht_vereist('qr_codes')
def database_qr():
"""QR-codes van toestelnummers, serienummers of SPB-nummers uit de database (ZIP met PNG's)."""
terug = redirect(url_for('qr_generator.qr_generator'))
if not heeft_recht('toestellen_beheren'):
flash("Je hebt het recht 'Toestelleninventaris beheren' nodig om QR-codes uit de database te maken.", "error")
return terug
qr_veld = request.form.get('qr_veld')
if qr_veld not in QR_VELDEN:
flash("Kies wat er in de QR-code moet komen.", "warning")
return terug
boven = request.form.get('boven') if request.form.get('boven') in DB_VELDEN else None
onder = request.form.get('onder') if request.form.get('onder') in DB_VELDEN else None
nummers = [n.strip() for n in re.split(r'[\s,;]+', request.form.get('nummers') or '') if n.strip()]
try:
rijen = toestellen_voor_qr(qr_veld, request.form.get('status') or '', (request.form.get('zoek') or '').strip(),
nummers)
except Exception as e:
flash(f"Fout bij het ophalen van de toestellen: {e}", "danger")
return terug
if not rijen:
flash(f"Geen toestellen gevonden met een {DB_VELDEN[qr_veld][0].split(' (')[0].lower()} voor deze keuze.",
"warning")
return terug
if len(rijen) > MAX_DB_CODES:
flash(f"Dat zijn {len(rijen)} toestellen; maximaal {MAX_DB_CODES} per keer. Gebruik een filter.", "warning")
return terug
font = _lettertype()
zip_buffer = io.BytesIO()
gebruikt = set()
with zipfile.ZipFile(zip_buffer, "w", zipfile.ZIP_DEFLATED) as zip_file:
for r in rijen:
qr_text = str(r[qr_veld]).strip()
png = _qr_png(qr_text, str(r[boven] or '') if boven else '', str(r[onder] or '') if onder else '', font)
zip_file.writestr(_bestandsnaam(qr_text, gebruikt), png)
zip_buffer.seek(0)
if nummers:
gevonden = {str(r[qr_veld]).strip().upper() for r in rijen}
niet = [n for n in nummers if n.upper() not in gevonden]
if niet:
flash(f"Niet gevonden: {', '.join(niet[:20])}{' ...' if len(niet) > 20 else ''}", "warning")
return send_file(zip_buffer, mimetype='application/zip', as_attachment=True,
download_name=f"qr_{qr_veld}.zip")
+4 -2
View File
@@ -1,3 +1,5 @@
# Voorbeeld: kopieer dit bestand naar config.ini en vul de lege waarden in.
# config.ini zelf staat NIET in git (bevat wachtwoorden).
# ===================================================================== # =====================================================================
# PIVOT – algemene instellingen (gelden voor ALLE scholen) # PIVOT – algemene instellingen (gelden voor ALLE scholen)
# #
@@ -15,8 +17,8 @@
# (per school te overschrijven in het scholenregister) # (per school te overschrijven in het scholenregister)
host = localhost host = localhost
port = 3306 port = 3306
user = marijn user =
password =8778Zappa4861! password =
# Database met de lijst van scholen (wordt automatisch ingericht) # Database met de lijst van scholen (wordt automatisch ingericht)
centrale_database = pivot centrale_database = pivot
+10 -1
View File
@@ -27,6 +27,15 @@ def _get(section, key, fallback='', env=None):
return config_file.get(section, key, fallback=fallback) return config_file.get(section, key, fallback=fallback)
def _versie():
"""Versienummer uit het bestand VERSION (wordt bij elke commit verhoogd met verhoog_versie.py, zie CLAUDE.md)."""
try:
with open(os.path.join(base_dir, 'VERSION'), encoding='utf-8') as f:
return f.read().strip()
except OSError:
return _get('algemeen', 'app_version', 'onbekend')
def _secret_key(): def _secret_key():
"""FLASK_SECRET_KEY, config.ini, of anders een willekeurige sleutel die bewaard wordt in geheimen/flask.key. """FLASK_SECRET_KEY, config.ini, of anders een willekeurige sleutel die bewaard wordt in geheimen/flask.key.
(Nooit een vaste standaardsleutel: daarmee kan iemand een sessie voor eender welke school vervalsen.)""" (Nooit een vaste standaardsleutel: daarmee kan iemand een sessie voor eender welke school vervalsen.)"""
@@ -86,7 +95,7 @@ class Config(metaclass=_SchoolConfigMeta):
_get('algemeen', 'superbeheerders', env='PIVOT_SUPERBEHEERDERS').replace(';', ',').split(',') _get('algemeen', 'superbeheerders', env='PIVOT_SUPERBEHEERDERS').replace(';', ',').split(',')
if x.strip()] if x.strip()]
APP_VERSION = _get('algemeen', 'app_version', '1.1.0') APP_VERSION = _versie()
# Elke bezochte pagina bewaren in de tabel Logs (centrale database) # Elke bezochte pagina bewaren in de tabel Logs (centrale database)
LOGBOEK = _get('algemeen', 'logboek', 'ja').strip().lower() in ('ja', 'yes', 'true', '1', 'on') LOGBOEK = _get('algemeen', 'logboek', 'ja').strip().lower() in ('ja', 'yes', 'true', '1', 'on')
+105 -8
View File
@@ -37,11 +37,87 @@
--shadow: 0 1px 3px rgba(15, 23, 42, .08), 0 4px 12px rgba(15, 23, 42, .05); --shadow: 0 1px 3px rgba(15, 23, 42, .08), 0 4px 12px rgba(15, 23, 42, .05);
--shadow-lg: 0 10px 30px rgba(15, 23, 42, .18); --shadow-lg: 0 10px 30px rgba(15, 23, 42, .18);
/* Overige vlakken */
--header-bg: var(--primary-color);
--row-hover: #f8fbfe;
--badge-bg: #e2e8f0; --badge-text: #334155;
--overlay: rgba(255, 255, 255, .85);
--header-height: 64px; --header-height: 64px;
--page-width: 1200px; --page-width: 1200px;
--font: "Segoe UI", system-ui, -apple-system, Roboto, Arial, sans-serif; --font: "Segoe UI", system-ui, -apple-system, Roboto, Arial, sans-serif;
} }
/* ---------- 1b. Donker thema (aan/uit via het gebruikersmenu rechtsboven) ---------- */
/* Alleen op scherm: afdrukken gebeurt altijd in het lichte thema */
@media screen {
html[data-theme="dark"] {
color-scheme: dark;
--primary-color: #3a8cc9;
--primary-dark: #2d73a8;
--primary-light: #17324a;
--background-color: #0f172a;
--surface: #1e293b;
--surface-muted: #172033;
--border: #334155;
--border-strong: #475569;
--text: #e2e8f0;
--text-muted: #94a3b8;
--success-bg: #14361f; --success-text: #86efac;
--warning-bg: #3a300b; --warning-text: #fde047;
--danger-bg: #3f1515; --danger-text: #fca5a5;
--info-bg: #172a4d; --info-text: #93c5fd;
--shadow-sm: 0 1px 2px rgba(0, 0, 0, .4);
--shadow: 0 1px 3px rgba(0, 0, 0, .4), 0 4px 12px rgba(0, 0, 0, .25);
--shadow-lg: 0 10px 30px rgba(0, 0, 0, .6);
--header-bg: #13304a;
--row-hover: #243349;
--badge-bg: #334155; --badge-text: #e2e8f0;
--overlay: rgba(15, 23, 42, .85);
}
html[data-theme="dark"] .flash-message,
html[data-theme="dark"] .flash-message.success,
html[data-theme="dark"] .flash-message.error,
html[data-theme="dark"] .flash-message.danger,
html[data-theme="dark"] .flash-message.warning { border-color: var(--border); }
html[data-theme="dark"] .dash-section-title.admin::after { background: var(--danger-bg); }
html[data-theme="dark"] .tile.tile-tools .tile-icon,
html[data-theme="dark"] .tile.tile-tools .tile-links a:hover { background: #123b38; }
html[data-theme="dark"] .tile.tile-tools h3,
html[data-theme="dark"] .tile.tile-tools .tile-links a:hover { color: #5eead4; }
html[data-theme="dark"] .tile.tile-scholengroep .tile-icon,
html[data-theme="dark"] .tile.tile-scholengroep .tile-links a:hover { background: #2e1f52; }
html[data-theme="dark"] .tile.tile-scholengroep h3,
html[data-theme="dark"] .tile.tile-scholengroep .tile-links a:hover { color: #c4b5fd; }
html[data-theme="dark"] .google-btn { background: var(--surface); color: var(--text); border-color: var(--border-strong); }
html[data-theme="dark"] .google-btn:hover { background: var(--surface-muted); }
html[data-theme="dark"] body.auth-page { background: var(--background-color); }
/* Logo is donkerblauw op transparant: op een lichte plaat zetten zodat het leesbaar blijft */
html[data-theme="dark"] .auth-logo { background: #f1f5f9; padding: 18px 24px; border-radius: var(--radius-lg); box-shadow: var(--shadow); }
html[data-theme="dark"] mark { background: #854d0e; color: #fef9c3; }
/* Pagina's met eigen pastelkleuren (gemarkeerde rijen, legendes, filters) */
html[data-theme="dark"] .tellers .let,
html[data-theme="dark"] .filters button.let { background: var(--warning-bg); border-color: #854d0e; }
html[data-theme="dark"] .filters button.let[aria-pressed="true"] { background: #854d0e; color: #fef9c3; }
html[data-theme="dark"] td.leeg,
html[data-theme="dark"] tr.flagged td,
html[data-theme="dark"] table.aw .vandaag,
html[data-theme="dark"] td.inv input.gewijzigd,
html[data-theme="dark"] .panel.directie,
html[data-theme="dark"] .panel.vraag { background: #2e2710 !important; border-color: #854d0e; }
html[data-theme="dark"] tr.rij-fout td { background: #3a1a1a; }
html[data-theme="dark"] button.st:not(.aanwezig):not(.afwezig) { background: var(--surface-muted); border-color: var(--border-strong); color: var(--text-muted); }
html[data-theme="dark"] #personeelBody tr.g-account td, html[data-theme="dark"] .legende i.account { background: #14361f; border-color: #166534; }
html[data-theme="dark"] #personeelBody tr.g-alias td, html[data-theme="dark"] .legende i.alias { background: #3a2a10; border-color: #9a5b13; }
html[data-theme="dark"] #personeelBody tr.g-geen td, html[data-theme="dark"] .legende i.geen { background: #3a1a1a; border-color: #991b1b; }
}
/* ---------- 2. Basis ---------- */ /* ---------- 2. Basis ---------- */
*, *::before, *::after { box-sizing: border-box; } *, *::before, *::after { box-sizing: border-box; }
@@ -84,7 +160,7 @@ a { color: var(--primary-color); }
align-items: center; align-items: center;
gap: 16px; gap: 16px;
padding: 0 24px; padding: 0 24px;
background: var(--primary-color); background: var(--header-bg);
color: #fff; color: #fff;
box-shadow: 0 2px 8px rgba(0, 0, 0, .15); box-shadow: 0 2px 8px rgba(0, 0, 0, .15);
} }
@@ -130,10 +206,29 @@ a { color: var(--primary-color); }
} }
.home-btn, .header-btn { background: rgba(255, 255, 255, .16); color: #fff; } .home-btn, .header-btn { background: rgba(255, 255, 255, .16); color: #fff; }
.home-btn:hover, .header-btn:hover { background: rgba(255, 255, 255, .28); } .home-btn:hover, .header-btn:hover { background: rgba(255, 255, 255, .28); }
.logout-btn { background: #fff; color: var(--danger); } .logout-btn { background: var(--surface); color: var(--danger); }
.logout-btn:hover { background: var(--danger-bg); } .logout-btn:hover { background: var(--danger-bg); }
.app-header form { margin: 0; } .app-header form { margin: 0; }
/* Gebruikersmenu (klik op je naam rechtsboven) */
.user-menu { position: relative; }
.user-menu-btn { opacity: 1; font-weight: 500; }
.user-menu-btn .caret { font-size: 10px; opacity: .8; }
.user-menu-panel {
display: none;
position: absolute; right: 0; top: calc(100% + 8px);
z-index: 1100;
min-width: 260px;
padding: 6px 14px;
background: var(--surface); color: var(--text);
border: 1px solid var(--border); border-radius: var(--radius);
box-shadow: var(--shadow-lg);
}
.user-menu.open .user-menu-panel { display: block; }
.user-menu-head { padding: 8px 0 10px; border-bottom: 1px solid var(--border); font-size: 13px; color: var(--text-muted); overflow-wrap: anywhere; }
.user-menu-head strong { display: block; color: var(--text); font-size: 14px; }
.user-menu-panel .switch-row { font-size: 14px; font-weight: 500; cursor: pointer; }
/* ---------- 4. Pagina-layout ---------- */ /* ---------- 4. Pagina-layout ---------- */
.app-main { .app-main {
flex: 1; flex: 1;
@@ -187,7 +282,7 @@ select, textarea {
font: inherit; font: inherit;
font-size: 15px; font-size: 15px;
color: var(--text); color: var(--text);
background: #fff; background: var(--surface);
border: 1px solid var(--border-strong); border: 1px solid var(--border-strong);
border-radius: var(--radius-sm); border-radius: var(--radius-sm);
transition: border-color .15s, box-shadow .15s; transition: border-color .15s, box-shadow .15s;
@@ -260,7 +355,7 @@ button:disabled, .btn:disabled { opacity: .5; cursor: not-allowed; }
.btn-secondary { background: #475569; } .btn-secondary { background: #475569; }
.btn-secondary:hover { background: #334155; } .btn-secondary:hover { background: #334155; }
.btn-light { background: #fff; color: var(--text); border-color: var(--border-strong); } .btn-light { background: var(--surface); color: var(--text); border-color: var(--border-strong); }
.btn-light:hover { background: var(--surface-muted); } .btn-light:hover { background: var(--surface-muted); }
.btn-success { background: var(--success); } .btn-success { background: var(--success); }
.btn-success:hover { background: #15803d; } .btn-success:hover { background: #15803d; }
@@ -291,7 +386,7 @@ button:disabled, .btn:disabled { opacity: .5; cursor: not-allowed; }
border-radius: var(--radius); border-radius: var(--radius);
margin-top: 12px; margin-top: 12px;
} }
table { width: 100%; border-collapse: collapse; background: #fff; text-align: left; } table { width: 100%; border-collapse: collapse; background: var(--surface); text-align: left; }
th, td { padding: 11px 14px; border-bottom: 1px solid var(--border); vertical-align: middle; } th, td { padding: 11px 14px; border-bottom: 1px solid var(--border); vertical-align: middle; }
th { th {
background: var(--surface-muted); background: var(--surface-muted);
@@ -299,7 +394,7 @@ th {
color: var(--text-muted); white-space: nowrap; color: var(--text-muted); white-space: nowrap;
} }
tbody tr:last-child td { border-bottom: 0; } tbody tr:last-child td { border-bottom: 0; }
tbody tr:hover { background: #f8fbfe; } tbody tr:hover { background: var(--row-hover); }
td.actions, th.actions { text-align: right; white-space: nowrap; } td.actions, th.actions { text-align: right; white-space: nowrap; }
td.actions form { display: inline; margin: 0; } td.actions form { display: inline; margin: 0; }
td.actions > * + * { margin-left: 6px; } td.actions > * + * { margin-left: 6px; }
@@ -324,7 +419,7 @@ td.center, th.center { text-align: center; }
display: inline-flex; align-items: center; gap: 4px; display: inline-flex; align-items: center; gap: 4px;
padding: 3px 10px; border-radius: 999px; padding: 3px 10px; border-radius: 999px;
font-size: 12.5px; font-weight: 600; white-space: nowrap; font-size: 12.5px; font-weight: 600; white-space: nowrap;
background: #e2e8f0; color: #334155; background: var(--badge-bg); color: var(--badge-text);
} }
.badge-success { background: var(--success-bg); color: var(--success-text); } .badge-success { background: var(--success-bg); color: var(--success-text); }
.badge-warning { background: var(--warning-bg); color: var(--warning-text); } .badge-warning { background: var(--warning-bg); color: var(--warning-text); }
@@ -448,7 +543,7 @@ td.center, th.center { text-align: center; }
position: fixed; inset: 0; position: fixed; inset: 0;
z-index: 9999; z-index: 9999;
flex-direction: column; justify-content: center; align-items: center; gap: 16px; flex-direction: column; justify-content: center; align-items: center; gap: 16px;
background: rgba(255, 255, 255, .85); background: var(--overlay);
backdrop-filter: blur(2px); backdrop-filter: blur(2px);
} }
.spinner { .spinner {
@@ -563,6 +658,8 @@ body.auth-page { background: linear-gradient(160deg, #eaf2f9 0%, var(--backgroun
/* ---------- 17. Responsief ---------- */ /* ---------- 17. Responsief ---------- */
@media (max-width: 900px) { @media (max-width: 900px) {
.brand-school, .header-user { display: none; } .brand-school, .header-user { display: none; }
.user-menu-btn { display: inline-flex; }
.user-menu-btn .user-naam { display: none; }
} }
@media (max-width: 720px) { @media (max-width: 720px) {
.app-header { .app-header {
Binary file not shown.

Before

Width:  |  Height:  |  Size: 396 KiB

After

Width:  |  Height:  |  Size: 397 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 396 KiB

+1 -1
View File
@@ -7,7 +7,7 @@
<style> <style>
/* Bewerken-knop altijd zichtbaar: laatste kolom blijft rechts staan bij horizontaal scrollen */ /* Bewerken-knop altijd zichtbaar: laatste kolom blijft rechts staan bij horizontaal scrollen */
.lln-tabel td.actions, .lln-tabel th.actions { .lln-tabel td.actions, .lln-tabel th.actions {
position: sticky; right: 0; background: #fff; white-space: nowrap; position: sticky; right: 0; background: var(--surface); white-space: nowrap;
box-shadow: -6px 0 6px -6px rgba(15, 23, 42, .15); box-shadow: -6px 0 6px -6px rgba(15, 23, 42, .15);
} }
.lln-tabel th.actions { background: var(--surface-muted); } .lln-tabel th.actions { background: var(--surface-muted); }
+1 -1
View File
@@ -21,7 +21,7 @@
tr.lln.open td:first-child::before { transform: rotate(90deg); } tr.lln.open td:first-child::before { transform: rotate(90deg); }
tr.detail td { background: var(--surface-muted); padding: 10px 14px 14px 44px; } tr.detail td { background: var(--surface-muted); padding: 10px 14px 14px 44px; }
.day-chips { display: flex; flex-wrap: wrap; gap: 6px; } .day-chips { display: flex; flex-wrap: wrap; gap: 6px; }
.day-chip { display: inline-flex; gap: 6px; align-items: center; padding: 4px 10px; border-radius: 999px; background: #fff; .day-chip { display: inline-flex; gap: 6px; align-items: center; padding: 4px 10px; border-radius: 999px; background: var(--surface);
border: 1px solid var(--border); font-size: 13px; } border: 1px solid var(--border); font-size: 13px; }
.day-chip .dd { font-weight: 700; color: var(--danger-text); } .day-chip .dd { font-weight: 700; color: var(--danger-text); }
.count-badge { display: inline-block; min-width: 34px; text-align: center; padding: 3px 10px; border-radius: 999px; font-weight: 700; .count-badge { display: inline-block; min-width: 34px; text-align: center; padding: 3px 10px; border-radius: 999px; font-weight: 700;
+51 -3
View File
@@ -4,10 +4,14 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}PIVOT{% endblock %} – {{ school_name_short|upper or 'PIVOT' }}</title> <title>{% block title %}PIVOT{% endblock %} – {{ school_name_short|upper or 'PIVOT' }}</title>
<script>
/* Thema meteen toepassen (vóór het tekenen) zodat de pagina niet eerst licht oplicht */
try { if (localStorage.getItem('pivot.thema') === 'donker') document.documentElement.dataset.theme = 'dark'; } catch (e) { /* geen opslag */ }
</script>
<link rel="icon" type="image/png" href="{{ url_for('static', filename='pivot_mark.png') }}"> <link rel="icon" type="image/png" href="{{ url_for('static', filename='pivot_mark.png') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='global.css') }}"> <link rel="stylesheet" href="{{ url_for('static', filename='global.css') }}">
{% block head %}{% endblock %} {% block head %}{% endblock %}
<style>.header-school { display: inline-block; max-width: 200px; overflow: hidden; text-overflow: ellipsis; vertical-align: middle; }</style> <style>.header-school { display: inline-block; min-width: 0; flex-shrink: 1; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; vertical-align: middle; }</style>
{% block extra_css %}{% endblock %} {% block extra_css %}{% endblock %}
</head> </head>
<body class="{% block body_class %}{% endblock %}"> <body class="{% block body_class %}{% endblock %}">
@@ -44,7 +48,7 @@
onchange="if (this.value) location.href = this.value"> onchange="if (this.value) location.href = this.value">
<option value="" selected>🏫 {{ school.naam }} ⇄</option> <option value="" selected>🏫 {{ school.naam }} ⇄</option>
{% for s in andere_scholen %} {% for s in andere_scholen %}
<option value="{{ url_for('scholen.wissel', code=s.code) }}" style="color:#1e293b">{{ s.naam }}</option> <option value="{{ url_for('scholen.wissel', code=s.code) }}" style="color:var(--text)">{{ s.naam }}</option>
{% endfor %} {% endfor %}
</select> </select>
{% else %} {% else %}
@@ -52,7 +56,22 @@
{% endif %} {% endif %}
{% endif %} {% endif %}
{% if session.get('user') %} {% if session.get('user') %}
<span class="header-user">👤 {{ session.get('user') }}</span> <div class="user-menu" id="user-menu">
<button type="button" class="header-btn header-user user-menu-btn" id="user-menu-btn"
aria-haspopup="true" aria-expanded="false" aria-controls="user-menu-panel" title="Gebruikersmenu">
👤 <span class="user-naam">{{ session.get('user') }}</span> <span class="caret">▼</span>
</button>
<div class="user-menu-panel" id="user-menu-panel">
<div class="user-menu-head">Aangemeld als <strong>{{ session.get('user') }}</strong></div>
<label class="switch-row" for="thema-schakelaar">
<span>🌙 Donker thema</span>
<span class="switch">
<input type="checkbox" id="thema-schakelaar">
<span class="slider"></span>
</span>
</label>
</div>
</div>
{% endif %} {% endif %}
<form action="{{ url_for('auth.logout') }}" method="get"> <form action="{{ url_for('auth.logout') }}" method="get">
<button type="submit" class="logout-btn">Uitloggen</button> <button type="submit" class="logout-btn">Uitloggen</button>
@@ -134,6 +153,35 @@
}); });
}, 6000); }, 6000);
// Gebruikersmenu rechtsboven + donker thema aan/uit (onthouden in deze browser)
(function () {
const menu = document.getElementById('user-menu');
if (!menu) return;
const knop = document.getElementById('user-menu-btn');
const schakelaar = document.getElementById('thema-schakelaar');
function zetOpen(open) {
menu.classList.toggle('open', open);
knop.setAttribute('aria-expanded', open);
}
knop.addEventListener('click', function (e) {
e.stopPropagation();
zetOpen(!menu.classList.contains('open'));
});
document.addEventListener('click', function (e) {
if (!menu.contains(e.target)) zetOpen(false);
});
document.addEventListener('keydown', function (e) {
if (e.key === 'Escape') zetOpen(false);
});
schakelaar.checked = document.documentElement.dataset.theme === 'dark';
schakelaar.addEventListener('change', function () {
if (schakelaar.checked) document.documentElement.dataset.theme = 'dark';
else delete document.documentElement.dataset.theme;
try { localStorage.setItem('pivot.thema', schakelaar.checked ? 'donker' : 'licht'); } catch (err) { /* negeren */ }
});
})();
// Escape sluit open modals // Escape sluit open modals
document.addEventListener('keydown', function (e) { document.addEventListener('keydown', function (e) {
if (e.key !== 'Escape') return; if (e.key !== 'Escape') return;
+1 -1
View File
@@ -23,7 +23,7 @@
.modus-keuze small { display: block; color: var(--text-muted); font-weight: 400; } .modus-keuze small { display: block; color: var(--text-muted); font-weight: 400; }
.tellers { display: flex; flex-wrap: wrap; gap: 10px; margin: 6px 0 14px; } .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; .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; } border-radius: var(--radius); border: 1px solid var(--border); background: var(--surface); 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.actief { border-color: var(--primary-color); box-shadow: 0 0 0 2px rgba(30,111,165,.18); }
.teller-knop .n { font-size: 18px; } .teller-knop .n { font-size: 18px; }
tr.rij-fout td { background: #fff7f7; } tr.rij-fout td { background: #fff7f7; }
+1 -1
View File
@@ -23,7 +23,7 @@
.modus-keuze small { display: block; color: var(--text-muted); font-weight: 400; } .modus-keuze small { display: block; color: var(--text-muted); font-weight: 400; }
.tellers { display: flex; flex-wrap: wrap; gap: 10px; margin: 6px 0 14px; } .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; .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; } border-radius: var(--radius); border: 1px solid var(--border); background: var(--surface); 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.actief { border-color: var(--primary-color); box-shadow: 0 0 0 2px rgba(30,111,165,.18); }
.teller-knop .n { font-size: 18px; } .teller-knop .n { font-size: 18px; }
tr.rij-fout td { background: #fff7f7; } tr.rij-fout td { background: #fff7f7; }
+1 -1
View File
@@ -23,7 +23,7 @@
#personeelBody tr.g-account td { background: #e8f7ee; } #personeelBody tr.g-account td { background: #e8f7ee; }
#personeelBody tr.g-alias td { background: #fff1e0; } #personeelBody tr.g-alias td { background: #fff1e0; }
#personeelBody tr.g-geen td { background: #fdeaea; } #personeelBody tr.g-geen td { background: #fdeaea; }
#personeelBody tr[class*="g-"] input.adres, #personeelBody tr[class*="g-"] select.rol { background: #fff; } #personeelBody tr[class*="g-"] input.adres, #personeelBody tr[class*="g-"] select.rol { background: var(--surface); }
.legende { display: flex; flex-wrap: wrap; gap: 6px 18px; align-items: center; margin: -6px 0 14px; font-size: .88em; color: var(--text-muted); } .legende { display: flex; flex-wrap: wrap; gap: 6px 18px; align-items: center; margin: -6px 0 14px; font-size: .88em; color: var(--text-muted); }
.legende button.kleurfilter { all: unset; box-sizing: border-box; cursor: pointer; display: inline-flex; align-items: center; .legende button.kleurfilter { all: unset; box-sizing: border-box; cursor: pointer; display: inline-flex; align-items: center;
gap: 6px; padding: 4px 10px; border-radius: 999px; border: 1px solid transparent; } gap: 6px; padding: 4px 10px; border-radius: 999px; border: 1px solid transparent; }
+1 -1
View File
@@ -11,7 +11,7 @@
.nav-bar form { display: flex; gap: 6px; margin: 0; } .nav-bar form { display: flex; gap: 6px; margin: 0; }
.nav-bar input[type=date] { width: auto; } .nav-bar input[type=date] { width: auto; }
.seg { display: inline-flex; border: 1px solid var(--border-strong); border-radius: var(--radius-sm); overflow: hidden; } .seg { display: inline-flex; border: 1px solid var(--border-strong); border-radius: var(--radius-sm); overflow: hidden; }
.seg a { padding: 8px 14px; text-decoration: none; color: var(--text); font-weight: 600; font-size: 14px; background: #fff; } .seg a { padding: 8px 14px; text-decoration: none; color: var(--text); font-weight: 600; font-size: 14px; background: var(--surface); }
.seg a.on { background: var(--primary-color); color: #fff; } .seg a.on { background: var(--primary-color); color: #fff; }
.day { margin-bottom: 20px; } .day { margin-bottom: 20px; }
.day-head { display: flex; align-items: baseline; gap: 12px; margin-bottom: 10px; } .day-head { display: flex; align-items: baseline; gap: 12px; margin-bottom: 10px; }
+3 -3
View File
@@ -8,18 +8,18 @@
{% block extra_css %} {% block extra_css %}
<style> <style>
.soort-switch { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 18px; } .soort-switch { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 18px; }
.soort-switch a { padding: 8px 14px; border-radius: 999px; border: 1px solid var(--border); background: #fff; text-decoration: none; color: var(--text); font-weight: 600; font-size: 14px; } .soort-switch a { padding: 8px 14px; border-radius: 999px; border: 1px solid var(--border); background: var(--surface); text-decoration: none; color: var(--text); font-weight: 600; font-size: 14px; }
.soort-switch a.on { background: var(--primary-color); border-color: var(--primary-color); color: #fff; } .soort-switch a.on { background: var(--primary-color); border-color: var(--primary-color); color: #fff; }
.grid2 { display: grid; grid-template-columns: 1fr 1fr; gap: 0 16px; } .grid2 { display: grid; grid-template-columns: 1fr 1fr; gap: 0 16px; }
.grid3 { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 0 16px; } .grid3 { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 0 16px; }
@media (max-width: 640px) { .grid2, .grid3 { grid-template-columns: 1fr; } } @media (max-width: 640px) { .grid2, .grid3 { grid-template-columns: 1fr; } }
.dagdeel { display: flex; gap: 6px; flex-wrap: wrap; } .dagdeel { display: flex; gap: 6px; flex-wrap: wrap; }
.dagdeel label { display: inline-flex; align-items: center; gap: 6px; padding: 8px 12px; border: 1px solid var(--border); border-radius: var(--radius-sm); background: #fff; cursor: pointer; font-weight: 500; } .dagdeel label { display: inline-flex; align-items: center; gap: 6px; padding: 8px 12px; border: 1px solid var(--border); border-radius: var(--radius-sm); background: var(--surface); cursor: pointer; font-weight: 500; }
.dagdeel input:checked + span { font-weight: 700; color: var(--primary-color); } .dagdeel input:checked + span { font-weight: 700; color: var(--primary-color); }
.klassen-box { border: 1px solid var(--border); border-radius: var(--radius); padding: 10px; background: var(--surface-muted); } .klassen-box { border: 1px solid var(--border); border-radius: var(--radius); padding: 10px; background: var(--surface-muted); }
.klassen-box input[type=search] { margin-bottom: 8px; } .klassen-box input[type=search] { margin-bottom: 8px; }
.klassen { display: flex; flex-wrap: wrap; gap: 6px; max-height: 210px; overflow-y: auto; } .klassen { display: flex; flex-wrap: wrap; gap: 6px; max-height: 210px; overflow-y: auto; }
.klas { display: inline-flex; align-items: center; gap: 5px; padding: 4px 10px; border: 1px solid var(--border); border-radius: 999px; background: #fff; font-size: 13px; font-weight: 600; cursor: pointer; user-select: none; } .klas { display: inline-flex; align-items: center; gap: 5px; padding: 4px 10px; border: 1px solid var(--border); border-radius: 999px; background: var(--surface); font-size: 13px; font-weight: 600; cursor: pointer; user-select: none; }
.klas input { width: 14px; height: 14px; } .klas input { width: 14px; height: 14px; }
.klas.on { background: var(--primary-light); border-color: var(--primary-color); color: var(--primary-color); } .klas.on { background: var(--primary-light); border-color: var(--primary-color); color: var(--primary-color); }
.gekozen { font-size: 13px; color: var(--text-muted); margin-top: 6px; } .gekozen { font-size: 13px; color: var(--text-muted); margin-top: 6px; }
+55 -1
View File
@@ -26,7 +26,7 @@
<div class="page-head"> <div class="page-head">
<div> <div>
<h2>💡 QR-codes maken</h2> <h2>💡 QR-codes maken</h2>
<p class="muted" style="margin:0">Maak eenvoudig QR-codes aan vanuit een Excel-bestand.</p> <p class="muted" style="margin:0">Maak eenvoudig QR-codes aan vanuit een Excel-bestand{{ ' of uit de database (hieronder)' if db_toegang }}.</p>
</div> </div>
</div> </div>
@@ -50,4 +50,58 @@
</li> </li>
</ol> </ol>
</div> </div>
{% if db_toegang %}
<div class="card">
<div class="page-head">
<div>
<h2>🗄️ QR-codes uit de database</h2>
<p class="muted" style="margin:0">Maak QR-codes van de toestellen in PIVOT: één PNG per toestel, samen in een ZIP.</p>
</div>
</div>
<form action="{{ url_for('qr_generator.database_qr') }}" method="post">
<div class="field">
<label for="qr_veld">In de QR-code</label>
<select id="qr_veld" name="qr_veld" required>
{% for v in qr_velden %}<option value="{{ v }}">{{ db_velden[v][0] }}</option>{% endfor %}
</select>
<div class="field-hint">Toestellen zonder deze waarde worden overgeslagen.</div>
</div>
<div class="options-box">
<div class="field">
<label for="boven">Tekst boven de QR-code</label>
<select id="boven" name="boven">
<option value="">Geen</option>
{% for k, v in db_velden.items() %}<option value="{{ k }}">{{ v[0] }}</option>{% endfor %}
</select>
</div>
<div class="field">
<label for="onder">Tekst onder de QR-code</label>
<select id="onder" name="onder">
<option value="">Geen</option>
{% for k, v in db_velden.items() %}<option value="{{ k }}" {{ 'selected' if k == 'toestelnummer' }}>{{ v[0] }}</option>{% endfor %}
</select>
</div>
</div>
<div class="field">
<label for="status">Status</label>
<select id="status" name="status">
<option value="">Alle toestellen</option>
{% for s in statussen %}<option value="{{ s }}">{{ s }}</option>{% endfor %}
</select>
</div>
<div class="field">
<label for="zoek">Zoeken (optioneel)</label>
<input type="text" id="zoek" name="zoek" placeholder="bv. CB-01 of Lenovo">
<div class="field-hint">Enkel toestellen waarvan het toestelnummer, serienummer, SPB-nummer of type deze tekst bevat.</div>
</div>
<div class="field">
<label for="nummers">Enkel deze nummers (optioneel)</label>
<textarea id="nummers" name="nummers" rows="4" placeholder="Eén per regel, of gescheiden door komma's"></textarea>
<div class="field-hint">Nummers van het gekozen soort (zie "In de QR-code"). Leeg = alle toestellen. Max. {{ max_db_codes }} per keer.</div>
</div>
<button type="submit" class="btn-block btn-lg">🚀 Genereer QR-codes (ZIP)</button>
</form>
</div>
{% endif %}
{% endblock %} {% endblock %}
+1 -1
View File
@@ -21,7 +21,7 @@
.code-panel { margin-top: 18px; border: 2px solid var(--success); border-radius: var(--radius-lg); background: var(--success-bg); padding: 20px; text-align: center; } .code-panel { margin-top: 18px; border: 2px solid var(--success); border-radius: var(--radius-lg); background: var(--success-bg); padding: 20px; text-align: center; }
.big-code { font-family: ui-monospace, Consolas, monospace; font-size: 40px; font-weight: 800; letter-spacing: 6px; color: var(--success-text); margin: 6px 0 4px; } .big-code { font-family: ui-monospace, Consolas, monospace; font-size: 40px; font-weight: 800; letter-spacing: 6px; color: var(--success-text); margin: 6px 0 4px; }
.code-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); gap: 6px; margin-top: 12px; } .code-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); gap: 6px; margin-top: 12px; }
.code-grid span { font-family: ui-monospace, Consolas, monospace; background: #fff; border: 1px solid var(--border); border-radius: 6px; padding: 6px; font-size: 15px; } .code-grid span { font-family: ui-monospace, Consolas, monospace; background: var(--surface); border: 1px solid var(--border); border-radius: 6px; padding: 6px; font-size: 15px; }
details summary { cursor: pointer; color: var(--text-muted); font-size: 14px; margin-top: 12px; } details summary { cursor: pointer; color: var(--text-muted); font-size: 14px; margin-top: 12px; }
.exception-active { margin-top: 14px; } .exception-active { margin-top: 14px; }
.pick-list { list-style: none; margin: 10px 0 0; padding: 0; } .pick-list { list-style: none; margin: 10px 0 0; padding: 0; }
+33
View File
@@ -0,0 +1,33 @@
"""
Verhoogt het versienummer in VERSION met één stap. Uitvoeren vóór elke commit:
python verhoog_versie.py
Regels: het laatste cijfer gaat telkens één omhoog, tot 99.
1.1.0 -> 1.1.1
1.1.99 -> 1.2.0
1.99.99 -> 2.0.0 (het eerste cijfer heeft geen maximum)
"""
import os
PAD = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'VERSION')
MAX = 99
def volgende(versie):
hoofd, sub, patch = (int(x) for x in versie.strip().split('.'))
patch += 1
if patch > MAX:
patch, sub = 0, sub + 1
if sub > MAX:
sub, hoofd = 0, hoofd + 1
return f"{hoofd}.{sub}.{patch}"
if __name__ == '__main__':
with open(PAD, encoding='utf-8') as f:
oud = f.read().strip()
nieuw = volgende(oud)
with open(PAD, 'w', encoding='utf-8') as f:
f.write(nieuw + '\n')
print(f"{oud} -> {nieuw}")