Willkommen zurück

🧪 Prototyp — Rolle wählen
oder

Prototyp — kein echtes Login erforderlich

Dashboard
Schulverbände
4
aktive Mandanten
Lehrpersonen
87
systemweit
Aktive Pläne
3
Schuljahr 25/26
Offene Wünsche
12
ausstehend
Schulverbände
VerbandSchulhäuserLehrpersonenStatus
Daten-Backup
Alle Verbände, Schulhäuser, Klassen, LP und Fächer als JSON sichern/wiederherstellen
Schulverbände
VerbandSchulhäuserLehrpersonenStatusEinladungAktionen
Alle Benutzer
PersonVerbandRolleLoginStatus
CB
Claude Baumann
admin@authenticus.ch
authenticusAdminSSOAktiv
MH
M. Huber
m.huber@thun-nord.ch
Thun-NordSchulleitungMicrosoftAktiv
SR
S. Ritter
s.ritter@thun-nord.ch
Thun-NordLehrpersonE-MailAktiv
BK
B. Keller
b.keller@steffisburg.ch
SteffisburgLehrpersonGoogleInaktiv
Lehrpersonen
24
in deinem Verband
Pensen erfasst
18
von 24
Wünsche offen
6
noch nicht zugeteilt
Plan-Status
Entwurf
Schuljahr 25/26
Pensumsübersicht
LehrpersonStufeWunschZugeteiltAuslastungStatus
MH
M. Huber
Klasse 3a
Mittelstufe22 Std.22 Std.
92%
OK
SR
S. Ritter
Klasse 5b
Oberstufe24 Std.20 Std.
83%
Offen
BK
B. Keller
Klasse 2c
Unterstufe24 Std.24 Std.
100%
OK
AT
A. Tanner
Klasse 4a
Mittelstufe20 Std.18 Std.
75%
Offen
Lehrpersonen verwalten
LehrpersonE-MailModusPensumStv. bis
MH
M. Huber
m.huber@thun-nord.ch22/28
SR
S. Ritter
s.ritter@thun-nord.ch📋 Ohne Login20/28
BK
B. Keller
b.keller@thun-nord.ch24/2831.07.2026
AT
A. Tanner
a.tanner@thun-nord.ch📋 Ohne Login18/28
Mein Pensum
20 Std.
von 24 zugeteilt
Mein Wunsch
22 Std.
eingereicht
Klasse
5b
Oberstufe
Status
Offen
Plan in Bearbeitung
Meine Wünsche & Verfügbarkeiten
Gewünschtes Pensum
Eingereicht am 28.05.2026
22 Std./Wo.
Bevorzugte Stufe
Oberstufe (5.–6. Klasse)
Oberstufe
Freier Tag
Gewünschter freier Wochentag
Mittwoch

Wünsche & Einschränkungen

Pensum-Wunsch, bevorzugte Stufen und Fächer

Pensum & Modus
Verfügbarkeit nach Halbtag ✅ Verfügbar  🚫 Nicht verfügbar  ⚠️ Falls nötig
Tag 🌅 Vormittag  (Startzeit – Endzeit) 🌆 Nachmittag  (Startzeit – Endzeit)
Schulstufen & Fächer ✅ Gewünscht  ❌ Ausgeschlossen
Lädt…

Schulstufen

Standard-Schulstufen und deren Fächer-Vorlagen verwalten

Stufen 0
Lädt…
Fächer-Defaults
← Stufe auswählen

Stundenplan

Beginn 08:00 | 45 Min.
Klick auf «generieren» erstellt bis zu 3 optimierte Varianten aus den LP-Wünschen.

Klasse auswählen um den Stundenplan anzuzeigen

Planungsregeln

Steuere welche Regeln bei der Stundenplangenerierung berücksichtigt werden

