diff --git a/ROADMAP.md b/ROADMAP.md index 0b5fee6..cbec2f6 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -46,10 +46,13 @@ volgt. 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] **Module Vormgeving / thema** (`huisstijl`, default aan): schoollogo + uploaden via Instellingen (PNG/SVG/JPG/GIF/WebP) met **kwaliteitscheck** + (bestandstype, resolutie, transparantie/achtergrond). Logo verschijnt in + elke roosterweergave en op afdrukken. Daarnaast **schoolnaam** (in de balk + + op afdrukken) en **thema-kleuren** (primair + accent) die app-breed + worden toegepast als CSS-variabelen. Geüploade media wereldwijd-leesbaar + (FILE_UPLOAD_PERMISSIONS) en self-healing volume-rechten via gosu-entrypoint. - [x] **Vakkleur als vlak** (toggle 🎨, onthouden per browser): vak volledig in de activiteitskleur i.p.v. een streepje, met automatisch leesbare tekstkleur diff --git a/backend/config/settings.py b/backend/config/settings.py index c36645c..5e5485b 100644 --- a/backend/config/settings.py +++ b/backend/config/settings.py @@ -155,9 +155,13 @@ USE_TZ = True STATIC_URL = "/static/" STATIC_ROOT = os.environ.get("DJANGO_STATIC_ROOT", str(BASE_DIR / "staticfiles")) -# Geüploade bestanden (o.a. pictogrammen bij vakken). +# Geüploade bestanden (o.a. pictogrammen bij vakken, schoollogo). MEDIA_URL = "/media/" MEDIA_ROOT = os.environ.get("DJANGO_MEDIA_ROOT", str(BASE_DIR / "media")) +# Geüploade bestanden wereldwijd-leesbaar wegschrijven, anders kan nginx (die als +# een andere gebruiker draait) ze niet serveren → gebroken afbeeldingen. +FILE_UPLOAD_PERMISSIONS = 0o644 +FILE_UPLOAD_DIRECTORY_PERMISSIONS = 0o755 # E-mail (voor o.a. wachtwoordreset). Zonder SMTP-config gaat e-mail naar de # console-log (handig voor test); zet DJANGO_EMAIL_BACKEND/EMAIL_* in productie. diff --git a/backend/modules/huisstijl/apps.py b/backend/modules/huisstijl/apps.py index 7336134..ff0e417 100644 --- a/backend/modules/huisstijl/apps.py +++ b/backend/modules/huisstijl/apps.py @@ -5,7 +5,7 @@ class HuisstijlConfig(AppConfig): default_auto_field = "django.db.models.BigAutoField" name = "modules.huisstijl" label = "module_huisstijl" - verbose_name = "Module: Logo" + verbose_name = "Module: Vormgeving" def ready(self): from plugins.registry import MenuItem, ModuleSpec, registry @@ -13,7 +13,7 @@ class HuisstijlConfig(AppConfig): registry.register( ModuleSpec( key="huisstijl", - name="Logo", + name="Vormgeving", description=( "Toont het schoollogo in elke roosterweergave en op afdrukken. " "Logo te uploaden via Instellingen, met een kwaliteitscheck " @@ -24,7 +24,7 @@ class HuisstijlConfig(AppConfig): core=False, default_enabled=True, menu_items=( - MenuItem("Logo", "/logo", icon="badge", order=74, group="Instellingen"), + MenuItem("Vormgeving", "/vormgeving", icon="badge", order=74, group="Instellingen"), ), ) ) diff --git a/backend/modules/huisstijl/migrations/0002_thema_velden.py b/backend/modules/huisstijl/migrations/0002_thema_velden.py new file mode 100644 index 0000000..75264f9 --- /dev/null +++ b/backend/modules/huisstijl/migrations/0002_thema_velden.py @@ -0,0 +1,26 @@ +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("module_huisstijl", "0001_initial"), + ] + + operations = [ + migrations.AddField( + model_name="huisstijl", + name="schoolnaam", + field=models.CharField(blank=True, default="", max_length=120, verbose_name="schoolnaam"), + ), + migrations.AddField( + model_name="huisstijl", + name="primaire_kleur", + field=models.CharField(blank=True, default="", max_length=9, verbose_name="primaire kleur"), + ), + migrations.AddField( + model_name="huisstijl", + name="accent_kleur", + field=models.CharField(blank=True, default="", max_length=9, verbose_name="accentkleur"), + ), + ] diff --git a/backend/modules/huisstijl/models.py b/backend/modules/huisstijl/models.py index e616798..7fb2215 100644 --- a/backend/modules/huisstijl/models.py +++ b/backend/modules/huisstijl/models.py @@ -2,10 +2,14 @@ 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.""" + """Eén-rij-instelling met de vormgeving (thema) van de school: logo, + schoolnaam en kleuren. Wordt in elke roosterweergave en op afdrukken + toegepast.""" logo = models.FileField("logo", upload_to="huisstijl/", null=True, blank=True) + schoolnaam = models.CharField("schoolnaam", max_length=120, blank=True, default="") + primaire_kleur = models.CharField("primaire kleur", max_length=9, blank=True, default="") + accent_kleur = models.CharField("accentkleur", max_length=9, blank=True, default="") aangepast_op = models.DateTimeField(auto_now=True) class Meta: diff --git a/backend/modules/huisstijl/serializers.py b/backend/modules/huisstijl/serializers.py index 6a368dd..016db9e 100644 --- a/backend/modules/huisstijl/serializers.py +++ b/backend/modules/huisstijl/serializers.py @@ -8,4 +8,4 @@ class HuisstijlSerializer(serializers.ModelSerializer): class Meta: model = Huisstijl - fields = ["id", "logo", "aangepast_op"] + fields = ["id", "logo", "schoolnaam", "primaire_kleur", "accent_kleur", "aangepast_op"] diff --git a/backend/modules/huisstijl/views.py b/backend/modules/huisstijl/views.py index 0330bb1..fddbc66 100644 --- a/backend/modules/huisstijl/views.py +++ b/backend/modules/huisstijl/views.py @@ -109,25 +109,39 @@ def huisstijl(request): return Response(HuisstijlSerializer(obj, context={"request": request}).data) if request.method in ("PUT", "POST"): + analyse = None + # Thema-tekstvelden (kleuren/naam) — uit JSON of form-data. + for veld in ("schoolnaam", "primaire_kleur", "accent_kleur"): + if veld in request.data: + waarde = (request.data.get(veld) or "").strip() + if veld.endswith("_kleur") and waarde and not _is_kleur(waarde): + return Response({"detail": f"Ongeldige kleur voor {veld}: gebruik een hex-code zoals #2563eb."}, status=400) + setattr(obj, veld, waarde) + # Optioneel een nieuw logo. f = request.FILES.get("logo") - if not f: - return Response({"detail": "Geen bestand ontvangen (veld 'logo')."}, status=400) - try: - analyse = analyseer_logo(f) - except Exception as e: # noqa: BLE001 — geef een leesbare reden i.p.v. 500. - return Response({"detail": f"Kon de afbeelding niet verwerken: {e}"}, status=400) - if not analyse["ok"]: - return Response({"detail": " ".join(analyse["fouten"]), "kwaliteit": analyse}, status=400) - try: + if f: + try: + analyse = analyseer_logo(f) + except Exception as e: # noqa: BLE001 — geef een leesbare reden i.p.v. 500. + return Response({"detail": f"Kon de afbeelding niet verwerken: {e}"}, status=400) + 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 + try: obj.save() except Exception as e: # noqa: BLE001 - return Response({"detail": f"Opslaan van het logo mislukte: {e}"}, status=400) + return Response({"detail": f"Opslaan mislukte: {e}"}, status=400) data = HuisstijlSerializer(obj, context={"request": request}).data - data["kwaliteit"] = analyse + if analyse is not None: + data["kwaliteit"] = analyse return Response(data) return Response(HuisstijlSerializer(obj, context={"request": request}).data) + + +def _is_kleur(s): + import re + return bool(re.fullmatch(r"#([0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})", s or "")) diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index f750a24..d39059c 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -1,5 +1,5 @@ import { useEffect, useMemo, useState } from "react"; -import { getMe, login, logout, getModules, setModuleState, wachtwoordVergeten } from "./api.js"; +import { getMe, login, logout, getModules, setModuleState, wachtwoordVergeten, getHuisstijl } from "./api.js"; import Logo from "./Logo.jsx"; import { PersonenPage, FunctiesPage, @@ -47,9 +47,29 @@ const PAGES = { "/vak-wizard": VakWizardPage, "/personen-wizard": PersonenWizardPage, "/auto-rooster": AutoRoosterPage, - "/logo": HuisstijlPage, + "/vormgeving": HuisstijlPage, }; +// Thema (vormgeving) app-breed toepassen via CSS-variabelen. +function hexNaarRgba(hex, a) { + if (!hex) return ""; + const m = hex.replace("#", ""); + const v = m.length === 3 ? m.split("").map((c) => c + c).join("") : m; + if (v.length < 6) return ""; + const r = parseInt(v.slice(0, 2), 16), g = parseInt(v.slice(2, 4), 16), b = parseInt(v.slice(4, 6), 16); + if ([r, g, b].some(Number.isNaN)) return ""; + return `rgba(${r}, ${g}, ${b}, ${a})`; +} +export function pasThemaToe(h) { + const root = document.documentElement; + if (h && h.primaire_kleur) { + root.style.setProperty("--brand", h.primaire_kleur); + const soft = hexNaarRgba(h.primaire_kleur, 0.14); + if (soft) root.style.setProperty("--brand-soft", soft); + } + if (h && h.accent_kleur) root.style.setProperty("--accent", h.accent_kleur); +} + const RESET_PARAMS = (() => { try { const q = new URLSearchParams(window.location.search); @@ -181,6 +201,11 @@ function Shell({ user, onLogout }) { const [loading, setLoading] = useState(true); const [cogOpen, setCogOpen] = useState(false); const [openMenu, setOpenMenu] = useState(null); + const [schoolnaam, setSchoolnaam] = useState(""); + + useEffect(() => { + getHuisstijl().then((h) => { pasThemaToe(h); setSchoolnaam(h && h.schoolnaam ? h.schoolnaam : ""); }).catch(() => {}); + }, []); async function load() { setLoading(true); @@ -338,6 +363,7 @@ function Shell({ user, onLogout }) {
Vrije locatie-aanduidingen (bv. gymzaal, zwembad).
De schoolnaam verschijnt in de balk en op afdrukken. De kleuren bepalen het thema van de hele app (knoppen, tabs, accenten).
+Het logo verschijnt in elke roosterweergave en op afdrukken. Gebruik bij voorkeur een transparante PNG of SVG; een logo met een gevulde (witte) achtergrond valt op een gekleurde balk lelijk op.