diff --git a/backend/migrations/versions/0004_class_based_assessments.py b/backend/migrations/versions/0004_class_based_assessments.py new file mode 100644 index 0000000..457bee8 --- /dev/null +++ b/backend/migrations/versions/0004_class_based_assessments.py @@ -0,0 +1,60 @@ +"""assessments: herstructureer naar klasgebonden model + +Revision ID: 0004 +Revises: 0003 +Create Date: 2026-03-05 + +Wijziging: assessments zijn niet langer gekoppeld aan een individuele +leerkracht (user_id) maar aan een klas (class_id). Meerdere leerkrachten +van dezelfde klas delen één set beoordelingen. + +OPGELET: dit dropt de bestaande assessments tabel — testdata gaat verloren. +""" +from alembic import op +import sqlalchemy as sa + +revision = '0004' +down_revision = '0003' +branch_labels = None +depends_on = None + + +def upgrade(): + # Drop oude tabel volledig (testomgeving — geen productiedata) + op.execute("DROP TABLE IF EXISTS assessments CASCADE") + + # Nieuwe tabel: klasgebonden, geen user_id + op.execute(""" + CREATE TABLE assessments ( + id SERIAL PRIMARY KEY, + class_id INTEGER NOT NULL REFERENCES classes(id) ON DELETE CASCADE, + school_year_id INTEGER NOT NULL REFERENCES school_years(id) ON DELETE CASCADE, + vak_id VARCHAR(50) NOT NULL, + goal_id VARCHAR(50) NOT NULL, + status VARCHAR(10) NOT NULL, + opmerking VARCHAR(500), + updated_at TIMESTAMP DEFAULT NOW(), + UNIQUE(class_id, school_year_id, vak_id, goal_id) + ) + """) + op.execute("CREATE INDEX IF NOT EXISTS ix_assessments_class_year ON assessments(class_id, school_year_id)") + op.execute("CREATE INDEX IF NOT EXISTS ix_assessments_vak ON assessments(vak_id)") + + +def downgrade(): + op.execute("DROP TABLE IF EXISTS assessments CASCADE") + # Zet terug naar user-gebaseerde tabel (zonder data) + op.execute(""" + CREATE TABLE assessments ( + id SERIAL PRIMARY KEY, + user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, + school_id INTEGER NOT NULL REFERENCES schools(id) ON DELETE CASCADE, + school_year_id INTEGER NOT NULL REFERENCES school_years(id) ON DELETE CASCADE, + vak_id VARCHAR(50) NOT NULL, + goal_id VARCHAR(50) NOT NULL, + status VARCHAR(10) NOT NULL, + opmerking VARCHAR(500), + updated_at TIMESTAMP DEFAULT NOW(), + UNIQUE(user_id, school_year_id, vak_id, goal_id) + ) + """) diff --git a/backend/models.py b/backend/models.py index 72f7cbb..cfdd73c 100644 --- a/backend/models.py +++ b/backend/models.py @@ -151,8 +151,8 @@ class Assessment(db.Model): __tablename__ = 'assessments' id = db.Column(db.Integer, primary_key=True) - user_id = db.Column(db.Integer, db.ForeignKey('users.id', ondelete='CASCADE'), nullable=False) - school_id = db.Column(db.Integer, db.ForeignKey('schools.id', ondelete='CASCADE'), nullable=False) + # Klasgebonden — geen koppeling aan individuele leerkracht + class_id = db.Column(db.Integer, db.ForeignKey('classes.id', ondelete='CASCADE'), nullable=False) school_year_id = db.Column(db.Integer, db.ForeignKey('school_years.id', ondelete='CASCADE'), nullable=False) vak_id = db.Column(db.String(50), nullable=False) goal_id = db.Column(db.String(50), nullable=False) @@ -160,17 +160,18 @@ class Assessment(db.Model): opmerking = db.Column(db.String(500), nullable=True) updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) - user = db.relationship('User') - school = db.relationship('School') + klas = db.relationship('Class') school_year = db.relationship('SchoolYear', back_populates='assessments') __table_args__ = ( - db.UniqueConstraint('user_id', 'school_year_id', 'vak_id', 'goal_id'), + db.UniqueConstraint('class_id', 'school_year_id', 'vak_id', 'goal_id', + name='uq_assessment_class_year_vak_goal'), ) def to_dict(self): return { 'id': self.id, + 'class_id': self.class_id, 'vak_id': self.vak_id, 'goal_id': self.goal_id, 'status': self.status, diff --git a/backend/routes/api.py b/backend/routes/api.py index 6201dba..a6aa3a0 100644 --- a/backend/routes/api.py +++ b/backend/routes/api.py @@ -19,18 +19,36 @@ def director_required(f): return decorated -def get_active_year(school_id=None): - """Geeft het globaal actief schooljaar terug (school_id wordt genegeerd).""" +def get_active_year(): + """Geeft het globaal actief schooljaar terug.""" return SchoolYear.query.filter_by(school_id=None, is_active=True).first() +def check_class_access(class_id): + """ + Geeft de klas terug als de huidige gebruiker er toegang toe heeft. + - Leerkrachten: enkel hun eigen klassen (via teacher_classes). + - Directeur en hoger: alle klassen van hun school. + - Geeft False terug als de klas niet bestaat. + - Geeft None terug als de gebruiker geen toegang heeft. + """ + klas = Class.query.filter_by(id=class_id).first() + if not klas: + return False + if klas.school_id != current_user.school_id: + return None + if current_user.is_teacher: + if not any(c.id == class_id for c in current_user.classes): + return None + return klas + + # ── Doelen (statische JSON bestanden) ───────────────────────────────────────── @api_bp.route('/doelen/index') @login_required def doelen_index(): data = load_index() - # Altijd een geldig object teruggeven — lege vakkenlijst is geen fout return jsonify(data) @@ -50,16 +68,30 @@ def doelen_vak(vak_id): @api_bp.route('/assessments', methods=['GET']) @login_required def get_assessments(): - if not current_user.school_id: + """Haal beoordelingen op voor een klas (en optioneel een vak).""" + class_id_str = request.args.get('class_id') + if not class_id_str: return jsonify({'assessments': []}) - school_year = get_active_year(current_user.school_id) + + try: + class_id = int(class_id_str) + except ValueError: + return jsonify({'error': 'Ongeldig class_id'}), 400 + + klas = check_class_access(class_id) + if klas is False: + return jsonify({'error': 'Klas niet gevonden'}), 404 + if klas is None: + return jsonify({'error': 'Geen toegang tot deze klas'}), 403 + + school_year = get_active_year() if not school_year: return jsonify({'assessments': []}) year_id = request.args.get('year_id', school_year.id) vak_id = request.args.get('vak_id') - query = Assessment.query.filter_by(user_id=current_user.id, school_year_id=year_id) + query = Assessment.query.filter_by(class_id=class_id, school_year_id=year_id) if vak_id: query = query.filter_by(vak_id=vak_id) @@ -68,30 +100,39 @@ def get_assessments(): @api_bp.route('/assessments', methods=['POST']) @login_required -@limiter.limit('120 per minute') # max 2 per seconde per gebruiker +@limiter.limit('120 per minute') def save_assessment(): data = request.get_json() or {} + class_id = data.get('class_id') vak_id = (data.get('vak_id') or '').strip() goal_id = (data.get('goal_id') or '').strip() status = (data.get('status') or '').strip() opmerking = (data.get('opmerking') or '').strip()[:500] - if not vak_id or not goal_id: - return jsonify({'error': 'vak_id en goal_id zijn verplicht'}), 400 + if not class_id or not vak_id or not goal_id: + return jsonify({'error': 'class_id, vak_id en goal_id zijn verplicht'}), 400 if status not in ('groen', 'oranje', 'roze', ''): - return jsonify({'error': 'Ongeldige status — gebruik groen, oranje, roze of leeg'}), 400 - # Sanitiseer input — voorkomt oversized data in DB + return jsonify({'error': 'Ongeldige status'}), 400 if len(vak_id) > 100 or len(goal_id) > 50: return jsonify({'error': 'Ongeldige invoer'}), 400 - if not current_user.school_id: - return jsonify({'error': 'Account is nog niet gekoppeld aan een school'}), 400 - school_year = get_active_year(current_user.school_id) + try: + class_id = int(class_id) + except (ValueError, TypeError): + return jsonify({'error': 'Ongeldig class_id'}), 400 + + klas = check_class_access(class_id) + if klas is False: + return jsonify({'error': 'Klas niet gevonden'}), 404 + if klas is None: + return jsonify({'error': 'Geen toegang tot deze klas'}), 403 + + school_year = get_active_year() if not school_year: return jsonify({'error': 'Geen actief schooljaar gevonden'}), 400 assessment = Assessment.query.filter_by( - user_id=current_user.id, + class_id=class_id, school_year_id=school_year.id, vak_id=vak_id, goal_id=goal_id, @@ -109,8 +150,7 @@ def save_assessment(): assessment.updated_at = datetime.utcnow() else: assessment = Assessment( - user_id=current_user.id, - school_id=current_user.school_id, + class_id=class_id, school_year_id=school_year.id, vak_id=vak_id, goal_id=goal_id, @@ -120,38 +160,44 @@ def save_assessment(): db.session.add(assessment) db.session.commit() - # Auditlog enkel bij statuswijziging (niet bij elke klik) audit_log('assessment.save', 'assessment', - target_type='goal', target_id=f'{vak_id}:{goal_id}', - detail={'status': status}) + target_type='class', target_id=str(class_id), + detail={'status': status, 'vak': vak_id, 'goal': goal_id}) return jsonify({'assessment': assessment.to_dict()}) - - @api_bp.route('/assessments/opmerking', methods=['POST']) @login_required @limiter.limit('120 per minute') def save_opmerking(): - """Sla enkel een opmerking op bij een bestaand of nieuw assessment record.""" data = request.get_json() or {} + class_id = data.get('class_id') vak_id = (data.get('vak_id') or '').strip() goal_id = (data.get('goal_id') or '').strip() opmerking = (data.get('opmerking') or '').strip()[:500] - if not vak_id or not goal_id: - return jsonify({'error': 'vak_id en goal_id zijn verplicht'}), 400 + if not class_id or not vak_id or not goal_id: + return jsonify({'error': 'class_id, vak_id en goal_id zijn verplicht'}), 400 if len(vak_id) > 100 or len(goal_id) > 50: return jsonify({'error': 'Ongeldige invoer'}), 400 - if not current_user.school_id: - return jsonify({'error': 'Account niet gekoppeld aan een school'}), 400 - school_year = get_active_year(current_user.school_id) + try: + class_id = int(class_id) + except (ValueError, TypeError): + return jsonify({'error': 'Ongeldig class_id'}), 400 + + klas = check_class_access(class_id) + if klas is False: + return jsonify({'error': 'Klas niet gevonden'}), 404 + if klas is None: + return jsonify({'error': 'Geen toegang tot deze klas'}), 403 + + school_year = get_active_year() if not school_year: return jsonify({'error': 'Geen actief schooljaar'}), 400 assessment = Assessment.query.filter_by( - user_id=current_user.id, + class_id=class_id, school_year_id=school_year.id, vak_id=vak_id, goal_id=goal_id, @@ -161,10 +207,8 @@ def save_opmerking(): assessment.opmerking = opmerking or None assessment.updated_at = datetime.utcnow() else: - # Maak een record aan zonder status voor de opmerking assessment = Assessment( - user_id=current_user.id, - school_id=current_user.school_id, + class_id=class_id, school_year_id=school_year.id, vak_id=vak_id, goal_id=goal_id, @@ -176,32 +220,43 @@ def save_opmerking(): db.session.commit() return jsonify({'ok': True}) + @api_bp.route('/assessments/bulk-import', methods=['POST']) @login_required @limiter.limit('5 per minute') def bulk_import_assessments(): """ - Importeer beoordelingen vanuit de legacy standalone JSON export. - Body: { "vakken": { "vak_id": { "goal_id": "status", ... }, ... } } - of v4 formaat: { "vakken": { "vak_id": { "statussen": { "goal_id": "status" } } } } + Importeer beoordelingen vanuit legacy standalone JSON export. + Body: { "class_id": 1, "vakken": { "vak_id": { "goal_id": "status" } } } """ - if not current_user.school_id: - return jsonify({'error': 'Account niet gekoppeld aan een school'}), 400 + data = request.get_json() or {} + class_id = data.get('class_id') + vakken = data.get('vakken', {}) - school_year = get_active_year(current_user.school_id) - if not school_year: - return jsonify({'error': 'Geen actief schooljaar'}), 400 - - data = request.get_json() or {} - vakken = data.get('vakken', {}) + if not class_id: + return jsonify({'error': 'class_id is verplicht'}), 400 if not vakken: return jsonify({'error': 'Geen vakken gevonden in payload'}), 400 + try: + class_id = int(class_id) + except (ValueError, TypeError): + return jsonify({'error': 'Ongeldig class_id'}), 400 + + klas = check_class_access(class_id) + if klas is False: + return jsonify({'error': 'Klas niet gevonden'}), 404 + if klas is None: + return jsonify({'error': 'Geen toegang tot deze klas'}), 403 + + school_year = get_active_year() + if not school_year: + return jsonify({'error': 'Geen actief schooljaar'}), 400 + totaal = 0 fouten = 0 for vak_id, vak_data in vakken.items(): - # Sanitiseer vak_id if not isinstance(vak_id, str) or len(vak_id) > 100: fouten += 1 continue @@ -224,7 +279,7 @@ def bulk_import_assessments(): try: assessment = Assessment.query.filter_by( - user_id=current_user.id, + class_id=class_id, school_year_id=school_year.id, vak_id=vak_id, goal_id=goal_id, @@ -235,8 +290,7 @@ def bulk_import_assessments(): assessment.updated_at = datetime.utcnow() else: db.session.add(Assessment( - user_id=current_user.id, - school_id=current_user.school_id, + class_id=class_id, school_year_id=school_year.id, vak_id=vak_id, goal_id=goal_id, @@ -249,6 +303,7 @@ def bulk_import_assessments(): db.session.commit() audit_log('assessment.bulk_import', 'assessment', + target_type='class', target_id=str(class_id), detail={'totaal': totaal, 'fouten': fouten}) return jsonify({'totaal': totaal, 'fouten': fouten}) @@ -261,47 +316,44 @@ def bulk_import_assessments(): def school_overview(): if not current_user.school_id: return jsonify({'error': 'Geen school gekoppeld'}), 400 - school_year = get_active_year(current_user.school_id) + + school_year = get_active_year() if not school_year: return jsonify({'error': 'Geen actief schooljaar'}), 400 - # year_id param: directeur/admin kan wisselen, leerkracht zit vast aan actief jaar year_id_param = request.args.get('year_id') - if year_id_param and current_user.is_director: - year_id = int(year_id_param) - selected_year = SchoolYear.query.filter_by( - id=year_id, school_id=current_user.school_id - ).first() or school_year + if year_id_param: + selected_year = SchoolYear.query.get(int(year_id_param)) or school_year else: selected_year = school_year - year_id = school_year.id - vak_id = request.args.get('vak_id') + vak_id = request.args.get('vak_id') - teachers = User.query.filter_by( - school_id=current_user.school_id, role='teacher', is_active=True - ).all() + # Alle klassen van deze school + klassen = Class.query.filter_by(school_id=current_user.school_id)\ + .order_by(Class.name).all() + class_ids = [k.id for k in klassen] - query = Assessment.query.filter_by( - school_id=current_user.school_id, school_year_id=year_id + query = Assessment.query.filter( + Assessment.class_id.in_(class_ids), + Assessment.school_year_id == selected_year.id, ) if vak_id: query = query.filter_by(vak_id=vak_id) - by_teacher = {t.id: {} for t in teachers} + # Groepeer per klas → vak → goal + by_class = {k.id: {} for k in klassen} for a in query.all(): - by_teacher.setdefault(a.user_id, {}) - by_teacher[a.user_id].setdefault(a.vak_id, {}) - by_teacher[a.user_id][a.vak_id][a.goal_id] = a.status + by_class[a.class_id].setdefault(a.vak_id, {})[a.goal_id] = a.status return jsonify({ - 'school_year': selected_year.to_dict(), - 'teachers': [t.to_dict() for t in teachers], - 'assessments_by_teacher': by_teacher, + 'school_year': selected_year.to_dict(), + 'classes': [k.to_dict() for k in klassen], + 'assessments_by_class': by_class, }) -# ── Gebruikersbeheer (school_ict / directeur) ────────────────────────────────── +# ── Gebruikersbeheer (director / school_ict) ─────────────────────────────────── @api_bp.route('/users', methods=['GET']) @login_required @@ -347,39 +399,39 @@ def delete_user(user_id): return jsonify({'deleted': True}) - -# ── Schooljaren (directeur/admin leesbaar) ──────────────────────────────────── +# ── Schooljaren ──────────────────────────────────────────────────────────────── @api_bp.route('/school/years') @login_required @director_required def get_school_years(): - """Geeft alle globale schooljaren terug (voor jaarselectie in directeur dashboard).""" - years = SchoolYear.query.filter_by(school_id=None) .order_by(SchoolYear.label.desc()).all() + years = SchoolYear.query.filter_by(school_id=None)\ + .order_by(SchoolYear.label.desc()).all() return jsonify({'years': [y.to_dict() for y in years]}) -# ── Huidig ingelogde gebruiker ──────────────────────────────────────────────── +# ── Huidig ingelogde gebruiker ───────────────────────────────────────────────── @api_bp.route('/me') @login_required def me(): - school_year = get_active_year(current_user.school_id) if current_user.school_id else None + school_year = get_active_year() if current_user.school_id else None return jsonify({ 'user': current_user.to_dict(), 'school_year': school_year.to_dict() if school_year else None, }) -# ── Klassen voor leerkracht (zelf instellen) ────────────────────────────────── +# ── Klassen voor leerkracht ──────────────────────────────────────────────────── @api_bp.route('/my/classes', methods=['GET']) @login_required def my_classes(): - """Geeft alle beschikbare klassen en eigen klassen terug.""" + """Geeft alle klassen van de school en de eigen klassen van de leerkracht.""" if not current_user.school_id: return jsonify({'all_classes': [], 'my_classes': []}) - all_cls = Class.query.filter_by(school_id=current_user.school_id) .order_by(Class.name).all() + all_cls = Class.query.filter_by(school_id=current_user.school_id)\ + .order_by(Class.name).all() return jsonify({ 'all_classes': [{'id': c.id, 'name': c.name} for c in all_cls], 'my_classes': [{'id': c.id, 'name': c.name} for c in current_user.classes], @@ -389,12 +441,12 @@ def my_classes(): @api_bp.route('/my/classes', methods=['PUT']) @login_required def set_my_classes(): - """Leerkracht stelt eigen klassen in.""" + """Leerkracht stelt zijn eigen klassen in.""" data = request.get_json() or {} class_ids = data.get('class_ids', []) classes = Class.query.filter( Class.id.in_(class_ids), - Class.school_id == current_user.school_id + Class.school_id == current_user.school_id, ).all() current_user.classes = classes audit_log('class.user_assignment', 'class', target_type='user', @@ -404,8 +456,7 @@ def set_my_classes(): return jsonify({'my_classes': [{'id': c.id, 'name': c.name} for c in current_user.classes]}) - -# ── Auditlog ────────────────────────────────────────────────────────────────── +# ── Auditlog ─────────────────────────────────────────────────────────────────── @api_bp.route('/audit-log') @login_required @@ -413,17 +464,14 @@ def get_audit_log(): if not current_user.is_school_ict: return jsonify({'error': 'Geen toegang'}), 403 - page = max(1, int(request.args.get('page', 1))) - per_page = min(100, max(1, int(request.args.get('per_page', 50)))) # max 100 per pagina - category = request.args.get('category') - search = request.args.get('search', '').strip() + page = max(1, int(request.args.get('page', 1))) + per_page = min(100, max(1, int(request.args.get('per_page', 50)))) + category = request.args.get('category') + search = request.args.get('search', '').strip() query = AuditLog.query - - # School ICT ziet enkel eigen school if not current_user.is_scholengroep_ict: query = query.filter(AuditLog.school_id == current_user.school_id) - if category: query = query.filter(AuditLog.category == category) if search: @@ -435,7 +483,8 @@ def get_audit_log(): ) total = query.count() - entries = query.order_by(AuditLog.timestamp.desc()) .offset((page - 1) * per_page).limit(per_page).all() + entries = query.order_by(AuditLog.timestamp.desc())\ + .offset((page - 1) * per_page).limit(per_page).all() return jsonify({ 'total': total, @@ -445,17 +494,11 @@ def get_audit_log(): }) -# ── SSO-lookup: welke loginmethodes heeft dit e-maildomein? ────────────────── +# ── SSO-lookup ───────────────────────────────────────────────────────────────── @api_bp.route('/sso-lookup') def sso_lookup(): - """ - Publieke endpoint — geen auth vereist. - Geeft aan welke SSO-methodes beschikbaar zijn voor een e-maildomein. - Legt NOOIT credentials bloot — enkel of Google geconfigureerd is. - """ from flask import current_app - from app import limiter email = request.args.get('email', '').lower().strip() if not email or '@' not in email: @@ -474,11 +517,7 @@ def sso_lookup(): ) if not school: - return jsonify({ - 'found': False, - 'microsoft': microsoft_available, - 'google': False, - }) + return jsonify({'found': False, 'microsoft': microsoft_available, 'google': False}) return jsonify({ 'found': True, diff --git a/backend/templates/directeur.html b/backend/templates/directeur.html index 04250a2..9cd4af4 100644 --- a/backend/templates/directeur.html +++ b/backend/templates/directeur.html @@ -9,361 +9,139 @@ --primary: #4f46e5; --primary-dark: #4338ca; --success: #10b981; --warning: #f59e0b; --danger: #ef4444; --gray-50: #f9fafb; --gray-100: #f3f4f6; --gray-200: #e5e7eb; - --gray-300: #d1d5db; --gray-400: #9ca3af; --gray-500: #6b7280; --gray-600: #4b5563; - --gray-700: #374151; --gray-800: #1f2937; + --gray-300: #d1d5db; --gray-400: #9ca3af; --gray-500: #6b7280; + --gray-600: #4b5563; --gray-700: #374151; --gray-800: #1f2937; --status-groen: #10b981; --status-oranje: #f59e0b; --status-roze: #ec4899; } * { margin: 0; padding: 0; box-sizing: border-box; } body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: var(--gray-100); color: var(--gray-800); } .container { max-width: 1600px; margin: 0 auto; padding: 1rem; } - .header { background: linear-gradient(135deg, var(--primary), var(--primary-dark)); color: white; border-radius: 12px; padding: 1.5rem; margin-bottom: 1rem; display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 1rem; } - .header h1 { font-size: 1.5rem; display: flex; align-items: center; gap: 0.5rem; } - .btn { display: inline-flex; align-items: center; gap: 0.35rem; padding: 0.5rem 1rem; border: none; border-radius: 6px; font-size: 0.85rem; font-weight: 500; cursor: pointer; transition: all 0.2s; text-decoration: none; } + + .header { background: linear-gradient(135deg, var(--primary), var(--primary-dark)); color: white; border-radius: 12px; padding: 1.25rem 1.5rem; margin-bottom: 1rem; display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 1rem; } + .header h1 { font-size: 1.4rem; display: flex; align-items: center; gap: 0.5rem; } + .btn { display: inline-flex; align-items: center; gap: 0.35rem; padding: 0.45rem 0.85rem; border: none; border-radius: 6px; font-size: 0.85rem; font-weight: 500; cursor: pointer; transition: all 0.2s; text-decoration: none; } .btn-light { background: rgba(255,255,255,0.2); color: white; border: 1px solid rgba(255,255,255,0.3); } .btn-light:hover { background: rgba(255,255,255,0.3); } .btn-primary { background: var(--primary); color: white; } .btn-primary:hover { background: var(--primary-dark); } .btn-secondary { background: var(--gray-200); color: var(--gray-700); } - .stats-grid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); - gap: 0.75rem; - margin-bottom: 0; - } - .stat-card { - background: white; - border-radius: 12px; - padding: 1.1rem 1rem 1rem; - text-align: center; - box-shadow: 0 1px 3px rgba(0,0,0,0.08); - display: flex; flex-direction: column; align-items: center; gap: 0.3rem; - transition: transform 0.15s, box-shadow 0.15s; - } - .stat-card:hover { transform: translateY(-2px); box-shadow: 0 4px 12px rgba(0,0,0,0.12); } - .stat-icon { - width: 36px; height: 36px; border-radius: 8px; - display: flex; align-items: center; justify-content: center; - font-size: 1.1rem; margin-bottom: 0.1rem; - } - .stat-card.highlight .stat-icon { background: rgba(79,70,229,0.12); } - .stat-card.c-blauw .stat-icon { background: rgba(99,102,241,0.1); } - .stat-card.accent-groen .stat-icon { background: rgba(16,185,129,0.12); } - .stat-card.accent-oranje .stat-icon { background: rgba(245,158,11,0.12); } - .stat-card.accent-roze .stat-icon { background: rgba(236,72,153,0.12); } + .btn-secondary:hover { background: var(--gray-300); } + .btn-success { background: var(--success); color: white; } + .btn-success:hover { background: #059669; } - .stat-value { font-size: 1.9rem; font-weight: 800; line-height: 1; letter-spacing: -0.02em; } - .stat-card.highlight .stat-value { color: var(--primary); } - .stat-card.c-blauw .stat-value { color: #6366f1; } - .stat-card.accent-groen .stat-value { color: var(--status-groen); } - .stat-card.accent-oranje .stat-value { color: var(--status-oranje); } - .stat-card.accent-roze .stat-value { color: var(--status-roze); } - .stat-label { font-size: 0.7rem; text-transform: uppercase; letter-spacing: 0.06em; color: var(--gray-400); font-weight: 600; } - .section { background: white; border-radius: 12px; padding: 1.5rem; margin-bottom: 1rem; box-shadow: 0 1px 3px rgba(0,0,0,0.1); } - .section h2 { font-size: 1.1rem; color: var(--gray-700); margin-bottom: 1rem; } + /* Stats */ + .stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(130px, 1fr)); gap: 0.75rem; margin-bottom: 1rem; } + .stat-card { background: white; border-radius: 10px; padding: 1rem; text-align: center; box-shadow: 0 1px 3px rgba(0,0,0,.08); } + .stat-card.highlight { background: linear-gradient(135deg, var(--primary), var(--primary-dark)); color: white; } + .stat-value { font-size: 1.8rem; font-weight: 700; line-height: 1.2; } + .stat-label { font-size: 0.7rem; text-transform: uppercase; letter-spacing: .05em; opacity: .8; margin-top: .2rem; } - .status-selector { width: 22px; height: 22px; border-radius: 5px; border: 2px solid var(--gray-300); display: inline-flex; align-items: center; justify-content: center; font-size: .85rem; font-weight: bold; flex-shrink: 0; } - .status-selector.status-groen { background: var(--status-groen); border-color: var(--status-groen); color: white; } - .status-selector.status-groen::after { content: '✓'; } - .status-selector.status-oranje { background: var(--status-oranje); border-color: var(--status-oranje); color: white; } - .status-selector.status-oranje::after { content: '~'; } - .status-selector.status-roze { background: var(--status-roze); border-color: var(--status-roze); color: white; } - .status-selector.status-roze::after { content: '!'; } - .status-selector.status-none { background: white; border-color: var(--gray-300); color: var(--gray-400); } - .status-selector.status-none::after { content: '○'; } - .legend-container { background: white; border-radius: 12px; padding: 1rem 1.5rem; margin-bottom: 1rem; box-shadow: 0 1px 3px rgba(0,0,0,.1); } - .legend-title { font-weight: 600; color: var(--gray-700); margin-bottom: .75rem; font-size: .9rem; } - .legend-grid { display: flex; flex-wrap: wrap; gap: 1.5rem; align-items: center; } - .legend-section { display: flex; flex-wrap: wrap; gap: .75rem; align-items: center; } - .legend-item { display: flex; align-items: center; gap: .4rem; font-size: .85rem; white-space: nowrap; } - .legend-color { width: 18px; height: 18px; border-radius: 4px; flex-shrink: 0; } - .legend-divider { width: 1px; height: 28px; background: var(--gray-300); margin: 0 .5rem; } - .filters-bar { display: flex; gap: 1rem; flex-wrap: wrap; align-items: flex-end; margin-bottom: 1rem; } - .filter-group { display: flex; flex-direction: column; gap: 0.25rem; } - .filter-group label { font-size: 0.75rem; font-weight: 500; color: var(--gray-500); } - .filter-group select, .filter-group input { padding: 0.5rem; border: 1px solid var(--gray-300); border-radius: 6px; font-size: 0.9rem; min-width: 150px; } - .table-scroll { overflow-x: auto; } - table { width: 100%; border-collapse: collapse; font-size: 0.85rem; } - thead { background: var(--gray-50); } - th { padding: 0.75rem 0.5rem; text-align: center; font-weight: 600; color: var(--gray-600); border-bottom: 2px solid var(--gray-200); white-space: nowrap; } - th.goal-header { text-align: left; min-width: 250px; } - th.teacher-header { min-width: 80px; font-size: 0.75rem; } - td { padding: 0.5rem; border-bottom: 1px solid var(--gray-100); text-align: center; } + /* Tabs */ + .tabs { display: flex; gap: .25rem; background: white; border-radius: 10px; padding: .35rem; margin-bottom: 1rem; box-shadow: 0 1px 3px rgba(0,0,0,.08); flex-wrap: wrap; } + .tab-btn { flex: 1; min-width: 100px; padding: .5rem .75rem; border: none; border-radius: 7px; font-size: .85rem; font-weight: 500; cursor: pointer; background: transparent; color: var(--gray-600); transition: all .2s; } + .tab-btn.active { background: var(--primary); color: white; } + .tab-btn:hover:not(.active) { background: var(--gray-100); } + .tab-content { display: none; } + .tab-content.active { display: block; } + + /* Sectie card */ + .card { background: white; border-radius: 12px; padding: 1.5rem; margin-bottom: 1rem; box-shadow: 0 1px 3px rgba(0,0,0,.08); } + .card h2 { font-size: 1.05rem; color: var(--gray-700); margin-bottom: 1rem; display: flex; align-items: center; gap: .5rem; } + + /* Klassen chips */ + .klas-chips { display: flex; flex-wrap: wrap; gap: .6rem; } + .klas-chip { background: var(--gray-50); border: 1px solid var(--gray-200); border-radius: 8px; padding: .5rem .85rem; font-size: .85rem; } + .klas-chip .klas-name { font-weight: 600; color: var(--gray-800); } + .klas-chip .klas-teachers { font-size: .75rem; color: var(--gray-500); margin-top: .15rem; } + + /* Vak progress cards */ + .vak-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: .75rem; } + .vak-card { background: var(--gray-50); border-radius: 8px; padding: 1rem; } + .vak-card-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: .6rem; } + .vak-card-header h3 { font-size: .9rem; font-weight: 600; } + .progress-bar { height: 8px; background: var(--gray-200); border-radius: 4px; overflow: hidden; } + .progress-inner { height: 100%; display: flex; } + .p-groen { background: var(--status-groen); } + .p-oranje { background: var(--status-oranje); } + .p-roze { background: var(--status-roze); } + .vak-legend { display: flex; gap: .75rem; font-size: .75rem; color: var(--gray-600); margin-top: .4rem; flex-wrap: wrap; } + .dot { width: 9px; height: 9px; border-radius: 50%; display: inline-block; margin-right: 2px; } + + /* Filter balk */ + .filter-bar { background: white; border-radius: 10px; padding: .85rem 1.25rem; margin-bottom: 1rem; box-shadow: 0 1px 3px rgba(0,0,0,.08); display: flex; gap: .75rem; flex-wrap: wrap; align-items: flex-end; } + .fg { display: flex; flex-direction: column; gap: .2rem; } + .fg label { font-size: .72rem; font-weight: 600; color: var(--gray-500); text-transform: uppercase; letter-spacing: .04em; } + .fg select, .fg input { padding: .45rem .65rem; border: 1px solid var(--gray-300); border-radius: 6px; font-size: .88rem; min-width: 140px; } + .fg select:focus, .fg input:focus { outline: none; border-color: var(--primary); box-shadow: 0 0 0 3px rgba(79,70,229,.1); } + .leeftijd-cbs { display: flex; flex-wrap: wrap; gap: .3rem; } + .lft-cb { display: flex; align-items: center; gap: .2rem; padding: .28rem .5rem; border: 1px solid var(--gray-300); border-radius: 5px; font-size: .78rem; cursor: pointer; user-select: none; } + .lft-cb:has(input:checked) { background: var(--primary); border-color: var(--primary); color: white; } + .lft-cb input { display: none; } + + /* Detail tabel */ + .table-wrap { overflow-x: auto; } + table { width: 100%; border-collapse: collapse; font-size: .83rem; } + thead { background: var(--gray-50); position: sticky; top: 0; z-index: 5; } + th { padding: .65rem .6rem; text-align: center; font-weight: 600; color: var(--gray-600); border-bottom: 2px solid var(--gray-200); white-space: nowrap; } + th.goal-col { text-align: left; min-width: 240px; } + th.klas-col { min-width: 75px; font-size: .75rem; } + td { padding: .45rem .6rem; border-bottom: 1px solid var(--gray-100); text-align: center; } td.goal-cell { text-align: left; } tr:hover { background: var(--gray-50); } - .goal-code { font-weight: 600; color: var(--gray-700); } - .goal-desc { font-size: 0.8rem; color: var(--gray-500); max-width: 300px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } - .status-indicator { width: 24px; height: 24px; border-radius: 4px; display: inline-flex; align-items: center; justify-content: center; font-size: 0.75rem; font-weight: bold; } - .status-indicator.groen { background: var(--status-groen); color: white; } - .status-indicator.oranje { background: var(--status-oranje); color: white; } - .status-indicator.roze { background: var(--status-roze); color: white; } - .status-indicator.none { background: var(--gray-200); color: var(--gray-400); } - .progress-bar { height: 8px; background: var(--gray-200); border-radius: 4px; overflow: hidden; min-width: 60px; } - .progress-bar-inner { height: 100%; display: flex; } - .p-groen { background: var(--status-groen); } - .p-oranje { background: var(--status-oranje); } - .p-roze { background: var(--status-roze); } - .leeftijd-badge { font-size: 0.7rem; padding: 0.15rem 0.35rem; background: var(--gray-200); border-radius: 3px; } - .empty-state { text-align: center; padding: 3rem; color: var(--gray-500); } - .loading { text-align: center; padding: 3rem; color: var(--gray-500); } - .notification { position: fixed; bottom: 1rem; right: 1rem; padding: 1rem 1.5rem; border-radius: 8px; color: white; font-weight: 500; transform: translateY(100px); opacity: 0; transition: all 0.3s; z-index: 1001; } + .goal-nr { font-weight: 600; font-size: .82rem; } + .goal-desc { font-size: .77rem; color: var(--gray-500); max-width: 260px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + .lft-badges { display: flex; flex-wrap: wrap; gap: .2rem; } + .lft-badge { font-size: .68rem; padding: .1rem .3rem; background: var(--gray-200); border-radius: 3px; color: var(--gray-600); } + .si { width: 26px; height: 26px; border-radius: 5px; display: inline-flex; align-items: center; justify-content: center; font-size: .78rem; font-weight: 700; } + .si.groen { background: var(--status-groen); color: white; } + .si.oranje { background: var(--status-oranje); color: white; } + .si.roze { background: var(--status-roze); color: white; } + .si.none { background: var(--gray-200); color: var(--gray-400); } + .sum-bar { display: flex; height: 18px; border-radius: 3px; overflow: hidden; min-width: 50px; } + .sum-cell { background: var(--gray-50); } + + /* Klasvergelijking */ + .vergelijk-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; margin-bottom: 1rem; } + @media (max-width: 600px) { .vergelijk-grid { grid-template-columns: 1fr; } } + .vergelijk-select { padding: .5rem .75rem; border: 1px solid var(--gray-300); border-radius: 6px; font-size: .9rem; width: 100%; } + .diff-row-same { background: #f0fdf4 !important; } + .diff-row-differ { background: #fff7ed !important; } + + /* Empty / loading */ + .empty { text-align: center; padding: 3rem; color: var(--gray-400); font-size: .9rem; } + .spinner { width: 36px; height: 36px; border: 3px solid var(--gray-200); border-top-color: var(--primary); border-radius: 50%; animation: spin 1s linear infinite; margin: 0 auto .75rem; } + @keyframes spin { to { transform: rotate(360deg); } } + + /* Notificatie */ + .notification { position: fixed; bottom: 1rem; right: 1rem; padding: .9rem 1.4rem; border-radius: 8px; color: white; font-weight: 500; transform: translateY(100px); opacity: 0; transition: all .3s; z-index: 1001; } .notification.show { transform: translateY(0); opacity: 1; } .notification.success { background: var(--success); } - .notification.error { background: var(--danger); } - /* Leerkrachten beheer */ - .teacher-list { display: flex; flex-wrap: wrap; gap: 0.5rem; margin-bottom: 1rem; } - .teacher-chip { display: inline-flex; align-items: center; gap: 0.5rem; padding: 0.4rem 0.75rem; background: var(--gray-100); border-radius: 9999px; font-size: 0.85rem; } - .teacher-chip .name { font-weight: 500; } - .teacher-chip .klas { color: var(--gray-500); } - .teacher-chips { display: flex; flex-wrap: wrap; gap: 0.5rem; } + .notification.error { background: var(--danger); } + .notification.warning { background: var(--warning); } - /* Leerkrachten sectie */ - .teachers-section { background: white; border-radius: 12px; padding: 1.5rem; margin-bottom: 1rem; box-shadow: 0 1px 3px rgba(0,0,0,0.1); } - .teachers-section h2 { font-size: 1.1rem; color: var(--gray-700); margin-bottom: 1rem; display: flex; align-items: center; gap: 0.5rem; } - .action-bar { display: flex; gap: 0.5rem; flex-wrap: wrap; } + /* Legenda footer */ + .legend-footer { padding: .85rem 1.25rem; border-top: 1px solid var(--gray-200); display: flex; gap: 1.25rem; flex-wrap: wrap; font-size: .78rem; color: var(--gray-600); } + .legend-item { display: flex; align-items: center; gap: .35rem; } - /* Statistieken */ - .stats-overview { background: white; border-radius: 12px; padding: 1.5rem; margin-bottom: 1rem; box-shadow: 0 1px 3px rgba(0,0,0,0.1); } - .stats-overview h2 { font-size: 1.1rem; color: var(--gray-700); margin-bottom: 1rem; } - - /* Vak statistieken */ - .vak-stats h2 { font-size: 1.1rem; color: var(--gray-700); margin-bottom: 1rem; } - .vak-card { background: var(--gray-50); border-radius: 8px; padding: 1rem; margin-bottom: 0.75rem; } - .vak-card-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.75rem; } - .vak-card-header h3 { font-size: 1rem; color: var(--gray-800); } - .vak-card-header .percentage { font-weight: 600; color: var(--primary); } - .vak-card-stats { display: flex; gap: 1rem; font-size: 0.8rem; color: var(--gray-600); } - .vak-card-stats span { display: flex; align-items: center; gap: 0.25rem; } - .dot { width: 10px; height: 10px; border-radius: 50%; flex-shrink: 0; } - .dot-groen { background: var(--status-groen); } - .dot-oranje { background: var(--status-oranje); } - .dot-roze { background: var(--status-roze); } - - /* Progress bars vak stats */ - .progress-groen { background: var(--status-groen); } - .progress-oranje { background: var(--status-oranje); } - .progress-roze { background: var(--status-roze); } - .modal-overlay { display: none; position: fixed; inset: 0; background: rgba(0,0,0,0.5); z-index: 1000; align-items: center; justify-content: center; } - .modal-overlay.active { display: flex; } - .modal { background: white; border-radius: 12px; padding: 1.5rem; max-width: 450px; width: 90%; } - .modal h2 { margin-bottom: 1rem; } - .form-group { margin-bottom: 1rem; } - .form-group label { display: block; font-size: 0.85rem; font-weight: 600; color: var(--gray-700); margin-bottom: 0.35rem; } - .form-group input { width: 100%; padding: 0.6rem 0.75rem; border: 1px solid var(--gray-300); border-radius: 6px; font-size: 0.9rem; } - .modal-buttons { display: flex; gap: 0.5rem; justify-content: flex-end; margin-top: 1rem; } - - /* Tab navigatie */ - .tab-btn { - padding: 0.5rem 1rem; border: none; border-radius: 8px 8px 0 0; - background: var(--gray-100); color: var(--gray-600); - font-size: 0.875rem; font-weight: 500; cursor: pointer; - transition: all 0.15s; border-bottom: 2px solid transparent; + @media (prefers-color-scheme: dark) { + :root { --gray-50:#1a1a2e;--gray-100:#16213e;--gray-200:#0f3460;--gray-300:#1a1a3e;--gray-700:#e5e7eb;--gray-800:#f3f4f6; } + body { background:#0f172a;color:#e2e8f0; } + .card,.tabs,.stats-grid .stat-card,.filter-bar { background:#1e293b!important; } + th,thead { background:#1e293b!important;color:#94a3b8!important;border-color:#334155!important; } + td { border-color:#1e293b!important; } + tr:hover { background:#263548!important; } + select,input { background:#0f172a!important;color:#e2e8f0!important;border-color:#334155!important; } + .vak-card,.klas-chip { background:#162032!important;border-color:#334155!important; } + .lft-badge { background:#334155!important;color:#94a3b8!important; } + .lft-cb { border-color:#334155!important;color:#e2e8f0; } + .si.none { background:#334155!important; } + .sum-cell { background:#1e293b!important; } + .tab-btn { color:#94a3b8; } + .btn-secondary { background:#334155!important;color:#e2e8f0!important; } + .diff-row-same { background:#064e3b!important; } + .diff-row-differ { background:#451a03!important; } } - .tab-btn:hover { background: var(--gray-200); color: var(--gray-800); } - .tab-btn.active { - background: white; color: var(--primary); - border-bottom: 2px solid var(--primary); - box-shadow: 0 -2px 8px rgba(79,70,229,0.08); - } - - /* Version badge */ - .version-badge { - font-size: 0.7rem; background: rgba(255,255,255,0.25); - padding: 0.15rem 0.5rem; border-radius: 9999px; font-weight: 500; - vertical-align: middle; - } - - /* Klasoverzicht progress */ - .klas-progress-row { - display: flex; align-items: center; gap: 0.75rem; - margin-bottom: 0.5rem; - } - .klas-label { font-size: 0.82rem; color: var(--gray-600); min-width: 80px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } - .klas-progress-bar { - flex: 1; height: 10px; background: var(--gray-200); - border-radius: 5px; overflow: hidden; display: flex; - } - .klas-pct { font-size: 0.78rem; color: var(--gray-500); min-width: 36px; text-align: right; } - - /* Vergelijking progress bars */ - .vergelijk-bar-wrap { - height: 12px; background: var(--gray-200); - border-radius: 4px; overflow: hidden; - margin-bottom: 0.2rem; - } - -@media (prefers-color-scheme: dark) { - :root { - --gray-50: #1a1a2e; - --gray-100: #16213e; - --gray-200: #0f3460; - --gray-300: #1a1a3e; - --gray-400: #6b7280; - --gray-500: #9ca3af; - --gray-600: #d1d5db; - --gray-700: #e5e7eb; - --gray-800: #f3f4f6; - --gray-900: #f9fafb; - } - - body { background: #0f172a; color: #e2e8f0; } - - /* Kaarten en secties - NIET de gradient header! */ - .card, .section, .stat-card, .school-card, - .table-container, .filters-container, .legend-container, - .stats-bar .stat-card, .stats-overview, .vak-stats, - .import-section, .detail-section, .filters-bar, - .teachers-section { - background: #1e293b !important; - border-color: #334155 !important; - } - - /* Tab knoppen */ - .tab-btn { background: #162032 !important; color: #94a3b8 !important; } - .tab-btn:hover { background: #1e293b !important; color: #e2e8f0 !important; } - .tab-btn.active { background: #1e293b !important; color: #a5b4fc !important; border-bottom-color: #6366f1 !important; } - - /* Tabellen */ - thead, th { background: #1e293b !important; color: #94a3b8 !important; border-color: #334155 !important; } - td { border-color: #1e293b !important; color: #e2e8f0; } - tr:hover td, tr:hover { background: #263548 !important; } - tr.status-groen { background: #064e3b !important; } - tr.status-groen:hover { background: #065f46 !important; } - tr.status-oranje { background: #451a03 !important; } - tr.status-oranje:hover { background: #78350f !important; } - tr.status-roze { background: #500724 !important; } - tr.status-roze:hover { background: #701a35 !important; } - - /* Inputs en selects */ - input, select, textarea { - background: #0f172a !important; - color: #e2e8f0 !important; - border-color: #334155 !important; - } - input::placeholder { color: #64748b !important; } - input:focus, select:focus, textarea:focus { - border-color: #6366f1 !important; - box-shadow: 0 0 0 3px rgba(99,102,241,0.2) !important; - } - - /* Role select inline */ - .role-select { - background: #1e293b !important; - color: #e2e8f0 !important; - border-color: #334155 !important; - } - - /* Modals */ - .modal { background: #1e293b !important; color: #e2e8f0; } - .modal h2 { color: #f1f5f9; } - - /* Knoppen */ - .btn-secondary { background: #334155 !important; color: #e2e8f0 !important; } - .btn-secondary:hover { background: #475569 !important; } - - /* Status selector knoppen (leerkracht tabel) */ - .status-selector.status-none { background: #1e293b !important; border-color: #475569 !important; color: #64748b !important; } - - /* Stat cards */ - .stat-card { background: #1e293b !important; } - .stat-card.highlight .stat-value { color: #a5b4fc !important; } - .stat-card.c-blauw .stat-value { color: #818cf8 !important; } - .stat-card.accent-groen .stat-value { color: #34d399 !important; } - .stat-card.accent-oranje .stat-value { color: #fbbf24 !important; } - .stat-card.accent-roze .stat-value { color: #f472b6 !important; } - .stat-card.highlight .stat-icon { background: rgba(165,180,252,0.15) !important; } - .stat-card.c-blauw .stat-icon { background: rgba(129,140,248,0.15) !important; } - .stat-card.accent-groen .stat-icon { background: rgba(52,211,153,0.15) !important; } - .stat-card.accent-oranje .stat-icon { background: rgba(251,191,36,0.15) !important; } - .stat-card.accent-roze .stat-icon { background: rgba(244,114,182,0.15) !important; } - - /* School card header */ - .school-card-header { background: #162032 !important; border-color: #334155 !important; } - .school-card { border-color: #334155 !important; } - - /* Drop zone */ - .drop-zone { background: #162032 !important; border-color: #334155 !important; } - .drop-zone:hover, .drop-zone.over { background: #1a2744 !important; border-color: #6366f1 !important; } - - /* Domain chips */ - .domain-chip { background: #1e3a5f !important; border-color: #2563eb !important; color: #93c5fd !important; } - - /* Badges */ - .leeftijd-badge { background: #334155 !important; color: #94a3b8 !important; } - .ebg-begrijpen { color: #1f2937 !important; } - - /* MIA container */ - .mia-container { background: #162032 !important; } - .mia-item { background: #1e293b !important; border-color: #334155 !important; color: #e2e8f0 !important; } - .mia-item.niet-aanklikbaar { background: #162032 !important; color: #64748b !important; } - - /* Not configured box */ - .not-configured { background: #162032 !important; border-color: #334155 !important; color: #94a3b8 !important; } - .not-configured code { background: #0f172a !important; color: #a5b4fc !important; } - - /* Profile section */ - .profile-section { background: #162032 !important; } - - /* Leeftijd checkboxes */ - .leeftijd-checkbox { border-color: #334155 !important; color: #e2e8f0; } - .leeftijd-checkbox:has(input:checked) { background: var(--primary) !important; } - - /* Vak indicator */ - .vak-indicator { /* gradient blijft, ziet er goed uit */ } - - /* Nieuwe layout blokken */ - .teachers-section { background: #1e293b !important; } - .stats-overview { background: #1e293b !important; } - .teacher-chip { background: #334155 !important; color: #e2e8f0 !important; } - .teacher-chip .klas { color: #94a3b8 !important; } - .vak-card-header h3 { color: #e2e8f0 !important; } - .vak-card-stats { color: #94a3b8 !important; } - - /* Klas progress bars achtergrond */ - .progress-bar, .klas-progress-bar, .vergelijk-bar-wrap { background: #334155 !important; } - - /* Vak card */ - .vak-card { background: #162032 !important; } - - /* Upload results */ - .upload-ok { background: #064e3b !important; border-color: #065f46 !important; } - .upload-err { background: #450a0a !important; border-color: #7f1d1d !important; } - - /* Alert boxes */ - .alert-error { background: #450a0a !important; border-color: #7f1d1d !important; color: #fca5a5 !important; } - .alert-success { background: #064e3b !important; border-color: #065f46 !important; color: #6ee7b7 !important; } - .alert-warning { background: #451a03 !important; border-color: #78350f !important; color: #fcd34d !important; } - .alert-info { background: #1e3a5f !important; border-color: #1d4ed8 !important; color: #93c5fd !important; } - - /* Error text */ - .form-error, #sa-error, #addUser-error { color: #f87171 !important; } - .form-hint { color: #64748b !important; } - - /* Superadmin toggle */ - .superadmin-toggle { border-color: #334155 !important; } - .superadmin-toggle button { color: #475569 !important; } - .superadmin-toggle button:hover { color: #94a3b8 !important; } - - /* Superadmin form inputs */ - .superadmin-form label { color: #94a3b8 !important; } - - /* Footer */ - .footer { color: #64748b !important; } - /* Legend container */ - .legend-container { background: #1e293b !important; } - .legend-title { color: #94a3b8 !important; } - .legend-divider { background: #334155 !important; } - .status-selector.status-none { background: #1e293b !important; border-color: #475569 !important; color: #64748b !important; } - .status-legend { background: #162032 !important; border-color: #334155 !important; color: #94a3b8 !important; } - - /* Scrollbar (webkit) */ - ::-webkit-scrollbar { width: 8px; height: 8px; } - ::-webkit-scrollbar-track { background: #0f172a; } - ::-webkit-scrollbar-thumb { background: #334155; border-radius: 4px; } - ::-webkit-scrollbar-thumb:hover { background: #475569; } -} - - - +
@@ -371,456 +149,223 @@
-

