add features
This commit is contained in:
792
frontend/src/pages/MitgliedDetail.tsx
Normal file
792
frontend/src/pages/MitgliedDetail.tsx
Normal file
@@ -0,0 +1,792 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import {
|
||||
Container,
|
||||
Box,
|
||||
Typography,
|
||||
Card,
|
||||
CardContent,
|
||||
CardHeader,
|
||||
Avatar,
|
||||
Button,
|
||||
Chip,
|
||||
Tabs,
|
||||
Tab,
|
||||
Grid,
|
||||
TextField,
|
||||
FormControl,
|
||||
InputLabel,
|
||||
Select,
|
||||
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 {
|
||||
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 (
|
||||
<div role="tabpanel" hidden={value !== index} aria-labelledby={`tab-${index}`}>
|
||||
{value === index && <Box sx={{ pt: 3 }}>{children}</Box>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// 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>
|
||||
);
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// 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<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);
|
||||
|
||||
// Edit form state — only the fields the user is allowed to change
|
||||
const [formData, setFormData] = useState<UpdateMemberProfileData>({});
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// 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,
|
||||
funktion: member.profile.funktion,
|
||||
status: member.profile.status,
|
||||
eintrittsdatum: member.profile.eintrittsdatum ?? undefined,
|
||||
geburtsdatum: 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,
|
||||
});
|
||||
}
|
||||
}, [member]);
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// Save
|
||||
// ----------------------------------------------------------------
|
||||
const handleSave = async () => {
|
||||
if (!userId) return;
|
||||
setSaving(true);
|
||||
setSaveError(null);
|
||||
try {
|
||||
const updated = await membersService.updateMember(userId, formData);
|
||||
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({
|
||||
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,
|
||||
tshirt_groesse: member.profile.tshirt_groesse ?? undefined,
|
||||
schuhgroesse: member.profile.schuhgroesse ?? 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">
|
||||
{/* Back button */}
|
||||
<Button
|
||||
variant="text"
|
||||
onClick={() => navigate('/mitglieder')}
|
||||
sx={{ mb: 2 }}
|
||||
>
|
||||
← Mitgliederliste
|
||||
</Button>
|
||||
|
||||
{/* 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?.mitglieds_nr && (
|
||||
<Chip
|
||||
icon={<BadgeIcon />}
|
||||
label={`Nr. ${profile.mitglieds_nr}`}
|
||||
size="small"
|
||||
variant="outlined"
|
||||
/>
|
||||
)}
|
||||
{profile?.status && (
|
||||
<Chip
|
||||
label={STATUS_LABELS[profile.status]}
|
||||
size="small"
|
||||
color={STATUS_COLORS[profile.status]}
|
||||
/>
|
||||
)}
|
||||
</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 && 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 */}
|
||||
{canEdit && (
|
||||
<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 }}>
|
||||
Für dieses Mitglied wurde noch kein Profil angelegt.
|
||||
{canWrite && ' Ein Kommandant kann das Profil unter "Stammdaten" erstellen.'}
|
||||
</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"
|
||||
>
|
||||
<Tab label="Stammdaten" id="tab-0" aria-controls="tabpanel-0" />
|
||||
<Tab label="Qualifikationen" id="tab-1" aria-controls="tabpanel-1" />
|
||||
<Tab label="Einsätze" id="tab-2" aria-controls="tabpanel-2" />
|
||||
</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"
|
||||
type="date"
|
||||
fullWidth
|
||||
size="small"
|
||||
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="Mitgliedsnummer"
|
||||
fullWidth
|
||||
size="small"
|
||||
value={formData.mitglieds_nr ?? ''}
|
||||
onChange={(e) => handleFieldChange('mitglieds_nr', e.target.value || undefined)}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
label="Eintrittsdatum"
|
||||
type="date"
|
||||
fullWidth
|
||||
size="small"
|
||||
value={formData.eintrittsdatum ?? ''}
|
||||
onChange={(e) => handleFieldChange('eintrittsdatum', e.target.value || undefined)}
|
||||
InputLabelProps={{ shrink: true }}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
label="Geburtsdatum"
|
||||
type="date"
|
||||
fullWidth
|
||||
size="small"
|
||||
value={formData.geburtsdatum ?? ''}
|
||||
onChange={(e) => handleFieldChange('geburtsdatum', e.target.value || undefined)}
|
||||
InputLabelProps={{ shrink: true }}
|
||||
/>
|
||||
</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
|
||||
? <Chip label={STATUS_LABELS[profile.status]} size="small" color={STATUS_COLORS[profile.status]} />
|
||||
: null
|
||||
} />
|
||||
<FieldRow label="Mitgliedsnummer" value={profile?.mitglieds_nr ?? 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
|
||||
}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</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>
|
||||
|
||||
{/* Uniform sizing */}
|
||||
<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>
|
||||
|
||||
{/* Driving licenses */}
|
||||
<Grid item xs={12} md={6}>
|
||||
<Card>
|
||||
<CardHeader
|
||||
avatar={<DriveEtaIcon color="primary" />}
|
||||
title="Führerscheinklassen"
|
||||
/>
|
||||
<CardContent>
|
||||
{profile?.fuehrerscheinklassen && profile.fuehrerscheinklassen.length > 0 ? (
|
||||
<Box sx={{ display: 'flex', gap: 0.5, flexWrap: 'wrap' }}>
|
||||
{profile.fuehrerscheinklassen.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: Qualifikationen (placeholder) ---- */}
|
||||
<TabPanel value={activeTab} index={1}>
|
||||
<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">
|
||||
Qualifikationen & Lehrgänge
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary" textAlign="center" maxWidth={480}>
|
||||
Diese Funktion wird in einer zukünftigen Version verfügbar sein.
|
||||
Geplant: Atemschutz, G26-Untersuchungen, Absolvierte Kurse, Gültigkeitsdaten.
|
||||
</Typography>
|
||||
</Box>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabPanel>
|
||||
|
||||
{/* ---- Tab 2: Einsätze (placeholder) ---- */}
|
||||
<TabPanel value={activeTab} index={2}>
|
||||
<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;
|
||||
Reference in New Issue
Block a user