323 lines
11 KiB
TypeScript
323 lines
11 KiB
TypeScript
import { useState, useEffect, useMemo } from 'react';
|
|
import {
|
|
Autocomplete,
|
|
Box,
|
|
Button,
|
|
Container,
|
|
IconButton,
|
|
LinearProgress,
|
|
MenuItem,
|
|
Stack,
|
|
TextField,
|
|
Typography,
|
|
} from '@mui/material';
|
|
import { Add as AddIcon, Delete as DeleteIcon } from '@mui/icons-material';
|
|
import { useParams, useNavigate } from 'react-router-dom';
|
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
|
import DashboardLayout from '../components/dashboard/DashboardLayout';
|
|
import { personalEquipmentApi } from '../services/personalEquipment';
|
|
import { ausruestungsanfrageApi } from '../services/ausruestungsanfrage';
|
|
import { membersService } from '../services/members';
|
|
import { usePermissionContext } from '../contexts/PermissionContext';
|
|
import { useNotification } from '../contexts/NotificationContext';
|
|
import { PageHeader } from '../components/templates';
|
|
import type {
|
|
ZustandOption,
|
|
UpdatePersoenlicheAusruestungPayload,
|
|
} from '../types/personalEquipment.types';
|
|
|
|
interface EigenschaftRow {
|
|
id?: number;
|
|
eigenschaft_id?: number | null;
|
|
name: string;
|
|
wert: string;
|
|
}
|
|
|
|
export default function PersoenlicheAusruestungEdit() {
|
|
const { id } = useParams<{ id: string }>();
|
|
const navigate = useNavigate();
|
|
const queryClient = useQueryClient();
|
|
const { hasPermission } = usePermissionContext();
|
|
const { showSuccess, showError } = useNotification();
|
|
|
|
const canViewAll = hasPermission('persoenliche_ausruestung:view_all');
|
|
|
|
const { data: item, isLoading, isError } = useQuery({
|
|
queryKey: ['persoenliche-ausruestung', 'detail', id],
|
|
queryFn: () => personalEquipmentApi.getById(id!),
|
|
enabled: !!id,
|
|
});
|
|
|
|
const { data: membersList } = useQuery({
|
|
queryKey: ['members-list-compact'],
|
|
queryFn: () => membersService.getMembers({ pageSize: 500 }),
|
|
staleTime: 5 * 60 * 1000,
|
|
enabled: canViewAll,
|
|
});
|
|
|
|
const memberOptions = useMemo(() => {
|
|
return (membersList?.items ?? []).map((m) => ({
|
|
id: m.id,
|
|
name: [m.given_name, m.family_name].filter(Boolean).join(' ') || m.email,
|
|
}));
|
|
}, [membersList]);
|
|
|
|
const { data: artikelEigenschaften = [] } = useQuery({
|
|
queryKey: ['ausruestungsanfrage', 'eigenschaften', item?.artikel_id],
|
|
queryFn: () => ausruestungsanfrageApi.getArtikelEigenschaften(item!.artikel_id!),
|
|
enabled: !!item?.artikel_id,
|
|
staleTime: 5 * 60 * 1000,
|
|
});
|
|
|
|
const { data: zustandOptions = [] } = useQuery<ZustandOption[]>({
|
|
queryKey: ['persoenliche-ausruestung', 'zustand-options'],
|
|
queryFn: () => personalEquipmentApi.getZustandOptions(),
|
|
staleTime: 5 * 60 * 1000,
|
|
});
|
|
|
|
const [catalogEigenschaftValues, setCatalogEigenschaftValues] = useState<Record<number, string>>({});
|
|
|
|
// Form state
|
|
const [bezeichnung, setBezeichnung] = useState('');
|
|
const [seriennummer, setSeriennummer] = useState('');
|
|
const [inventarnummer, setInventarnummer] = useState('');
|
|
const [zustand, setZustand] = useState('gut');
|
|
const [notizen, setNotizen] = useState('');
|
|
const [userId, setUserId] = useState<{ id: string; name: string } | null>(null);
|
|
const [eigenschaften, setEigenschaften] = useState<EigenschaftRow[]>([]);
|
|
|
|
// Initialize form from loaded item
|
|
useEffect(() => {
|
|
if (!item) return;
|
|
setBezeichnung(item.bezeichnung);
|
|
setSeriennummer(item.seriennummer ?? '');
|
|
setInventarnummer(item.inventarnummer ?? '');
|
|
setZustand(item.zustand);
|
|
setNotizen(item.notizen ?? '');
|
|
if (item.eigenschaften) {
|
|
setEigenschaften(item.eigenschaften.map(e => ({
|
|
id: e.id,
|
|
eigenschaft_id: e.eigenschaft_id,
|
|
name: e.name,
|
|
wert: e.wert,
|
|
})));
|
|
}
|
|
if (item.artikel_id && item.eigenschaften) {
|
|
const vals: Record<number, string> = {};
|
|
item.eigenschaften.forEach(e => {
|
|
if (e.eigenschaft_id != null) vals[e.eigenschaft_id] = e.wert;
|
|
});
|
|
setCatalogEigenschaftValues(vals);
|
|
}
|
|
}, [item]);
|
|
|
|
// Set userId when item + memberOptions are ready
|
|
useEffect(() => {
|
|
if (!item?.user_id || memberOptions.length === 0) return;
|
|
const match = memberOptions.find(m => m.id === item.user_id);
|
|
if (match) setUserId(match);
|
|
}, [item, memberOptions]);
|
|
|
|
const updateMutation = useMutation({
|
|
mutationFn: (data: UpdatePersoenlicheAusruestungPayload) => personalEquipmentApi.update(id!, data),
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: ['persoenliche-ausruestung'] });
|
|
showSuccess('Persönliche Ausrüstung aktualisiert');
|
|
navigate(`/persoenliche-ausruestung/${id}`);
|
|
},
|
|
onError: () => {
|
|
showError('Fehler beim Speichern');
|
|
},
|
|
});
|
|
|
|
const handleSave = () => {
|
|
if (!bezeichnung.trim() || !item) return;
|
|
|
|
const payload: UpdatePersoenlicheAusruestungPayload = {
|
|
bezeichnung: bezeichnung.trim(),
|
|
kategorie: null,
|
|
user_id: userId?.id || null,
|
|
groesse: null,
|
|
seriennummer: seriennummer || null,
|
|
inventarnummer: inventarnummer || null,
|
|
zustand,
|
|
notizen: notizen || null,
|
|
eigenschaften: item.artikel_id
|
|
? Object.entries(catalogEigenschaftValues)
|
|
.filter(([, v]) => v.trim())
|
|
.map(([id, wert]) => ({
|
|
eigenschaft_id: Number(id),
|
|
name: artikelEigenschaften.find(e => e.id === Number(id))?.name ?? '',
|
|
wert,
|
|
}))
|
|
: eigenschaften.filter(e => e.name.trim() && e.wert.trim()).map(e => ({ eigenschaft_id: e.eigenschaft_id, name: e.name, wert: e.wert })),
|
|
};
|
|
updateMutation.mutate(payload);
|
|
};
|
|
|
|
const addEigenschaft = () => {
|
|
setEigenschaften(prev => [...prev, { name: '', wert: '' }]);
|
|
};
|
|
|
|
const updateEigenschaft = (idx: number, field: 'name' | 'wert', value: string) => {
|
|
setEigenschaften(prev => prev.map((e, i) => i === idx ? { ...e, [field]: value } : e));
|
|
};
|
|
|
|
const removeEigenschaft = (idx: number) => {
|
|
setEigenschaften(prev => prev.filter((_, i) => i !== idx));
|
|
};
|
|
|
|
if (isLoading) {
|
|
return (
|
|
<DashboardLayout>
|
|
<Container maxWidth="sm">
|
|
<LinearProgress />
|
|
</Container>
|
|
</DashboardLayout>
|
|
);
|
|
}
|
|
|
|
if (isError || !item) {
|
|
return (
|
|
<DashboardLayout>
|
|
<Container maxWidth="sm">
|
|
<Typography color="error">Fehler beim Laden.</Typography>
|
|
</Container>
|
|
</DashboardLayout>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<DashboardLayout>
|
|
<Container maxWidth="sm">
|
|
<PageHeader
|
|
title="Ausrüstung bearbeiten"
|
|
backTo={`/persoenliche-ausruestung/${id}`}
|
|
/>
|
|
|
|
<Stack spacing={2}>
|
|
<TextField
|
|
label="Bezeichnung"
|
|
required
|
|
size="small"
|
|
value={bezeichnung}
|
|
onChange={(e) => setBezeichnung(e.target.value)}
|
|
/>
|
|
|
|
{canViewAll && (
|
|
<Autocomplete
|
|
options={memberOptions}
|
|
getOptionLabel={(o) => o.name}
|
|
value={userId}
|
|
onChange={(_e, v) => setUserId(v)}
|
|
renderInput={(params) => (
|
|
<TextField {...params} label="Benutzer" size="small" />
|
|
)}
|
|
size="small"
|
|
/>
|
|
)}
|
|
|
|
<TextField
|
|
label="Seriennummer"
|
|
size="small"
|
|
value={seriennummer}
|
|
onChange={(e) => setSeriennummer(e.target.value)}
|
|
/>
|
|
|
|
<TextField
|
|
label="Inventarnummer"
|
|
size="small"
|
|
value={inventarnummer}
|
|
onChange={(e) => setInventarnummer(e.target.value)}
|
|
/>
|
|
|
|
<TextField
|
|
label="Zustand"
|
|
select
|
|
size="small"
|
|
value={zustand}
|
|
onChange={(e) => setZustand(e.target.value)}
|
|
>
|
|
{zustandOptions.map((opt) => (
|
|
<MenuItem key={opt.key} value={opt.key}>{opt.label}</MenuItem>
|
|
))}
|
|
</TextField>
|
|
|
|
<TextField
|
|
label="Notizen"
|
|
size="small"
|
|
multiline
|
|
rows={2}
|
|
value={notizen}
|
|
onChange={(e) => setNotizen(e.target.value)}
|
|
/>
|
|
|
|
{/* Eigenschaften */}
|
|
{item.artikel_id ? (
|
|
<>
|
|
<Typography variant="subtitle2">Eigenschaften</Typography>
|
|
{artikelEigenschaften.map(e =>
|
|
e.typ === 'options' && e.optionen?.length ? (
|
|
<TextField key={e.id} select size="small" label={e.name} required={e.pflicht}
|
|
value={catalogEigenschaftValues[e.id] ?? ''}
|
|
onChange={ev => setCatalogEigenschaftValues(prev => ({ ...prev, [e.id]: ev.target.value }))}
|
|
>
|
|
<MenuItem value="">—</MenuItem>
|
|
{e.optionen.map(opt => <MenuItem key={opt} value={opt}>{opt}</MenuItem>)}
|
|
</TextField>
|
|
) : (
|
|
<TextField key={e.id} size="small" label={e.name} required={e.pflicht}
|
|
value={catalogEigenschaftValues[e.id] ?? ''}
|
|
onChange={ev => setCatalogEigenschaftValues(prev => ({ ...prev, [e.id]: ev.target.value }))}
|
|
/>
|
|
)
|
|
)}
|
|
</>
|
|
) : (
|
|
<>
|
|
<Typography variant="subtitle2">Eigenschaften</Typography>
|
|
{eigenschaften.map((e, idx) => (
|
|
<Box key={idx} sx={{ display: 'flex', gap: 1, alignItems: 'center' }}>
|
|
<TextField
|
|
size="small"
|
|
label="Name"
|
|
value={e.name}
|
|
onChange={(ev) => updateEigenschaft(idx, 'name', ev.target.value)}
|
|
sx={{ flex: 1 }}
|
|
/>
|
|
<TextField
|
|
size="small"
|
|
label="Wert"
|
|
value={e.wert}
|
|
onChange={(ev) => updateEigenschaft(idx, 'wert', ev.target.value)}
|
|
sx={{ flex: 1 }}
|
|
/>
|
|
<IconButton size="small" onClick={() => removeEigenschaft(idx)}>
|
|
<DeleteIcon fontSize="small" />
|
|
</IconButton>
|
|
</Box>
|
|
))}
|
|
<Button size="small" startIcon={<AddIcon />} onClick={addEigenschaft}>
|
|
Eigenschaft hinzufügen
|
|
</Button>
|
|
</>
|
|
)}
|
|
|
|
<Box sx={{ display: 'flex', gap: 1, justifyContent: 'flex-end', mt: 1 }}>
|
|
<Button onClick={() => navigate(`/persoenliche-ausruestung/${id}`)}>
|
|
Abbrechen
|
|
</Button>
|
|
<Button
|
|
variant="contained"
|
|
onClick={handleSave}
|
|
disabled={updateMutation.isPending || !bezeichnung.trim()}
|
|
>
|
|
Speichern
|
|
</Button>
|
|
</Box>
|
|
</Stack>
|
|
</Container>
|
|
</DashboardLayout>
|
|
);
|
|
}
|