Aufgabe: Ab-wann + Bis-wann mit gutem Picker; neue Klasse nur Weltkueche frei
- Modal: "Ab wann" (Sofort/Zeitpunkt) + "Bis wann" (Presets + date/time statt datetime-local), Live-Zusammenfassung; zukuenftiger Start => vorbereitet - assignments.php: startsAt-Param (Default NOW()), T->Space - modules.php: neue Klasse ohne Eintrag -> nur Weltkueche frei (_defaultMode), Rest gesperrt; Freigabe-Matrix bleibt (getrennt von der Aufgabe) - schueler.html: Hinweis "Deine Lehrperson kann weitere Simulationen freischalten" Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -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()]);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
+1
-1
@@ -311,7 +311,7 @@ async function renderModules() {
|
||||
html += '</div>';
|
||||
|
||||
if (lockedByTeacher.length > 0) {
|
||||
html += '<div class="coming-divider">Kann durch die Lehrperson freigegeben werden</div>';
|
||||
html += '<div class="coming-divider">🔒 Deine Lehrperson kann weitere Simulationen freischalten</div>';
|
||||
html += '<div class="mod-grid">';
|
||||
lockedByTeacher.forEach(function(m) { html += renderModCard(m); });
|
||||
html += '</div>';
|
||||
|
||||
+63
-20
@@ -386,14 +386,26 @@ schueler01,Max M.,,,,,</pre>
|
||||
<option value="2">Stufe 2 — Fortgeschritten</option>
|
||||
<option value="3">Stufe 3 — Profi</option>
|
||||
</select>
|
||||
<label>Fällig bis</label>
|
||||
<div style="display:flex;gap:.3rem;flex-wrap:wrap;margin-bottom:.35rem">
|
||||
<label>Ab wann?</label>
|
||||
<div style="display:flex;gap:.3rem;margin-bottom:.4rem">
|
||||
<button type="button" id="ss-start-now" class="btn" style="font-size:.7rem;padding:.3rem .7rem" onclick="ssStartMode('now')">▶ Sofort</button>
|
||||
<button type="button" id="ss-start-at" class="btn btn-outline" style="font-size:.7rem;padding:.3rem .7rem" onclick="ssStartMode('at')">🕒 Zeitpunkt wählen</button>
|
||||
</div>
|
||||
<div id="ss-start-fields" style="display:none;gap:.4rem;margin-bottom:.5rem">
|
||||
<input type="date" id="ss-start-date" onchange="ssUpdateSummary()" style="padding:.35rem .5rem;border:1.5px solid rgba(0,0,0,.12);border-radius:6px;font-size:.78rem;font-family:inherit">
|
||||
<input type="time" id="ss-start-time" value="08:00" onchange="ssUpdateSummary()" style="padding:.35rem .5rem;border:1.5px solid rgba(0,0,0,.12);border-radius:6px;font-size:.78rem;font-family:inherit">
|
||||
</div>
|
||||
<label>Bis wann (Frist)</label>
|
||||
<div style="display:flex;gap:.3rem;flex-wrap:wrap;margin-bottom:.4rem">
|
||||
<button type="button" class="btn btn-outline" style="font-size:.66rem;padding:.25rem .55rem" onclick="ssSetDue('heute')">Heute 13:00</button>
|
||||
<button type="button" class="btn btn-outline" style="font-size:.66rem;padding:.25rem .55rem" onclick="ssSetDue('morgen')">Morgen</button>
|
||||
<button type="button" class="btn btn-outline" style="font-size:.66rem;padding:.25rem .55rem" onclick="ssSetDue('woche')">+1 Woche</button>
|
||||
<button type="button" class="btn btn-outline" style="font-size:.66rem;padding:.25rem .55rem" onclick="ssSetDue('')">Kein Limit</button>
|
||||
</div>
|
||||
<input type="datetime-local" id="ss-ends" onchange="ssUpdateSummary()">
|
||||
<div style="display:flex;gap:.4rem;align-items:center">
|
||||
<input type="date" id="ss-end-date" onchange="ssUpdateSummary()" style="padding:.35rem .5rem;border:1.5px solid rgba(0,0,0,.12);border-radius:6px;font-size:.78rem;font-family:inherit">
|
||||
<input type="time" id="ss-end-time" value="13:00" onchange="ssUpdateSummary()" style="padding:.35rem .5rem;border:1.5px solid rgba(0,0,0,.12);border-radius:6px;font-size:.78rem;font-family:inherit">
|
||||
</div>
|
||||
<label style="display:flex;align-items:flex-start;gap:.4rem;margin-top:.6rem;cursor:pointer">
|
||||
<input type="checkbox" id="ss-paused" onchange="ssUpdateSummary()" style="margin-top:.15rem"> <span style="font-size:.74rem">⏸ Vorbereiten — die Klasse startet erst, wenn du auf der Aufgaben-Card „▶ Start freigeben" drückst (gemeinsamer Start)</span>
|
||||
</label>
|
||||
@@ -2895,7 +2907,9 @@ async function loadModuleMatrix() {
|
||||
var thumb = img
|
||||
? '<img src="'+img+'" alt="" style="width:50px;height:50px;border-radius:9px;object-fit:cover;flex:none">'
|
||||
: '<span style="font-size:1.6rem;width:50px;text-align:center">'+(mod?mod.icon:'📋')+'</span>';
|
||||
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
|
||||
? '<span style="background:#fdf1e6;color:#c25e1e;border:1px solid #e8c3a0;padding:.08rem .5rem;border-radius:99px;font-size:.62rem;font-weight:700">⏸ Vorbereitet</span>'
|
||||
: '<span style="background:#e5f2e7;color:#2a7a3a;border:1px solid #b9dcc0;padding:.08rem .5rem;border-radius:99px;font-size:.62rem;font-weight:700">▶ Läuft</span>';
|
||||
@@ -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 = '<b>'+esc(modTxt)+'</b> · Stufe '+lvl+' · fällig <b>'+esc(endTxt)+'</b> · '+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 <b>'+esc(_ssFmt(startsAt))+'</b>') : (paused ? '⏸ auf dein Signal' : '▶ sofort');
|
||||
var endTxt = endsAt ? ('fällig <b>'+esc(_ssFmt(endsAt))+'</b>') : 'ohne Frist';
|
||||
var note = (paused || future) ? ' <span style="color:#c25e1e">· Klasse wartet auf „Start freigeben"</span>' : '';
|
||||
el.innerHTML = '<b>'+esc(modTxt)+'</b> · 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();
|
||||
|
||||
Reference in New Issue
Block a user