refactor(mitglieder): split member profile into Stammdaten/Ausrüstung/Qualifikationen tabs with sub-tabs per qualification type
This commit is contained in:
@@ -224,6 +224,7 @@ function MitgliedDetail() {
|
||||
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);
|
||||
@@ -579,8 +580,9 @@ function MitgliedDetail() {
|
||||
scrollButtons="auto"
|
||||
>
|
||||
<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" />
|
||||
<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>
|
||||
|
||||
@@ -816,98 +818,6 @@ function MitgliedDetail() {
|
||||
</Card>
|
||||
</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 */}
|
||||
<Grid item xs={12} md={6}>
|
||||
<Card>
|
||||
@@ -981,281 +891,401 @@ function MitgliedDetail() {
|
||||
</Grid>
|
||||
</TabPanel>
|
||||
|
||||
{/* ---- Tab 1: Qualifikationen ---- */}
|
||||
{/* ---- Tab 1: Ausrüstung & Uniform ---- */}
|
||||
<TabPanel value={activeTab} index={1}>
|
||||
{atemschutzLoading ? (
|
||||
<Box sx={{ display: 'flex', justifyContent: 'center', pt: 6 }}>
|
||||
<CircularProgress />
|
||||
</Box>
|
||||
) : atemschutz ? (
|
||||
<Grid container spacing={3}>
|
||||
<Grid item xs={12} md={6}>
|
||||
<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="Atemschutz"
|
||||
action={
|
||||
<Chip
|
||||
icon={atemschutz.einsatzbereit ? <CheckCircleIcon /> : <HighlightOffIcon />}
|
||||
label={atemschutz.einsatzbereit ? 'Einsatzbereit' : 'Nicht einsatzbereit'}
|
||||
color={atemschutz.einsatzbereit ? 'success' : 'error'}
|
||||
size="small"
|
||||
/>
|
||||
}
|
||||
title="Persönliche Ausrüstung"
|
||||
/>
|
||||
<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} />
|
||||
</>
|
||||
{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>
|
||||
</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>
|
||||
</TabPanel>
|
||||
|
||||
{/* ---- Tab 2: Einsätze (placeholder) ---- */}
|
||||
{/* ---- 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 }}>
|
||||
|
||||
Reference in New Issue
Block a user