feat: personal equipment tracking, order assignment, purge fix, widget consolidation
- Migration 084: new persoenliche_ausruestung table with catalog link, user assignment, soft delete; adds zuweisung_typ/ausruestung_id/persoenlich_id columns to ausruestung_anfrage_positionen; seeds feature group + 5 permissions - Fix user data purge: table was shop_anfragen, renamed to ausruestung_anfragen in mig 046 — caused full transaction rollback. Also keep mitglieder_profile row but NULL FDISK-synced fields (dienstgrad, geburtsdatum, etc.) instead of deleting the profile - Personal equipment CRUD: backend service/controller/routes at /api/persoenliche-ausruestung; frontend page with DataTable, user filter, catalog Autocomplete, FAB create dialog; widget in Status group; sidebar entry (Checkroom icon); card in MitgliedDetail Tab 0 - Ausruestungsanfrage item assignment: when a request reaches erledigt, auto-opens ItemAssignmentDialog listing all delivered positions; each item can be assigned as general equipment (vehicle/storage), personal item (user, prefilled with requester), or not tracked; POST /requests/:id/assign backend - StatCard refactored to use WidgetCard as outer shell for consistent header styling across all dashboard widget templates Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -43,6 +43,9 @@ import { useAuth } from '../contexts/AuthContext';
|
||||
import { usePermissionContext } from '../contexts/PermissionContext';
|
||||
import { membersService } from '../services/members';
|
||||
import { atemschutzApi } from '../services/atemschutz';
|
||||
import { personalEquipmentApi } from '../services/personalEquipment';
|
||||
import { ZUSTAND_LABELS, ZUSTAND_COLORS } from '../types/personalEquipment.types';
|
||||
import type { PersoenlicheAusruestung } from '../types/personalEquipment.types';
|
||||
import { toGermanDate, fromGermanDate } from '../utils/dateInput';
|
||||
import {
|
||||
MemberWithProfile,
|
||||
@@ -208,6 +211,7 @@ function MitgliedDetail() {
|
||||
const { userId } = useParams<{ userId: string }>();
|
||||
const navigate = useNavigate();
|
||||
const canWrite = useCanWrite();
|
||||
const { hasPermission } = usePermissionContext();
|
||||
const currentUserId = useCurrentUserId();
|
||||
const isOwnProfile = currentUserId === userId;
|
||||
const canEdit = canWrite || isOwnProfile;
|
||||
@@ -225,6 +229,10 @@ function MitgliedDetail() {
|
||||
const [atemschutz, setAtemschutz] = useState<AtemschutzUebersicht | null>(null);
|
||||
const [atemschutzLoading, setAtemschutzLoading] = useState(false);
|
||||
|
||||
// Personal equipment data
|
||||
const [personalEquipment, setPersonalEquipment] = useState<PersoenlicheAusruestung[]>([]);
|
||||
const [personalEquipmentLoading, setPersonalEquipmentLoading] = useState(false);
|
||||
|
||||
// FDISK-synced sub-section data
|
||||
const [befoerderungen, setBefoerderungen] = useState<Befoerderung[]>([]);
|
||||
const [untersuchungen, setUntersuchungen] = useState<Untersuchung[]>([]);
|
||||
@@ -272,6 +280,18 @@ function MitgliedDetail() {
|
||||
.finally(() => setAtemschutzLoading(false));
|
||||
}, [userId]);
|
||||
|
||||
// Load personal equipment for this user
|
||||
useEffect(() => {
|
||||
if (!userId) return;
|
||||
const canView = hasPermission('persoenliche_ausruestung:view') || hasPermission('persoenliche_ausruestung:view_all');
|
||||
if (!canView) return;
|
||||
setPersonalEquipmentLoading(true);
|
||||
personalEquipmentApi.getByUserId(userId)
|
||||
.then(setPersonalEquipment)
|
||||
.catch(() => setPersonalEquipment([]))
|
||||
.finally(() => setPersonalEquipmentLoading(false));
|
||||
}, [userId]);
|
||||
|
||||
// Load FDISK-synced sub-section data
|
||||
useEffect(() => {
|
||||
if (!userId) return;
|
||||
@@ -842,6 +862,44 @@ function MitgliedDetail() {
|
||||
</Card>
|
||||
</Grid>
|
||||
|
||||
{/* Personal equipment */}
|
||||
{(hasPermission('persoenliche_ausruestung:view') || hasPermission('persoenliche_ausruestung:view_all')) && (
|
||||
<Grid item xs={12} md={6}>
|
||||
<Card>
|
||||
<CardHeader
|
||||
avatar={<SecurityIcon color="primary" />}
|
||||
title="Persönliche Ausrüstung"
|
||||
/>
|
||||
<CardContent>
|
||||
{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' }}>
|
||||
<Box>
|
||||
<Typography variant="body2" fontWeight={500}>{item.bezeichnung}</Typography>
|
||||
{item.kategorie && (
|
||||
<Typography variant="caption" color="text.secondary">{item.kategorie}</Typography>
|
||||
)}
|
||||
</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>
|
||||
|
||||
Reference in New Issue
Block a user