Quelltext des aktuellen Standes
<?php $this->layout('layouts/main', ['page_title' => $page_title, 'current_route' => $current_route, 'full_width' => true]); ?>
<style>
/* ── Kanban Board – Modernes Design ─────────────────────────────── */
:root {
--kb-bg: var(--color-gray-100);
--kb-col-w: 300px;
--kb-primary: var(--color-primary);
--kb-primary-dk: var(--color-primary-dark);
--kb-dark: var(--color-gray-800);
--kb-border: rgba(0,0,0,.1);
--kb-shadow: 0 1px 3px rgba(0,0,0,.12), 0 0 1px rgba(0,0,0,.08);
--kb-radius: 0.375rem;
--kb-prio-low: var(--bs-teal);
--kb-prio-normal: var(--color-primary);
--kb-prio-high: var(--bs-orange);
--kb-prio-urgent: var(--bs-danger);
}
/* ── Page wrapper ────────────────────────────────────────────────── */
.kb-page {
display: flex; flex-direction: column;
height: calc(100vh - 57px);
}
/* ── Wrapper ─────────────────────────────────────────────────────── */
.kb-wrap {
display: flex; flex-direction: column;
flex: 1; min-height: 0;
background: var(--kb-bg);
}
/* ── Filterbar ───────────────────────────────────────────────────── */
.kb-filterbar {
display: flex; align-items: center; gap: .5rem; flex-wrap: wrap;
padding: .625rem 1rem;
background: var(--color-bg);
border-bottom: 1px solid var(--color-gray-300);
flex-shrink: 0;
}
.kb-search {
padding: .3rem .65rem; border: 1px solid var(--color-gray-400); border-radius: var(--kb-radius);
font-size: .82rem; width: 180px; outline: none; transition: border-color .15s;
}
.kb-search:focus { border-color: var(--kb-primary); box-shadow: 0 0 0 2px rgba(var(--color-primary-rgb), .15); }
.kb-flt-select {
padding: .3rem .6rem; border: 1px solid var(--color-gray-400); border-radius: var(--kb-radius);
font-size: .82rem; background: var(--color-bg); color: var(--color-gray-700); cursor: pointer; outline: none;
}
.kb-flt-select:focus { border-color: var(--kb-primary); }
.kb-flt-label { display: flex; align-items: center; gap: .3rem; font-size: .82rem; color: var(--color-gray-700); cursor: pointer; white-space: nowrap; }
/* ── Board Scroll Area ───────────────────────────────────────────── */
.kb-scroll {
flex: 1; overflow-x: auto; overflow-y: hidden;
padding: 1rem;
}
.kb-scroll::-webkit-scrollbar { height: 8px; }
.kb-scroll::-webkit-scrollbar-thumb { background: var(--color-gray-400); border-radius: 4px; }
.kb-board { display: flex; gap: .875rem; height: 100%; align-items: flex-start; }
/* ── Spalte ──────────────────────────────────────────────────────── */
.kb-col {
flex: 0 0 var(--kb-col-w);
background: var(--color-bg);
border-radius: var(--kb-radius);
box-shadow: var(--kb-shadow);
border: 1px solid var(--kb-border);
display: flex; flex-direction: column;
height: 100%; max-height: 100%;
overflow: hidden;
}
.kb-col-top {
height: 4px; border-radius: var(--kb-radius) var(--kb-radius) 0 0; flex-shrink: 0;
}
.kb-col-hd {
padding: .625rem .875rem .5rem;
display: flex; align-items: center; gap: .4rem;
flex-shrink: 0;
}
.kb-col-name {
flex: 1; font-size: .875rem; font-weight: 700; color: var(--kb-dark);
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.kb-col-count {
background: var(--color-gray-200); color: var(--color-secondary);
border-radius: 9999px; font-size: .7rem; font-weight: 700;
padding: .1rem .5rem; flex-shrink: 0;
}
.kb-col-count.wip-warn { background: var(--alert-danger-bg); color: var(--alert-danger-text); }
.kb-col-wip {
font-size: .68rem; color: var(--color-secondary); flex-shrink: 0; white-space: nowrap;
}
.kb-col-menu-btn {
background: none; border: none; cursor: pointer; color: var(--color-gray-500);
padding: .15rem .3rem; border-radius: .2rem; font-size: 1rem; line-height: 1;
transition: all .15s; flex-shrink: 0;
}
.kb-col-menu-btn:hover { background: var(--color-gray-50); color: var(--color-gray-700); }
.kb-col-quick-add-btn {
width: calc(100% - 1rem); margin: 0 .5rem .5rem;
padding: .35rem .5rem; background: none;
border: 1px dashed var(--color-gray-400); border-radius: .25rem;
font-size: .8rem; color: var(--color-secondary); cursor: pointer; text-align: left;
transition: all .15s; flex-shrink: 0;
}
.kb-col-quick-add-btn:hover { background: var(--color-gray-50); border-color: var(--color-gray-500); color: var(--color-gray-800); }
/* ── Task-Liste ──────────────────────────────────────────────────── */
.kb-col-body {
flex: 1; overflow-y: auto; padding: .375rem .5rem;
display: flex; flex-direction: column; gap: .375rem;
min-height: 48px;
}
.kb-col-body::-webkit-scrollbar { width: 4px; }
.kb-col-body::-webkit-scrollbar-thumb { background: var(--color-gray-300); border-radius: 2px; }
.kb-col.drag-over .kb-col-body {
background: rgba(var(--color-primary-rgb), .07);
outline: 2px dashed var(--kb-primary);
outline-offset: -2px; border-radius: .2rem;
}
.kb-card.drag-target-before {
border-top: 2px solid var(--kb-primary);
margin-top: 2px;
}
/* ── Task-Karte ──────────────────────────────────────────────────── */
.kb-card {
background: var(--color-bg); border-radius: .3rem;
border: 1px solid rgba(0,0,0,.09);
border-left-width: 4px;
padding: .55rem .65rem .5rem;
cursor: pointer; user-select: none;
box-shadow: 0 1px 2px rgba(0,0,0,.06);
transition: box-shadow .15s, border-color .15s, opacity .2s;
position: relative;
}
.kb-card:hover { box-shadow: 0 2px 6px rgba(0,0,0,.12); border-top-color: rgba(0,0,0,.15); }
.kb-card:active { cursor: grabbing; }
.kb-card.dragging { opacity: .35; }
.kb-card--done { opacity: .55; }
.kb-card--done .kb-card-title { text-decoration: line-through; color: var(--color-secondary); }
.kb-card--hidden { display: none; }
.kb-prio-low { border-left-color: var(--kb-prio-low); }
.kb-prio-normal { border-left-color: var(--kb-prio-normal); }
.kb-prio-high { border-left-color: var(--kb-prio-high); }
.kb-prio-urgent { border-left-color: var(--kb-prio-urgent); }
.kb-card-hd { display: flex; align-items: flex-start; gap: .4rem; margin-bottom: .3rem; }
.kb-card-title { flex: 1; font-size: .84rem; font-weight: 600; color: var(--color-gray-900); line-height: 1.35; }
.kb-done-btn {
flex-shrink: 0; width: 1.15rem; height: 1.15rem; border-radius: 50%;
border: 2px solid var(--color-gray-400); background: var(--color-bg); cursor: pointer;
display: flex; align-items: center; justify-content: center;
font-size: .6rem; color: transparent; transition: all .15s; padding: 0;
margin-top: .1rem;
}
.kb-done-btn:hover { border-color: var(--bs-success); color: var(--bs-success); }
.kb-done-btn--on { border-color: var(--bs-success); background: var(--bs-success); color: var(--color-on-accent); }
.kb-done-btn--on:hover { border-color: var(--bs-success-dark); background: var(--bs-success-dark); }
.kb-card-meta { display: flex; align-items: center; gap: .3rem; flex-wrap: wrap; }
.kb-label { display: inline-block; padding: .1rem .45rem; border-radius: 9999px; font-size: .68rem; font-weight: 700; color: var(--color-on-accent); }
.kb-prio-badge { font-size: .68rem; font-weight: 700; padding: .1rem .4rem; border-radius: 9999px; }
.prio-low-b { background: var(--alert-success-bg); color: var(--alert-success-text); }
.prio-normal-b { background: var(--alert-info-bg); color: var(--alert-info-text); }
.prio-high-b { background: var(--alert-warning-bg); color: var(--alert-warning-text); }
.prio-urgent-b { background: var(--alert-danger-bg); color: var(--alert-danger-text); }
.kb-due { font-size: .7rem; color: var(--color-secondary); }
.kb-due.overdue { color: var(--bs-danger); font-weight: 700; }
.kb-avatars { display: flex; gap: .15rem; margin-left: auto; }
.kb-avatar {
width: 1.4rem; height: 1.4rem; border-radius: 50%;
display: flex; align-items: center; justify-content: center;
font-size: .58rem; font-weight: 700; color: var(--color-on-accent);
border: 1.5px solid var(--color-bg); flex-shrink: 0;
}
/* ── Quick-Add Form ──────────────────────────────────────────────── */
.kb-quick-form {
display: none; padding: 0 .5rem .5rem; flex-shrink: 0;
}
.kb-quick-form.open { display: block; }
.kb-quick-input {
width: 100%; padding: .35rem .5rem; border: 1px solid var(--kb-primary);
border-radius: .25rem; font-size: .82rem; box-sizing: border-box;
margin-bottom: .3rem; outline: none; font-family: inherit;
}
.kb-quick-row { display: flex; gap: .3rem; }
/* ── Neue Spalte Button ──────────────────────────────────────────── */
.kb-add-col {
flex: 0 0 240px;
background: rgba(255,255,255,.6); border: 2px dashed var(--color-gray-400);
border-radius: var(--kb-radius); cursor: pointer;
display: flex; align-items: center; justify-content: center;
min-height: 80px; font-size: .85rem; color: var(--color-secondary); font-weight: 600;
transition: all .15s; align-self: flex-start; padding: 1rem;
}
.kb-add-col:hover { background: rgba(255,255,255,.9); border-color: var(--color-gray-500); color: var(--color-gray-800); }
/* ── Modals: Basis kommt aus app.css (.modal-overlay/.modal-card),
hier nur Zustands-Toggle und Kanban-spezifische Maße ── */
.kb-overlay { display: none; z-index: 9000; }
.kb-overlay.open { display: flex; }
#kb-col-overlay .modal-card { max-width: 380px; }
.kb-overlay textarea.form-control { resize: vertical; }
.kb-overlay .modal-body > .form-group:last-child { margin-bottom: 0; }
.kb-modal-ft-left { margin-right: auto; }
.kb-two-col { display: grid; grid-template-columns: 1fr 1fr; gap: 0 .75rem; }
.kb-assignee-list {
display: flex; flex-wrap: wrap; gap: .3rem;
max-height: 110px; overflow-y: auto;
border: 1px solid var(--color-gray-300); border-radius: .25rem; padding: .4rem;
}
.kb-assignee-item { display: flex; align-items: center; gap: .3rem; font-size: .8rem; white-space: nowrap; }
.kb-color-swatches { display: flex; gap: .4rem; flex-wrap: wrap; }
.kb-swatch {
width: 1.6rem; height: 1.6rem; border-radius: 50%; cursor: pointer;
border: 2px solid transparent; transition: transform .1s;
}
.kb-swatch:hover { transform: scale(1.15); }
.kb-swatch.active { border-color: var(--kb-dark); }
/* ── Responsive ──────────────────────────────────────────────────── */
@media (max-width: 640px) {
.kb-search { width: 120px; }
}
</style>
<?php
// Tasks nach Spalte gruppieren
$tasksByCol = [];
foreach ($tasks as $t) {
$tasksByCol[(int)$t['column_id']][] = $t;
}
// Avatar-Farben (Palette)
$avatarColors = ['#3c8dbc','#00a65a','#dd4b39','#f39c12','#6f42c1','#e83e8c','#20c997','#fd7e14'];
function kbAvatarColor(int $uid, array $colors): string {
return $colors[$uid % count($colors)];
}
function kbInitials(string $name): string {
$parts = preg_split('/\s+/', trim($name));
if (count($parts) >= 2) return mb_strtoupper(mb_substr($parts[0],0,1) . mb_substr(end($parts),0,1));
return mb_strtoupper(mb_substr($name,0,2));
}
$today = date('Y-m-d');
?>
<div class="kb-page">
<div class="content-header">
<div>
<h1><?= $this->te('Kanban') ?></h1>
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="/"><?= $this->te('Home') ?></a></li>
<li class="breadcrumb-item active"><?= $this->te('Kanban') ?></li>
</ol>
</div>
<div class="content-header-actions">
<?php if ($can_manage): ?>
<a href="/kanban/categories" class="btn btn-default"><?= $this->te('Labels') ?></a>
<button class="btn btn-default" onclick="kbOpenColModal(null)"><?= $this->te('+ Spalte') ?></button>
<button class="btn btn-primary" onclick="kbOpenTaskModal({})">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="14" height="14"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
<?= $this->te('Aufgabe') ?>
</button>
<?php endif; ?>
</div>
</div>
<div class="kb-wrap">
<!-- ── Filterbar ────────────────────────────────────────────────── -->
<div class="kb-filterbar">
<input type="search" class="kb-search" id="kb-search" placeholder="<?= $this->te('Suchen...') ?>" oninput="kbFilter()">
<select class="kb-flt-select" id="kb-flt-prio" onchange="kbFilter()">
<option value=""><?= $this->te('Alle Prioritäten') ?></option>
<option value="urgent"><?= $this->te('Dringend') ?></option>
<option value="high"><?= $this->te('Hoch') ?></option>
<option value="normal"><?= $this->te('Normal') ?></option>
<option value="low"><?= $this->te('Niedrig') ?></option>
</select>
<select class="kb-flt-select" id="kb-flt-cat" onchange="kbFilter()">
<option value="0"><?= $this->te('Alle Labels') ?></option>
<?php foreach ($categories as $cat): ?>
<option value="<?= (int)$cat['id'] ?>"><?= htmlspecialchars($cat['name']) ?></option>
<?php endforeach; ?>
</select>
<label class="kb-flt-label">
<input type="checkbox" id="kb-flt-open" onchange="kbFilter()"> <?= $this->te('Nur offen') ?>
</label>
</div>
<!-- ── Board ───────────────────────────────────────────────────── -->
<div class="kb-scroll">
<div class="kb-board" id="kb-board">
<?php foreach ($columns as $col):
$colId = (int)$col['id'];
$colColor = htmlspecialchars($col['color'] ?: '#6366f1');
$colName = htmlspecialchars($col['name']);
$wipLimit = $col['wip_limit'] ? (int)$col['wip_limit'] : null;
$colTasks = $tasksByCol[$colId] ?? [];
$taskCount = count($colTasks);
$wipWarn = $wipLimit !== null && $taskCount > $wipLimit;
?>
<div class="kb-col" id="kb-col-<?= $colId ?>"
data-col-id="<?= $colId ?>"
ondragover="kbDragOver(event,this)"
ondragleave="kbDragLeave(this)"
ondrop="kbDrop(event,this)">
<div class="kb-col-top" style="background:<?= $colColor ?>"></div>
<div class="kb-col-hd">
<span class="kb-col-name" title="<?= $colName ?>"><?= $colName ?></span>
<span class="kb-col-count <?= $wipWarn ? 'wip-warn' : '' ?>" id="kb-count-<?= $colId ?>"><?= $taskCount ?></span>
<?php if ($wipLimit): ?>
<span class="kb-col-wip">/<?= $wipLimit ?></span>
<?php endif; ?>
<?php if ($can_manage): ?>
<button class="kb-col-menu-btn" title="<?= $this->te('Spalte bearbeiten') ?>"
onclick="kbOpenColModal(<?= htmlspecialchars(json_encode(['id'=>$col['id'],'name'=>$col['name'],'color'=>$col['color'],'wip_limit'=>$col['wip_limit']])) ?>)">⋮</button>
<?php endif; ?>
</div>
<div class="kb-col-body" id="kb-body-<?= $colId ?>">
<?php foreach ($colTasks as $task):
$tid = (int)$task['id'];
$prio = htmlspecialchars($task['priority'] ?? 'normal');
$isDone = (bool)($task['done'] ?? false);
$dueDate = $task['due_date'] ?? '';
$isOverdue = $dueDate && $dueDate < $today && !$isDone;
$dueFmt = $dueDate ? date('d.m.Y', strtotime($dueDate)) : '';
$assigneeNames = $task['assignee_names'] ? explode('|||', $task['assignee_names']) : [];
$assigneeIds = $task['assignee_ids'] ? explode(',', $task['assignee_ids']) : [];
$taskJson = htmlspecialchars(json_encode([
'id' => $task['id'],
'title' => $task['title'],
'description' => $task['description'] ?? '',
'column_id' => $task['column_id'],
'category_id' => $task['category_id'],
'priority' => $task['priority'],
'due_date' => $task['due_date'] ?? '',
'done' => (bool)$task['done'],
'assignee_ids'=> $assigneeIds,
]), ENT_QUOTES);
?>
<div class="kb-card kb-prio-<?= $prio ?> <?= $isDone ? 'kb-card--done' : '' ?>"
id="kb-task-<?= $tid ?>"
data-task-id="<?= $tid ?>"
data-col-id="<?= $colId ?>"
data-prio="<?= $prio ?>"
data-cat="<?= (int)($task['category_id'] ?? 0) ?>"
data-done="<?= $isDone ? '1' : '0' ?>"
data-title="<?= htmlspecialchars(strtolower($task['title'])) ?>"
draggable="true"
ondragstart="kbDragStart(event,this)"
ondragend="kbDragEnd(this)"
onclick="kbOpenTaskModal(<?= $taskJson ?>)">
<div class="kb-card-hd">
<?php if (true): // always show done btn ?>
<button class="kb-done-btn <?= $isDone ? 'kb-done-btn--on' : '' ?>"
data-id="<?= $tid ?>" title="<?= $isDone ? 'Als offen markieren' : 'Erledigt' ?>"
onclick="event.stopPropagation();kbToggleDone(<?= $tid ?>)">✓</button>
<?php endif; ?>
<span class="kb-card-title"><?= htmlspecialchars($task['title']) ?></span>
</div>
<div class="kb-card-meta">
<?php if (!empty($task['category_name'])): ?>
<span class="kb-label" style="background:<?= htmlspecialchars($task['category_color'] ?: 'var(--indigo-500)') ?>"><?= htmlspecialchars($task['category_name']) ?></span>
<?php endif; ?>
<span class="kb-prio-badge prio-<?= $this->e($prio) ?>-b"><?php
$prioLabels = ['low'=>'Niedrig','normal'=>'Normal','high'=>'Hoch','urgent'=>'Dringend'];
echo $prioLabels[$prio] ?? $this->e($prio);
?></span>
<?php if ($dueFmt): ?>
<span class="kb-due <?= $isOverdue ? 'overdue' : '' ?>">⏱ <?= $dueFmt ?></span>
<?php endif; ?>
<?php if (!empty($assigneeNames)): ?>
<span class="kb-avatars">
<?php foreach (array_slice($assigneeNames, 0, 3) as $ai => $aname): ?>
<span class="kb-avatar" style="background:<?= kbAvatarColor((int)($assigneeIds[$ai] ?? $ai), $avatarColors) ?>" title="<?= htmlspecialchars($aname) ?>"><?= kbInitials($aname) ?></span>
<?php endforeach; ?>
<?php if (count($assigneeNames) > 3): ?>
<span class="kb-avatar" style="background:var(--color-secondary)">+<?= count($assigneeNames)-3 ?></span>
<?php endif; ?>
</span>
<?php endif; ?>
</div>
</div>
<?php endforeach; ?>
</div><!-- /.kb-col-body -->
<?php if ($can_manage): ?>
<div class="kb-quick-form" id="kb-qf-<?= $colId ?>">
<input class="kb-quick-input" id="kb-qi-<?= $colId ?>" type="text" placeholder="<?= $this->te('Aufgabe hinzufügen…') ?>"
onkeydown="if(event.key==='Enter')kbQuickSubmit(<?= $colId ?>);if(event.key==='Escape')kbQuickClose(<?= $colId ?>)">
<div class="kb-quick-row">
<button class="btn btn-primary btn-sm" onclick="kbQuickSubmit(<?= $colId ?>)"><?= $this->te('Hinzufügen') ?></button>
<button class="btn btn-default btn-sm" onclick="kbQuickClose(<?= $colId ?>)"><?= $this->te('Abbrechen') ?></button>
</div>
</div>
<button class="kb-col-quick-add-btn" id="kb-qb-<?= $colId ?>" onclick="kbQuickOpen(<?= $colId ?>)"><?= $this->te('+ Aufgabe hinzufügen') ?></button>
<?php endif; ?>
</div><!-- /.kb-col -->
<?php endforeach; ?>
<?php if ($can_manage): ?>
<div class="kb-add-col" onclick="kbOpenColModal(null)"><?= $this->te('+ Neue Spalte') ?></div>
<?php endif; ?>
</div><!-- /#kb-board -->
</div><!-- /.kb-scroll -->
</div><!-- /.kb-wrap -->
</div><!-- /.kb-page -->
<!-- ── Task-Modal ─────────────────────────────────────────────────── -->
<div class="modal-overlay kb-overlay" id="kb-task-overlay" onclick="if(event.target===this)kbCloseTaskModal()">
<div class="modal-card" role="dialog" aria-modal="true">
<div class="modal-header">
<h2 id="kb-modal-title-lbl"><?= $this->te('Aufgabe') ?></h2>
<button aria-label="<?= $this->te('Schließen') ?>" type="button" class="modal-close" onclick="kbCloseTaskModal()">×</button>
</div>
<div class="modal-body">
<input type="hidden" id="kb-modal-id">
<div class="form-group">
<label for="kb-modal-title"><?= $this->te('Titel') ?> <span class="text-danger">*</span></label>
<input class="form-control" id="kb-modal-title" type="text" placeholder="<?= $this->te('Aufgabentitel') ?>" maxlength="500">
</div>
<div class="form-group">
<label for="kb-modal-desc"><?= $this->te('Beschreibung') ?></label>
<textarea class="form-control" id="kb-modal-desc" placeholder="<?= $this->te('Optional…') ?>" rows="4"></textarea>
</div>
<div class="kb-two-col">
<div class="form-group">
<label for="kb-modal-col"><?= $this->te('Spalte') ?></label>
<select class="form-control" id="kb-modal-col">
<?php foreach ($columns as $col): ?>
<option value="<?= (int)$col['id'] ?>"><?= htmlspecialchars($col['name']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="form-group">
<label for="kb-modal-prio"><?= $this->te('Priorität') ?></label>
<select class="form-control" id="kb-modal-prio">
<option value="low"><?= $this->te('Niedrig') ?></option>
<option value="normal"><?= $this->te('Normal') ?></option>
<option value="high"><?= $this->te('Hoch') ?></option>
<option value="urgent"><?= $this->te('Dringend') ?></option>
</select>
</div>
</div>
<div class="kb-two-col">
<div class="form-group">
<label for="kb-modal-cat"><?= $this->te('Label') ?></label>
<select class="form-control" id="kb-modal-cat">
<option value=""><?= $this->te('— kein Label —') ?></option>
<?php foreach ($categories as $cat): ?>
<option value="<?= (int)$cat['id'] ?>"><?= htmlspecialchars($cat['name']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="form-group">
<label for="kb-modal-due"><?= $this->te('Fälligkeitsdatum') ?></label>
<input class="form-control" id="kb-modal-due" type="date">
</div>
</div>
<?php if (!empty($users)): ?>
<div class="form-group">
<label><?= $this->te('Bearbeiter') ?></label>
<div class="kb-assignee-list">
<?php foreach ($users as $u): ?>
<label class="kb-assignee-item">
<input type="checkbox" class="kb-modal-assignee" value="<?= (int)$u['id'] ?>">
<?= htmlspecialchars($u['display_name']) ?>
</label>
<?php endforeach; ?>
</div>
</div>
<?php endif; ?>
</div>
<div class="modal-footer">
<?php if ($can_manage): ?>
<div class="kb-modal-ft-left">
<button type="button" class="btn btn-danger btn-sm" id="kb-modal-del-btn" onclick="kbDeleteTask()" style="display:none"><?= $this->te('🗑 Löschen') ?></button>
</div>
<button type="button" class="btn btn-default" onclick="kbCloseTaskModal()"><?= $this->te('Abbrechen') ?></button>
<button type="button" class="btn btn-primary" id="kb-modal-save-btn" onclick="kbSaveTask()"><?= $this->te('Speichern') ?></button>
<?php else: ?>
<button type="button" class="btn btn-default" onclick="kbCloseTaskModal()"><?= $this->te('Schließen') ?></button>
<?php endif; ?>
</div>
</div>
</div>
<!-- ── Spalten-Modal ───────────────────────────────────────────────── -->
<?php if ($can_manage): ?>
<div class="modal-overlay kb-overlay" id="kb-col-overlay" onclick="if(event.target===this)kbCloseColModal()">
<div class="modal-card" role="dialog" aria-modal="true">
<div class="modal-header">
<h2 id="kb-col-modal-title"><?= $this->te('Spalte') ?></h2>
<button aria-label="<?= $this->te('Schließen') ?>" type="button" class="modal-close" onclick="kbCloseColModal()">×</button>
</div>
<div class="modal-body">
<input type="hidden" id="kb-col-id">
<div class="form-group">
<label for="kb-col-name-inp"><?= $this->te('Name') ?> <span class="text-danger">*</span></label>
<input class="form-control" id="kb-col-name-inp" type="text" maxlength="255">
</div>
<div class="form-group">
<label><?= $this->te('Farbe') ?></label>
<div class="kb-color-swatches" id="kb-col-swatches">
<?php foreach (['#6c757d','#3c8dbc','#00a65a','#fd7e14','#dc3545','#f39c12','#6f42c1','#e83e8c','#20c997','#17a2b8','#343a40','#6366f1'] as $sc): ?>
<span class="kb-swatch" style="background:<?= $sc ?>" data-color="<?= $sc ?>" onclick="kbSelectColor('<?= $sc ?>')"></span>
<?php endforeach; ?>
</div>
<input type="hidden" id="kb-col-color" value="#6c757d">
</div>
<div class="form-group">
<label for="kb-col-wip"><?= $this->te('WIP-Limit') ?></label>
<input class="form-control" id="kb-col-wip" type="number" min="1" max="999" placeholder="z.B. 5">
<span class="form-hint"><?= $this->te('Leer = unbegrenzt.') ?></span>
</div>
</div>
<div class="modal-footer">
<div class="kb-modal-ft-left">
<button type="button" class="btn btn-danger btn-sm" id="kb-col-del-btn" onclick="kbDeleteColumn()" style="display:none"><?= $this->te('🗑 Löschen') ?></button>
</div>
<button type="button" class="btn btn-default" onclick="kbCloseColModal()"><?= $this->te('Abbrechen') ?></button>
<button type="button" class="btn btn-primary" onclick="kbSaveColumn()"><?= $this->te('Speichern') ?></button>
</div>
</div>
</div>
<?php endif; ?>
<script>
'use strict';
var CSRF = (document.querySelector('meta[name="csrf-token"]') || {content:''}).content;
var canManage = <?php echo $can_manage ? 'true' : 'false'; ?>;
/* ── AJAX helpers ──────────────────────────────────────────────── */
function post(url, data) {
var body = new URLSearchParams();
body.append('csrf_token', CSRF);
for (var k in data) {
var v = data[k];
if (Array.isArray(v)) { v.forEach(function(vi){ body.append(k, vi); }); }
else { body.append(k, v == null ? '' : v); }
}
return fetch(url, {
method: 'POST',
headers: {'Content-Type':'application/x-www-form-urlencoded','X-Requested-With':'XMLHttpRequest','X-CSRF-Token':CSRF},
body: body.toString()
}).then(function(r){ return r.json(); }).catch(function(){ return {error:'Netzwerkfehler'}; });
}
function postJson(url, obj) {
obj._csrf = CSRF;
return fetch(url, {
method: 'POST',
headers: {'Content-Type':'application/json','X-Requested-With':'XMLHttpRequest','X-CSRF-Token':CSRF},
body: JSON.stringify(obj)
}).then(function(r){ return r.json(); }).catch(function(){ return {error:'Netzwerkfehler'}; });
}
/* ── Filter ────────────────────────────────────────────────────── */
function kbFilter() {
var search = (document.getElementById('kb-search').value || '').toLowerCase();
var prio = document.getElementById('kb-flt-prio').value;
var cat = document.getElementById('kb-flt-cat').value;
var onlyOpen= document.getElementById('kb-flt-open').checked;
document.querySelectorAll('.kb-card').forEach(function(card) {
var ok = true;
if (search && card.dataset.title.indexOf(search) < 0) ok = false;
if (prio && card.dataset.prio !== prio) ok = false;
if (cat && cat !== '0' && card.dataset.cat !== cat) ok = false;
if (onlyOpen && card.dataset.done === '1') ok = false;
card.classList.toggle('kb-card--hidden', !ok);
});
}
/* ── Drag & Drop ───────────────────────────────────────────────── */
var _dragId = null;
var _dragSrcCol = null;
var _dragTargetCard = null;
function kbDragStart(e, card) {
_dragId = parseInt(card.dataset.taskId);
_dragSrcCol = parseInt(card.dataset.colId);
card.classList.add('dragging');
e.dataTransfer.effectAllowed = 'move';
}
function kbDragEnd(card) {
card.classList.remove('dragging');
document.querySelectorAll('.kb-col.drag-over').forEach(function(c){ c.classList.remove('drag-over'); });
document.querySelectorAll('.kb-card.drag-target-before').forEach(function(c){ c.classList.remove('drag-target-before'); });
_dragTargetCard = null;
}
function kbDragOver(e, col) {
e.preventDefault();
e.dataTransfer.dropEffect = 'move';
col.classList.add('drag-over');
// Detect card-level hover for within-column reordering
document.querySelectorAll('.kb-card.drag-target-before').forEach(function(c){ c.classList.remove('drag-target-before'); });
var target = e.target ? e.target.closest('.kb-card') : null;
if (target && target.dataset.taskId && parseInt(target.dataset.taskId) !== _dragId) {
target.classList.add('drag-target-before');
_dragTargetCard = target;
} else {
_dragTargetCard = null;
}
}
function kbDragLeave(col) {
col.classList.remove('drag-over');
}
function kbDrop(e, col) {
e.preventDefault();
col.classList.remove('drag-over');
document.querySelectorAll('.kb-card.drag-target-before').forEach(function(c){ c.classList.remove('drag-target-before'); });
if (!_dragId || !canManage) return;
var targetColId = parseInt(col.dataset.colId);
var card = document.getElementById('kb-task-' + _dragId);
var body = document.getElementById('kb-body-' + targetColId);
if (!card || !body) return;
if (targetColId !== _dragSrcCol) {
// Move to different column
if (_dragTargetCard) {
body.insertBefore(card, _dragTargetCard);
} else {
body.appendChild(card);
}
card.dataset.colId = targetColId;
updateCount(_dragSrcCol, -1);
updateCount(targetColId, 1);
postJson('/kanban/tasks/' + _dragId + '/move', {column_id: targetColId}).then(function(d){
if (d.error) { alert(d.error); location.reload(); }
});
} else {
// Reorder within same column
if (_dragTargetCard && _dragTargetCard !== card) {
body.insertBefore(card, _dragTargetCard);
kbSaveCardOrder(targetColId);
}
}
_dragId = null; _dragSrcCol = null; _dragTargetCard = null;
}
function updateCount(colId, delta) {
var el = document.getElementById('kb-count-' + colId);
if (el) el.textContent = Math.max(0, parseInt(el.textContent || '0') + delta);
}
function kbSaveCardOrder(colId) {
var body = document.getElementById('kb-body-' + colId);
if (!body) return;
var order = [];
body.querySelectorAll('.kb-card[data-task-id]').forEach(function(c){
order.push(parseInt(c.dataset.taskId));
});
postJson('/kanban/tasks/reorder', {order: order}).then(function(d){
if (d.error) { console.error('Sortierung konnte nicht gespeichert werden:', d.error); }
});
}
/* ── Quick-Add ─────────────────────────────────────────────────── */
function kbQuickOpen(colId) {
var form = document.getElementById('kb-qf-' + colId);
var btn = document.getElementById('kb-qb-' + colId);
var inp = document.getElementById('kb-qi-' + colId);
if (!form) return;
form.classList.add('open');
if (btn) btn.style.display = 'none';
if (inp) { inp.value = ''; inp.focus(); }
}
function kbQuickClose(colId) {
var form = document.getElementById('kb-qf-' + colId);
var btn = document.getElementById('kb-qb-' + colId);
if (form) form.classList.remove('open');
if (btn) btn.style.display = '';
}
function kbQuickSubmit(colId) {
var inp = document.getElementById('kb-qi-' + colId);
var title = (inp ? inp.value : '').trim();
if (!title) return;
post('/kanban/tasks', {title: title, column_id: colId}).then(function(d) {
if (d.error) { alert(d.error); return; }
kbQuickClose(colId);
location.reload();
});
}
/* ── Done Toggle ───────────────────────────────────────────────── */
function kbToggleDone(taskId) {
post('/kanban/tasks/' + taskId + '/done', {}).then(function(d) {
if (!d.ok) { alert(d.error || 'Fehler beim Speichern'); return; }
var card = document.getElementById('kb-task-' + taskId);
var btn = card ? card.querySelector('.kb-done-btn') : null;
if (card) {
card.classList.toggle('kb-card--done', d.done);
card.dataset.done = d.done ? '1' : '0';
}
if (btn) {
btn.classList.toggle('kb-done-btn--on', d.done);
btn.title = d.done ? 'Als offen markieren' : 'Erledigt';
}
// Re-apply filter (onlyOpen might need to hide it now)
kbFilter();
});
}
/* ── Task Modal ────────────────────────────────────────────────── */
function kbOpenTaskModal(task) {
document.getElementById('kb-modal-id').value = task.id || '';
document.getElementById('kb-modal-title').value= task.title || '';
document.getElementById('kb-modal-desc').value = task.description || '';
document.getElementById('kb-modal-due').value = task.due_date || '';
var colSel = document.getElementById('kb-modal-col');
if (colSel) colSel.value = task.column_id || '';
var prioSel = document.getElementById('kb-modal-prio');
if (prioSel) prioSel.value = task.priority || 'normal';
var catSel = document.getElementById('kb-modal-cat');
if (catSel) catSel.value = task.category_id || '';
var assigneeIds = task.assignee_ids || [];
if (typeof assigneeIds === 'string') assigneeIds = assigneeIds.split(',').filter(Boolean);
document.querySelectorAll('.kb-modal-assignee').forEach(function(cb){
cb.checked = assigneeIds.indexOf(String(cb.value)) >= 0 || assigneeIds.indexOf(parseInt(cb.value)) >= 0;
});
var isNew = !task.id;
document.getElementById('kb-modal-title-lbl').textContent = isNew ? 'Neue Aufgabe' : 'Aufgabe bearbeiten';
var delBtn = document.getElementById('kb-modal-del-btn');
if (delBtn) delBtn.style.display = isNew ? 'none' : '';
document.getElementById('kb-task-overlay').classList.add('open');
document.getElementById('kb-modal-title').focus();
}
function kbCloseTaskModal() {
document.getElementById('kb-task-overlay').classList.remove('open');
}
function kbSaveTask() {
var id = document.getElementById('kb-modal-id').value;
var title = document.getElementById('kb-modal-title').value.trim();
if (!title) { document.getElementById('kb-modal-title').focus(); return; }
var assigneeIds = [];
document.querySelectorAll('.kb-modal-assignee:checked').forEach(function(cb){ assigneeIds.push(cb.value); });
var data = {
title: title,
description: document.getElementById('kb-modal-desc').value,
column_id: document.getElementById('kb-modal-col').value,
priority: document.getElementById('kb-modal-prio').value,
category_id: document.getElementById('kb-modal-cat').value,
due_date: document.getElementById('kb-modal-due').value,
assignee_ids:assigneeIds
};
var url = id ? '/kanban/tasks/' + id : '/kanban/tasks';
var btn = document.getElementById('kb-modal-save-btn');
if (btn) btn.disabled = true;
post(url, data).then(function(d) {
if (btn) btn.disabled = false;
if (d.error) { alert(d.error); return; }
kbCloseTaskModal();
location.reload();
});
}
function kbDeleteTask() {
var id = document.getElementById('kb-modal-id').value;
if (!id || !confirm('Aufgabe wirklich löschen?')) return;
post('/kanban/tasks/' + id + '/delete', {}).then(function(d){
if (d.ok) { kbCloseTaskModal(); location.reload(); }
});
}
/* ── Column Modal ──────────────────────────────────────────────── */
function kbOpenColModal(col) {
document.getElementById('kb-col-id').value = col ? col.id : '';
document.getElementById('kb-col-name-inp').value = col ? col.name : '';
document.getElementById('kb-col-wip').value = col && col.wip_limit ? col.wip_limit : '';
var color = col ? col.color : '#6c757d';
document.getElementById('kb-col-color').value = color;
kbSelectColor(color);
document.getElementById('kb-col-modal-title').textContent = col ? 'Spalte bearbeiten' : 'Neue Spalte';
var delBtn = document.getElementById('kb-col-del-btn');
if (delBtn) delBtn.style.display = col ? '' : 'none';
document.getElementById('kb-col-overlay').classList.add('open');
document.getElementById('kb-col-name-inp').focus();
}
function kbCloseColModal() {
document.getElementById('kb-col-overlay').classList.remove('open');
}
function kbSelectColor(color) {
document.getElementById('kb-col-color').value = color;
document.querySelectorAll('.kb-swatch').forEach(function(s){
s.classList.toggle('active', s.dataset.color === color);
});
}
function kbSaveColumn() {
var id = document.getElementById('kb-col-id').value;
var name = document.getElementById('kb-col-name-inp').value.trim();
var color = document.getElementById('kb-col-color').value;
var wip = document.getElementById('kb-col-wip').value;
if (!name) { document.getElementById('kb-col-name-inp').focus(); return; }
var url = id ? '/kanban/columns/' + id : '/kanban/columns';
var data = {name: name, color: color, wip_limit: wip};
post(url, data).then(function(d){
if (d.error) { alert(d.error); return; }
kbCloseColModal();
location.reload();
});
}
function kbDeleteColumn() {
var id = document.getElementById('kb-col-id').value;
if (!id || !confirm('Spalte löschen? Aufgaben werden in die erste andere Spalte verschoben.')) return;
post('/kanban/columns/' + id + '/delete', {}).then(function(d){
if (d.ok) { kbCloseColModal(); location.reload(); }
});
}
/* ── Keyboard ──────────────────────────────────────────────────── */
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') {
kbCloseTaskModal();
kbCloseColModal();
}
});
/* ── "Neue Aufgabe" per Spalte-Button (falls kein canManage, ignorieren) ── */
<?php if ($can_manage): ?>
document.addEventListener('click', function(e) {
if (e.target.closest('#kb-col-overlay, #kb-task-overlay')) return;
var addColBtn = e.target.closest('.kb-add-col');
if (addColBtn) kbOpenColModal(null);
});
<?php endif; ?>
</script>