Compare commits
5 Commits
5efd5fce26
...
add-google
| Author | SHA1 | Date | |
|---|---|---|---|
| 28c05edb0b | |||
| 85778855ca | |||
| 51c0755d67 | |||
| 5f2e1fdb1b | |||
| aa47399b62 |
@@ -95,7 +95,7 @@ def create_app():
|
||||
'style-src': ["'self'", "'unsafe-inline'"], # inline styles in templates (aanvaardbaar)
|
||||
'img-src': ["'self'", 'data:'],
|
||||
'font-src': ["'self'"],
|
||||
'connect-src': ["'self'"],
|
||||
'connect-src': ["'self'", 'cdnjs.cloudflare.com'],
|
||||
'form-action': ["'self'"], # voorkomt form hijacking
|
||||
'base-uri': ["'self'"], # voorkomt base tag injection
|
||||
'frame-ancestors': ["'none'"], # clickjacking preventie
|
||||
|
||||
23
backend/migrations/versions/0002_assessment_opmerking.py
Normal file
23
backend/migrations/versions/0002_assessment_opmerking.py
Normal file
@@ -0,0 +1,23 @@
|
||||
"""assessment: voeg opmerking kolom toe
|
||||
|
||||
Revision ID: 0002
|
||||
Revises: 0001
|
||||
Create Date: 2026-03-02
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
revision = '0002'
|
||||
down_revision = '0001'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
op.add_column('assessments',
|
||||
sa.Column('opmerking', sa.String(500), nullable=True)
|
||||
)
|
||||
|
||||
|
||||
def downgrade():
|
||||
op.drop_column('assessments', 'opmerking')
|
||||
@@ -151,6 +151,7 @@ class Assessment(db.Model):
|
||||
vak_id = db.Column(db.String(50), nullable=False)
|
||||
goal_id = db.Column(db.String(50), nullable=False)
|
||||
status = db.Column(db.String(10), nullable=False)
|
||||
opmerking = db.Column(db.String(500), nullable=True)
|
||||
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||
|
||||
user = db.relationship('User')
|
||||
@@ -167,6 +168,7 @@ class Assessment(db.Model):
|
||||
'vak_id': self.vak_id,
|
||||
'goal_id': self.goal_id,
|
||||
'status': self.status,
|
||||
'opmerking': self.opmerking,
|
||||
'updated_at': self.updated_at.isoformat() if self.updated_at else None,
|
||||
}
|
||||
|
||||
|
||||
@@ -265,11 +265,11 @@ def remove_user_from_school(school_id, user_id):
|
||||
return jsonify({'deleted': True})
|
||||
|
||||
|
||||
# ── Scholengroep ICT beheer (superadmin) ──────────────────────────────────────
|
||||
# ── Scholengroep ICT beheer ───────────────────────────────────────────────────
|
||||
|
||||
@admin_bp.route('/scholengroep-ict', methods=['GET'])
|
||||
@login_required
|
||||
@superadmin_required
|
||||
@scholengroep_ict_required # scholengroep_ict mag de lijst lezen; superadmin ook
|
||||
def list_scholengroep_ict():
|
||||
users = User.query.filter_by(role='scholengroep_ict', is_active=True)\
|
||||
.order_by(User.last_name).all()
|
||||
|
||||
@@ -70,10 +70,11 @@ def get_assessments():
|
||||
@login_required
|
||||
@limiter.limit('120 per minute') # max 2 per seconde per gebruiker
|
||||
def save_assessment():
|
||||
data = request.get_json() or {}
|
||||
vak_id = (data.get('vak_id') or '').strip()
|
||||
goal_id = (data.get('goal_id') or '').strip()
|
||||
status = (data.get('status') or '').strip()
|
||||
data = request.get_json() or {}
|
||||
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
|
||||
@@ -104,6 +105,7 @@ def save_assessment():
|
||||
|
||||
if assessment:
|
||||
assessment.status = status
|
||||
assessment.opmerking = opmerking or None
|
||||
assessment.updated_at = datetime.utcnow()
|
||||
else:
|
||||
assessment = Assessment(
|
||||
@@ -113,6 +115,7 @@ def save_assessment():
|
||||
vak_id=vak_id,
|
||||
goal_id=goal_id,
|
||||
status=status,
|
||||
opmerking=opmerking or None,
|
||||
)
|
||||
db.session.add(assessment)
|
||||
|
||||
@@ -124,6 +127,55 @@ def save_assessment():
|
||||
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 {}
|
||||
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 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)
|
||||
if not school_year:
|
||||
return jsonify({'error': 'Geen actief schooljaar'}), 400
|
||||
|
||||
assessment = Assessment.query.filter_by(
|
||||
user_id=current_user.id,
|
||||
school_year_id=school_year.id,
|
||||
vak_id=vak_id,
|
||||
goal_id=goal_id,
|
||||
).first()
|
||||
|
||||
if assessment:
|
||||
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,
|
||||
school_year_id=school_year.id,
|
||||
vak_id=vak_id,
|
||||
goal_id=goal_id,
|
||||
status='',
|
||||
opmerking=opmerking or None,
|
||||
)
|
||||
db.session.add(assessment)
|
||||
|
||||
db.session.commit()
|
||||
return jsonify({'ok': True})
|
||||
|
||||
@api_bp.route('/assessments/bulk-import', methods=['POST'])
|
||||
@login_required
|
||||
@limiter.limit('5 per minute')
|
||||
|
||||
@@ -24,10 +24,40 @@
|
||||
.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(150px, 1fr)); gap: 1rem; margin-bottom: 1rem; }
|
||||
.stat-card { background: white; border-radius: 8px; padding: 1rem; text-align: center; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
|
||||
.stat-value { font-size: 2rem; font-weight: 700; }
|
||||
.stat-label { font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.05em; color: var(--gray-500); margin-top: 0.25rem; }
|
||||
.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); }
|
||||
|
||||
.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; }
|
||||
|
||||
@@ -82,6 +112,36 @@
|
||||
/* 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; }
|
||||
|
||||
/* 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; }
|
||||
|
||||
/* 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%; }
|
||||
@@ -163,6 +223,16 @@
|
||||
|
||||
/* 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; }
|
||||
@@ -198,6 +268,14 @@
|
||||
/* 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; }
|
||||
|
||||
/* Progress bars achtergrond */
|
||||
.progress-bar { background: #334155 !important; }
|
||||
|
||||
@@ -242,13 +320,17 @@
|
||||
::-webkit-scrollbar-thumb:hover { background: #475569; }
|
||||
}
|
||||
</style>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf-autotable/3.5.31/jspdf.plugin.autotable.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
|
||||
<!-- Header -->
|
||||
<div class="header">
|
||||
<div>
|
||||
<h1>🏫 Directeur Dashboard</h1>
|
||||
<div style="opacity:0.85;font-size:0.85rem;margin-top:0.25rem;" id="schoolInfo"></div>
|
||||
<h1>🏫 Directeur Dashboard <span class="version-badge">v4.0</span></h1>
|
||||
<div style="opacity:0.85;font-size:0.85rem;margin-top:0.25rem;" id="schoolInfo">Schooloverzicht van alle leerdoelen en leerkrachten</div>
|
||||
</div>
|
||||
<div style="display:flex;gap:0.5rem;align-items:center;flex-wrap:wrap;">
|
||||
<div style="display:flex;flex-direction:column;gap:0.2rem;">
|
||||
@@ -263,23 +345,72 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Stats -->
|
||||
<div class="stats-grid" id="statsGrid">
|
||||
<div class="stat-card"><div class="stat-value" id="statTeachers">-</div><div class="stat-label">Leerkrachten</div></div>
|
||||
<div class="stat-card"><div class="stat-value" id="statVakken">-</div><div class="stat-label">Vakken</div></div>
|
||||
<div class="stat-card"><div class="stat-value" id="statBeoordeeld">-</div><div class="stat-label">Beoordelingen</div></div>
|
||||
<div class="stat-card" style="border-left:3px solid var(--status-groen)"><div class="stat-value" id="statGroen">-</div><div class="stat-label">Groen</div></div>
|
||||
<div class="stat-card" style="border-left:3px solid var(--status-oranje)"><div class="stat-value" id="statOranje">-</div><div class="stat-label">Oranje</div></div>
|
||||
<div class="stat-card" style="border-left:3px solid var(--status-roze)"><div class="stat-value" id="statRoze">-</div><div class="stat-label">Roze</div></div>
|
||||
<!-- Schoolbrede statistieken -->
|
||||
<div class="stats-overview" id="statsOverview">
|
||||
<h2>📊 Schoolbrede statistieken</h2>
|
||||
<div class="stats-grid" id="statsGrid">
|
||||
<div class="stat-card highlight">
|
||||
<div class="stat-icon">👩🏫</div>
|
||||
<div class="stat-value" id="statTeachers">-</div>
|
||||
<div class="stat-label">Leerkrachten</div>
|
||||
</div>
|
||||
<div class="stat-card c-blauw">
|
||||
<div class="stat-icon">📚</div>
|
||||
<div class="stat-value" id="statVakken">-</div>
|
||||
<div class="stat-label">Vakken</div>
|
||||
</div>
|
||||
<div class="stat-card c-blauw">
|
||||
<div class="stat-icon">📋</div>
|
||||
<div class="stat-value" id="statBeoordeeld">-</div>
|
||||
<div class="stat-label">Beoordelingen</div>
|
||||
</div>
|
||||
<div class="stat-card accent-groen">
|
||||
<div class="stat-icon">✅</div>
|
||||
<div class="stat-value" id="statGroen">-</div>
|
||||
<div class="stat-label">Groen</div>
|
||||
</div>
|
||||
<div class="stat-card accent-oranje">
|
||||
<div class="stat-icon">🔶</div>
|
||||
<div class="stat-value" id="statOranje">-</div>
|
||||
<div class="stat-label">Oranje</div>
|
||||
</div>
|
||||
<div class="stat-card accent-roze">
|
||||
<div class="stat-icon">🆕</div>
|
||||
<div class="stat-value" id="statRoze">-</div>
|
||||
<div class="stat-label">Roze</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Leerkrachten beheer -->
|
||||
<div class="section">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:1rem;">
|
||||
<h2>👩🏫 Leerkrachten</h2>
|
||||
<!-- Leerkrachten sectie -->
|
||||
<div class="teachers-section" id="teachersSection">
|
||||
<h2>
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path>
|
||||
<circle cx="9" cy="7" r="4"></circle>
|
||||
<path d="M23 21v-2a4 4 0 0 0-3-3.87"></path>
|
||||
<path d="M16 3.13a4 4 0 0 1 0 7.75"></path>
|
||||
</svg>
|
||||
Leerkrachten
|
||||
</h2>
|
||||
<div class="teacher-chips" id="teacherChips"></div>
|
||||
<div class="action-bar" style="margin-top:1rem;">
|
||||
<button id="btnAddTeacher" class="btn btn-primary">+ Leerkracht toevoegen</button>
|
||||
<button id="btnExportCSV" class="btn btn-secondary">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path><polyline points="7 10 12 15 17 10"></polyline><line x1="12" y1="15" x2="12" y2="3"></line></svg>
|
||||
Exporteer CSV
|
||||
</button>
|
||||
<button id="btnExportPDF" class="btn btn-secondary">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path><polyline points="7 10 12 15 17 10"></polyline><line x1="12" y1="15" x2="12" y2="3"></line></svg>
|
||||
Exporteer PDF
|
||||
</button>
|
||||
</div>
|
||||
<div class="teacher-list" id="teacherList">Laden...</div>
|
||||
</div>
|
||||
|
||||
<!-- Statistieken per vak -->
|
||||
<div class="vak-stats section" id="vakStats" style="display:none;">
|
||||
<h2>📚 Statistieken per vak</h2>
|
||||
<div id="vakStatsContent"></div>
|
||||
</div>
|
||||
|
||||
<!-- Tab navigatie -->
|
||||
@@ -289,40 +420,41 @@
|
||||
<button class="tab-btn" id="tab-vergelijk">⚖️ Klasvergelijking</button>
|
||||
</div>
|
||||
|
||||
<!-- Tab: Doelen (bestaande view) -->
|
||||
<!-- Tab: Doelen -->
|
||||
<div id="panel-doelen" class="section">
|
||||
|
||||
<!-- Legenda -->
|
||||
<div class="legend-container">
|
||||
<div class="legend-title">Legenda</div>
|
||||
<div class="legend-grid">
|
||||
<div class="legend-section">
|
||||
<div class="legend-item">
|
||||
<div class="status-selector status-groen" style="pointer-events:none;flex-shrink:0;"></div>
|
||||
<span>Doen we al</span>
|
||||
<!-- Legenda -->
|
||||
<div class="legend-container">
|
||||
<div class="legend-title">Legenda</div>
|
||||
<div class="legend-grid">
|
||||
<div class="legend-section">
|
||||
<div class="legend-item">
|
||||
<div class="status-selector status-groen" style="pointer-events:none;flex-shrink:0;"></div>
|
||||
<span>Doen we al</span>
|
||||
</div>
|
||||
<div class="legend-item">
|
||||
<div class="status-selector status-oranje" style="pointer-events:none;flex-shrink:0;"></div>
|
||||
<span>Doen we ongeveer</span>
|
||||
</div>
|
||||
<div class="legend-item">
|
||||
<div class="status-selector status-roze" style="pointer-events:none;flex-shrink:0;"></div>
|
||||
<span>Nieuw (doen we nog niet)</span>
|
||||
</div>
|
||||
<div class="legend-item">
|
||||
<div class="status-selector status-none" style="pointer-events:none;flex-shrink:0;"></div>
|
||||
<span>Niet beoordeeld</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="legend-item">
|
||||
<div class="status-selector status-oranje" style="pointer-events:none;flex-shrink:0;"></div>
|
||||
<span>Doen we ongeveer</span>
|
||||
<div class="legend-divider"></div>
|
||||
<div class="legend-section">
|
||||
<div class="legend-item"><div style="width:10px;height:10px;border-radius:50%;background:var(--status-groen);flex-shrink:0;"></div><span>Groen = consensus</span></div>
|
||||
<div class="legend-item"><div style="width:10px;height:10px;border-radius:50%;background:var(--status-oranje);flex-shrink:0;"></div><span>Oranje = gedeeltelijk</span></div>
|
||||
<div class="legend-item"><div style="width:10px;height:10px;border-radius:50%;background:var(--status-roze);flex-shrink:0;"></div><span>Roze = nog te doen</span></div>
|
||||
</div>
|
||||
<div class="legend-item">
|
||||
<div class="status-selector status-roze" style="pointer-events:none;flex-shrink:0;"></div>
|
||||
<span>Nieuw (doen we nog niet)</span>
|
||||
</div>
|
||||
<div class="legend-item">
|
||||
<div class="status-selector status-none" style="pointer-events:none;flex-shrink:0;"></div>
|
||||
<span>Niet beoordeeld</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="legend-divider"></div>
|
||||
<div class="legend-section">
|
||||
<div class="legend-item"><div style="width:10px;height:10px;border-radius:50%;background:var(--status-groen);flex-shrink:0;"></div><span>Groen = consensus</span></div>
|
||||
<div class="legend-item"><div style="width:10px;height:10px;border-radius:50%;background:var(--status-oranje);flex-shrink:0;"></div><span>Oranje = gedeeltelijk</span></div>
|
||||
<div class="legend-item"><div style="width:10px;height:10px;border-radius:50%;background:var(--status-roze);flex-shrink:0;"></div><span>Roze = nog te doen</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Filters -->
|
||||
<div class="filters-bar">
|
||||
<div class="filter-group">
|
||||
<label>Vak</label>
|
||||
@@ -346,9 +478,17 @@
|
||||
<label>Status</label>
|
||||
<select id="filterStatus">
|
||||
<option value="all">Alle statussen</option>
|
||||
<option value="consensus">✓ Iedereen groen</option>
|
||||
<option value="groen">✓ Groen (minstens 1)</option>
|
||||
<option value="oranje">~ Oranje (minstens 1)</option>
|
||||
<option value="roze">! Roze (minstens 1)</option>
|
||||
<option value="verschil">⚠ Verschillen</option>
|
||||
<option value="niemand">○ Niemand beoordeeld</option>
|
||||
<option value="niemand">○ Geen status</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
<label>Sectie</label>
|
||||
<select id="filterSectie">
|
||||
<option value="all">Alle secties</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
@@ -371,6 +511,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tabel -->
|
||||
<div class="table-scroll">
|
||||
<table>
|
||||
<thead id="tableHead">
|
||||
@@ -381,7 +522,15 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Legenda tabel onderaan -->
|
||||
<div class="legend" style="padding:1rem 1.5rem;border-top:1px solid var(--gray-200);display:flex;gap:1.5rem;flex-wrap:wrap;font-size:0.8rem;color:var(--gray-600);">
|
||||
<div class="legend-item"><div class="status-indicator groen">✓</div><span>Doen we al</span></div>
|
||||
<div class="legend-item"><div class="status-indicator oranje">~</div><span>Doen we ongeveer</span></div>
|
||||
<div class="legend-item"><div class="status-indicator roze">!</div><span>Nieuw</span></div>
|
||||
<div class="legend-item"><div class="status-indicator none">○</div><span>Niet beoordeeld</span></div>
|
||||
</div>
|
||||
|
||||
</div><!-- /panel-doelen -->
|
||||
|
||||
<!-- Tab: Klasoverzicht -->
|
||||
@@ -412,24 +561,22 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Empty state -->
|
||||
<div class="empty-state" id="emptyState" style="display:none;">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" width="80" height="80" style="opacity:0.4;margin-bottom:1rem;">
|
||||
<path d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
|
||||
</svg>
|
||||
<h3 style="font-size:1.1rem;margin-bottom:0.5rem;color:var(--gray-700);">Nog geen data beschikbaar</h3>
|
||||
<p>Nog geen leerkrachten hebben doelen beoordeeld.</p>
|
||||
</div>
|
||||
|
||||
<footer class="footer">💡 Gegevens worden automatisch geladen van de server.</footer>
|
||||
|
||||
</div><!-- /container -->
|
||||
|
||||
<!-- Modal: leerkracht toevoegen -->
|
||||
<div class="modal-overlay" id="addTeacherModal">
|
||||
<div class="modal">
|
||||
<h2>Leerkracht toevoegen</h2>
|
||||
<div class="form-group"><label>Voornaam</label><input type="text" id="newFirstName"></div>
|
||||
<div class="form-group"><label>Achternaam</label><input type="text" id="newLastName"></div>
|
||||
<div class="form-group"><label>E-mailadres</label><input type="email" id="newEmail"></div>
|
||||
<div class="form-group"><label>Tijdelijk wachtwoord</label><input type="password" id="newPassword"></div>
|
||||
<div id="addTeacherError" style="color:var(--danger);font-size:0.85rem;display:none;"></div>
|
||||
<div class="modal-buttons">
|
||||
<button id="btnCancelTeacher" class="btn btn-secondary">Annuleren</button>
|
||||
<button id="btnConfirmTeacher" class="btn btn-primary">Toevoegen</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="notification" id="notification"></div>
|
||||
|
||||
<script nonce="{{ csp_nonce() }}">
|
||||
function bind(id, ev, fn) {
|
||||
const el = document.getElementById(id);
|
||||
@@ -446,6 +593,8 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||
bind('jaarSelector', 'change', switchJaar);
|
||||
bind('btnVernieuw', 'click', loadOverview);
|
||||
bind('btnAddTeacher', 'click', openAddTeacher);
|
||||
bind('btnExportCSV', 'click', exportToCSV);
|
||||
bind('btnExportPDF', 'click', exportToPDF);
|
||||
bind('tab-doelen', 'click', () => switchTab('doelen'));
|
||||
bind('tab-klassen', 'click', () => switchTab('klassen'));
|
||||
document.getElementById('tab-vergelijk') && bind('tab-vergelijk', 'click', () => switchTab('vergelijk'));
|
||||
@@ -455,6 +604,7 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||
bind('filterTeacher', 'change', applyFilters);
|
||||
document.getElementById('filterKlas') && bind('filterKlas', 'change', applyFilters);
|
||||
bind('filterStatus', 'change', applyFilters);
|
||||
bind('filterSectie', 'change', applyFilters);
|
||||
bind('filterSearch', 'input', applyFilters);
|
||||
document.querySelectorAll('.leeftijd-checkbox input').forEach(cb => cb.addEventListener('change', applyFilters));
|
||||
await loadUser();
|
||||
@@ -514,15 +664,73 @@ function populateKlasFilter() {
|
||||
}
|
||||
|
||||
function renderTeacherList() {
|
||||
const el = document.getElementById('teacherList');
|
||||
if (!teachers.length) { el.innerHTML = '<em style="color:var(--gray-500)">Nog geen leerkrachten</em>'; return; }
|
||||
el.innerHTML = teachers.map(t => `
|
||||
<div class="teacher-chip">
|
||||
<span>${t.full_name}</span>
|
||||
<button data-action="removeTeacher" data-id="${t.id}"
|
||||
style="width:18px;height:18px;border-radius:50%;border:none;background:var(--gray-300);cursor:pointer;font-size:0.7rem;"
|
||||
title="Verwijderen">×</button>
|
||||
</div>`).join('');
|
||||
// Nieuwe chips in teacherChips (nieuwe layout)
|
||||
const chips = document.getElementById('teacherChips');
|
||||
if (chips) {
|
||||
chips.innerHTML = !teachers.length
|
||||
? '<em style="color:var(--gray-500)">Nog geen leerkrachten</em>'
|
||||
: teachers.map(t => `
|
||||
<div class="teacher-chip">
|
||||
<span class="name">${t.full_name}</span>
|
||||
<span class="klas">${(t.classes||[]).map(c=>c.name).join(', ') || ''}</span>
|
||||
<button data-action="removeTeacher" data-id="${t.id}"
|
||||
style="width:18px;height:18px;border-radius:50%;border:none;background:var(--gray-300);cursor:pointer;font-size:0.7rem;"
|
||||
title="Verwijderen">×</button>
|
||||
</div>`).join('');
|
||||
}
|
||||
// Toon/verberg de sectie
|
||||
const section = document.getElementById('teachersSection');
|
||||
if (section) section.style.display = teachers.length ? 'block' : 'none';
|
||||
}
|
||||
|
||||
function updateVakStats() {
|
||||
if (!overviewData) return;
|
||||
const container = document.getElementById('vakStatsContent');
|
||||
const vakStats = document.getElementById('vakStats');
|
||||
if (!container || !vakStats) return;
|
||||
|
||||
const stats = {};
|
||||
Object.values(overviewData.assessments_by_teacher || {}).forEach(tv => {
|
||||
Object.entries(tv).forEach(([vakId, goals]) => {
|
||||
if (!stats[vakId]) stats[vakId] = { groen:0, oranje:0, roze:0, total:0 };
|
||||
Object.values(goals).forEach(s => {
|
||||
stats[vakId].total++;
|
||||
if (s === 'groen') stats[vakId].groen++;
|
||||
if (s === 'oranje') stats[vakId].oranje++;
|
||||
if (s === 'roze') stats[vakId].roze++;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
const sorted = Object.entries(stats).sort((a,b) => vakNaam(a[0]).localeCompare(vakNaam(b[0]),'nl'));
|
||||
if (!sorted.length) { vakStats.style.display = 'none'; return; }
|
||||
|
||||
vakStats.style.display = 'block';
|
||||
container.innerHTML = sorted.map(([vakId, s]) => {
|
||||
const total = s.total;
|
||||
const gPct = total ? s.groen / total * 100 : 0;
|
||||
const oPct = total ? s.oranje / total * 100 : 0;
|
||||
const rPct = total ? s.roze / total * 100 : 0;
|
||||
return `
|
||||
<div class="vak-card">
|
||||
<div class="vak-card-header">
|
||||
<h3>${vakNaam(vakId)}</h3>
|
||||
<span class="percentage">${total} beoordelingen</span>
|
||||
</div>
|
||||
<div class="progress-bar">
|
||||
<div class="progress-bar-inner">
|
||||
<div class="progress-groen" style="width:${gPct}%"></div>
|
||||
<div class="progress-oranje" style="width:${oPct}%"></div>
|
||||
<div class="progress-roze" style="width:${rPct}%"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="vak-card-stats">
|
||||
<span><div class="dot dot-groen"></div> ${s.groen} groen</span>
|
||||
<span><div class="dot dot-oranje"></div> ${s.oranje} oranje</span>
|
||||
<span><div class="dot dot-roze"></div> ${s.roze} roze</span>
|
||||
</div>
|
||||
</div>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
async function loadOverview() {
|
||||
@@ -538,7 +746,10 @@ async function loadOverview() {
|
||||
// Laad vak data voor doelomschrijvingen
|
||||
await loadVakData();
|
||||
updateStats();
|
||||
updateVakStats();
|
||||
populateVakFilter();
|
||||
populateTeacherFilter();
|
||||
populateSectieFilter();
|
||||
applyFilters();
|
||||
}
|
||||
|
||||
@@ -624,13 +835,31 @@ function populateTeacherFilter() {
|
||||
});
|
||||
}
|
||||
|
||||
function populateSectieFilter() {
|
||||
const sel = document.getElementById('filterSectie');
|
||||
if (!sel) return;
|
||||
const cur = sel.value;
|
||||
const secties = new Set();
|
||||
Object.values(allGoals).forEach(goals => {
|
||||
goals.forEach(g => { if (g.sectie) secties.add(g.sectie); });
|
||||
});
|
||||
sel.innerHTML = '<option value="all">Alle secties</option>';
|
||||
[...secties].sort((a, b) => a.localeCompare(b, 'nl')).forEach(s => {
|
||||
const o = document.createElement('option');
|
||||
o.value = o.textContent = s;
|
||||
sel.appendChild(o);
|
||||
});
|
||||
sel.value = cur || 'all';
|
||||
}
|
||||
|
||||
function applyFilters() {
|
||||
if (!overviewData) return;
|
||||
const vakFilter = document.getElementById('filterVak').value;
|
||||
const teacherFilter = document.getElementById('filterTeacher').value;
|
||||
const statusFilter = document.getElementById('filterStatus').value;
|
||||
const search = document.getElementById('filterSearch').value.toLowerCase();
|
||||
const leeftijdFilter = [...document.querySelectorAll('.leeftijd-checkbox input:checked')].map(cb => cb.value);
|
||||
const statusFilter = document.getElementById('filterStatus').value;
|
||||
const sectieFilter = document.getElementById('filterSectie')?.value || 'all';
|
||||
const search = document.getElementById('filterSearch').value.toLowerCase();
|
||||
const leeftijdFilter = [...document.querySelectorAll('.leeftijd-checkbox input:checked')].map(cb => cb.value);
|
||||
|
||||
const shownTeachers = teacherFilter === 'all'
|
||||
? overviewData.teachers
|
||||
@@ -652,13 +881,16 @@ function applyFilters() {
|
||||
(allGoals[vakId] || []).forEach(goal => {
|
||||
if (search && !`${goal.goNr} ${goal.inhoud}`.toLowerCase().includes(search)) return;
|
||||
if (leeftijdFilter.length > 0 && !leeftijdFilter.some(l => (goal.leeftijden||[]).includes(l))) return;
|
||||
if (sectieFilter !== 'all' && goal.sectie !== sectieFilter) return;
|
||||
|
||||
const statussen = shownTeachers.map(t => {
|
||||
return overviewData.assessments_by_teacher[t.id]?.[vakId]?.[goal.id] || '';
|
||||
});
|
||||
|
||||
if (statusFilter === 'consensus' && !statussen.every(s => s === 'groen')) return;
|
||||
if (statusFilter === 'niemand' && statussen.some(s => s)) return;
|
||||
if (statusFilter === 'groen' && !statussen.some(s => s === 'groen')) return;
|
||||
if (statusFilter === 'oranje' && !statussen.some(s => s === 'oranje')) return;
|
||||
if (statusFilter === 'roze' && !statussen.some(s => s === 'roze')) return;
|
||||
if (statusFilter === 'niemand' && statussen.some(s => s)) return;
|
||||
if (statusFilter === 'verschil') {
|
||||
const filled = statussen.filter(s => s);
|
||||
if (filled.length <= 1 || new Set(filled).size <= 1) return;
|
||||
@@ -755,6 +987,130 @@ function vakNaam(id) {
|
||||
.replace(/\b\w/g, c => c.toUpperCase());
|
||||
}
|
||||
|
||||
function exportToCSV() {
|
||||
if (!overviewData) { showNotification('Nog geen data om te exporteren', 'warning'); return; }
|
||||
const shownVakken = Object.keys(allGoals);
|
||||
const shownTeachers = overviewData.teachers;
|
||||
|
||||
let csv = 'Vak,Doelnummer,Omschrijving,Leeftijden';
|
||||
shownTeachers.forEach(t => { csv += `,"${t.full_name}"`; });
|
||||
csv += ',Groen,Oranje,Roze\n';
|
||||
|
||||
shownVakken.forEach(vakId => {
|
||||
(allGoals[vakId] || []).forEach(goal => {
|
||||
const statussen = shownTeachers.map(t =>
|
||||
overviewData.assessments_by_teacher[t.id]?.[vakId]?.[goal.id] || '');
|
||||
const groen = statussen.filter(s => s === 'groen').length;
|
||||
const oranje = statussen.filter(s => s === 'oranje').length;
|
||||
const roze = statussen.filter(s => s === 'roze').length;
|
||||
const desc = (goal.inhoud || '').replace(/"/g, '""');
|
||||
csv += `"${vakNaam(vakId)}","${goal.goNr}","${desc}","${(goal.leeftijden||[]).join(', ')}"`;
|
||||
statussen.forEach(s => { csv += `,"${s || '-'}"`; });
|
||||
csv += `,${groen},${oranje},${roze}\n`;
|
||||
});
|
||||
});
|
||||
|
||||
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `Leerdoelen_Export_${new Date().toISOString().split('T')[0]}.csv`;
|
||||
document.body.appendChild(a); a.click();
|
||||
document.body.removeChild(a); URL.revokeObjectURL(url);
|
||||
showNotification('CSV geëxporteerd!', 'success');
|
||||
}
|
||||
|
||||
function exportToPDF() {
|
||||
if (!overviewData) { showNotification('Nog geen data om te exporteren', 'warning'); return; }
|
||||
const { jsPDF } = window.jspdf;
|
||||
const doc = new jsPDF('l', 'mm', 'a4');
|
||||
|
||||
const vakFilter = document.getElementById('filterVak').value;
|
||||
const teacherFilter = document.getElementById('filterTeacher').value;
|
||||
const statusFilter = document.getElementById('filterStatus').value;
|
||||
const sectieFilter = document.getElementById('filterSectie')?.value || 'all';
|
||||
const search = document.getElementById('filterSearch').value.toLowerCase();
|
||||
const leeftijdFilter = [...document.querySelectorAll('.leeftijd-checkbox input:checked')].map(cb => cb.value);
|
||||
|
||||
const shownVakken = vakFilter === 'all' ? Object.keys(allGoals) : [vakFilter];
|
||||
const shownTeachers = teacherFilter === 'all'
|
||||
? overviewData.teachers
|
||||
: overviewData.teachers.filter(t => t.id == teacherFilter);
|
||||
|
||||
// Titel
|
||||
doc.setFontSize(16);
|
||||
doc.text('Leerdoelen Overzicht — Directeur', 14, 15);
|
||||
doc.setFontSize(9);
|
||||
doc.setTextColor(100);
|
||||
doc.text(`Export: ${new Date().toLocaleDateString('nl-BE')}`, 14, 21);
|
||||
|
||||
// Headers
|
||||
const headers = ['Vak', 'Code', 'Omschrijving', 'Leeftijd',
|
||||
...shownTeachers.map(t => t.full_name), '✓', '~', '!'];
|
||||
|
||||
// Rijen
|
||||
const rows = [];
|
||||
shownVakken.forEach(vakId => {
|
||||
(allGoals[vakId] || []).forEach(goal => {
|
||||
if (search && !`${goal.goNr} ${goal.inhoud} ${goal.sectie||''}`.toLowerCase().includes(search)) return;
|
||||
if (leeftijdFilter.length && !leeftijdFilter.some(l => (goal.leeftijden||[]).includes(l))) return;
|
||||
if (sectieFilter !== 'all' && goal.sectie !== sectieFilter) return;
|
||||
|
||||
const statussen = shownTeachers.map(t =>
|
||||
overviewData.assessments_by_teacher[t.id]?.[vakId]?.[goal.id] || '');
|
||||
|
||||
if (statusFilter === 'groen' && !statussen.some(s => s === 'groen')) return;
|
||||
if (statusFilter === 'oranje' && !statussen.some(s => s === 'oranje')) return;
|
||||
if (statusFilter === 'roze' && !statussen.some(s => s === 'roze')) return;
|
||||
if (statusFilter === 'niemand' && statussen.some(s => s)) return;
|
||||
if (statusFilter === 'verschil') {
|
||||
const filled = statussen.filter(s => s);
|
||||
if (filled.length <= 1 || new Set(filled).size <= 1) return;
|
||||
}
|
||||
|
||||
const groen = statussen.filter(s => s === 'groen').length;
|
||||
const oranje = statussen.filter(s => s === 'oranje').length;
|
||||
const roze = statussen.filter(s => s === 'roze').length;
|
||||
|
||||
rows.push([
|
||||
vakNaam(vakId),
|
||||
goal.goNr,
|
||||
(goal.inhoud || '').substring(0, 70) + (goal.inhoud?.length > 70 ? '…' : ''),
|
||||
(goal.leeftijden || []).join(', '),
|
||||
...statussen.map(s => s === 'groen' ? '✓' : s === 'oranje' ? '~' : s === 'roze' ? '!' : '-'),
|
||||
groen, oranje, roze
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
doc.autoTable({
|
||||
head: [headers],
|
||||
body: rows,
|
||||
startY: 26,
|
||||
styles: { fontSize: 7, cellPadding: 2, overflow: 'linebreak' },
|
||||
headStyles: { fillColor: [79, 70, 229], textColor: 255, fontSize: 7 },
|
||||
columnStyles: {
|
||||
0: { cellWidth: 22 },
|
||||
1: { cellWidth: 14 },
|
||||
2: { cellWidth: 'auto' },
|
||||
3: { cellWidth: 18 },
|
||||
},
|
||||
margin: { left: 10, right: 10 },
|
||||
didParseCell: (data) => {
|
||||
// Kleur de status cellen
|
||||
if (data.section === 'body' && data.column.index >= 4) {
|
||||
const v = data.cell.raw;
|
||||
if (v === '✓') { data.cell.styles.textColor = [16, 185, 129]; data.cell.styles.fontStyle = 'bold'; }
|
||||
if (v === '~') { data.cell.styles.textColor = [245, 158, 11]; data.cell.styles.fontStyle = 'bold'; }
|
||||
if (v === '!') { data.cell.styles.textColor = [236, 72, 153]; data.cell.styles.fontStyle = 'bold'; }
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
doc.save(`Leerdoelen_Directeur_${new Date().toISOString().split('T')[0]}.pdf`);
|
||||
showNotification('PDF geëxporteerd!', 'success');
|
||||
}
|
||||
|
||||
function showNotification(msg, type='success') {
|
||||
const el = document.getElementById('notification');
|
||||
el.textContent = msg;
|
||||
|
||||
@@ -81,6 +81,21 @@
|
||||
.leeftijden { display: flex; flex-wrap: wrap; gap: 0.25rem; }
|
||||
.leeftijd-badge { font-size: 0.7rem; padding: 0.15rem 0.35rem; background: var(--gray-200); border-radius: 3px; color: var(--gray-600); }
|
||||
.beschrijving-cell { max-width: 400px; }
|
||||
/* Opmerkingen kolom */
|
||||
.opm-col { width: 200px; min-width: 150px; }
|
||||
.opm-cell { padding: 0.4rem 0.5rem; vertical-align: middle; }
|
||||
.opm-input {
|
||||
width: 100%; padding: 0.3rem 0.4rem;
|
||||
border: 1px solid var(--gray-300); border-radius: 4px;
|
||||
font-size: 0.8rem; background: transparent; color: var(--gray-700);
|
||||
transition: border-color 0.15s;
|
||||
}
|
||||
.opm-input:focus {
|
||||
outline: none; border-color: var(--primary); background: white;
|
||||
box-shadow: 0 0 0 2px rgba(79,70,229,0.1);
|
||||
}
|
||||
.opm-input::placeholder { color: var(--gray-400); }
|
||||
|
||||
.mia-container { background: var(--gray-50); border-radius: 6px; padding: 0.5rem; font-size: 0.8rem; margin-top: 0.5rem; }
|
||||
.mia-items { display: flex; flex-wrap: wrap; gap: 0.25rem; margin-top: 0.25rem; }
|
||||
.mia-item { background: white; padding: 0.2rem 0.4rem; border-radius: 3px; border: 1px solid var(--gray-200); }
|
||||
@@ -182,7 +197,12 @@
|
||||
.leeftijd-badge { background: #334155 !important; color: #94a3b8 !important; }
|
||||
.ebg-begrijpen { color: #1f2937 !important; }
|
||||
|
||||
/* MIA container */
|
||||
/* Opmerkingen input */
|
||||
.opm-input { background: transparent !important; color: #e2e8f0 !important; border-color: #334155 !important; }
|
||||
.opm-input:focus { background: #0f172a !important; border-color: #6366f1 !important; }
|
||||
.opm-input::placeholder { color: #475569 !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; }
|
||||
@@ -387,10 +407,11 @@
|
||||
<th>Leeftijden</th>
|
||||
<th>Sectie</th>
|
||||
<th>Beschrijving</th>
|
||||
<th class="opm-col">Opm.</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="tableBody">
|
||||
<tr><td colspan="6" class="empty-state">Selecteer een vak om te beginnen</td></tr>
|
||||
<tr><td colspan="7" class="empty-state">Selecteer een vak om te beginnen</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@@ -419,7 +440,8 @@
|
||||
let currentUser = null;
|
||||
let currentVakId = null;
|
||||
let vakData = {}; // cache van geladen vak JSON
|
||||
let assessments = {}; // { goal_id: status } voor huidig vak
|
||||
let assessments = {};
|
||||
let opmerkingen = {}; // { goal_id: 'tekst' } // { goal_id: status } voor huidig vak
|
||||
let doelzinnen = [];
|
||||
let filteredData = [];
|
||||
let saveTimeout = null;
|
||||
@@ -581,7 +603,11 @@ async function switchVak() {
|
||||
const res2 = await fetch(`/api/assessments?vak_id=${vakId}`);
|
||||
const data2 = await res2.json();
|
||||
assessments = {};
|
||||
data2.assessments.forEach(a => { assessments[a.goal_id] = a.status; });
|
||||
opmerkingen = {};
|
||||
data2.assessments.forEach(a => {
|
||||
assessments[a.goal_id] = a.status;
|
||||
if (a.opmerking) opmerkingen[a.goal_id] = a.opmerking;
|
||||
});
|
||||
|
||||
processVakData(vakId);
|
||||
populateSectieFilter();
|
||||
@@ -659,7 +685,7 @@ function populateSectieFilter() {
|
||||
function renderTable() {
|
||||
const tbody = document.getElementById('tableBody');
|
||||
if (!filteredData.length) {
|
||||
tbody.innerHTML = `<tr><td colspan="6" class="empty-state">${currentVakId ? 'Geen doelen gevonden' : 'Selecteer een vak'}</td></tr>`;
|
||||
tbody.innerHTML = `<tr><td colspan="7" class="empty-state">${currentVakId ? 'Geen doelen gevonden' : 'Selecteer een vak'}</td></tr>`;
|
||||
return;
|
||||
}
|
||||
tbody.innerHTML = filteredData.map(d => {
|
||||
@@ -676,6 +702,13 @@ function renderTable() {
|
||||
${d.inhoud}
|
||||
${renderMIA(d.mia)}
|
||||
</td>
|
||||
<td class="opm-cell">
|
||||
<input type="text" class="opm-input" maxlength="150"
|
||||
value="${escapeHtml(opmerkingen[d.id] || '')}"
|
||||
data-action="saveOpmerking" data-id="${d.id}"
|
||||
placeholder="..."
|
||||
title="${escapeHtml(opmerkingen[d.id] || '')}">
|
||||
</td>
|
||||
</tr>`;
|
||||
}).join('');
|
||||
}
|
||||
@@ -696,15 +729,21 @@ function renderMIA(items) {
|
||||
|
||||
function showLoading() {
|
||||
document.getElementById('tableBody').innerHTML =
|
||||
`<tr><td colspan="6" class="loading"><div class="spinner"></div>Laden...</td></tr>`;
|
||||
`<tr><td colspan="7" class="loading"><div class="spinner"></div>Laden...</td></tr>`;
|
||||
}
|
||||
|
||||
function renderEmptyState() {
|
||||
document.getElementById('tableBody').innerHTML =
|
||||
`<tr><td colspan="6" class="empty-state">Selecteer een vak om te beginnen</td></tr>`;
|
||||
`<tr><td colspan="7" class="empty-state">Selecteer een vak om te beginnen</td></tr>`;
|
||||
}
|
||||
|
||||
// ── Status ────────────────────────────────────────────────────────────────────
|
||||
function escapeHtml(str) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = str || '';
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
function cycleStatus(goalId) {
|
||||
const cycle = ['', 'groen', 'oranje', 'roze'];
|
||||
const cur = assessments[goalId] || '';
|
||||
@@ -784,6 +823,32 @@ document.addEventListener('click', function(e) {
|
||||
if (action === 'cycleStatus') { cycleStatus(btn.dataset.id); }
|
||||
});
|
||||
|
||||
document.addEventListener('change', function(e) {
|
||||
const inp = e.target.closest('[data-action="saveOpmerking"]');
|
||||
if (inp) saveOpmerking(inp.dataset.id, inp.value);
|
||||
});
|
||||
|
||||
|
||||
// ── Opmerking opslaan ─────────────────────────────────────────────────────────
|
||||
let opmTimer = null;
|
||||
function saveOpmerking(goalId, tekst) {
|
||||
if (!currentVakId) return;
|
||||
opmerkingen[goalId] = tekst.trim() || undefined;
|
||||
if (!opmerkingen[goalId]) delete opmerkingen[goalId];
|
||||
|
||||
// Debounce: wacht 600ms voor versturen
|
||||
clearTimeout(opmTimer);
|
||||
opmTimer = setTimeout(async () => {
|
||||
try {
|
||||
await fetch('/api/assessments/opmerking', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({ vak_id: currentVakId, goal_id: goalId, opmerking: tekst.trim() })
|
||||
});
|
||||
} catch(e) { console.error('Opmerking opslaan mislukt:', e); }
|
||||
}, 600);
|
||||
}
|
||||
|
||||
// ── Legacy JSON import (uit vorige standalone versie) ────────────────────────
|
||||
async function importLegacyJson(file) {
|
||||
if (!file) return;
|
||||
|
||||
@@ -128,12 +128,13 @@
|
||||
<div class="stat-card"><div class="stat-value" id="st-teachers">-</div><div class="stat-label">Leerkrachten</div></div>
|
||||
</div>
|
||||
|
||||
<!-- Scholengroep ICT accounts — alleen superadmin -->
|
||||
{% if is_superadmin %}
|
||||
<!-- Scholengroep ICT accounts — iedereen ziet lijst, enkel superadmin kan beheren -->
|
||||
<div class="section">
|
||||
<div class="section-header">
|
||||
<h2>👥 Scholengroep ICT medewerkers</h2>
|
||||
<button class="btn btn-primary btn-sm">+ Toevoegen</button>
|
||||
{% if is_superadmin %}
|
||||
<button class="btn btn-primary btn-sm" id="btnAddSgIct">+ Toevoegen</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
<p class="section-hint">
|
||||
Scholengroep ICT medewerkers kunnen alle scholen en gebruikers beheren,
|
||||
@@ -144,7 +145,6 @@
|
||||
<tbody id="sgIctTable"><tr class="empty-row"><td colspan="4">Laden...</td></tr></tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
|
||||
<!-- Schooljaren — globaal -->
|
||||
@@ -363,8 +363,7 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||
document.getElementById('btnSaveJaar') && bind('btnSaveJaar', 'click', addJaar);
|
||||
document.getElementById('btnCancelUser') && bind('btnCancelUser', 'click', closeModal);
|
||||
document.getElementById('btnSaveUser') && bind('btnSaveUser', 'click', addUser);
|
||||
const tasks = [loadStats(), loadSchoolsTable(), loadSchoolsGrid()];
|
||||
if (IS_SUPERADMIN) tasks.push(loadSgIct());
|
||||
const tasks = [loadStats(), loadSchoolsTable(), loadSchoolsGrid(), loadSgIct()];
|
||||
await Promise.all(tasks);
|
||||
await loadJaren();
|
||||
await loadAuditLog();
|
||||
@@ -396,7 +395,7 @@ async function loadSgIct() {
|
||||
<td>${u.full_name}</td>
|
||||
<td style="color:var(--gray-500);font-size:.82rem;">${u.email}</td>
|
||||
<td style="color:var(--gray-500);font-size:.8rem;">${u.last_login ? new Date(u.last_login).toLocaleDateString('nl-BE') : 'Nog niet ingelogd'}</td>
|
||||
<td><button class="btn btn-danger btn-sm" data-action="removeSgIct" data-id="${u.id}" data-name="${u.full_name.replace(/'/g,''')}">Verwijderen</button></td>
|
||||
<td>${IS_SUPERADMIN ? `<button class="btn btn-danger btn-sm" data-action="removeSgIct" data-id="${u.id}" data-name="${u.full_name.replace(/'/g,''')}">Verwijderen</button>` : ''}</td>
|
||||
</tr>`).join('');
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user