Quelltext des aktuellen Standes
<?php $this->layout('layouts/main', [
'page_title' => $page_title,
'current_route' => $current_route,
'full_width' => true,
]); ?>
<?php
$f = $filters;
$hasFilter = $f['q'] !== '' || $f['category'] !== '' || $f['priority'] !== '' || ($f['sort'] !== '' && $f['sort'] !== 'smart');
?>
<div class="todo-wrap">
<div class="content-header">
<div>
<h1><?= $this->te('Meine To-Do-Liste') ?></h1>
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="/"><?= $this->te('Home') ?></a></li>
<li class="breadcrumb-item active"><?= $this->te('To-Do-Liste') ?></li>
</ol>
</div>
<div class="content-header-actions">
<button type="button" class="btn btn-primary btn-sm" onclick="Todo.openAddModal()"><i class="fas fa-plus"></i> <?= $this->te('Neue Aufgabe') ?></button>
</div>
</div>
<div class="page-body todo-body">
<!-- KPI Small Boxes -->
<div class="kpi-row">
<div class="kpi-col">
<div class="small-box bg-info">
<div class="inner"><h3 id="kpiTotal"><?= (int)$stats['total'] ?></h3><p><?= $this->te('Offen') ?></p></div>
<div class="icon"><i class="fas fa-tasks"></i></div>
</div>
</div>
<div class="kpi-col">
<div class="small-box <?= (int)$stats['today'] > 0 ? 'bg-warning' : 'bg-secondary' ?>" id="kpiTodayBox" data-on="bg-warning">
<div class="inner"><h3 id="kpiToday"><?= (int)$stats['today'] ?></h3><p><?= $this->te('Heute fällig') ?></p></div>
<div class="icon"><i class="fas fa-calendar-day"></i></div>
</div>
</div>
<div class="kpi-col">
<div class="small-box <?= (int)$stats['overdue'] > 0 ? 'bg-danger' : 'bg-secondary' ?>" id="kpiOverdueBox" data-on="bg-danger">
<div class="inner"><h3 id="kpiOverdue"><?= (int)$stats['overdue'] ?></h3><p><?= $this->te('Überfällig') ?></p></div>
<div class="icon"><i class="fas fa-exclamation-triangle"></i></div>
</div>
</div>
</div>
<!-- Filterleiste: Suche / Filter / Sortierung -->
<div class="card card-outline card-primary mb-3">
<div class="filter-card-body">
<form method="get" action="/todos" style="display:flex;gap:0.5rem;flex-wrap:wrap;align-items:center;width:100%;">
<div class="filter-input-wrap" style="flex:1;min-width:200px;">
<svg class="filter-search-icon" viewBox="0 0 16 16" width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="7" cy="7" r="5"/><line x1="11" y1="11" x2="14" y2="14"/></svg>
<input type="search" name="q" class="form-control form-control-sm" value="<?= $this->e($f['q']) ?>" placeholder="<?= $this->te('Aufgaben durchsuchen …') ?>">
</div>
<select name="category" class="form-control form-control-sm" style="width:auto;" onchange="this.form.submit()">
<option value=""><?= $this->te('Alle Kategorien') ?></option>
<?php foreach ($categories as $cat): ?>
<option value="<?= $this->e($cat) ?>"<?= $f['category'] === $cat ? ' selected' : '' ?>><?= $this->e($cat) ?></option>
<?php endforeach; ?>
</select>
<select name="priority" class="form-control form-control-sm" style="width:auto;" onchange="this.form.submit()">
<option value=""><?= $this->te('Alle Prioritäten') ?></option>
<option value="3"<?= $f['priority'] === '3' ? ' selected' : '' ?>><?= $this->te('Hoch') ?></option>
<option value="2"<?= $f['priority'] === '2' ? ' selected' : '' ?>><?= $this->te('Mittel') ?></option>
<option value="1"<?= $f['priority'] === '1' ? ' selected' : '' ?>><?= $this->te('Niedrig') ?></option>
<option value="0"<?= $f['priority'] === '0' ? ' selected' : '' ?>><?= $this->te('Keine') ?></option>
</select>
<select name="sort" class="form-control form-control-sm" style="width:auto;" title="<?= $this->te('Sortierung') ?>" onchange="this.form.submit()">
<option value="smart"<?= $f['sort'] === 'smart' ? ' selected' : '' ?>><?= $this->te('Smart') ?></option>
<option value="due"<?= $f['sort'] === 'due' ? ' selected' : '' ?>><?= $this->te('Fälligkeit') ?></option>
<option value="priority"<?= $f['sort'] === 'priority' ? ' selected' : '' ?>><?= $this->te('Priorität') ?></option>
<option value="created"<?= $f['sort'] === 'created' ? ' selected' : '' ?>><?= $this->te('Erstellt') ?></option>
<option value="alpha"<?= $f['sort'] === 'alpha' ? ' selected' : '' ?>>A–Z</option>
<option value="manual"<?= $f['sort'] === 'manual' ? ' selected' : '' ?>><?= $this->te('Manuell') ?></option>
</select>
<button type="submit" class="btn btn-primary btn-sm"><?= $this->te('Suchen') ?></button>
<?php if ($hasFilter): ?>
<a href="/todos" class="btn btn-secondary btn-sm"><?= $this->te('Zurücksetzen') ?></a>
<?php endif; ?>
</form>
</div>
</div>
<datalist id="todoCatList">
<?php foreach ($categories as $cat): ?>
<option value="<?= $this->e($cat) ?>"></option>
<?php endforeach; ?>
</datalist>
<div class="todo-columns">
<!-- Offene To-Dos -->
<div class="todo-col">
<div class="todo-card">
<div class="todo-card-header">
<span class="todo-section-label"><?= $this->te('Offen') ?></span>
<span class="status-pill status-pill--info open-count"><?= count($open_todos) ?></span>
</div>
<ul class="todo-list" id="todoList">
<li class="todo-empty" id="todoEmpty"<?= empty($open_todos) ? '' : ' hidden' ?>>
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="#d1d5db" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" style="margin-bottom:0.5rem;"><polyline points="20 6 9 17 4 12"/></svg>
<span><?= $hasFilter ? 'Keine Treffer für die Filter.' : 'Keine offenen Aufgaben' ?></span>
</li>
<?php foreach ($open_todos as $t): ?>
<?= $this->render('todos/_item', ['t' => $t, 'subs' => $subtasks[(int)$t['id']] ?? []]) ?>
<?php endforeach; ?>
</ul>
</div>
</div>
<!-- Erledigte To-Dos -->
<div class="todo-col">
<div class="todo-card todo-card--done" id="todoDoneCard"<?= empty($done_todos) ? ' hidden' : '' ?>>
<div class="todo-card-header todo-done-header">
<button class="todo-done-toggle" onclick="Todo.toggleDoneSection(this)" aria-expanded="false" type="button">
<span class="todo-toggle-arrow">
<svg width="10" height="10" viewBox="0 0 10 10" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><polyline points="2,3 5,7 8,3"/></svg>
</span>
<span class="todo-section-label"><?= $this->te('Erledigt') ?></span>
<span class="status-pill status-pill--success done-count"><?= count($done_todos) ?></span>
</button>
<button class="todo-clear-done" onclick="Todo.clearDone()" type="button" title="<?= $this->te('Alle erledigten löschen') ?>"><?= $this->te('Aufräumen') ?></button>
</div>
<ul class="todo-list todo-list--done" id="todoDoneList" hidden>
<?php foreach ($done_todos as $t): ?>
<?= $this->render('todos/_item', ['t' => $t, 'subs' => $subtasks[(int)$t['id']] ?? []]) ?>
<?php endforeach; ?>
</ul>
</div>
<div class="todo-card todo-card--empty-done" id="todoDoneEmpty"<?= empty($done_todos) ? '' : ' hidden' ?>>
<p class="todo-empty"><?= $this->te('Noch nichts erledigt.') ?></p>
</div>
</div>
</div><!-- .todo-columns -->
</div>
</div>
<!-- Modal: Neue Aufgabe -->
<div id="todoAddModal" class="modal-overlay" role="dialog" aria-modal="true" aria-labelledby="todoAddModalTitle" style="display:none;">
<div class="modal-card">
<div class="modal-header">
<h2 id="todoAddModalTitle"><?= $this->te('Neue Aufgabe') ?></h2>
<button type="button" class="modal-close" onclick="Todo.closeAddModal()" aria-label="<?= $this->te('Dialog schließen') ?>">×</button>
</div>
<form id="todoAddForm" onsubmit="Todo.add(event)" autocomplete="off">
<div class="modal-body">
<div class="form-group">
<label for="todoInput"><?= $this->te('Aufgabe') ?> <span class="text-danger">*</span></label>
<input id="todoInput" type="text" class="form-control form-control-sm"
placeholder="<?= $this->te('Was ist zu tun?') ?>" maxlength="500" required autocomplete="off">
</div>
<div class="todo-modal-grid">
<div class="form-group">
<label for="todoAddDue"><?= $this->te('Fälligkeitsdatum') ?></label>
<input id="todoAddDue" type="date" class="form-control form-control-sm">
</div>
<div class="form-group">
<label for="todoAddTime"><?= $this->te('Uhrzeit') ?></label>
<input id="todoAddTime" type="time" class="form-control form-control-sm">
</div>
<div class="form-group">
<label for="todoAddPrio"><?= $this->te('Priorität') ?></label>
<select id="todoAddPrio" class="form-control form-control-sm">
<option value="0"><?= $this->te('Keine') ?></option>
<option value="1"><?= $this->te('Niedrig') ?></option>
<option value="2"><?= $this->te('Mittel') ?></option>
<option value="3"><?= $this->te('Hoch') ?></option>
</select>
</div>
<div class="form-group">
<label for="todoAddCat"><?= $this->te('Kategorie') ?></label>
<input id="todoAddCat" type="text" class="form-control form-control-sm" list="todoCatList" maxlength="100" placeholder="<?= $this->te('Kategorie') ?>">
</div>
<div class="form-group">
<label for="todoAddRecur"><?= $this->te('Wiederholung') ?></label>
<select id="todoAddRecur" class="form-control form-control-sm">
<option value="none"><?= $this->te('Keine Wiederholung') ?></option>
<option value="daily"><?= $this->te('Täglich') ?></option>
<option value="weekly"><?= $this->te('Wöchentlich') ?></option>
<option value="monthly"><?= $this->te('Monatlich') ?></option>
<option value="yearly"><?= $this->te('Jährlich') ?></option>
</select>
</div>
<div class="form-group">
<label for="todoAddLead"><?= $this->te('Erinnern (Tage vorher)') ?></label>
<input id="todoAddLead" type="number" class="form-control form-control-sm" min="0" max="60" value="1">
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary btn-sm" onclick="Todo.closeAddModal()"><?= $this->te('Abbrechen') ?></button>
<button type="submit" class="btn btn-primary btn-sm"><i class="fas fa-plus"></i> <?= $this->te('Hinzufügen') ?></button>
</div>
</form>
</div>
</div>
<style>
.todo-wrap { display:flex; flex-direction:column; }
.todo-body { --todo-primary:var(--color-primary); min-height:calc(100vh - 300px); }
/* [hidden] muss klassenbasierte display-Regeln (display:flex u. a.) schlagen,
sonst bleiben Editor-Panel, Leerzustand & co. dauerhaft sichtbar. */
.todo-wrap [hidden] { display:none !important; }
/* Modal „Neue Aufgabe": Felder-Raster (Standard-Modal aus app.css) */
.todo-modal-grid { display:grid; grid-template-columns:1fr 1fr; gap:0.75rem; }
.todo-modal-grid .form-group { margin-bottom:0; }
/* Layout */
.todo-columns { display:grid; grid-template-columns:1fr 1fr; gap:1.5rem; align-items:start; }
.todo-col { display:flex; flex-direction:column; gap:1rem; }
.todo-card--empty-done { opacity:0.5; }
.todo-card { background:var(--color-bg); border-radius:0.4rem; box-shadow:0 1px 3px rgba(0,0,0,0.07); margin-bottom:1rem; overflow:hidden; }
.todo-card--done { box-shadow:0 1px 2px rgba(0,0,0,0.05); }
.todo-card-header { display:flex; align-items:center; gap:0.5rem; padding:0.85rem 1.25rem; border-bottom:1px solid var(--gray-100); }
.todo-done-header { padding:0; }
.todo-section-label { font-size:0.75rem; font-weight:700; text-transform:uppercase; letter-spacing:0.06em; color:var(--gray-500); display:flex; align-items:center; gap:0.35rem; }
.todo-done-toggle { flex:1; display:flex; align-items:center; gap:0.5rem; padding:0.85rem 1.25rem; background:none; border:none; cursor:pointer; text-align:left; transition:background 0.12s; }
.todo-done-toggle:hover { background:var(--gray-50); }
.todo-toggle-arrow { display:inline-flex; align-items:center; transition:transform 0.2s; color:var(--gray-400); }
.todo-done-toggle[aria-expanded="true"] .todo-toggle-arrow { transform:rotate(180deg); }
.todo-clear-done { margin-right:1rem; background:none; border:none; color:var(--gray-400); font-size:0.78rem; font-weight:600; cursor:pointer; padding:0.35rem 0.5rem; border-radius:0.3rem; }
.todo-clear-done:hover { color:var(--red-600); background:var(--red-100); }
.todo-list { list-style:none; margin:0; padding:0; }
.todo-list--done { border-top:1px solid var(--gray-100); }
.todo-empty { padding:2rem 1.25rem; text-align:center; color:var(--gray-400); font-size:0.85rem; display:flex; flex-direction:column; align-items:center; gap:0.25rem; }
/* Item */
.todo-item { display:flex; align-items:flex-start; gap:0.55rem; padding:0.7rem 1rem 0.7rem 0.75rem; border-bottom:1px solid var(--gray-50); transition:background 0.1s; position:relative; border-left:3px solid transparent; }
.todo-item:last-child { border-bottom:none; }
.todo-item:hover { background:var(--gray-50); }
.todo-item.todo-dragging { opacity:0.35; box-shadow:0 4px 12px rgba(0,0,0,0.1); }
.todo-item.todo-drag-over { outline:2px dashed var(--todo-primary); outline-offset:-2px; background:var(--blue-50); }
.todo-item--done { opacity:0.7; }
.todo-item--overdue { background:var(--red-50); }
.todo-item--overdue:hover { background:var(--red-100); }
/* Prioritäts-Akzent (linke Kante) */
.todo-prio--1 { border-left-color:var(--blue-400); }
.todo-prio--2 { border-left-color:var(--amber-500); }
.todo-prio--3 { border-left-color:var(--red-600); }
.todo-drag { color:var(--gray-300); cursor:grab; flex-shrink:0; line-height:1; user-select:none; padding:0.25rem 0.1rem; display:flex; align-items:center; opacity:0; transition:opacity 0.15s; margin-top:0.15rem; }
.todo-drag:active { cursor:grabbing; }
.todo-item:hover .todo-drag { opacity:1; }
.todo-main { flex:1; display:flex; flex-direction:column; gap:0.3rem; min-width:0; padding-top:0.05rem; }
.todo-row1 { display:flex; align-items:flex-start; }
.todo-title { font-size:0.92rem; color:var(--gray-900); word-break:break-word; cursor:text; line-height:1.45; }
.todo-title--done { text-decoration:line-through; color:var(--gray-400); cursor:default; }
.todo-rename-input { padding:0.25rem 0.4rem; border:1.5px solid var(--todo-primary); border-radius:0.35rem; font-size:0.92rem; outline:none; box-shadow:0 0 0 3px rgba(var(--color-primary-rgb), 0.15); width:100%; }
/* Meta-Badges */
.todo-meta { display:flex; flex-wrap:wrap; gap:0.35rem; }
.todo-meta:empty { display:none; }
.todo-badge { display:inline-flex; align-items:center; gap:0.3rem; font-size:0.72rem; font-weight:600; padding:0.12rem 0.5rem; border-radius:0.3rem; color:var(--gray-500); background:var(--gray-100); }
.todo-badge-due { color:var(--todo-primary); background:var(--color-primary-pale); }
.todo-badge-due.todo-due--soon { color:var(--amber-600); background:var(--amber-100); }
.todo-badge-due.todo-due--today { color:var(--orange-600); background:var(--orange-100); }
.todo-badge-due.todo-due--overdue { color:var(--red-600); background:var(--red-100); animation:todoPulse 2s ease-in-out infinite; }
@keyframes todoPulse { 0%,100%{opacity:1} 50%{opacity:0.65} }
.todo-badge-recur { color:var(--violet-600); background:var(--violet-100); }
.todo-badge-cat { color:var(--teal-700); background:var(--teal-100); }
.todo-badge-subs { color:var(--slate-600); background:var(--slate-200); }
.todo-badge-subs.is-complete { color:var(--emerald-800); background:var(--emerald-100); }
.todo-badge-prio { background:transparent; padding-left:0; }
.todo-prio-dot { width:8px; height:8px; border-radius:50%; }
.todo-badge-prio--1 { color:var(--blue-600); } .todo-badge-prio--1 .todo-prio-dot { background:var(--blue-400); }
.todo-badge-prio--2 { color:var(--amber-700); } .todo-badge-prio--2 .todo-prio-dot { background:var(--amber-500); }
.todo-badge-prio--3 { color:var(--red-700); } .todo-badge-prio--3 .todo-prio-dot { background:var(--red-600); }
.todo-notes-view { font-size:0.78rem; color:var(--gray-500); white-space:pre-wrap; word-break:break-word; line-height:1.45; padding:0.3rem 0.55rem; background:var(--gray-50); border-radius:0.3rem; border-left:2px solid var(--gray-200); }
/* Editor-Panel */
.todo-editor { background:var(--gray-50); border:1px solid var(--slate-100); border-radius:0.4rem; padding:0.75rem; display:flex; flex-direction:column; gap:0.6rem; margin-top:0.2rem; }
.todo-editor textarea, .todo-editor input, .todo-editor select { font-family:inherit; font-size:0.83rem; border:1.5px solid var(--gray-200); border-radius:0.3rem; padding:0.4rem 0.5rem; outline:none; color:var(--gray-700); background:var(--color-bg); }
.todo-editor textarea:focus, .todo-editor input:focus, .todo-editor select:focus { border-color:var(--todo-primary); box-shadow:0 0 0 3px rgba(var(--color-primary-rgb), 0.12); }
.todo-editor textarea { resize:vertical; min-height:40px; }
.todo-editor-grid { display:grid; grid-template-columns:1fr 1fr; gap:0.5rem; }
.todo-f-label { display:flex; flex-direction:column; gap:0.2rem; font-size:0.72rem; font-weight:600; color:var(--gray-500); }
.todo-editor-actions { display:flex; gap:0.5rem; }
/* Unteraufgaben */
.todo-subs { margin-top:0.2rem; padding-left:0.1rem; }
.todo-sublist { list-style:none; margin:0 0 0.3rem; padding:0; display:flex; flex-direction:column; gap:0.1rem; }
.todo-sub { display:flex; align-items:center; gap:0.5rem; padding:0.25rem 0.3rem; border-radius:0.3rem; }
.todo-sub:hover { background:var(--gray-100); }
.todo-sub-title { flex:1; font-size:0.82rem; color:var(--gray-700); word-break:break-word; cursor:text; }
.todo-sub--done .todo-sub-title { text-decoration:line-through; color:var(--gray-400); }
.todo-subadd-input { width:100%; font-size:0.8rem; border:1px dashed var(--gray-300); border-radius:0.3rem; padding:0.35rem 0.5rem; outline:none; background:transparent; color:var(--gray-700); }
.todo-subadd-input:focus { border-style:solid; border-color:var(--todo-primary); background:var(--color-bg); }
/* Checkbox */
.todo-check-wrap { position:relative; flex-shrink:0; width:20px; height:20px; cursor:pointer; margin-top:0.15rem; }
.todo-check-wrap--sm { width:17px; height:17px; margin-top:0; }
.todo-checkbox { position:absolute; opacity:0; width:0; height:0; }
.todo-checkmark { display:block; width:20px; height:20px; border:2px solid var(--gray-300); border-radius:0.35rem; background:var(--color-bg); transition:all 0.15s; position:relative; }
.todo-check-wrap--sm .todo-checkmark { width:17px; height:17px; }
.todo-checkbox:checked + .todo-checkmark, .todo-checkmark.checked { background:var(--todo-primary); border-color:var(--todo-primary); }
.todo-checkmark.checked::after, .todo-checkbox:checked + .todo-checkmark::after {
content:''; position:absolute; left:4px; top:1px; width:5px; height:9px;
border:2px solid var(--color-on-accent); border-top:none; border-left:none; transform:rotate(45deg);
}
.todo-check-wrap--sm .todo-checkmark.checked::after { left:3px; top:0; width:4px; height:8px; }
/* Aktionen */
.todo-actions { display:flex; align-items:center; gap:0.1rem; flex-shrink:0; opacity:0; transition:opacity 0.15s; padding-top:0.05rem; }
.todo-item:hover .todo-actions, .todo-item--done .todo-actions { opacity:1; }
.todo-act { display:flex; align-items:center; justify-content:center; width:27px; height:27px; background:none; border:none; cursor:pointer; color:var(--slate-300); border-radius:0.35rem; transition:color 0.12s, background 0.12s; }
.todo-act:hover { color:var(--todo-primary); background:var(--color-primary-pale); }
.todo-act-del:hover { color:var(--red-600); background:var(--red-100); }
.todo-sub .todo-act-del { width:22px; height:22px; opacity:0; }
.todo-sub:hover .todo-act-del { opacity:1; }
.todo-done-date { font-size:0.7rem; color:var(--gray-400); white-space:nowrap; flex-shrink:0; align-self:center; }
@media(max-width:900px) { .todo-columns { grid-template-columns:1fr; } }
@media(max-width:640px) {
.todo-actions { opacity:1; }
.todo-drag { opacity:1; }
.todo-editor-grid { grid-template-columns:1fr; }
.todo-modal-grid { grid-template-columns:1fr; }
}
</style>
<script>
(function() {
'use strict';
var CSRF = (document.querySelector('meta[name="csrf-token"]') || {}).content || '';
function api(url, data, method) {
return fetch(url, {
method: method || 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': CSRF,
'X-Requested-With': 'XMLHttpRequest'
},
body: JSON.stringify(data || {})
}).then(function(r) { return r.json().catch(function() { return {}; }); });
}
function toast(msg) {
// Schlichtes, nicht-blockierendes Feedback
var t = document.createElement('div');
t.textContent = msg;
t.style.cssText = 'position:fixed;bottom:24px;left:50%;transform:translateX(-50%);background:#111827;color:#fff;padding:0.6rem 1.1rem;border-radius:0.4rem;font-size:0.85rem;z-index:9999;box-shadow:0 4px 16px rgba(0,0,0,0.25);opacity:0;transition:opacity 0.2s;';
document.body.appendChild(t);
requestAnimationFrame(function() { t.style.opacity = '1'; });
setTimeout(function() { t.style.opacity = '0'; setTimeout(function() { t.remove(); }, 250); }, 2600);
}
function htmlToEl(html) {
var tpl = document.createElement('template');
tpl.innerHTML = html.trim();
return tpl.content.firstElementChild;
}
function openList() { return document.getElementById('todoList'); }
function doneList() { return document.getElementById('todoDoneList'); }
function setKpi(id, val) {
var el = document.getElementById(id);
if (el) el.textContent = val;
}
// Zahl setzen und die Akzentfarbe der Kachel je nach Wert umschalten
// (data-on = Farbklasse bei Wert > 0, sonst bg-secondary).
function setKpiBox(valId, boxId, val) {
setKpi(valId, val);
var box = document.getElementById(boxId);
if (!box) return;
var onCls = box.dataset.on || '';
if (val > 0) { if (onCls) box.classList.add(onCls); box.classList.remove('bg-secondary'); }
else { if (onCls) box.classList.remove(onCls); box.classList.add('bg-secondary'); }
}
function refreshCounts() {
var openItems = openList() ? Array.prototype.slice.call(openList().querySelectorAll(':scope > .todo-item')) : [];
var open = openItems.length;
var done = doneList() ? doneList().querySelectorAll(':scope > .todo-item').length : 0;
var ob = document.querySelector('.open-count'); if (ob) ob.textContent = open;
var db = document.querySelector('.done-count'); if (db) db.textContent = done;
// KPI-Kacheln live nachziehen (Offen / Heute fällig / Überfällig),
// damit sie nach Abhaken/Anlegen/Löschen nicht bis zum Reload veralten.
var today = new Date().toISOString().slice(0, 10);
var todayN = 0, overdueN = 0;
openItems.forEach(function (li) {
var due = li.dataset.due || '';
if (!due) return;
if (due === today) todayN++;
else if (due < today) overdueN++;
});
setKpi('kpiTotal', open);
setKpiBox('kpiToday', 'kpiTodayBox', todayN);
setKpiBox('kpiOverdue', 'kpiOverdueBox', overdueN);
var empty = document.getElementById('todoEmpty');
if (empty) empty.hidden = open !== 0;
var doneCard = document.getElementById('todoDoneCard');
var doneEmpty = document.getElementById('todoDoneEmpty');
if (doneCard) doneCard.hidden = done === 0;
if (doneEmpty) doneEmpty.hidden = done !== 0;
}
function recountSubs(itemLi) {
if (!itemLi) return;
// Nur die direkten Unteraufgaben dieses Items zählen
var subList = itemLi.querySelector('.todo-sublist');
var subs = subList ? subList.querySelectorAll(':scope > .todo-sub') : [];
var done = subList ? subList.querySelectorAll(':scope > .todo-sub--done') : [];
var badge = itemLi.querySelector('.todo-badge-subs');
if (subs.length === 0) { if (badge) badge.remove(); return; }
var label = done.length + '/' + subs.length;
if (!badge) {
var meta = itemLi.querySelector('.todo-meta');
if (!meta) {
meta = htmlToEl('<div class="todo-meta"></div>');
var row1 = itemLi.querySelector('.todo-row1');
row1.parentNode.insertBefore(meta, row1.nextSibling);
}
badge = htmlToEl('<span class="todo-badge todo-badge-subs"><svg viewBox="0 0 16 16" width="10" height="10" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="2,8 6,12 14,4"/></svg> ' + label + '</span>');
meta.appendChild(badge);
} else {
badge.childNodes[badge.childNodes.length - 1].nodeValue = ' ' + label;
}
badge.classList.toggle('is-complete', done.length === subs.length);
}
var Todo = {};
// ── Neue Aufgabe ───────────────────────────────────────────────────────
Todo.add = function(e) {
e.preventDefault();
var title = document.getElementById('todoInput').value.trim();
if (!title) return;
var payload = {
title: title,
due_date: document.getElementById('todoAddDue').value || null,
due_time: document.getElementById('todoAddTime').value || null,
priority: parseInt(document.getElementById('todoAddPrio').value, 10) || 0,
category: document.getElementById('todoAddCat').value.trim() || null,
recurrence: document.getElementById('todoAddRecur').value,
remind_lead_days: parseInt(document.getElementById('todoAddLead').value, 10) || 0
};
api('/todos', payload).then(function(res) {
if (res.error) { toast(res.error); return; }
var li = htmlToEl(res.html);
openList().appendChild(li);
initDrag(li);
refreshCounts();
// Formular zurücksetzen und Dialog schließen
document.getElementById('todoAddForm').reset();
document.getElementById('todoAddLead').value = '1';
Todo.closeAddModal();
});
};
// ── Modal „Neue Aufgabe" ──────────────────────────────────────────────────
Todo.openAddModal = function() {
var m = document.getElementById('todoAddModal');
if (!m) return;
m.style.display = 'flex';
var i = document.getElementById('todoInput');
if (i) i.focus();
};
Todo.closeAddModal = function() {
var m = document.getElementById('todoAddModal');
if (m) m.style.display = 'none';
};
// ── Erledigt / offen ───────────────────────────────────────────────────
Todo.toggleDone = function(cb) {
var li = cb.closest('.todo-item, .todo-sub');
var id = li.dataset.id;
var isSub = li.classList.contains('todo-sub');
api('/todos/' + id + '/done', {}).then(function(res) {
if (res.error) { toast(res.error); cb.checked = !cb.checked; return; }
if (isSub) {
li.classList.toggle('todo-sub--done', res.done);
var mark = li.querySelector('.todo-checkmark');
if (mark) mark.classList.toggle('checked', res.done);
recountSubs(cb.closest('.todo-item'));
return;
}
// Top-Level: serverseitig gerendertes HTML einsetzen
li.remove();
if (res.html) {
var newLi = htmlToEl(res.html);
if (res.done) {
if (doneList()) doneList().insertBefore(newLi, doneList().firstChild);
} else {
openList().appendChild(newLi);
initDrag(newLi);
}
}
// Wiederkehrende Aufgabe: nächste Instanz einfügen
if (res.next_html) {
var next = htmlToEl(res.next_html);
openList().appendChild(next);
initDrag(next);
toast('Nächste Wiederholung angelegt.');
}
refreshCounts();
});
};
Todo.toggleDoneSection = function(btn) {
var expanded = btn.getAttribute('aria-expanded') === 'true';
btn.setAttribute('aria-expanded', String(!expanded));
if (doneList()) doneList().hidden = expanded;
};
Todo.clearDone = function() {
if (!confirm('Alle erledigten Aufgaben endgültig löschen?')) return;
api('/todos/clear-done', {}).then(function(res) {
if (res.error) { toast(res.error); return; }
if (doneList()) doneList().querySelectorAll(':scope > .todo-item').forEach(function(el) { el.remove(); });
refreshCounts();
toast(res.count + ' Aufgabe(n) gelöscht.');
});
};
// ── Umbenennen ─────────────────────────────────────────────────────────
Todo.startRename = function(span) {
var li = span.closest('.todo-item, .todo-sub');
var input = li.querySelector('.todo-rename-input');
input.value = span.textContent;
span.hidden = true;
input.hidden = false;
input.focus();
input.select();
};
Todo.renameKey = function(e, input) {
if (e.key === 'Enter') { e.preventDefault(); input.blur(); }
if (e.key === 'Escape') { input.dataset.cancel = '1'; input.blur(); }
};
Todo.saveRename = function(input) {
var li = input.closest('.todo-item, .todo-sub');
var span = li.querySelector('.todo-title, .todo-sub-title');
if (input.dataset.cancel === '1') {
delete input.dataset.cancel;
span.hidden = false; input.hidden = true;
return;
}
var title = input.value.trim();
if (!title || title === span.textContent) {
span.hidden = false; input.hidden = true;
return;
}
api('/todos/' + li.dataset.id, { title: title }).then(function(res) {
if (res.error) { toast(res.error); span.hidden = false; input.hidden = true; return; }
span.textContent = title;
span.hidden = false; input.hidden = true;
});
};
// ── Editor-Panel ───────────────────────────────────────────────────────
Todo.toggleEditor = function(btn) {
var li = btn.closest('.todo-item');
var ed = li.querySelector('.todo-editor');
if (ed) ed.hidden = !ed.hidden;
if (ed && !ed.hidden) { var f = ed.querySelector('textarea'); if (f) f.focus(); }
};
Todo.saveEditor = function(btn) {
var li = btn.closest('.todo-item');
var ed = li.querySelector('.todo-editor');
var payload = {
notes: ed.querySelector('.todo-f-notes').value,
priority: parseInt(ed.querySelector('.todo-f-priority').value, 10) || 0,
category: ed.querySelector('.todo-f-category').value.trim() || null,
due_date: ed.querySelector('.todo-f-due').value || null,
due_time: ed.querySelector('.todo-f-time').value || null,
recurrence: ed.querySelector('.todo-f-recurrence').value,
remind_lead_days: parseInt(ed.querySelector('.todo-f-lead').value, 10) || 0
};
api('/todos/' + li.dataset.id, payload).then(function(res) {
if (res.error) { toast(res.error); return; }
if (res.html) {
var newLi = htmlToEl(res.html);
li.replaceWith(newLi);
initDrag(newLi);
}
});
};
// ── Unteraufgaben ──────────────────────────────────────────────────────
Todo.toggleSubs = function(btn) {
var subs = btn.closest('.todo-item').querySelector('.todo-subs');
if (!subs) return;
subs.hidden = !subs.hidden;
if (!subs.hidden) { var inp = subs.querySelector('.todo-subadd-input'); if (inp) inp.focus(); }
};
Todo.addSub = function(e, form) {
e.preventDefault();
var li = form.closest('.todo-item');
var input = form.querySelector('.todo-subadd-input');
var title = input.value.trim();
if (!title) return;
api('/todos', { title: title, parent_id: parseInt(li.dataset.id, 10) }).then(function(res) {
if (res.error) { toast(res.error); return; }
var sub = htmlToEl(res.html);
form.closest('.todo-subs').querySelector('.todo-sublist').appendChild(sub);
input.value = '';
input.focus();
recountSubs(li);
});
};
// ── Löschen ────────────────────────────────────────────────────────────
Todo.del = function(btn) {
var li = btn.closest('.todo-item, .todo-sub');
var isSub = li.classList.contains('todo-sub');
if (!isSub && !confirm('Aufgabe wirklich löschen?')) return;
var parent = isSub ? li.closest('.todo-item') : null;
api('/todos/' + li.dataset.id + '/delete', {}).then(function(res) {
if (res.error) { toast(res.error); return; }
li.remove();
if (isSub) recountSubs(parent); else refreshCounts();
});
};
// ── Drag & Drop (nur offene Top-Level-Items) ───────────────────────────
var dragEl = null;
function initDrag(li) {
if (li.dataset.dragInit) return;
li.dataset.dragInit = '1';
li.addEventListener('dragstart', function(e) {
// Drag nur über das Handle starten (Inputs/Buttons nicht blockieren)
dragEl = li; li.classList.add('todo-dragging');
e.dataTransfer.effectAllowed = 'move';
});
li.addEventListener('dragend', function() {
li.classList.remove('todo-dragging');
document.querySelectorAll('.todo-drag-over').forEach(function(el) { el.classList.remove('todo-drag-over'); });
dragEl = null;
saveOrder();
});
li.addEventListener('dragover', function(e) {
e.preventDefault();
if (dragEl && dragEl !== li) li.classList.add('todo-drag-over');
});
li.addEventListener('dragleave', function() { li.classList.remove('todo-drag-over'); });
li.addEventListener('drop', function(e) {
e.preventDefault();
li.classList.remove('todo-drag-over');
if (dragEl && dragEl !== li) {
var items = Array.from(li.parentNode.querySelectorAll(':scope > .todo-item'));
var from = items.indexOf(dragEl), to = items.indexOf(li);
if (from < to) li.parentNode.insertBefore(dragEl, li.nextSibling);
else li.parentNode.insertBefore(dragEl, li);
}
});
}
function saveOrder() {
var ids = Array.from(openList().querySelectorAll(':scope > .todo-item')).map(function(li) {
return parseInt(li.dataset.id, 10);
});
api('/todos/reorder', { ids: ids });
}
// Init
if (openList()) openList().querySelectorAll(':scope > .todo-item').forEach(initDrag);
refreshCounts();
// Modal schließen: Klick auf den Backdrop oder Escape
var addModal = document.getElementById('todoAddModal');
if (addModal) {
addModal.addEventListener('click', function(e) {
if (e.target === addModal) Todo.closeAddModal();
});
}
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape' && addModal && addModal.style.display !== 'none') {
Todo.closeAddModal();
}
});
window.Todo = Todo;
})();
</script>