diff --git a/ROADMAP.md b/ROADMAP.md new file mode 100644 index 0000000..50f75ea --- /dev/null +++ b/ROADMAP.md @@ -0,0 +1,52 @@ +# Lageplan – Verbesserungs-Fahrplan + +Priorisierter Plan zur Weiterentwicklung. Wir arbeiten ihn Punkt für Punkt ab. +Legende Aufwand: 🟢 klein · 🟡 mittel · 🔴 gross + +--- + +## Phase 0 – Fundament & Sicherheit + +- [x] **0.1 – JWT-Secret härten** 🟢 ✅ + - Gemeinsames Secret-Modul; in Produktion hart fehlschlagen, wenn `NEXTAUTH_SECRET` + fehlt oder < 32 Zeichen. Kein öffentlich bekannter Fallback mehr. + - Betrifft: `src/lib/auth.ts`, `src/middleware.ts` + - _Hinweis: Cookie-Flags (httpOnly/secure/sameSite) sind bereits korrekt._ +- [x] **0.2 – Token-Laufzeit verkürzt** 🟢 ✅ + - „Angemeldet bleiben" 30 → 14 Tage. Laufzeiten zentralisiert (`SESSION_MAX_AGE_*` in `auth.ts`). + - _Offen für später: automatische Verlängerung bei Aktivität + serverseitiger Widerruf (grösserer Umbau, braucht Live-Test)._ +- [ ] **0.3 – Tenant-Admin-UX** 🟡 + - Mitglieder einladen / Rolle ändern / deaktivieren. APIs existieren bereits. + +## Phase 1 – Einsatz vs. Übung + +- [x] **1.1 – `mode`-Feld am Projekt** 🟢 ✅ + - Feld `mode` (EINSATZ/UEBUNG) im Schema + Auto-Migration + Validierung. + - Auswahl beim Erstellen (Segmented-Control), Badge in Projektliste, eigene Nummer `Ü-…`. +- [ ] **1.2 – UI je nach Modus** 🟡 _(teilweise: Journal-Titel wird bei Übung zu „Übungs-Journal")_ + - Offen: Übung → Journal ganz aus, stattdessen „Übungsziele / Zielerreichung / Auswertung"-Panel. + +## Phase 2 – Symbole aufräumen & ergänzen + +- [ ] **2.1 – Kategorien vereinheitlichen** 🟢 (Motorspritze, Hydrant, Leitern …) +- [ ] **2.2 – Fehlende taktische Zeichen** 🟡 (Warteraum, Rettungsachse … – Input von Fabian) +- [ ] **2.3 – Symbol-Schönheitsfehler** 🟢 (Eingang/Treppe Umriss, Absperrung Seitenlinien) +- [ ] **2.4 – Custom-Felder pro Symbol** 🟢 (z. B. Stockwerke; `Item.properties` existiert) + +## Phase 3 – Lage-/Rapportansicht + +- [ ] **3.1 – Reduzierte Lage-View** 🟡 + - Read-Only (Pendenzen + Stand + Aufgaben) für Grossbildschirm. `Rapport` + Token existieren. + +## Phase 4 – Echte Kollaboration + +- [ ] **4.1 – Karte & Journal getrennt live-editierbar** 🔴 + - Single-Editor-Lock aufbrechen; Socket.io-Sync erweitern. + +## Phase 5 – Kür + +- [ ] **5.1 – Linien-Typ-Abfrage** 🟡 (Rettungsachse / Leitung / normal) + +--- + +_Fortschritt wird hier laufend abgehakt._ diff --git a/prisma/migrate.js b/prisma/migrate.js index 13c98c2..84d38e3 100644 --- a/prisma/migrate.js +++ b/prisma/migrate.js @@ -70,6 +70,7 @@ async function migrate() { `ALTER TABLE projects ADD COLUMN IF NOT EXISTS "planBounds" JSONB`, `ALTER TABLE projects ADD COLUMN IF NOT EXISTS "einsatzleiter" TEXT`, `ALTER TABLE projects ADD COLUMN IF NOT EXISTS "journalfuehrer" TEXT`, + `ALTER TABLE projects ADD COLUMN IF NOT EXISTS "mode" TEXT NOT NULL DEFAULT 'EINSATZ'`, `ALTER TABLE projects ADD COLUMN IF NOT EXISTS "editingById" TEXT`, `ALTER TABLE projects ADD COLUMN IF NOT EXISTS "editingUserName" TEXT`, `ALTER TABLE projects ADD COLUMN IF NOT EXISTS "editingSessionId" TEXT`, diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 6913cf8..51d25c2 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -151,6 +151,9 @@ model TenantMembership { model Project { id String @id @default(uuid()) einsatzNr String? + // "EINSATZ" = echter Einsatz (mit Journal), "UEBUNG" = Übung (Auswertung statt Journal). + // Als String (nicht Enum) gehalten, damit die idempotente Raw-SQL-Migration einfach bleibt. + mode String @default("EINSATZ") title String location String? description String? diff --git a/src/app/api/projects/route.ts b/src/app/api/projects/route.ts index 2fd676f..a9251f7 100644 --- a/src/app/api/projects/route.ts +++ b/src/app/api/projects/route.ts @@ -55,9 +55,11 @@ export async function POST(request: NextRequest) { ) } - // Generate unique Einsatz-Nr: E-YYYY-NNNN (auto-increment per tenant per year) + // Generate unique number per tenant/year/mode. + // Einsatz: E-YYYY-NNNN · Übung: Ü-YYYY-NNNN (getrennte Zählung je Modus) + const mode = validated.data.mode === 'UEBUNG' ? 'UEBUNG' : 'EINSATZ' const year = new Date().getFullYear() - const einsatzPrefix = `E-${year}-` + const einsatzPrefix = `${mode === 'UEBUNG' ? 'Ü' : 'E'}-${year}-` let einsatzNr = `${einsatzPrefix}0001` try { const lastProject = await (prisma as any).project.findFirst({ @@ -78,6 +80,7 @@ export async function POST(request: NextRequest) { const project = await (prisma as any).project.create({ data: { ...validated.data, + mode, einsatzNr, ownerId: user.id, tenantId: user.tenantId || null, diff --git a/src/app/app/page.tsx b/src/app/app/page.tsx index 627a32d..54fb66f 100644 --- a/src/app/app/page.tsx +++ b/src/app/app/page.tsx @@ -534,6 +534,7 @@ export default function AppPage() { headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ title: currentProject.title, + mode: currentProject.mode, location: currentProject.location || undefined, description: currentProject.description || undefined, einsatzleiter: currentProject.einsatzleiter || undefined, @@ -929,6 +930,7 @@ export default function AppPage() { projectId={currentProject?.id || null} projectTitle={currentProject?.title || ''} projectLocation={currentProject?.location || ''} + mode={currentProject?.mode} einsatzleiter={currentProject?.einsatzleiter || ''} journalfuehrer={currentProject?.journalfuehrer || ''} einsatzNr={(currentProject as any)?.einsatzNr || ''} diff --git a/src/components/dialogs/project-dialog.tsx b/src/components/dialogs/project-dialog.tsx index 40ef43a..ec1aac5 100644 --- a/src/components/dialogs/project-dialog.tsx +++ b/src/components/dialogs/project-dialog.tsx @@ -12,8 +12,8 @@ import { DialogFooter, } from '@/components/ui/dialog' import { useToast } from '@/components/ui/use-toast' -import { MapPin, Loader2, X } from 'lucide-react' -import type { Project } from '@/types' +import { MapPin, Loader2, X, Flame, GraduationCap } from 'lucide-react' +import type { Project, ProjectMode } from '@/types' interface NominatimResult { place_id: number @@ -44,6 +44,7 @@ export function ProjectDialog({ onOpenChange, onProjectCreated, }: ProjectDialogProps) { + const [mode, setMode] = useState('EINSATZ') const [title, setTitle] = useState('') const [location, setLocation] = useState('') const [description, setDescription] = useState('') @@ -142,6 +143,7 @@ export function ProjectDialog({ try { const body: any = { title: title.trim(), + mode, location: location.trim() || undefined, description: description.trim() || undefined, einsatzleiter: einsatzleiter.trim() || undefined, @@ -169,6 +171,7 @@ export function ProjectDialog({ onProjectCreated(data.project) // Reset form + setMode('EINSATZ') setTitle('') setLocation('') setDescription('') @@ -189,6 +192,7 @@ export function ProjectDialog({ const handleClose = () => { if (!isCreating) { + setMode('EINSATZ') setTitle('') setLocation('') setDescription('') @@ -200,21 +204,59 @@ export function ProjectDialog({ } } + const isUebung = mode === 'UEBUNG' + return ( - Neuer Einsatz + {isUebung ? 'Neue Übung' : 'Neuer Einsatz'} - +
+ {/* Modus-Auswahl: Einsatz vs. Übung */} +
+ +
+ + +
+

