rework internal order system
This commit is contained in:
@@ -93,6 +93,7 @@ function KategorieDialog({ open, onClose }: KategorieDialogProps) {
|
||||
const { showSuccess, showError } = useNotification();
|
||||
const queryClient = useQueryClient();
|
||||
const [newName, setNewName] = useState('');
|
||||
const [newParentId, setNewParentId] = useState<number | null>(null);
|
||||
const [editId, setEditId] = useState<number | null>(null);
|
||||
const [editName, setEditName] = useState('');
|
||||
|
||||
@@ -102,14 +103,17 @@ function KategorieDialog({ open, onClose }: KategorieDialogProps) {
|
||||
enabled: open,
|
||||
});
|
||||
|
||||
const topLevel = useMemo(() => kategorien.filter(k => !k.parent_id), [kategorien]);
|
||||
const childrenOf = useCallback((parentId: number) => kategorien.filter(k => k.parent_id === parentId), [kategorien]);
|
||||
|
||||
const createMut = useMutation({
|
||||
mutationFn: (name: string) => ausruestungsanfrageApi.createKategorie(name),
|
||||
onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['ausruestungsanfrage'] }); showSuccess('Kategorie erstellt'); setNewName(''); },
|
||||
mutationFn: ({ name, parentId }: { name: string; parentId?: number | null }) => ausruestungsanfrageApi.createKategorie(name, parentId),
|
||||
onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['ausruestungsanfrage'] }); showSuccess('Kategorie erstellt'); setNewName(''); setNewParentId(null); },
|
||||
onError: () => showError('Fehler beim Erstellen'),
|
||||
});
|
||||
|
||||
const updateMut = useMutation({
|
||||
mutationFn: ({ id, name }: { id: number; name: string }) => ausruestungsanfrageApi.updateKategorie(id, name),
|
||||
mutationFn: ({ id, name }: { id: number; name: string }) => ausruestungsanfrageApi.updateKategorie(id, { name }),
|
||||
onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['ausruestungsanfrage'] }); showSuccess('Kategorie aktualisiert'); setEditId(null); },
|
||||
onError: () => showError('Fehler beim Aktualisieren'),
|
||||
});
|
||||
@@ -120,55 +124,75 @@ function KategorieDialog({ open, onClose }: KategorieDialogProps) {
|
||||
onError: () => showError('Fehler beim Löschen'),
|
||||
});
|
||||
|
||||
const renderKategorie = (k: { id: number; name: string; parent_id?: number | null }, indent: number) => {
|
||||
const children = childrenOf(k.id);
|
||||
return (
|
||||
<Box key={k.id}>
|
||||
<Box sx={{ display: 'flex', gap: 1, alignItems: 'center', ml: indent * 3 }}>
|
||||
{editId === k.id ? (
|
||||
<>
|
||||
<TextField
|
||||
size="small"
|
||||
value={editName}
|
||||
onChange={e => setEditName(e.target.value)}
|
||||
sx={{ flexGrow: 1 }}
|
||||
onKeyDown={e => { if (e.key === 'Enter' && editName.trim()) updateMut.mutate({ id: k.id, name: editName.trim() }); }}
|
||||
/>
|
||||
<IconButton size="small" onClick={() => { if (editName.trim()) updateMut.mutate({ id: k.id, name: editName.trim() }); }}><CheckIcon fontSize="small" /></IconButton>
|
||||
<IconButton size="small" onClick={() => setEditId(null)}><CloseIcon fontSize="small" /></IconButton>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Typography variant={indent === 0 ? 'body1' : 'body2'} sx={{ flexGrow: 1, fontWeight: indent === 0 ? 600 : 400 }}>
|
||||
{indent > 0 && '└ '}{k.name}
|
||||
</Typography>
|
||||
<Tooltip title="Unterkategorie hinzufügen">
|
||||
<IconButton size="small" onClick={() => setNewParentId(k.id)}><AddIcon fontSize="small" /></IconButton>
|
||||
</Tooltip>
|
||||
<IconButton size="small" onClick={() => { setEditId(k.id); setEditName(k.name); }}><EditIcon fontSize="small" /></IconButton>
|
||||
<IconButton size="small" color="error" onClick={() => deleteMut.mutate(k.id)}><DeleteIcon fontSize="small" /></IconButton>
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
{children.map(c => renderKategorie(c, indent + 1))}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onClose={onClose} maxWidth="sm" fullWidth>
|
||||
<DialogTitle>Kategorien verwalten</DialogTitle>
|
||||
<DialogContent sx={{ display: 'flex', flexDirection: 'column', gap: 2, pt: '16px !important' }}>
|
||||
<Box sx={{ display: 'flex', gap: 1 }}>
|
||||
<DialogContent sx={{ display: 'flex', flexDirection: 'column', gap: 1.5, pt: '20px !important' }}>
|
||||
<Box sx={{ display: 'flex', gap: 1, flexWrap: 'wrap', alignItems: 'center' }}>
|
||||
<TextField
|
||||
size="small"
|
||||
label="Neue Kategorie"
|
||||
label={newParentId ? 'Neue Unterkategorie' : 'Neue Kategorie'}
|
||||
value={newName}
|
||||
onChange={e => setNewName(e.target.value)}
|
||||
sx={{ flexGrow: 1 }}
|
||||
onKeyDown={e => { if (e.key === 'Enter' && newName.trim()) createMut.mutate(newName.trim()); }}
|
||||
onKeyDown={e => { if (e.key === 'Enter' && newName.trim()) createMut.mutate({ name: newName.trim(), parentId: newParentId }); }}
|
||||
/>
|
||||
{newParentId && (
|
||||
<Chip
|
||||
label={`Unter: ${kategorien.find(k => k.id === newParentId)?.name}`}
|
||||
size="small"
|
||||
onDelete={() => setNewParentId(null)}
|
||||
/>
|
||||
)}
|
||||
<Button
|
||||
variant="contained"
|
||||
size="small"
|
||||
onClick={() => { if (newName.trim()) createMut.mutate(newName.trim()); }}
|
||||
onClick={() => { if (newName.trim()) createMut.mutate({ name: newName.trim(), parentId: newParentId }); }}
|
||||
disabled={!newName.trim() || createMut.isPending}
|
||||
>
|
||||
Erstellen
|
||||
</Button>
|
||||
</Box>
|
||||
<Divider />
|
||||
{kategorien.length === 0 ? (
|
||||
{topLevel.length === 0 ? (
|
||||
<Typography color="text.secondary">Keine Kategorien vorhanden.</Typography>
|
||||
) : (
|
||||
kategorien.map(k => (
|
||||
<Box key={k.id} sx={{ display: 'flex', gap: 1, alignItems: 'center' }}>
|
||||
{editId === k.id ? (
|
||||
<>
|
||||
<TextField
|
||||
size="small"
|
||||
value={editName}
|
||||
onChange={e => setEditName(e.target.value)}
|
||||
sx={{ flexGrow: 1 }}
|
||||
onKeyDown={e => { if (e.key === 'Enter' && editName.trim()) updateMut.mutate({ id: k.id, name: editName.trim() }); }}
|
||||
/>
|
||||
<IconButton size="small" onClick={() => { if (editName.trim()) updateMut.mutate({ id: k.id, name: editName.trim() }); }}><CheckIcon fontSize="small" /></IconButton>
|
||||
<IconButton size="small" onClick={() => setEditId(null)}><CloseIcon fontSize="small" /></IconButton>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Typography sx={{ flexGrow: 1 }}>{k.name}</Typography>
|
||||
<IconButton size="small" onClick={() => { setEditId(k.id); setEditName(k.name); }}><EditIcon fontSize="small" /></IconButton>
|
||||
<IconButton size="small" color="error" onClick={() => deleteMut.mutate(k.id)}><DeleteIcon fontSize="small" /></IconButton>
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
))
|
||||
topLevel.map(k => renderKategorie(k, 0))
|
||||
)}
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
@@ -419,7 +443,7 @@ function DetailModal({ requestId, onClose, showAdminActions, showEditButton, can
|
||||
/>
|
||||
)}
|
||||
</DialogTitle>
|
||||
<DialogContent sx={{ display: 'flex', flexDirection: 'column', gap: 2, pt: '16px !important' }}>
|
||||
<DialogContent sx={{ display: 'flex', flexDirection: 'column', gap: 2, pt: '20px !important' }}>
|
||||
{isLoading ? (
|
||||
<Typography color="text.secondary">Lade Details...</Typography>
|
||||
) : !detail ? (
|
||||
@@ -614,7 +638,7 @@ function DetailModal({ requestId, onClose, showAdminActions, showEditButton, can
|
||||
{/* Approve/Reject sub-dialog */}
|
||||
<Dialog open={actionDialog != null} onClose={() => setActionDialog(null)} maxWidth="sm" fullWidth>
|
||||
<DialogTitle>{actionDialog?.action === 'genehmigt' ? 'Anfrage genehmigen' : 'Anfrage ablehnen'}</DialogTitle>
|
||||
<DialogContent sx={{ display: 'flex', flexDirection: 'column', gap: 2, pt: '16px !important' }}>
|
||||
<DialogContent sx={{ display: 'flex', flexDirection: 'column', gap: 2, pt: '20px !important' }}>
|
||||
<TextField
|
||||
fullWidth
|
||||
label="Admin Notizen (optional)"
|
||||
@@ -643,7 +667,7 @@ function DetailModal({ requestId, onClose, showAdminActions, showEditButton, can
|
||||
{/* Link to order sub-dialog */}
|
||||
<Dialog open={linkDialog} onClose={() => { setLinkDialog(false); setSelectedBestellung(null); }} maxWidth="sm" fullWidth>
|
||||
<DialogTitle>Mit Bestellung verknüpfen</DialogTitle>
|
||||
<DialogContent sx={{ display: 'flex', flexDirection: 'column', gap: 2, pt: '16px !important' }}>
|
||||
<DialogContent sx={{ display: 'flex', flexDirection: 'column', gap: 2, pt: '20px !important' }}>
|
||||
<Autocomplete
|
||||
options={bestellungen}
|
||||
getOptionLabel={(o) => `#${o.id} – ${o.bezeichnung}`}
|
||||
@@ -693,6 +717,19 @@ function KatalogTab() {
|
||||
queryFn: () => ausruestungsanfrageApi.getKategorien(),
|
||||
});
|
||||
|
||||
// Build display names for hierarchical categories (e.g. "Kleidung > A-Uniform")
|
||||
const kategorieOptions = useMemo(() => {
|
||||
const map = new Map(kategorien.map(k => [k.id, k]));
|
||||
const getDisplayName = (k: { id: number; name: string; parent_id?: number | null }): string => {
|
||||
if (k.parent_id) {
|
||||
const parent = map.get(k.parent_id);
|
||||
if (parent) return `${parent.name} > ${k.name}`;
|
||||
}
|
||||
return k.name;
|
||||
};
|
||||
return kategorien.map(k => ({ id: k.id, name: getDisplayName(k), isChild: !!k.parent_id }));
|
||||
}, [kategorien]);
|
||||
|
||||
const createItemMut = useMutation({
|
||||
mutationFn: (data: AusruestungArtikelFormData) => ausruestungsanfrageApi.createItem(data),
|
||||
onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['ausruestungsanfrage'] }); showSuccess('Artikel erstellt'); setArtikelDialogOpen(false); },
|
||||
@@ -733,7 +770,7 @@ function KatalogTab() {
|
||||
<InputLabel>Kategorie</InputLabel>
|
||||
<Select value={filterKategorie} label="Kategorie" onChange={e => setFilterKategorie(e.target.value as number | '')}>
|
||||
<MenuItem value="">Alle</MenuItem>
|
||||
{kategorien.map(k => <MenuItem key={k.id} value={k.id}>{k.name}</MenuItem>)}
|
||||
{kategorieOptions.map(k => <MenuItem key={k.id} value={k.id}>{k.name}</MenuItem>)}
|
||||
</Select>
|
||||
</FormControl>
|
||||
{canManageCategories && (
|
||||
@@ -772,7 +809,7 @@ function KatalogTab() {
|
||||
)}
|
||||
</Box>
|
||||
</TableCell>
|
||||
<TableCell>{item.kategorie_name || item.kategorie || '-'}</TableCell>
|
||||
<TableCell>{kategorieOptions.find(k => k.id === item.kategorie_id)?.name || item.kategorie_name || item.kategorie || '-'}</TableCell>
|
||||
<TableCell sx={{ maxWidth: 300, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
|
||||
{item.beschreibung || '-'}
|
||||
</TableCell>
|
||||
@@ -792,7 +829,7 @@ function KatalogTab() {
|
||||
{/* Artikel create/edit dialog */}
|
||||
<Dialog open={artikelDialogOpen} onClose={() => setArtikelDialogOpen(false)} maxWidth="sm" fullWidth>
|
||||
<DialogTitle>{editArtikel ? 'Artikel bearbeiten' : 'Neuer Artikel'}</DialogTitle>
|
||||
<DialogContent sx={{ display: 'flex', flexDirection: 'column', gap: 2, pt: '16px !important' }}>
|
||||
<DialogContent sx={{ display: 'flex', flexDirection: 'column', gap: 2, pt: '20px !important' }}>
|
||||
<TextField label="Bezeichnung" required value={artikelForm.bezeichnung} onChange={e => setArtikelForm(f => ({ ...f, bezeichnung: e.target.value }))} fullWidth />
|
||||
<TextField label="Beschreibung" multiline rows={2} value={artikelForm.beschreibung ?? ''} onChange={e => setArtikelForm(f => ({ ...f, beschreibung: e.target.value }))} />
|
||||
<FormControl fullWidth>
|
||||
@@ -803,7 +840,7 @@ function KatalogTab() {
|
||||
onChange={e => setArtikelForm(f => ({ ...f, kategorie_id: e.target.value ? Number(e.target.value) : null }))}
|
||||
>
|
||||
<MenuItem value="">Keine</MenuItem>
|
||||
{kategorien.map(k => <MenuItem key={k.id} value={k.id}>{k.name}</MenuItem>)}
|
||||
{kategorieOptions.map(k => <MenuItem key={k.id} value={k.id}>{k.name}</MenuItem>)}
|
||||
</Select>
|
||||
</FormControl>
|
||||
{canManage && <EigenschaftenEditor artikelId={editArtikel?.id ?? null} />}
|
||||
@@ -1010,7 +1047,7 @@ function MeineAnfragenTab() {
|
||||
{/* Create Request Dialog */}
|
||||
<Dialog open={createDialogOpen} onClose={() => { setCreateDialogOpen(false); resetCreateForm(); }} maxWidth="sm" fullWidth>
|
||||
<DialogTitle>Neue Bestellung</DialogTitle>
|
||||
<DialogContent sx={{ display: 'flex', flexDirection: 'column', gap: 2, pt: '16px !important' }}>
|
||||
<DialogContent sx={{ display: 'flex', flexDirection: 'column', gap: 2, pt: '20px !important' }}>
|
||||
<TextField
|
||||
label="Bezeichnung (optional)"
|
||||
value={newBezeichnung}
|
||||
|
||||
Reference in New Issue
Block a user