refactor(mitglieder): split member profile into Stammdaten/Ausrüstung/Qualifikationen tabs with sub-tabs per qualification type
This commit is contained in:
@@ -1,10 +1,11 @@
|
|||||||
import { useState, useCallback } from 'react';
|
import { useState, useCallback } from 'react';
|
||||||
import {
|
import {
|
||||||
Box, Paper, Typography, TextField, Button, Alert,
|
Box, Paper, Typography, TextField, Button, Alert,
|
||||||
CircularProgress, Divider, Autocomplete,
|
CircularProgress, Divider, Autocomplete, Chip,
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import DeleteSweepIcon from '@mui/icons-material/DeleteSweep';
|
import DeleteSweepIcon from '@mui/icons-material/DeleteSweep';
|
||||||
import RestartAltIcon from '@mui/icons-material/RestartAlt';
|
import RestartAltIcon from '@mui/icons-material/RestartAlt';
|
||||||
|
import GroupAddIcon from '@mui/icons-material/GroupAdd';
|
||||||
import { api } from '../../services/api';
|
import { api } from '../../services/api';
|
||||||
import { adminApi } from '../../services/admin';
|
import { adminApi } from '../../services/admin';
|
||||||
import { useNotification } from '../../contexts/NotificationContext';
|
import { useNotification } from '../../contexts/NotificationContext';
|
||||||
@@ -60,26 +61,45 @@ export default function DataManagementTab() {
|
|||||||
queryKey: ['admin', 'users'],
|
queryKey: ['admin', 'users'],
|
||||||
queryFn: adminApi.getUsers,
|
queryFn: adminApi.getUsers,
|
||||||
});
|
});
|
||||||
const [selectedUser, setSelectedUser] = useState<UserOverview | null>(null);
|
const [selectedUsers, setSelectedUsers] = useState<UserOverview[]>([]);
|
||||||
|
const [fdiskInput, setFdiskInput] = useState('');
|
||||||
const [purging, setPurging] = useState(false);
|
const [purging, setPurging] = useState(false);
|
||||||
const [purgeConfirmOpen, setPurgeConfirmOpen] = useState(false);
|
const [purgeConfirmOpen, setPurgeConfirmOpen] = useState(false);
|
||||||
|
|
||||||
|
const handleAddUser = useCallback((user: UserOverview) => {
|
||||||
|
setSelectedUsers(prev => prev.some(u => u.id === user.id) ? prev : [...prev, user]);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleRemoveUser = useCallback((userId: string) => {
|
||||||
|
setSelectedUsers(prev => prev.filter(u => u.id !== userId));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleAddAll = useCallback(() => {
|
||||||
|
setSelectedUsers(users);
|
||||||
|
}, [users]);
|
||||||
|
|
||||||
const handlePurge = useCallback(async () => {
|
const handlePurge = useCallback(async () => {
|
||||||
if (!selectedUser) return;
|
if (selectedUsers.length === 0) return;
|
||||||
setPurging(true);
|
setPurging(true);
|
||||||
try {
|
let totalDeleted = 0;
|
||||||
const result = await adminApi.purgeFdiskData(selectedUser.id);
|
let errorCount = 0;
|
||||||
const total = result.profileFieldsCleared + result.ausbildungen + result.befoerderungen + result.untersuchungen + result.fahrgenehmigungen;
|
for (const user of selectedUsers) {
|
||||||
showSuccess(`${total} FDISK-Eintraege fuer ${selectedUser.name || selectedUser.email} geloescht`);
|
try {
|
||||||
setSelectedUser(null);
|
const result = await adminApi.purgeFdiskData(user.id);
|
||||||
} catch (err: unknown) {
|
totalDeleted += result.profileFieldsCleared + result.ausbildungen + result.befoerderungen + result.untersuchungen + result.fahrgenehmigungen;
|
||||||
const msg = (err as { response?: { data?: { message?: string } } })?.response?.data?.message || 'Fehler beim Loeschen';
|
} catch {
|
||||||
showError(msg);
|
errorCount++;
|
||||||
} finally {
|
}
|
||||||
setPurging(false);
|
|
||||||
setPurgeConfirmOpen(false);
|
|
||||||
}
|
}
|
||||||
}, [selectedUser, showSuccess, showError]);
|
setPurging(false);
|
||||||
|
setPurgeConfirmOpen(false);
|
||||||
|
if (errorCount === 0) {
|
||||||
|
showSuccess(`${totalDeleted} FDISK-Eintraege fuer ${selectedUsers.length} ${selectedUsers.length === 1 ? 'Benutzer' : 'Benutzer'} geloescht`);
|
||||||
|
setSelectedUsers([]);
|
||||||
|
} else {
|
||||||
|
showError(`${errorCount} von ${selectedUsers.length} Benutzern konnten nicht verarbeitet werden`);
|
||||||
|
}
|
||||||
|
}, [selectedUsers, showSuccess, showError]);
|
||||||
|
|
||||||
const [states, setStates] = useState<Record<string, SectionState>>(() =>
|
const [states, setStates] = useState<Record<string, SectionState>>(() =>
|
||||||
Object.fromEntries(SECTIONS.map(s => [s.key, { days: s.defaultDays, previewCount: null, loading: false }]))
|
Object.fromEntries(SECTIONS.map(s => [s.key, { days: s.defaultDays, previewCount: null, loading: false }]))
|
||||||
@@ -168,38 +188,73 @@ export default function DataManagementTab() {
|
|||||||
{/* FDISK data purge */}
|
{/* FDISK data purge */}
|
||||||
<Paper sx={{ p: 3, mb: 3 }}>
|
<Paper sx={{ p: 3, mb: 3 }}>
|
||||||
<Typography variant="subtitle1" fontWeight={600} sx={{ mb: 1 }}>
|
<Typography variant="subtitle1" fontWeight={600} sx={{ mb: 1 }}>
|
||||||
FDISK-Daten eines Benutzers loeschen
|
FDISK-Daten loeschen
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
|
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
|
||||||
Loescht alle FDISK-synchronisierten Daten eines Benutzers: Profilfelder, Ausbildungen,
|
Loescht alle FDISK-synchronisierten Daten der ausgewaehlten Benutzer: Profilfelder, Ausbildungen,
|
||||||
Befoerderungen, Untersuchungen und Fahrgenehmigungen. Beim naechsten FDISK-Sync werden
|
Befoerderungen, Untersuchungen und Fahrgenehmigungen. Beim naechsten FDISK-Sync werden
|
||||||
die Daten erneut importiert.
|
die Daten erneut importiert.
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
<Box sx={{ display: 'flex', gap: 2, alignItems: 'center', flexWrap: 'wrap' }}>
|
<Box sx={{ display: 'flex', gap: 2, alignItems: 'center', flexWrap: 'wrap' }}>
|
||||||
<Autocomplete
|
<Autocomplete
|
||||||
options={users}
|
options={users.filter(u => !selectedUsers.some(s => s.id === u.id))}
|
||||||
loading={usersLoading}
|
loading={usersLoading}
|
||||||
value={selectedUser}
|
value={null}
|
||||||
onChange={(_e, v) => setSelectedUser(v)}
|
inputValue={fdiskInput}
|
||||||
|
onInputChange={(_e, val, reason) => {
|
||||||
|
if (reason === 'input') setFdiskInput(val);
|
||||||
|
}}
|
||||||
|
onChange={(_e, user) => {
|
||||||
|
if (user) {
|
||||||
|
handleAddUser(user);
|
||||||
|
setFdiskInput('');
|
||||||
|
}
|
||||||
|
}}
|
||||||
getOptionLabel={(u) => `${u.name || 'Kein Name'} (${u.email})`}
|
getOptionLabel={(u) => `${u.name || 'Kein Name'} (${u.email})`}
|
||||||
isOptionEqualToValue={(a, b) => a.id === b.id}
|
isOptionEqualToValue={(a, b) => a.id === b.id}
|
||||||
sx={{ minWidth: 320, flex: 1 }}
|
sx={{ minWidth: 320, flex: 1 }}
|
||||||
renderInput={(params) => (
|
renderInput={(params) => (
|
||||||
<TextField {...params} label="Benutzer waehlen" size="small" />
|
<TextField {...params} label="Benutzer hinzufuegen" size="small" />
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
variant="contained"
|
variant="outlined"
|
||||||
color="error"
|
size="small"
|
||||||
disabled={!selectedUser || purging}
|
startIcon={<GroupAddIcon />}
|
||||||
startIcon={purging ? <CircularProgress size={16} /> : <DeleteSweepIcon />}
|
onClick={handleAddAll}
|
||||||
onClick={() => setPurgeConfirmOpen(true)}
|
disabled={usersLoading || users.length === 0 || selectedUsers.length === users.length}
|
||||||
>
|
>
|
||||||
FDISK-Daten loeschen
|
Alle hinzufuegen
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
|
{selectedUsers.length > 0 && (
|
||||||
|
<>
|
||||||
|
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1, mt: 2 }}>
|
||||||
|
{selectedUsers.map(u => (
|
||||||
|
<Chip
|
||||||
|
key={u.id}
|
||||||
|
label={u.name || u.email}
|
||||||
|
onDelete={() => handleRemoveUser(u.id)}
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
<Box sx={{ mt: 2 }}>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
color="error"
|
||||||
|
disabled={purging}
|
||||||
|
startIcon={purging ? <CircularProgress size={16} /> : <DeleteSweepIcon />}
|
||||||
|
onClick={() => setPurgeConfirmOpen(true)}
|
||||||
|
>
|
||||||
|
FDISK-Daten loeschen ({selectedUsers.length})
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|
||||||
<ConfirmDialog
|
<ConfirmDialog
|
||||||
@@ -207,9 +262,14 @@ export default function DataManagementTab() {
|
|||||||
onClose={() => !purging && setPurgeConfirmOpen(false)}
|
onClose={() => !purging && setPurgeConfirmOpen(false)}
|
||||||
onConfirm={handlePurge}
|
onConfirm={handlePurge}
|
||||||
title="FDISK-Daten loeschen?"
|
title="FDISK-Daten loeschen?"
|
||||||
message={selectedUser ? (
|
message={selectedUsers.length > 0 ? (
|
||||||
<>
|
<>
|
||||||
Alle FDISK-synchronisierten Daten fuer <strong>{selectedUser.name ?? selectedUser.email}</strong> werden geloescht:
|
Alle FDISK-synchronisierten Daten fuer{' '}
|
||||||
|
{selectedUsers.length === 1
|
||||||
|
? <strong>{selectedUsers[0].name ?? selectedUsers[0].email}</strong>
|
||||||
|
: <strong>{selectedUsers.length} Benutzer</strong>
|
||||||
|
}{' '}
|
||||||
|
werden geloescht:
|
||||||
<ul style={{ margin: '8px 0', paddingLeft: 20 }}>
|
<ul style={{ margin: '8px 0', paddingLeft: 20 }}>
|
||||||
<li>Profilfelder (Status, Dienstgrad, Geburtsdatum, Geburtsort, Geschlecht, Beruf, Wohnort, PLZ)</li>
|
<li>Profilfelder (Status, Dienstgrad, Geburtsdatum, Geburtsort, Geschlecht, Beruf, Wohnort, PLZ)</li>
|
||||||
<li>Alle Ausbildungen, Befoerderungen, Untersuchungen und Fahrgenehmigungen</li>
|
<li>Alle Ausbildungen, Befoerderungen, Untersuchungen und Fahrgenehmigungen</li>
|
||||||
|
|||||||
@@ -224,6 +224,7 @@ function MitgliedDetail() {
|
|||||||
const [saveError, setSaveError] = useState<string | null>(null);
|
const [saveError, setSaveError] = useState<string | null>(null);
|
||||||
const [editMode, setEditMode] = useState(false);
|
const [editMode, setEditMode] = useState(false);
|
||||||
const [activeTab, setActiveTab] = useState(0);
|
const [activeTab, setActiveTab] = useState(0);
|
||||||
|
const [qualSubTab, setQualSubTab] = useState(0);
|
||||||
|
|
||||||
// Atemschutz data for Qualifikationen tab
|
// Atemschutz data for Qualifikationen tab
|
||||||
const [atemschutz, setAtemschutz] = useState<AtemschutzUebersicht | null>(null);
|
const [atemschutz, setAtemschutz] = useState<AtemschutzUebersicht | null>(null);
|
||||||
@@ -579,8 +580,9 @@ function MitgliedDetail() {
|
|||||||
scrollButtons="auto"
|
scrollButtons="auto"
|
||||||
>
|
>
|
||||||
<Tab label="Stammdaten" id="tab-0" aria-controls="tabpanel-0" />
|
<Tab label="Stammdaten" id="tab-0" aria-controls="tabpanel-0" />
|
||||||
<Tab label="Qualifikationen" id="tab-1" aria-controls="tabpanel-1" />
|
<Tab label="Ausrüstung & Uniform" id="tab-1" aria-controls="tabpanel-1" />
|
||||||
<Tab label="Einsätze" id="tab-2" aria-controls="tabpanel-2" />
|
<Tab label="Qualifikationen" id="tab-2" aria-controls="tabpanel-2" />
|
||||||
|
<Tab label="Einsätze" id="tab-3" aria-controls="tabpanel-3" />
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
@@ -816,98 +818,6 @@ function MitgliedDetail() {
|
|||||||
</Card>
|
</Card>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
{/* Uniform sizing + Personal equipment (merged) */}
|
|
||||||
<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} />
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{(hasPermission('persoenliche_ausruestung:view') || hasPermission('persoenliche_ausruestung:view_all')) && (
|
|
||||||
<>
|
|
||||||
<Divider sx={{ my: 1.5 }} />
|
|
||||||
<Typography variant="caption" color="text.secondary" sx={{ display: 'block', mb: 1 }}>
|
|
||||||
Persönliche Ausrüstung
|
|
||||||
</Typography>
|
|
||||||
{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={ZUSTAND_LABELS[item.zustand]}
|
|
||||||
color={ZUSTAND_COLORS[item.zustand]}
|
|
||||||
size="small"
|
|
||||||
variant="outlined"
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</Grid>
|
|
||||||
|
|
||||||
{/* Driving licenses */}
|
{/* Driving licenses */}
|
||||||
<Grid item xs={12} md={6}>
|
<Grid item xs={12} md={6}>
|
||||||
<Card>
|
<Card>
|
||||||
@@ -981,281 +891,401 @@ function MitgliedDetail() {
|
|||||||
</Grid>
|
</Grid>
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
|
|
||||||
{/* ---- Tab 1: Qualifikationen ---- */}
|
{/* ---- Tab 1: Ausrüstung & Uniform ---- */}
|
||||||
<TabPanel value={activeTab} index={1}>
|
<TabPanel value={activeTab} index={1}>
|
||||||
{atemschutzLoading ? (
|
<Grid container spacing={3}>
|
||||||
<Box sx={{ display: 'flex', justifyContent: 'center', pt: 6 }}>
|
<Grid item xs={12} md={6}>
|
||||||
<CircularProgress />
|
<Card>
|
||||||
</Box>
|
<CardHeader
|
||||||
) : atemschutz ? (
|
avatar={<SecurityIcon color="primary" />}
|
||||||
<Grid container spacing={3}>
|
title="Ausrüstung & Uniform"
|
||||||
<Grid item xs={12} md={6}>
|
/>
|
||||||
|
<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>
|
<Card>
|
||||||
<CardHeader
|
<CardHeader
|
||||||
avatar={<SecurityIcon color="primary" />}
|
avatar={<SecurityIcon color="primary" />}
|
||||||
title="Atemschutz"
|
title="Persönliche Ausrüstung"
|
||||||
action={
|
|
||||||
<Chip
|
|
||||||
icon={atemschutz.einsatzbereit ? <CheckCircleIcon /> : <HighlightOffIcon />}
|
|
||||||
label={atemschutz.einsatzbereit ? 'Einsatzbereit' : 'Nicht einsatzbereit'}
|
|
||||||
color={atemschutz.einsatzbereit ? 'success' : 'error'}
|
|
||||||
size="small"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<FieldRow
|
{personalEquipmentLoading ? (
|
||||||
label="Lehrgang"
|
<CircularProgress size={24} />
|
||||||
value={
|
) : personalEquipment.length === 0 ? (
|
||||||
atemschutz.atemschutz_lehrgang
|
<Typography color="text.secondary" variant="body2">
|
||||||
? `Ja${atemschutz.lehrgang_datum ? ` (${new Date(atemschutz.lehrgang_datum).toLocaleDateString('de-AT')})` : ''}`
|
Keine persönlichen Gegenstände erfasst
|
||||||
: 'Nein'
|
</Typography>
|
||||||
}
|
) : (
|
||||||
/>
|
personalEquipment.map((item) => (
|
||||||
|
<Box
|
||||||
<Divider sx={{ my: 1.5 }} />
|
key={item.id}
|
||||||
<Typography variant="caption" color="text.secondary" sx={{ display: 'block', mb: 0.5 }}>
|
sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', py: 0.75, borderBottom: '1px solid', borderColor: 'divider', cursor: 'pointer', '&:hover': { bgcolor: 'action.hover' } }}
|
||||||
G26.3 Untersuchung
|
onClick={() => navigate(`/persoenliche-ausruestung/${item.id}`)}
|
||||||
</Typography>
|
>
|
||||||
<FieldRow
|
<Box>
|
||||||
label="Datum"
|
<Typography variant="body2" fontWeight={500}>{item.bezeichnung}</Typography>
|
||||||
value={atemschutz.untersuchung_datum
|
{item.kategorie && (
|
||||||
? new Date(atemschutz.untersuchung_datum).toLocaleDateString('de-AT')
|
<Typography variant="caption" color="text.secondary">{item.kategorie}</Typography>
|
||||||
: null}
|
)}
|
||||||
/>
|
{item.eigenschaften && item.eigenschaften.length > 0 && (
|
||||||
<FieldRow
|
<Box sx={{ display: 'flex', gap: 0.5, flexWrap: 'wrap', mt: 0.25 }}>
|
||||||
label="Gültig bis"
|
{item.eigenschaften.map((e) => (
|
||||||
value={
|
<Chip
|
||||||
<ValidityChip
|
key={e.id}
|
||||||
date={atemschutz.untersuchung_gueltig_bis}
|
label={`${e.name}: ${e.wert}`}
|
||||||
gueltig={atemschutz.untersuchung_gueltig}
|
size="small"
|
||||||
tageRest={atemschutz.untersuchung_tage_rest}
|
variant="outlined"
|
||||||
/>
|
sx={{ height: 18, fontSize: '0.65rem' }}
|
||||||
}
|
/>
|
||||||
/>
|
))}
|
||||||
<FieldRow
|
</Box>
|
||||||
label="Ergebnis"
|
)}
|
||||||
value={
|
</Box>
|
||||||
atemschutz.untersuchung_ergebnis
|
<Chip
|
||||||
? UntersuchungErgebnisLabel[atemschutz.untersuchung_ergebnis]
|
label={ZUSTAND_LABELS[item.zustand]}
|
||||||
: null
|
color={ZUSTAND_COLORS[item.zustand]}
|
||||||
}
|
size="small"
|
||||||
/>
|
variant="outlined"
|
||||||
|
/>
|
||||||
<Divider sx={{ my: 1.5 }} />
|
</Box>
|
||||||
<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>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
|
||||||
) : (
|
|
||||||
<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>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* FDISK-synced sub-sections */}
|
|
||||||
<Grid container spacing={3} sx={{ mt: 0 }}>
|
|
||||||
{/* Beförderungen */}
|
|
||||||
{befoerderungen.length > 0 && (
|
|
||||||
<Grid item xs={12} md={6}>
|
|
||||||
<Card>
|
|
||||||
<CardHeader
|
|
||||||
avatar={<MilitaryTechIcon color="primary" />}
|
|
||||||
title="Beförderungen"
|
|
||||||
/>
|
|
||||||
<CardContent>
|
|
||||||
{befoerderungen.map((b) => (
|
|
||||||
<FieldRow
|
|
||||||
key={b.id}
|
|
||||||
label={b.datum ? new Date(b.datum).toLocaleDateString('de-AT') : '—'}
|
|
||||||
value={b.dienstgrad}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</Grid>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Ausbildungen */}
|
|
||||||
{ausbildungen.length > 0 && (
|
|
||||||
<Grid item xs={12} md={6}>
|
|
||||||
<Card>
|
|
||||||
<CardHeader
|
|
||||||
avatar={<SchoolIcon color="primary" />}
|
|
||||||
title="Ausbildungen"
|
|
||||||
/>
|
|
||||||
<CardContent>
|
|
||||||
{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>
|
|
||||||
</Grid>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Untersuchungen */}
|
|
||||||
{untersuchungen.length > 0 && (
|
|
||||||
<Grid item xs={12} md={6}>
|
|
||||||
<Card>
|
|
||||||
<CardHeader
|
|
||||||
avatar={<LocalHospitalIcon color="primary" />}
|
|
||||||
title="Untersuchungen"
|
|
||||||
/>
|
|
||||||
<CardContent>
|
|
||||||
{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>
|
|
||||||
</Grid>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Fahrgenehmigungen */}
|
|
||||||
{fahrgenehmigungen.length > 0 && (
|
|
||||||
<Grid item xs={12} md={6}>
|
|
||||||
<Card>
|
|
||||||
<CardHeader
|
|
||||||
avatar={<DriveEtaIcon color="primary" />}
|
|
||||||
title="Gesetzliche Fahrgenehmigungen"
|
|
||||||
/>
|
|
||||||
<CardContent>
|
|
||||||
{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>
|
|
||||||
</Grid>
|
|
||||||
)}
|
)}
|
||||||
</Grid>
|
</Grid>
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
|
|
||||||
{/* ---- Tab 2: Einsätze (placeholder) ---- */}
|
{/* ---- Tab 2: Qualifikationen ---- */}
|
||||||
<TabPanel value={activeTab} index={2}>
|
<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>
|
<Card>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<Box sx={{ display: 'flex', flexDirection: 'column', alignItems: 'center', py: 6, gap: 2 }}>
|
<Box sx={{ display: 'flex', flexDirection: 'column', alignItems: 'center', py: 6, gap: 2 }}>
|
||||||
|
|||||||
Reference in New Issue
Block a user