Ka-Note/docs/feature-tasks.md

7.7 KiB

Feature: Aufgaben (Tasks)

Zweck

Strukturierte Aufgaben, die direkt aus Markdown-Text entstehen — kein separates Task-Formular nötig. Tasks sind echte Datenobjekte (DB-basiert, sync-fähig) und können Personen zugewiesen werden.

Syntax im Markdown-Editor

Syntax Bedeutung
[] Aufgabe erledigen Unverknüpfte Task-Markierung (Chip, noch kein Task-Objekt)
[T:abc123] Verknüpfte Aufgabe (ID referenziert Task-Objekt in DB)
  • [] muss am Zeilenanfang stehen (führende Leerzeichen erlaubt)
  • Inline-Tasks (z.B. mitten in einem Satz) werden nicht unterstützt

Shortcut

Ctrl+9 im Markdown-Editor:

  • Wenn Zeile beginnt mit [] [] wird entfernt
  • Wenn Zeile beginnt mit [T:...] → kein Toggle (Task existiert in DB)
  • Sonst → [] wird am Zeilenanfang eingefügt

Workflow

  1. [] Aufgabe tippen → wird als grauer Chip [ ] gerendert
  2. Chip anklicken → TaskCreateModal öffnet sich mit vorausgefüllten Feldern:
    • Titel (aus der Zeile extrahiert, ohne [], Mentions, Pfeile)
    • Assignee (aus -> ABBR oder @NAME in der Zeile)
    • Fälligkeitsdatum (optional)
  3. Speichern → Text wird zu [T:{id}] Aufgabe tippen geändert
  4. Chip [T:...] anklicken → Popup: Erledigt/Offen togglen

Felder

Feld Typ Beschreibung
title string Titel der Aufgabe
contextId string Zugehöriger Kontext (von Topic des Quell-Eintrags)
historyEntryId string | null Quell-HistoryEntry (null bei externen Tasks)
assignee string | null Vollständiger Personenname (nie Abkürzung)
status open | done Status
completedAt string | null ISO-Datum des Abschlusses
dueDate string | null Fälligkeitsdatum (YYYY-MM-DD)
source local | email | api Herkunft
externalId string | null ID im Quellsystem (für externe Quellen)
externalUrl string | null Deep-Link ins Quellsystem

Wo Tasks sichtbar sind

  • AgendaView: Eigener Abschnitt "Aufgaben" unterhalb der Topics (pro Kontext, ungefiltert)
  • Personen-Tab "Aufgaben": Alle Tasks zugewiesen an diese Person (kontextübergreifend)
  • Chip im Text: Visuelles Feedback (grau = offen, grün/durchgestrichen = erledigt)

Chip-Visualisierung

  • Unverknüpft [ ]: grauer Chip, Klick öffnet Modal
  • Offen [ ]: grauer Chip
  • Erledigt [✓]: grüner Chip mit Strikethrough
  • Nicht gefunden (gelöschter Task): ausgegraut

Extensibility: Externe Quellen

Das source-Feld und externalId/externalUrl bereiten künftige Integrationen vor:

  • source: 'email' — geplant: geflagte E-Mails via Graph-API → Task; contextId beim Import Pflicht
  • source: 'api' — geplant: POST /api/tasks/import von Drittsystemen; contextId im Payload Pflicht
  • Sync läuft über denselben Endpunkt wie lokale Tasks — externe Quellen pushen einfach in dieselbe tasks-Tabelle
  • Index auf (userId, source, externalId) für spätere Deduplizierung vorhanden

Bekannte Einschränkungen

  • [T:...]-Chips können nicht per Shortcut entfernt werden (Task existiert in DB)
  • Assignee-Auflösung von -> ABBR zu vollem Namen: der extrahierte Wert wird direkt verwendet (keine automatische Abkürzungsauflösung im Modal-Prefill)
  • Tasks im AgendaView zeigen alle Tasks des Kontexts (kein Status-Filter)

Implementierungshinweise (für Wartung)

Architektur: Wo HistoryEntries auftauchen

HistoryEntries sind topic-scoped, nicht context-scoped. Ein HistoryEntry gehört zu einem Topic (entry.topicId), das Topic gehört zu einem Kontext (topic.contextId). Daraus folgt:

HistoryEntry.text
  → gerendert in RenderedMarkdown
  → Task-Chip-Klick braucht entry.id + entry.topicId
  → ContextPage löst daraus contextId auf

