import React, { useState, useEffect, useCallback } 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, Badge as BadgeIcon, Security as SecurityIcon, History as HistoryIcon, DriveEta as DriveEtaIcon, } from '@mui/icons-material'; import { useParams, useNavigate } from 'react-router-dom'; import DashboardLayout from '../components/dashboard/DashboardLayout'; import { useAuth } from '../contexts/AuthContext'; import { membersService } from '../services/members'; 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'; // ---------------------------------------------------------------- // Role helpers // ---------------------------------------------------------------- function useCanWrite(): boolean { const { user } = useAuth(); const groups: string[] = (user as any)?.groups ?? []; return groups.includes('feuerwehr-admin') || groups.includes('feuerwehr-kommandant'); } function useCurrentUserId(): string | undefined { const { user } = useAuth(); return (user as any)?.id; } // ---------------------------------------------------------------- // Tab panel helper // ---------------------------------------------------------------- interface TabPanelProps { children?: React.ReactNode; value: number; index: number; } function TabPanel({ children, value, index }: TabPanelProps) { return ( ); } // ---------------------------------------------------------------- // 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 ?? '—'} ); } // ---------------------------------------------------------------- // Main component // ---------------------------------------------------------------- function MitgliedDetail() { const { userId } = useParams<{ userId: string }>(); const navigate = useNavigate(); const canWrite = useCanWrite(); 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); // Edit form state — only the fields the user is allowed to change const [formData, setFormData] = useState({}); // ---------------------------------------------------------------- // 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]); // Populate form from current profile useEffect(() => { if (member?.profile) { setFormData({ mitglieds_nr: member.profile.mitglieds_nr ?? undefined, 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: member.profile.fuehrerscheinklassen, 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({ mitglieds_nr: member.profile.mitglieds_nr ?? undefined, 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: member.profile.fuehrerscheinklassen, 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 ( {/* Back button */} {/* Header card */} {initials} {displayName} {profile?.mitglieds_nr && ( } label={`Nr. ${profile.mitglieds_nr}`} size="small" variant="outlined" /> )} {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 */} {canEdit && ( {editMode ? ( {saving ? : } ) : ( setEditMode(true)} aria-label="Bearbeiten"> )} )} {!profile && ( Für dieses Mitglied wurde noch kein Profil angelegt. {canWrite && ' Ein Kommandant kann das Profil unter "Stammdaten" erstellen.'} )} {saveError && ( setSaveError(null)}> {saveError} )} {/* Tabs */} setActiveTab(v)} aria-label="Mitglied Details" > {/* ---- 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('mitglieds_nr', e.target.value || undefined)} /> 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 )} {/* Uniform sizing */} } 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" /> ) : ( <> )} {/* Driving licenses */} } title="Führerscheinklassen" /> {editMode && canWrite ? ( handleFieldChange('fuehrerscheinklassen', newValue)} renderInput={(params) => ( )} size="small" /> ) : profile?.fuehrerscheinklassen && profile.fuehrerscheinklassen.length > 0 ? ( {profile.fuehrerscheinklassen.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: Qualifikationen (placeholder) ---- */} Qualifikationen & Lehrgänge Diese Funktion wird in einer zukünftigen Version verfügbar sein. Geplant: Atemschutz, G26-Untersuchungen, Absolvierte Kurse, Gültigkeitsdaten. {/* ---- Tab 2: Einsätze (placeholder) ---- */} Einsätze dieses Mitglieds Diese Funktion wird verfügbar sobald das Einsatz-Modul implementiert ist. ); } export default MitgliedDetail;