🏫 Directeur Dashboard v4.0

-
Schooloverzicht van alle leerdoelen en leerkrachten
+

🏫 Directeur Dashboard v5.0

+
Laden...
-
-
- - -
- - - 👩‍🏫 Leerkrachtsweergave - +
+ + 👤 Leerkrachtenweergave Uitloggen
- -
-

📊 Schoolbrede statistieken

-
-
-
👩‍🏫
-
-
-
Leerkrachten
-
-
-
📚
-
-
-
Vakken
-
-
-
📋
-
-
-
Beoordelingen
-
-
-
-
-
-
Groen
-
-
-
🔶
-
-
-
Oranje
-
-
-
🆕
-
-
-
Roze
+ +
+
-
Klassen
+
-
Vakken
+
-
Beoordelingen
+
-
Groen
+
-
Oranje
+
-
Roze
+
+ + +
+ + + + +
+ + +
+
+

🏫 Klassen overzicht

+
+
Laden...
+
+

📊 Voortgang per klas

+
Laden...
+
- -
-

- - - - - - - Leerkrachten -

-
-
- - - - - Klassenbeheer - - - + +
+
+

📚 Statistieken per vak

+
Laden...
- - - - -
- - - -
- - -
- - -
-
Legenda
-
-
-
-
- Doen we al -
-
-
- Doen we ongeveer -
-
-
- Nieuw (doen we nog niet) -
-
-
- Niet beoordeeld -
-
-
-
-
Groen = consensus
-
Oranje = gedeeltelijk
-
Roze = nog te doen
-
-
-
- - -
-
+ +
+
+
-
-
- - -
-
+
-
-
+
- - - - + - +
-
- - -
-
- - -
-
+
-
- - - - - - - - - +
+ {% for age in ['2,5-4','4-5','5-6','6-7','7-8','8-9','9-10','10-11','11-12'] %} + + {% endfor %} +
+
+
+ + +
+
+ +
+ +
- - -
- - - - - - - -
DoelSamenvatting
Laden...
-
- - -
-
Doen we al
-
~
Doen we ongeveer
-
!
Nieuw
-
Niet beoordeeld
-
- -
- - -