Hernoem Leerplein naar Bovenliggende groepen; route /groepsindeling; categorie Groepsindeling
This commit is contained in:
parent
fa50438d35
commit
c49531f692
8 changed files with 61 additions and 49 deletions
|
|
@ -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)
|
||||
]
|
||||
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
},
|
||||
),
|
||||
]
|
||||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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=(
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -199,7 +199,7 @@ export function LocatiesPage() {
|
|||
|
||||
/* --------------------------------- Groepen -------------------------------- */
|
||||
export function GroepenPage() {
|
||||
return <LeerpleinPage />;
|
||||
return <GroepsindelingPage />;
|
||||
}
|
||||
|
||||
/* -------------------------------- 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 <LeerpleinPage />;
|
||||
return <GroepsindelingPage />;
|
||||
}
|
||||
|
||||
/* ------------------------------- 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() {
|
|||
<p className="muted">Beheer bovenliggende groepen, groepen en subgroepen op één plek. Een groep hoort altijd onder een bovenliggende groep; een subgroep hoort altijd onder een groep.</p>
|
||||
<Foutmelding msg={error} />
|
||||
|
||||
<form className="form-card structuur-form" onSubmit={opslaanLeerplein}>
|
||||
<form className="form-card indeling-form" onSubmit={opslaanLeerplein}>
|
||||
<h2>{BOVENGROEP_LABEL}</h2>
|
||||
<div className="form-row">
|
||||
<input placeholder={`Naam ${BOVENGROEP_LABEL_LC}`} value={leerpleinForm.naam} required onChange={(e) => setLeerpleinForm({ ...leerpleinForm, naam: e.target.value })} />
|
||||
|
|
@ -911,7 +911,7 @@ export function LeerpleinPage() {
|
|||
</div>
|
||||
</form>
|
||||
|
||||
<form className="form-card structuur-form" onSubmit={opslaanGroep}>
|
||||
<form className="form-card indeling-form" onSubmit={opslaanGroep}>
|
||||
<h2>Groep</h2>
|
||||
<div className="form-row">
|
||||
<select value={groepForm.leerplein} required onChange={(e) => setGroepForm({ ...groepForm, leerplein: e.target.value })}>
|
||||
|
|
@ -937,7 +937,7 @@ export function LeerpleinPage() {
|
|||
</div>
|
||||
</form>
|
||||
|
||||
<form className="form-card structuur-form" onSubmit={opslaanSubgroep}>
|
||||
<form className="form-card indeling-form" onSubmit={opslaanSubgroep}>
|
||||
<h2>Subgroep</h2>
|
||||
<div className="form-row">
|
||||
<select value={subgroepForm.groep} required onChange={(e) => {
|
||||
|
|
@ -980,7 +980,7 @@ export function LeerpleinPage() {
|
|||
</form>
|
||||
|
||||
{loading ? <p>Laden…</p> : (
|
||||
<div className="structuur-tree">
|
||||
<div className="indeling-tree">
|
||||
{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 }) {
|
|||
</div>
|
||||
<span className="split-count">{kolommen.length}</span>
|
||||
</div>
|
||||
<div className="split-kolommen">
|
||||
<div className="split-kolommen split-vertakking">
|
||||
{kolommen.map(([gid, groepBlokken]) => {
|
||||
const groep = groepen.find((g) => g.id === gid);
|
||||
return (
|
||||
<div className="split-kolom" key={gid}>
|
||||
<div className="split-kop">{groep ? groep.naam : "Groep"}</div>
|
||||
{groepBlokken.map((b) => renderCompactBlok(b, iso, kal))}
|
||||
<div className="split-kop split-node-kop">{groep ? groep.naam : "Groep"}</div>
|
||||
<div className="split-vakken">{groepBlokken.map((b) => renderCompactBlok(b, iso, kal))}</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
|
@ -1977,11 +1977,11 @@ export function RoosterScherm({ mode }) {
|
|||
</div>
|
||||
<span className="split-count">{kolomAantal}</span>
|
||||
</div>
|
||||
<div className="split-kolommen">
|
||||
<div className="split-kolommen split-vertakking">
|
||||
{heleGroep.length > 0 && (
|
||||
<div className="split-kolom">
|
||||
<div className="split-kop">Hele groep</div>
|
||||
{heleGroep.map((b) => renderCompactBlok(b, iso, kal))}
|
||||
<div className="split-kop split-node-kop">Hele groep</div>
|
||||
<div className="split-vakken">{heleGroep.map((b) => renderCompactBlok(b, iso, kal))}</div>
|
||||
</div>
|
||||
)}
|
||||
{subIds.map((sid) => {
|
||||
|
|
@ -1989,8 +1989,8 @@ export function RoosterScherm({ mode }) {
|
|||
const blocks = groepBlokken.filter((b) => b.subgroep === sid);
|
||||
return (
|
||||
<div className="split-kolom" key={sid}>
|
||||
<div className="split-kop">{sub ? sub.naam : "Subgroep"}</div>
|
||||
{blocks.map((b) => renderCompactBlok(b, iso, kal))}
|
||||
<div className="split-kop split-node-kop">{sub ? sub.naam : "Subgroep"}</div>
|
||||
<div className="split-vakken">{blocks.map((b) => renderCompactBlok(b, iso, kal))}</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
|
|
|
|||
Loading…
Reference in a new issue