feat(projects): Einsatz/Übung-Modus
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 22m24s

- Feld mode (EINSATZ/UEBUNG) am Projekt + idempotente Auto-Migration
- Auswahl beim Erstellen (Segmented-Control mit Lucide-Icons)
- Badge "Einsatz"/"Übung" in der Projektliste
- Eigene Nummerierung für Übungen (Ü-YYYY-NNNN, getrennt gezählt)
- Journal-Titel wird bei Übung zu "Übungs-Journal"
- ROADMAP.md mit Verbesserungs-Fahrplan ergänzt

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Pepe Ziberi
2026-07-18 20:12:07 +02:00
parent 93af663dd1
commit 1641101b73
10 changed files with 126 additions and 9 deletions

View File

@@ -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<ProjectMode>('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 (
<Dialog open={open} onOpenChange={handleClose}>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle>Neuer Einsatz</DialogTitle>
<DialogTitle>{isUebung ? 'Neue Übung' : 'Neuer Einsatz'}</DialogTitle>
</DialogHeader>
<div className="space-y-4 py-4">
{/* Modus-Auswahl: Einsatz vs. Übung */}
<div className="space-y-2">
<Label>Art</Label>
<div className="grid grid-cols-2 gap-2">
<button
type="button"
onClick={() => setMode('EINSATZ')}
disabled={isCreating}
className={`flex items-center justify-center gap-2 rounded-md border px-3 py-2 text-sm font-medium transition-colors ${
!isUebung
? 'border-red-500 bg-red-50 text-red-700 dark:bg-red-950/40 dark:text-red-300'
: 'border-input text-muted-foreground hover:bg-accent'
}`}
>
<Flame className="w-4 h-4" /> Einsatz
</button>
<button
type="button"
onClick={() => setMode('UEBUNG')}
disabled={isCreating}
className={`flex items-center justify-center gap-2 rounded-md border px-3 py-2 text-sm font-medium transition-colors ${
isUebung
? 'border-blue-500 bg-blue-50 text-blue-700 dark:bg-blue-950/40 dark:text-blue-300'
: 'border-input text-muted-foreground hover:bg-accent'
}`}
>
<GraduationCap className="w-4 h-4" /> Übung
</button>
</div>
<p className="text-xs text-muted-foreground">
{isUebung
? 'Übung: zum Vorbereiten und Krokieren von Szenarien.'
: 'Einsatz: echte Lage mit Einsatz-Journal.'}
</p>
</div>
<div className="space-y-2">
<Label htmlFor="project-title">Titel *</Label>
<Input
id="project-title"
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder="z.B. Wohnungsbrand Musterstrasse"
placeholder={isUebung ? 'z.B. Übung Hauptstrasse — Zimmerbrand' : 'z.B. Wohnungsbrand Musterstrasse'}
disabled={isCreating}
/>
</div>

View File

@@ -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<JournalEntry[]>([])
const [checkItems, setCheckItems] = useState<JournalCheckItem[]>([])
const [pendenzen, setPendenzen] = useState<JournalPendenz[]>([])
@@ -432,7 +434,7 @@ export function JournalView({ projectId, projectTitle, projectLocation, einsatzl
<div className="flex items-center justify-between mb-2 print:mb-1">
<h2 className="text-lg md:text-xl font-bold flex items-center gap-2 print:text-base text-red-800 dark:text-red-400">
<ClipboardList className="w-5 h-5 md:w-6 md:h-6 print:w-4 print:h-4" />
Einsatz-Journal
{isUebung ? 'Übungs-Journal' : 'Einsatz-Journal'}
</h2>
<div className="flex gap-1.5 print:hidden">
<Button

View File

@@ -423,6 +423,11 @@ export function Topbar({
>
<div className="flex items-center gap-2">
<h4 className="font-medium">{p.title}</h4>
{(p as any).mode === 'UEBUNG' ? (
<span className="text-xs bg-blue-100 text-blue-700 dark:bg-blue-950/50 dark:text-blue-300 px-1.5 py-0.5 rounded">Übung</span>
) : (
<span className="text-xs bg-red-100 text-red-700 dark:bg-red-950/50 dark:text-red-300 px-1.5 py-0.5 rounded">Einsatz</span>
)}
{project?.id === p.id && (
<span className="text-xs bg-primary text-primary-foreground px-1.5 py-0.5 rounded">Aktiv</span>
)}