159 lines
7.7 KiB
Markdown
159 lines
7.7 KiB
Markdown
# 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:
|
|
```js
|
|
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:
|
|
|
|
```svelte
|
|
<!-- 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.:
|
|
```svelte
|
|
<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.
|