Files
Aime/docs/10-projektansicht.md
T
schakal00783andClaude Opus 5 d0e02eb976 feat(gui): Ordner-Ebene eines Projekts umbenennbar
Der erste Reiter hieß fest "Ordner" und trägt jetzt einen frei wählbaren
Namen. Er steht in der neuen Spalte projects.root_tab_name (NULL = "Ordner")
statt als Zeile in project_tabs: die Ordner-Ebene ist kein echter Reiter --
es gibt sie immer, sie hat keine eigene id und ist nicht löschbar. Eine
eigene Zeile hätte eine id gebraucht, die in project_pins.tab_id landen
müsste, womit die "0 = Ordner-Ebene"-Regel und damit der UNIQUE-Index
gefallen wären.

PATCH /api/projects/:id/tabs/0 fängt die 0 ab und schreibt ins Projekt;
leerer Name setzt auf "Ordner" zurück. Ebenen-Auswahl im Pin-Dialog und die
Liste bestehender Anheftungen zeigen den Namen mit.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02 23:11:12 +02:00

5.6 KiB
Raw Blame History

10 — Projektansicht (Ordner, Unterreiter, angeheftete Aufnahmen)

Dazugekommen am 2026-08-02. Ein eigener Reiter „Projektansicht" neben „Aufnahmen": Aufnahmen lassen sich in Projektordnern bündeln, ohne dass an der Aufnahme selbst etwas verändert wird.

Modell in einem Satz

Ein Pin ist eine Referenz, kein Besitz. Die Aufnahme bleibt unverändert in der Aufnahmenliste, darf in beliebig vielen Ordnern und Reitern hängen, und das Lösen einer Anheftung löscht nichts.

flowchart TD
  P["📁 Ordner<br/>„Project X“"] --> L0["Ebene „Ordner“<br/>tab_id = 0"]
  P --> T1["📑 Unterreiter<br/>„Projektrelevante Aufzeichnungen“"]
  P --> T2["📑 Unterreiter<br/>„Angebote“"]
  L0 --> R1["📌 #87 „1. Gespräch DFG – Projektstatusmeeting“"]
  T1 --> R2["📌 #86"]
  T1 --> R3["📌 #74"]
  R1 -.->|"Klick"| REC["Reiter „Aufnahmen“<br/>öffnet die Aufnahme im Detailbereich"]
  R2 -.->|"Klick"| REC

Zwei Ebenen, mehr nicht: Ordner und darin Unterreiter. Auf beiden kann angeheftet werden — der Ordner selbst ist also nicht nur ein Behälter für Reiter.

