From fdbb31065ffea2e0171a8586ca90d909322c32ac Mon Sep 17 00:00:00 2001 From: guyg1474 <340572510+guyg1474@users.noreply.github.com> Date: Thu, 15 Oct 2026 20:12:59 +0200 Subject: [PATCH] Kanban: Voraussetzung gibt schon bei "Wartet" frei MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Eine abhängige Aufgabe bleibt gesperrt (in "Offen"), bis jede Voraussetzung in "Wartet" oder "Erledigt" steht – vorher erst bei "Erledigt" - Server (UNLOCKING), Karte, Dialog und Meldungen angepasst; Test prüft "In Arbeit" (gesperrt) und "Wartet" (frei) Co-Authored-By: Claude Opus 5.5 --- README.md | 3 ++- backend/app/db.py | 2 +- backend/app/kanban.py | 16 +++++++++------- backend/tests/test_kanban.py | 7 +++++-- frontend/src/components/kanban/TaskCard.jsx | 8 ++++---- frontend/src/components/kanban/TaskDialog.jsx | 14 +++++++------- frontend/src/components/kanban/board.js | 3 +++ frontend/src/pages/KanbanBoard.jsx | 4 ++-- 8 files changed, 33 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index e9d50b9..0f7ed83 100644 --- a/README.md +++ b/README.md @@ -226,7 +226,8 @@ und löschen (löschen: wer angelegt hat und Administratoren). - **Checkliste** je Aufgabe: Punkte im Aufgaben-Dialog anlegen und abhaken; die Karte zeigt den Stand (☑ 2/5). - **Voraussetzungen:** Im Dialog unter „Setzt voraus“ andere Aufgaben desselben Projekts wählen. Solange eine - davon nicht erledigt ist, ist die Aufgabe gesperrt (🔒, bleibt in „Offen“). Kreise werden abgelehnt. + davon noch nicht in „Wartet“ oder „Erledigt“ steht, ist die Aufgabe gesperrt (🔒, bleibt in „Offen“). + Kreise werden abgelehnt. Erledigte Aufgaben räumen sich selbst auf: **2 Tage** nach „Erledigt“ verschwinden sie ins **Archiv** (Button „Archiv (n)“ im Board, dort auch „Zurückholen“), **14 Tage** nach „Erledigt“ werden sie diff --git a/backend/app/db.py b/backend/app/db.py index fb828f5..4116aea 100644 --- a/backend/app/db.py +++ b/backend/app/db.py @@ -273,7 +273,7 @@ CREATE TABLE IF NOT EXISTS kanban_checklist ( position INTEGER NOT NULL ); --- Abhängigkeiten: task_id ist gesperrt, bis requires_id erledigt ist (nur innerhalb eines Projekts) +-- Abhängigkeiten: task_id ist gesperrt, bis requires_id in "Wartet" oder "Erledigt" steht (nur innerhalb eines Projekts) CREATE TABLE IF NOT EXISTS kanban_dependencies ( task_id INTEGER NOT NULL REFERENCES kanban_tasks(id) ON DELETE CASCADE, requires_id INTEGER NOT NULL REFERENCES kanban_tasks(id) ON DELETE CASCADE, diff --git a/backend/app/kanban.py b/backend/app/kanban.py index 9bb54a2..cda651c 100644 --- a/backend/app/kanban.py +++ b/backend/app/kanban.py @@ -28,6 +28,8 @@ MAX_LINKS = 20 MAX_CHECKLIST = 100 MAX_CHECK_TEXT = 200 MAX_REQUIRES = 30 +# Eine Voraussetzung gibt ihre abhängigen Aufgaben frei, sobald sie so weit ist +UNLOCKING = ("wartet", "erledigt") ARCHIVE_AFTER = timedelta(days=2) DELETE_AFTER = timedelta(days=14) CLEANUP_SECONDS = 3600 @@ -226,16 +228,16 @@ def _task_extras(conn, project_id: int) -> dict: ): e = extras.setdefault(r["task_id"], {"checklist": [], "requires": [], "blocked": False}) e["requires"].append(r["requires_id"]) - e["blocked"] = e["blocked"] or r["status"] != "erledigt" + e["blocked"] = e["blocked"] or r["status"] not in UNLOCKING return extras def _open_requirements(conn, task_id: int) -> list: - """Titel der noch nicht erledigten Voraussetzungen einer Aufgabe.""" + """Titel der Voraussetzungen, die noch nicht in "Wartet" oder "Erledigt" stehen.""" return [r["title"] for r in conn.execute( - """SELECT t.title FROM kanban_dependencies d JOIN kanban_tasks t ON t.id = d.requires_id - WHERE d.task_id = ? AND t.status != 'erledigt' ORDER BY t.title""", - (task_id,), + f"""SELECT t.title FROM kanban_dependencies d JOIN kanban_tasks t ON t.id = d.requires_id + WHERE d.task_id = ? AND t.status NOT IN ({','.join('?' * len(UNLOCKING))}) ORDER BY t.title""", + (task_id, *UNLOCKING), )] @@ -291,9 +293,9 @@ def _save_requires(conn, task_id: int, ids: list) -> None: def _ensure_unlocked(conn, task_id: int, status: str) -> None: - """Gesperrte Aufgaben bleiben in "Offen", bis alle Voraussetzungen erledigt sind.""" + """Gesperrte Aufgaben bleiben in "Offen", bis alle Voraussetzungen in "Wartet" oder "Erledigt" stehen.""" if status != "offen" and (missing := _open_requirements(conn, task_id)): - raise ValidationError("Gesperrt – erst erledigen: " + ", ".join(f"„{t}“" for t in missing)) + raise ValidationError("Gesperrt – erst auf „Wartet“ oder „Erledigt“ setzen: " + ", ".join(f"„{t}“" for t in missing)) def _board(conn, project_id: int) -> dict: diff --git a/backend/tests/test_kanban.py b/backend/tests/test_kanban.py index 82f7ad3..3ca6997 100644 --- a/backend/tests/test_kanban.py +++ b/backend/tests/test_kanban.py @@ -85,7 +85,7 @@ def test_checkliste_speichern_und_abhaken(login): assert anna.patch(f"/api/kanban/aufgaben/{t['id']}", json={"checklist": [{"text": " "}]}).status_code == 400 -def test_voraussetzung_sperrt_bis_erledigt(login): +def test_voraussetzung_sperrt_bis_wartet_oder_erledigt(login): anna = login("anna") pid = make_board(anna) a = task(anna, pid, "Wand streichen") @@ -94,7 +94,10 @@ def test_voraussetzung_sperrt_bis_erledigt(login): assert b["blocked"] is True and b["requires"] == [a["id"]] res = anna.patch(f"/api/kanban/aufgaben/{b['id']}", json={"status": "arbeit"}) assert res.status_code == 400 and "Wand streichen" in res.json()["error"] - anna.patch(f"/api/kanban/aufgaben/{a['id']}", json={"status": "erledigt"}) + # "In Arbeit" reicht noch nicht, "Wartet" gibt frei + anna.patch(f"/api/kanban/aufgaben/{a['id']}", json={"status": "arbeit"}) + assert find(board(anna, pid), "Bilder aufhängen")["blocked"] is True + anna.patch(f"/api/kanban/aufgaben/{a['id']}", json={"status": "wartet"}) data = anna.patch(f"/api/kanban/aufgaben/{b['id']}", json={"status": "arbeit"}).json() assert find(data, "Bilder aufhängen")["blocked"] is False diff --git a/frontend/src/components/kanban/TaskCard.jsx b/frontend/src/components/kanban/TaskCard.jsx index a0cdc14..79bfe0b 100644 --- a/frontend/src/components/kanban/TaskCard.jsx +++ b/frontend/src/components/kanban/TaskCard.jsx @@ -1,4 +1,4 @@ -import { initials, PRIORITIES } from './board.js'; +import { initials, PRIORITIES, UNLOCKING } from './board.js'; // Karte einer Aufgabe im Board (ziehbar) und Anzeige des Bearbeiters @@ -13,11 +13,11 @@ export function Assignee({ id, users }) { ); } -// Titel der noch offenen Voraussetzungen einer Aufgabe +// Titel der Voraussetzungen, die noch nicht in "Wartet" oder "Erledigt" stehen export function waitingFor(task, tasks) { return (task.requires || []) .map((id) => tasks.find((t) => t.id === id)) - .filter((t) => t && t.status !== 'erledigt') + .filter((t) => t && !UNLOCKING.includes(t.status)) .map((t) => t.title); } @@ -43,7 +43,7 @@ export default function TaskCard({ task, users, waitingFor: waiting = [], draggi {task.description && {task.description}} {task.blocked && ( - + wartet auf {waiting.join(', ') || 'Voraussetzungen'} )} diff --git a/frontend/src/components/kanban/TaskDialog.jsx b/frontend/src/components/kanban/TaskDialog.jsx index e3e5d71..9ab687a 100644 --- a/frontend/src/components/kanban/TaskDialog.jsx +++ b/frontend/src/components/kanban/TaskDialog.jsx @@ -1,9 +1,9 @@ import { useState } from 'react'; import { fmtDate, personName } from '../../api.js'; -import { COLUMNS, PRIORITIES } from './board.js'; +import { COLUMNS, PRIORITIES, UNLOCKING } from './board.js'; // Aufgabe anlegen oder bearbeiten: Titel, Beschreibung, Checkliste, Status, Priorität, Bearbeiter, -// Voraussetzungen (gesperrt, bis diese erledigt sind); löschen +// Voraussetzungen (gesperrt, bis diese in "Wartet" oder "Erledigt" stehen); löschen export default function TaskDialog({ ref, target, users, tasks = [], me, onSave, onDelete }) { const task = target?.task; @@ -22,7 +22,7 @@ export default function TaskDialog({ ref, target, users, tasks = [], me, onSave, const others = tasks .filter((t) => t.id !== task?.id) .sort((a, b) => Number(form.requires.includes(b.id)) - Number(form.requires.includes(a.id)) || a.title.localeCompare(b.title, 'de')); - const missing = form.requires.map((id) => tasks.find((t) => t.id === id)).filter((t) => t && t.status !== 'erledigt'); + const missing = form.requires.map((id) => tasks.find((t) => t.id === id)).filter((t) => t && !UNLOCKING.includes(t.status)); const blocked = missing.length > 0; const toggleRequire = (id) => setForm((f) => ({ ...f, requires: f.requires.includes(id) ? f.requires.filter((x) => x !== id) : [...f.requires, id] })); const setCheck = (index, patch) => setForm((f) => ({ ...f, checklist: f.checklist.map((c, i) => (i === index ? { ...c, ...patch } : c)) })); @@ -42,7 +42,7 @@ export default function TaskDialog({ ref, target, users, tasks = [], me, onSave, // Noch nicht hinzugefügter Checklisten-Text soll nicht verloren gehen const checklist = newCheck.trim() ? [...form.checklist, { text: newCheck.trim(), done: false }] : form.checklist; if (blocked && form.status !== 'offen' && form.status !== task?.status) { - setError(`Gesperrt – erst erledigen: ${missing.map((t) => `„${t.title}“`).join(', ')}`); + setError(`Gesperrt – erst auf „Wartet“ oder „Erledigt“ setzen: ${missing.map((t) => `„${t.title}“`).join(', ')}`); return; } setBusy(true); @@ -136,7 +136,7 @@ export default function TaskDialog({ ref, target, users, tasks = [], me, onSave,
- Setzt voraus – gesperrt, bis diese Aufgaben erledigt sind + Setzt voraus – gesperrt, bis diese Aufgaben in „Wartet“ oder „Erledigt“ stehen {others.length === 0 ? (

Keine anderen Aufgaben in diesem Projekt.

) : ( @@ -146,13 +146,13 @@ export default function TaskDialog({ ref, target, users, tasks = [], me, onSave, ))} )} - {blocked &&

🔒 Gesperrt, bis erledigt: {missing.map((t) => t.title).join(', ')}

} + {blocked &&

🔒 Gesperrt, bis in „Wartet“ oder „Erledigt“: {missing.map((t) => t.title).join(', ')}

}
{error &&

{error}

} {task && ( diff --git a/frontend/src/components/kanban/board.js b/frontend/src/components/kanban/board.js index 8b14708..9f6ae67 100644 --- a/frontend/src/components/kanban/board.js +++ b/frontend/src/components/kanban/board.js @@ -27,3 +27,6 @@ export function positionAt(cards, index) { if (after == null) return before + 1; return (before + after) / 2; } + +// Eine Voraussetzung gibt ihre abhängigen Aufgaben frei, sobald sie in einer dieser Spalten steht +export const UNLOCKING = ['wartet', 'erledigt']; diff --git a/frontend/src/pages/KanbanBoard.jsx b/frontend/src/pages/KanbanBoard.jsx index 8ef62d6..f66bd0c 100644 --- a/frontend/src/pages/KanbanBoard.jsx +++ b/frontend/src/pages/KanbanBoard.jsx @@ -116,10 +116,10 @@ export default function KanbanBoard() { if (!drag) return; // Die Karte behält ihre Priorität: einsortiert wird nur zwischen Karten derselben Stufe const moved = data.tasks.find((t) => t.id === drag.id); - // Gesperrt: bleibt in "Offen", bis die Voraussetzungen erledigt sind + // Gesperrt: bleibt in "Offen", bis die Voraussetzungen in "Wartet" oder "Erledigt" stehen if (moved.blocked && status !== 'offen' && moved.status !== status) { setDrag(null); - setError(`„${moved.title}“ ist gesperrt – erst erledigen: ${waitingFor(moved, allTasks).join(', ')}`); + setError(`„${moved.title}“ ist gesperrt – erst auf „Wartet“ oder „Erledigt“ setzen: ${waitingFor(moved, allTasks).join(', ')}`); return; } const cards = byColumn[status].filter((t) => t.id !== drag.id);