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 (
{value === index && {children}}
);
}
// ----------------------------------------------------------------
// 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) => (
))}
handleFieldChange('dienstgrad_seit', e.target.value || undefined)}
InputLabelProps={{ shrink: true }}
/>
handleFieldChange('status', e.target.value as StatusEnum)}
>
{STATUS_VALUES.map((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) => (
))}
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;