feat(schema): Phase 1 Symbol Architecture — SymbolTemplate, TenantCategory, TenantSymbol refactor + seed + migration scripts
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 14m40s
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 14m40s
This commit is contained in:
269
docs/roadmap-feedback-fabian.md
Normal file
269
docs/roadmap-feedback-fabian.md
Normal file
@@ -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)
|
||||||
370
plans/phase-1-symbol-architecture.md
Normal file
370
plans/phase-1-symbol-architecture.md
Normal file
@@ -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*
|
||||||
112
prisma/migrate-tenant-symbols.ts
Normal file
112
prisma/migrate-tenant-symbols.ts
Normal file
@@ -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()
|
||||||
|
})
|
||||||
@@ -90,6 +90,7 @@ model Tenant {
|
|||||||
iconCategories IconCategory[]
|
iconCategories IconCategory[]
|
||||||
iconAssets IconAsset[]
|
iconAssets IconAsset[]
|
||||||
tenantSymbols TenantSymbol[]
|
tenantSymbols TenantSymbol[]
|
||||||
|
tenantCategories TenantCategory[]
|
||||||
upgradeRequests UpgradeRequest[]
|
upgradeRequests UpgradeRequest[]
|
||||||
dictionaryEntries DictionaryEntry[]
|
dictionaryEntries DictionaryEntry[]
|
||||||
rapports Rapport[]
|
rapports Rapport[]
|
||||||
@@ -392,10 +393,53 @@ model TenantSymbol {
|
|||||||
iconId String
|
iconId String
|
||||||
icon IconAsset @relation(fields: [iconId], references: [id], onDelete: Cascade)
|
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([tenantId])
|
||||||
|
@@index([categoryId])
|
||||||
@@map("tenant_symbols")
|
@@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) ────────────
|
// ─── Dictionary (Global + Tenant word library) ────────────
|
||||||
|
|
||||||
model DictionaryEntry {
|
model DictionaryEntry {
|
||||||
|
|||||||
123
prisma/seed-symbol-templates.ts
Normal file
123
prisma/seed-symbol-templates.ts
Normal file
@@ -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<Map<string, string>> {
|
||||||
|
const mapping = new Map<string, string>()
|
||||||
|
|
||||||
|
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()
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user