OpenNIT

Quelltext des aktuellen Standes

Fassung 1.1.1

index.php

38 KB
<?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 &hellip;') ?>">
                    </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') ?>">&times;</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>