Aufgaben-Reede & Soft-Delete — Konzept
Das Problem
In Schulen sterben Aufgaben selten einen offiziellen Tod. Sie schlafen ein.
- Klassenrats-Ideen aus Enthusiasmus, kein Realitätsbezug
- AG-Vorhaben, die zwischen Halbjahren versanden
- Projektwochen-Aufgaben, deren Termin verstrichen ist
- "Mache ich noch"-Aufgaben, die nie wieder angefasst werden
Heute gibt es zwei Ausgänge: Done oder Hard-Delete. Beides verlangt eine bewusste Entscheidung — die niemand trifft, weil niemand sich verantwortlich fühlt. Folge: die Liste wuchert, niemand findet mehr was, der Verantwortlicher sieht 80 offene Aufgaben und schaltet ab.
Außerdem ist Hard-Delete heute unsichtbar: Aufgabe einfach weg, niemand erfährt's aktiv. Das wird in Schulen kritisch wenn Eltern oder Schulleitung später nach einer Aufgabe fragen ("Habt ihr das nicht beschlossen?").
Lösung in zwei Teilen
Teil 1 — Lebenszyklus mit "Reede"
aktiv ──── (kein Update >14d ODER dueDate verstrichen) ────► REEDE
│
┌──────────────────────┼──────────────────────┐
▼ ▼ ▼
wiederbeleben vertagen löschen
(neuer Termin) (Status: parked) (Soft-Delete)
│ │ │
└──────► aktiv └──► out-of-list ▼
PAPIERKORB (30d)
│
▼
hard deleteReede ist kein neuer Status, sondern ein Smart-View (rein clientseitiger Filter über bestehende Felder). Aufgaben wandern nicht in eine andere Tabelle, sie werden nur sichtbar gemacht.
Heuristik (alle Bedingungen kombinierbar):
updatedAtälter als 14 Tage UNDstatus !== 'done'dueDatein der Vergangenheit UNDstatus !== 'done'UND seit dueDate keine Änderung
Schwellen sind später per Tenant-Setting anpassbar (reede.staleDays, reede.overdueDays).
Teil 2 — Soft-Delete + Papierkorb
Hard-Delete ersetzt durch:
- Soft-Delete:
deletedAt,deletedBywerden gesetzt, Aufgabe verschwindet aus allen normalen Views - Papierkorb (30 Tage): Eigener Smart-View "Papierkorb" — nur Space-Manager+ sehen ihn
- Wiederherstellen: Manager kann zurückholen (setzt
deletedAt = null) - Auto-Hard-Delete: Cron räumt nach 30 Tagen endgültig auf
- Activity-Log: beide Schritte (
task.deletedundtask.purged) werden protokolliert — wir haben das schon für Hard-Delete, wird erweitert
Datenmodell
model WorkItem {
...bestehende Felder...
/// Phase 14: Soft-Delete für Papierkorb
deletedAt DateTime? @map("deleted_at")
deletedBy String? @map("deleted_by") @db.VarChar(255)
/// Phase 14: Vertagt — bleibt in DB, raus aus Listen, kein Auto-Reede
parkedAt DateTime? @map("parked_at")
parkedBy String? @map("parked_by") @db.VarChar(255)
parkedNote String? @map("parked_note") @db.Text
}Keine neue Tabelle. Drei optionale Felder reichen für alle drei Zustände (aktiv / parked / gelöscht).
Index:
@@index([spaceId, deletedAt])— Papierkorb-Queries@@index([deletedAt])— Auto-Purge-Cron
Activity-Log-Erweiterung
| ContentType | Wann | Sichtbar für |
|---|---|---|
task.deleted | Soft-Delete | Alle |
task.restored | Aus Papierkorb wiederhergestellt | Alle |
task.purged | Hard-Delete (auto nach 30d oder manuell durch Manager) | Alle |
task.parked | Vertagt | Alle |
task.revived | Aus Reede wiederbelebt (neuer Termin) | Alle |
Damit ist jede Zustands-Änderung im Space-Stream sichtbar. Erfüllt DSGVO-Rechenschaftspflicht (Art. 5 + 30) und schützt vor "ist plötzlich weg, niemand weiß warum".
UI
Reede-View
Im Aufgaben-Hub neben Liste / Kanban / Gantt ein neuer Tab "Reede" (Icon: anchor).
Pro Aufgabe:
- Titel + Verantwortlicher
- Grund-Badge: "Schläft seit X Tagen" / "Termin überschritten um X Tagen"
- 3 Aktionen: Wiederbeleben (öffnet Datepicker für neues
dueDate), Vertagen (setztparkedAt), Löschen (Soft-Delete)
Papierkorb-View
Im selben Tab-Bereich, nur für Manager sichtbar (file:delete_any).
- Aufgabe + "Gelöscht von X am Y"
- Countdown: "Wird in N Tagen endgültig entfernt"
- 2 Aktionen: Wiederherstellen, Sofort endgültig löschen
Monatlicher Digest
Cron jeden 1. des Monats: pro Verantwortlicher eine Übersicht "Du hast 5 Aufgaben in der Reede" — als Chat-Nachricht vom System-Bot in den Workflow-Raum. Kein zweiter Mail-Kanal.
Backend-Änderungen
Neue Endpoints
| Methode | Pfad | Wirkung |
|---|---|---|
POST /spaces/:id/items/:itemId/park | parkedAt = now(), parkedBy = auth.sub, parkedNote aus body | |
POST /spaces/:id/items/:itemId/revive | setzt parkedAt = null, optional neues dueDate | |
POST /spaces/:id/items/:itemId/restore | setzt deletedAt = null (aus Papierkorb) | |
GET /spaces/:id/items/trash | nur deletedAt IS NOT NULL, Manager+ | |
GET /spaces/:id/items/reede | Server-seitige Heuristik, optional (Frontend kann auch filtern) |
Geänderter Endpoint
| Methode | Pfad | Vorher | Nachher |
|---|---|---|---|
DELETE /spaces/:id/items/:itemId | prisma.workItem.delete | setzt deletedAt, deletedBy |
Filter in allen List-Queries
Alle bestehenden Queries (/items, /by-events, /me/tasks, etc.) bekommen deletedAt: null als Default-Filter. Nur Trash-View hebt das auf.
Auto-Purge-Cron
Täglich 03:00 UTC:
await prisma.workItem.deleteMany({
where: { deletedAt: { lt: subDays(new Date(), 30) } }
});
// + recordActivity('task.purged', ...) pro gelöschtem EintragMigration
0056_workitem_soft_delete:
ALTER TABLE work_items
ADD COLUMN deleted_at TIMESTAMP,
ADD COLUMN deleted_by VARCHAR(255),
ADD COLUMN parked_at TIMESTAMP,
ADD COLUMN parked_by VARCHAR(255),
ADD COLUMN parked_note TEXT;
CREATE INDEX work_items_deleted_idx ON work_items (space_id, deleted_at);
CREATE INDEX work_items_purge_idx ON work_items (deleted_at) WHERE deleted_at IS NOT NULL;Bestehende Daten unverändert. Migration ist additiv und reversibel.
Phasenplan
| Phase | Inhalt | Aufwand |
|---|---|---|
| A | Migration + Soft-Delete + Papierkorb-View | ~3h |
| B | Vertagen + Reede-Smart-View (clientseitig) | ~3h |
| C | Auto-Purge-Cron + erweiterte Activity-Logs | ~1h |
| D | Monatlicher Digest in Chat | ~2h |
| E | Tenant-Settings für Reede-Schwellen | ~1h (live 2026-05-06) |
Alle Phasen A–E sind live. Settings-Keys: tasks.reede_stale_days (Default 14), tasks.trash_retention_days (Default 30) — UI unter Settings → Workspace → Aufgaben-Pflege (Admin-only).
Offene Fragen
- Wer darf vertagen? Verantwortlicher allein, oder auch Mitglieder mit
task:write? — Vorschlag: Verantwortlicher + Manager. - Sichtbarkeit von vertagten Aufgaben: Komplett raus aus aktiver Liste, oder weiterhin als "graues" Badge sichtbar?
- Kanban-Verhalten: Vertagte Aufgaben bekommen eigene Spalte "Vertagt", oder sind in Kanban gar nicht sichtbar?
- Papierkorb-Berechtigung: Manager+ sieht alle gelöschten? Oder nur die selbst gelöschten? — Vorschlag: Manager sieht alle (DSGVO-Audit).
Phase F — Resultat-Dokumentation beim Erledigen (geplant 2026-05-09)
Warum
Bisher dokumentiert das Activity-Log dass eine Aufgabe erledigt wurde. Was aber das Resultat war — Beschluss, Schreiben, Notiz, oder ein "Wir haben uns dagegen entschieden" — bleibt im Kopf des Verantwortlichen. Schulen müssen ihre Handlungen nachvollziehen können (Schulaufsicht, Eltern-Anfragen Jahre später, Kollegiums-Beschlüsse). Ohne strukturierte Resultat-Dokumentation fehlen die Belege.
DSGVO Art. 5 (Rechenschaftspflicht) + Schul-Verwaltungsvorschriften der Länder (Aufbewahrung 5–10 Jahre für Schul-Akten) verlangen, dass jede beschlossene Maßnahme nachvollziehbar dokumentiert ist.
Datenmodell
Erweiterung auf WorkItem (additiv, alle nullable):
model WorkItem {
...
/// Resultat-Typ — Soft-Pflicht beim Status-Wechsel auf "done".
/// Werte: 'decision' | 'letter' | 'note' | 'snoozed' | 'other'
completionType String? @map("completion_type") @db.VarChar(32)
/// Freitext-Beschreibung des Resultats. Optional, aber Pflicht bei
/// completionType='snoozed' (Begründung warum ohne Erledigung beendet).
completionNote String? @map("completion_note") @db.Text
/// Optional: Verknüpfung zu einem DMS-Dokument als Beleg.
completionDocumentId String? @map("completion_document_id") @db.Uuid
/// Snapshot bei Übergang nach 'done'. updatedAt deckt das nicht zuverlässig
/// ab, weil eine erledigte Aufgabe nachträglich editiert werden kann.
completedAt DateTime? @map("completed_at")
completedBy String? @map("completed_by") @db.VarChar(255)
/// Begründung beim Soft-Delete — Pflichtfeld, fließt in automatische
/// Berichte ein ("Aufgabe X wurde am Y gelöscht, Begründung: …").
deletionReason String? @map("deletion_reason") @db.Text
}completionType-Werte
| Wert | Bedeutung | UI-Label |
|---|---|---|
decision | Beschluss / Entscheidung getroffen | Beschluss |
letter | Schreiben / Mitteilung versendet | Schreiben |
note | Reine Notiz, kein extern wirksames Resultat | Notiz |
snoozed | Aus Reede heraus eingeschlafen — bewusst nicht weiter verfolgt | Eingeschlafen |
other | Sonstiges — Freitext erforderlich | Sonstiges |
UI — Inline statt Modal
Kein Modal (Mobile-Keyboard-Probleme + schlechtere Lesbarkeit auf kleinen Geräten). Stattdessen Inline im Detail-Panel:
- Klick auf "Erledigt"-Status-Button öffnet inline-Block unterhalb der Status-Buttons:
- Header: "Aufgabe abschließen"
- Resultat-Typ — Dropdown (Pflicht, kein leeres Default)
- Resultat-Notiz — Textarea (optional, aber Pflicht für
snoozed/other) - "Beleg verknüpfen" — Button öffnet DMS-Picker (Phase G übernimmt automatisch)
- Abschließen-Button + Abbrechen
- Erst nach Klick auf "Abschließen" wird
status='done'+ completion-Felder committed. - Bei Wechsel auf nicht-
doneStatus (todo,in_progress,review): kein Block, direkter Patch wie heute.
Auf Mobile (Hub-Detail-Tab oder Space-Tasks-Tab) ist dieser Block ein normaler Inline-Bereich, der sich mit dem Inhalt scrollt — keine Overlays, kein Z-Index-Wirrwarr.
Sichtbarkeit nach Erledigung
In der Details-Sicht (Hub + Space-Tasks-Panel) wird oberhalb der bisherigen Felder ein Resultat-Block gezeigt:
✓ Erledigt am 2026-05-09 von Lee Anders
Typ: Beschluss
Notiz: Klassenrat hat zugestimmt, Umsetzung ab Schuljahr 2026/27
Beleg: → Beschluss-Klassenrat-2026-05-09.pdf (DMS)Der Block ist nur lesbar (kein Inline-Edit nach Abschluss — wer korrigieren will, setzt zurück auf in_progress, edit, dann wieder done).
Backend-Endpoint
Bestehender PATCH /spaces/:id/items/:itemId nimmt die neuen Felder optional entgegen. Beim Übergang status: 'done':
- Wenn
completionTypefehlt → 400 miterror: COMPLETION_TYPE_REQUIRED completedAt = now(),completedBy = auth.sub— werden serverseitig gesetzt- Activity-Event
task.completedmitcompletionTypeals payload
Migration
Live als 0068_workitem_completion (zwischen Konzept und Implementation sind 7 andere Migrationen aus Tenant-Box / AV / Rollout dazugekommen):
ALTER TABLE work_items
ADD COLUMN completion_type VARCHAR(32),
ADD COLUMN completion_note TEXT,
ADD COLUMN completion_document_id VARCHAR(50),
ADD COLUMN completed_at TIMESTAMP,
ADD COLUMN completed_by VARCHAR(255),
ADD COLUMN deletion_reason TEXT;
CREATE INDEX work_items_completed_idx
ON work_items (space_id, completed_at DESC)
WHERE completed_at IS NOT NULL;
-- FK auf documents wird per DO $$ defensive nur gesetzt, wenn die Tabelle
-- existiert (dev-DBs ohne DMS bleiben migrierbar).completion_document_id ist VARCHAR(50) wegen cuid()-IDs in Document (nicht UUID — siehe schema.prisma).
Backfill: bestehende done-Tasks bekommen completionType = 'note', completedAt = updatedAt, completedBy = createdBy (best guess). Aktivitäts-Log bleibt für die exakte Historie der Bestandsdaten.
Phase G — Aufgaben-Akte als DMS-Dokument (geplant 2026-05-09)
Warum
Eine Aufgabe ist ein Vorgang mit Anfang, Verlauf, Resultat und Beteiligten. In der Schul-Verwaltung gehören solche Vorgänge in eine Akte — nicht in eine Datenbank-Tabelle, die für Außenstehende undurchschaubar ist. Eine Akte ist auch nach Jahren von Schulleitung, Schulaufsicht oder Eltern lesbar, ohne Prilog-Login.
Beim Erledigen (oder beim "Einschlafen lassen") wird automatisch ein Akten-Dokument im DMS angelegt, das den vollständigen Lifecycle dokumentiert. Dieses Dokument ist die einzige Quelle, die in automatische Berichte (Jahresbericht, Halbjahres-Übersicht, Compliance-Report) einfließt.
Inhalt der Akte
Markdown im DMS, automatisch generiert:
# Aufgabe: <Titel>
**Space:** <Space-Name> (<Typ>)
**Verantwortlich:** <Display-Name>
**Erstellt:** <Datum> von <Display-Name>
**Abgeschlossen:** <Datum> von <Display-Name>
**Resultat-Typ:** <Beschluss/Schreiben/...>
## Beschreibung
<description>
## Verlauf
- 2026-04-12 — Erstellt (Lee Anders)
- 2026-04-15 — Status: in Arbeit (Lee Anders)
- 2026-04-22 — Kommentar: "Termin verschoben auf …" (Anna Mueller)
- 2026-05-01 — Verantwortlich: Lee Anders → Anna Mueller
- 2026-05-09 — Status: erledigt (Anna Mueller)
## Resultat
<completionNote>
## Belege
- → Beschluss-Klassenrat-2026-05-09.pdf
- → Anschreiben-Eltern-2026-05-09.docx
## Checklisten
- [x] Klassenrat informieren
- [x] Eltern anschreiben
- [x] Termin im Kalender eintragen
## Kommentare
> Lee Anders — 2026-04-15
> Sollten wir vorab mit Schulleitung sprechen?
> Anna Mueller — 2026-04-16
> Schulleitung hat zugestimmt.Ablage
Pro Space ein Aufgaben-Akten-Ordner im Space-DMS:
<Space>/
Akten/
Aufgaben/
2026-05-09-aufgabe-<slug>.mdEigentümer der Akte: der Space (nicht der Erlediger), damit sie in der Aufbewahrungs-Policy des Spaces läuft.
Verknüpfung mit completionDocumentId
Die Akte ist ein eigenes DMS-Dokument. WorkItem.completionDocumentId zeigt darauf — beim Klick im Resultat-Block öffnet sich die Akte direkt.
Wenn der User schon manuell ein Dokument verknüpft hat (z.B. eine PDF-Beschluss-Vorlage), wird dieses zusätzlich in der Akte verlinkt — die Akte selbst ersetzt die manuelle Verknüpfung nicht.
Trigger
status→done: Akte wird erstelltstatus→donemitcompletionType='snoozed'(aus Reede): Akte wird erstellt, mit Hinweis "Eingeschlafen ohne Erledigung"- Spätere Edit am erledigten Task (z.B. zurück auf
in_progressund wiederdone): Akte wird versioniert im DMS (neue Version, alte bleibt)
Soft-Delete-Begründung in der Akte
Beim Soft-Delete (Papierkorb): kurzer Eintrag in der Akte, falls schon erstellt. Wenn die Aufgabe noch nicht abgeschlossen war, wird stattdessen ein Kurz-Akten-Dokument angelegt: "Aufgabe gelöscht am … von … Begründung: …". Damit gibt es keine "stillen Verschwinden"-Aufgaben.
Phasen
| Phase | Inhalt | Aufwand |
|---|---|---|
| F.1 | Migration + Backend (completion-Felder + Validierung) | ~2h |
| F.2 | Frontend Inline-Done-Block in task-detail-panel.tsx (wirkt im Hub + Space) | ~2h |
| F.3 | Resultat-Block in der Detail-Anzeige (read-only nach Abschluss) | ~1h |
| F.4 | Soft-Delete-Begründung als Pflichtfeld im Lösch-Dialog | ~1h |
| G.1 | Akten-Generator-Service (Markdown-Build aus WorkItem + Activities + Comments + Checklists) | ~3h |
| G.2 | Auto-Erstellung beim Done-Trigger + Verknüpfung über completionDocumentId | ~1h |
| G.3 | Akten-Ordner-Struktur im Space-DMS + Versionierung bei Re-Done | ~2h |
| G.4 | UI-Link zur Akte im Resultat-Block + Reede-Aktionen | ~1h |
Gesamt-Aufwand Phase F: ~6h. Phase G: ~7h.
Reihenfolge
F vor G. F ist alleine schon nutzbar (Resultat strukturiert dokumentiert). G ergänzt den DMS-Akten-Output für externe Berichte und Compliance-Audits.
Warum so?
- Keine neue Tabelle — additive Spalten, alle Queries weiterhin schlank
- Reede ist Filter, kein Status — keine Migrations-Falle, kein "wie kommt eine Aufgabe wieder raus"
- Soft-Delete + 30d Auto-Purge — DSGVO-konformes Recht auf Vergessenwerden bleibt erhalten, aber mit sichtbarer Frist
- Activity-Log dokumentiert jeden Übergang — Rechenschaft erfüllt
- Heuristik kein Auto-Verschieben — System schlägt nur vor, Mensch entscheidet (verhindert Sommerferien-Falschalarme)