'); w.document.close(); } function printSchedulerVariante() { const today = new Date().toISOString().slice(0,10).replace(/-/g,''); const w = window.open('', '_blank'); const gridHTML = document.getElementById('scheduler-grid-output').innerHTML; const legendeHTML = document.getElementById('sp-legende')?.innerHTML || ''; w.document.write( '' + 'Stundenplan_' + today + '' + '' + '' + legendeHTML + gridHTML + '' ); w.document.close(); w.onload = function(){ w.document.title = 'Stundenplan_' + today; w.print(); }; } async function importAlleData() { const input = document.createElement('input'); input.type = 'file'; input.accept = '.json,application/json'; input.onchange = async (evt) => { const file = evt.target.files[0]; if (!file) return; const btn = document.getElementById('btn-import'); try { const text = await file.text(); const obj = JSON.parse(text); if (!obj.version || obj.app !== 'PensUnikum') throw new Error('Ungültige PensUnikum-Export-Datei'); const daten = obj.daten || {}; const tabellen = ['verbaende','schulhaeuser','schulklassen','benutzer','klassen_faecher','lp_wuensche','schulstufen']; const totalRows = tabellen.reduce((s,t) => s + (daten[t]?.length||0), 0); if (!confirm('Import von "' + file.name + '"?\n\n' + totalRows + ' Datensätze werden importiert (Upsert).\nFortfahren?')) return; if (btn) { btn.disabled = true; btn.textContent = '⏳ Importiere…'; } let total = 0; for (const tbl of tabellen) { const rows = daten[tbl]; if (!rows || !rows.length) continue; const { error } = await db.from(tbl).upsert(rows, { onConflict: 'id' }); if (error) throw new Error(tbl + ': ' + error.message); total += rows.length; } showToast('✅ Import: ' + total + ' Datensätze importiert'); setTimeout(() => location.reload(), 1500); } catch(e) { showToast('❌ Import fehlgeschlagen: ' + e.message); console.error('Import:', e); } finally { if (btn) { btn.disabled = false; btn.textContent = '📤 Daten importieren (JSON)'; } } }; input.click(); } function renderStundenplan() { console.log('[SP] faecher sample:', _sp_faecher[0]); if (!_sp_faecher.length) { const ansicht = document.getElementById('sp-ansicht')?.value || 'klasse'; const lpId = document.getElementById('sp-lp-select')?.value; const emptyMsg = ansicht === 'lp' ? (lpId ? 'Für diese Lehrperson sind noch keine Fächer erfasst.' : 'Bitte eine Lehrperson auswählen.') : 'Für diese Klasse sind noch keine Fächer erfasst.'; document.getElementById('sp-grid-wrap').innerHTML = '

' + emptyMsg + '

