(function(){ // ============================================================ // todayrail.jsx — "Heute"-Spalte rechts (Desktop) bzw. Ausklapp-Panel (Mobile) // Ersetzt den frueheren Tagesplaner: heutige Termine, Lernbloecke/Nachbearbeitungen, heutige Todos // und eine Zeitbilanz (benoetigte Dauer gegen freie Arbeitszeit). // Depends on window: t, localISO, hexToRgba, prioCol // ============================================================ const pad2 = n => String(n).padStart(2, "0"); const hm = d => `${pad2(d.getHours())}:${pad2(d.getMinutes())}`; const toMin = (s, def) => { const m = /^(\d{1,2}):(\d{2})/.exec(s || ""); return m ? (+m[1]) * 60 + (+m[2]) : def; }; const fmtMin = m => { m = Math.max(0, Math.round(m)); const h = Math.floor(m / 60), r = m % 60; return h ? (r ? `${h} h ${r} min` : `${h} h`) : `${r} min`; }; // Heute relevante Todos: heute geplant, ueberfaellig oder heute faellig (wie die Gruppen im Aufgaben-Tab). function railTodos(todos, today) { return (todos || []).filter(t => !t.done && !t.deletedAt && !t.recurring && !(t.snoozedUntil && t.snoozedUntil > today) && ((t.plannedDay && t.plannedDay !== "someday" && t.plannedDay <= today) || t.deadline === today) ).sort((a, b) => (a.plannedDay || a.deadline || "").localeCompare(b.plannedDay || b.deadline || "") || ({ hoch: 0, mittel: 1, niedrig: 2 }[a.priority || "mittel"] - { hoch: 0, mittel: 1, niedrig: 2 }[b.priority || "mittel"]) ); } // Heutige Termine mit Uhrzeit als [{startMin, endMin, title, cal}] function railEvents(events, today) { return (events || []).filter(e => e.start?.dateTime && e.start.dateTime.slice(0, 10) === today).map(e => { const s = new Date(e.start.dateTime), en = e.end?.dateTime ? new Date(e.end.dateTime) : s; // Endet ein Termin an einem spaeteren Tag (ueber Mitternacht), belegt er den Rest des Tages. const endMin = localISO(en) > today ? 24 * 60 : en.getHours() * 60 + en.getMinutes(); return { startMin: s.getHours() * 60 + s.getMinutes(), endMin: Math.max(endMin, s.getHours() * 60 + s.getMinutes()), title: e.subject || "", time: hm(s), cal: e._calName || "" }; }).sort((a, b) => a.startMin - b.startMin); } // Zeitbilanz: geplant = Dauer aller heute geplanten Todos; gearbeitet = heute erfasste Zeit (Todo-Timer, Lern-/Vorlesungs-Timer). // "Graue Zeit": Wird ein heute geplantes Todo erledigt, bevor seine Dauer abgelaufen ist, zaehlt die nicht gebrauchte Restzeit // als erledigt (nur in dieser Tagesbilanz, nicht in der Statistik). Ueberfaellige Todos zaehlen nicht zur Planung, sondern als Bonus. function railBalance({ todos, today, lectures, exams }) { const [y, m, d] = today.split("-").map(Number); const dayStart = new Date(y, m - 1, d).getTime(), dayEnd = dayStart + 86400000, now = Date.now(); const lecMap = new Map((lectures || []).map(l => [String(l.id), l])); const blockByTodo = new Map(); (exams || []).filter(e => !e.deletedAt).forEach(e => (e.studyBlocks || []).forEach(b => { if (b.todoId != null) blockByTodo.set(String(b.todoId), b); })); const sessMin = arr => (arr || []).filter(s => s.date === today).reduce((s, x) => s + (Number(x.ms) > 0 ? Number(x.ms) : 0), 0) / 60000; const logMin = t => (t.timeLog || []).reduce((s, e) => { const st = Math.max(e.start, dayStart), en = Math.min(e.end == null ? now : e.end, dayEnd); return s + Math.max(0, en - st); }, 0) / 60000; const doneToday = t => t.done && t.doneAt && localISO(new Date(t.doneAt)) === today; const lecOf = t => (t.sourceLectureId != null ? lecMap.get(String(t.sourceLectureId)) : null); const defMin = t => { const l = lecOf(t); return l ? Math.round((Number(l.hoursAfter) || 0) * 60) : 0; }; const durOf = t => (Number(t.duration) > 0 ? Number(t.duration) : defMin(t)); const plan = (todos || []).filter(t => !t.deletedAt && !t.recurring && t.plannedDay === today && (!t.done || doneToday(t))); let planned = 0, grey = 0, noDur = 0; const lecGroups = new Map(); plan.forEach(t => { const dur = durOf(t); if (dur > 0) planned += dur; else noDur++; const lec = lecOf(t), blk = blockByTodo.get(String(t.id)); if (lec) { const g = lecGroups.get(String(lec.id)) || { lec, dur: 0, allDone: true }; g.dur += dur; if (!t.done) g.allDone = false; lecGroups.set(String(lec.id), g); } else if (blk) { if (t.done) grey += Math.max(0, (dur > 0 ? dur : Math.round((Number(blk.hours) || 0) * 60)) - sessMin(blk.sessions)); } else if (t.done) { grey += Math.max(0, dur - logMin(t)); } }); lecGroups.forEach(g => { if (g.allDone) grey += Math.max(0, g.dur - sessMin(g.lec.sessions)); }); let worked = 0; (lectures || []).forEach(l => { worked += sessMin(l.sessions); }); (exams || []).filter(e => !e.deletedAt).forEach(e => (e.studyBlocks || []).forEach(b => { worked += sessMin(b.sessions); })); (todos || []).forEach(t => { if (!t.deletedAt && !lecOf(t) && !blockByTodo.get(String(t.id))) worked += logMin(t); }); const overdue = railTodos(todos, today).filter(t => t.plannedDay !== today); const bonusMin = overdue.reduce((s, t) => s + durOf(t), 0); return { planned, worked: Math.round(worked), grey: Math.round(grey), noDur, bonusMin, bonusN: overdue.length }; } // Eigene Auswahlliste im App-Stil (die native