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();