Files
Lageplan/plans/phase-1-symbol-architecture.md
Pepe Ziberi 4b92df8fea
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 14m40s
feat(schema): Phase 1 Symbol Architecture — SymbolTemplate, TenantCategory, TenantSymbol refactor + seed + migration scripts
2026-05-20 11:11:32 +02:00

14 KiB

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

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:

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:

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:

// 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:

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/*.svgSymbolTemplate
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)

  1. GET /api/templates + POST /api/templates/import
  2. CRUD /api/tenant/categories
  3. Refactor /api/tenant/symbols (Gruppierung, Upload, Kategorie-Zuordnung)
  4. GET /api/icons/:id/image an TenantSymbol anpassen

Sprint C — Admin UI (Woche 2-3)

  1. Symbol-Manager: Kategorie-Verwaltung (anlegen/umbenennen/löschen/sortieren)
  2. Symbol-Manager: Import-Dialog (Paket-Vorschau, granulare Auswahl)
  3. Symbol-Manager: Eigenen SVG-Upload mit Kategorie-Zuordnung
  4. Symbol-Manager: Drag & Drop (Kategorien sortieren, Symbole verschieben)

Sprint D — Frontend Sidebar & Polish (Woche 3-4)

  1. SymbolPalette-Komponente extrahieren und nach Kategorien gruppieren
  2. Symbol-Suche in Sidebar (Volltext)
  3. instanceLabel / Stockwerke implementieren (Phase 1.4)
  4. Häufig-benutzte Symbole (Recent) — Phase 1.5
  5. 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