268 lines
9.5 KiB
Markdown
268 lines
9.5 KiB
Markdown
# 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.
|
||
> **Status: ABGESCHLOSSEN** ✅ (Sprints A–D implementiert, getestet und gepusht)
|
||
|
||
---
|
||
|
||
## 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
|
||
description String?
|
||
sortOrder Int @default(0)
|
||
createdAt DateTime @default(now())
|
||
updatedAt DateTime @updatedAt
|
||
|
||
symbols TenantSymbol[]
|
||
|
||
@@unique([tenantId, name])
|
||
@@index([tenantId])
|
||
@@map("tenant_categories")
|
||
}
|
||
```
|
||
|
||
### 2.2 Bestehendes `TenantSymbol` umbaut
|
||
|
||
**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: 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
|
||
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 (kann später entfernt werden)
|
||
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. Migration & Seed (Sprint A) ✅
|
||
|
||
- `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 (Sprint B) ✅
|
||
|
||
### 4.1 Templates
|
||
|
||
```
|
||
GET /api/templates
|
||
→ { packages: [{ packageId, packageName, categoryCount, symbolCount, previewSymbols }] }
|
||
|
||
POST /api/templates/import
|
||
Body: { packageId }
|
||
→ importiert alle Symbole des Pakets als TenantSymbols
|
||
→ antwortet mit { imported, skipped, message }
|
||
```
|
||
|
||
**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, 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
|
||
```
|
||
|
||
**Implementiert:** `src/app/api/tenant/categories/route.ts`
|
||
|
||
### 4.3 Tenant Symbols
|
||
|
||
```
|
||
GET /api/tenant/symbols?grouped=true|false
|
||
→ grouped=true: { groups: [{ category, symbols }], symbols: [...] }
|
||
→ grouped=false: { symbols: [...] }
|
||
|
||
POST /api/tenant/symbols
|
||
- Multipart: { file, categoryId? } → Upload nach MinIO
|
||
- JSON: { templateId, categoryId? } → aus SymbolTemplate
|
||
- JSON: { iconId, customName?, categoryId? } → aus IconAsset (Legacy)
|
||
|
||
PATCH /api/tenant/symbols Body: { id, name?, customName?, categoryId?, sortOrder? }
|
||
|
||
DELETE /api/tenant/symbols Body: { id } → löscht TenantSymbol
|
||
```
|
||
|
||
**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/: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 (Sprints C + D) ✅
|
||
|
||
### 5.1 Admin — Symbol-Manager (`src/components/admin/symbol-manager.tsx`)
|
||
|
||
**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
|
||
|
||
**Actions:**
|
||
- Upload-Button → Dialog mit Drag & Drop + Kategorie-Auswahl
|
||
- Import-Button → Dialog mit allen verfügbaren Paketen
|
||
|
||
### 5.2 Sidebar — RightSidebar (`src/components/layout/right-sidebar.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
|
||
|
||
### 5.3 API /icons — Kompatibilität
|
||
|
||
`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
|
||
|
||
### Neue Dateien ✅
|
||
| Pfad | Beschreibung |
|
||
|------|-------------|
|
||
| `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/symbols/[id]/image/route.ts` | TenantSymbol Bild-Serving |
|
||
|
||
### Modifizierte Dateien ✅
|
||
| Pfad | Änderung |
|
||
|------|----------|
|
||
| `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 (Tatsächlich)
|
||
|
||
| 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 (Umgesetzt)
|
||
|
||
| 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*
|
||
*Phase 1 abgeschlossen: 2026-05-20*
|