+ {isUebung + ? 'Übung: zum Vorbereiten und Krokieren von Szenarien.' + : 'Einsatz: echte Lage mit Einsatz-Journal.'} +

+
+
setTitle(e.target.value)} - placeholder="z.B. Wohnungsbrand Musterstrasse" + placeholder={isUebung ? 'z.B. Übung Hauptstrasse — Zimmerbrand' : 'z.B. Wohnungsbrand Musterstrasse'} disabled={isCreating} />
diff --git a/src/components/journal/journal-view.tsx b/src/components/journal/journal-view.tsx index 5b7811a..a67f793 100644 --- a/src/components/journal/journal-view.tsx +++ b/src/components/journal/journal-view.tsx @@ -44,6 +44,7 @@ interface JournalViewProps { projectId: string | null projectTitle: string projectLocation: string + mode?: 'EINSATZ' | 'UEBUNG' einsatzleiter: string journalfuehrer: string canEdit: boolean @@ -65,7 +66,8 @@ function formatDateTime(dateStr: string) { d.toLocaleTimeString('de-CH', { hour: '2-digit', minute: '2-digit' }) } -export function JournalView({ projectId, projectTitle, projectLocation, einsatzleiter, journalfuehrer, canEdit, tenantId, einsatzNr, tenantName, tenantLogoUrl, mapRef, mapScreenshot: preCapuredScreenshot }: JournalViewProps) { +export function JournalView({ projectId, projectTitle, projectLocation, mode, einsatzleiter, journalfuehrer, canEdit, tenantId, einsatzNr, tenantName, tenantLogoUrl, mapRef, mapScreenshot: preCapuredScreenshot }: JournalViewProps) { + const isUebung = mode === 'UEBUNG' const [entries, setEntries] = useState([]) const [checkItems, setCheckItems] = useState([]) const [pendenzen, setPendenzen] = useState([]) @@ -432,7 +434,7 @@ export function JournalView({ projectId, projectTitle, projectLocation, einsatzl

- Einsatz-Journal + {isUebung ? 'Übungs-Journal' : 'Einsatz-Journal'}