Add 'opmerking' column to assessments and implement related functionality
All checks were successful
Build & Push / Build & Push image (push) Successful in 38s
All checks were successful
Build & Push / Build & Push image (push) Successful in 38s
- Updated the database migration to include an 'opmerking' column in the assessments table. - Modified the Assessment model to include the new 'opmerking' field. - Enhanced the API to handle saving and retrieving remarks associated with assessments. - Updated the frontend to display and edit remarks in the assessments table.
This commit is contained in:
@@ -151,6 +151,7 @@ class Assessment(db.Model):
|
|||||||
vak_id = db.Column(db.String(50), nullable=False)
|
vak_id = db.Column(db.String(50), nullable=False)
|
||||||
goal_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)
|
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)
|
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||||
|
|
||||||
user = db.relationship('User')
|
user = db.relationship('User')
|
||||||
@@ -167,6 +168,7 @@ class Assessment(db.Model):
|
|||||||
'vak_id': self.vak_id,
|
'vak_id': self.vak_id,
|
||||||
'goal_id': self.goal_id,
|
'goal_id': self.goal_id,
|
||||||
'status': self.status,
|
'status': self.status,
|
||||||
|
'opmerking': self.opmerking,
|
||||||
'updated_at': self.updated_at.isoformat() if self.updated_at else None,
|
'updated_at': self.updated_at.isoformat() if self.updated_at else None,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -74,6 +74,7 @@ def save_assessment():
|
|||||||
vak_id = (data.get('vak_id') or '').strip()
|
vak_id = (data.get('vak_id') or '').strip()
|
||||||
goal_id = (data.get('goal_id') or '').strip()
|
goal_id = (data.get('goal_id') or '').strip()
|
||||||
status = (data.get('status') or '').strip()
|
status = (data.get('status') or '').strip()
|
||||||
|
opmerking = (data.get('opmerking') or '').strip()[:500]
|
||||||
|
|
||||||
if not vak_id or not goal_id:
|
if not vak_id or not goal_id:
|
||||||
return jsonify({'error': 'vak_id en goal_id zijn verplicht'}), 400
|
return jsonify({'error': 'vak_id en goal_id zijn verplicht'}), 400
|
||||||
@@ -104,6 +105,7 @@ def save_assessment():
|
|||||||
|
|
||||||
if assessment:
|
if assessment:
|
||||||
assessment.status = status
|
assessment.status = status
|
||||||
|
assessment.opmerking = opmerking or None
|
||||||
assessment.updated_at = datetime.utcnow()
|
assessment.updated_at = datetime.utcnow()
|
||||||
else:
|
else:
|
||||||
assessment = Assessment(
|
assessment = Assessment(
|
||||||
@@ -113,6 +115,7 @@ def save_assessment():
|
|||||||
vak_id=vak_id,
|
vak_id=vak_id,
|
||||||
goal_id=goal_id,
|
goal_id=goal_id,
|
||||||
status=status,
|
status=status,
|
||||||
|
opmerking=opmerking or None,
|
||||||
)
|
)
|
||||||
db.session.add(assessment)
|
db.session.add(assessment)
|
||||||
|
|
||||||
@@ -124,6 +127,55 @@ def save_assessment():
|
|||||||
return jsonify({'assessment': assessment.to_dict()})
|
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'])
|
@api_bp.route('/assessments/bulk-import', methods=['POST'])
|
||||||
@login_required
|
@login_required
|
||||||
@limiter.limit('5 per minute')
|
@limiter.limit('5 per minute')
|
||||||
|
|||||||
@@ -82,6 +82,36 @@
|
|||||||
/* Leerkrachten beheer */
|
/* Leerkrachten beheer */
|
||||||
.teacher-list { display: flex; flex-wrap: wrap; gap: 0.5rem; margin-bottom: 1rem; }
|
.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 { 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 { 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-overlay.active { display: flex; }
|
||||||
.modal { background: white; border-radius: 12px; padding: 1.5rem; max-width: 450px; width: 90%; }
|
.modal { background: white; border-radius: 12px; padding: 1.5rem; max-width: 450px; width: 90%; }
|
||||||
@@ -198,6 +228,14 @@
|
|||||||
/* Vak indicator */
|
/* Vak indicator */
|
||||||
.vak-indicator { /* gradient blijft, ziet er goed uit */ }
|
.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 bars achtergrond */
|
||||||
.progress-bar { background: #334155 !important; }
|
.progress-bar { background: #334155 !important; }
|
||||||
|
|
||||||
@@ -245,10 +283,12 @@
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
|
|
||||||
|
<!-- Header -->
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<div>
|
<div>
|
||||||
<h1>🏫 Directeur Dashboard</h1>
|
<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"></div>
|
<div style="opacity:0.85;font-size:0.85rem;margin-top:0.25rem;" id="schoolInfo">Schooloverzicht van alle leerdoelen en leerkrachten</div>
|
||||||
</div>
|
</div>
|
||||||
<div style="display:flex;gap:0.5rem;align-items:center;flex-wrap:wrap;">
|
<div style="display:flex;gap:0.5rem;align-items:center;flex-wrap:wrap;">
|
||||||
<div style="display:flex;flex-direction:column;gap:0.2rem;">
|
<div style="display:flex;flex-direction:column;gap:0.2rem;">
|
||||||
@@ -263,23 +303,48 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Stats -->
|
<!-- Schoolbrede statistieken -->
|
||||||
|
<div class="stats-overview" id="statsOverview">
|
||||||
|
<h2>📊 Schoolbrede statistieken</h2>
|
||||||
<div class="stats-grid" id="statsGrid">
|
<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 highlight"><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="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"><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-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-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>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Leerkrachten beheer -->
|
|
||||||
<div class="section">
|
|
||||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:1rem;">
|
|
||||||
<h2>👩🏫 Leerkrachten</h2>
|
|
||||||
<button id="btnAddTeacher" class="btn btn-primary">+ Leerkracht toevoegen</button>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="teacher-list" id="teacherList">Laden...</div>
|
|
||||||
|
<!-- 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>
|
||||||
|
|
||||||
|
<!-- Statistieken per vak -->
|
||||||
|
<div class="vak-stats section" id="vakStats" style="display:none;">
|
||||||
|
<h2>📚 Statistieken per vak</h2>
|
||||||
|
<div id="vakStatsContent"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Tab navigatie -->
|
<!-- Tab navigatie -->
|
||||||
@@ -289,7 +354,7 @@
|
|||||||
<button class="tab-btn" id="tab-vergelijk">⚖️ Klasvergelijking</button>
|
<button class="tab-btn" id="tab-vergelijk">⚖️ Klasvergelijking</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Tab: Doelen (bestaande view) -->
|
<!-- Tab: Doelen -->
|
||||||
<div id="panel-doelen" class="section">
|
<div id="panel-doelen" class="section">
|
||||||
|
|
||||||
<!-- Legenda -->
|
<!-- Legenda -->
|
||||||
@@ -323,6 +388,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Filters -->
|
||||||
<div class="filters-bar">
|
<div class="filters-bar">
|
||||||
<div class="filter-group">
|
<div class="filter-group">
|
||||||
<label>Vak</label>
|
<label>Vak</label>
|
||||||
@@ -346,9 +412,17 @@
|
|||||||
<label>Status</label>
|
<label>Status</label>
|
||||||
<select id="filterStatus">
|
<select id="filterStatus">
|
||||||
<option value="all">Alle statussen</option>
|
<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="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>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="filter-group">
|
<div class="filter-group">
|
||||||
@@ -371,6 +445,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Tabel -->
|
||||||
<div class="table-scroll">
|
<div class="table-scroll">
|
||||||
<table>
|
<table>
|
||||||
<thead id="tableHead">
|
<thead id="tableHead">
|
||||||
@@ -381,7 +456,15 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</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>
|
||||||
|
|
||||||
</div><!-- /panel-doelen -->
|
</div><!-- /panel-doelen -->
|
||||||
|
|
||||||
<!-- Tab: Klasoverzicht -->
|
<!-- Tab: Klasoverzicht -->
|
||||||
@@ -412,24 +495,22 @@
|
|||||||
</div>
|
</div>
|
||||||
</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 -->
|
<!-- Modal: leerkracht toevoegen -->
|
||||||
<div class="modal-overlay" id="addTeacherModal">
|
<div class="modal-overlay" id="addTeacherModal">
|
||||||
<div class="modal">
|
<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() }}">
|
<script nonce="{{ csp_nonce() }}">
|
||||||
function bind(id, ev, fn) {
|
function bind(id, ev, fn) {
|
||||||
const el = document.getElementById(id);
|
const el = document.getElementById(id);
|
||||||
@@ -446,6 +527,8 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|||||||
bind('jaarSelector', 'change', switchJaar);
|
bind('jaarSelector', 'change', switchJaar);
|
||||||
bind('btnVernieuw', 'click', loadOverview);
|
bind('btnVernieuw', 'click', loadOverview);
|
||||||
bind('btnAddTeacher', 'click', openAddTeacher);
|
bind('btnAddTeacher', 'click', openAddTeacher);
|
||||||
|
bind('btnExportCSV', 'click', exportToCSV);
|
||||||
|
bind('btnExportPDF', 'click', exportToPDF);
|
||||||
bind('tab-doelen', 'click', () => switchTab('doelen'));
|
bind('tab-doelen', 'click', () => switchTab('doelen'));
|
||||||
bind('tab-klassen', 'click', () => switchTab('klassen'));
|
bind('tab-klassen', 'click', () => switchTab('klassen'));
|
||||||
document.getElementById('tab-vergelijk') && bind('tab-vergelijk', 'click', () => switchTab('vergelijk'));
|
document.getElementById('tab-vergelijk') && bind('tab-vergelijk', 'click', () => switchTab('vergelijk'));
|
||||||
@@ -455,6 +538,7 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|||||||
bind('filterTeacher', 'change', applyFilters);
|
bind('filterTeacher', 'change', applyFilters);
|
||||||
document.getElementById('filterKlas') && bind('filterKlas', 'change', applyFilters);
|
document.getElementById('filterKlas') && bind('filterKlas', 'change', applyFilters);
|
||||||
bind('filterStatus', 'change', applyFilters);
|
bind('filterStatus', 'change', applyFilters);
|
||||||
|
bind('filterSectie', 'change', applyFilters);
|
||||||
bind('filterSearch', 'input', applyFilters);
|
bind('filterSearch', 'input', applyFilters);
|
||||||
document.querySelectorAll('.leeftijd-checkbox input').forEach(cb => cb.addEventListener('change', applyFilters));
|
document.querySelectorAll('.leeftijd-checkbox input').forEach(cb => cb.addEventListener('change', applyFilters));
|
||||||
await loadUser();
|
await loadUser();
|
||||||
@@ -514,15 +598,73 @@ function populateKlasFilter() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function renderTeacherList() {
|
function renderTeacherList() {
|
||||||
const el = document.getElementById('teacherList');
|
// Nieuwe chips in teacherChips (nieuwe layout)
|
||||||
if (!teachers.length) { el.innerHTML = '<em style="color:var(--gray-500)">Nog geen leerkrachten</em>'; return; }
|
const chips = document.getElementById('teacherChips');
|
||||||
el.innerHTML = teachers.map(t => `
|
if (chips) {
|
||||||
|
chips.innerHTML = !teachers.length
|
||||||
|
? '<em style="color:var(--gray-500)">Nog geen leerkrachten</em>'
|
||||||
|
: teachers.map(t => `
|
||||||
<div class="teacher-chip">
|
<div class="teacher-chip">
|
||||||
<span>${t.full_name}</span>
|
<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}"
|
<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;"
|
style="width:18px;height:18px;border-radius:50%;border:none;background:var(--gray-300);cursor:pointer;font-size:0.7rem;"
|
||||||
title="Verwijderen">×</button>
|
title="Verwijderen">×</button>
|
||||||
</div>`).join('');
|
</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() {
|
async function loadOverview() {
|
||||||
@@ -538,7 +680,10 @@ async function loadOverview() {
|
|||||||
// Laad vak data voor doelomschrijvingen
|
// Laad vak data voor doelomschrijvingen
|
||||||
await loadVakData();
|
await loadVakData();
|
||||||
updateStats();
|
updateStats();
|
||||||
|
updateVakStats();
|
||||||
populateVakFilter();
|
populateVakFilter();
|
||||||
|
populateTeacherFilter();
|
||||||
|
populateSectieFilter();
|
||||||
applyFilters();
|
applyFilters();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -624,11 +769,29 @@ 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() {
|
function applyFilters() {
|
||||||
if (!overviewData) return;
|
if (!overviewData) return;
|
||||||
const vakFilter = document.getElementById('filterVak').value;
|
const vakFilter = document.getElementById('filterVak').value;
|
||||||
const teacherFilter = document.getElementById('filterTeacher').value;
|
const teacherFilter = document.getElementById('filterTeacher').value;
|
||||||
const statusFilter = document.getElementById('filterStatus').value;
|
const statusFilter = document.getElementById('filterStatus').value;
|
||||||
|
const sectieFilter = document.getElementById('filterSectie')?.value || 'all';
|
||||||
const search = document.getElementById('filterSearch').value.toLowerCase();
|
const search = document.getElementById('filterSearch').value.toLowerCase();
|
||||||
const leeftijdFilter = [...document.querySelectorAll('.leeftijd-checkbox input:checked')].map(cb => cb.value);
|
const leeftijdFilter = [...document.querySelectorAll('.leeftijd-checkbox input:checked')].map(cb => cb.value);
|
||||||
|
|
||||||
@@ -652,12 +815,15 @@ function applyFilters() {
|
|||||||
(allGoals[vakId] || []).forEach(goal => {
|
(allGoals[vakId] || []).forEach(goal => {
|
||||||
if (search && !`${goal.goNr} ${goal.inhoud}`.toLowerCase().includes(search)) return;
|
if (search && !`${goal.goNr} ${goal.inhoud}`.toLowerCase().includes(search)) return;
|
||||||
if (leeftijdFilter.length > 0 && !leeftijdFilter.some(l => (goal.leeftijden||[]).includes(l))) 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 => {
|
const statussen = shownTeachers.map(t => {
|
||||||
return overviewData.assessments_by_teacher[t.id]?.[vakId]?.[goal.id] || '';
|
return overviewData.assessments_by_teacher[t.id]?.[vakId]?.[goal.id] || '';
|
||||||
});
|
});
|
||||||
|
|
||||||
if (statusFilter === 'consensus' && !statussen.every(s => s === 'groen')) 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 === 'niemand' && statussen.some(s => s)) return;
|
||||||
if (statusFilter === 'verschil') {
|
if (statusFilter === 'verschil') {
|
||||||
const filled = statussen.filter(s => s);
|
const filled = statussen.filter(s => s);
|
||||||
@@ -755,6 +921,44 @@ function vakNaam(id) {
|
|||||||
.replace(/\b\w/g, c => c.toUpperCase());
|
.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; }
|
||||||
|
showNotification('PDF export: installeer jsPDF of gebruik de CSV export.', 'warning');
|
||||||
|
}
|
||||||
|
|
||||||
function showNotification(msg, type='success') {
|
function showNotification(msg, type='success') {
|
||||||
const el = document.getElementById('notification');
|
const el = document.getElementById('notification');
|
||||||
el.textContent = msg;
|
el.textContent = msg;
|
||||||
|
|||||||
@@ -81,6 +81,21 @@
|
|||||||
.leeftijden { display: flex; flex-wrap: wrap; gap: 0.25rem; }
|
.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); }
|
.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; }
|
.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-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-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); }
|
.mia-item { background: white; padding: 0.2rem 0.4rem; border-radius: 3px; border: 1px solid var(--gray-200); }
|
||||||
@@ -182,6 +197,11 @@
|
|||||||
.leeftijd-badge { background: #334155 !important; color: #94a3b8 !important; }
|
.leeftijd-badge { background: #334155 !important; color: #94a3b8 !important; }
|
||||||
.ebg-begrijpen { color: #1f2937 !important; }
|
.ebg-begrijpen { color: #1f2937 !important; }
|
||||||
|
|
||||||
|
/* 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 */
|
||||||
.mia-container { background: #162032 !important; }
|
.mia-container { background: #162032 !important; }
|
||||||
.mia-item { background: #1e293b !important; border-color: #334155 !important; color: #e2e8f0 !important; }
|
.mia-item { background: #1e293b !important; border-color: #334155 !important; color: #e2e8f0 !important; }
|
||||||
@@ -387,10 +407,11 @@
|
|||||||
<th>Leeftijden</th>
|
<th>Leeftijden</th>
|
||||||
<th>Sectie</th>
|
<th>Sectie</th>
|
||||||
<th>Beschrijving</th>
|
<th>Beschrijving</th>
|
||||||
|
<th class="opm-col">Opm.</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody id="tableBody">
|
<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>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
@@ -419,7 +440,8 @@
|
|||||||
let currentUser = null;
|
let currentUser = null;
|
||||||
let currentVakId = null;
|
let currentVakId = null;
|
||||||
let vakData = {}; // cache van geladen vak JSON
|
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 doelzinnen = [];
|
||||||
let filteredData = [];
|
let filteredData = [];
|
||||||
let saveTimeout = null;
|
let saveTimeout = null;
|
||||||
@@ -581,7 +603,11 @@ async function switchVak() {
|
|||||||
const res2 = await fetch(`/api/assessments?vak_id=${vakId}`);
|
const res2 = await fetch(`/api/assessments?vak_id=${vakId}`);
|
||||||
const data2 = await res2.json();
|
const data2 = await res2.json();
|
||||||
assessments = {};
|
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);
|
processVakData(vakId);
|
||||||
populateSectieFilter();
|
populateSectieFilter();
|
||||||
@@ -659,7 +685,7 @@ function populateSectieFilter() {
|
|||||||
function renderTable() {
|
function renderTable() {
|
||||||
const tbody = document.getElementById('tableBody');
|
const tbody = document.getElementById('tableBody');
|
||||||
if (!filteredData.length) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
tbody.innerHTML = filteredData.map(d => {
|
tbody.innerHTML = filteredData.map(d => {
|
||||||
@@ -676,6 +702,13 @@ function renderTable() {
|
|||||||
${d.inhoud}
|
${d.inhoud}
|
||||||
${renderMIA(d.mia)}
|
${renderMIA(d.mia)}
|
||||||
</td>
|
</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>`;
|
</tr>`;
|
||||||
}).join('');
|
}).join('');
|
||||||
}
|
}
|
||||||
@@ -696,15 +729,21 @@ function renderMIA(items) {
|
|||||||
|
|
||||||
function showLoading() {
|
function showLoading() {
|
||||||
document.getElementById('tableBody').innerHTML =
|
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() {
|
function renderEmptyState() {
|
||||||
document.getElementById('tableBody').innerHTML =
|
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 ────────────────────────────────────────────────────────────────────
|
// ── Status ────────────────────────────────────────────────────────────────────
|
||||||
|
function escapeHtml(str) {
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.textContent = str || '';
|
||||||
|
return div.innerHTML;
|
||||||
|
}
|
||||||
|
|
||||||
function cycleStatus(goalId) {
|
function cycleStatus(goalId) {
|
||||||
const cycle = ['', 'groen', 'oranje', 'roze'];
|
const cycle = ['', 'groen', 'oranje', 'roze'];
|
||||||
const cur = assessments[goalId] || '';
|
const cur = assessments[goalId] || '';
|
||||||
@@ -784,6 +823,32 @@ document.addEventListener('click', function(e) {
|
|||||||
if (action === 'cycleStatus') { cycleStatus(btn.dataset.id); }
|
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) ────────────────────────
|
// ── Legacy JSON import (uit vorige standalone versie) ────────────────────────
|
||||||
async function importLegacyJson(file) {
|
async function importLegacyJson(file) {
|
||||||
if (!file) return;
|
if (!file) return;
|
||||||
|
|||||||
Reference in New Issue
Block a user