update
This commit is contained in:
@@ -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) ---- */}
|
||||
|
||||
Reference in New Issue
Block a user