Compare commits
10
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eb07a9afef | ||
|
|
beaa5b2e5c | ||
|
|
8620dfb916 | ||
|
|
91a5b8a7f3 | ||
|
|
8b7fa3f716 | ||
|
|
684db2ec82 | ||
|
|
857f9c8f8b | ||
|
|
a85eff883e | ||
|
|
45d0b7f04a | ||
|
|
2b4e8de681 |
No files matched your search
+129
-26
@@ -1,42 +1,31 @@
|
|||||||
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()
|
||||||
|
|
||||||
with zipfile.ZipFile(zip_buffer, "a", zipfile.ZIP_DEFLATED) as zip_file:
|
|
||||||
for index, row in df.iterrows():
|
|
||||||
# Gebruik .iloc voor veiligheid tegen kolomnamen
|
|
||||||
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 ""
|
|
||||||
bottom_text = str(row.iloc[2]) if not pd.isna(row.iloc[2]) else ""
|
|
||||||
|
|
||||||
if qr_text.strip() == "":
|
|
||||||
continue
|
|
||||||
|
|
||||||
|
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 = qrcode.QRCode(error_correction=qrcode.constants.ERROR_CORRECT_H)
|
||||||
qr.add_data(qr_text)
|
qr.add_data(qr_text)
|
||||||
qr.make(fit=True)
|
qr.make(fit=True)
|
||||||
@@ -53,30 +42,96 @@ def generate_qr_images(excel_file, show_top, show_bottom):
|
|||||||
qr_y = padding
|
qr_y = padding
|
||||||
canvas.paste(qr_img, (qr_x, qr_y))
|
canvas.paste(qr_img, (qr_x, qr_y))
|
||||||
|
|
||||||
if show_top and top_text:
|
if top_text:
|
||||||
bbox = draw.textbbox((0, 0), top_text, font=font)
|
bbox = draw.textbbox((0, 0), top_text, font=font)
|
||||||
text_w = bbox[2] - bbox[0]
|
text_w = bbox[2] - bbox[0]
|
||||||
draw.text(((canvas.width - text_w) / 2, 5), top_text, fill="black", font=font)
|
draw.text(((canvas.width - text_w) / 2, 5), top_text, fill="black", font=font)
|
||||||
|
|
||||||
if show_bottom and bottom_text:
|
if bottom_text:
|
||||||
bbox = draw.textbbox((0, 0), bottom_text, font=font)
|
bbox = draw.textbbox((0, 0), bottom_text, font=font)
|
||||||
text_w = bbox[2] - bbox[0]
|
text_w = bbox[2] - bbox[0]
|
||||||
draw.text(((canvas.width - text_w) / 2, qr_y + qr_height + 5), bottom_text, fill="black", font=font)
|
draw.text(((canvas.width - text_w) / 2, qr_y + qr_height + 5), bottom_text, fill="black", font=font)
|
||||||
|
|
||||||
img_io = io.BytesIO()
|
img_io = io.BytesIO()
|
||||||
canvas.save(img_io, 'PNG')
|
canvas.save(img_io, 'PNG')
|
||||||
img_io.seek(0)
|
return img_io.getvalue()
|
||||||
zip_file.writestr(f"qr_{index+1}.png", 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:
|
||||||
|
for index, row in df.iterrows():
|
||||||
|
# Gebruik .iloc voor veiligheid tegen kolomnamen
|
||||||
|
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 ""
|
||||||
|
bottom_text = str(row.iloc[2]) if not pd.isna(row.iloc[2]) else ""
|
||||||
|
|
||||||
|
if qr_text.strip() == "":
|
||||||
|
continue
|
||||||
|
|
||||||
|
png = _qr_png(qr_text, top_text if show_top else "", bottom_text if show_bottom else "", font)
|
||||||
|
zip_file.writestr(f"qr_{index+1}.png", png)
|
||||||
|
|
||||||
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
|
||||||
@@ -123,3 +178,51 @@ def upload_file():
|
|||||||
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")
|
||||||
+105
-8
@@ -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 |
@@ -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); }
|
||||||
|
|||||||
@@ -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
@@ -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;
|
||||||
|
|||||||
@@ -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; }
|
||||||
|
|||||||
@@ -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; }
|
||||||
|
|||||||
@@ -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; }
|
||||||
|
|||||||
@@ -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; }
|
||||||
|
|||||||
@@ -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
@@ -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 %}
|
||||||
@@ -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; }
|
||||||
|
|||||||
Reference in new issue
Block a user