From 1cf27fbd52d55c2408091208694a5ef48a29e771 Mon Sep 17 00:00:00 2001 From: Alex Mueller Date: Tue, 15 Sep 2026 08:53:06 +0200 Subject: [PATCH] feat(dashboard): filter AP2 courses strictly by user account profession --- assets/app_auth.js | 35 +++++-- dashboard.html | 256 ++++++++++++++++++++++++++++++++++++--------- server.py | 18 +++- 3 files changed, 248 insertions(+), 61 deletions(-) diff --git a/assets/app_auth.js b/assets/app_auth.js index 60817a9..e9af1ed 100644 --- a/assets/app_auth.js +++ b/assets/app_auth.js @@ -114,12 +114,12 @@ } }, - async register(name, email, password, role = 'solo', rememberMe = false) { + async register(name, email, password, role = 'solo', rememberMe = false, profession = '') { try { const res = await fetch('/api/auth/register', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ name, email, password, role }) + body: JSON.stringify({ name, email, password, role, profession }) }); const data = await res.json().catch(() => ({})); if (!res.ok) { @@ -577,11 +577,26 @@ + +
@@ -618,6 +633,7 @@ const tabReg = document.getElementById('auth-tab-register'); const nameField = document.getElementById('auth-field-name'); const roleField = document.getElementById('auth-field-role'); + const profField = document.getElementById('auth-field-profession'); const submitBtn = document.getElementById('auth-submit-btn'); const errorBox = document.getElementById('auth-error-box'); const form = document.getElementById('auth-form'); @@ -634,8 +650,9 @@ tabLog.style.color = '#64748b'; tabLog.style.boxShadow = 'none'; - nameField.style.display = 'block'; - roleField.style.display = 'block'; + if (nameField) nameField.style.display = 'block'; + if (roleField) roleField.style.display = 'block'; + if (profField) profField.style.display = 'block'; submitBtn.innerText = 'Jetzt kostenlos registrieren →'; submitBtn.style.background = '#0284c7'; } else { @@ -648,8 +665,9 @@ tabReg.style.boxShadow = 'none'; } - nameField.style.display = 'none'; - roleField.style.display = 'none'; + if (nameField) nameField.style.display = 'none'; + if (roleField) roleField.style.display = 'none'; + if (profField) profField.style.display = 'none'; submitBtn.innerText = 'Anmelden →'; submitBtn.style.background = '#059669'; } @@ -678,6 +696,7 @@ const password = document.getElementById('auth-input-password').value; const name = document.getElementById('auth-input-name')?.value.trim() || email.split('@')[0]; const role = document.getElementById('auth-select-role')?.value || 'solo'; + const profession = document.getElementById('auth-select-profession')?.value || ''; const rememberMe = document.getElementById('auth-remember-me')?.checked || false; submitBtn.disabled = true; @@ -685,7 +704,7 @@ let result; if (isRegister) { - result = await window.AUTH.register(name, email, password, role, rememberMe); + result = await window.AUTH.register(name, email, password, role, rememberMe, profession); } else { result = await window.AUTH.login(email, password, rememberMe); } diff --git a/dashboard.html b/dashboard.html index 4118afb..ee13be9 100644 --- a/dashboard.html +++ b/dashboard.html @@ -257,6 +257,7 @@ - - - - + + + + +
-
+
🎓 @@ -395,20 +396,28 @@
-
- - - +
+
+ + + +
+ +
+ Lade Berufsstatus... +
@@ -1189,7 +1198,7 @@
-
+

📘 AP1: IT-Arbeitsplatz (LF 1–8)

Gemeinsame Kernqualifikationen für alle IT-Berufe: Verträge, Ergonomie, Subnetting, BSI, BPMN 2.0, Belege & Netzplan.

@@ -1197,7 +1206,7 @@
-
+

📕 AP2: Systemintegration (FISI)

VLAN & Routing, VRT-Symbole, Active Directory, RAID 0-10, Backup 3-2-1-1-0, Enterprise SQL & 40h-Projektleitfaden.

@@ -1205,7 +1214,7 @@
-
+

💻 AP2: Anwendungsentwicklung (FIAE)

Clean Architecture, GoF-Entwurfsmuster, UML 2.5, Algorithmen (O-Notation), OOP & SOLID, REST/GraphQL & 80h-Projektleitfaden.

