diff --git a/plans/phase-1-symbol-architecture.md b/plans/phase-1-symbol-architecture.md index 428052a..cf87ae4 100644 --- a/plans/phase-1-symbol-architecture.md +++ b/plans/phase-1-symbol-architecture.md @@ -2,6 +2,7 @@ > Basierend auf: `docs/roadmap-feedback-fabian.md` > Ziel: Mandantenspezifische Symbol-Bibliothek mit Template-Import, eigener Kategorisierung und vollständiger Entkopplung von globalen Icons. +> **Status: ABGESCHLOSSEN** ✅ (Sprints A–D implementiert, getestet und gepusht) --- @@ -40,14 +41,16 @@ model SymbolTemplate { } 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 + id String @id @default(uuid()) + tenantId String + tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade) + name String + description String? + sortOrder Int @default(0) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt - symbols TenantSymbol[] + symbols TenantSymbol[] @@unique([tenantId, name]) @@index([tenantId]) @@ -55,7 +58,7 @@ model TenantCategory { } ``` -### 2.2 Bestehendes `TenantSymbol` umbauen +### 2.2 Bestehendes `TenantSymbol` umbaut **Vorher:** ```prisma @@ -77,18 +80,18 @@ model TenantSymbol { tenantId String tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade) - categoryId String - category TenantCategory @relation(fields: [categoryId], references: [id], onDelete: Cascade) + categoryId String? + category TenantCategory? @relation(fields: [categoryId], references: [id], onDelete: SetNull) name String // endgültiger Anzeigename (kann aus Template importiert oder custom sein) - svgPath String // z.B. "signaturen/TLF.svg" oder tenant-spezifischer MinIO-Key + 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) + // Legacy-Feld für Migration (kann später entfernt werden) migratedFromIconId String? @@index([tenantId]) @@ -101,270 +104,164 @@ model TenantSymbol { --- -## 3. Migrationstrategie +## 3. Migration & Seed (Sprint A) ✅ -### 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). +- `prisma/migrations/20260520_symbol_architecture/migration.sql` — erstellt `symbol_templates`, `tenant_categories`, neue Spalten in `tenant_symbols` +- `prisma/seed-symbol-templates.ts` — füllt `SymbolTemplate` aus `public/signaturen/*.svg` +- `prisma/migrate-tenant-symbols.ts` — migriert bestehende `TenantSymbol`-Einträge +- `prisma/migrate.js` — erweitert um Migrationsschritte für Produktion +- Alle Seed-Skripte sind idempotent (`upsert` statt `deleteMany`) --- -## 4. API-Design +## 4. API-Design (Sprint B) ✅ ### 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 }] }] } + → { packages: [{ packageId, packageName, categoryCount, symbolCount, previewSymbols }] } 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 }] + Body: { packageId } + → importiert alle Symbole des Pakets als TenantSymbols + → antwortet mit { imported, skipped, message } ``` -### 4.2 Tenant Categories (Admin) +**Implementiert:** +- `src/app/api/templates/route.ts` — GET mit `groupBy` Aggregation und Vorschau +- `src/app/api/templates/import/route.ts` — POST mit Auto-Kategorie-Erstellung und Deduplikation + +### 4.2 Tenant Categories ``` -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) +GET /api/tenant/categories → [{ id, name, sortOrder, description }] +POST /api/tenant/categories Body: { name } → neue Kategorie +PATCH /api/tenant/categories Body: { id, name } → umbenennen +DELETE /api/tenant/categories?id=... → 204 oder 409 wenn nicht leer ``` -### 4.3 Tenant Symbols (Admin + Sidebar) +**Implementiert:** `src/app/api/tenant/categories/route.ts` + +### 4.3 Tenant Symbols ``` -GET /api/tenant/symbols - → { categories: [{ id, name, sortOrder, symbols: [{ id, name, svgPath, sortOrder, isUploaded }] }] } - // Gruppiert nach TenantCategory +GET /api/tenant/symbols?grouped=true|false + → grouped=true: { groups: [{ category, symbols }], symbols: [...] } + → grouped=false: { symbols: [...] } POST /api/tenant/symbols - Body: { templateId } // Import aus Template - Body: { name, svgPath, categoryId } // Manuelle Erstellung - → { id, name, svgPath, categoryId, sortOrder } + - Multipart: { file, categoryId? } → Upload nach MinIO + - JSON: { templateId, categoryId? } → aus SymbolTemplate + - JSON: { iconId, customName?, categoryId? } → aus IconAsset (Legacy) -POST /api/tenant/symbols/upload - Multipart: { file (SVG/PNG), name, categoryId } - → uploaded TenantSymbol +PATCH /api/tenant/symbols Body: { id, name?, customName?, categoryId?, sortOrder? } -PATCH /api/tenant/symbols/:id - Body: { name?, categoryId?, sortOrder? } - → updated - -DELETE /api/tenant/symbols/:id - → 204 +DELETE /api/tenant/symbols Body: { id } → löscht TenantSymbol ``` -### 4.4 Icon-Serving (unchanged path für Kompatibilität) +**Implementiert:** `src/app/api/tenant/symbols/route.ts` + `src/app/api/tenant/symbols/[id]/image/route.ts` + +### 4.4 Icon-Serving (TenantSymbol-First Lookup) ``` -GET /api/icons/:tenantSymbolId/image - → Liest `TenantSymbol.svgPath` und serviert Datei (aus public/ oder MinIO) +GET /api/icons/:id/image + → 1. TenantSymbol (svgPath aus public/ oder MinIO) + → 2. Fallback: IconAsset (legacy) + +GET /api/tenant/symbols/:id/image + → TenantSymbol-Bild aus MinIO oder public/ ``` +**Implementiert:** +- `src/app/api/icons/[id]/image/route.ts` +- `src/app/api/tenant/symbols/[id]/image/route.ts` + --- -## 5. Frontend-Änderungen +## 5. Frontend-Änderungen (Sprints C + D) ✅ ### 5.1 Admin — Symbol-Manager (`src/components/admin/symbol-manager.tsx`) -**Umbau in 3 Bereiche:** +**Tabs:** +1. **Meine Symbole** — nach TenantCategory gruppierte Symbol-Liste + - Expandable Kategorien + - SymbolCard: Bild, Name (inline-edit), Kategorie-Select, Löschen + - Suche über alle Symbole +2. **Kategorien** — CRUD für TenantCategory + - Neue Kategorie erstellen + - Umbenennen (inline) + - Löschen nur wenn leer +3. **Vorlagen importieren** — Template-Pakete als Cards + - Vorschau der ersten 4 Symbole + - 1-Klick Import -``` -┌─ Symbol-Manager ─────────────────────────────────┐ -│ │ -│ [+ Kategorie anlegen] [📦 Vorlagen importieren] │ -│ │ -│ ┌─ Fahrzeuge ─────────────┐ [⋮] [✎] [🗑] │ -│ │ 🚒 TLF [✎] [🗑] [↕] │ -│ │ 🚒 RW [✎] [🗑] [↕] │ -│ │ [+ Symbol hinzufügen] │ -│ └──────────────────────────┘ │ -│ ┌─ Wasser ─────────────────┐ [⋮] [✎] [🗑] │ -│ │ 🟦 Hydrant [✎] [🗑] [↕] │ -│ └──────────────────────────┘ │ -│ │ -│ [Eigenes SVG hochladen] │ -│ │ -└────────────────────────────────────────────────────┘ -``` +**Actions:** +- Upload-Button → Dialog mit Drag & Drop + Kategorie-Auswahl +- Import-Button → Dialog mit allen verfügbaren Paketen -**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 Sidebar — RightSidebar (`src/components/layout/right-sidebar.tsx`) -### 5.2 Admin — Import-Dialog (`src/components/admin/import-templates-dialog.tsx`) +- Verwendet neue `tenantSymbolGroups` aus `/api/icons` +- **Meine Symbole** Sektion: nach TenantCategory gruppiert, expandable +- **Bibliothek** Sektion: globale IconAsset-Kategorien (Legacy, read-only) +- Drag & Drop funktioniert mit neuen TenantSymbol-IDs -``` -┌─ 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 API /icons — Kompatibilität -### 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. +`src/app/api/icons/route.ts` liefert: +- `categories` — Legacy IconAsset-Kategorien (für alte Clients) +- `mySymbols` — Legacy TenantSymbol-Liste (für alte Clients) +- `tenantSymbols` — Neue flache TenantSymbol-Liste +- `tenantSymbolGroups` — Neue gruppierte Liste für Sidebar --- -## 6. Dateien: Create / Modify / Delete +## 6. Dateien: Create / Modify -### Neue Dateien +### Neue Dateien ✅ | Pfad | Beschreibung | |------|-------------| -| `prisma/migrations/2026xxxx_symbol_architecture/` | Prisma Migration | -| `prisma/seed-symbol-templates.ts` | Seed-Skript: `public/signaturen/*.svg` → `SymbolTemplate` | +| `prisma/migrations/20260520_symbol_architecture/migration.sql` | Migration neue Tabellen + Spalten | +| `prisma/seed-symbol-templates.ts` | Seed: `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 | +| `src/app/api/tenant/symbols/[id]/image/route.ts` | TenantSymbol Bild-Serving | -### Zu modifizierende Dateien +### Modifizierte 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` | +| `prisma/schema.prisma` | Neue Modelle + TenantSymbol Refactor | +| `src/app/api/tenant/symbols/route.ts` | Refactor: Gruppierung, Upload, CRUD | +| `src/app/api/icons/route.ts` | Liefert tenantSymbolGroups | +| `src/app/api/icons/[id]/image/route.ts` | TenantSymbol-First Lookup | +| `src/components/admin/symbol-manager.tsx` | Vollständiger Umbau mit Kategorien, Import, Upload | +| `src/components/layout/right-sidebar.tsx` | Gruppierte TenantSymbol-Anzeige | --- -## 7. Ausführungsreihenfolge (Execution Order) +## 7. Ausführungsreihenfolge (Tatsächlich) -### 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 +| Sprint | Status | Inhalt | +|--------|--------|--------| +| **A** | ✅ Fertig | Schema, Migration, Seed, Daten-Migration | +| **B** | ✅ Fertig | Templates API, Categories API, Symbols API erweitert | +| **C** | ✅ Fertig | Admin UI: Symbol-Manager mit Kategorien, Import, Upload | +| **D** | ✅ Fertig | Frontend Sidebar gruppiert nach TenantCategory | --- -## 8. Risiken & Entscheidungen +## 8. Risiken & Entscheidungen (Umgesetzt) -| 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) +| Thema | Entscheidung | +|-------|-------------| +| **SVG-Speicherort** | Templates: `public/signaturen/` Pfade in DB (read-only, kein MinIO-Overhead). Uploads: MinIO `tenant-{id}/symbols/`. | +| **Migration alter Tenants** | Vollautomatisch via `prisma/migrate.js` + `prisma/migrate-tenant-symbols.ts`. | +| **Sidebar vs SymbolPalette** | `RightSidebar` direkt angepasst, keine separate `SymbolPalette`-Komponente nötig. | +| **instanceLabel / Stockwerke** | Nicht in Sprint D enthalten (laut Roadmap Phase 1.4 — kann separat geplant werden). | --- *Plan erstellt: 2026-05-20* -*Nächster Schritt: Genehmigung durch Pepe, dann Sprint A starten* +*Phase 1 abgeschlossen: 2026-05-20*