Compare commits
3 Commits
5f2e1fdb1b
...
add-google
| Author | SHA1 | Date | |
|---|---|---|---|
| 28c05edb0b | |||
| 85778855ca | |||
| 51c0755d67 |
@@ -95,7 +95,7 @@ def create_app():
|
|||||||
'style-src': ["'self'", "'unsafe-inline'"], # inline styles in templates (aanvaardbaar)
|
'style-src': ["'self'", "'unsafe-inline'"], # inline styles in templates (aanvaardbaar)
|
||||||
'img-src': ["'self'", 'data:'],
|
'img-src': ["'self'", 'data:'],
|
||||||
'font-src': ["'self'"],
|
'font-src': ["'self'"],
|
||||||
'connect-src': ["'self'"],
|
'connect-src': ["'self'", 'cdnjs.cloudflare.com'],
|
||||||
'form-action': ["'self'"], # voorkomt form hijacking
|
'form-action': ["'self'"], # voorkomt form hijacking
|
||||||
'base-uri': ["'self'"], # voorkomt base tag injection
|
'base-uri': ["'self'"], # voorkomt base tag injection
|
||||||
'frame-ancestors': ["'none'"], # clickjacking preventie
|
'frame-ancestors': ["'none'"], # clickjacking preventie
|
||||||
|
|||||||
@@ -265,11 +265,11 @@ def remove_user_from_school(school_id, user_id):
|
|||||||
return jsonify({'deleted': True})
|
return jsonify({'deleted': True})
|
||||||
|
|
||||||
|
|
||||||
# ── Scholengroep ICT beheer (superadmin) ──────────────────────────────────────
|
# ── Scholengroep ICT beheer ───────────────────────────────────────────────────
|
||||||
|
|
||||||
@admin_bp.route('/scholengroep-ict', methods=['GET'])
|
@admin_bp.route('/scholengroep-ict', methods=['GET'])
|
||||||
@login_required
|
@login_required
|
||||||
@superadmin_required
|
@scholengroep_ict_required # scholengroep_ict mag de lijst lezen; superadmin ook
|
||||||
def list_scholengroep_ict():
|
def list_scholengroep_ict():
|
||||||
users = User.query.filter_by(role='scholengroep_ict', is_active=True)\
|
users = User.query.filter_by(role='scholengroep_ict', is_active=True)\
|
||||||
.order_by(User.last_name).all()
|
.order_by(User.last_name).all()
|
||||||
|
|||||||
@@ -24,10 +24,40 @@
|
|||||||
.btn-primary { background: var(--primary); color: white; }
|
.btn-primary { background: var(--primary); color: white; }
|
||||||
.btn-primary:hover { background: var(--primary-dark); }
|
.btn-primary:hover { background: var(--primary-dark); }
|
||||||
.btn-secondary { background: var(--gray-200); color: var(--gray-700); }
|
.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; }
|
.stats-grid {
|
||||||
.stat-card { background: white; border-radius: 8px; padding: 1rem; text-align: center; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
|
display: grid;
|
||||||
.stat-value { font-size: 2rem; font-weight: 700; }
|
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
|
||||||
.stat-label { font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.05em; color: var(--gray-500); margin-top: 0.25rem; }
|
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 { 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; }
|
.section h2 { font-size: 1.1rem; color: var(--gray-700); margin-bottom: 1rem; }
|
||||||
|
|
||||||
@@ -193,6 +223,16 @@
|
|||||||
|
|
||||||
/* Stat cards */
|
/* Stat cards */
|
||||||
.stat-card { background: #1e293b !important; }
|
.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 */
|
||||||
.school-card-header { background: #162032 !important; border-color: #334155 !important; }
|
.school-card-header { background: #162032 !important; border-color: #334155 !important; }
|
||||||
@@ -280,6 +320,8 @@
|
|||||||
::-webkit-scrollbar-thumb:hover { background: #475569; }
|
::-webkit-scrollbar-thumb:hover { background: #475569; }
|
||||||
}
|
}
|
||||||
</style>
|
</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>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
@@ -307,12 +349,36 @@
|
|||||||
<div class="stats-overview" id="statsOverview">
|
<div class="stats-overview" id="statsOverview">
|
||||||
<h2>📊 Schoolbrede statistieken</h2>
|
<h2>📊 Schoolbrede statistieken</h2>
|
||||||
<div class="stats-grid" id="statsGrid">
|
<div class="stats-grid" id="statsGrid">
|
||||||
<div class="stat-card highlight"><div class="stat-value" id="statTeachers">-</div><div class="stat-label">Leerkrachten</div></div>
|
<div class="stat-card highlight">
|
||||||
<div class="stat-card"><div class="stat-value" id="statVakken">-</div><div class="stat-label">Vakken</div></div>
|
<div class="stat-icon">👩🏫</div>
|
||||||
<div class="stat-card"><div class="stat-value" id="statBeoordeeld">-</div><div class="stat-label">Beoordelingen</div></div>
|
<div class="stat-value" id="statTeachers">-</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-label">Leerkrachten</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>
|
||||||
<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 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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -956,7 +1022,93 @@ function exportToCSV() {
|
|||||||
|
|
||||||
function exportToPDF() {
|
function exportToPDF() {
|
||||||
if (!overviewData) { showNotification('Nog geen data om te exporteren', 'warning'); return; }
|
if (!overviewData) { showNotification('Nog geen data om te exporteren', 'warning'); return; }
|
||||||
showNotification('PDF export: installeer jsPDF of gebruik de CSV export.', 'warning');
|
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') {
|
function showNotification(msg, type='success') {
|
||||||
|
|||||||
@@ -128,12 +128,13 @@
|
|||||||
<div class="stat-card"><div class="stat-value" id="st-teachers">-</div><div class="stat-label">Leerkrachten</div></div>
|
<div class="stat-card"><div class="stat-value" id="st-teachers">-</div><div class="stat-label">Leerkrachten</div></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Scholengroep ICT accounts — alleen superadmin -->
|
<!-- Scholengroep ICT accounts — iedereen ziet lijst, enkel superadmin kan beheren -->
|
||||||
{% if is_superadmin %}
|
|
||||||
<div class="section">
|
<div class="section">
|
||||||
<div class="section-header">
|
<div class="section-header">
|
||||||
<h2>👥 Scholengroep ICT medewerkers</h2>
|
<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>
|
</div>
|
||||||
<p class="section-hint">
|
<p class="section-hint">
|
||||||
Scholengroep ICT medewerkers kunnen alle scholen en gebruikers beheren,
|
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>
|
<tbody id="sgIctTable"><tr class="empty-row"><td colspan="4">Laden...</td></tr></tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
|
|
||||||
<!-- Schooljaren — globaal -->
|
<!-- Schooljaren — globaal -->
|
||||||
@@ -363,8 +363,7 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|||||||
document.getElementById('btnSaveJaar') && bind('btnSaveJaar', 'click', addJaar);
|
document.getElementById('btnSaveJaar') && bind('btnSaveJaar', 'click', addJaar);
|
||||||
document.getElementById('btnCancelUser') && bind('btnCancelUser', 'click', closeModal);
|
document.getElementById('btnCancelUser') && bind('btnCancelUser', 'click', closeModal);
|
||||||
document.getElementById('btnSaveUser') && bind('btnSaveUser', 'click', addUser);
|
document.getElementById('btnSaveUser') && bind('btnSaveUser', 'click', addUser);
|
||||||
const tasks = [loadStats(), loadSchoolsTable(), loadSchoolsGrid()];
|
const tasks = [loadStats(), loadSchoolsTable(), loadSchoolsGrid(), loadSgIct()];
|
||||||
if (IS_SUPERADMIN) tasks.push(loadSgIct());
|
|
||||||
await Promise.all(tasks);
|
await Promise.all(tasks);
|
||||||
await loadJaren();
|
await loadJaren();
|
||||||
await loadAuditLog();
|
await loadAuditLog();
|
||||||
@@ -396,7 +395,7 @@ async function loadSgIct() {
|
|||||||
<td>${u.full_name}</td>
|
<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:.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 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('');
|
</tr>`).join('');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user