@@ -1213,7 +1222,7 @@
-
+

📊 AP2: Daten- & Prozessanalyse (FIDP)

Prozessoptimierung, BPMN 2.0, ISO 25012 Datenqualität, ETL/CDC, Star-Schema, Statistik, ML-Evaluation & 40h-Projektleitfaden.

@@ -1221,7 +1230,7 @@
-
+

🏭 AP2: Digitale Vernetzung (FIDV)

Cyber-Physische Systeme, RAMI 4.0, IO-Link, Profinet IO, TSN, OPC UA/MQTT, 5G-Campus, IEC 62443, Purdue & 40h-Projekt.

@@ -1229,7 +1238,7 @@
-
+

⚡ AP2: IT-System-Elektroniker (ITSE)

VDE 0100/0701, DGUV V3, USV (VFI), LWL-Spleißen & OTDR, EMV, Brandschottung & 40h-Projektleitfaden.

@@ -1237,7 +1246,7 @@
-
+

💼 AP2: IT-System-Management (ITSM)

7P-Marketing, Handelskalkulation, TCO/ROI, EVB-IT Verträge, SLAs, ITIL & 40h-Projektleitfaden.

@@ -1245,7 +1254,7 @@
-
+

📈 AP2: Digitalisierungsmanagement (KDM)

Business Model Canvas, Plattformökonomie, BPMN 2.0, GoBD/DMS, XRechnung, DSFA & 40h-Projektleitfaden.

@@ -1253,13 +1262,25 @@
-
+

⚡ Subnetting-Drill Master

Interaktiver Rechentrainer: Netz-ID, Broadcast-Adresse, Hostbereich & Präfixlängen im Sekundentakt trainieren.

