Skip to content

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 delete

Reede 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 UND status !== 'done'
  • dueDate in der Vergangenheit UND status !== '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:

  1. Soft-Delete: deletedAt, deletedBy werden gesetzt, Aufgabe verschwindet aus allen normalen Views
  2. Papierkorb (30 Tage): Eigener Smart-View "Papierkorb" — nur Space-Manager+ sehen ihn
  3. Wiederherstellen: Manager kann zurückholen (setzt deletedAt = null)
  4. Auto-Hard-Delete: Cron räumt nach 30 Tagen endgültig auf
  5. Activity-Log: beide Schritte (task.deleted und task.purged) werden protokolliert — wir haben das schon für Hard-Delete, wird erweitert

Datenmodell

prisma
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

ContentTypeWannSichtbar für
task.deletedSoft-DeleteAlle
task.restoredAus Papierkorb wiederhergestelltAlle
task.purgedHard-Delete (auto nach 30d oder manuell durch Manager)Alle
task.parkedVertagtAlle
task.revivedAus 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 (setzt parkedAt), 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

MethodePfadWirkung
POST /spaces/:id/items/:itemId/parkparkedAt = now(), parkedBy = auth.sub, parkedNote aus body
POST /spaces/:id/items/:itemId/revivesetzt parkedAt = null, optional neues dueDate
POST /spaces/:id/items/:itemId/restoresetzt deletedAt = null (aus Papierkorb)
GET /spaces/:id/items/trashnur deletedAt IS NOT NULL, Manager+
GET /spaces/:id/items/reedeServer-seitige Heuristik, optional (Frontend kann auch filtern)

Geänderter Endpoint

MethodePfadVorherNachher
DELETE /spaces/:id/items/:itemIdprisma.workItem.deletesetzt 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:

ts
await prisma.workItem.deleteMany({
  where: { deletedAt: { lt: subDays(new Date(), 30) } }
});
// + recordActivity('task.purged', ...) pro gelöschtem Eintrag

Migration

0056_workitem_soft_delete:

sql
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

PhaseInhaltAufwand
AMigration + Soft-Delete + Papierkorb-View~3h
BVertagen + Reede-Smart-View (clientseitig)~3h
CAuto-Purge-Cron + erweiterte Activity-Logs~1h
DMonatlicher Digest in Chat~2h
ETenant-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

  1. Wer darf vertagen? Verantwortlicher allein, oder auch Mitglieder mit task:write? — Vorschlag: Verantwortlicher + Manager.
  2. Sichtbarkeit von vertagten Aufgaben: Komplett raus aus aktiver Liste, oder weiterhin als "graues" Badge sichtbar?
  3. Kanban-Verhalten: Vertagte Aufgaben bekommen eigene Spalte "Vertagt", oder sind in Kanban gar nicht sichtbar?
  4. 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):

prisma
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

WertBedeutungUI-Label
decisionBeschluss / Entscheidung getroffenBeschluss
letterSchreiben / Mitteilung versendetSchreiben
noteReine Notiz, kein extern wirksames ResultatNotiz
snoozedAus Reede heraus eingeschlafen — bewusst nicht weiter verfolgtEingeschlafen
otherSonstiges — Freitext erforderlichSonstiges

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-done Status (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 completionType fehlt → 400 mit error: COMPLETION_TYPE_REQUIRED
  • completedAt = now(), completedBy = auth.sub — werden serverseitig gesetzt
  • Activity-Event task.completed mit completionType als payload

Migration

Live als 0068_workitem_completion (zwischen Konzept und Implementation sind 7 andere Migrationen aus Tenant-Box / AV / Rollout dazugekommen):

sql
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:

markdown
# 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>.md

Eigentü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

  • statusdone: Akte wird erstellt
  • statusdone mit completionType='snoozed' (aus Reede): Akte wird erstellt, mit Hinweis "Eingeschlafen ohne Erledigung"
  • Spätere Edit am erledigten Task (z.B. zurück auf in_progress und wieder done): 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

PhaseInhaltAufwand
F.1Migration + Backend (completion-Felder + Validierung)~2h
F.2Frontend Inline-Done-Block in task-detail-panel.tsx (wirkt im Hub + Space)~2h
F.3Resultat-Block in der Detail-Anzeige (read-only nach Abschluss)~1h
F.4Soft-Delete-Begründung als Pflichtfeld im Lösch-Dialog~1h
G.1Akten-Generator-Service (Markdown-Build aus WorkItem + Activities + Comments + Checklists)~3h
G.2Auto-Erstellung beim Done-Trigger + Verknüpfung über completionDocumentId~1h
G.3Akten-Ordner-Struktur im Space-DMS + Versionierung bei Re-Done~2h
G.4UI-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)