diff --git a/docs/roadmap-feedback-fabian.md b/docs/roadmap-feedback-fabian.md new file mode 100644 index 0000000..70582af --- /dev/null +++ b/docs/roadmap-feedback-fabian.md @@ -0,0 +1,269 @@ +# Roadmap — Feedback Fabian (Mai 2026) + +> **Quelle**: User-Feedback von Fabian, Führungsoffizier / Übungsleiter Feuerwehr. +> **Erhalten**: 20.05.2026 +> **Status**: Geplant, noch nicht umgesetzt. + +Dieses Dokument ist die strukturierte Roadmap aus Fabians Feedback. Jede KI / jeder Entwickler der an Lageplan arbeitet, soll diese Datei vor Beginn lesen, um Kontext und Priorisierung zu kennen. + +--- + +## Originalfeedback (Zusammenfassung) + +Fabian sucht eine Lösung um **Einsätze und Übungen einfach zu skizzieren und zu krokieren**. Lobt die App, gibt aber konkrete Verbesserungsvorschläge aus Anwendersicht: + +1. **Einsatz vs. Übung unterscheiden** — bei Übungen kein Journal nötig, dafür Übungsziele/Auswertung +2. **Symbol-Bibliothek erweitern** — Warteraum, Rettungsachse, taktische Zeichen +3. **Symbol-Schönheitsfehler** — Treppe/Eingang mit Umriss, Absperrung +4. **Stockwerke/Geschosse** — eigene Werte eintragen +5. **Symbol-Kategorien aufräumen** — Motorspritze unter Organisation statt Geräte; Hydrant/Leitern uneinheitlich +6. **Linien mit Typ** — nach Zeichnen wählen: Rettungsachse (gestrichelt + R), Leitung (blau), Schlauch, etc. +7. **Multi-User Live** — eine Person zeichnet Karte, andere schreibt Journal gleichzeitig +8. **Rapport-/Lageansicht** — separate Sicht mit Pendenzen, aktueller Stand, wichtige Punkte für Führungsunterstützung (FU), darstellbar auf grossem Bildschirm; nicht alle Journaleinträge sichtbar für alle + +--- + +## Phasen-Plan + +### Phase 1 — Symbol-Architektur Redesign (3-4 Wochen) ⭐⭐ + +**Strategischer Umbau** statt nur Kategorien aufräumen. Begründung: +- Fabians Feedback zeigt, dass die aktuelle Kategorisierung uneinheitlich ist +- Symbol-Bibliothek ist zu starr — andere Anwender (THW, Sanität, ausländische Wehren) brauchen andere Symbole +- Mandantenfähigkeit ist zentrales Versprechen → muss auch für Symbole konsequent durchgezogen werden + +#### 1.1 Neue Symbol-Architektur: Template-Import + 100% mandantenspezifisch + +**Konzept**: Mandant startet mit leerer Bibliothek, importiert beim Onboarding (oder jederzeit) **kuratierte Vorlagen-Pakete** (Feuerwehr CH, THW, Sanität…). Importierte Symbole werden vollständig eigene Mandanten-Daten — umbenennbar, löschbar, kategorisierbar, ergänzbar. + +**Datenmodell**: +``` +TenantCategory (per-tenant, frei definierbar) + - id, tenantId, name, sortOrder, icon + +TenantSymbol (per-tenant, ehemals "Meine Symbole") + - id, tenantId, categoryId → TenantCategory + - name, svgPath, sortOrder, customName + +SymbolTemplate (global, read-only) + - id, packageId ("feuerwehr-ch", "thw", "sanitaet") + - categoryName, name, svgPath, tags +``` + +**Tasks**: +- [ ] Schema-Migration: `TenantCategory` neu, `TenantSymbol.categoryId` ergänzen, Standard-`Icon` zu `SymbolTemplate` umwandeln +- [ ] **Migration der bestehenden Tenants**: Auto-Import des `feuerwehr-ch` Pakets beim ersten Login, sodass nichts kaputt geht +- [ ] API: CRUD für `TenantCategory`, erweitert für `TenantSymbol` +- [ ] API: `GET /api/templates` — listet verfügbare Pakete +- [ ] API: `POST /api/templates/import` — importiert ausgewählte Symbole als TenantSymbols +- **Files**: `prisma/schema.prisma`, `src/app/api/tenant/categories/`, `src/app/api/tenant/symbols/`, `src/app/api/templates/` + +#### 1.2 UX: Symbol-Manager im Admin + +**Sidebar / Symbol-Verwaltung**: +``` +┌─ Meine Symbole ─────────────┐ +│ ┌ Fahrzeuge ─────────┐ │ +│ │ 🚒 TLF │ │ +│ │ 🚒 RW │ │ +│ │ + Symbol │ │ +│ └────────────────────┘ │ +│ ┌ Wasser ────────────┐ │ +│ │ 🟦 Hydrant │ │ +│ └────────────────────┘ │ +│ [+ Kategorie] │ +│ [📦 Vorlagen importieren] │ +└─────────────────────────────┘ +``` + +**Tasks**: +- [ ] Admin-Tab: Kategorien anlegen/umbenennen/sortieren (Drag & Drop) +- [ ] Symbole pro Kategorie verwalten (Drag & Drop zwischen Kategorien) +- [ ] Eigene SVG-Uploads direkt einer Kategorie zuordnen +- [ ] Import-Dialog: Pakete-Auswahl mit Vorschau, granulare Symbol-Auswahl +- [ ] Mehrfach-Import desselben Symbols erlaubt (z.B. "TLF rot" + "TLF blau" auf gleicher SVG-Basis) +- **Files**: `src/components/admin/icons-tab.tsx` (umbauen), neuer `import-templates-dialog.tsx` + +#### 1.3 Vorlagen-Pakete kuratieren + +Aus dem aktuellen Symbol-Bestand werden die ersten Pakete: + +- [ ] 📦 **Feuerwehr Schweiz** — taktische Zeichen CH (aktueller Bestand, sauber kategorisiert) +- [ ] 📦 **Sanität / Rettungsdienst** — falls Symbole vorhanden, sonst Phase 2 +- [ ] 📦 **Polizei / Verkehr** — als Stub für später +- [ ] Innerhalb der Pakete: **fehlende taktische Zeichen ergänzen**: + - Warteraum + - Rettungsachse + - Sammelplatz + - Verletztennest / Eingang Verletzter + - Einsatzleitung (EL) / Kommandoposten + - Bereitstellungsraum +- [ ] **Symbol-Schönheitsfehler im Paket** beheben: + - Treppe — Umriss entfernen + - Eingang — Umriss entfernen + - Absperrung — Seitenlinien aufräumen +- **Files**: `prisma/seed/symbol-templates.ts`, `public/icons/` + +#### 1.4 Stockwerke/Geschosse editierbar + +- [ ] Symbol-Property: `instanceLabel` (Freitext, pro Symbol-Instanz auf der Karte) +- [ ] Bei Gebäude-Symbolen: optionales Textfeld für Geschosszahl (z.B. "EG+3") +- [ ] Anzeige als Badge auf dem Symbol +- [ ] Editierbar via Doppelklick oder Sidebar +- **Files**: `src/types/`, `map-view.tsx` Symbol-Renderer, ggf. DrawFeature.properties erweitern + +--- + +### Phase 1.5 — Quick Polish (parallel oder danach, 1 Woche) 🔥 + +Kleinkram der nicht in den Architektur-Umbau passt aber wichtig ist. + +- [ ] Default-Linientyp / -farbe pro Mandant konfigurierbar +- [ ] Symbol-Suche im Sidebar (Volltext über Name, Tags, Kategorie) +- [ ] Häufig-benutzte Symbole oben anzeigen (Recent / Favoriten) + +--- + +### Phase 2 — Übungsmodus (2-3 Wochen) ⭐ + +Neuer Projekt-Typ, eigene Logik. + +#### 2.1 Projekt-Typ Auswahl +- [ ] Bei "Neues Projekt": Auswahl `Einsatz | Übung` +- [ ] DB-Schema: `Project.type: 'einsatz' | 'uebung'` +- [ ] UI-Anpassung in `app/page.tsx` + +#### 2.2 Übungs-Metadaten +- [ ] Felder: Übungstitel, Datum, **Übungsziele (Liste)** +- [ ] Übungsleiter, Teilnehmer (optional) +- [ ] Eigene Sidebar-Sektion für Übungsdaten + +#### 2.3 Journal ausblenden bei Übung +- [ ] Bei `type === 'uebung'`: Journal-Tab → "Übungsauswertung" +- [ ] Karte bleibt voll funktionsfähig +- [ ] Krokierung kann **im Voraus** erstellt und mit Übungsleitern geteilt werden (bestehender Share-Link) + +#### 2.4 Übungsauswertung +- [ ] Checkliste **Übungsziele** mit Status: `erreicht | teilweise | nicht erreicht` +- [ ] Notizen / Erkenntnisse pro Ziel +- [ ] Gesamtbewertung / Erkenntnisse +- [ ] **PDF-Export** für Debriefing (analog Rapport) +- **Files**: neue Komponente `uebungsauswertung-tab.tsx`, neuer PDF-Renderer + +--- + +### Phase 3 — Linien mit Typ (1 Woche) ⭐ + +Smart Lines mit vordefinierten Stilen. + +#### 3.1 Linientypen definieren +- [ ] **Schlauch** (default, aktuelle Linie) +- [ ] **Rettungsachse** — gestrichelt, Label "R" +- [ ] **Leitung** — blau +- [ ] **Absperrung** — rot gestrichelt +- [ ] **Frei** — Standard ohne Vorlage + +#### 3.2 UX +- [ ] Nach Zeichnen einer Linie: kleines Popup "Was ist das?" +- [ ] Auswahl per Klick → Linie wird automatisch gestylt +- [ ] In Sidebar pro Linie nachträglich änderbar (Dropdown) +- [ ] Tastatur-Shortcut für Typ-Wechsel? + +#### 3.3 Daten-Modell +- [ ] `DrawFeature` erweitern: `lineCategory: 'hose' | 'rescue' | 'pipe' | 'barrier' | 'free'` +- [ ] Default-Stile als Konstante (Farbe, Strich, Label) +- **Files**: `src/types/`, `tool-store.ts`, `map-view.tsx` + +--- + +### Phase 4 — Rapport-/Lageansicht (Führungsunterstützung) (2-3 Wochen) ⭐ + +Eigenes Modul, hoher Mehrwert für FU. + +#### 4.1 Sichtbarkeits-Stufen pro Journaleintrag +- [ ] DB: `JournalEntry.visibility: 'intern' | 'rapport' | 'public'` +- [ ] UI: Dropdown beim Erstellen / Editieren +- [ ] Migration: bestehende Einträge → `intern` (sicher) + +#### 4.2 Rapport-Modus (Display) +- [ ] Neuer Tab/Modus: **Rapport-Ansicht** +- [ ] Sektionen: + - **Aktuelle Lage** (Freitext, prominentes Feld) + - **Pendenzen** (Liste mit Status: offen/erledigt) + - **Wichtige Punkte / Befehle** + - **Mittel / Personal** (aktueller Stand) +- [ ] Grosse Schrift, kontrastreich (Beamer-tauglich) +- [ ] Auto-Refresh via Socket.IO + +#### 4.3 Display-Sharing +- [ ] Read-only Token-Link (wie bestehende Rapport-URL) +- [ ] Vollbild-Modus +- [ ] Optional: Auswahl welche Sektionen sichtbar + +#### 4.4 Berechtigungen +- [ ] Rolle "Rapport-Viewer" — sieht nur Rapport, nicht Karte / Journal +- [ ] Berechtigung pro Projekt vergebbar + +--- + +### Phase 5 — Multi-User Live-Editing polish (optional, später) + +Realtime-Sync läuft schon via Socket.IO — hier nur Verbesserungen. + +- [ ] **Live-Cursor** anderer User auf Karte (mit Name) +- [ ] **Presence-Anzeige** ("Fabian schreibt im Journal", "Pepe zeichnet") +- [ ] **Soft-Locks** während Symbol bewegt wird +- [ ] **Conflict Resolution** bei gleichzeitigem Edit testen + +--- + +## Priorisierungs-Matrix + +| Phase | Aufwand | Impact | Priorität | Empfohlene Reihenfolge | +|-------|---------|--------|-----------|------------------------| +| 1 — Symbol-Architektur Redesign | L (3-4 W) | Sehr Hoch | ⭐⭐ Höchste | **1.** | +| 1.5 — Quick Polish | S | Mittel | 🔥 Hoch | parallel zu 1 | +| 3 — Linien-Typen | M | Hoch | ⭐ Hoch | **2.** | +| 4 — Rapport-Ansicht | L | Sehr Hoch (FU) | ⭐ Sehr Hoch | **3.** | +| 2 — Übungsmodus | L | Sehr Hoch | ⭐ Sehr Hoch | **4.** | +| 5 — Multi-User polish | M | Mittel | 📅 Mittel | später | + +**Begründung der Reihenfolge**: +1. **Phase 1 zuerst** — Architektur-Umbau, weil alles andere darauf aufbaut. Sobald TenantSymbols + Kategorien sauber strukturiert sind, sind 1.3 (fehlende Symbole) und 1.4 (Stockwerke) trivial. Auch Phase 2 (Übungsmodus) profitiert: Übungs-Pakete als eigene Templates möglich. +2. **Phase 3 (Linien-Typen)** — kleine Erweiterung, hoher UX-Gewinn, unabhängig von Phase 1. +3. **Phase 4 (Rapport-Ansicht)** — USP gegenüber anderen Krokier-Tools, FU-Funktion fehlt überall sonst. +4. **Phase 2 (Übungsmodus)** — grosses Update; baut konzeptuell auf Phase 4 auf (Übungsauswertung ≈ Rapport). +5. **Phase 5** — Polish, kein User wartet darauf. + +**Risiko-Hinweis Phase 1**: +- Schema-Migration von `Icon` (global) zu `SymbolTemplate` + Auto-Import muss **wasserdicht** sein, damit existierende Mandanten ihre Symbole nicht verlieren +- Vor Deploy: vollständiges DB-Backup (PostgreSQL) und MinIO-Backup (Icon-Files) +- Auf Staging testen falls möglich, sonst kleinen Test-Tenant zuerst migrieren + +--- + +## Hinweise für Entwickler / KIs + +- **Stack**: Next.js 15, React 19, MapLibre GL JS, Prisma, PostgreSQL, MinIO, Socket.IO, Zustand +- **Sprache**: Schweizerdeutsch / Hochdeutsch (Code englisch, UI deutsch) +- **Bei Symbol-Änderungen**: SVGs in `public/icons/` + Symbol-Definitionen + ggf. DB-Migration +- **Bei Schema-Änderungen**: Prisma Migration erstellen + DB-Backup vor Deploy +- **Vor Deploy**: Build testen (`npx next build`), commit + push, Portainer baut automatisch via Webhook +- **Realtime**: bei DB-Änderungen, die alle User sehen müssen, Socket-Event triggern + +--- + +## Status-Tracking + +> Status pro Aufgabe oben in Checkboxen. Hier kurzer Überblick: + +- [ ] **Phase 1** — nicht gestartet +- [ ] **Phase 2** — nicht gestartet +- [ ] **Phase 3** — nicht gestartet +- [ ] **Phase 4** — nicht gestartet +- [ ] **Phase 5** — nicht gestartet + +--- + +**Letztes Update**: 2026-05-20 +**Verantwortlich**: Pepe (adminpepe) diff --git a/plans/phase-1-symbol-architecture.md b/plans/phase-1-symbol-architecture.md new file mode 100644 index 0000000..428052a --- /dev/null +++ b/plans/phase-1-symbol-architecture.md @@ -0,0 +1,370 @@ +# Phase 1 — Symbol-Architektur Redesign: Detaillierter Plan + +> Basierend auf: `docs/roadmap-feedback-fabian.md` +> Ziel: Mandantenspezifische Symbol-Bibliothek mit Template-Import, eigener Kategorisierung und vollständiger Entkopplung von globalen Icons. + +--- + +## 1. Ziel & Konzept + +**Problem heute:** +- `TenantSymbol` verweist auf `IconAsset` (global). Mandant kann zwar `customName` setzen, aber nicht die Kategorie ändern, das SVG bearbeiten oder Symbole aus verschiedenen Paketen frei mischen. +- Kategorien (`IconCategory`) sind global mit `tenantId` Override — uneinheitlich. +- Keine Möglichkeit, Vorlagen-Pakete (Feuerwehr CH, THW, Sanität) als Unit zu importieren. + +**Lösung:** +- `SymbolTemplate` = globale, read-only Vorlagen (je Paket). Wird einmalig aus bestehenden `public/signaturen/` und `IconAsset` generiert. +- `TenantCategory` = pro Mandant, frei anlegbar/umbenennbar/sortierbar. +- `TenantSymbol` = pro Mandant, **vollständig eigenständig** (`name`, `svgPath`, `categoryId`). Kein Verweis mehr auf globale `IconAsset`. +- Mandant startet mit leerer Bibliothek und importiert Pakete nach Bedarf. + +--- + +## 2. Datenmodell-Änderungen + +### 2.1 Neue Modelle + +```prisma +model SymbolTemplate { + id String @id @default(uuid()) + packageId String // z.B. "feuerwehr-ch" + packageName String // z.B. "Feuerwehr Schweiz" + categoryName String // z.B. "Fahrzeuge" + name String + svgPath String // Relativer Pfad in public/ oder SVG-Inhalt + tags String[] @default([]) + sortOrder Int @default(0) + + @@index([packageId]) + @@map("symbol_templates") +} + +model TenantCategory { + id String @id @default(uuid()) + tenantId String + tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade) + name String + sortOrder Int @default(0) + icon String? // Optional: Emoji/Lucide-Icon-Name für UI + + symbols TenantSymbol[] + + @@unique([tenantId, name]) + @@index([tenantId]) + @@map("tenant_categories") +} +``` + +### 2.2 Bestehendes `TenantSymbol` umbauen + +**Vorher:** +```prisma +model TenantSymbol { + id String @id @default(uuid()) + customName String? + sortOrder Int @default(0) + tenantId String + tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade) + iconId String + icon IconAsset @relation(fields: [iconId], references: [id], onDelete: Cascade) +} +``` + +**Nachher:** +```prisma +model TenantSymbol { + id String @id @default(uuid()) + tenantId String + tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade) + + categoryId String + category TenantCategory @relation(fields: [categoryId], references: [id], onDelete: Cascade) + + name String // endgültiger Anzeigename (kann aus Template importiert oder custom sein) + svgPath String // z.B. "signaturen/TLF.svg" oder tenant-spezifischer MinIO-Key + sortOrder Int @default(0) + isUploaded Boolean @default(false) // true = eigener Upload, false = aus Template importiert + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + // Legacy-Feld für Migration (nach erfolgreichem Deploy entfernen) + migratedFromIconId String? + + @@index([tenantId]) + @@index([categoryId]) + @@map("tenant_symbols") +} +``` + +> **Hinweis:** `IconAsset`, `IconCategory` und `hiddenIconIds` bleiben vorerst bestehen (read-only Legacy), werden aber nicht mehr für neue Features verwendet. In einer späteren Phase können sie entfernt werden. + +--- + +## 3. Migrationstrategie + +### 3.1 Vor dem Deploy +- **PostgreSQL-Backup** erstellen +- **MinIO-Backup** der Icon-Dateien +- Auf Staging testen (falls vorhanden) + +### 3.2 Schritt-für-Schritt-Migration (in einer Transaktion) + +1. **Neue Tabellen anlegen** (`SymbolTemplate`, `TenantCategory`, neue `TenantSymbol`-Spalten) +2. **SymbolTemplate füllen:** + - Alle `public/signaturen/*.svg` einlesen + - Bestehende `IconAsset` mit `isSystem = true` als `feuerwehr-ch` Paket überführen + - Zuordnung: `categoryName` aus `IconCategory.name` +3. **TenantCategory pro Tenant anlegen:** + - Für jeden Tenant eine Default-Kategorie "Meine Symbole" erstellen + - Optional: Weitere Kategorien aus `IconCategory` ableiten (nur wenn `tenantId` gesetzt) +4. **TenantSymbol migrieren:** + - Für jeden bestehenden `TenantSymbol`-Eintrag: + - `name` = `customName || icon.name` + - `svgPath` = `icon.fileKey` + - `categoryId` = Default-Kategorie des Tenants (oder `icon.categoryId` wenn passend) + - `migratedFromIconId` = `iconId` (für Nachvollziehbarkeit) +5. **App-Code auf neue Modelle umstellen** +6. **Alte Relation `TenantSymbol.icon` und `TenantSymbol.iconId` entfernen** (nach erfolgreichem Live-Test) + +### 3.3 Rollback-Plan +Falls etwas schiefgeht: Backup wiederherstellen. Die Migration ist idempotent (neue Tabellen, alte bleiben erhalten). + +--- + +## 4. API-Design + +### 4.1 Templates + +``` +GET /api/templates + → { packages: [{ id, name, description, symbolCount, previewUrls }] } + +GET /api/templates?packageId=feuerwehr-ch + → { packageId, packageName, categories: [{ categoryName, symbols: [{ id, name, svgPath, tags }] }] } + +POST /api/templates/import + Body: { packageId: "feuerwehr-ch", symbolIds?: ["id1", "id2"] } + → importiert ausgewählte Symbole als TenantSymbols (oder alle wenn symbolIds fehlt) + → antwortet mit [{ tenantSymbolId, name, categoryId }] +``` + +### 4.2 Tenant Categories (Admin) + +``` +GET /api/tenant/categories + → [{ id, name, sortOrder, icon, symbolCount }] + +POST /api/tenant/categories + Body: { name, sortOrder?, icon? } + → { id, name, sortOrder, icon } + +PATCH /api/tenant/categories/:id + Body: { name?, sortOrder?, icon? } + → updated category + +DELETE /api/tenant/categories/:id + → 204 (nur erlaubt wenn leer, sonst 409) +``` + +### 4.3 Tenant Symbols (Admin + Sidebar) + +``` +GET /api/tenant/symbols + → { categories: [{ id, name, sortOrder, symbols: [{ id, name, svgPath, sortOrder, isUploaded }] }] } + // Gruppiert nach TenantCategory + +POST /api/tenant/symbols + Body: { templateId } // Import aus Template + Body: { name, svgPath, categoryId } // Manuelle Erstellung + → { id, name, svgPath, categoryId, sortOrder } + +POST /api/tenant/symbols/upload + Multipart: { file (SVG/PNG), name, categoryId } + → uploaded TenantSymbol + +PATCH /api/tenant/symbols/:id + Body: { name?, categoryId?, sortOrder? } + → updated + +DELETE /api/tenant/symbols/:id + → 204 +``` + +### 4.4 Icon-Serving (unchanged path für Kompatibilität) + +``` +GET /api/icons/:tenantSymbolId/image + → Liest `TenantSymbol.svgPath` und serviert Datei (aus public/ oder MinIO) +``` + +--- + +## 5. Frontend-Änderungen + +### 5.1 Admin — Symbol-Manager (`src/components/admin/symbol-manager.tsx`) + +**Umbau in 3 Bereiche:** + +``` +┌─ Symbol-Manager ─────────────────────────────────┐ +│ │ +│ [+ Kategorie anlegen] [📦 Vorlagen importieren] │ +│ │ +│ ┌─ Fahrzeuge ─────────────┐ [⋮] [✎] [🗑] │ +│ │ 🚒 TLF [✎] [🗑] [↕] │ +│ │ 🚒 RW [✎] [🗑] [↕] │ +│ │ [+ Symbol hinzufügen] │ +│ └──────────────────────────┘ │ +│ ┌─ Wasser ─────────────────┐ [⋮] [✎] [🗑] │ +│ │ 🟦 Hydrant [✎] [🗑] [↕] │ +│ └──────────────────────────┘ │ +│ │ +│ [Eigenes SVG hochladen] │ +│ │ +└────────────────────────────────────────────────────┘ +``` + +**Features:** +- Kategorien per Drag & Drop sortieren (`@dnd-kit` oder native) +- Symbole zwischen Kategorien verschieben (Drag & Drop) +- Kategorie anlegen/umbenennen/löschen (nur wenn leer) +- Symbol umbenennen, Kategorie ändern, löschen +- "Vorlagen importieren" öffnet Dialog mit Paket-Vorschau + +### 5.2 Admin — Import-Dialog (`src/components/admin/import-templates-dialog.tsx`) + +``` +┌─ Vorlagen importieren ─────────────────────────────┐ +│ │ +│ [📦 Feuerwehr Schweiz] [📦 THW] [📦 Sanität] │ +│ │ +│ ┌─ Feuerwehr Schweiz ───────────────────────────┐│ +│ │ Kategorie: Fahrzeuge (3 Symbole) ││ +│ │ ☑️ TLF ☑️ RW ☐ DLK ││ +│ │ Kategorie: Wasser (5 Symbole) ││ +│ │ ☑️ Hydrant ☑️ Löschwasser ││ +│ │ ││ +│ │ [Alle auswählen] [Ausgewählte importieren] ││ +│ └────────────────────────────────────────────────┘│ +│ │ +└────────────────────────────────────────────────────┘ +``` + +### 5.3 Sidebar / LeftToolbar — Symbol-Palette umbauen + +Aktuell gibt es zwei Komponenten: +- `LeftToolbar` = Zeichenwerkzeuge (Bleistift, Linie, etc.) +- Symbol-Palette = vermutlich in `map-view.tsx` oder separater Komponente + +**Ziel:** Die Symbol-Palette (die Symbole die auf die Karte gezogen werden) muss nach `TenantCategory` gruppiert werden. + +Da die Symbol-Palette vermutlich inline in `map-view.tsx` oder einer anderen Komponente ist, suchen und extrahieren in eine eigene `SymbolPalette`-Komponente: + +```tsx +// src/components/map/symbol-palette.tsx +interface SymbolPaletteProps { + categories: TenantCategoryWithSymbols[] + onSymbolDragStart: (symbol: TenantSymbol) => void + canEdit: boolean +} +``` + +**Layout:** +- Collapsible Kategorien (wie aktuell in Symbol-Manager) +- Symbole als Grid pro Kategorie +- Search-Input oben (Volltext über Name + Tags) +- Recent/Favoriten-Sektion (später in Phase 1.5) + +### 5.4 DrawFeature — `instanceLabel` für Stockwerke (Phase 1.4) + +Erweiterung des `properties`-Objekts: +```ts +interface SymbolProperties { + iconId: string + scale: number + rotation: number + instanceLabel?: string // z.B. "EG+3", "Wohngebäude A" +} +``` + +Renderer: Badge auf dem Symbol-Overlay (MapLibre Marker oder CSS-Overlay). +Edit: Doppelklick auf Symbol → kleiner Inline-Edit oder Dialog. + +--- + +## 6. Dateien: Create / Modify / Delete + +### Neue Dateien +| Pfad | Beschreibung | +|------|-------------| +| `prisma/migrations/2026xxxx_symbol_architecture/` | Prisma Migration | +| `prisma/seed-symbol-templates.ts` | Seed-Skript: `public/signaturen/*.svg` → `SymbolTemplate` | +| `src/app/api/templates/route.ts` | GET /api/templates | +| `src/app/api/templates/import/route.ts` | POST /api/templates/import | +| `src/app/api/tenant/categories/route.ts` | CRUD TenantCategory | +| `src/app/api/tenant/categories/[id]/route.ts` | PATCH/DELETE einzelne Kategorie | +| `src/components/admin/import-templates-dialog.tsx` | Import-Dialog UI | +| `src/components/map/symbol-palette.tsx` | Extrahierte Symbol-Palette | + +### Zu modifizierende Dateien +| Pfad | Änderung | +|------|----------| +| `prisma/schema.prisma` | Neue Modelle + TenantSymbol umbauen | +| `src/app/api/tenant/symbols/route.ts` | Refactor: Gruppierung nach Category, Upload, CRUD | +| `src/app/api/icons/route.ts` | Legacy-Modus, ggf. auf TenantSymbol umleiten | +| `src/components/admin/symbol-manager.tsx` | Vollständiger Umbau mit Kategorie-Verwaltung | +| `src/app/app/page.tsx` | Symbol-Palette Props anpassen | +| `src/components/map/map-view.tsx` | Symbol-Rendering mit instanceLabel Badge | +| `src/types/index.ts` | Neue Typen: `TenantCategory`, `TenantSymbol`, `SymbolTemplate` | + +--- + +## 7. Ausführungsreihenfolge (Execution Order) + +### Sprint A — Schema & Daten (Woche 1) +1. `prisma/schema.prisma` erweitern (`SymbolTemplate`, `TenantCategory`, `TenantSymbol` Refactor) +2. Prisma Migration erstellen & testen (`npx prisma migrate dev`) +3. `prisma/seed-symbol-templates.ts` schreiben (feuerwehr-ch Paket aus `public/signaturen/`) +4. Migration-Skript für bestehende Tenants (Default-Kategorie + TenantSymbol-Migration) +5. Build testen, auf Staging deployen + +### Sprint B — API (Woche 1-2) +6. `GET /api/templates` + `POST /api/templates/import` +7. `CRUD /api/tenant/categories` +8. `Refactor /api/tenant/symbols` (Gruppierung, Upload, Kategorie-Zuordnung) +9. `GET /api/icons/:id/image` an TenantSymbol anpassen + +### Sprint C — Admin UI (Woche 2-3) +10. Symbol-Manager: Kategorie-Verwaltung (anlegen/umbenennen/löschen/sortieren) +11. Symbol-Manager: Import-Dialog (Paket-Vorschau, granulare Auswahl) +12. Symbol-Manager: Eigenen SVG-Upload mit Kategorie-Zuordnung +13. Symbol-Manager: Drag & Drop (Kategorien sortieren, Symbole verschieben) + +### Sprint D — Frontend Sidebar & Polish (Woche 3-4) +14. `SymbolPalette`-Komponente extrahieren und nach Kategorien gruppieren +15. Symbol-Suche in Sidebar (Volltext) +16. `instanceLabel` / Stockwerke implementieren (Phase 1.4) +17. Häufig-benutzte Symbole (Recent) — Phase 1.5 +18. End-to-End-Test, Deploy + +--- + +## 8. Risiken & Entscheidungen + +| Thema | Option A (empfohlen) | Option B | +|-------|---------------------|----------| +| **SVG-Speicherort** | `svgPath` = relativer Pfad in `public/signaturen/` (klein, schnell, kein MinIO nötig für Templates) | `svgPath` = MinIO-Key (konsistent mit Uploads, aber Overhead) | +| **TenantSymbol Uploads** | Eigenes MinIO-Bucket `tenant-{id}/symbols/` | In DB als Text speichern (Base64 oder SVG-String) | +| **Migration alter Tenants** | Auto-Import feuerwehr-ch Paket + Default-Kategorie | Manuelle Migration pro Tenant | +| **LeftToolbar vs SymbolPalette** | SymbolPalette als separate Komponente neben LeftToolbar | In LeftToolbar integrieren | + +**Empfehlung:** +- Templates: `public/signaturen/` Pfade in DB (read-only, kein MinIO-Overhead) +- Uploads: MinIO `tenant-{id}/symbols/` +- Migration: Vollautomatisch beim ersten Login nach Deploy (kein manueller Eingriff) + +--- + +*Plan erstellt: 2026-05-20* +*Nächster Schritt: Genehmigung durch Pepe, dann Sprint A starten* diff --git a/prisma/migrate-tenant-symbols.ts b/prisma/migrate-tenant-symbols.ts new file mode 100644 index 0000000..9136844 --- /dev/null +++ b/prisma/migrate-tenant-symbols.ts @@ -0,0 +1,112 @@ +/** + * Post-Migration Script: Migrate existing TenantSymbols to new architecture. + * + * Run AFTER applying the schema migration (20260520_symbol_architecture). + * + * Steps: + * 1. Create a default "Meine Symbole" TenantCategory for each tenant that has TenantSymbols + * 2. Migrate existing TenantSymbols: set name, svgPath, categoryId, migratedFromIconId + * 3. Verify consistency + * + * Usage: + * npx ts-node prisma/migrate-tenant-symbols.ts + */ + +import { PrismaClient } from '@prisma/client' + +const prisma = new PrismaClient() + +async function main() { + console.log('🔧 Migrating existing TenantSymbols...') + + // Step 1: Get all tenants that have existing tenantSymbols + const tenantsWithSymbols = await (prisma as any).tenantSymbol.groupBy({ + by: ['tenantId'], + _count: { id: true }, + }) + + console.log(`Found ${tenantsWithSymbols.length} tenants with existing symbols`) + + let categoriesCreated = 0 + let symbolsMigrated = 0 + + for (const group of tenantsWithSymbols) { + const tenantId = group.tenantId + + // Step 2: Create default category for this tenant + let defaultCategory = await (prisma as any).tenantCategory.findFirst({ + where: { tenantId, name: 'Meine Symbole' }, + }) + + if (!defaultCategory) { + defaultCategory = await (prisma as any).tenantCategory.create({ + data: { + tenantId, + name: 'Meine Symbole', + sortOrder: 0, + }, + }) + categoriesCreated++ + } + + // Step 3: Migrate all tenantSymbols for this tenant + const tenantSymbols = await (prisma as any).tenantSymbol.findMany({ + where: { tenantId }, + include: { icon: true }, + }) + + for (const ts of tenantSymbols) { + const updates: any = {} + + // name = customName || icon.name + if (!ts.name) { + updates.name = ts.customName || ts.icon?.name || 'Unbenannt' + } + + // svgPath = icon.fileKey + if (!ts.svgPath && ts.icon?.fileKey) { + updates.svgPath = ts.icon.fileKey + } + + // categoryId = default category + if (!ts.categoryId) { + updates.categoryId = defaultCategory.id + } + + // migratedFromIconId = current iconId + if (!ts.migratedFromIconId) { + updates.migratedFromIconId = ts.iconId + } + + if (Object.keys(updates).length > 0) { + await (prisma as any).tenantSymbol.update({ + where: { id: ts.id }, + data: updates, + }) + symbolsMigrated++ + } + } + } + + console.log(`✅ Done: ${categoriesCreated} categories created, ${symbolsMigrated} symbols migrated`) + + // Verification + const unmappedCount = await (prisma as any).tenantSymbol.count({ + where: { categoryId: null }, + }) + + if (unmappedCount > 0) { + console.warn(`⚠️ ${unmappedCount} tenantSymbols still have no categoryId!`) + } else { + console.log('✅ All tenantSymbols have a category assigned') + } +} + +main() + .catch((e) => { + console.error(e) + process.exit(1) + }) + .finally(async () => { + await prisma.$disconnect() + }) diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 9ed69b5..4799ce1 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -90,6 +90,7 @@ model Tenant { iconCategories IconCategory[] iconAssets IconAsset[] tenantSymbols TenantSymbol[] + tenantCategories TenantCategory[] upgradeRequests UpgradeRequest[] dictionaryEntries DictionaryEntry[] rapports Rapport[] @@ -392,10 +393,53 @@ model TenantSymbol { iconId String icon IconAsset @relation(fields: [iconId], references: [id], onDelete: Cascade) + // New fields for Phase 1 Symbol Architecture + categoryId String? + category TenantCategory? @relation(fields: [categoryId], references: [id], onDelete: SetNull) + + name String? // Display name (migrated from customName || icon.name) + svgPath String? // e.g. "signaturen/TLF.svg" or tenant-specific MinIO key + isUploaded Boolean @default(false) + migratedFromIconId String? + @@index([tenantId]) + @@index([categoryId]) @@map("tenant_symbols") } +// ─── Symbol Templates (global read-only packages) ───────── + +model SymbolTemplate { + id String @id @default(uuid()) + packageId String // e.g. "feuerwehr-ch" + packageName String // e.g. "Feuerwehr Schweiz" + categoryName String // e.g. "Fahrzeuge" + name String + svgPath String // relative path in public/ or MinIO key + tags String[] @default([]) + sortOrder Int @default(0) + + @@index([packageId]) + @@map("symbol_templates") +} + +// ─── Tenant Categories (per-tenant, user-managed) ───────── + +model TenantCategory { + id String @id @default(uuid()) + tenantId String + tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade) + name String + sortOrder Int @default(0) + icon String? // Optional emoji or Lucide icon name for UI + + symbols TenantSymbol[] + + @@unique([tenantId, name]) + @@index([tenantId]) + @@map("tenant_categories") +} + // ─── Dictionary (Global + Tenant word library) ──────────── model DictionaryEntry { diff --git a/prisma/seed-symbol-templates.ts b/prisma/seed-symbol-templates.ts new file mode 100644 index 0000000..2df84ff --- /dev/null +++ b/prisma/seed-symbol-templates.ts @@ -0,0 +1,123 @@ +/** + * Seed-Skript: Erstellt SymbolTemplate-Einträge aus bestehenden IconAsset (isSystem=true) + * oder aus dem Dateisystem (public/signaturen/*.svg). + * + * Ausführen: + * npx ts-node prisma/seed-symbol-templates.ts + * oder als Teil des Deployments via npx prisma db seed + * + * Idempotent: bereits existierende Templates werden übersprungen. + */ + +import { PrismaClient } from '@prisma/client' +import { readdirSync, statSync } from 'fs' +import { join } from 'path' + +const prisma = new PrismaClient() + +const SIGNATUREN_DIR = join(process.cwd(), 'public', 'signaturen') +const PACKAGE_ID = 'feuerwehr-ch' +const PACKAGE_NAME = 'Feuerwehr Schweiz' + +/** + * Versucht Kategorie-Namen aus bestehenden IconAsset / IconCategory abzuleiten. + * Fallback: "Sonstiges" + */ +async function getCategoryMapping(): Promise> { + const mapping = new Map() + + const iconAssets = await (prisma as any).iconAsset.findMany({ + where: { isSystem: true }, + include: { category: { select: { name: true } } }, + }) + + for (const asset of iconAssets) { + const fileName = asset.fileKey.replace(/^signaturen\//, '').replace(/\.svg$/i, '') + const categoryName = asset.category?.name || 'Sonstiges' + mapping.set(fileName.toLowerCase(), categoryName) + } + + return mapping +} + +/** + * Liest alle .svg Dateien aus public/signaturen/ + */ +function getSvgFiles(dir: string): string[] { + const files: string[] = [] + try { + const entries = readdirSync(dir) + for (const entry of entries) { + const fullPath = join(dir, entry) + const stat = statSync(fullPath) + if (stat.isFile() && entry.endsWith('.svg')) { + files.push(entry) + } + } + } catch (err) { + console.warn('Could not read signaturen directory:', (err as Error).message) + } + return files.sort() +} + +async function main() { + console.log('🌱 Seeding SymbolTemplate...') + + const categoryMapping = await getCategoryMapping() + const svgFiles = getSvgFiles(SIGNATUREN_DIR) + + console.log(`Found ${svgFiles.length} SVG files in public/signaturen/`) + console.log(`IconAsset mapping covers ${categoryMapping.size} entries`) + + let created = 0 + let skipped = 0 + + for (let i = 0; i < svgFiles.length; i++) { + const fileName = svgFiles[i] + const nameWithoutExt = fileName.replace(/\.svg$/i, '') + const displayName = nameWithoutExt + .replace(/_/g, ' ') + .replace(/-/g, ' ') + .replace(/([a-z])([A-Z])/g, '$1 $2') + + const fileKey = `signaturen/${fileName}` + + // Check if already exists + const existing = await (prisma as any).symbolTemplate.findFirst({ + where: { svgPath: fileKey }, + }) + + if (existing) { + skipped++ + continue + } + + const categoryName = + categoryMapping.get(nameWithoutExt.toLowerCase()) || 'Sonstiges' + + await (prisma as any).symbolTemplate.create({ + data: { + packageId: PACKAGE_ID, + packageName: PACKAGE_NAME, + categoryName, + name: displayName, + svgPath: fileKey, + tags: [nameWithoutExt.toLowerCase(), categoryName.toLowerCase()], + sortOrder: i, + }, + }) + + created++ + } + + console.log(`✅ Done: ${created} created, ${skipped} skipped`) +} + +main() + .catch((e) => { + console.error(e) + process.exit(1) + }) + .finally(async () => { + await prisma.$disconnect() + })