huisstijl-module (logo + kwaliteitscheck) + print-aanvullingen (legenda, pagina per dag, koptekst met logo)
This commit is contained in:
parent
1b559dd2c6
commit
e8934c6352
15 changed files with 393 additions and 8 deletions
|
|
@ -43,8 +43,13 @@ volgt.
|
||||||
- [x] **Printopties-dialog** (🖨 in Rooster/Roostermaker): kies wát (huidige
|
- [x] **Printopties-dialog** (🖨 in Rooster/Roostermaker): kies wát (huidige
|
||||||
selectie / alle klassen) en hóé je afdrukt — indeling (kolommen / dagen
|
selectie / alle klassen) en hóé je afdrukt — indeling (kolommen / dagen
|
||||||
onder elkaar), vakken in kleur, begeleiding/lokaal tonen, optionele
|
onder elkaar), vakken in kleur, begeleiding/lokaal tonen, optionele
|
||||||
koptekst — plus **papierformaat** (A4/A3/A5/Letter) en oriëntatie
|
koptekst, **kleurenlegenda** en **elke dag op nieuwe pagina** — plus
|
||||||
(liggend/staand). Toegepast via body-classes + dynamische `@page`.
|
**papierformaat** (A4/A3/A5/Letter) en oriëntatie (liggend/staand).
|
||||||
|
Afdruk-koptekst met logo + schooljaar + week + afdrukdatum.
|
||||||
|
- [x] **Module Huisstijl** (`huisstijl`, default aan): schoollogo uploaden via
|
||||||
|
Instellingen (PNG/SVG/JPG/GIF/WebP) met **kwaliteitscheck** (bestandstype,
|
||||||
|
resolutie, transparantie/achtergrond — waarschuwt bij gevulde/witte
|
||||||
|
achtergrond). Logo verschijnt in elke roosterweergave en op afdrukken.
|
||||||
|
|
||||||
- [x] **Vakkleur als vlak** (toggle 🎨, onthouden per browser): vak volledig in de
|
- [x] **Vakkleur als vlak** (toggle 🎨, onthouden per browser): vak volledig in de
|
||||||
activiteitskleur i.p.v. een streepje, met automatisch leesbare tekstkleur
|
activiteitskleur i.p.v. een streepje, met automatisch leesbare tekstkleur
|
||||||
|
|
|
||||||
|
|
@ -78,6 +78,7 @@ MODULE_APPS = [
|
||||||
"modules.blokwizard", # Vak-wizard
|
"modules.blokwizard", # Vak-wizard
|
||||||
"modules.personenwizard", # Personen-wizard & import
|
"modules.personenwizard", # Personen-wizard & import
|
||||||
"modules.autorooster", # Automatisch rooster
|
"modules.autorooster", # Automatisch rooster
|
||||||
|
"modules.huisstijl", # Schoollogo / huisstijl
|
||||||
]
|
]
|
||||||
|
|
||||||
INSTALLED_APPS = DJANGO_APPS + THIRD_PARTY_APPS + CORE_APPS + MODULE_APPS
|
INSTALLED_APPS = DJANGO_APPS + THIRD_PARTY_APPS + CORE_APPS + MODULE_APPS
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ urlpatterns = [
|
||||||
path("api/", include("modules.personenwizard.urls")),
|
path("api/", include("modules.personenwizard.urls")),
|
||||||
path("api/", include("modules.autorooster.urls")),
|
path("api/", include("modules.autorooster.urls")),
|
||||||
path("api/", include("modules.accounts.urls")),
|
path("api/", include("modules.accounts.urls")),
|
||||||
|
path("api/", include("modules.huisstijl.urls")),
|
||||||
]
|
]
|
||||||
|
|
||||||
if settings.DEBUG:
|
if settings.DEBUG:
|
||||||
|
|
|
||||||
0
backend/modules/huisstijl/__init__.py
Normal file
0
backend/modules/huisstijl/__init__.py
Normal file
30
backend/modules/huisstijl/apps.py
Normal file
30
backend/modules/huisstijl/apps.py
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class HuisstijlConfig(AppConfig):
|
||||||
|
default_auto_field = "django.db.models.BigAutoField"
|
||||||
|
name = "modules.huisstijl"
|
||||||
|
label = "module_huisstijl"
|
||||||
|
verbose_name = "Module: Huisstijl (logo)"
|
||||||
|
|
||||||
|
def ready(self):
|
||||||
|
from plugins.registry import MenuItem, ModuleSpec, registry
|
||||||
|
|
||||||
|
registry.register(
|
||||||
|
ModuleSpec(
|
||||||
|
key="huisstijl",
|
||||||
|
name="Huisstijl",
|
||||||
|
description=(
|
||||||
|
"Toont het schoollogo in elke roosterweergave en op afdrukken. "
|
||||||
|
"Logo te uploaden via Instellingen, met een kwaliteitscheck "
|
||||||
|
"(bestandstype, resolutie en achtergrond/transparantie)."
|
||||||
|
),
|
||||||
|
version="0.1.0",
|
||||||
|
category="Instellingen",
|
||||||
|
core=False,
|
||||||
|
default_enabled=True,
|
||||||
|
menu_items=(
|
||||||
|
MenuItem("Huisstijl", "/huisstijl", icon="badge", order=74, group="Instellingen"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
23
backend/modules/huisstijl/migrations/0001_initial.py
Normal file
23
backend/modules/huisstijl/migrations/0001_initial.py
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = []
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="Huisstijl",
|
||||||
|
fields=[
|
||||||
|
("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
|
||||||
|
("logo", models.FileField(blank=True, null=True, upload_to="huisstijl/", verbose_name="logo")),
|
||||||
|
("aangepast_op", models.DateTimeField(auto_now=True)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
"verbose_name": "huisstijl",
|
||||||
|
"verbose_name_plural": "huisstijl",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
0
backend/modules/huisstijl/migrations/__init__.py
Normal file
0
backend/modules/huisstijl/migrations/__init__.py
Normal file
21
backend/modules/huisstijl/models.py
Normal file
21
backend/modules/huisstijl/models.py
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
from django.db import models
|
||||||
|
|
||||||
|
|
||||||
|
class Huisstijl(models.Model):
|
||||||
|
"""Eén-rij-instelling met het schoollogo dat in elke roosterweergave en op
|
||||||
|
afdrukken getoond wordt."""
|
||||||
|
|
||||||
|
logo = models.FileField("logo", upload_to="huisstijl/", null=True, blank=True)
|
||||||
|
aangepast_op = models.DateTimeField(auto_now=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = "huisstijl"
|
||||||
|
verbose_name_plural = "huisstijl"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def actueel(cls):
|
||||||
|
obj, _ = cls.objects.get_or_create(pk=1)
|
||||||
|
return obj
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return "Huisstijl (logo)"
|
||||||
11
backend/modules/huisstijl/serializers.py
Normal file
11
backend/modules/huisstijl/serializers.py
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
from .models import Huisstijl
|
||||||
|
|
||||||
|
|
||||||
|
class HuisstijlSerializer(serializers.ModelSerializer):
|
||||||
|
logo = serializers.FileField(read_only=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Huisstijl
|
||||||
|
fields = ["id", "logo", "aangepast_op"]
|
||||||
7
backend/modules/huisstijl/urls.py
Normal file
7
backend/modules/huisstijl/urls.py
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
from django.urls import path
|
||||||
|
|
||||||
|
from . import views
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path("huisstijl/", views.huisstijl, name="huisstijl"),
|
||||||
|
]
|
||||||
125
backend/modules/huisstijl/views.py
Normal file
125
backend/modules/huisstijl/views.py
Normal file
|
|
@ -0,0 +1,125 @@
|
||||||
|
"""Logo-beheer met kwaliteitscheck.
|
||||||
|
|
||||||
|
GET /api/huisstijl/ -> huidig logo (alle ingelogde gebruikers).
|
||||||
|
PUT /api/huisstijl/ -> upload logo (alleen beheerders); voert een
|
||||||
|
kwaliteitscheck uit en geeft waarschuwingen terug.
|
||||||
|
DELETE /api/huisstijl/ -> logo verwijderen (alleen beheerders).
|
||||||
|
"""
|
||||||
|
from rest_framework.decorators import api_view
|
||||||
|
from rest_framework.response import Response
|
||||||
|
|
||||||
|
from .models import Huisstijl
|
||||||
|
from .serializers import HuisstijlSerializer
|
||||||
|
|
||||||
|
TOEGESTAAN = {"png", "jpg", "jpeg", "gif", "webp", "svg"}
|
||||||
|
MAX_BYTES = 3 * 1024 * 1024 # 3 MB
|
||||||
|
|
||||||
|
|
||||||
|
def _hex(rgb):
|
||||||
|
return "#%02x%02x%02x" % (rgb[0], rgb[1], rgb[2])
|
||||||
|
|
||||||
|
|
||||||
|
def analyseer_logo(f):
|
||||||
|
"""Beoordeel een geüpload logo. Geeft {ok, fouten, waarschuwingen, info}.
|
||||||
|
|
||||||
|
'ok' is False bij harde problemen (verkeerd bestandstype, onleesbaar).
|
||||||
|
Anders wordt het bestand opgeslagen, maar mogelijk met waarschuwingen
|
||||||
|
(bv. een niet-transparante / gevulde achtergrond, of lage resolutie).
|
||||||
|
"""
|
||||||
|
naam = (f.name or "").lower()
|
||||||
|
ext = naam.rsplit(".", 1)[-1] if "." in naam else ""
|
||||||
|
fouten, waarsch = [], []
|
||||||
|
if ext not in TOEGESTAAN:
|
||||||
|
fouten.append(f"Bestandstype '.{ext}' wordt niet ondersteund. Gebruik PNG, SVG, JPG, GIF of WebP.")
|
||||||
|
return {"ok": False, "fouten": fouten, "waarschuwingen": waarsch, "info": {}}
|
||||||
|
if getattr(f, "size", 0) and f.size > MAX_BYTES:
|
||||||
|
waarsch.append(f"Het bestand is groot ({f.size // 1024} KB); overweeg een kleiner logo.")
|
||||||
|
|
||||||
|
if ext == "svg":
|
||||||
|
return {"ok": True, "fouten": [], "waarschuwingen": waarsch,
|
||||||
|
"info": {"formaat": "SVG", "transparant": True, "breedte": None, "hoogte": None, "achtergrond": None}}
|
||||||
|
|
||||||
|
try:
|
||||||
|
from PIL import Image
|
||||||
|
except Exception:
|
||||||
|
# Zonder Pillow slaan we toch op, maar zonder beeldcontrole.
|
||||||
|
return {"ok": True, "fouten": [], "waarschuwingen": waarsch + ["Beeldcontrole niet beschikbaar."], "info": {"formaat": ext.upper()}}
|
||||||
|
|
||||||
|
try:
|
||||||
|
f.seek(0)
|
||||||
|
img = Image.open(f)
|
||||||
|
img.load()
|
||||||
|
except Exception:
|
||||||
|
return {"ok": False, "fouten": ["Kan de afbeelding niet lezen of het bestand is beschadigd."], "waarschuwingen": [], "info": {}}
|
||||||
|
|
||||||
|
breedte, hoogte = img.size
|
||||||
|
transparant = False
|
||||||
|
achtergrond = None
|
||||||
|
heeft_alpha = img.mode in ("RGBA", "LA") or (img.mode == "P" and "transparency" in img.info)
|
||||||
|
|
||||||
|
if heeft_alpha:
|
||||||
|
rgba = img.convert("RGBA")
|
||||||
|
hoeken = [rgba.getpixel((0, 0)), rgba.getpixel((breedte - 1, 0)),
|
||||||
|
rgba.getpixel((0, hoogte - 1)), rgba.getpixel((breedte - 1, hoogte - 1))]
|
||||||
|
if all(p[3] < 16 for p in hoeken):
|
||||||
|
transparant = True
|
||||||
|
else:
|
||||||
|
achtergrond = hoeken[0][:3]
|
||||||
|
waarsch.append("De hoeken zijn niet doorzichtig; het logo lijkt een gevulde achtergrond te hebben. "
|
||||||
|
"Op een gekleurde balk valt dat lelijk op — gebruik bij voorkeur een transparante PNG of SVG.")
|
||||||
|
else:
|
||||||
|
rgb = img.convert("RGB")
|
||||||
|
hoeken = [rgb.getpixel((0, 0)), rgb.getpixel((breedte - 1, 0)),
|
||||||
|
rgb.getpixel((0, hoogte - 1)), rgb.getpixel((breedte - 1, hoogte - 1))]
|
||||||
|
achtergrond = hoeken[0]
|
||||||
|
eensgezind = len(set(hoeken)) == 1
|
||||||
|
if eensgezind and min(achtergrond) > 245:
|
||||||
|
waarsch.append("Effen witte achtergrond zonder transparantie. Op een gekleurd vlak valt het witte blok op; "
|
||||||
|
"een transparante PNG of SVG is netter.")
|
||||||
|
elif eensgezind:
|
||||||
|
waarsch.append(f"Effen achtergrondkleur gedetecteerd ({_hex(achtergrond)}). Gebruik bij voorkeur een transparante PNG of SVG.")
|
||||||
|
else:
|
||||||
|
waarsch.append("Geen transparantie (bv. JPG). Voor een nette weergave op gekleurde vlakken is een transparante PNG of SVG beter.")
|
||||||
|
|
||||||
|
if breedte < 80 or hoogte < 80:
|
||||||
|
waarsch.append(f"Lage resolutie ({breedte}×{hoogte}px); dit kan korrelig printen.")
|
||||||
|
|
||||||
|
info = {
|
||||||
|
"formaat": img.format or ext.upper(),
|
||||||
|
"breedte": breedte,
|
||||||
|
"hoogte": hoogte,
|
||||||
|
"transparant": transparant,
|
||||||
|
"achtergrond": _hex(achtergrond) if achtergrond else None,
|
||||||
|
}
|
||||||
|
return {"ok": True, "fouten": [], "waarschuwingen": waarsch, "info": info}
|
||||||
|
|
||||||
|
|
||||||
|
@api_view(["GET", "PUT", "POST", "DELETE"])
|
||||||
|
def huisstijl(request):
|
||||||
|
obj = Huisstijl.actueel()
|
||||||
|
schrijven = request.method in ("PUT", "POST", "DELETE")
|
||||||
|
if schrijven and not (request.user and request.user.is_staff):
|
||||||
|
return Response({"detail": "Alleen beheerders kunnen het logo wijzigen."}, status=403)
|
||||||
|
|
||||||
|
if request.method == "DELETE":
|
||||||
|
if obj.logo:
|
||||||
|
obj.logo.delete(save=True)
|
||||||
|
return Response(HuisstijlSerializer(obj, context={"request": request}).data)
|
||||||
|
|
||||||
|
if request.method in ("PUT", "POST"):
|
||||||
|
f = request.FILES.get("logo")
|
||||||
|
if not f:
|
||||||
|
return Response({"detail": "Geen bestand ontvangen (veld 'logo')."}, status=400)
|
||||||
|
analyse = analyseer_logo(f)
|
||||||
|
if not analyse["ok"]:
|
||||||
|
return Response({"detail": " ".join(analyse["fouten"]), "kwaliteit": analyse}, status=400)
|
||||||
|
f.seek(0)
|
||||||
|
if obj.logo:
|
||||||
|
obj.logo.delete(save=False)
|
||||||
|
obj.logo = f
|
||||||
|
obj.save()
|
||||||
|
data = HuisstijlSerializer(obj, context={"request": request}).data
|
||||||
|
data["kwaliteit"] = analyse
|
||||||
|
return Response(data)
|
||||||
|
|
||||||
|
return Response(HuisstijlSerializer(obj, context={"request": request}).data)
|
||||||
|
|
@ -8,6 +8,7 @@ import {
|
||||||
AfwezigheidPage, GroepsindelingPage, StagePage, GebruikersPage, UrenPage,
|
AfwezigheidPage, GroepsindelingPage, StagePage, GebruikersPage, UrenPage,
|
||||||
LeerlingRoosterPage, OuderRoosterPage,
|
LeerlingRoosterPage, OuderRoosterPage,
|
||||||
VakWizardPage, PersonenWizardPage, AutoRoosterPage, WachtwoordResetPage,
|
VakWizardPage, PersonenWizardPage, AutoRoosterPage, WachtwoordResetPage,
|
||||||
|
HuisstijlPage,
|
||||||
} from "./pages.jsx";
|
} from "./pages.jsx";
|
||||||
|
|
||||||
const ICONS = {
|
const ICONS = {
|
||||||
|
|
@ -46,6 +47,7 @@ const PAGES = {
|
||||||
"/vak-wizard": VakWizardPage,
|
"/vak-wizard": VakWizardPage,
|
||||||
"/personen-wizard": PersonenWizardPage,
|
"/personen-wizard": PersonenWizardPage,
|
||||||
"/auto-rooster": AutoRoosterPage,
|
"/auto-rooster": AutoRoosterPage,
|
||||||
|
"/huisstijl": HuisstijlPage,
|
||||||
};
|
};
|
||||||
|
|
||||||
const RESET_PARAMS = (() => {
|
const RESET_PARAMS = (() => {
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,30 @@ export const login = (username, password) =>
|
||||||
request("POST", "/auth/login/", { username, password });
|
request("POST", "/auth/login/", { username, password });
|
||||||
export const logout = () => request("POST", "/auth/logout/");
|
export const logout = () => request("POST", "/auth/logout/");
|
||||||
|
|
||||||
|
// --- Huisstijl (schoollogo) -----------------------------------------------
|
||||||
|
export const getHuisstijl = () => request("GET", "/huisstijl/");
|
||||||
|
export const verwijderLogo = () => request("DELETE", "/huisstijl/");
|
||||||
|
export async function uploadLogo(file) {
|
||||||
|
const fd = new FormData();
|
||||||
|
fd.append("logo", file);
|
||||||
|
const token = getCookie("csrftoken");
|
||||||
|
const res = await fetch(`${BASE}/huisstijl/`, {
|
||||||
|
method: "PUT",
|
||||||
|
headers: token ? { "X-CSRFToken": token } : {},
|
||||||
|
credentials: "same-origin",
|
||||||
|
body: fd,
|
||||||
|
});
|
||||||
|
let data = null;
|
||||||
|
try { data = await res.json(); } catch { data = null; }
|
||||||
|
if (!res.ok) {
|
||||||
|
const err = new Error((data && (data.detail || JSON.stringify(data))) || "Upload mislukt");
|
||||||
|
err.status = res.status;
|
||||||
|
err.data = data;
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
// --- Plugin-framework -----------------------------------------------------
|
// --- Plugin-framework -----------------------------------------------------
|
||||||
export const getModules = () => request("GET", "/modules/");
|
export const getModules = () => request("GET", "/modules/");
|
||||||
export const setModuleState = (key, enabled) =>
|
export const setModuleState = (key, enabled) =>
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,9 @@ import {
|
||||||
wachtwoordReset,
|
wachtwoordReset,
|
||||||
adminWachtwoordReset,
|
adminWachtwoordReset,
|
||||||
gebruikersImport,
|
gebruikersImport,
|
||||||
|
getHuisstijl,
|
||||||
|
uploadLogo,
|
||||||
|
verwijderLogo,
|
||||||
} from "./api.js";
|
} from "./api.js";
|
||||||
|
|
||||||
const ROLLEN = [
|
const ROLLEN = [
|
||||||
|
|
@ -2077,7 +2080,8 @@ export function RoosterScherm({ mode }) {
|
||||||
const [printPending, setPrintPending] = useState(false);
|
const [printPending, setPrintPending] = useState(false);
|
||||||
const [printAllesActief, setPrintAllesActief] = useState(false);
|
const [printAllesActief, setPrintAllesActief] = useState(false);
|
||||||
const [printKop, setPrintKop] = useState("");
|
const [printKop, setPrintKop] = useState("");
|
||||||
const [printOpts, setPrintOpts] = useState({ bereik: "huidig", indeling: "kolom", kleur: false, begeleiding: true, lokaal: true, papier: "A4", orientatie: "landscape" });
|
const [printLegenda, setPrintLegenda] = useState(false);
|
||||||
|
const [printOpts, setPrintOpts] = useState({ bereik: "huidig", indeling: "kolom", kleur: false, begeleiding: true, lokaal: true, legenda: false, paginaPerDag: false, papier: "A4", orientatie: "landscape" });
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
listResource("schooljaren").then((j) => { setSchooljaren(j); if (j.length) setSchooljaar((s) => s || String(j[0].id)); }).catch((e) => setError(e.message));
|
listResource("schooljaren").then((j) => { setSchooljaren(j); if (j.length) setSchooljaar((s) => s || String(j[0].id)); }).catch((e) => setError(e.message));
|
||||||
|
|
@ -2105,6 +2109,12 @@ export function RoosterScherm({ mode }) {
|
||||||
// Bij printen "alle klassen" tijdelijk de scope negeren.
|
// Bij printen "alle klassen" tijdelijk de scope negeren.
|
||||||
const effScope = printAllesActief ? { type: "alles", id: "" } : scope;
|
const effScope = printAllesActief ? { type: "alles", id: "" } : scope;
|
||||||
const zichtbareBlokken = blokken.filter((b) => blokInScope(b, effScope, scopeCtx));
|
const zichtbareBlokken = blokken.filter((b) => blokInScope(b, effScope, scopeCtx));
|
||||||
|
// Distinct vakken (voor de optionele kleurenlegenda bij het afdrukken).
|
||||||
|
const legendaItems = (() => {
|
||||||
|
const seen = new Set(); const out = [];
|
||||||
|
zichtbareBlokken.forEach((b) => { if (!seen.has(b.activiteit_naam)) { seen.add(b.activiteit_naam); out.push({ naam: b.activiteit_naam, kleur: b.activiteit_kleur, icoon: b.activiteit_icoon }); } });
|
||||||
|
return out.sort((a, x) => a.naam.localeCompare(x.naam));
|
||||||
|
})();
|
||||||
// Conflicten over álle blokken (niet alleen de zichtbare scope), zodat de
|
// Conflicten over álle blokken (niet alleen de zichtbare scope), zodat de
|
||||||
// melding compleet is en de betrokken blokken in het rooster omlijnd worden.
|
// melding compleet is en de betrokken blokken in het rooster omlijnd worden.
|
||||||
const conflictInfo = berekenRoosterConflicten(blokken, scopeCtx, tijdsloten);
|
const conflictInfo = berekenRoosterConflicten(blokken, scopeCtx, tijdsloten);
|
||||||
|
|
@ -2215,8 +2225,10 @@ export function RoosterScherm({ mode }) {
|
||||||
b.toggle("print-kleur", !!printOpts.kleur);
|
b.toggle("print-kleur", !!printOpts.kleur);
|
||||||
b.toggle("print-geen-begeleiding", !printOpts.begeleiding);
|
b.toggle("print-geen-begeleiding", !printOpts.begeleiding);
|
||||||
b.toggle("print-geen-lokaal", !printOpts.lokaal);
|
b.toggle("print-geen-lokaal", !printOpts.lokaal);
|
||||||
|
b.toggle("print-paginadag", !!printOpts.paginaPerDag);
|
||||||
zetPaginaStijl(printOpts.papier, printOpts.orientatie);
|
zetPaginaStijl(printOpts.papier, printOpts.orientatie);
|
||||||
setPrintKop(printOpts.kop || "");
|
setPrintKop(printOpts.kop || "");
|
||||||
|
setPrintLegenda(!!printOpts.legenda);
|
||||||
if (printOpts.bereik === "alles") setPrintAllesActief(true);
|
if (printOpts.bereik === "alles") setPrintAllesActief(true);
|
||||||
setPrintOpen(false);
|
setPrintOpen(false);
|
||||||
setPrintPending(true);
|
setPrintPending(true);
|
||||||
|
|
@ -2510,8 +2522,17 @@ export function RoosterScherm({ mode }) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="rooster-scherm">
|
<section className="rooster-scherm">
|
||||||
{printKop && <div className="print-only print-kop">{printKop}</div>}
|
<div className="print-only print-kop">
|
||||||
|
<RoosterLogo print />
|
||||||
|
<div className="print-kop-tekst">
|
||||||
|
<strong>{printKop || (edit ? "Roostermaker" : "Rooster")}</strong>
|
||||||
|
<span>{(schooljaren.find((j) => String(j.id) === String(schooljaar)) || {}).naam || ""}{schooljaar ? " · " : ""}{schooljaar ? weekTitel(maandag) : ""} · afgedrukt {fmtDM(new Date())}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="rooster-kop">
|
||||||
<h1>{edit ? "Roostermaker" : "Rooster"}</h1>
|
<h1>{edit ? "Roostermaker" : "Rooster"}</h1>
|
||||||
|
<RoosterLogo />
|
||||||
|
</div>
|
||||||
<p className="muted no-print">
|
<p className="muted no-print">
|
||||||
{edit
|
{edit
|
||||||
? "Sleep een blok naar een ander tijdslot om het te verplaatsen. Vrije dagen, afwezigheid en uitzonderingen worden meegenomen."
|
? "Sleep een blok naar een ander tijdslot om het te verplaatsen. Vrije dagen, afwezigheid en uitzonderingen worden meegenomen."
|
||||||
|
|
@ -2770,6 +2791,17 @@ export function RoosterScherm({ mode }) {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{schooljaar && tab === "week" && printLegenda && legendaItems.length > 0 && (
|
||||||
|
<div className="print-only kleur-legenda">
|
||||||
|
<strong>Legenda</strong>
|
||||||
|
<div className="legenda-items">
|
||||||
|
{legendaItems.map((v) => (
|
||||||
|
<span className="legenda-item" key={v.naam}><span className="legenda-swatch" style={{ background: v.kleur }} />{v.icoon || vakIcoon(v.naam)} {v.naam}</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{edit && tab === "wizard" && blokwizardAan && <VakWizardPage />}
|
{edit && tab === "wizard" && blokwizardAan && <VakWizardPage />}
|
||||||
|
|
||||||
{edit && tab === "auto" && autoroosterAan && <AutoRoosterPage />}
|
{edit && tab === "auto" && autoroosterAan && <AutoRoosterPage />}
|
||||||
|
|
@ -2797,6 +2829,10 @@ export function RoosterScherm({ mode }) {
|
||||||
<summary><span className="ico">📍</span> Locaties</summary>
|
<summary><span className="ico">📍</span> Locaties</summary>
|
||||||
<div className="inst-body"><p className="muted small">Vrije locatie-aanduidingen (bv. gymzaal, zwembad).</p><LocatiesPage /></div>
|
<div className="inst-body"><p className="muted small">Vrije locatie-aanduidingen (bv. gymzaal, zwembad).</p><LocatiesPage /></div>
|
||||||
</details>
|
</details>
|
||||||
|
<details className="inst-sectie">
|
||||||
|
<summary><span className="ico">🖼️</span> Huisstijl / logo</summary>
|
||||||
|
<div className="inst-body"><HuisstijlPage /></div>
|
||||||
|
</details>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
@ -2850,6 +2886,8 @@ export function RoosterScherm({ mode }) {
|
||||||
<label className="check-item"><input type="checkbox" checked={printOpts.kleur} onChange={(e) => setPrintOpts({ ...printOpts, kleur: e.target.checked })} /><span>Vakken in kleur</span></label>
|
<label className="check-item"><input type="checkbox" checked={printOpts.kleur} onChange={(e) => setPrintOpts({ ...printOpts, kleur: e.target.checked })} /><span>Vakken in kleur</span></label>
|
||||||
<label className="check-item"><input type="checkbox" checked={printOpts.begeleiding} onChange={(e) => setPrintOpts({ ...printOpts, begeleiding: e.target.checked })} /><span>Begeleiding tonen</span></label>
|
<label className="check-item"><input type="checkbox" checked={printOpts.begeleiding} onChange={(e) => setPrintOpts({ ...printOpts, begeleiding: e.target.checked })} /><span>Begeleiding tonen</span></label>
|
||||||
<label className="check-item"><input type="checkbox" checked={printOpts.lokaal} onChange={(e) => setPrintOpts({ ...printOpts, lokaal: e.target.checked })} /><span>Lokaal/locatie tonen</span></label>
|
<label className="check-item"><input type="checkbox" checked={printOpts.lokaal} onChange={(e) => setPrintOpts({ ...printOpts, lokaal: e.target.checked })} /><span>Lokaal/locatie tonen</span></label>
|
||||||
|
<label className="check-item"><input type="checkbox" checked={printOpts.legenda} onChange={(e) => setPrintOpts({ ...printOpts, legenda: e.target.checked })} /><span>Kleurenlegenda toevoegen</span></label>
|
||||||
|
<label className="check-item"><input type="checkbox" checked={printOpts.paginaPerDag} onChange={(e) => setPrintOpts({ ...printOpts, paginaPerDag: e.target.checked })} /><span>Elke dag op nieuwe pagina</span></label>
|
||||||
</div>
|
</div>
|
||||||
<div className="editor-actions">
|
<div className="editor-actions">
|
||||||
<button type="button" className="btn primary" onClick={startPrint}>Afdrukken</button>
|
<button type="button" className="btn primary" onClick={startPrint}>Afdrukken</button>
|
||||||
|
|
@ -3048,7 +3086,7 @@ export function PeriodeRoosterPage() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="weekmaker">
|
<section className="weekmaker">
|
||||||
<h1>Perioderooster</h1>
|
<div className="rooster-kop"><h1>Perioderooster</h1><RoosterLogo /></div>
|
||||||
<p className="muted no-print">Geef alle klassen in één overzicht hun weekvakken. Je bouwt de weekopzet hier eerst los van de kalender; bij <b>voltooien</b> kies je de periode waarin dit rooster gaat gelden. Dezelfde begeleider of hetzelfde lokaal op hetzelfde moment wordt meteen als conflict gemarkeerd.</p>
|
<p className="muted no-print">Geef alle klassen in één overzicht hun weekvakken. Je bouwt de weekopzet hier eerst los van de kalender; bij <b>voltooien</b> kies je de periode waarin dit rooster gaat gelden. Dezelfde begeleider of hetzelfde lokaal op hetzelfde moment wordt meteen als conflict gemarkeerd.</p>
|
||||||
<Foutmelding msg={error} />
|
<Foutmelding msg={error} />
|
||||||
|
|
||||||
|
|
@ -3237,6 +3275,75 @@ export function PeriodeRoosterPage() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* -------------------------------- Huisstijl / logo ------------------------ */
|
||||||
|
// Klein logo dat in elke roosterweergave en op afdrukken verschijnt.
|
||||||
|
export function RoosterLogo({ print = false }) {
|
||||||
|
const [logo, setLogo] = useState(null);
|
||||||
|
useEffect(() => { getHuisstijl().then((h) => setLogo(h && h.logo ? h.logo : null)).catch(() => {}); }, []);
|
||||||
|
if (!logo) return null;
|
||||||
|
return <img src={logo} alt="Logo" className={print ? "rooster-logo print-logo" : "rooster-logo no-print"} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function HuisstijlPage() {
|
||||||
|
const [huis, setHuis] = useState(null);
|
||||||
|
const [kwaliteit, setKwaliteit] = useState(null);
|
||||||
|
const [bezig, setBezig] = useState(false);
|
||||||
|
const [error, setError] = useState(null);
|
||||||
|
|
||||||
|
useEffect(() => { getHuisstijl().then(setHuis).catch((e) => setError(e.message)); }, []);
|
||||||
|
|
||||||
|
async function kies(e) {
|
||||||
|
const f = e.target.files && e.target.files[0];
|
||||||
|
e.target.value = "";
|
||||||
|
if (!f) return;
|
||||||
|
setBezig(true); setError(null); setKwaliteit(null);
|
||||||
|
try {
|
||||||
|
const r = await uploadLogo(f);
|
||||||
|
setHuis(r);
|
||||||
|
setKwaliteit(r.kwaliteit || null);
|
||||||
|
} catch (err) {
|
||||||
|
setError(err.message);
|
||||||
|
if (err.data && err.data.kwaliteit) setKwaliteit(err.data.kwaliteit);
|
||||||
|
}
|
||||||
|
setBezig(false);
|
||||||
|
}
|
||||||
|
async function wis() {
|
||||||
|
setBezig(true); setError(null);
|
||||||
|
try { const r = await verwijderLogo(); setHuis(r); setKwaliteit(null); } catch (e) { setError(e.message); }
|
||||||
|
setBezig(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="huisstijl">
|
||||||
|
<Foutmelding msg={error} />
|
||||||
|
<p className="muted small">Het logo verschijnt in elke roosterweergave en op afdrukken. Gebruik bij voorkeur een <b>transparante PNG</b> of <b>SVG</b>; een logo met een gevulde (witte) achtergrond valt op een gekleurde balk lelijk op.</p>
|
||||||
|
<div className="logo-vak">
|
||||||
|
<div className="logo-preview" title="Voorbeeld op een geruite achtergrond (zo zie je transparantie)">
|
||||||
|
{huis && huis.logo ? <img src={huis.logo} alt="Logo" /> : <span className="muted small">Nog geen logo</span>}
|
||||||
|
</div>
|
||||||
|
<div className="logo-acties">
|
||||||
|
<label className="btn primary">{bezig ? "Bezig…" : (huis && huis.logo ? "Ander logo kiezen" : "Logo kiezen")}
|
||||||
|
<input type="file" accept=".png,.svg,.jpg,.jpeg,.gif,.webp,image/*" style={{ display: "none" }} onChange={kies} disabled={bezig} />
|
||||||
|
</label>
|
||||||
|
{huis && huis.logo && <button type="button" className="btn danger" onClick={wis} disabled={bezig}>Verwijderen</button>}
|
||||||
|
<div className="muted small">Toegestaan: PNG, SVG, JPG, GIF, WebP.</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{kwaliteit && (
|
||||||
|
<div className={kwaliteit.waarschuwingen && kwaliteit.waarschuwingen.length ? "banner waarschuwing" : "banner ok"}>
|
||||||
|
<b>Kwaliteitscheck:</b>
|
||||||
|
{kwaliteit.info && (kwaliteit.info.breedte || kwaliteit.info.formaat) && (
|
||||||
|
<div className="small">Formaat {kwaliteit.info.formaat}{kwaliteit.info.breedte ? `, ${kwaliteit.info.breedte}×${kwaliteit.info.hoogte}px` : ""}{kwaliteit.info.transparant ? ", transparante achtergrond ✓" : ""}{kwaliteit.info.achtergrond ? `, achtergrond ${kwaliteit.info.achtergrond}` : ""}.</div>
|
||||||
|
)}
|
||||||
|
{kwaliteit.waarschuwingen && kwaliteit.waarschuwingen.length > 0
|
||||||
|
? <ul className="conflict-lijst">{kwaliteit.waarschuwingen.map((w, i) => <li key={i} className="waarschuwing">{w}</li>)}</ul>
|
||||||
|
: <div className="small">Geen problemen gevonden — mooi en netjes. ✓</div>}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/* -------------------------------- Urenoverzicht --------------------------- */
|
/* -------------------------------- Urenoverzicht --------------------------- */
|
||||||
export function UrenPage() {
|
export function UrenPage() {
|
||||||
const [schooljaren, setSchooljaren] = useState([]);
|
const [schooljaren, setSchooljaren] = useState([]);
|
||||||
|
|
@ -3457,7 +3564,7 @@ export function EenvoudigRooster({ mode }) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="ez">
|
<section className="ez">
|
||||||
<h1>{ouder ? "Weekrooster" : "Mijn rooster"}</h1>
|
<div className="rooster-kop"><h1>{ouder ? "Weekrooster" : "Mijn rooster"}</h1><RoosterLogo /></div>
|
||||||
<Foutmelding msg={error} />
|
<Foutmelding msg={error} />
|
||||||
<div className="form-row ez-bar no-print">
|
<div className="form-row ez-bar no-print">
|
||||||
<select value={scopeType} onChange={(e) => { setScopeType(e.target.value); setScopeId(""); }}>
|
<select value={scopeType} onChange={(e) => { setScopeType(e.target.value); setScopeId(""); }}>
|
||||||
|
|
|
||||||
|
|
@ -346,6 +346,10 @@ input[type="color"] { padding: 2px; width: 44px; min-width: 44px; height: 38px;
|
||||||
/* Vakken in kleur afdrukken */
|
/* Vakken in kleur afdrukken */
|
||||||
body.print-kleur .vak-kaart { background: var(--vk-kleur) !important; color: var(--vk-tekst) !important; -webkit-print-color-adjust: exact; print-color-adjust: exact; }
|
body.print-kleur .vak-kaart { background: var(--vk-kleur) !important; color: var(--vk-tekst) !important; -webkit-print-color-adjust: exact; print-color-adjust: exact; }
|
||||||
body.print-kleur .vak-kaart .vk-naam, body.print-kleur .vak-kaart .muted { color: inherit !important; }
|
body.print-kleur .vak-kaart .vk-naam, body.print-kleur .vak-kaart .muted { color: inherit !important; }
|
||||||
|
|
||||||
|
/* Elke dag op een nieuwe pagina (in 'dagen onder elkaar') */
|
||||||
|
body.print-paginadag.print-lijst .rooster-kolom { break-after: page; page-break-after: always; }
|
||||||
|
body.print-paginadag.print-lijst .rooster-kolom:last-child { break-after: auto; page-break-after: auto; }
|
||||||
}
|
}
|
||||||
|
|
||||||
/* --- Onderkant zijbalk: subtiel Instellingen-blok --- */
|
/* --- Onderkant zijbalk: subtiel Instellingen-blok --- */
|
||||||
|
|
@ -580,7 +584,31 @@ input[type="color"] { padding: 2px; width: 44px; min-width: 44px; height: 38px;
|
||||||
.vak-kaart.kleur-vlak { background: var(--vk-kleur); color: var(--vk-tekst); }
|
.vak-kaart.kleur-vlak { background: var(--vk-kleur); color: var(--vk-tekst); }
|
||||||
.vak-kaart.kleur-vlak .vk-naam { color: inherit; }
|
.vak-kaart.kleur-vlak .vk-naam { color: inherit; }
|
||||||
.print-only { display: none; }
|
.print-only { display: none; }
|
||||||
.print-kop { font-size: 16px; font-weight: 700; margin: 0 0 8px; }
|
.print-kop { display: flex; align-items: center; gap: 12px; margin: 0 0 8px; }
|
||||||
|
.print-kop-tekst { display: flex; flex-direction: column; }
|
||||||
|
.print-kop-tekst strong { font-size: 16px; }
|
||||||
|
.print-kop-tekst span { font-size: 12px; color: #444; }
|
||||||
|
.print-logo { max-height: 46px; max-width: 180px; object-fit: contain; }
|
||||||
|
|
||||||
|
/* Schoollogo in de roosterweergave */
|
||||||
|
.rooster-kop { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
|
||||||
|
.rooster-logo { max-height: 40px; max-width: 150px; object-fit: contain; }
|
||||||
|
|
||||||
|
/* Huisstijl-pagina: logo-upload + voorbeeld op geruite achtergrond */
|
||||||
|
.huisstijl .logo-vak { display: flex; flex-wrap: wrap; gap: 16px; align-items: center; margin: 10px 0; }
|
||||||
|
.logo-preview { width: 220px; height: 120px; display: grid; place-items: center; border: 1px solid var(--line); border-radius: 10px;
|
||||||
|
background-color: #fff;
|
||||||
|
background-image: linear-gradient(45deg, #e7e8ec 25%, transparent 25%), linear-gradient(-45deg, #e7e8ec 25%, transparent 25%), linear-gradient(45deg, transparent 75%, #e7e8ec 75%), linear-gradient(-45deg, transparent 75%, #e7e8ec 75%);
|
||||||
|
background-size: 16px 16px; background-position: 0 0, 0 8px, 8px -8px, -8px 0; }
|
||||||
|
.logo-preview img { max-width: 200px; max-height: 104px; object-fit: contain; }
|
||||||
|
.logo-acties { display: flex; flex-direction: column; gap: 8px; align-items: flex-start; }
|
||||||
|
.logo-acties .btn { cursor: pointer; }
|
||||||
|
|
||||||
|
/* Kleurenlegenda (alleen bij afdrukken) */
|
||||||
|
.kleur-legenda { margin-top: 10px; padding-top: 8px; border-top: 1px solid #ccc; }
|
||||||
|
.legenda-items { display: flex; flex-wrap: wrap; gap: 6px 14px; margin-top: 4px; }
|
||||||
|
.legenda-item { display: inline-flex; align-items: center; gap: 5px; font-size: 12px; }
|
||||||
|
.legenda-swatch { width: 12px; height: 12px; border-radius: 3px; display: inline-block; border: 1px solid rgba(0,0,0,.15); -webkit-print-color-adjust: exact; print-color-adjust: exact; }
|
||||||
.vak-kaart.kleur-vlak .rb-greep, .vak-kaart.kleur-vlak .icon-btn { color: inherit; opacity: .85; }
|
.vak-kaart.kleur-vlak .rb-greep, .vak-kaart.kleur-vlak .icon-btn { color: inherit; opacity: .85; }
|
||||||
.vak-kaart.kleur-vlak .rb-status, .vak-kaart.kleur-vlak .rb-verv { color: inherit; }
|
.vak-kaart.kleur-vlak .rb-status, .vak-kaart.kleur-vlak .rb-verv { color: inherit; }
|
||||||
.wm-vak.kleur-vlak .wm-vak-naam { color: inherit; }
|
.wm-vak.kleur-vlak .wm-vak-naam { color: inherit; }
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue