refactor: move equipment type assignment from detail page to settings page

This commit is contained in:
Matthias Hochmeister
2026-03-28 17:37:01 +01:00
parent bccb0745b8
commit a52bb2a57c
2 changed files with 124 additions and 106 deletions

View File

@@ -1,7 +1,6 @@
import React, { useEffect, useState, useCallback } from 'react';
import {
Alert,
Autocomplete,
Box,
Button,
Chip,
@@ -55,8 +54,6 @@ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import DashboardLayout from '../components/dashboard/DashboardLayout';
import ChatAwareFab from '../components/shared/ChatAwareFab';
import { equipmentApi } from '../services/equipment';
import { ausruestungTypenApi } from '../services/ausruestungTypen';
import type { AusruestungTyp } from '../services/ausruestungTypen';
import { fromGermanDate } from '../utils/dateInput';
import {
AusruestungDetail,
@@ -412,109 +409,6 @@ const UebersichtTab: React.FC<UebersichtTabProps> = ({ equipment, onStatusUpdate
</Dialog>
<StatusHistorySection equipmentId={equipment.id} />
<TypenSection equipmentId={equipment.id} canWrite={canWrite} />
</Box>
);
};
// -- Typen Section (equipment type assignment) --------------------------------
interface TypenSectionProps {
equipmentId: string;
canWrite: boolean;
}
const TypenSection: React.FC<TypenSectionProps> = ({ equipmentId, canWrite }) => {
const queryClient = useQueryClient();
const { showSuccess, showError } = useNotification();
const [editing, setEditing] = useState(false);
const [selectedTypen, setSelectedTypen] = useState<AusruestungTyp[]>([]);
const { data: assignedTypen = [] } = useQuery({
queryKey: ['ausruestungTypen', 'equipment', equipmentId],
queryFn: () => ausruestungTypenApi.getTypesForEquipment(equipmentId),
});
const { data: allTypen = [] } = useQuery({
queryKey: ['ausruestungTypen'],
queryFn: ausruestungTypenApi.getAll,
enabled: editing,
});
const saveMutation = useMutation({
mutationFn: (typIds: number[]) =>
ausruestungTypenApi.setTypesForEquipment(equipmentId, typIds),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['ausruestungTypen', 'equipment', equipmentId] });
showSuccess('Typen aktualisiert');
setEditing(false);
},
onError: () => showError('Typen konnten nicht gespeichert werden'),
});
const startEditing = () => {
setSelectedTypen(assignedTypen);
setEditing(true);
};
const handleSave = () => {
saveMutation.mutate(selectedTypen.map((t) => t.id));
};
return (
<Box sx={{ mt: 3 }}>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 1 }}>
<Typography variant="h6">Typen</Typography>
{canWrite && !editing && (
<Button size="small" startIcon={<Edit />} onClick={startEditing}>
Bearbeiten
</Button>
)}
</Box>
{editing ? (
<Box>
<Autocomplete
multiple
options={allTypen}
getOptionLabel={(o) => o.name}
value={selectedTypen}
onChange={(_, newVal) => setSelectedTypen(newVal)}
isOptionEqualToValue={(o, v) => o.id === v.id}
renderTags={(value, getTagProps) =>
value.map((option, index) => (
<Chip label={option.name} size="small" {...getTagProps({ index })} key={option.id} />
))
}
renderInput={(params) => <TextField {...params} label="Typen zuordnen" size="small" />}
sx={{ mb: 1 }}
/>
<Box sx={{ display: 'flex', gap: 1 }}>
<Button
size="small"
variant="contained"
startIcon={saveMutation.isPending ? <CircularProgress size={14} /> : <Save />}
onClick={handleSave}
disabled={saveMutation.isPending}
>
Speichern
</Button>
<Button size="small" onClick={() => setEditing(false)} disabled={saveMutation.isPending}>
Abbrechen
</Button>
</Box>
</Box>
) : (
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 0.5 }}>
{assignedTypen.length === 0 ? (
<Typography variant="body2" color="text.secondary">Keine Typen zugeordnet</Typography>
) : (
assignedTypen.map((t) => (
<Chip key={t.id} label={t.name} size="small" variant="outlined" />
))
)}
</Box>
)}
</Box>
);
};

View File

@@ -1,8 +1,10 @@
import { useState } from 'react';
import {
Alert,
Autocomplete,
Box,
Button,
Chip,
CircularProgress,
Container,
Dialog,
@@ -10,6 +12,7 @@ import {
DialogContent,
DialogContentText,
DialogTitle,
Divider,
IconButton,
Paper,
Table,
@@ -27,6 +30,7 @@ import { useNavigate } from 'react-router-dom';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import DashboardLayout from '../components/dashboard/DashboardLayout';
import { ausruestungTypenApi, AusruestungTyp } from '../services/ausruestungTypen';
import { equipmentApi } from '../services/equipment';
import { usePermissions } from '../hooks/usePermissions';
import { useNotification } from '../contexts/NotificationContext';
@@ -302,9 +306,129 @@ function AusruestungEinstellungen() {
</Button>
</DialogActions>
</Dialog>
<Divider sx={{ my: 4 }} />
<EquipmentTypeAssignment allTypes={typen} />
</Container>
</DashboardLayout>
);
}
export default AusruestungEinstellungen;
// ── Per-equipment type assignment ──────────────────────────────────────────────
function EquipmentTypeAssignment({ allTypes }: { allTypes: AusruestungTyp[] }) {
const { showSuccess, showError } = useNotification();
const { data: equipment = [], isLoading } = useQuery({
queryKey: ['equipment'],
queryFn: equipmentApi.getAll,
});
const [assignDialog, setAssignDialog] = useState<{ equipmentId: string; equipmentName: string } | null>(null);
const [selected, setSelected] = useState<AusruestungTyp[]>([]);
const [saving, setSaving] = useState(false);
const [equipmentTypesMap, setEquipmentTypesMap] = useState<Record<string, AusruestungTyp[]>>({});
const openAssign = async (equipmentId: string, equipmentName: string) => {
let current = equipmentTypesMap[equipmentId];
if (!current) {
try { current = await ausruestungTypenApi.getTypesForEquipment(equipmentId); }
catch { current = []; }
setEquipmentTypesMap((m) => ({ ...m, [equipmentId]: current }));
}
setSelected(current);
setAssignDialog({ equipmentId, equipmentName });
};
const handleSave = async () => {
if (!assignDialog) return;
try {
setSaving(true);
await ausruestungTypenApi.setTypesForEquipment(assignDialog.equipmentId, selected.map((t) => t.id));
setEquipmentTypesMap((m) => ({ ...m, [assignDialog.equipmentId]: selected }));
setAssignDialog(null);
showSuccess('Typen gespeichert');
} catch {
showError('Fehler beim Speichern');
} finally {
setSaving(false);
}
};
return (
<>
<Typography variant="h6" gutterBottom>Typzuweisung je Gerät</Typography>
{isLoading ? (
<CircularProgress size={24} />
) : (
<Paper variant="outlined">
<TableContainer>
<Table size="small">
<TableHead>
<TableRow>
<TableCell>Gerät</TableCell>
<TableCell>Zugewiesene Typen</TableCell>
<TableCell align="right">Aktionen</TableCell>
</TableRow>
</TableHead>
<TableBody>
{equipment.map((e) => {
const types = equipmentTypesMap[e.id];
return (
<TableRow key={e.id} hover>
<TableCell>{e.bezeichnung}</TableCell>
<TableCell>
{types === undefined ? (
<Typography variant="body2" color="text.disabled"></Typography>
) : types.length === 0 ? (
<Typography variant="body2" color="text.disabled">Keine</Typography>
) : (
<Box sx={{ display: 'flex', gap: 0.5, flexWrap: 'wrap' }}>
{types.map((t) => <Chip key={t.id} label={t.name} size="small" variant="outlined" />)}
</Box>
)}
</TableCell>
<TableCell align="right">
<Tooltip title="Typen zuweisen">
<IconButton size="small" onClick={() => openAssign(e.id, e.bezeichnung)}>
<Edit fontSize="small" />
</IconButton>
</Tooltip>
</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
</TableContainer>
</Paper>
)}
<Dialog open={!!assignDialog} onClose={() => setAssignDialog(null)} maxWidth="sm" fullWidth>
<DialogTitle sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
Typen für {assignDialog?.equipmentName}
<IconButton size="small" onClick={() => setAssignDialog(null)}><Close /></IconButton>
</DialogTitle>
<DialogContent sx={{ mt: 1 }}>
<Autocomplete
multiple
options={allTypes}
getOptionLabel={(o) => o.name}
value={selected}
onChange={(_e, val) => setSelected(val)}
isOptionEqualToValue={(a, b) => a.id === b.id}
renderInput={(params) => <TextField {...params} label="Ausrüstungstypen" />}
/>
</DialogContent>
<DialogActions>
<Button onClick={() => setAssignDialog(null)}>Abbrechen</Button>
<Button variant="contained" onClick={handleSave} disabled={saving}
startIcon={saving ? <CircularProgress size={16} /> : <Save />}>
Speichern
</Button>
</DialogActions>
</Dialog>
</>
);
}