▶ Subnetting-Drill öffnen
+ + +
@@ -1481,18 +1502,83 @@ } catch (e) {} } + // Profession-Mapping: Welche AP2-Kurse gehören zu welchem Ausbildungsberuf? + const AP2_PROFESSION_MAP = { + 'ihk_ap2_fisi': 'FISI', + 'ihk_ap2_fiae': 'FIAE', + 'ihk_ap2_fidp': 'FIDP', + 'ihk_ap2_fidv': 'FIDV', + 'ihk_ap2_itse': 'ITSE', + 'ihk_ap2_itsm': 'ITSM', + 'ihk_ap2_kdm': 'KDM' + }; + + const PROFESSION_NAMES = { + 'FISI': 'Fachinformatiker Systemintegration (FISI)', + 'FIAE': 'Fachinformatiker Anwendungsentwicklung (FIAE)', + 'FIDP': 'Fachinformatiker Daten- & Prozessanalyse (FIDP)', + 'FIDV': 'Fachinformatiker Digitale Vernetzung (FIDV)', + 'ITSE': 'IT-System-Elektroniker/in (ITSE)', + 'ITSM': 'Kaufleute für IT-System-Management (ITSM)', + 'KDM': 'Kaufleute für Digitalisierungsmanagement (KDM)', + 'ALL': 'Alle 7 IT-Berufe (Dozent / Admin)' + }; + // Filter State let currentCategory = 'all'; let currentYear = 'all'; // 'all', 1, 2, 3, or 'assigned' - let currentProfession = 'FISI'; + let accountProfession = ''; // Im DB-Account hinterlegter Beruf + let currentProfession = ''; // Aktuelle Dropdown-Auswahl im Dashboard let userAssignedCourses = []; + // Helper: Berufs-Status-Badge im Azubi-Panel aktualisieren + function updateProfessionStatusBadge() { + const msgEl = document.getElementById('azubi-prof-account-msg'); + const badgeEl = document.getElementById('azubi-prof-account-badge'); + const navProf = document.getElementById('nav-auth-profession'); + if (!msgEl || !badgeEl) return; + + if (navProf) { + if (currentProfession && currentProfession !== 'ALL') { + navProf.innerText = currentProfession; + navProf.style.display = 'inline-block'; + } else if (currentProfession === 'ALL') { + navProf.innerText = 'ALLE BERUFE'; + navProf.style.display = 'inline-block'; + } else { + navProf.style.display = 'none'; + } + } + + if (!currentProfession) { + badgeEl.style.background = '#fef2f2'; + badgeEl.style.borderColor = '#fecaca'; + badgeEl.style.color = '#991b1b'; + msgEl.innerHTML = '⚠️ Kein Ausbildungsberuf im Account angegeben: Spezifische AP2-Prüfungskurse sind ausgeblendet. Wähle oben deinen Beruf und klicke auf Speichern.'; + } else if (currentProfession === 'ALL') { + badgeEl.style.background = '#eff6ff'; + badgeEl.style.borderColor = '#bfdbfe'; + badgeEl.style.color = '#1e40af'; + msgEl.innerHTML = '🌟 Dozenten- & Admin-Vollansicht: Alle 7 IT-Ausbildungsberufe sind auf dem Dashboard sichtbar.'; + } else if (currentProfession === accountProfession) { + badgeEl.style.background = '#f0fdf4'; + badgeEl.style.borderColor = '#bbf7d0'; + badgeEl.style.color = '#166534'; + msgEl.innerHTML = `✅ Im Account gespeichert: ${PROFESSION_NAMES[currentProfession] || currentProfession}. Es wird exklusiv dein passender AP2-Prüfungskurs angezeigt.`; + } else { + badgeEl.style.background = '#fffbeb'; + badgeEl.style.borderColor = '#fde68a'; + badgeEl.style.color = '#92400e'; + msgEl.innerHTML = `⏳ Vorschau für: ${PROFESSION_NAMES[currentProfession] || currentProfession} (noch nicht im Account gespeichert). Klicke auf 💾 Im Account speichern.`; + } + } + // Unified Card Filtering function applyAllFilters() { const cards = document.querySelectorAll('.portal-card'); const query = (document.getElementById('course-filter-input').value || '').toLowerCase().trim(); - const manifestProf = (window.IT_AUSBILDUNGEN && window.IT_AUSBILDUNGEN.getProfession) + const manifestProf = (window.IT_AUSBILDUNGEN && window.IT_AUSBILDUNGEN.getProfession && currentProfession && currentProfession !== 'ALL') ? window.IT_AUSBILDUNGEN.getProfession(currentProfession) : null; @@ -1502,19 +1588,34 @@ } let visibleCount = 0; + let counts = { all: 0, ihk: 0, devops: 0, coding: 0, data: 0 }; cards.forEach(card => { const cardCat = card.getAttribute('data-category'); const cardId = card.getAttribute('data-course-id'); const cardTitle = (card.getAttribute('data-title') || '').toLowerCase(); - // 1. Category check + // 1. Ausbildungsberuf-Prüfung: + // AP2 Kurse für Ausbildungsberufe sieht man NUR, wenn dieser Beruf im Account / Filter gewählt ist! + const requiredProf = AP2_PROFESSION_MAP[cardId]; + let matchesProfession = true; + if (requiredProf) { + if (currentProfession === 'ALL') { + matchesProfession = true; + } else if (currentProfession && currentProfession === requiredProf) { + matchesProfession = true; + } else { + matchesProfession = false; + } + } + + // 2. Category check const matchesCategory = (currentCategory === 'all' || cardCat === currentCategory); - // 2. Search query check + // 3. Search query check const matchesQuery = (!query || cardTitle.includes(query) || card.innerText.toLowerCase().includes(query)); - // 3. Year / Assigned check + // 4. Year / Assigned check let matchesYearOrAssigned = true; if (currentYear === 'assigned') { matchesYearOrAssigned = userAssignedCourses.includes(cardId); @@ -1522,13 +1623,54 @@ matchesYearOrAssigned = yearAllowedCourses.includes(cardId); } - if (matchesCategory && matchesQuery && matchesYearOrAssigned) { + // Zähler für Fachbereichs-Pills nur bei erlaubtem Beruf mitzählen + if (matchesProfession) { + counts.all++; + if (cardCat && counts[cardCat] !== undefined) { + counts[cardCat]++; + } + } + + if (matchesProfession && matchesCategory && matchesQuery && matchesYearOrAssigned) { card.style.display = 'flex'; visibleCount++; } else { card.style.display = 'none'; } }); + + // Filter auch auf den IHK AP1 & AP2 Schnellwahl-Bereich anwenden + const ihkCards = document.querySelectorAll('.ihk-quick-card'); + ihkCards.forEach(card => { + const prof = card.getAttribute('data-ihk-profession'); + if (!prof || prof === 'common') { + card.style.display = 'block'; + } else if (currentProfession === 'ALL') { + card.style.display = 'block'; + } else if (currentProfession && currentProfession === prof) { + card.style.display = 'block'; + } else { + card.style.display = 'none'; + } + }); + + const noProfHint = document.getElementById('ihk-no-prof-hint'); + if (noProfHint) { + noProfHint.style.display = (!currentProfession || currentProfession === '') ? 'block' : 'none'; + } + + // Tab Pills Zähler dynamisch aktualisieren + const pillAll = document.getElementById('tab-pill-all'); + const pillIhk = document.getElementById('tab-pill-ihk'); + const pillDevops = document.getElementById('tab-pill-devops'); + const pillCoding = document.getElementById('tab-pill-coding'); + const pillData = document.getElementById('tab-pill-data'); + + if (pillAll) pillAll.innerText = `🌟 Alle Fachbereiche (${counts.all})`; + if (pillIhk) pillIhk.innerText = `🎓 IHK Prüfung (${counts.ihk})`; + if (pillDevops) pillDevops.innerText = `🐧 DevOps, Linux & AD (${counts.devops})`; + if (pillCoding) pillCoding.innerText = `💻 Backend & Systems (${counts.coding})`; + if (pillData) pillData.innerText = `🗄️ Daten & Web (${counts.data})`; } // 3. Live Category Filter @@ -1562,6 +1704,7 @@ const select = document.getElementById('azubi-prof-select'); if (select) { currentProfession = select.value; + updateProfessionStatusBadge(); renderLernfelder(currentProfession, currentYear); applyAllFilters(); } @@ -1570,7 +1713,7 @@ // 3e. Save Apprenticeship Choice to Backend async function saveApprenticeshipChoice() { const select = document.getElementById('azubi-prof-select'); - const prof = select ? select.value : 'FISI'; + const prof = select ? select.value : ''; const yr = (typeof currentYear === 'number') ? currentYear : 1; const btn = document.getElementById('btn-save-apprenticeship'); if (btn) btn.innerText = 'Speichere...'; @@ -1587,11 +1730,14 @@ body: JSON.stringify({ profession: prof, training_year: yr }) }); if (res.ok) { + accountProfession = prof; + currentProfession = prof; + updateProfessionStatusBadge(); if (btn) { - btn.innerText = '✓ Gespeichert'; + btn.innerText = '✓ Im Account gespeichert!'; btn.style.background = '#10b981'; setTimeout(() => { - btn.innerText = '💾 Speichern'; + btn.innerText = '💾 Im Account speichern'; btn.style.background = '#0284c7'; }, 2000); } @@ -1601,15 +1747,20 @@ user.training_year = yr; localStorage.setItem('auth_user', JSON.stringify(user)); } + applyAllFilters(); } else { - if (btn) btn.innerText = 'Fehler'; + if (btn) btn.innerText = 'Fehler beim Speichern'; } } catch (e) { - if (btn) btn.innerText = 'Fehler'; + if (btn) btn.innerText = 'Fehler beim Speichern'; } } else { + accountProfession = prof; + currentProfession = prof; + updateProfessionStatusBadge(); if (btn) btn.innerText = '✓ Lokal gemerkt'; - setTimeout(() => { if (btn) btn.innerText = '💾 Speichern'; }, 2000); + setTimeout(() => { if (btn) btn.innerText = '💾 Im Account speichern'; }, 2000); + applyAllFilters(); } } @@ -1743,11 +1894,11 @@ roleEl.innerText = user.role === 'admin' ? 'Admin' : (user.role === 'teacher' ? 'Dozent' : 'Schüler'); } - if (user.profession) { - currentProfession = user.profession; - const profSel = document.getElementById('azubi-prof-select'); - if (profSel) profSel.value = user.profession; - } + accountProfession = user.profession || ''; + currentProfession = accountProfession || ((user.role === 'admin' || user.role === 'teacher') ? 'ALL' : ''); + const profSel = document.getElementById('azubi-prof-select'); + if (profSel) profSel.value = currentProfession; + updateProfessionStatusBadge(); if (user.training_year) { const yrPill = document.getElementById(`pill-year-${user.training_year}`); @@ -1776,6 +1927,12 @@ const mTl = document.getElementById('mobile-drawer-teacher-link'); if (mTl) mTl.style.display = 'flex'; } + } else { + accountProfession = ''; + currentProfession = ''; + const profSel = document.getElementById('azubi-prof-select'); + if (profSel) profSel.value = ''; + updateProfessionStatusBadge(); } // Gelöste Kapitel sammeln (localStorage + API) @@ -1827,10 +1984,12 @@ if (resMe.ok) { const meData = await resMe.json(); if (meData.user) { - if (meData.user.profession) { - currentProfession = meData.user.profession; + if (meData.user.profession !== undefined) { + accountProfession = meData.user.profession || ''; + currentProfession = accountProfession || ((user && (user.role === 'admin' || user.role === 'teacher')) ? 'ALL' : ''); const profSel = document.getElementById('azubi-prof-select'); if (profSel) profSel.value = currentProfession; + updateProfessionStatusBadge(); } if (meData.user.training_year && currentYear === 'all') { const yrPill = document.getElementById(`pill-year-${meData.user.training_year}`); @@ -1847,6 +2006,7 @@ if (countTxt) countTxt.innerText = `Dir wurden ${userAssignedCourses.length} Kurse von deinem Ausbilder zugeteilt.`; } } + applyAllFilters(); } } } catch (e) { diff --git a/server.py b/server.py index c77a7a0..7ab9465 100644 --- a/server.py +++ b/server.py @@ -568,7 +568,7 @@ class PlatformRequestHandler(http.server.SimpleHTTPRequestHandler): return self.send_json({ "classrooms": parsed_classes, "assigned_courses": list(all_assigned), - "profession": (u_row and u_row["profession"]) or (parsed_classes and parsed_classes[0].get("profession")) or "FISI", + "profession": (u_row["profession"] if (u_row and u_row["profession"] is not None) else "") or (parsed_classes and parsed_classes[0].get("profession")) or "", "training_year": (u_row and u_row["training_year"]) or (parsed_classes and parsed_classes[0].get("training_year")) or 1 }) @@ -761,19 +761,23 @@ class PlatformRequestHandler(http.server.SimpleHTTPRequestHandler): if role not in ("student", "teacher", "solo"): role = "solo" + profession = str(body.get("profession", "")).strip()[:10] + if profession not in ("FISI", "FIAE", "FIDP", "FIDV", "ITSE", "ITSM", "KDM", "ALL"): + profession = "" + pwd_hash, salt = hash_password(password) conn = get_db() try: c = conn.cursor() - c.execute("INSERT INTO users (email, password_hash, salt, name, role) VALUES (?, ?, ?, ?, ?)", - (email, pwd_hash, salt, name, role)) + c.execute("INSERT INTO users (email, password_hash, salt, name, role, profession) VALUES (?, ?, ?, ?, ?, ?)", + (email, pwd_hash, salt, name, role, profession)) user_id = c.lastrowid conn.commit() token = generate_token(user_id, email, role) return self.send_json({ "message": "Erfolgreich registriert!", "token": token, - "user": {"id": user_id, "email": email, "name": name, "role": role, "xp": 0, "level": 1} + "user": {"id": user_id, "email": email, "name": name, "role": role, "profession": profession, "xp": 0, "level": 1} }, 201) except sqlite3.IntegrityError: return self.send_json({"error": "Ein Benutzer mit dieser E-Mail existiert bereits"}, 409) @@ -815,6 +819,8 @@ class PlatformRequestHandler(http.server.SimpleHTTPRequestHandler): "email": user["email"], "name": user["name"], "role": user["role"], + "profession": user["profession"] if "profession" in user.keys() and user["profession"] else "", + "training_year": user["training_year"] if "training_year" in user.keys() and user["training_year"] else 1, "xp": user["xp"], "level": user["level"], "streak_days": user["streak_days"] @@ -982,7 +988,9 @@ class PlatformRequestHandler(http.server.SimpleHTTPRequestHandler): if not user_token: return self.send_json({"error": "Nicht authentifiziert"}, 401) - profession = str(body.get("profession", "FISI")).strip()[:10] + profession = str(body.get("profession", "")).strip()[:10] + if profession not in ("FISI", "FIAE", "FIDP", "FIDV", "ITSE", "ITSM", "KDM", "ALL", ""): + profession = "" try: training_year = int(body.get("training_year", 1)) if training_year not in (1, 2, 3):