diff --git a/App/php/api/assignments.php b/App/php/api/assignments.php index e58b740..90b368c 100644 --- a/App/php/api/assignments.php +++ b/App/php/api/assignments.php @@ -50,7 +50,9 @@ if ($method === 'POST') { $classId = (int)($body['classId'] ?? 0); $moduleId = $body['moduleId'] ?? ''; $level = max(1, min(5, (int)($body['level'] ?? 1))); - $endsAt = !empty($body['endsAt']) ? $body['endsAt'] : null; + // datetime-local liefert 'YYYY-MM-DDTHH:MM' → 'T' für MySQL zu Leerzeichen. + $endsAt = !empty($body['endsAt']) ? str_replace('T', ' ', $body['endsAt']) : null; + $startsAt = !empty($body['startsAt']) ? str_replace('T', ' ', $body['startsAt']) : null; if (!$classId || !$moduleId) Response::error('classId und moduleId erforderlich'); @@ -63,10 +65,17 @@ if ($method === 'POST') { [$classId, $moduleId] ); - $db->execute( - 'INSERT INTO class_assignments (class_id, teacher_id, module_id, level, starts_at, ends_at) VALUES (?, ?, ?, ?, NOW(), ?)', - [$classId, $teacherId, $moduleId, $level, $endsAt] - ); + if ($startsAt) { + $db->execute( + 'INSERT INTO class_assignments (class_id, teacher_id, module_id, level, starts_at, ends_at) VALUES (?, ?, ?, ?, ?, ?)', + [$classId, $teacherId, $moduleId, $level, $startsAt, $endsAt] + ); + } else { + $db->execute( + 'INSERT INTO class_assignments (class_id, teacher_id, module_id, level, starts_at, ends_at) VALUES (?, ?, ?, ?, NOW(), ?)', + [$classId, $teacherId, $moduleId, $level, $endsAt] + ); + } Response::ok(['id' => (int)$db->lastInsertId()]); } diff --git a/App/php/api/modules.php b/App/php/api/modules.php index fda8520..fb908cb 100644 --- a/App/php/api/modules.php +++ b/App/php/api/modules.php @@ -16,6 +16,11 @@ require_once __DIR__ . '/../lib/Response.php'; $method = $_SERVER['REQUEST_METHOD']; $db = Database::get(); +// Default-Modus für eine Klasse OHNE expliziten Eintrag: eine neue Klasse startet +// mit NUR Weltküche frei (Einstiegs-Sim), alles andere gesperrt. Die Lehrperson +// gibt weitere Module über die Freigabe-Matrix frei. +function _defaultMode($moduleId) { return $moduleId === 'weltkueche' ? 'free' : 'locked'; } + // Modul-Katalog dynamisch aus module_info (Source of Truth) // Nur aktive + beta + geplante Module anzeigen (keine archivierten) $ALL_MODULES = $db->fetchAll( @@ -85,7 +90,7 @@ if ($method === 'GET') { foreach ($ALL_MODULES as $mod) { if ($filterId && $mod['id'] !== $filterId) continue; $cm = $classMap[$mod['id']] ?? null; - $mode = $overrides[$mod['id']] ?? ($cm['mode'] ?? 'locked'); + $mode = $overrides[$mod['id']] ?? ($cm['mode'] ?? _defaultMode($mod['id'])); // 'geplant' überschreibt jeden mode-Eintrag — Modul ist nicht spielbar. if ($mod['status'] === 'geplant') $mode = 'coming_soon'; @@ -172,7 +177,7 @@ if ($method === 'GET') { 'modules' => [], ]; foreach ($ALL_MODULES as $mod) { - $row['modules'][$mod['id']] = $overrides[$s['id']][$mod['id']] ?? $classMap[$mod['id']] ?? 'locked'; + $row['modules'][$mod['id']] = $overrides[$s['id']][$mod['id']] ?? $classMap[$mod['id']] ?? _defaultMode($mod['id']); } $matrix[] = $row; } @@ -189,7 +194,7 @@ if ($method === 'GET') { 'card_image' => $mod['card_image'], 'status' => $mod['status'], 'available_from' => $mod['available_from'], - 'classMode' => $classMap[$mod['id']] ?? 'locked', + 'classMode' => $classMap[$mod['id']] ?? _defaultMode($mod['id']), 'currentLevel' => $meta ? (int)($meta['current_level'] ?? 1) : 1, 'paused' => $meta ? (bool)($meta['paused'] ?? 0) : false, 'audioInfo' => $meta ? (bool)($meta['audio_info'] ?? 1) : true, @@ -210,7 +215,7 @@ if ($method === 'GET') { 'desc' => $mod['desc'], 'icon' => $mod['icon'], 'status' => $mod['status'], - 'mode' => $classMap[$mod['id']] ?? 'locked', + 'mode' => $classMap[$mod['id']] ?? _defaultMode($mod['id']), ]; } Response::ok($result); diff --git a/App/schueler.html b/App/schueler.html index 0bb4ede..ebdd07d 100644 --- a/App/schueler.html +++ b/App/schueler.html @@ -311,7 +311,7 @@ async function renderModules() { html += ''; if (lockedByTeacher.length > 0) { - html += '
Kann durch die Lehrperson freigegeben werden
'; + html += '
🔒 Deine Lehrperson kann weitere Simulationen freischalten
'; html += '
'; lockedByTeacher.forEach(function(m) { html += renderModCard(m); }); html += '
'; diff --git a/App/teacher.html b/App/teacher.html index daa97d9..98ec2ac 100644 --- a/App/teacher.html +++ b/App/teacher.html @@ -386,14 +386,26 @@ schueler01,Max M.,,,,, - -
+ +
+ + +
+ + +
- +
+ + +
@@ -2895,7 +2907,9 @@ async function loadModuleMatrix() { var thumb = img ? '' : ''+(mod?mod.icon:'📋')+''; - var end = a.ends_at ? 'fällig ' + new Date(a.ends_at).toLocaleDateString('de-AT',{weekday:'short',day:'2-digit',month:'2-digit',hour:'2-digit',minute:'2-digit'}) : 'ohne Zeitlimit'; + var end = a.ends_at ? 'fällig ' + new Date(a.ends_at.replace(' ','T')).toLocaleDateString('de-AT',{weekday:'short',day:'2-digit',month:'2-digit',hour:'2-digit',minute:'2-digit'}) : 'ohne Zeitlimit'; + var startMs = a.starts_at ? new Date(a.starts_at.replace(' ','T')).getTime() : 0; + if (startMs && startMs > Date.now()) end = 'ab ' + new Date(startMs).toLocaleDateString('de-AT',{weekday:'short',day:'2-digit',month:'2-digit',hour:'2-digit',minute:'2-digit'}) + ' · ' + end; var stateBadge = paused ? '⏸ Vorbereitet' : '▶ Läuft'; @@ -3041,6 +3055,7 @@ async function loadAllMods() { if (Array.isArray(r)) ALL_MODS = r.filter(function(m){return m.status!=='archiv' && m.status!=='geplant';}); return ALL_MODS; } +var _ssStartMode = 'now'; async function showStartSim(preselectedModuleId) { await loadAllMods(); var sel = document.getElementById('ss-module'); @@ -3049,44 +3064,72 @@ async function showStartSim(preselectedModuleId) { if (preselectedModuleId) sel.value = preselectedModuleId; document.getElementById('ss-paused').checked = false; document.getElementById('ss-level').value = '1'; - ssSetDue('woche'); // Default-Frist: +1 Woche (Hausübung); setzt Feld + Bestätigung + var now = new Date(); + document.getElementById('ss-start-date').value = _ssDate(now); + document.getElementById('ss-start-time').value = '08:00'; + ssStartMode('now'); + ssSetDue('woche'); // Default-Frist: +1 Woche showModal('modal-startsim'); } function _ssPad(n){ return String(n).padStart(2,'0'); } -function _ssDtLocal(d){ return d.getFullYear()+'-'+_ssPad(d.getMonth()+1)+'-'+_ssPad(d.getDate())+'T'+_ssPad(d.getHours())+':'+_ssPad(d.getMinutes()); } +function _ssDate(d){ return d.getFullYear()+'-'+_ssPad(d.getMonth()+1)+'-'+_ssPad(d.getDate()); } +function ssStartMode(mode){ + _ssStartMode = mode; + var now = document.getElementById('ss-start-now'), at = document.getElementById('ss-start-at'); + var fields = document.getElementById('ss-start-fields'); + now.className = mode==='now' ? 'btn' : 'btn btn-outline'; + at.className = mode==='at' ? 'btn' : 'btn btn-outline'; + fields.style.display = mode==='at' ? 'flex' : 'none'; + ssUpdateSummary(); +} function ssSetDue(kind){ - var el = document.getElementById('ss-ends'); - if (kind === '') { el.value = ''; ssUpdateSummary(); return; } + var dd = document.getElementById('ss-end-date'), tt = document.getElementById('ss-end-time'); + if (kind === '') { dd.value = ''; ssUpdateSummary(); return; } var d = new Date(); if (kind === 'morgen') d.setDate(d.getDate()+1); else if (kind === 'woche') d.setDate(d.getDate()+7); - d.setHours(13, 0, 0, 0); - el.value = _ssDtLocal(d); + dd.value = _ssDate(d); + tt.value = '13:00'; ssUpdateSummary(); } +// Kombiniert date+time zu 'YYYY-MM-DDTHH:MM' (oder null, wenn kein Datum) +function _ssCombine(dateId, timeId){ + var d = document.getElementById(dateId).value; + if (!d) return null; + var t = document.getElementById(timeId).value || '00:00'; + return d + 'T' + t; +} +function ssStartValue(){ return _ssStartMode === 'now' ? null : _ssCombine('ss-start-date','ss-start-time'); } +function ssEndValue(){ return _ssCombine('ss-end-date','ss-end-time'); } +function _ssFmt(v){ return new Date(v).toLocaleDateString('de-AT',{weekday:'short',day:'2-digit',month:'2-digit',hour:'2-digit',minute:'2-digit'}); } function ssUpdateSummary(){ var el = document.getElementById('ss-summary'); if (!el) return; var modSel = document.getElementById('ss-module'); var modTxt = (modSel && modSel.options[modSel.selectedIndex]) ? modSel.options[modSel.selectedIndex].text : '—'; var lvl = document.getElementById('ss-level').value; - var ends = document.getElementById('ss-ends').value; - var paused = document.getElementById('ss-paused').checked; - var endTxt = ends ? new Date(ends).toLocaleDateString('de-AT',{weekday:'short',day:'2-digit',month:'2-digit',hour:'2-digit',minute:'2-digit'}) : 'ohne Zeitlimit'; - var startTxt = paused ? '⏸ startet auf dein Signal' : '▶ startet sofort'; - el.innerHTML = ''+esc(modTxt)+' · Stufe '+lvl+' · fällig '+esc(endTxt)+' · '+startTxt; + var startsAt = ssStartValue(); + var endsAt = ssEndValue(); + var paused = document.getElementById('ss-paused').checked; + var future = startsAt && new Date(startsAt) > new Date(); + var startTxt = startsAt ? ('ab '+esc(_ssFmt(startsAt))+'') : (paused ? '⏸ auf dein Signal' : '▶ sofort'); + var endTxt = endsAt ? ('fällig '+esc(_ssFmt(endsAt))+'') : 'ohne Frist'; + var note = (paused || future) ? ' · Klasse wartet auf „Start freigeben"' : ''; + el.innerHTML = ''+esc(modTxt)+' · Stufe '+lvl+' · '+startTxt+' · '+endTxt+note; } async function startSimulation() { var moduleId = document.getElementById('ss-module').value; var level = parseInt(document.getElementById('ss-level').value); - var endsAt = document.getElementById('ss-ends').value || null; - var paused = document.getElementById('ss-paused').checked; + var startsAt = ssStartValue(); + var endsAt = ssEndValue(); + var future = startsAt && new Date(startsAt) > new Date(); + // Zukünftiger Start ⇒ automatisch „vorbereitet" (Klasse wartet), sonst Checkbox. + var paused = document.getElementById('ss-paused').checked || !!future; // Sowohl class_modules (Modus + Level + Pause) als auch class_assignments - // (Auftragshistorie) setzen, damit Schüler-Cockpit + saves.php-Filter + - // Live-View den gleichen Auftragszustand sehen. + // (Auftragshistorie inkl. Zeitfenster) setzen. var rm = await api('modules', {action:'set_mode', classId:currentClassId, moduleId:moduleId, mode:'teacher_started', currentLevel:level, dueDate:endsAt, paused:paused}); if (rm && rm.error) { alert(rm.error); return; } - var r = await api('assignments', {action:'create', classId:currentClassId, moduleId:moduleId, level:level, endsAt:endsAt}); + var r = await api('assignments', {action:'create', classId:currentClassId, moduleId:moduleId, level:level, startsAt:startsAt, endsAt:endsAt}); if (r.error) { alert(r.error); return; } hideModal('modal-startsim'); loadModuleMatrix();