refactor: move equipment type assignment from detail page to settings page
This commit is contained in:
@@ -1,7 +1,6 @@
|
|||||||
import React, { useEffect, useState, useCallback } from 'react';
|
import React, { useEffect, useState, useCallback } from 'react';
|
||||||
import {
|
import {
|
||||||
Alert,
|
Alert,
|
||||||
Autocomplete,
|
|
||||||
Box,
|
Box,
|
||||||
Button,
|
Button,
|
||||||
Chip,
|
Chip,
|
||||||
@@ -55,8 +54,6 @@ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
|||||||
import DashboardLayout from '../components/dashboard/DashboardLayout';
|
import DashboardLayout from '../components/dashboard/DashboardLayout';
|
||||||
import ChatAwareFab from '../components/shared/ChatAwareFab';
|
import ChatAwareFab from '../components/shared/ChatAwareFab';
|
||||||
import { equipmentApi } from '../services/equipment';
|
import { equipmentApi } from '../services/equipment';
|
||||||
import { ausruestungTypenApi } from '../services/ausruestungTypen';
|
|
||||||
import type { AusruestungTyp } from '../services/ausruestungTypen';
|
|
||||||
import { fromGermanDate } from '../utils/dateInput';
|
import { fromGermanDate } from '../utils/dateInput';
|
||||||
import {
|
import {
|
||||||
AusruestungDetail,
|
AusruestungDetail,
|
||||||
@@ -412,109 +409,6 @@ const UebersichtTab: React.FC<UebersichtTabProps> = ({ equipment, onStatusUpdate
|
|||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
<StatusHistorySection equipmentId={equipment.id} />
|
<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>
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import {
|
import {
|
||||||
Alert,
|
Alert,
|
||||||
|
Autocomplete,
|
||||||
Box,
|
Box,
|
||||||
Button,
|
Button,
|
||||||
|
Chip,
|
||||||
CircularProgress,
|
CircularProgress,
|
||||||
Container,
|
Container,
|
||||||
Dialog,
|
Dialog,
|
||||||
@@ -10,6 +12,7 @@ import {
|
|||||||
DialogContent,
|
DialogContent,
|
||||||
DialogContentText,
|
DialogContentText,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
|
Divider,
|
||||||
IconButton,
|
IconButton,
|
||||||
Paper,
|
Paper,
|
||||||
Table,
|
Table,
|
||||||
@@ -27,6 +30,7 @@ import { useNavigate } from 'react-router-dom';
|
|||||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
import DashboardLayout from '../components/dashboard/DashboardLayout';
|
import DashboardLayout from '../components/dashboard/DashboardLayout';
|
||||||
import { ausruestungTypenApi, AusruestungTyp } from '../services/ausruestungTypen';
|
import { ausruestungTypenApi, AusruestungTyp } from '../services/ausruestungTypen';
|
||||||
|
import { equipmentApi } from '../services/equipment';
|
||||||
import { usePermissions } from '../hooks/usePermissions';
|
import { usePermissions } from '../hooks/usePermissions';
|
||||||
import { useNotification } from '../contexts/NotificationContext';
|
import { useNotification } from '../contexts/NotificationContext';
|
||||||
|
|
||||||
@@ -302,9 +306,129 @@ function AusruestungEinstellungen() {
|
|||||||
</Button>
|
</Button>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
|
<Divider sx={{ my: 4 }} />
|
||||||
|
<EquipmentTypeAssignment allTypes={typen} />
|
||||||
</Container>
|
</Container>
|
||||||
</DashboardLayout>
|
</DashboardLayout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default AusruestungEinstellungen;
|
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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user