From c49531f692d6d33baeb20917e85c1fc4b49df1ce Mon Sep 17 00:00:00 2001 From: bes-r <27369668+bes-r@users.noreply.github.com> Date: Mon, 15 Jun 2026 22:52:46 +0200 Subject: [PATCH] Hernoem Leerplein naar Bovenliggende groepen; route /groepsindeling; categorie Groepsindeling --- backend/config/settings.py | 2 +- backend/core/apps.py | 2 +- .../0003_rename_leerplein_verbose_names.py | 19 +++++++++++ backend/modules/leerplein/models.py | 10 +++--- backend/modules/stage/apps.py | 2 +- frontend/src/App.jsx | 16 +++++----- frontend/src/pages.jsx | 32 +++++++++---------- frontend/src/styles.css | 27 ++++++---------- 8 files changed, 61 insertions(+), 49 deletions(-) create mode 100644 backend/modules/leerplein/migrations/0003_rename_leerplein_verbose_names.py diff --git a/backend/config/settings.py b/backend/config/settings.py index 8e25f7c..2a77179 100644 --- a/backend/config/settings.py +++ b/backend/config/settings.py @@ -72,7 +72,7 @@ CORE_APPS = [ MODULE_APPS = [ "modules.printing", # Printen/Exporteren "modules.accounts", # Multi-user & toegang - "modules.leerplein", # Leerplein (groepsstructuur) + "modules.leerplein", # Bovenliggende groepen "modules.stage", # Stage (intern/extern) ] diff --git a/backend/core/apps.py b/backend/core/apps.py index 7808263..831d81f 100644 --- a/backend/core/apps.py +++ b/backend/core/apps.py @@ -32,7 +32,7 @@ class CoreConfig(AppConfig): MenuItem("Roostermaker", "/roostermaker", icon="calendar", order=12), # Submenu Organisatie: wie hoort waar. MenuItem("Personen", "/personen", icon="users", order=20, group="Organisatie"), - MenuItem("Groepsindeling", "/structuur", icon="layers", order=24, group="Organisatie"), + MenuItem("Groepsindeling", "/groepsindeling", icon="layers", order=24, group="Organisatie"), MenuItem("Afwezigheid", "/afwezigheid", icon="away", order=28, group="Organisatie"), ), ) diff --git a/backend/modules/leerplein/migrations/0003_rename_leerplein_verbose_names.py b/backend/modules/leerplein/migrations/0003_rename_leerplein_verbose_names.py new file mode 100644 index 0000000..234679d --- /dev/null +++ b/backend/modules/leerplein/migrations/0003_rename_leerplein_verbose_names.py @@ -0,0 +1,19 @@ +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("module_leerplein", "0002_leerplein_medewerkers"), + ] + + operations = [ + migrations.AlterModelOptions( + name="leerplein", + options={ + "ordering": ["naam"], + "verbose_name": "bovenliggende groep", + "verbose_name_plural": "bovenliggende groepen", + }, + ), + ] diff --git a/backend/modules/leerplein/models.py b/backend/modules/leerplein/models.py index 87f9685..7aeba88 100644 --- a/backend/modules/leerplein/models.py +++ b/backend/modules/leerplein/models.py @@ -1,8 +1,8 @@ """ -Leerplein-module: een overkoepelende groepsstructuur. +Module voor bovenliggende groepen. -- Een Leerplein bundelt meerdere groepen en/of subgroepen, en kan daarnaast - losse (individuele) leerlingen bevatten. +- Een bovenliggende groep bundelt meerdere groepen en/of subgroepen, en kan + daarnaast losse (individuele) leerlingen bevatten. - Een SubgroepGroep-koppeling verbindt een subgroep met een groep. De module raakt de kernmodellen niet aan; het zijn losse koppeltabellen die @@ -33,8 +33,8 @@ class Leerplein(models.Model): aangemaakt_op = models.DateTimeField(auto_now_add=True) class Meta: - verbose_name = "leerplein" - verbose_name_plural = "leerpleinen" + verbose_name = "bovenliggende groep" + verbose_name_plural = "bovenliggende groepen" ordering = ["naam"] def __str__(self) -> str: diff --git a/backend/modules/stage/apps.py b/backend/modules/stage/apps.py index 35b898b..f7c27e0 100644 --- a/backend/modules/stage/apps.py +++ b/backend/modules/stage/apps.py @@ -19,7 +19,7 @@ class StageConfig(AppConfig): "krijgen een begeleider; een interne stage ook een ruimte." ), version="0.1.0", - category="Structuur", + category="Groepsindeling", core=False, default_enabled=False, menu_items=( diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index fb08aff..b698400 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -5,7 +5,7 @@ import { PersonenPage, FunctiesPage, ActiviteitenPage, LocatiesPage, TijdslotenPage, SchooljaarPage, RoosterWeergavePage, RoostermakerPage, - AfwezigheidPage, LeerpleinPage, StagePage, GebruikersPage, + AfwezigheidPage, GroepsindelingPage, StagePage, GebruikersPage, } from "./pages.jsx"; const ICONS = { @@ -14,16 +14,16 @@ const ICONS = { away: "🌴", gear: "⚙️", }; -const STRUCTUUR_PADEN = new Set(["/structuur", "/leerplein", "/groepen", "/subgroepen"]); -const STRUCTUUR_ITEM = { label: "Groepsindeling", path: "/structuur", icon: "layers", order: 24, group: "Organisatie" }; -const normaliseerPad = (path) => (STRUCTUUR_PADEN.has(path) ? "/structuur" : path); +const GROEPSINDELING_PADEN = new Set(["/groepsindeling", "/structuur", "/leerplein", "/groepen", "/subgroepen"]); +const GROEPSINDELING_ITEM = { label: "Groepsindeling", path: "/groepsindeling", icon: "layers", order: 24, group: "Organisatie" }; +const normaliseerPad = (path) => (GROEPSINDELING_PADEN.has(path) ? "/groepsindeling" : path); // Koppel een menu-pad aan een echt scherm. Onbekende paden tonen een // placeholder (worden in een latere fase ingevuld). const PAGES = { "/rooster": RoosterWeergavePage, "/roostermaker": RoostermakerPage, - "/structuur": LeerpleinPage, + "/groepsindeling": GroepsindelingPage, "/personen": PersonenPage, "/functies": FunctiesPage, "/activiteiten": ActiviteitenPage, @@ -137,13 +137,13 @@ function Shell({ user, onLogout }) { } // Bouw de navigatie op uit de menu-items die ACTIEVE modules aanbieden. - // Verouderde structuurpaden blijven werken, maar verschijnen als één tab. + // Verouderde groepspaden blijven werken, maar verschijnen als één tab. const { tabs, instellingenTabs, alleItems } = useMemo(() => { const perPad = new Map(); const voegToe = (raw) => { const pad = normaliseerPad(raw.path); - const item = pad === "/structuur" - ? { ...raw, ...STRUCTUUR_ITEM, module: raw.module } + const item = pad === "/groepsindeling" + ? { ...raw, ...GROEPSINDELING_ITEM, module: raw.module } : { ...raw, path: pad }; const huidig = perPad.get(item.path); if (!huidig || item.order < huidig.order) perPad.set(item.path, item); diff --git a/frontend/src/pages.jsx b/frontend/src/pages.jsx index a36d1a9..f0094a5 100644 --- a/frontend/src/pages.jsx +++ b/frontend/src/pages.jsx @@ -199,7 +199,7 @@ export function LocatiesPage() { /* --------------------------------- Groepen -------------------------------- */ export function GroepenPage() { - return ; + return ; } /* -------------------------------- Personen -------------------------------- */ @@ -241,7 +241,7 @@ export function PersonenPage() { } useEffect(() => { laadHulplijsten(); }, []); - // Leerplein-koppeling loopt via de leerplein-API (de module bezit die relatie). + // De bovenliggende-groep-koppeling loopt via de bestaande API. async function syncLeerpleinen(persoonId, gekozenIds) { const huidige = await listResource("leerpleinen").catch(() => []); for (const lp of huidige) { @@ -446,7 +446,7 @@ export function PersonenPage() { /* ------------------------------- Subgroepen ------------------------------- */ export function SubgroepenPage() { - return ; + return ; } /* ------------------------------- Tijdsloten ------------------------------- */ @@ -703,7 +703,7 @@ export function AfwezigheidPage() { /* ----------------------------- Groepsindeling ----------------------------- */ // Gezamenlijk beheer: bovenliggende groep -> groep -> subgroep. -export function LeerpleinPage() { +export function GroepsindelingPage() { const [leerpleinen, setLeerpleinen] = useState([]); const [groepen, setGroepen] = useState([]); const [subgroepen, setSubgroepen] = useState([]); @@ -891,7 +891,7 @@ export function LeerpleinPage() {

Beheer bovenliggende groepen, groepen en subgroepen op één plek. Een groep hoort altijd onder een bovenliggende groep; een subgroep hoort altijd onder een groep.

-
+

{BOVENGROEP_LABEL}

setLeerpleinForm({ ...leerpleinForm, naam: e.target.value })} /> @@ -911,7 +911,7 @@ export function LeerpleinPage() {
-
+

Groep

{ @@ -980,7 +980,7 @@ export function LeerpleinPage() { {loading ?

Laden…

: ( -
+
{leerpleinen.map((lp) => { const lpGroepen = groepen.filter((g) => (lp.groepen || []).includes(g.id)); const lpSubgroepen = subgroepen.filter((s) => (lp.subgroepen || []).includes(s.id) || lpGroepen.some((g) => g.id === s.groep)); @@ -1942,13 +1942,13 @@ export function RoosterScherm({ mode }) {
{kolommen.length}
-
+
{kolommen.map(([gid, groepBlokken]) => { const groep = groepen.find((g) => g.id === gid); return (
-
{groep ? groep.naam : "Groep"}
- {groepBlokken.map((b) => renderCompactBlok(b, iso, kal))} +
{groep ? groep.naam : "Groep"}
+
{groepBlokken.map((b) => renderCompactBlok(b, iso, kal))}
); })} @@ -1977,11 +1977,11 @@ export function RoosterScherm({ mode }) {
{kolomAantal}
-
+
{heleGroep.length > 0 && (
-
Hele groep
- {heleGroep.map((b) => renderCompactBlok(b, iso, kal))} +
Hele groep
+
{heleGroep.map((b) => renderCompactBlok(b, iso, kal))}
)} {subIds.map((sid) => { @@ -1989,8 +1989,8 @@ export function RoosterScherm({ mode }) { const blocks = groepBlokken.filter((b) => b.subgroep === sid); return (
-
{sub ? sub.naam : "Subgroep"}
- {blocks.map((b) => renderCompactBlok(b, iso, kal))} +
{sub ? sub.naam : "Subgroep"}
+
{blocks.map((b) => renderCompactBlok(b, iso, kal))}
); })} diff --git a/frontend/src/styles.css b/frontend/src/styles.css index 47e338e..9e01331 100644 --- a/frontend/src/styles.css +++ b/frontend/src/styles.css @@ -190,12 +190,13 @@ input[type="color"] { padding: 2px; width: 44px; min-width: 44px; height: 38px; .split-title span { font-size: 11px; color: var(--muted); } .split-count { min-width: 22px; height: 22px; display: inline-grid; place-items: center; border-radius: 999px; background: var(--brand-soft); color: var(--brand); font-size: 12px; font-weight: 800; } .split-groep { font-size: 12px; font-weight: 800; color: var(--brand); margin: 0 2px 7px; } -.split-kolommen { position: relative; display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; align-items: stretch; padding-top: 12px; } -.split-kolommen::before { content: ""; position: absolute; top: 0; left: 10%; right: 10%; border-top: 2px solid #bfdbfe; } -.split-kolom { position: relative; min-width: 0; border-left: 1px solid var(--line); padding-left: 7px; height: 100%; } -.split-kolom::before { content: ""; position: absolute; top: -12px; left: 50%; width: 2px; height: 12px; background: #bfdbfe; } -.split-kolom:nth-child(odd) { border-left: 0; padding-left: 0; } +.split-kolommen { position: relative; display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; align-items: stretch; margin-left: 12px; padding: 12px 0 0 16px; border-left: 2px solid #bfdbfe; } +.split-kolommen::before { content: ""; position: absolute; top: 0; left: -2px; height: 12px; border-left: 2px solid #bfdbfe; } +.split-kolom { position: relative; min-width: 0; height: 100%; padding-left: 0; } +.split-kolom::before { content: ""; position: absolute; top: 14px; left: -16px; width: 16px; border-top: 2px solid #bfdbfe; } .split-kop { font-size: 11px; font-weight: 700; color: var(--muted); margin: 0 0 4px; } +.split-node-kop { display: inline-flex; max-width: 100%; align-items: center; border: 1px solid #bfdbfe; border-radius: 6px; background: #fff; color: var(--brand); padding: 4px 7px; margin-bottom: 6px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.split-vakken { display: grid; gap: 6px; } .split-vak { position: relative; border: 1px solid var(--line); border-left: 4px solid var(--brand); border-radius: 6px; background: #fff; padding: 6px 6px 5px; margin-bottom: 6px; cursor: pointer; box-shadow: 0 1px 0 rgba(31,41,55,.03); } .split-vak.open { box-shadow: 0 0 0 2px var(--brand-soft); } .split-vak.vervallen { opacity: .55; } @@ -222,14 +223,13 @@ input[type="color"] { padding: 2px; width: 44px; min-width: 44px; height: 38px; .split-preview-children::before { content: ""; position: absolute; top: 0; left: 12px; right: 12px; border-top: 2px solid #bfdbfe; } .split-preview-children span { position: relative; padding: 5px 8px; border-radius: 6px; background: var(--brand-soft); color: var(--brand); font-size: 12px; font-weight: 700; } .split-preview-children span::before { content: ""; position: absolute; top: -13px; left: 50%; width: 2px; height: 13px; background: #bfdbfe; } -.structuur-form h2 { margin: 0 0 10px; font-size: 17px; } +.indeling-form h2 { margin: 0 0 10px; font-size: 17px; } .check-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(190px, 1fr)); gap: 7px; margin-top: 8px; } .check-item { display: flex; align-items: center; gap: 7px; min-width: 0; border: 1px solid var(--line); border-radius: 7px; background: #fff; padding: 7px 9px; font-size: 13px; } .check-item input { min-width: auto; } .check-item span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .lijst-kop { display: flex; align-items: center; justify-content: space-between; gap: 10px; flex-wrap: wrap; } -.structuur-overzicht { display: grid; gap: 14px; margin-top: 18px; } -.structuur-tree { display: grid; gap: 14px; margin-top: 18px; } +.indeling-tree { display: grid; gap: 14px; margin-top: 18px; } .tree-root { border: 1px solid #bfdbfe; border-radius: 8px; background: #f8fbff; padding: 12px; } .tree-node { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; border: 1px solid var(--line); border-radius: 8px; background: #fff; padding: 10px 12px; } .tree-node-main { display: flex; align-items: flex-start; gap: 10px; min-width: 0; } @@ -247,13 +247,6 @@ input[type="color"] { padding: 2px; width: 44px; min-width: 44px; height: 38px; .tree-subchildren { margin-top: 0; border-left-color: #dbeafe; } .tree-meta { padding-left: 16px; } .tree-empty { padding: 8px 0 8px 12px; } -.structuur-card { border: 1px solid #bfdbfe; border-radius: 8px; background: #f8fbff; padding: 12px; } -.structuur-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; margin-bottom: 10px; } -.structuur-head.compact { align-items: center; margin-bottom: 6px; } -.structuur-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 10px; } -.structuur-groep { border: 1px solid var(--line); border-top: 3px solid var(--brand); border-radius: 8px; background: #fff; padding: 10px; } -.structuur-subgroepen { display: grid; gap: 7px; margin-top: 9px; padding-top: 9px; border-top: 1px dashed var(--line); } -.structuur-subgroep { border-left: 3px solid #93c5fd; border-radius: 6px; background: #f9fafb; padding: 7px 9px; } .modal-overlay { position: fixed; inset: 0; background: rgba(15,23,42,.45); display: flex; align-items: flex-start; justify-content: center; padding: 40px 16px; z-index: 900; overflow: auto; } .modal { background: var(--panel); border-radius: 12px; padding: 18px 20px; width: min(560px, 100%); box-shadow: 0 20px 50px rgba(0,0,0,.3); } .modal-kop { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; } @@ -321,8 +314,8 @@ input[type="color"] { padding: 2px; width: 44px; min-width: 44px; height: 38px; .topbar-user { flex-direction: column; align-items: flex-end; gap: 2px; } .content, .content-wide { padding: 18px 12px 28px; } .top-tabs-settings { margin-left: 0; border-left: 0; padding-left: 0; } - .split-kolommen { grid-template-columns: 1fr; } - .split-kolom { border-left: 0; padding-left: 0; } + .split-kolommen { grid-template-columns: 1fr; margin-left: 8px; padding-left: 14px; } + .split-kolom::before { left: -14px; width: 14px; } .tree-node { flex-direction: column; align-items: stretch; } .tree-children { margin-left: 10px; padding-left: 12px; } .tree-meta { padding-left: 0; }