This commit is contained in:
Matthias Hochmeister
2026-03-13 15:42:15 +01:00
parent 3dda069611
commit 75c919c063
13 changed files with 926 additions and 30 deletions

View File

@@ -32,11 +32,14 @@ import {
Security as SecurityIcon,
History as HistoryIcon,
DriveEta as DriveEtaIcon,
CheckCircle as CheckCircleIcon,
HighlightOff as HighlightOffIcon,
} 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 { atemschutzApi } from '../services/atemschutz';
import { toGermanDate, fromGermanDate } from '../utils/dateInput';
import {
MemberWithProfile,
@@ -54,6 +57,8 @@ import {
formatPhone,
UpdateMemberProfileData,
} from '../types/member.types';
import type { AtemschutzUebersicht } from '../types/atemschutz.types';
import { UntersuchungErgebnisLabel } from '../types/atemschutz.types';
// ----------------------------------------------------------------
// Role helpers
@@ -188,6 +193,27 @@ function FieldRow({ label, value }: { label: string; value: React.ReactNode }) {
);
}
// ----------------------------------------------------------------
// 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
// ----------------------------------------------------------------
@@ -208,6 +234,10 @@ function MitgliedDetail() {
const [editMode, setEditMode] = useState(false);
const [activeTab, setActiveTab] = useState(0);
// Atemschutz data for Qualifikationen tab
const [atemschutz, setAtemschutz] = useState<AtemschutzUebersicht | null>(null);
const [atemschutzLoading, setAtemschutzLoading] = useState(false);
// Edit form state — only the fields the user is allowed to change
const [formData, setFormData] = useState<UpdateMemberProfileData>({});
@@ -232,6 +262,16 @@ function MitgliedDetail() {
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]);
// Populate form from current profile
useEffect(() => {
if (member?.profile) {
@@ -434,8 +474,8 @@ function MitgliedDetail() {
)}
</Box>
{/* Edit controls */}
{canEdit && (canWrite || !!profile) && (
{/* Edit controls — only shown when profile exists */}
{canEdit && !!profile && (
<Box>
{editMode ? (
<Box sx={{ display: 'flex', gap: 1 }}>
@@ -875,22 +915,128 @@ function MitgliedDetail() {
</Grid>
</TabPanel>
{/* ---- Tab 1: Qualifikationen (placeholder) ---- */}
{/* ---- Tab 1: Qualifikationen ---- */}
<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>
{atemschutzLoading ? (
<Box sx={{ display: 'flex', justifyContent: 'center', pt: 6 }}>
<CircularProgress />
</Box>
) : atemschutz ? (
<Grid container spacing={3}>
<Grid item xs={12} md={6}>
<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>
</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>
)}
</TabPanel>
{/* ---- Tab 2: Einsätze (placeholder) ---- */}