import React, { useState, useEffect, useCallback, useMemo } from 'react'; import { Container, Box, Typography, Card, CardContent, CardHeader, Avatar, Autocomplete, Button, Chip, Tabs, Tab, Grid, TextField, MenuItem, CircularProgress, Alert, Divider, Tooltip, IconButton, Stack, } from '@mui/material'; import { Edit as EditIcon, Save as SaveIcon, Cancel as CancelIcon, Person as PersonIcon, Phone as PhoneIcon, Security as SecurityIcon, History as HistoryIcon, DriveEta as DriveEtaIcon, CheckCircle as CheckCircleIcon, HighlightOff as HighlightOffIcon, MilitaryTech as MilitaryTechIcon, LocalHospital as LocalHospitalIcon, School as SchoolIcon, } from '@mui/icons-material'; import { useParams, useNavigate } from 'react-router-dom'; import DashboardLayout from '../components/dashboard/DashboardLayout'; import { useAuth } from '../contexts/AuthContext'; import { usePermissionContext } from '../contexts/PermissionContext'; import { membersService } from '../services/members'; import { atemschutzApi } from '../services/atemschutz'; import { personalEquipmentApi } from '../services/personalEquipment'; import type { PersoenlicheAusruestung, ZustandOption } from '../types/personalEquipment.types'; import { toGermanDate, fromGermanDate } from '../utils/dateInput'; import { MemberWithProfile, StatusEnum, DienstgradEnum, FunktionEnum, TshirtGroesseEnum, DIENSTGRAD_VALUES, STATUS_VALUES, FUNKTION_VALUES, TSHIRT_GROESSE_VALUES, STATUS_LABELS, STATUS_COLORS, getMemberDisplayName, formatPhone, UpdateMemberProfileData, } from '../types/member.types'; import type { Befoerderung, Untersuchung, Fahrgenehmigung, Ausbildung } from '../types/member.types'; import { StatusChip, TabPanel, PageHeader } from '../components/templates'; import type { AtemschutzUebersicht } from '../types/atemschutz.types'; import { UntersuchungErgebnisLabel } from '../types/atemschutz.types'; // ---------------------------------------------------------------- // Role helpers // ---------------------------------------------------------------- function useCanWrite(): boolean { const { hasPermission } = usePermissionContext(); return hasPermission('mitglieder:edit'); } function useCurrentUserId(): string | undefined { const { user } = useAuth(); return (user as any)?.id; } // ---------------------------------------------------------------- // Rank history timeline component // ---------------------------------------------------------------- interface RankTimelineProps { entries: NonNullable; } function RankTimeline({ entries }: RankTimelineProps) { if (entries.length === 0) { return ( Keine Dienstgradänderungen eingetragen. ); } return ( {entries.map((entry, idx) => ( {/* Timeline dot */} {/* Content */} {entry.dienstgrad_neu} {entry.dienstgrad_alt && ( vorher: {entry.dienstgrad_alt} )} {new Date(entry.datum).toLocaleDateString('de-AT')} {entry.durch_user_name && ( · durch {entry.durch_user_name} )} {entry.bemerkung && ( {entry.bemerkung} )} ))} ); } // ---------------------------------------------------------------- // Read-only field row // ---------------------------------------------------------------- function FieldRow({ label, value }: { label: string; value: React.ReactNode }) { return ( {label} {value ?? '—'} ); } // ---------------------------------------------------------------- // Validity chip — green / yellow (< 90 days) / red (expired) // ---------------------------------------------------------------- function ValidityChip({ date, gueltig, tageRest, }: { date: string | null; gueltig: boolean; tageRest: number | null; }) { if (!date) return ; const formatted = new Date(date).toLocaleDateString('de-AT'); const color: 'success' | 'warning' | 'error' = !gueltig ? 'error' : tageRest !== null && tageRest < 90 ? 'warning' : 'success'; const suffix = !gueltig ? ' (abgelaufen)' : tageRest !== null && tageRest < 90 ? ` (${tageRest} Tage)` : ''; return ; } // ---------------------------------------------------------------- // Main component // ---------------------------------------------------------------- function MitgliedDetail() { const { userId } = useParams<{ userId: string }>(); const navigate = useNavigate(); const canWrite = useCanWrite(); const { hasPermission } = usePermissionContext(); const currentUserId = useCurrentUserId(); const isOwnProfile = currentUserId === userId; const canEdit = canWrite || isOwnProfile; // --- state --- const [member, setMember] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [saving, setSaving] = useState(false); const [saveError, setSaveError] = useState(null); const [editMode, setEditMode] = useState(false); const [activeTab, setActiveTab] = useState(0); const [qualSubTab, setQualSubTab] = useState(0); // Atemschutz data for Qualifikationen tab const [atemschutz, setAtemschutz] = useState(null); const [atemschutzLoading, setAtemschutzLoading] = useState(false); // Personal equipment data const [personalEquipment, setPersonalEquipment] = useState([]); const [personalEquipmentLoading, setPersonalEquipmentLoading] = useState(false); const [zustandOptions, setZustandOptions] = useState([]); // FDISK-synced sub-section data const [befoerderungen, setBefoerderungen] = useState([]); const [untersuchungen, setUntersuchungen] = useState([]); const [fahrgenehmigungen, setFahrgenehmigungen] = useState([]); const [ausbildungen, setAusbildungen] = useState([]); // Edit form state — only the fields the user is allowed to change const [formData, setFormData] = useState({}); // Merge Führerscheinklassen from profile + Fahrgenehmigungen (FDISK) const displayedFuehrerscheinklassen = useMemo(() => { const fromFG = fahrgenehmigungen.map(f => f.klasse).filter(Boolean); const fromProfile = member?.profile?.fuehrerscheinklassen ?? []; return [...new Set([...fromProfile, ...fromFG])].sort(); }, [fahrgenehmigungen, member?.profile?.fuehrerscheinklassen]); // ---------------------------------------------------------------- // Data loading // ---------------------------------------------------------------- const loadMember = useCallback(async () => { if (!userId) return; setLoading(true); setError(null); try { const data = await membersService.getMember(userId); setMember(data); } catch { setError('Mitglied konnte nicht geladen werden.'); } finally { setLoading(false); } }, [userId]); useEffect(() => { loadMember(); }, [loadMember]); // Load atemschutz data alongside the member useEffect(() => { if (!userId) return; setAtemschutzLoading(true); atemschutzApi.getByUserId(userId) .then((data) => setAtemschutz(data)) .catch(() => setAtemschutz(null)) .finally(() => setAtemschutzLoading(false)); }, [userId]); // Load personal equipment for this user useEffect(() => { if (!userId) return; const canView = hasPermission('persoenliche_ausruestung:view') || hasPermission('persoenliche_ausruestung:view_all'); if (!canView) return; setPersonalEquipmentLoading(true); personalEquipmentApi.getByUserId(userId) .then(setPersonalEquipment) .catch(() => setPersonalEquipment([])) .finally(() => setPersonalEquipmentLoading(false)); personalEquipmentApi.getZustandOptions() .then(setZustandOptions) .catch(() => {}); }, [userId]); // Load FDISK-synced sub-section data useEffect(() => { if (!userId) return; membersService.getBefoerderungen(userId).then(setBefoerderungen).catch(() => setBefoerderungen([])); membersService.getUntersuchungen(userId).then(setUntersuchungen).catch(() => setUntersuchungen([])); membersService.getFahrgenehmigungen(userId).then(setFahrgenehmigungen).catch(() => setFahrgenehmigungen([])); membersService.getAusbildungen(userId).then(setAusbildungen).catch(() => setAusbildungen([])); }, [userId]); // Populate form from current profile useEffect(() => { if (member?.profile) { setFormData({ dienstgrad: member.profile.dienstgrad ?? undefined, dienstgrad_seit: toGermanDate(member.profile.dienstgrad_seit) || undefined, funktion: member.profile.funktion, status: member.profile.status, eintrittsdatum: toGermanDate(member.profile.eintrittsdatum) || undefined, austrittsdatum: toGermanDate(member.profile.austrittsdatum) || undefined, geburtsdatum: toGermanDate(member.profile.geburtsdatum) || undefined, telefon_mobil: member.profile.telefon_mobil ?? undefined, telefon_privat: member.profile.telefon_privat ?? undefined, notfallkontakt_name: member.profile.notfallkontakt_name ?? undefined, notfallkontakt_telefon: member.profile.notfallkontakt_telefon ?? undefined, fuehrerscheinklassen: displayedFuehrerscheinklassen, tshirt_groesse: member.profile.tshirt_groesse ?? undefined, schuhgroesse: member.profile.schuhgroesse ?? undefined, bemerkungen: member.profile.bemerkungen ?? undefined, fdisk_standesbuch_nr: member.profile.fdisk_standesbuch_nr ?? undefined, }); } }, [member]); // ---------------------------------------------------------------- // Save // ---------------------------------------------------------------- const handleSave = async () => { if (!userId) return; setSaving(true); setSaveError(null); try { let payload: UpdateMemberProfileData; if (canWrite) { // Admin / Kommandant: send all fields with date conversion payload = { ...formData, eintrittsdatum: formData.eintrittsdatum ? fromGermanDate(formData.eintrittsdatum) || undefined : undefined, austrittsdatum: formData.austrittsdatum ? fromGermanDate(formData.austrittsdatum) || undefined : undefined, geburtsdatum: formData.geburtsdatum ? fromGermanDate(formData.geburtsdatum) || undefined : undefined, dienstgrad_seit: formData.dienstgrad_seit ? fromGermanDate(formData.dienstgrad_seit) || undefined : undefined, }; } else { // Regular member (own profile): only send fields allowed by SelfUpdateMemberProfileSchema payload = { telefon_mobil: formData.telefon_mobil, telefon_privat: formData.telefon_privat, notfallkontakt_name: formData.notfallkontakt_name, notfallkontakt_telefon: formData.notfallkontakt_telefon, tshirt_groesse: formData.tshirt_groesse, schuhgroesse: formData.schuhgroesse, bild_url: formData.bild_url, fdisk_standesbuch_nr: formData.fdisk_standesbuch_nr, }; } const updated = await membersService.updateMember(userId, payload); setMember(updated); setEditMode(false); } catch { setSaveError('Speichern fehlgeschlagen. Bitte versuchen Sie es erneut.'); } finally { setSaving(false); } }; const handleCancelEdit = () => { setEditMode(false); setSaveError(null); // Reset form to current profile values if (member?.profile) { setFormData({ dienstgrad: member.profile.dienstgrad ?? undefined, dienstgrad_seit: toGermanDate(member.profile.dienstgrad_seit) || undefined, funktion: member.profile.funktion, status: member.profile.status, eintrittsdatum: toGermanDate(member.profile.eintrittsdatum) || undefined, austrittsdatum: toGermanDate(member.profile.austrittsdatum) || undefined, geburtsdatum: toGermanDate(member.profile.geburtsdatum) || undefined, telefon_mobil: member.profile.telefon_mobil ?? undefined, telefon_privat: member.profile.telefon_privat ?? undefined, notfallkontakt_name: member.profile.notfallkontakt_name ?? undefined, notfallkontakt_telefon: member.profile.notfallkontakt_telefon ?? undefined, fuehrerscheinklassen: displayedFuehrerscheinklassen, tshirt_groesse: member.profile.tshirt_groesse ?? undefined, schuhgroesse: member.profile.schuhgroesse ?? undefined, bemerkungen: member.profile.bemerkungen ?? undefined, fdisk_standesbuch_nr: member.profile.fdisk_standesbuch_nr ?? undefined, }); } }; const handleFieldChange = (field: keyof UpdateMemberProfileData, value: any) => { setFormData((prev) => ({ ...prev, [field]: value })); }; // ---------------------------------------------------------------- // Render helpers // ---------------------------------------------------------------- if (loading) { return ( ); } if (error || !member) { return ( {error ?? 'Mitglied nicht gefunden.'} ); } const displayName = getMemberDisplayName(member); const profile = member.profile; const initials = [member.given_name?.[0], member.family_name?.[0]] .filter(Boolean) .join('') .toUpperCase() || member.email?.[0]?.toUpperCase() || '?'; return ( {/* Header card */} {initials} {displayName} {profile?.status && ( )} {member.email} {profile?.dienstgrad && ( Dienstgrad: {profile.dienstgrad} {profile.dienstgrad_seit ? ` (seit ${new Date(profile.dienstgrad_seit).toLocaleDateString('de-AT')})` : ''} )} {profile && Array.isArray(profile.funktion) && profile.funktion.length > 0 && ( {profile.funktion.map((f) => ( ))} )} {/* Edit controls — only shown when profile exists */} {canEdit && !!profile && ( {editMode ? ( {saving ? : } ) : ( setEditMode(true)} aria-label="Bearbeiten"> )} )} {!profile && ( { if (!userId) return; setSaving(true); setSaveError(null); try { await membersService.createMemberProfile(userId, { status: 'aktiv' }); await loadMember(); } catch { setSaveError('Profil konnte nicht erstellt werden.'); } finally { setSaving(false); } }} > {saving ? : 'Profil erstellen'} ) : undefined } > Für dieses Mitglied wurde noch kein Profil angelegt. {!canWrite && ' Wende dich an einen Administrator, um dein Profil anlegen zu lassen.'} )} {saveError && ( setSaveError(null)}> {saveError} )} {/* Tabs */} setActiveTab(v)} aria-label="Mitglied Details" variant="scrollable" scrollButtons="auto" > {/* ---- Tab 0: Stammdaten ---- */} {/* Personal data */} } title="Persönliche Daten" /> {editMode && canWrite ? ( handleFieldChange('dienstgrad', e.target.value as DienstgradEnum || undefined)} > {DIENSTGRAD_VALUES.map((dg) => ( {dg} ))} handleFieldChange('dienstgrad_seit', e.target.value || undefined)} InputLabelProps={{ shrink: true }} /> handleFieldChange('status', e.target.value as StatusEnum)} > {STATUS_VALUES.map((s) => ( {STATUS_LABELS[s]} ))} handleFieldChange('eintrittsdatum', e.target.value || undefined)} InputLabelProps={{ shrink: true }} /> handleFieldChange('geburtsdatum', e.target.value || undefined)} InputLabelProps={{ shrink: true }} /> handleFieldChange('austrittsdatum', e.target.value || undefined)} InputLabelProps={{ shrink: true }} /> handleFieldChange('fdisk_standesbuch_nr', e.target.value || undefined)} /> handleFieldChange('funktion', newValue as FunktionEnum[])} renderInput={(params) => ( )} size="small" /> ) : ( <> : null } /> {editMode && isOwnProfile ? ( handleFieldChange('fdisk_standesbuch_nr', e.target.value || undefined)} /> ) : ( )} )} {/* Contact */} } title="Kontaktdaten" /> {editMode ? ( handleFieldChange('telefon_mobil', e.target.value || undefined)} placeholder="+436641234567" /> handleFieldChange('telefon_privat', e.target.value || undefined)} placeholder="+4371234567" /> Notfallkontakt handleFieldChange('notfallkontakt_name', e.target.value || undefined)} /> handleFieldChange('notfallkontakt_telefon', e.target.value || undefined)} placeholder="+436641234567" /> ) : ( <> {member.email} } /> Notfallkontakt )} {/* Driving licenses */} } title="Führerscheinklassen" /> {editMode && canWrite ? ( handleFieldChange('fuehrerscheinklassen', newValue)} renderInput={(params) => ( )} size="small" /> ) : displayedFuehrerscheinklassen.length > 0 ? ( {displayedFuehrerscheinklassen.map((k) => ( ))} ) : ( )} {/* Rank history */} } title="Dienstgrad-Verlauf" /> {/* Remarks — Kommandant/Admin only */} {canWrite && ( {editMode ? ( handleFieldChange('bemerkungen', e.target.value || undefined)} /> ) : ( {profile?.bemerkungen ?? 'Keine Bemerkungen eingetragen.'} )} )} {/* ---- Tab 1: Ausrüstung & Uniform ---- */} } title="Ausrüstung & Uniform" /> {editMode ? ( handleFieldChange('tshirt_groesse', e.target.value as TshirtGroesseEnum || undefined)} > {TSHIRT_GROESSE_VALUES.map((g) => ( {g} ))} handleFieldChange('schuhgroesse', e.target.value || undefined)} placeholder="z.B. 43" /> ) : ( <> )} {(hasPermission('persoenliche_ausruestung:view') || hasPermission('persoenliche_ausruestung:view_all')) && ( } title="Persönliche Ausrüstung" /> {personalEquipmentLoading ? ( ) : personalEquipment.length === 0 ? ( Keine persönlichen Gegenstände erfasst ) : ( personalEquipment.map((item) => ( navigate(`/persoenliche-ausruestung/${item.id}`)} > {item.bezeichnung} {item.kategorie && ( {item.kategorie} )} {item.eigenschaften && item.eigenschaften.length > 0 && ( {item.eigenschaften.map((e) => ( ))} )} o.key === item.zustand)?.label ?? item.zustand} color={(zustandOptions.find(o => o.key === item.zustand)?.color ?? 'default') as any} size="small" variant="outlined" /> )) )} )} {/* ---- Tab 2: Qualifikationen ---- */} setQualSubTab(v)} variant="scrollable" scrollButtons="auto" aria-label="Qualifikationen" > {/* Sub-tab 0: Atemschutz */} {atemschutzLoading ? ( ) : atemschutz ? ( } title="Atemschutz" action={ : } label={atemschutz.einsatzbereit ? 'Einsatzbereit' : 'Nicht einsatzbereit'} color={atemschutz.einsatzbereit ? 'success' : 'error'} size="small" /> } /> G26.3 Untersuchung } /> Leistungstest (Finnentest) } /> {atemschutz.bemerkung && ( <> )} ) : ( Kein Atemschutz-Eintrag Für dieses Mitglied ist kein Atemschutz-Datensatz vorhanden. {canWrite && ( )} )} {/* Sub-tab 1: Ausbildungen */} } title="Ausbildungen" /> {ausbildungen.length === 0 ? ( Keine Ausbildungen eingetragen. ) : ( ausbildungen.map((a) => ( {a.kursname} {a.kurs_datum ? new Date(a.kurs_datum).toLocaleDateString('de-AT') : '—'} {a.kurs_kurzbezeichnung && ( )} {a.erfolgscode && ( )} {a.ort && ( {a.ort} )} {a.ablaufdatum && ( Gültig bis: {new Date(a.ablaufdatum).toLocaleDateString('de-AT')} )} )) )} {/* Sub-tab 2: Untersuchungen */} } title="Untersuchungen" /> {untersuchungen.length === 0 ? ( Keine Untersuchungen eingetragen. ) : ( untersuchungen.map((u) => ( {u.art} {u.datum ? new Date(u.datum).toLocaleDateString('de-AT') : '—'} {u.ergebnis && ( {u.ergebnis} )} {u.anmerkungen && ( {u.anmerkungen} )} )) )} {/* Sub-tab 3: Beförderungen */} } title="Beförderungen" /> {befoerderungen.length === 0 ? ( Keine Beförderungen eingetragen. ) : ( befoerderungen.map((b) => ( )) )} {/* Sub-tab 4: Fahrgenehmigungen */} } title="Gesetzliche Fahrgenehmigungen" /> {fahrgenehmigungen.length === 0 ? ( Keine Fahrgenehmigungen eingetragen. ) : ( fahrgenehmigungen.map((f) => ( {f.klasse} {f.ausstellungsdatum ? new Date(f.ausstellungsdatum).toLocaleDateString('de-AT') : '—'} {(f.behoerde || f.nummer) && ( {[f.behoerde, f.nummer].filter(Boolean).join(' · ')} )} {f.gueltig_bis && ( Gültig bis: {new Date(f.gueltig_bis).toLocaleDateString('de-AT')} )} )) )} {/* ---- Tab 3: Einsätze (placeholder) ---- */} Einsätze dieses Mitglieds Diese Funktion wird verfügbar sobald das Einsatz-Modul implementiert ist. ); } export default MitgliedDetail;