update
This commit is contained in:
@@ -230,6 +230,47 @@ class MemberController {
|
|||||||
res.status(500).json({ success: false, message: 'Fehler beim Aktualisieren des Profils.' });
|
res.status(500).json({ success: false, message: 'Fehler beim Aktualisieren des Profils.' });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* GET /api/members/:userId/befoerderungen
|
||||||
|
*/
|
||||||
|
async getBefoerderungen(req: Request, res: Response): Promise<void> {
|
||||||
|
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<void> {
|
||||||
|
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<void> {
|
||||||
|
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();
|
export default new MemberController();
|
||||||
|
|||||||
@@ -39,6 +39,24 @@ router.post(
|
|||||||
memberController.createMemberProfile.bind(memberController)
|
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.
|
* Inline middleware for PATCH /:userId.
|
||||||
* Enforces that the caller is either the profile owner OR holds members:write.
|
* Enforces that the caller is either the profile owner OR holds members:write.
|
||||||
|
|||||||
@@ -626,6 +626,63 @@ class MemberService {
|
|||||||
throw new Error('Failed to fetch member stats');
|
throw new Error('Failed to fetch member stats');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns all Beförderungen for a member, newest first.
|
||||||
|
*/
|
||||||
|
async getBefoerderungen(userId: string): Promise<any[]> {
|
||||||
|
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<any[]> {
|
||||||
|
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<any[]> {
|
||||||
|
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();
|
export default new MemberService();
|
||||||
|
|||||||
@@ -33,6 +33,8 @@ import {
|
|||||||
DriveEta as DriveEtaIcon,
|
DriveEta as DriveEtaIcon,
|
||||||
CheckCircle as CheckCircleIcon,
|
CheckCircle as CheckCircleIcon,
|
||||||
HighlightOff as HighlightOffIcon,
|
HighlightOff as HighlightOffIcon,
|
||||||
|
MilitaryTech as MilitaryTechIcon,
|
||||||
|
LocalHospital as LocalHospitalIcon,
|
||||||
} from '@mui/icons-material';
|
} from '@mui/icons-material';
|
||||||
import { useParams, useNavigate } from 'react-router-dom';
|
import { useParams, useNavigate } from 'react-router-dom';
|
||||||
import DashboardLayout from '../components/dashboard/DashboardLayout';
|
import DashboardLayout from '../components/dashboard/DashboardLayout';
|
||||||
@@ -56,6 +58,7 @@ import {
|
|||||||
formatPhone,
|
formatPhone,
|
||||||
UpdateMemberProfileData,
|
UpdateMemberProfileData,
|
||||||
} from '../types/member.types';
|
} from '../types/member.types';
|
||||||
|
import type { Befoerderung, Untersuchung, Fahrgenehmigung } from '../types/member.types';
|
||||||
import type { AtemschutzUebersicht } from '../types/atemschutz.types';
|
import type { AtemschutzUebersicht } from '../types/atemschutz.types';
|
||||||
import { UntersuchungErgebnisLabel } from '../types/atemschutz.types';
|
import { UntersuchungErgebnisLabel } from '../types/atemschutz.types';
|
||||||
|
|
||||||
@@ -237,6 +240,11 @@ function MitgliedDetail() {
|
|||||||
const [atemschutz, setAtemschutz] = useState<AtemschutzUebersicht | null>(null);
|
const [atemschutz, setAtemschutz] = useState<AtemschutzUebersicht | null>(null);
|
||||||
const [atemschutzLoading, setAtemschutzLoading] = useState(false);
|
const [atemschutzLoading, setAtemschutzLoading] = useState(false);
|
||||||
|
|
||||||
|
// FDISK-synced sub-section data
|
||||||
|
const [befoerderungen, setBefoerderungen] = useState<Befoerderung[]>([]);
|
||||||
|
const [untersuchungen, setUntersuchungen] = useState<Untersuchung[]>([]);
|
||||||
|
const [fahrgenehmigungen, setFahrgenehmigungen] = useState<Fahrgenehmigung[]>([]);
|
||||||
|
|
||||||
// Edit form state — only the fields the user is allowed to change
|
// Edit form state — only the fields the user is allowed to change
|
||||||
const [formData, setFormData] = useState<UpdateMemberProfileData>({});
|
const [formData, setFormData] = useState<UpdateMemberProfileData>({});
|
||||||
|
|
||||||
@@ -271,6 +279,14 @@ function MitgliedDetail() {
|
|||||||
.finally(() => setAtemschutzLoading(false));
|
.finally(() => setAtemschutzLoading(false));
|
||||||
}, [userId]);
|
}, [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
|
// Populate form from current profile
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (member?.profile) {
|
if (member?.profile) {
|
||||||
@@ -1025,6 +1041,102 @@ function MitgliedDetail() {
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</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>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 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>
|
</TabPanel>
|
||||||
|
|
||||||
{/* ---- Tab 2: Einsätze (placeholder) ---- */}
|
{/* ---- Tab 2: Einsätze (placeholder) ---- */}
|
||||||
|
|||||||
@@ -6,6 +6,9 @@ import {
|
|||||||
MemberStats,
|
MemberStats,
|
||||||
CreateMemberProfileData,
|
CreateMemberProfileData,
|
||||||
UpdateMemberProfileData,
|
UpdateMemberProfileData,
|
||||||
|
Befoerderung,
|
||||||
|
Untersuchung,
|
||||||
|
Fahrgenehmigung,
|
||||||
} from '../types/member.types';
|
} from '../types/member.types';
|
||||||
|
|
||||||
// ----------------------------------------------------------------
|
// ----------------------------------------------------------------
|
||||||
@@ -125,4 +128,19 @@ export const membersService = {
|
|||||||
}
|
}
|
||||||
return response.data.data;
|
return response.data.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async getBefoerderungen(userId: string): Promise<Befoerderung[]> {
|
||||||
|
const response = await api.get<ApiItemResponse<Befoerderung[]>>(`/api/members/${userId}/befoerderungen`);
|
||||||
|
return response.data?.data ?? [];
|
||||||
|
},
|
||||||
|
|
||||||
|
async getUntersuchungen(userId: string): Promise<Untersuchung[]> {
|
||||||
|
const response = await api.get<ApiItemResponse<Untersuchung[]>>(`/api/members/${userId}/untersuchungen`);
|
||||||
|
return response.data?.data ?? [];
|
||||||
|
},
|
||||||
|
|
||||||
|
async getFahrgenehmigungen(userId: string): Promise<Fahrgenehmigung[]> {
|
||||||
|
const response = await api.get<ApiItemResponse<Fahrgenehmigung[]>>(`/api/members/${userId}/fahrgenehmigungen`);
|
||||||
|
return response.data?.data ?? [];
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -198,3 +198,33 @@ export const STATUS_COLORS: Record<StatusEnum, 'success' | 'warning' | 'error' |
|
|||||||
anwärter: 'default',
|
anwärter: 'default',
|
||||||
ausgetreten: 'error',
|
ausgetreten: 'error',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
// FDISK-synced sub-section types
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
|
||||||
|
export interface Befoerderung {
|
||||||
|
id: string;
|
||||||
|
datum: string | null;
|
||||||
|
dienstgrad: string;
|
||||||
|
created_at: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Untersuchung {
|
||||||
|
id: string;
|
||||||
|
datum: string | null;
|
||||||
|
anmerkungen: string | null;
|
||||||
|
art: string;
|
||||||
|
ergebnis: string | null;
|
||||||
|
created_at: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Fahrgenehmigung {
|
||||||
|
id: string;
|
||||||
|
ausstellungsdatum: string | null;
|
||||||
|
gueltig_bis: string | null;
|
||||||
|
behoerde: string | null;
|
||||||
|
nummer: string | null;
|
||||||
|
klasse: string;
|
||||||
|
created_at: string;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user