diff --git a/backend/src/controllers/member.controller.ts b/backend/src/controllers/member.controller.ts index c91c9db..de8821f 100644 --- a/backend/src/controllers/member.controller.ts +++ b/backend/src/controllers/member.controller.ts @@ -230,6 +230,47 @@ class MemberController { res.status(500).json({ success: false, message: 'Fehler beim Aktualisieren des Profils.' }); } } + /** + * GET /api/members/:userId/befoerderungen + */ + async getBefoerderungen(req: Request, res: Response): Promise { + try { + const { userId } = req.params; + const data = await memberService.getBefoerderungen(userId); + res.status(200).json({ success: true, data }); + } catch (error) { + logger.error('getBefoerderungen error', { error, userId: req.params.userId }); + res.status(500).json({ success: false, message: 'Fehler beim Laden der Beförderungen.' }); + } + } + + /** + * GET /api/members/:userId/untersuchungen + */ + async getUntersuchungen(req: Request, res: Response): Promise { + try { + const { userId } = req.params; + const data = await memberService.getUntersuchungen(userId); + res.status(200).json({ success: true, data }); + } catch (error) { + logger.error('getUntersuchungen error', { error, userId: req.params.userId }); + res.status(500).json({ success: false, message: 'Fehler beim Laden der Untersuchungen.' }); + } + } + + /** + * GET /api/members/:userId/fahrgenehmigungen + */ + async getFahrgenehmigungen(req: Request, res: Response): Promise { + try { + const { userId } = req.params; + const data = await memberService.getFahrgenehmigungen(userId); + res.status(200).json({ success: true, data }); + } catch (error) { + logger.error('getFahrgenehmigungen error', { error, userId: req.params.userId }); + res.status(500).json({ success: false, message: 'Fehler beim Laden der Fahrgenehmigungen.' }); + } + } } export default new MemberController(); diff --git a/backend/src/routes/member.routes.ts b/backend/src/routes/member.routes.ts index acd2045..6b0a6fc 100644 --- a/backend/src/routes/member.routes.ts +++ b/backend/src/routes/member.routes.ts @@ -39,6 +39,24 @@ router.post( memberController.createMemberProfile.bind(memberController) ); +router.get( + '/:userId/befoerderungen', + requirePermission('members:read'), + memberController.getBefoerderungen.bind(memberController) +); + +router.get( + '/:userId/untersuchungen', + requirePermission('members:read'), + memberController.getUntersuchungen.bind(memberController) +); + +router.get( + '/:userId/fahrgenehmigungen', + requirePermission('members:read'), + memberController.getFahrgenehmigungen.bind(memberController) +); + /** * Inline middleware for PATCH /:userId. * Enforces that the caller is either the profile owner OR holds members:write. diff --git a/backend/src/services/member.service.ts b/backend/src/services/member.service.ts index 2aed492..2f4483d 100644 --- a/backend/src/services/member.service.ts +++ b/backend/src/services/member.service.ts @@ -626,6 +626,63 @@ class MemberService { throw new Error('Failed to fetch member stats'); } } + + /** + * Returns all Beförderungen for a member, newest first. + */ + async getBefoerderungen(userId: string): Promise { + try { + const result = await pool.query( + `SELECT id, datum, dienstgrad, created_at + FROM befoerderungen + WHERE user_id = $1 + ORDER BY datum DESC NULLS LAST, created_at DESC`, + [userId] + ); + return result.rows; + } catch (error) { + logger.error('Error fetching Beförderungen', { error, userId }); + return []; + } + } + + /** + * Returns all Untersuchungen for a member, newest first. + */ + async getUntersuchungen(userId: string): Promise { + try { + const result = await pool.query( + `SELECT id, datum, anmerkungen, art, ergebnis, created_at + FROM untersuchungen + WHERE user_id = $1 + ORDER BY datum DESC NULLS LAST, created_at DESC`, + [userId] + ); + return result.rows; + } catch (error) { + logger.error('Error fetching Untersuchungen', { error, userId }); + return []; + } + } + + /** + * Returns all Fahrgenehmigungen for a member, newest first. + */ + async getFahrgenehmigungen(userId: string): Promise { + try { + const result = await pool.query( + `SELECT id, ausstellungsdatum, gueltig_bis, behoerde, nummer, klasse, created_at + FROM fahrgenehmigungen + WHERE user_id = $1 + ORDER BY ausstellungsdatum DESC NULLS LAST, created_at DESC`, + [userId] + ); + return result.rows; + } catch (error) { + logger.error('Error fetching Fahrgenehmigungen', { error, userId }); + return []; + } + } } export default new MemberService(); diff --git a/frontend/src/pages/MitgliedDetail.tsx b/frontend/src/pages/MitgliedDetail.tsx index 5759c28..ba4ad94 100644 --- a/frontend/src/pages/MitgliedDetail.tsx +++ b/frontend/src/pages/MitgliedDetail.tsx @@ -33,6 +33,8 @@ import { DriveEta as DriveEtaIcon, CheckCircle as CheckCircleIcon, HighlightOff as HighlightOffIcon, + MilitaryTech as MilitaryTechIcon, + LocalHospital as LocalHospitalIcon, } from '@mui/icons-material'; import { useParams, useNavigate } from 'react-router-dom'; import DashboardLayout from '../components/dashboard/DashboardLayout'; @@ -56,6 +58,7 @@ import { formatPhone, UpdateMemberProfileData, } from '../types/member.types'; +import type { Befoerderung, Untersuchung, Fahrgenehmigung } from '../types/member.types'; import type { AtemschutzUebersicht } from '../types/atemschutz.types'; import { UntersuchungErgebnisLabel } from '../types/atemschutz.types'; @@ -237,6 +240,11 @@ function MitgliedDetail() { const [atemschutz, setAtemschutz] = useState(null); const [atemschutzLoading, setAtemschutzLoading] = useState(false); + // FDISK-synced sub-section data + const [befoerderungen, setBefoerderungen] = useState([]); + const [untersuchungen, setUntersuchungen] = useState([]); + const [fahrgenehmigungen, setFahrgenehmigungen] = useState([]); + // Edit form state — only the fields the user is allowed to change const [formData, setFormData] = useState({}); @@ -271,6 +279,14 @@ function MitgliedDetail() { .finally(() => setAtemschutzLoading(false)); }, [userId]); + // Load FDISK-synced sub-section data + useEffect(() => { + if (!userId) return; + membersService.getBefoerderungen(userId).then(setBefoerderungen).catch(() => setBefoerderungen([])); + membersService.getUntersuchungen(userId).then(setUntersuchungen).catch(() => setUntersuchungen([])); + membersService.getFahrgenehmigungen(userId).then(setFahrgenehmigungen).catch(() => setFahrgenehmigungen([])); + }, [userId]); + // Populate form from current profile useEffect(() => { if (member?.profile) { @@ -1025,6 +1041,102 @@ function MitgliedDetail() { )} + + {/* FDISK-synced sub-sections */} + + {/* Beförderungen */} + {befoerderungen.length > 0 && ( + + + } + title="Beförderungen" + /> + + {befoerderungen.map((b) => ( + + ))} + + + + )} + + {/* Untersuchungen */} + {untersuchungen.length > 0 && ( + + + } + title="Untersuchungen" + /> + + {untersuchungen.map((u) => ( + + + + {u.art} + + + {u.datum ? new Date(u.datum).toLocaleDateString('de-AT') : '—'} + + + {u.ergebnis && ( + + {u.ergebnis} + + )} + {u.anmerkungen && ( + + {u.anmerkungen} + + )} + + ))} + + + + )} + + {/* Fahrgenehmigungen */} + {fahrgenehmigungen.length > 0 && ( + + + } + title="Gesetzliche Fahrgenehmigungen" + /> + + {fahrgenehmigungen.map((f) => ( + + + + {f.klasse} + + + {f.ausstellungsdatum ? new Date(f.ausstellungsdatum).toLocaleDateString('de-AT') : '—'} + + + {(f.behoerde || f.nummer) && ( + + {[f.behoerde, f.nummer].filter(Boolean).join(' · ')} + + )} + {f.gueltig_bis && ( + + Gültig bis: {new Date(f.gueltig_bis).toLocaleDateString('de-AT')} + + )} + + ))} + + + + )} + {/* ---- Tab 2: Einsätze (placeholder) ---- */} diff --git a/frontend/src/services/members.ts b/frontend/src/services/members.ts index 0092127..0a6724f 100644 --- a/frontend/src/services/members.ts +++ b/frontend/src/services/members.ts @@ -6,6 +6,9 @@ import { MemberStats, CreateMemberProfileData, UpdateMemberProfileData, + Befoerderung, + Untersuchung, + Fahrgenehmigung, } from '../types/member.types'; // ---------------------------------------------------------------- @@ -125,4 +128,19 @@ export const membersService = { } return response.data.data; }, + + async getBefoerderungen(userId: string): Promise { + const response = await api.get>(`/api/members/${userId}/befoerderungen`); + return response.data?.data ?? []; + }, + + async getUntersuchungen(userId: string): Promise { + const response = await api.get>(`/api/members/${userId}/untersuchungen`); + return response.data?.data ?? []; + }, + + async getFahrgenehmigungen(userId: string): Promise { + const response = await api.get>(`/api/members/${userId}/fahrgenehmigungen`); + return response.data?.data ?? []; + }, }; diff --git a/frontend/src/types/member.types.ts b/frontend/src/types/member.types.ts index f025b8f..d19f6db 100644 --- a/frontend/src/types/member.types.ts +++ b/frontend/src/types/member.types.ts @@ -198,3 +198,33 @@ export const STATUS_COLORS: Record