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
|
||||
selectie / alle klassen) en hóé je afdrukt — indeling (kolommen / dagen
|
||||
onder elkaar), vakken in kleur, begeleiding/lokaal tonen, optionele
|
||||
koptekst — plus **papierformaat** (A4/A3/A5/Letter) en oriëntatie
|
||||
(liggend/staand). Toegepast via body-classes + dynamische `@page`.
|
||||
koptekst, **kleurenlegenda** en **elke dag op nieuwe pagina** — plus
|
||||
**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
|
||||
activiteitskleur i.p.v. een streepje, met automatisch leesbare tekstkleur
|
||||
|
|
|
|||
|
|
@ -78,6 +78,7 @@ MODULE_APPS = [
|
|||
"modules.blokwizard", # Vak-wizard
|
||||
"modules.personenwizard", # Personen-wizard & import
|
||||
"modules.autorooster", # Automatisch rooster
|
||||
"modules.huisstijl", # Schoollogo / huisstijl
|
||||
]
|
||||
|
||||
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.autorooster.urls")),
|
||||
path("api/", include("modules.accounts.urls")),
|
||||
path("api/", include("modules.huisstijl.urls")),
|
||||
]
|
||||
|
||||
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,
|
||||
LeerlingRoosterPage, OuderRoosterPage,
|
||||
VakWizardPage, PersonenWizardPage, AutoRoosterPage, WachtwoordResetPage,
|
||||
HuisstijlPage,
|
||||
} from "./pages.jsx";
|
||||
|
||||
const ICONS = {
|
||||
|
|
@ -46,6 +47,7 @@ const PAGES = {
|
|||
"/vak-wizard": VakWizardPage,
|
||||
"/personen-wizard": PersonenWizardPage,
|
||||
"/auto-rooster": AutoRoosterPage,
|
||||
"/huisstijl": HuisstijlPage,
|
||||
};
|
||||
|
||||
const RESET_PARAMS = (() => {
|
||||
|
|
|
|||
|
|
@ -43,6 +43,30 @@ export const login = (username, password) =>
|
|||
request("POST", "/auth/login/", { username, password });
|
||||
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 -----------------------------------------------------
|
||||
export const getModules = () => request("GET", "/modules/");
|
||||
export const setModuleState = (key, enabled) =>
|
||||
|
|
|
|||
|
|
@ -11,6 +11,9 @@ import {
|
|||
wachtwoordReset,
|
||||
adminWachtwoordReset,
|
||||
gebruikersImport,
|
||||
getHuisstijl,
|
||||
uploadLogo,
|
||||
verwijderLogo,
|
||||
} from "./api.js";
|
||||
|
||||
const ROLLEN = [
|
||||
|
|
@ -2077,7 +2080,8 @@ export function RoosterScherm({ mode }) {
|
|||
const [printPending, setPrintPending] = useState(false);
|
||||
const [printAllesActief, setPrintAllesActief] = useState(false);
|
||||
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(() => {
|
||||
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.
|
||||
const effScope = printAllesActief ? { type: "alles", id: "" } : scope;
|
||||
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
|
||||
// melding compleet is en de betrokken blokken in het rooster omlijnd worden.
|
||||
const conflictInfo = berekenRoosterConflicten(blokken, scopeCtx, tijdsloten);
|
||||
|
|
@ -2215,8 +2225,10 @@ export function RoosterScherm({ mode }) {
|
|||
b.toggle("print-kleur", !!printOpts.kleur);
|
||||
b.toggle("print-geen-begeleiding", !printOpts.begeleiding);
|
||||
b.toggle("print-geen-lokaal", !printOpts.lokaal);
|
||||
b.toggle("print-paginadag", !!printOpts.paginaPerDag);
|
||||
zetPaginaStijl(printOpts.papier, printOpts.orientatie);
|
||||
setPrintKop(printOpts.kop || "");
|
||||
setPrintLegenda(!!printOpts.legenda);
|
||||
if (printOpts.bereik === "alles") setPrintAllesActief(true);
|
||||
setPrintOpen(false);
|
||||
setPrintPending(true);
|
||||
|
|
@ -2510,8 +2522,17 @@ export function RoosterScherm({ mode }) {
|
|||
|
||||
return (
|
||||
<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>
|
||||
<RoosterLogo />
|
||||
</div>
|
||||
<p className="muted no-print">
|
||||
{edit
|
||||
? "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>
|
||||
)}
|
||||
|
||||
{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 === "auto" && autoroosterAan && <AutoRoosterPage />}
|
||||
|
|
@ -2797,6 +2829,10 @@ export function RoosterScherm({ mode }) {
|
|||
<summary><span className="ico">📍</span> Locaties</summary>
|
||||
<div className="inst-body"><p className="muted small">Vrije locatie-aanduidingen (bv. gymzaal, zwembad).</p><LocatiesPage /></div>
|
||||
</details>
|
||||
<details className="inst-sectie">
|
||||
<summary><span className="ico">🖼️</span> Huisstijl / logo</summary>
|
||||
<div className="inst-body"><HuisstijlPage /></div>
|
||||
</details>
|
||||
</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.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.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 className="editor-actions">
|
||||
<button type="button" className="btn primary" onClick={startPrint}>Afdrukken</button>
|
||||
|
|
@ -3048,7 +3086,7 @@ export function PeriodeRoosterPage() {
|
|||
|
||||
return (
|
||||
<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>
|
||||
<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 --------------------------- */
|
||||
export function UrenPage() {
|
||||
const [schooljaren, setSchooljaren] = useState([]);
|
||||
|
|
@ -3457,7 +3564,7 @@ export function EenvoudigRooster({ mode }) {
|
|||
|
||||
return (
|
||||
<section className="ez">
|
||||
<h1>{ouder ? "Weekrooster" : "Mijn rooster"}</h1>
|
||||
<div className="rooster-kop"><h1>{ouder ? "Weekrooster" : "Mijn rooster"}</h1><RoosterLogo /></div>
|
||||
<Foutmelding msg={error} />
|
||||
<div className="form-row ez-bar no-print">
|
||||
<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 */
|
||||
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; }
|
||||
|
||||
/* 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 --- */
|
||||
|
|
@ -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 .vk-naam { color: inherit; }
|
||||
.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-status, .vak-kaart.kleur-vlak .rb-verv { color: inherit; }
|
||||
.wm-vak.kleur-vlak .wm-vak-naam { color: inherit; }
|
||||
|
|
|
|||
Loading…
Reference in a new issue