# 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*