Die Ordner-Ebene ist genauso benennbar wie ein Unterreiter (Vorgabe: „Ordner"). Ihr Name steht aber in projects.root_tab_name und nicht als Zeile in project_tabs, weil sie kein echter Reiter ist: es gibt sie immer, sie hat keine eigene id und lässt sich nicht löschen. PATCH …/tabs/0 schreibt deshalb ins Projekt; ein leerer Name setzt auf „Ordner" zurück.

Datenbank

CREATE TABLE projects (
  id INT AUTO_INCREMENT PRIMARY KEY,
  name VARCHAR(190) NOT NULL,
  root_tab_name VARCHAR(190) NULL,    -- Beschriftung der Ordner-Ebene, NULL = „Ordner"
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP);

CREATE TABLE project_tabs (
  id INT AUTO_INCREMENT PRIMARY KEY,
  project_id INT NOT NULL,
  name VARCHAR(190) NOT NULL,
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  INDEX proj_idx (project_id));

CREATE TABLE project_pins (
  id INT AUTO_INCREMENT PRIMARY KEY,
  project_id INT NOT NULL,
  tab_id INT NOT NULL DEFAULT 0,      -- 0 = direkt im Ordner
  recording_id INT NOT NULL,
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  UNIQUE KEY uniq_pin (project_id, tab_id, recording_id),
  INDEX rec_idx (recording_id));

Warum tab_id = 0 statt NULL: MySQL/MariaDB schließt NULL-Werte von einem UNIQUE-Index aus. Mit tab_id NULL für „Ordner-Ebene" ließe sich dieselbe Aufnahme beliebig oft auf dieselbe Ebene anheften, ohne dass der Index das verhindert. Mit 0 greift UNIQUE — das Anheften ist damit über INSERT IGNORE idempotent, ein Doppelklick erzeugt keinen zweiten Eintrag.

Weitere Regeln:

  • Papierkorb: getProject filtert auf recordings.deleted_at IS NULL. Eine gelöschte Aufnahme verschwindet aus dem Ordner, ihr Pin bleibt aber stehen — nach dem Wiederherstellen ist sie wieder da, ohne dass man erneut anheften muss.
  • Endgültiges Löschen: db.purgeRecording räumt project_pins mit ab (in derselben Schleife wie markers/words/passages), sonst blieben tote Verweise zurück.
  • Ordner/Reiter löschen entfernt nur die Zeilen in project_pins — nie eine Aufnahme.

Bedienung

Zwei Wege zum selben Ziel, weil beide Richtungen im Alltag vorkommen:

flowchart LR
  subgraph A["Weg 1 — aus dem Projekt heraus"]
    A1["Projektansicht → Ordner öffnen"] --> A2["Ebene wählen<br/>(Ordner oder Unterreiter)"]
    A2 --> A3["📌 Aufnahme anheften"] --> A4["Dropdown mit allen Aufnahmen<br/>+ Filterfeld"]
  end
  subgraph B["Weg 2 — aus der Aufnahmenliste"]
    B1["Aufnahmen → 📌 an der Zeile"] --> B2["Dialog: Ordner + Ebene wählen<br/>(inkl. „+ Neuen Ordner anlegen…“)"]
    B2 --> B3["zeigt zugleich alle bestehenden<br/>Anheftungen dieser Aufnahme, löschbar"]
  end

Der 📌-Knopf in der Aufnahmenliste trägt die Zahl der Projekte, in denen die Aufnahme hängt, und ist farbig markiert, sobald sie irgendwo angeheftet ist (GET /api/pins/counts — ein Request für die ganze Liste, nicht einer pro Zeile). Die Zahl steckt zusätzlich in der Listen-Signatur state.lastListSig, damit eine neue Anheftung die Liste neu zeichnet, ohne die 5-Sekunden-Regel („keine Änderung → DOM nicht anfassen") aufzuweichen.

Ein Klick auf eine angeheftete Aufnahme springt in den Reiter „Aufnahmen" und öffnet sie dort im Detailbereich (jumpToRecording, dieselbe Mechanik wie der Sprung aus einem Suchtreffer, nur ohne Zeitmarke).

API

Route Zweck
GET /api/projects Ordnerliste mit Anzahl Reiter und Anheftungen
POST /api/projects Ordner anlegen {name}
GET /api/projects/:id Ordner mit Reitern und aufgelösten Anheftungen
PATCH /api/projects/:id Ordner umbenennen
DELETE /api/projects/:id Ordner löschen (Reiter + Anheftungen mit)
POST /api/projects/:id/tabs Unterreiter anlegen {name}
PATCH /api/projects/:id/tabs/:tid Unterreiter umbenennen; :tid = 0 benennt die Ordner-Ebene um (leerer Name = zurück auf „Ordner")
DELETE /api/projects/:id/tabs/:tid Unterreiter löschen
POST /api/projects/:id/pins anheften {recording_id, tab_id} (tab_id fehlt/0 = Ordner-Ebene)
DELETE /api/projects/:id/pins/:pinId Anheftung lösen
GET /api/recordings/:id/pins wo hängt diese Aufnahme überall
GET /api/pins/counts {recording_id: anzahl} für die ganze Liste

Dateien

app/db.js (Tabellen + CRUD), app/server.js (Routen), app/web/src/main.js (viewProjects/renderProjectList/renderProjectDetail/pinPickRecordingDialog/pinToProjectDialog/ openModal), app/web/src/style.css (Ordnergitter, Reiterleiste, Dialog). Board-Backups: .bak-projects.