Der nicht-offensichtliche Fall: EventCard

EventCard zeigt einen Termin (Kontext vom Typ meeting), nicht direkt einen HistoryEntry. Die Meeting-Notizen sind aber intern als HistoryEntry gespeichert — unter dem speziellen Topic notesTopicId(event.id). Dieser Mechanismus ist im EventCard-Code versteckt (liveQuery auf notesTopicId). Wer EventCard nur von außen sieht ("das ist eine Meeting-Karte"), erwartet dort keinen HistoryEntry-Kontext — und vergisst deshalb, historyEntryId zu übergeben.

Merksatz: Überall wo entry.text oder ein abgeleiteter Text (title/body-Split) gerendert wird, muss entry.id als historyEntryId mitgegeben werden — egal wie die Komponente heißt.

historyEntryId muss überall explizit übergeben werden

RenderedMarkdown wird an ~10 Stellen instanziiert. Ohne historyEntryId-Prop öffnet ein Task-Chip-Klick das Modal mit historyEntryId: null — der Task wird angelegt, aber der Quelltext ([] …[T:id]) wird nicht aktualisiert. Der Fehler ist stumm: kein Fehler, kein visuelles Feedback, nur der Chip bleibt [ ].

Vollständige Instanz-Übersicht:

Datei Quelle historyEntryId Notiz
HistoryItem.svelte entry
JournalView.svelte:626/640 entry (daily-log)
JournalView.svelte:674 entry (Meeting-Scope)
EventCard.svelte notesEntry (versteckt) notesEntry = erster Entry aus notesTopicId(event.id)
DashboardView.svelte:539/541 entry read-only Dashboard, kein Task-Flow vorgesehen
DashboardView.svelte:569 entry read-only Dashboard
ArchivedView.svelte entry archivierte Einträge, kein Task-Flow vorgesehen
WiedervorlageCard.svelte entry (body-Split) Wiedervorlage-Kontext, kein Task-Flow vorgesehen
PersonsView.svelte task.text kein HistoryEntry
EditableMarkdown.svelte Topic-Body kein HistoryEntry, topicId wäre theoretisch möglich

Wenn zukünftig Task-Chips auch in Dashboard/Archiv/Wiedervorlage aktiv werden sollen, müssen dort historyEntryId und topicId ergänzt werden.

Debugging-Anleitung

Symptom: Chip klickbar, Modal öffnet, aber Text bleibt [] … (kein [T:id]).

Diagnose im Browser-Devtools-Konsole:

document.querySelector('[data-task-new]').closest('.markdown-content').parentElement.className

→ Klasse identifizieren → zugehörige Komponente finden → prüfen ob historyEntryId Prop gesetzt ist.

Empfohlenes Refactoring: HistoryEntryText-Komponente

Die Wurzel des Problems ist, dass RenderedMarkdown ein generisches Render-Primitive ist, aber der HistoryEntry-Kontext implizit mitgegeben werden muss. Eine dedizierte Wrapper-Komponente würde das explizit machen:

<!-- HistoryEntryText.svelte -->
<script lang="ts">
  import RenderedMarkdown from './RenderedMarkdown.svelte';
  import type { HistoryEntry } from '@ka-note/shared';
  interface Props { entry: HistoryEntry; class?: string; textOverride?: string; }
  let { entry, class: className = '', textOverride }: Props = $props();
</script>
<RenderedMarkdown
  text={textOverride ?? entry.text}
  {class}
  historyEntryId={entry.id}
  topicId={entry.topicId}
/>

Verwendung dann z.B.:

<HistoryEntryText {entry} />                          <!-- statt RenderedMarkdown text={entry.text} -->
<HistoryEntryText {entry} textOverride={body} class="text-sm" />   <!-- body-Split-Fall -->

Vorteile:

  • historyEntryId kann nicht mehr vergessen werden — es ist Pflicht-Prop (TypeScript-Fehler sonst)
  • Neue Views die HistoryEntry-Text rendern, verwenden automatisch die richtige Komponente
  • RenderedMarkdown ohne historyEntryId bleibt erlaubt für echte read-only-Kontexte (Dashboard, Archiv)

Aufwand: ~7 Stellen umbauen, kein Logik-Änderung. Kein dringender Handlungsbedarf, aber sinnvoll beim nächsten größeren Refactoring-Sprint.