14 KiB
Phase 1 — Symbol-Architektur Redesign: Detaillierter Plan
Basierend auf:
docs/roadmap-feedback-fabian.mdZiel: Mandantenspezifische Symbol-Bibliothek mit Template-Import, eigener Kategorisierung und vollständiger Entkopplung von globalen Icons.
1. Ziel & Konzept
Problem heute:
TenantSymbolverweist aufIconAsset(global). Mandant kann zwarcustomNamesetzen, aber nicht die Kategorie ändern, das SVG bearbeiten oder Symbole aus verschiedenen Paketen frei mischen.- Kategorien (
IconCategory) sind global mittenantIdOverride — 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 bestehendenpublic/signaturen/undIconAssetgeneriert.TenantCategory= pro Mandant, frei anlegbar/umbenennbar/sortierbar.TenantSymbol= pro Mandant, vollständig eigenständig (name,svgPath,categoryId). Kein Verweis mehr auf globaleIconAsset.- 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,IconCategoryundhiddenIconIdsbleiben 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)
- Neue Tabellen anlegen (
SymbolTemplate,TenantCategory, neueTenantSymbol-Spalten) - SymbolTemplate füllen:
- Alle
public/signaturen/*.svgeinlesen - Bestehende
IconAssetmitisSystem = truealsfeuerwehr-chPaket überführen - Zuordnung:
categoryNameausIconCategory.name
- Alle
- TenantCategory pro Tenant anlegen:
- Für jeden Tenant eine Default-Kategorie "Meine Symbole" erstellen
- Optional: Weitere Kategorien aus
IconCategoryableiten (nur wenntenantIdgesetzt)
- TenantSymbol migrieren:
- Für jeden bestehenden
TenantSymbol-Eintrag:name=customName || icon.namesvgPath=icon.fileKeycategoryId= Default-Kategorie des Tenants (odericon.categoryIdwenn passend)migratedFromIconId=iconId(für Nachvollziehbarkeit)
- Für jeden bestehenden
- App-Code auf neue Modelle umstellen
- Alte Relation
TenantSymbol.iconundTenantSymbol.iconIdentfernen (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-kitoder 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.tsxoder 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/*.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)
prisma/schema.prismaerweitern (SymbolTemplate,TenantCategory,TenantSymbolRefactor)- Prisma Migration erstellen & testen (
npx prisma migrate dev) prisma/seed-symbol-templates.tsschreiben (feuerwehr-ch Paket auspublic/signaturen/)- Migration-Skript für bestehende Tenants (Default-Kategorie + TenantSymbol-Migration)
- Build testen, auf Staging deployen
Sprint B — API (Woche 1-2)
GET /api/templates+POST /api/templates/importCRUD /api/tenant/categoriesRefactor /api/tenant/symbols(Gruppierung, Upload, Kategorie-Zuordnung)GET /api/icons/:id/imagean TenantSymbol anpassen
Sprint C — Admin UI (Woche 2-3)
- Symbol-Manager: Kategorie-Verwaltung (anlegen/umbenennen/löschen/sortieren)
- Symbol-Manager: Import-Dialog (Paket-Vorschau, granulare Auswahl)
- Symbol-Manager: Eigenen SVG-Upload mit Kategorie-Zuordnung
- Symbol-Manager: Drag & Drop (Kategorien sortieren, Symbole verschieben)
Sprint D — Frontend Sidebar & Polish (Woche 3-4)
SymbolPalette-Komponente extrahieren und nach Kategorien gruppieren- Symbol-Suche in Sidebar (Volltext)
instanceLabel/ Stockwerke implementieren (Phase 1.4)- Häufig-benutzte Symbole (Recent) — Phase 1.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