1312 lines
53 KiB
TypeScript
1312 lines
53 KiB
TypeScript
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<MemberWithProfile['dienstgrad_verlauf']>;
|
|
}
|
|
|
|
function RankTimeline({ entries }: RankTimelineProps) {
|
|
if (entries.length === 0) {
|
|
return (
|
|
<Typography color="text.secondary" variant="body2">
|
|
Keine Dienstgradänderungen eingetragen.
|
|
</Typography>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<Stack spacing={0}>
|
|
{entries.map((entry, idx) => (
|
|
<Box
|
|
key={entry.id}
|
|
sx={{
|
|
display: 'flex',
|
|
gap: 2,
|
|
position: 'relative',
|
|
pb: 2,
|
|
'&::before': idx < entries.length - 1 ? {
|
|
content: '""',
|
|
position: 'absolute',
|
|
left: 11,
|
|
top: 24,
|
|
bottom: 0,
|
|
width: 2,
|
|
bgcolor: 'divider',
|
|
} : {},
|
|
}}
|
|
>
|
|
{/* Timeline dot */}
|
|
<Box
|
|
sx={{
|
|
width: 24,
|
|
height: 24,
|
|
borderRadius: '50%',
|
|
bgcolor: 'primary.main',
|
|
flexShrink: 0,
|
|
mt: 0.25,
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
}}
|
|
>
|
|
<HistoryIcon sx={{ fontSize: 14, color: 'white' }} />
|
|
</Box>
|
|
|
|
{/* Content */}
|
|
<Box sx={{ flex: 1 }}>
|
|
<Typography variant="body2" fontWeight={500}>
|
|
{entry.dienstgrad_neu}
|
|
</Typography>
|
|
{entry.dienstgrad_alt && (
|
|
<Typography variant="caption" color="text.secondary">
|
|
vorher: {entry.dienstgrad_alt}
|
|
</Typography>
|
|
)}
|
|
<Box sx={{ display: 'flex', gap: 1, mt: 0.5, flexWrap: 'wrap' }}>
|
|
<Typography variant="caption" color="text.secondary">
|
|
{new Date(entry.datum).toLocaleDateString('de-AT')}
|
|
</Typography>
|
|
{entry.durch_user_name && (
|
|
<Typography variant="caption" color="text.secondary">
|
|
· durch {entry.durch_user_name}
|
|
</Typography>
|
|
)}
|
|
</Box>
|
|
{entry.bemerkung && (
|
|
<Typography variant="caption" color="text.secondary" sx={{ display: 'block' }}>
|
|
{entry.bemerkung}
|
|
</Typography>
|
|
)}
|
|
</Box>
|
|
</Box>
|
|
))}
|
|
</Stack>
|
|
);
|
|
}
|
|
|
|
// ----------------------------------------------------------------
|
|
// Read-only field row
|
|
// ----------------------------------------------------------------
|
|
function FieldRow({ label, value }: { label: string; value: React.ReactNode }) {
|
|
return (
|
|
<Box sx={{ display: 'flex', gap: 1, py: 0.75, borderBottom: '1px solid', borderColor: 'divider' }}>
|
|
<Typography variant="body2" color="text.secondary" sx={{ minWidth: 180, flexShrink: 0 }}>
|
|
{label}
|
|
</Typography>
|
|
<Typography variant="body2" sx={{ flex: 1 }}>
|
|
{value ?? '—'}
|
|
</Typography>
|
|
</Box>
|
|
);
|
|
}
|
|
|
|
// ----------------------------------------------------------------
|
|
// Validity chip — green / yellow (< 90 days) / red (expired)
|
|
// ----------------------------------------------------------------
|
|
function ValidityChip({
|
|
date,
|
|
gueltig,
|
|
tageRest,
|
|
}: {
|
|
date: string | null;
|
|
gueltig: boolean;
|
|
tageRest: number | null;
|
|
}) {
|
|
if (!date) return <span>—</span>;
|
|
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 <Chip label={`${formatted}${suffix}`} color={color} size="small" variant="outlined" />;
|
|
}
|
|
|
|
// ----------------------------------------------------------------
|
|
// 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<MemberWithProfile | null>(null);
|
|
const [loading, setLoading] = useState(true);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [saving, setSaving] = useState(false);
|
|
const [saveError, setSaveError] = useState<string | null>(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<AtemschutzUebersicht | null>(null);
|
|
const [atemschutzLoading, setAtemschutzLoading] = useState(false);
|
|
|
|
// Personal equipment data
|
|
const [personalEquipment, setPersonalEquipment] = useState<PersoenlicheAusruestung[]>([]);
|
|
const [personalEquipmentLoading, setPersonalEquipmentLoading] = useState(false);
|
|
const [zustandOptions, setZustandOptions] = useState<ZustandOption[]>([]);
|
|
|
|
// FDISK-synced sub-section data
|
|
const [befoerderungen, setBefoerderungen] = useState<Befoerderung[]>([]);
|
|
const [untersuchungen, setUntersuchungen] = useState<Untersuchung[]>([]);
|
|
const [fahrgenehmigungen, setFahrgenehmigungen] = useState<Fahrgenehmigung[]>([]);
|
|
const [ausbildungen, setAusbildungen] = useState<Ausbildung[]>([]);
|
|
|
|
// Edit form state — only the fields the user is allowed to change
|
|
const [formData, setFormData] = useState<UpdateMemberProfileData>({});
|
|
|
|
// 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 (
|
|
<DashboardLayout>
|
|
<Box sx={{ display: 'flex', justifyContent: 'center', pt: 8 }}>
|
|
<CircularProgress />
|
|
</Box>
|
|
</DashboardLayout>
|
|
);
|
|
}
|
|
|
|
if (error || !member) {
|
|
return (
|
|
<DashboardLayout>
|
|
<Container maxWidth="md">
|
|
<Alert severity="error" sx={{ mt: 4 }}>
|
|
{error ?? 'Mitglied nicht gefunden.'}
|
|
</Alert>
|
|
<Button sx={{ mt: 2 }} onClick={() => navigate('/mitglieder')}>
|
|
Zurück zur Liste
|
|
</Button>
|
|
</Container>
|
|
</DashboardLayout>
|
|
);
|
|
}
|
|
|
|
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 (
|
|
<DashboardLayout>
|
|
<Container maxWidth="lg">
|
|
<PageHeader
|
|
title={displayName}
|
|
backTo="/mitglieder"
|
|
/>
|
|
|
|
{/* Header card */}
|
|
<Card sx={{ mb: 3 }}>
|
|
<CardContent>
|
|
<Box sx={{ display: 'flex', gap: 3, alignItems: 'flex-start', flexWrap: 'wrap' }}>
|
|
<Avatar
|
|
src={profile?.bild_url ?? member.profile_picture_url ?? undefined}
|
|
alt={displayName}
|
|
sx={{ width: 80, height: 80, fontSize: '1.75rem' }}
|
|
>
|
|
{initials}
|
|
</Avatar>
|
|
|
|
<Box sx={{ flex: 1 }}>
|
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, flexWrap: 'wrap' }}>
|
|
<Typography variant="h5" fontWeight={600}>
|
|
{displayName}
|
|
</Typography>
|
|
{profile?.status && (
|
|
<StatusChip
|
|
status={profile.status}
|
|
labelMap={STATUS_LABELS}
|
|
colorMap={STATUS_COLORS}
|
|
/>
|
|
)}
|
|
</Box>
|
|
|
|
<Typography color="text.secondary" variant="body2" sx={{ mt: 0.5 }}>
|
|
{member.email}
|
|
</Typography>
|
|
|
|
{profile?.dienstgrad && (
|
|
<Typography variant="body2" sx={{ mt: 0.5 }}>
|
|
<strong>Dienstgrad:</strong> {profile.dienstgrad}
|
|
{profile.dienstgrad_seit
|
|
? ` (seit ${new Date(profile.dienstgrad_seit).toLocaleDateString('de-AT')})`
|
|
: ''}
|
|
</Typography>
|
|
)}
|
|
|
|
{profile && Array.isArray(profile.funktion) && profile.funktion.length > 0 && (
|
|
<Box sx={{ display: 'flex', gap: 0.5, mt: 1, flexWrap: 'wrap' }}>
|
|
{profile.funktion.map((f) => (
|
|
<Chip key={f} label={f} size="small" color="secondary" variant="outlined" />
|
|
))}
|
|
</Box>
|
|
)}
|
|
</Box>
|
|
|
|
{/* Edit controls — only shown when profile exists */}
|
|
{canEdit && !!profile && (
|
|
<Box>
|
|
{editMode ? (
|
|
<Box sx={{ display: 'flex', gap: 1 }}>
|
|
<Tooltip title="Änderungen speichern">
|
|
<span>
|
|
<IconButton
|
|
color="primary"
|
|
onClick={handleSave}
|
|
disabled={saving}
|
|
aria-label="Speichern"
|
|
>
|
|
{saving ? <CircularProgress size={20} /> : <SaveIcon />}
|
|
</IconButton>
|
|
</span>
|
|
</Tooltip>
|
|
<Tooltip title="Abbrechen">
|
|
<IconButton
|
|
onClick={handleCancelEdit}
|
|
disabled={saving}
|
|
aria-label="Abbrechen"
|
|
>
|
|
<CancelIcon />
|
|
</IconButton>
|
|
</Tooltip>
|
|
</Box>
|
|
) : (
|
|
<Tooltip title="Bearbeiten">
|
|
<IconButton onClick={() => setEditMode(true)} aria-label="Bearbeiten">
|
|
<EditIcon />
|
|
</IconButton>
|
|
</Tooltip>
|
|
)}
|
|
</Box>
|
|
)}
|
|
</Box>
|
|
|
|
{!profile && (
|
|
<Alert
|
|
severity="info"
|
|
sx={{ mt: 2 }}
|
|
action={
|
|
canWrite ? (
|
|
<Button
|
|
color="inherit"
|
|
size="small"
|
|
disabled={saving}
|
|
onClick={async () => {
|
|
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 ? <CircularProgress size={16} /> : 'Profil erstellen'}
|
|
</Button>
|
|
) : undefined
|
|
}
|
|
>
|
|
Für dieses Mitglied wurde noch kein Profil angelegt.
|
|
{!canWrite && ' Wende dich an einen Administrator, um dein Profil anlegen zu lassen.'}
|
|
</Alert>
|
|
)}
|
|
|
|
{saveError && (
|
|
<Alert severity="error" sx={{ mt: 2 }} onClose={() => setSaveError(null)}>
|
|
{saveError}
|
|
</Alert>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Tabs */}
|
|
<Box sx={{ borderBottom: 1, borderColor: 'divider' }}>
|
|
<Tabs
|
|
value={activeTab}
|
|
onChange={(_e, v) => setActiveTab(v)}
|
|
aria-label="Mitglied Details"
|
|
variant="scrollable"
|
|
scrollButtons="auto"
|
|
>
|
|
<Tab label="Stammdaten" id="tab-0" aria-controls="tabpanel-0" />
|
|
<Tab label="Ausrüstung & Uniform" id="tab-1" aria-controls="tabpanel-1" />
|
|
<Tab label="Qualifikationen" id="tab-2" aria-controls="tabpanel-2" />
|
|
<Tab label="Einsätze" id="tab-3" aria-controls="tabpanel-3" />
|
|
</Tabs>
|
|
</Box>
|
|
|
|
{/* ---- Tab 0: Stammdaten ---- */}
|
|
<TabPanel value={activeTab} index={0}>
|
|
<Grid container spacing={3}>
|
|
{/* Personal data */}
|
|
<Grid item xs={12} md={6}>
|
|
<Card>
|
|
<CardHeader
|
|
avatar={<PersonIcon color="primary" />}
|
|
title="Persönliche Daten"
|
|
/>
|
|
<CardContent>
|
|
{editMode && canWrite ? (
|
|
<Stack spacing={2}>
|
|
<TextField
|
|
label="Dienstgrad"
|
|
select
|
|
fullWidth
|
|
size="small"
|
|
value={formData.dienstgrad ?? ''}
|
|
onChange={(e) => handleFieldChange('dienstgrad', e.target.value as DienstgradEnum || undefined)}
|
|
>
|
|
<MenuItem value="">—</MenuItem>
|
|
{DIENSTGRAD_VALUES.map((dg) => (
|
|
<MenuItem key={dg} value={dg}>{dg}</MenuItem>
|
|
))}
|
|
</TextField>
|
|
|
|
<TextField
|
|
label="Dienstgrad seit"
|
|
fullWidth
|
|
size="small"
|
|
placeholder="TT.MM.JJJJ"
|
|
value={formData.dienstgrad_seit ?? ''}
|
|
onChange={(e) => handleFieldChange('dienstgrad_seit', e.target.value || undefined)}
|
|
InputLabelProps={{ shrink: true }}
|
|
/>
|
|
|
|
<TextField
|
|
label="Status"
|
|
select
|
|
fullWidth
|
|
size="small"
|
|
value={formData.status ?? 'aktiv'}
|
|
onChange={(e) => handleFieldChange('status', e.target.value as StatusEnum)}
|
|
>
|
|
{STATUS_VALUES.map((s) => (
|
|
<MenuItem key={s} value={s}>{STATUS_LABELS[s]}</MenuItem>
|
|
))}
|
|
</TextField>
|
|
|
|
<TextField
|
|
label="Eintrittsdatum"
|
|
fullWidth
|
|
size="small"
|
|
placeholder="TT.MM.JJJJ"
|
|
value={formData.eintrittsdatum ?? ''}
|
|
onChange={(e) => handleFieldChange('eintrittsdatum', e.target.value || undefined)}
|
|
InputLabelProps={{ shrink: true }}
|
|
/>
|
|
|
|
<TextField
|
|
label="Geburtsdatum"
|
|
fullWidth
|
|
size="small"
|
|
placeholder="TT.MM.JJJJ"
|
|
value={formData.geburtsdatum ?? ''}
|
|
onChange={(e) => handleFieldChange('geburtsdatum', e.target.value || undefined)}
|
|
InputLabelProps={{ shrink: true }}
|
|
/>
|
|
|
|
<TextField
|
|
label="Austrittsdatum"
|
|
fullWidth
|
|
size="small"
|
|
placeholder="TT.MM.JJJJ"
|
|
value={formData.austrittsdatum ?? ''}
|
|
onChange={(e) => handleFieldChange('austrittsdatum', e.target.value || undefined)}
|
|
InputLabelProps={{ shrink: true }}
|
|
/>
|
|
|
|
<TextField
|
|
label="Standesbuchnummer"
|
|
fullWidth
|
|
size="small"
|
|
value={formData.fdisk_standesbuch_nr ?? ''}
|
|
onChange={(e) => handleFieldChange('fdisk_standesbuch_nr', e.target.value || undefined)}
|
|
/>
|
|
|
|
<Autocomplete
|
|
multiple
|
|
options={[...FUNKTION_VALUES]}
|
|
value={formData.funktion ?? []}
|
|
onChange={(_e, newValue) => handleFieldChange('funktion', newValue as FunktionEnum[])}
|
|
renderInput={(params) => (
|
|
<TextField {...params} label="Funktion" size="small" />
|
|
)}
|
|
size="small"
|
|
/>
|
|
</Stack>
|
|
) : (
|
|
<>
|
|
<FieldRow label="Dienstgrad" value={profile?.dienstgrad ?? null} />
|
|
<FieldRow
|
|
label="Dienstgrad seit"
|
|
value={profile?.dienstgrad_seit
|
|
? new Date(profile.dienstgrad_seit).toLocaleDateString('de-AT')
|
|
: null}
|
|
/>
|
|
<FieldRow label="Status" value={
|
|
profile?.status
|
|
? <StatusChip status={profile.status} labelMap={STATUS_LABELS} colorMap={STATUS_COLORS} />
|
|
: null
|
|
} />
|
|
<FieldRow
|
|
label="Eintrittsdatum"
|
|
value={profile?.eintrittsdatum
|
|
? new Date(profile.eintrittsdatum).toLocaleDateString('de-AT')
|
|
: null}
|
|
/>
|
|
<FieldRow
|
|
label="Geburtsdatum"
|
|
value={
|
|
profile?.geburtsdatum
|
|
? new Date(profile.geburtsdatum).toLocaleDateString('de-AT')
|
|
: profile?._age
|
|
? `(${profile._age} Jahre)`
|
|
: null
|
|
}
|
|
/>
|
|
<FieldRow
|
|
label="Austrittsdatum"
|
|
value={profile?.austrittsdatum
|
|
? new Date(profile.austrittsdatum).toLocaleDateString('de-AT')
|
|
: null}
|
|
/>
|
|
{editMode && isOwnProfile ? (
|
|
<Box>
|
|
<TextField
|
|
label="Standesbuchnummer"
|
|
fullWidth
|
|
size="small"
|
|
value={formData.fdisk_standesbuch_nr ?? ''}
|
|
onChange={(e) => handleFieldChange('fdisk_standesbuch_nr', e.target.value || undefined)}
|
|
/>
|
|
</Box>
|
|
) : (
|
|
<FieldRow label="Standesbuchnummer" value={profile?.fdisk_standesbuch_nr ?? null} />
|
|
)}
|
|
<FieldRow label="Geburtsort" value={profile?.geburtsort ?? null} />
|
|
<FieldRow label="Geschlecht" value={profile?.geschlecht ?? null} />
|
|
<FieldRow label="Beruf" value={profile?.beruf ?? null} />
|
|
<FieldRow label="Wohnort" value={
|
|
profile?.wohnort
|
|
? [profile.plz, profile.wohnort].filter(Boolean).join(' ')
|
|
: null
|
|
} />
|
|
</>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
</Grid>
|
|
|
|
{/* Contact */}
|
|
<Grid item xs={12} md={6}>
|
|
<Card>
|
|
<CardHeader
|
|
avatar={<PhoneIcon color="primary" />}
|
|
title="Kontaktdaten"
|
|
/>
|
|
<CardContent>
|
|
{editMode ? (
|
|
<Stack spacing={2}>
|
|
<TextField
|
|
label="Mobil"
|
|
fullWidth
|
|
size="small"
|
|
value={formData.telefon_mobil ?? ''}
|
|
onChange={(e) => handleFieldChange('telefon_mobil', e.target.value || undefined)}
|
|
placeholder="+436641234567"
|
|
/>
|
|
<TextField
|
|
label="Privat"
|
|
fullWidth
|
|
size="small"
|
|
value={formData.telefon_privat ?? ''}
|
|
onChange={(e) => handleFieldChange('telefon_privat', e.target.value || undefined)}
|
|
placeholder="+4371234567"
|
|
/>
|
|
<Divider />
|
|
<Typography variant="caption" color="text.secondary">
|
|
Notfallkontakt
|
|
</Typography>
|
|
<TextField
|
|
label="Name"
|
|
fullWidth
|
|
size="small"
|
|
value={formData.notfallkontakt_name ?? ''}
|
|
onChange={(e) => handleFieldChange('notfallkontakt_name', e.target.value || undefined)}
|
|
/>
|
|
<TextField
|
|
label="Telefon"
|
|
fullWidth
|
|
size="small"
|
|
value={formData.notfallkontakt_telefon ?? ''}
|
|
onChange={(e) => handleFieldChange('notfallkontakt_telefon', e.target.value || undefined)}
|
|
placeholder="+436641234567"
|
|
/>
|
|
</Stack>
|
|
) : (
|
|
<>
|
|
<FieldRow label="Mobil" value={formatPhone(profile?.telefon_mobil)} />
|
|
<FieldRow label="Privat" value={formatPhone(profile?.telefon_privat)} />
|
|
<FieldRow
|
|
label="E-Mail"
|
|
value={
|
|
<a href={`mailto:${member.email}`} style={{ color: 'inherit' }}>
|
|
{member.email}
|
|
</a>
|
|
}
|
|
/>
|
|
<Divider sx={{ my: 1 }} />
|
|
<Typography variant="caption" color="text.secondary" sx={{ display: 'block', mb: 1 }}>
|
|
Notfallkontakt
|
|
</Typography>
|
|
<FieldRow label="Name" value={profile?.notfallkontakt_name ?? null} />
|
|
<FieldRow label="Telefon" value={formatPhone(profile?.notfallkontakt_telefon)} />
|
|
</>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
</Grid>
|
|
|
|
{/* Driving licenses */}
|
|
<Grid item xs={12} md={6}>
|
|
<Card>
|
|
<CardHeader
|
|
avatar={<DriveEtaIcon color="primary" />}
|
|
title="Führerscheinklassen"
|
|
/>
|
|
<CardContent>
|
|
{editMode && canWrite ? (
|
|
<Autocomplete
|
|
multiple
|
|
freeSolo
|
|
options={['AM', 'A', 'B', 'BE', 'C', 'CE', 'C1', 'C1E', 'D', 'DE', 'D1', 'D1E', 'F']}
|
|
value={formData.fuehrerscheinklassen ?? []}
|
|
onChange={(_e, newValue) => handleFieldChange('fuehrerscheinklassen', newValue)}
|
|
renderInput={(params) => (
|
|
<TextField {...params} label="Führerscheinklassen" size="small" placeholder="Klasse hinzufügen…" />
|
|
)}
|
|
size="small"
|
|
/>
|
|
) : displayedFuehrerscheinklassen.length > 0 ? (
|
|
<Box sx={{ display: 'flex', gap: 0.5, flexWrap: 'wrap' }}>
|
|
{displayedFuehrerscheinklassen.map((k) => (
|
|
<Chip key={k} label={k} size="small" variant="outlined" />
|
|
))}
|
|
</Box>
|
|
) : (
|
|
<Typography color="text.secondary" variant="body2">—</Typography>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
</Grid>
|
|
|
|
{/* Rank history */}
|
|
<Grid item xs={12}>
|
|
<Card>
|
|
<CardHeader
|
|
avatar={<HistoryIcon color="primary" />}
|
|
title="Dienstgrad-Verlauf"
|
|
/>
|
|
<CardContent>
|
|
<RankTimeline entries={member.dienstgrad_verlauf ?? []} />
|
|
</CardContent>
|
|
</Card>
|
|
</Grid>
|
|
|
|
{/* Remarks — Kommandant/Admin only */}
|
|
{canWrite && (
|
|
<Grid item xs={12}>
|
|
<Card>
|
|
<CardHeader title="Interne Bemerkungen" />
|
|
<CardContent>
|
|
{editMode ? (
|
|
<TextField
|
|
fullWidth
|
|
multiline
|
|
rows={4}
|
|
label="Bemerkungen"
|
|
value={formData.bemerkungen ?? ''}
|
|
onChange={(e) => handleFieldChange('bemerkungen', e.target.value || undefined)}
|
|
/>
|
|
) : (
|
|
<Typography variant="body2" color={profile?.bemerkungen ? 'text.primary' : 'text.secondary'}>
|
|
{profile?.bemerkungen ?? 'Keine Bemerkungen eingetragen.'}
|
|
</Typography>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
</Grid>
|
|
)}
|
|
</Grid>
|
|
</TabPanel>
|
|
|
|
{/* ---- Tab 1: Ausrüstung & Uniform ---- */}
|
|
<TabPanel value={activeTab} index={1}>
|
|
<Grid container spacing={3}>
|
|
<Grid item xs={12} md={6}>
|
|
<Card>
|
|
<CardHeader
|
|
avatar={<SecurityIcon color="primary" />}
|
|
title="Ausrüstung & Uniform"
|
|
/>
|
|
<CardContent>
|
|
{editMode ? (
|
|
<Stack spacing={2}>
|
|
<TextField
|
|
label="T-Shirt Größe"
|
|
select
|
|
fullWidth
|
|
size="small"
|
|
value={formData.tshirt_groesse ?? ''}
|
|
onChange={(e) => handleFieldChange('tshirt_groesse', e.target.value as TshirtGroesseEnum || undefined)}
|
|
>
|
|
<MenuItem value="">—</MenuItem>
|
|
{TSHIRT_GROESSE_VALUES.map((g) => (
|
|
<MenuItem key={g} value={g}>{g}</MenuItem>
|
|
))}
|
|
</TextField>
|
|
<TextField
|
|
label="Schuhgröße"
|
|
fullWidth
|
|
size="small"
|
|
value={formData.schuhgroesse ?? ''}
|
|
onChange={(e) => handleFieldChange('schuhgroesse', e.target.value || undefined)}
|
|
placeholder="z.B. 43"
|
|
/>
|
|
</Stack>
|
|
) : (
|
|
<>
|
|
<FieldRow label="T-Shirt Größe" value={profile?.tshirt_groesse ?? null} />
|
|
<FieldRow label="Schuhgröße" value={profile?.schuhgroesse ?? null} />
|
|
</>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
</Grid>
|
|
|
|
{(hasPermission('persoenliche_ausruestung:view') || hasPermission('persoenliche_ausruestung:view_all')) && (
|
|
<Grid item xs={12}>
|
|
<Card>
|
|
<CardHeader
|
|
avatar={<SecurityIcon color="primary" />}
|
|
title="Persönliche Ausrüstung"
|
|
/>
|
|
<CardContent>
|
|
{personalEquipmentLoading ? (
|
|
<CircularProgress size={24} />
|
|
) : personalEquipment.length === 0 ? (
|
|
<Typography color="text.secondary" variant="body2">
|
|
Keine persönlichen Gegenstände erfasst
|
|
</Typography>
|
|
) : (
|
|
personalEquipment.map((item) => (
|
|
<Box
|
|
key={item.id}
|
|
sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', py: 0.75, borderBottom: '1px solid', borderColor: 'divider', cursor: 'pointer', '&:hover': { bgcolor: 'action.hover' } }}
|
|
onClick={() => navigate(`/persoenliche-ausruestung/${item.id}`)}
|
|
>
|
|
<Box>
|
|
<Typography variant="body2" fontWeight={500}>{item.bezeichnung}</Typography>
|
|
{item.kategorie && (
|
|
<Typography variant="caption" color="text.secondary">{item.kategorie}</Typography>
|
|
)}
|
|
{item.eigenschaften && item.eigenschaften.length > 0 && (
|
|
<Box sx={{ display: 'flex', gap: 0.5, flexWrap: 'wrap', mt: 0.25 }}>
|
|
{item.eigenschaften.map((e) => (
|
|
<Chip
|
|
key={e.id}
|
|
label={`${e.name}: ${e.wert}`}
|
|
size="small"
|
|
variant="outlined"
|
|
sx={{ height: 18, fontSize: '0.65rem' }}
|
|
/>
|
|
))}
|
|
</Box>
|
|
)}
|
|
</Box>
|
|
<Chip
|
|
label={zustandOptions.find(o => o.key === item.zustand)?.label ?? item.zustand}
|
|
color={(zustandOptions.find(o => o.key === item.zustand)?.color ?? 'default') as any}
|
|
size="small"
|
|
variant="outlined"
|
|
/>
|
|
</Box>
|
|
))
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
</Grid>
|
|
)}
|
|
</Grid>
|
|
</TabPanel>
|
|
|
|
{/* ---- Tab 2: Qualifikationen ---- */}
|
|
<TabPanel value={activeTab} index={2}>
|
|
<Box sx={{ borderBottom: 1, borderColor: 'divider', mb: 2 }}>
|
|
<Tabs
|
|
value={qualSubTab}
|
|
onChange={(_e, v) => setQualSubTab(v)}
|
|
variant="scrollable"
|
|
scrollButtons="auto"
|
|
aria-label="Qualifikationen"
|
|
>
|
|
<Tab label="Atemschutz" id="qual-tab-0" aria-controls="qual-tabpanel-0" />
|
|
<Tab label="Ausbildungen" id="qual-tab-1" aria-controls="qual-tabpanel-1" />
|
|
<Tab label="Untersuchungen" id="qual-tab-2" aria-controls="qual-tabpanel-2" />
|
|
<Tab label="Beförderungen" id="qual-tab-3" aria-controls="qual-tabpanel-3" />
|
|
<Tab label="Fahrgenehmigungen" id="qual-tab-4" aria-controls="qual-tabpanel-4" />
|
|
</Tabs>
|
|
</Box>
|
|
|
|
{/* Sub-tab 0: Atemschutz */}
|
|
<TabPanel value={qualSubTab} index={0}>
|
|
{atemschutzLoading ? (
|
|
<Box sx={{ display: 'flex', justifyContent: 'center', pt: 6 }}>
|
|
<CircularProgress />
|
|
</Box>
|
|
) : atemschutz ? (
|
|
<Card>
|
|
<CardHeader
|
|
avatar={<SecurityIcon color="primary" />}
|
|
title="Atemschutz"
|
|
action={
|
|
<Chip
|
|
icon={atemschutz.einsatzbereit ? <CheckCircleIcon /> : <HighlightOffIcon />}
|
|
label={atemschutz.einsatzbereit ? 'Einsatzbereit' : 'Nicht einsatzbereit'}
|
|
color={atemschutz.einsatzbereit ? 'success' : 'error'}
|
|
size="small"
|
|
/>
|
|
}
|
|
/>
|
|
<CardContent>
|
|
<FieldRow
|
|
label="Lehrgang"
|
|
value={
|
|
atemschutz.atemschutz_lehrgang
|
|
? `Ja${atemschutz.lehrgang_datum ? ` (${new Date(atemschutz.lehrgang_datum).toLocaleDateString('de-AT')})` : ''}`
|
|
: 'Nein'
|
|
}
|
|
/>
|
|
|
|
<Divider sx={{ my: 1.5 }} />
|
|
<Typography variant="caption" color="text.secondary" sx={{ display: 'block', mb: 0.5 }}>
|
|
G26.3 Untersuchung
|
|
</Typography>
|
|
<FieldRow
|
|
label="Datum"
|
|
value={atemschutz.untersuchung_datum
|
|
? new Date(atemschutz.untersuchung_datum).toLocaleDateString('de-AT')
|
|
: null}
|
|
/>
|
|
<FieldRow
|
|
label="Gültig bis"
|
|
value={
|
|
<ValidityChip
|
|
date={atemschutz.untersuchung_gueltig_bis}
|
|
gueltig={atemschutz.untersuchung_gueltig}
|
|
tageRest={atemschutz.untersuchung_tage_rest}
|
|
/>
|
|
}
|
|
/>
|
|
<FieldRow
|
|
label="Ergebnis"
|
|
value={
|
|
atemschutz.untersuchung_ergebnis
|
|
? UntersuchungErgebnisLabel[atemschutz.untersuchung_ergebnis]
|
|
: null
|
|
}
|
|
/>
|
|
|
|
<Divider sx={{ my: 1.5 }} />
|
|
<Typography variant="caption" color="text.secondary" sx={{ display: 'block', mb: 0.5 }}>
|
|
Leistungstest (Finnentest)
|
|
</Typography>
|
|
<FieldRow
|
|
label="Datum"
|
|
value={atemschutz.leistungstest_datum
|
|
? new Date(atemschutz.leistungstest_datum).toLocaleDateString('de-AT')
|
|
: null}
|
|
/>
|
|
<FieldRow
|
|
label="Gültig bis"
|
|
value={
|
|
<ValidityChip
|
|
date={atemschutz.leistungstest_gueltig_bis}
|
|
gueltig={atemschutz.leistungstest_gueltig}
|
|
tageRest={atemschutz.leistungstest_tage_rest}
|
|
/>
|
|
}
|
|
/>
|
|
<FieldRow
|
|
label="Bestanden"
|
|
value={
|
|
atemschutz.leistungstest_bestanden === true
|
|
? 'Ja'
|
|
: atemschutz.leistungstest_bestanden === false
|
|
? 'Nein'
|
|
: null
|
|
}
|
|
/>
|
|
|
|
{atemschutz.bemerkung && (
|
|
<>
|
|
<Divider sx={{ my: 1.5 }} />
|
|
<FieldRow label="Bemerkung" value={atemschutz.bemerkung} />
|
|
</>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
) : (
|
|
<Card>
|
|
<CardContent>
|
|
<Box sx={{ display: 'flex', flexDirection: 'column', alignItems: 'center', py: 6, gap: 2 }}>
|
|
<SecurityIcon sx={{ fontSize: 64, color: 'text.disabled' }} />
|
|
<Typography variant="h6" color="text.secondary">
|
|
Kein Atemschutz-Eintrag
|
|
</Typography>
|
|
<Typography variant="body2" color="text.secondary" textAlign="center" maxWidth={480}>
|
|
Für dieses Mitglied ist kein Atemschutz-Datensatz vorhanden.
|
|
</Typography>
|
|
{canWrite && (
|
|
<Button variant="outlined" onClick={() => navigate('/atemschutz')}>
|
|
Zum Atemschutzmodul
|
|
</Button>
|
|
)}
|
|
</Box>
|
|
</CardContent>
|
|
</Card>
|
|
)}
|
|
</TabPanel>
|
|
|
|
{/* Sub-tab 1: Ausbildungen */}
|
|
<TabPanel value={qualSubTab} index={1}>
|
|
<Card>
|
|
<CardHeader
|
|
avatar={<SchoolIcon color="primary" />}
|
|
title="Ausbildungen"
|
|
/>
|
|
<CardContent>
|
|
{ausbildungen.length === 0 ? (
|
|
<Typography color="text.secondary" variant="body2">Keine Ausbildungen eingetragen.</Typography>
|
|
) : (
|
|
ausbildungen.map((a) => (
|
|
<Box key={a.id} sx={{ py: 0.75, borderBottom: '1px solid', borderColor: 'divider' }}>
|
|
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline' }}>
|
|
<Typography variant="body2" fontWeight={500}>
|
|
{a.kursname}
|
|
</Typography>
|
|
<Typography variant="caption" color="text.secondary">
|
|
{a.kurs_datum ? new Date(a.kurs_datum).toLocaleDateString('de-AT') : '—'}
|
|
</Typography>
|
|
</Box>
|
|
<Box sx={{ display: 'flex', gap: 0.5, flexWrap: 'wrap', alignItems: 'center', mt: 0.25 }}>
|
|
{a.kurs_kurzbezeichnung && (
|
|
<Chip label={a.kurs_kurzbezeichnung} size="small" variant="outlined" />
|
|
)}
|
|
{a.erfolgscode && (
|
|
<Chip
|
|
label={a.erfolgscode}
|
|
size="small"
|
|
variant="outlined"
|
|
color={
|
|
/mit erfolg|bestanden/i.test(a.erfolgscode) ? 'success'
|
|
: /teilgenommen/i.test(a.erfolgscode) ? 'info'
|
|
: 'default'
|
|
}
|
|
/>
|
|
)}
|
|
{a.ort && (
|
|
<Typography variant="body2" color="text.secondary">
|
|
{a.ort}
|
|
</Typography>
|
|
)}
|
|
</Box>
|
|
{a.ablaufdatum && (
|
|
<Typography variant="caption" color="text.secondary">
|
|
Gültig bis: {new Date(a.ablaufdatum).toLocaleDateString('de-AT')}
|
|
</Typography>
|
|
)}
|
|
</Box>
|
|
))
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
</TabPanel>
|
|
|
|
{/* Sub-tab 2: Untersuchungen */}
|
|
<TabPanel value={qualSubTab} index={2}>
|
|
<Card>
|
|
<CardHeader
|
|
avatar={<LocalHospitalIcon color="primary" />}
|
|
title="Untersuchungen"
|
|
/>
|
|
<CardContent>
|
|
{untersuchungen.length === 0 ? (
|
|
<Typography color="text.secondary" variant="body2">Keine Untersuchungen eingetragen.</Typography>
|
|
) : (
|
|
untersuchungen.map((u) => (
|
|
<Box key={u.id} sx={{ py: 0.75, borderBottom: '1px solid', borderColor: 'divider' }}>
|
|
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline' }}>
|
|
<Typography variant="body2" fontWeight={500}>
|
|
{u.art}
|
|
</Typography>
|
|
<Typography variant="caption" color="text.secondary">
|
|
{u.datum ? new Date(u.datum).toLocaleDateString('de-AT') : '—'}
|
|
</Typography>
|
|
</Box>
|
|
{u.ergebnis && (
|
|
<Typography variant="body2" color="text.secondary">
|
|
{u.ergebnis}
|
|
</Typography>
|
|
)}
|
|
{u.anmerkungen && (
|
|
<Typography variant="caption" color="text.secondary">
|
|
{u.anmerkungen}
|
|
</Typography>
|
|
)}
|
|
</Box>
|
|
))
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
</TabPanel>
|
|
|
|
{/* Sub-tab 3: Beförderungen */}
|
|
<TabPanel value={qualSubTab} index={3}>
|
|
<Card>
|
|
<CardHeader
|
|
avatar={<MilitaryTechIcon color="primary" />}
|
|
title="Beförderungen"
|
|
/>
|
|
<CardContent>
|
|
{befoerderungen.length === 0 ? (
|
|
<Typography color="text.secondary" variant="body2">Keine Beförderungen eingetragen.</Typography>
|
|
) : (
|
|
befoerderungen.map((b) => (
|
|
<FieldRow
|
|
key={b.id}
|
|
label={b.datum ? new Date(b.datum).toLocaleDateString('de-AT') : '—'}
|
|
value={b.dienstgrad}
|
|
/>
|
|
))
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
</TabPanel>
|
|
|
|
{/* Sub-tab 4: Fahrgenehmigungen */}
|
|
<TabPanel value={qualSubTab} index={4}>
|
|
<Card>
|
|
<CardHeader
|
|
avatar={<DriveEtaIcon color="primary" />}
|
|
title="Gesetzliche Fahrgenehmigungen"
|
|
/>
|
|
<CardContent>
|
|
{fahrgenehmigungen.length === 0 ? (
|
|
<Typography color="text.secondary" variant="body2">Keine Fahrgenehmigungen eingetragen.</Typography>
|
|
) : (
|
|
fahrgenehmigungen.map((f) => (
|
|
<Box key={f.id} sx={{ py: 0.75, borderBottom: '1px solid', borderColor: 'divider' }}>
|
|
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline' }}>
|
|
<Typography variant="body2" fontWeight={500}>
|
|
{f.klasse}
|
|
</Typography>
|
|
<Typography variant="caption" color="text.secondary">
|
|
{f.ausstellungsdatum ? new Date(f.ausstellungsdatum).toLocaleDateString('de-AT') : '—'}
|
|
</Typography>
|
|
</Box>
|
|
{(f.behoerde || f.nummer) && (
|
|
<Typography variant="body2" color="text.secondary">
|
|
{[f.behoerde, f.nummer].filter(Boolean).join(' · ')}
|
|
</Typography>
|
|
)}
|
|
{f.gueltig_bis && (
|
|
<Typography variant="caption" color="text.secondary">
|
|
Gültig bis: {new Date(f.gueltig_bis).toLocaleDateString('de-AT')}
|
|
</Typography>
|
|
)}
|
|
</Box>
|
|
))
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
</TabPanel>
|
|
</TabPanel>
|
|
|
|
{/* ---- Tab 3: Einsätze (placeholder) ---- */}
|
|
<TabPanel value={activeTab} index={3}>
|
|
<Card>
|
|
<CardContent>
|
|
<Box sx={{ display: 'flex', flexDirection: 'column', alignItems: 'center', py: 6, gap: 2 }}>
|
|
<PersonIcon sx={{ fontSize: 64, color: 'text.disabled' }} />
|
|
<Typography variant="h6" color="text.secondary">
|
|
Einsätze dieses Mitglieds
|
|
</Typography>
|
|
<Typography variant="body2" color="text.secondary" textAlign="center" maxWidth={480}>
|
|
Diese Funktion wird verfügbar sobald das Einsatz-Modul implementiert ist.
|
|
</Typography>
|
|
</Box>
|
|
</CardContent>
|
|
</Card>
|
|
</TabPanel>
|
|
</Container>
|
|
</DashboardLayout>
|
|
);
|
|
}
|
|
|
|
export default MitgliedDetail;
|