'; document.getElementById('sp-legende').innerHTML = ''; return; } // Farben zuweisen const farbMap = {}; _sp_faecher.forEach((f,i) => { farbMap[f.id] = SP_FARBEN[i % SP_FARBEN.length]; }); // Legende const legende = document.getElementById('sp-legende'); const spAnsicht = document.getElementById('sp-ansicht')?.value || 'klasse'; const lpSel = document.getElementById('sp-lp-select'); const lpName = spAnsicht === 'lp' ? (lpSel?.options[lpSel.selectedIndex]?.text || '') : ''; const lpTitle = (lpName && lpName !== '— Lehrperson wählen —') ? '
👤 ' + lpName + '
' : ''; legende.innerHTML = lpTitle + _sp_faecher.map(f => { const lp = f.benutzer ? f.benutzer.vorname+' '+f.benutzer.nachname : '—'; const meta = spAnsicht === 'lp' ? (f.schulklasse_bezeichnung || '—') : lp; return '
' + '' + ''+f.fach_name+'' + ''+meta+'' + ''+f.lektionen_pro_woche+'L/W' + '
'; }).join(''); // Max Lektionen pro Tag berechnen const maxProTag = Math.max(6, Math.ceil(_sp_faecher.reduce((s,f)=>s+(f.lektionen_pro_woche||0),0) / 5)); const LEKTIONEN = Math.min(maxProTag + 1, 10); const TAGE = 5; // Zellen-Matrix aufbauen [tag][lektion] = null | {fach, doppel?, skip?} const zellen = Array.from({length:TAGE}, () => Array(LEKTIONEN+1).fill(null)); // Fächer verteilen: Round-Robin über Tage, Doppellektionen zuerst _sp_faecher.forEach(fach => { let restDoppel = fach.doppellektionen_pro_woche || 0; let restEinzel = (fach.lektionen_pro_woche || 0) - restDoppel * 2; // Doppellektionen platzieren for (let t = 0; t < TAGE && restDoppel > 0; t++) { for (let l = 1; l < LEKTIONEN && restDoppel > 0; l++) { if (!zellen[t][l] && !zellen[t][l+1]) { zellen[t][l] = { fach, doppel: true }; zellen[t][l+1] = { fach, skip: true }; restDoppel--; break; } } } // Einzellektionen gleichmässig auf Tage verteilen (Round-Robin) let startTag = 0; while (restEinzel > 0) { let placed = false; for (let t = startTag; t < TAGE && restEinzel > 0; t++) { for (let l = 1; l <= LEKTIONEN && restEinzel > 0; l++) { if (!zellen[t][l]) { zellen[t][l] = { fach }; restEinzel--; startTag = (t + 1) % TAGE; placed = true; break; } } } if (!placed) break; // Kein Platz mehr } }); // Tabelle rendern const rowH = 52; let grid = ''; grid += '' + SP_TAGE.map(()=>'').join('') + ''; grid += ''; grid += ''; SP_TAGE.forEach(t => { grid += ''; }); grid += ''; for (let l = 1; l <= LEKTIONEN; l++) { const startEl = document.getElementById('sp-startzeit'); const lektEl = document.getElementById('sp-lektlaenge'); const startMins = startEl ? (function(){ const [h,m]=(startEl.value||'08:00').split(':'); return (+h)*60+(+m); })() : 480; const lektMin = lektEl ? (+lektEl.value || 45) : 45; const pauseMin = 5; function lektTime(n){ const tot=startMins+(n-1)*(lektMin+pauseMin); const h=Math.floor(tot/60),m=tot%60; return String(h).padStart(2,'0')+':'+String(m).padStart(2,'0'); } function lektTimeEnd(n){ const tot=startMins+(n-1)*(lektMin+pauseMin)+lektMin; const h=Math.floor(tot/60),m=tot%60; return String(h).padStart(2,'0')+':'+String(m).padStart(2,'0'); } // Pausen als breiter Balken über alle Tage (rowspan-aware) if (l > 1) { const vid2 = window._sp_current_vid || editTargetId || window._active_verband_id || window._last_verband_id; const activePausen = (typeof _sp_pausen_map !== 'undefined' && _sp_pausen_map[vid2]) ? _sp_pausen_map[vid2] : ((typeof VERBAENDE !== 'undefined' && VERBAENDE.find(x=>x.id===vid2)?.pausen) || []); const t2m = t => { const[h,m]=(t||'00:00').split(':'); return (+h)*60+(+m); }; const endLPrev = startMins + (l-2)*(lektMin+pauseMin) + lektMin; const endLCur = startMins + (l-1)*(lektMin+pauseMin) + lektMin; const pHere = activePausen .filter(p => t2m(p.von) > endLPrev && t2m(p.von) <= endLCur) .sort((a,b) => t2m(a.von) - t2m(b.von)); pHere.forEach(p => { const pDauer = t2m(p.bis) - t2m(p.von); // Count how many tag-columns are occupied by a doppel rowspan at this point // A doppel at tag t starts at row (l-1) with rowspan=2, so it occupies row l too let freeStart = 0; // index of first free column after occupied ones let occupiedCols = 0; for (let tt = 0; tt < TAGE; tt++) { if (zellen[tt][l-1] && zellen[tt][l-1].doppel) occupiedCols++; } grid += ''; grid += ''; // For each tag: if previous row had doppel → skip (rowspan covers it), else draw pause let freeCols = []; for (let tt = 0; tt < TAGE; tt++) { if (!(zellen[tt][l-1] && zellen[tt][l-1].doppel)) freeCols.push(tt); } if (freeCols.length > 0) { // Group consecutive free columns into colspan segments with occupied gaps let tt = 0; while (tt < TAGE) { if (zellen[tt][l-1] && zellen[tt][l-1].doppel) { tt++; // skip — covered by rowspan } else { // count consecutive free cols let span = 0; while (tt + span < TAGE && !(zellen[tt+span][l-1] && zellen[tt+span][l-1].doppel)) span++; grid += ''; tt += span; } } } grid += ''; }); } grid += ''; grid += ''; for (let t = 0; t < TAGE; t++) { const z = zellen[t][l]; if (z && z.skip) continue; // For doppel cells: rowspan must include any pause rows between l and l+1 let doppelRowspan = 2; if (z && z.doppel) { const vid2r = window._sp_current_vid || editTargetId || window._active_verband_id || window._last_verband_id; const apR = (typeof _sp_pausen_map !== 'undefined' && _sp_pausen_map[vid2r]) ? _sp_pausen_map[vid2r] : ((typeof VERBAENDE !== 'undefined' && VERBAENDE.find(x=>x.id===vid2r)?.pausen) || []); const t2mR = t => { const[h,m]=(t||'00:00').split(':'); return (+h)*60+(+m); }; const endL = startMins + (l-1)*(lektMin+pauseMin) + lektMin; const endL1 = startMins + l*(lektMin+pauseMin) + lektMin; const pausesBetween = apR.filter(p => t2mR(p.von) > endL && t2mR(p.von) <= endL1).length; doppelRowspan = 2 + pausesBetween; } const rowspan = (z && z.doppel) ? ' rowspan="'+doppelRowspan+'"' : ''; const ht = (z && z.doppel) ? rowH*2+1 : rowH; if (z && z.fach) { const f = z.fach; // lp wird in new cell block direkt gehandelt const farbe = farbMap[f.id]; const showRaum = document.getElementById('sp-show-raum')?.checked; const lpName = f.benutzer ? ((f.benutzer.vorname || '') + ' ' + (f.benutzer.nachname || '')).trim() : 'keine Lehrperson'; const lpColor = f.benutzer ? 'var(--color-text-muted)' : 'var(--color-text-faint)'; const schulhausName = (f.schulhaus && f.schulhaus.name) ? f.schulhaus.name : 'kein Schulhaus'; const schulhausColor = (f.schulhaus && f.schulhaus.name) ? 'var(--color-text-muted)' : 'var(--color-text-faint)'; grid += '' + '
'+f.fach_name+'
' + '
' + '' + ''+lpName+'' + '
' + (showRaum ? '
' + '' + ''+schulhausName+'' + '
' : '') + ''; } else { grid += '
'; } } grid += ''; } grid += '
Std.'+t+'
' + '
'+p.von+'
' + '
'+p.bis+'
' + '
' + '
' + '' + ''+p.bezeichnung+'' + ''+pDauer+' Min.' + '
' + '
' +'
'+l+'
' +'
'+lektTime(l)+'
' +(()=>{ // For doppel cells in this row: show end of l+1 + pause durations between l and l+1 const hasDoppelInRow = Array.from({length:TAGE}, (_,tt)=>zellen[tt][l]?.doppel).some(Boolean); if (hasDoppelInRow) { const vid2r = window._sp_current_vid || editTargetId || window._active_verband_id || window._last_verband_id; const apR = (typeof _sp_pausen_map !== 'undefined' && _sp_pausen_map[vid2r]) ? _sp_pausen_map[vid2r] : ((typeof VERBAENDE !== 'undefined' && VERBAENDE.find(x=>x.id===vid2r)?.pausen) || []); const t2mR = t => { const[h,m]=(t||'00:00').split(':'); return (+h)*60+(+m); }; const endL = startMins + (l-1)*(lektMin+pauseMin) + lektMin; const endL1 = startMins + l*(lektMin+pauseMin) + lektMin; const extraMin = apR .filter(p => t2mR(p.von) > endL && t2mR(p.von) <= endL1) .reduce((sum,p) => sum + (t2mR(p.bis) - t2mR(p.von)), 0); const rawEnd = endL1 + extraMin; const h=Math.floor(rawEnd/60), m=rawEnd%60; const endStr = String(h).padStart(2,'0')+':'+String(m).padStart(2,'0'); return '
'+endStr+'
'; } return '
'+lektTimeEnd(l)+'
'; })() +'
'; document.getElementById('sp-grid-wrap').innerHTML = grid; } // ═══════════════════════════════════════════════════ // PLANUNGSREGELN ENGINE // ═══════════════════════════════════════════════════ const STANDARD_REGELN = [ {code:'R01',kat:'hard',name:'LP-Doppelbelegung verhindern',desc:'Eine LP kann nicht gleichzeitig zwei Klassen unterrichten.',aktiv:true,sperrbar:false,gewicht:null,params:{}}, {code:'R02',kat:'hard',name:'Klassen-Doppelbelegung verhindern',desc:'Eine Klasse kann nicht gleichzeitig zwei Fächer haben.',aktiv:true,sperrbar:false,gewicht:null,params:{}}, {code:'R03',kat:'hard',name:'Zeitraster einhalten',desc:'Alle Lektionen müssen im definierten Stundenraster liegen.',aktiv:true,sperrbar:false,gewicht:null,params:{}}, {code:'R04',kat:'hard',name:'Keine Lektionen in Pausen',desc:'Pausen sind reserviert.',aktiv:true,sperrbar:false,gewicht:null,params:{}}, {code:'R05',kat:'hard',name:'Doppellektion ohne Unterbruch',desc:'Die zwei Slots einer Doppellektion müssen direkt aufeinanderfolgen.',aktiv:true,sperrbar:false,gewicht:null,params:{}}, {code:'R06',kat:'hard',name:'Alle Pflichtlektionen platzieren',desc:'Jede Lektion aus der Lektionentafel muss im Plan erscheinen.',aktiv:true,sperrbar:false,gewicht:null,params:{}}, {code:'R10',kat:'hard',name:'Gleiches Fach nicht direkt hintereinander',desc:'Zwei Lektionen desselben Fachs für dieselbe Klasse dürfen nicht aufeinanderfolgen.',aktiv:true,sperrbar:true,gewicht:null,params:{}}, {code:'R11',kat:'hard',name:'Sport nicht direkt nach Mittagessen',desc:'Nach der Mittagspause mind. 1 Slot Abstand vor Sportunterricht.',aktiv:true,sperrbar:true,gewicht:null,params:{min_abstand_slots:1}}, {code:'R12',kat:'hard',name:'LP nur an verfügbaren Tagen',desc:'Teilzeit-LP werden nur an ihren Vertragstagen eingeplant.',aktiv:true,sperrbar:true,gewicht:null,params:{}}, {code:'R13',kat:'soft',name:'Fächer über Woche verteilen',desc:'Jedes Fach soll auf möglichst viele verschiedene Tage verteilt sein.',aktiv:true,sperrbar:true,gewicht:8,params:{}}, {code:'R14',kat:'soft',name:'Max. 1× gleiches Fach pro Tag',desc:'Dasselbe Fach möglichst nicht zweimal am selben Tag.',aktiv:true,sperrbar:true,gewicht:6,params:{max_pro_tag:1}}, {code:'R15',kat:'soft',name:'Kerntächer auf alle 5 Tage',desc:'Deutsch und Mathematik sollen täglich vertreten sein.',aktiv:true,sperrbar:true,gewicht:7,params:{}}, {code:'R17',kat:'soft',name:'Keine LP-Lücken im Tagesplan',desc:'LP sollen keine freien Slots zwischen zwei Lektionen haben.',aktiv:true,sperrbar:true,gewicht:6,params:{}}, {code:'R19',kat:'soft',name:'Ausgeglichene LP-Tagesbelastung',desc:'Lektionen gleichmässig über den Tag verteilen.',aktiv:true,sperrbar:true,gewicht:5,params:{}}, {code:'R20',kat:'soft',name:'Max. 3 Klassen pro LP pro Tag',desc:'Eine LP soll nicht mehr als 3 Klassen an einem Tag unterrichten.',aktiv:true,sperrbar:true,gewicht:4,params:{max_klassen_pro_tag:3}}, {code:'R21',kat:'soft',name:'Max. 2 Doppellektionen pro Klasse/Tag',desc:'Zu viele Doppellektionen an einem Tag überlasten die Klasse.',aktiv:true,sperrbar:true,gewicht:5,params:{max_doppel:2}}, {code:'R24',kat:'soft',name:'LP-Lektionen gleichmässig über Woche',desc:'Nicht 80% der Lektionen auf 1–2 Tage konzentriert.',aktiv:true,sperrbar:true,gewicht:6,params:{}}, {code:'R25',kat:'soft',name:'Freitagnachmittag möglichst frei',desc:'Freitagnachmittag bevorzugt ohne Lektionen.',aktiv:false,sperrbar:true,gewicht:3,params:{}}, {code:'R26',kat:'paedagogisch',name:'Kernfächer bevorzugt am Morgen',desc:'Deutsch und Mathematik gelingen besser in der ersten Tageshälfte.',aktiv:true,sperrbar:true,gewicht:5,params:{max_slot:4}}, {code:'R27',kat:'paedagogisch',name:'Anspruchsvolle Fächer nicht nach Sport/Mittag',desc:'Nach Sport oder Mittagessen mind. 1 Slot vor kognitiv intensiven Fächern.',aktiv:true,sperrbar:true,gewicht:4,params:{abstand:1}}, {code:'R28',kat:'paedagogisch',name:'Kreativfächer für Nachmittag',desc:'Musik, BG und Sport sind nachmittags gut geeignet.',aktiv:false,sperrbar:true,gewicht:3,params:{}}, {code:'R31',kat:'paedagogisch',name:'Sport mind. 2× pro Woche, nie doppelt',desc:'Sport an verschiedenen Tagen für optimale Bewegungsförderung.',aktiv:true,sperrbar:true,gewicht:4,params:{}}, {code:'R32',kat:'paedagogisch',name:'Fremdsprachen nicht letzte Lektion',desc:'Englisch/Französisch erfordern Konzentration — besser nicht am Tagesende.',aktiv:false,sperrbar:true,gewicht:3,params:{}}, {code:'R33',kat:'paedagogisch',name:'Unterstufe: keine Lektionen nach 15:00',desc:'Kinder der 1.–3. Klasse brauchen frühere Schulschlusszeiten.',aktiv:true,sperrbar:true,gewicht:6,params:{stufe:'US'}}, {code:'R35',kat:'paedagogisch',name:'Mittelstufe: max. 8 Lektionen pro Tag',desc:'Mehr als 8 Lektionen sind für 4.–6.-Klässler zu belastend.',aktiv:true,sperrbar:true,gewicht:7,params:{stufe:'MS',max_lektionen:8}}, ]; function _rKey(vid){ return 'sp_regeln_'+(vid||'global'); } function getAktiveRegeln(vid){ let ov={}; try{ ov=JSON.parse(localStorage.getItem(_rKey(vid))||'{}'); }catch(e){} const std=STANDARD_REGELN.map(r=>({...r,...(ov[r.code]||{})})); return [...std,...(ov._custom||[])]; } function _saveRO(vid,code,patch){ let ov={}; try{ ov=JSON.parse(localStorage.getItem(_rKey(vid))||'{}'); }catch(e){} ov[code]={...(ov[code]||{}),...patch}; localStorage.setItem(_rKey(vid),JSON.stringify(ov)); } function _saveCustom(vid,regel){ let ov={}; try{ ov=JSON.parse(localStorage.getItem(_rKey(vid))||'{}'); }catch(e){} if(!ov._custom) ov._custom=[]; const i=ov._custom.findIndex(r=>r.code===regel.code); if(i>=0) ov._custom[i]=regel; else ov._custom.push(regel); localStorage.setItem(_rKey(vid),JSON.stringify(ov)); } function deleteCustomRule(vid,code){ let ov={}; try{ ov=JSON.parse(localStorage.getItem(_rKey(vid))||'{}'); }catch(e){} ov._custom=(ov._custom||[]).filter(r=>r.code!==code); localStorage.setItem(_rKey(vid),JSON.stringify(ov)); renderRegelnPanel(); showToast('Regel gelöscht'); } function resetRegelnToDefaults(){ const vid=_currentVid(); localStorage.removeItem(_rKey(vid)); renderRegelnPanel(); showToast('Regeln auf Standard zurückgesetzt'); } function _currentVid(){ return window._aktVerbandId||window._active_verband_id||window._last_verband_id||'global'; } let _regelFilter='alle'; function filterRegeln(f,btn){ _regelFilter=f; document.querySelectorAll('.regel-filter-btn').forEach(b=>b.classList.remove('active')); if(btn) btn.classList.add('active'); renderRegelnPanel(); } function renderRegelnPanel(){ const vid=_currentVid(); const regeln=getAktiveRegeln(vid); const kat=document.getElementById('regeln-katalog'); const csec=document.getElementById('custom-regeln-section'); const clist=document.getElementById('custom-regeln-list'); if(!kat) return; const gruppen=[ {key:'hard',label:'🔴 Hard Constraints',desc:'Werden immer erzwungen'}, {key:'soft',label:'🟡 Soft Constraints',desc:'Beeinflussen den Score'}, {key:'paedagogisch',label:'🟢 Pädagogische Empfehlungen',desc:'Lernpsychologische Qualitätsziele'}, ]; let html=''; for(const g of gruppen){ if(_regelFilter!=='alle'&&_regelFilter!==g.key&&_regelFilter!=='custom') continue; if(_regelFilter==='custom') continue; const items=regeln.filter(r=>r.kat===g.key&&!r.custom); if(!items.length) continue; html+=`
${g.label}
`; for(const r of items) html+=_rCard(r,vid); html+='
'; } kat.innerHTML=html; const custom=regeln.filter(r=>r.custom); if(csec) csec.style.display=(custom.length||_regelFilter==='custom')?'block':'none'; if(clist) clist.innerHTML=custom.length ? custom.map(r=>_rCard(r,vid)).join('') : '

Noch keine eigenen Regeln.

'; } function _rCard(r,vid){ var locked=!r.sperrbar; var bCls=r.custom?'badge-custom':r.kat==='hard'?'badge-hard':r.kat==='soft'?'badge-soft':'badge-paed'; var bTxt=r.custom?'EIGENE':r.kat==='hard'?'HARD':r.kat==='soft'?'SOFT':'PÄDAG.'; var extra=''; if(!locked&&r.kat!=='hard'&&r.aktiv){ var gew=r.gewicht||5; var dots=''; for(var di=0;di<10;di++){ dots+=''; } extra+='
Gewicht:
'+dots+'
'+gew+'
'; } if(r.aktiv&&r.sperrbar&&r.params){ var entries=Object.entries(r.params); for(var ei=0;ei'; } } var cbtns=r.custom?('
'):''; var lockIcon=locked?'🔒':''; var extraHtml=extra?('
'+extra+'
'):''; return '
'+ ''+ '
'+ '
'+ ''+r.code+''+ ''+r.name+''+ ''+bTxt+''+ lockIcon+'
'+ '
'+(r.desc||'')+'
'+ extraHtml+cbtns+ '
'; } function toggleRegel(code,vid,aktiv){ _saveRO(vid,code,{aktiv}); const card=document.getElementById('rc-'+code); if(card) card.classList.toggle('inactive',!aktiv); showToast('Regel '+(aktiv?'aktiviert':'deaktiviert')); renderRegelnPanel(); } function setRegelGewicht(code,gewicht,vid){ _saveRO(vid,code,{gewicht}); renderRegelnPanel(); } function setRegelParam(code,key,val,vid){ const r=getAktiveRegeln(vid).find(x=>x.code===code); if(!r) return; _saveRO(vid,code,{params:{...(r.params||{}),[key]:val}}); } function openCustomRuleModal(editCode){ const modal=document.getElementById('modal-custom-regel'); modal.style.display='flex'; document.getElementById('edit-regel-code').value=editCode||''; document.getElementById('modal-regel-title').textContent=editCode?'Regel bearbeiten':'Eigene Regel erfassen'; if(editCode){ const r=getAktiveRegeln(_currentVid()).find(x=>x.code===editCode); if(r){ document.getElementById('regel-name').value=r.name||''; document.getElementById('regel-beschreibung').value=r.desc||''; document.getElementById('regel-kategorie').value=r.kat||'soft'; document.getElementById('regel-typ').value=r.typ||'max_pro_tag'; document.getElementById('regel-fach').value=(r.params&&r.params.fach)||''; document.getElementById('regel-stufe').value=(r.params&&r.params.stufe)||''; document.getElementById('regel-gewicht').value=r.gewicht||5; document.getElementById('gewicht-val').textContent=r.gewicht||5; } } else { ['regel-name','regel-beschreibung','regel-fach'].forEach(id=>{const el=document.getElementById(id);if(el)el.value='';}); document.getElementById('regel-kategorie').value='soft'; document.getElementById('regel-typ').value='max_pro_tag'; document.getElementById('regel-stufe').value=''; document.getElementById('regel-gewicht').value=5; document.getElementById('gewicht-val').textContent=5; } updateParamForm(); } function closeCustomRuleModal(){ document.getElementById('modal-custom-regel').style.display='none'; } function updateParamForm(){ var typ=document.getElementById('regel-typ').value; var kat=document.getElementById('regel-kategorie').value; var gw=document.getElementById('gewicht-wrap'); if(gw) gw.style.display=kat==='hard'?'none':'block'; var tage=['Montag','Dienstag','Mittwoch','Donnerstag','Freitag']; var tageHtml='
'; for(var ti=0;ti'+tage[ti]+''; } tageHtml+='
'; var forms={ max_pro_tag:'', min_abstand:'', nur_vm_nm:'', nicht_nach_fach:'', fruehestens_slot:'', spaetestens_slot:'', nur_tage:tageHtml, max_doppel:'', }; var pw=document.getElementById('param-form-wrap'); if(pw) pw.innerHTML=forms[typ]||''; } function saveCustomRule(){ const name=(document.getElementById('regel-name').value||'').trim(); if(!name){ showToast('Bitte einen Namen eingeben'); return; } const vid=_currentVid(); const editCode=document.getElementById('edit-regel-code').value; const typ=document.getElementById('regel-typ').value; const kat=document.getElementById('regel-kategorie').value; const params={}; const fachVal=(document.getElementById('regel-fach').value||'').trim(); const stufe=document.getElementById('regel-stufe').value; if(fachVal) params.fach=fachVal.split(',').map(s=>s.trim()).filter(Boolean); if(stufe) params.stufe=stufe; const pv1=document.getElementById('pv1'); if(pv1){ if(pv1.type==='number') params.wert=+pv1.value; else params.wert=pv1.value; } const pvTage=document.querySelectorAll('.pv-tag:checked'); if(pvTage.length) params.tage=Array.from(pvTage).map(t=>t.value); const code=editCode||('CUST-'+Date.now().toString(36).toUpperCase().slice(-6)); const regel={code,kat,name,desc:(document.getElementById('regel-beschreibung').value||'').trim(), aktiv:true,sperrbar:true,custom:true,typ, gewicht:kat==='hard'?null:+document.getElementById('regel-gewicht').value,params}; _saveCustom(vid,regel); closeCustomRuleModal(); renderRegelnPanel(); showToast('Regel "'+name+'" gespeichert'); } // Login hardening: bind events without inline onclick function bindLoginControls(){ var chips=document.querySelectorAll('#view-login .role-chip[data-role]'); chips.forEach(function(chip){ if(chip.dataset.bound) return; chip.dataset.bound='1'; chip.addEventListener('click', function(){ setRole(chip.dataset.role, chip); }); }); ['login-m365-btn','login-google-btn','login-prototype-btn'].forEach(function(id){ var btn=document.getElementById(id); if(btn && !btn.dataset.bound){ btn.dataset.bound='1'; btn.addEventListener('click', function(e){ e.preventDefault(); doLogin(); }); } }); } if(document.readyState==='loading'){ document.addEventListener('DOMContentLoaded', bindLoginControls); }else{ bindLoginControls(); }