rework from modal to page
This commit is contained in:
@@ -3,7 +3,7 @@ import {
|
||||
Box, Tab, Tabs, Typography, Grid, Button, Chip,
|
||||
Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Paper,
|
||||
Dialog, DialogTitle, DialogContent, DialogActions, TextField, IconButton,
|
||||
MenuItem, Divider, Checkbox, FormControlLabel, Tooltip,
|
||||
MenuItem, Divider, Tooltip,
|
||||
} from '@mui/material';
|
||||
import {
|
||||
Add as AddIcon, Delete as DeleteIcon, Edit as EditIcon,
|
||||
@@ -18,7 +18,6 @@ import { usePermissionContext } from '../contexts/PermissionContext';
|
||||
import { ausruestungsanfrageApi } from '../services/ausruestungsanfrage';
|
||||
import { AUSRUESTUNG_STATUS_LABELS, AUSRUESTUNG_STATUS_COLORS } from '../types/ausruestungsanfrage.types';
|
||||
import type {
|
||||
AusruestungArtikel, AusruestungArtikelFormData,
|
||||
AusruestungAnfrageStatus, AusruestungAnfrage,
|
||||
AusruestungOverview,
|
||||
} from '../types/ausruestungsanfrage.types';
|
||||
@@ -154,125 +153,18 @@ function KategorieDialog({ open, onClose }: KategorieDialogProps) {
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Eigenschaften Editor (in Artikel dialog) ────────────────────────────────
|
||||
|
||||
interface EigenschaftenEditorProps {
|
||||
artikelId: number | null;
|
||||
}
|
||||
|
||||
function EigenschaftenEditor({ artikelId }: EigenschaftenEditorProps) {
|
||||
const { showSuccess, showError } = useNotification();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const [newName, setNewName] = useState('');
|
||||
const [newTyp, setNewTyp] = useState<'options' | 'freitext'>('options');
|
||||
const [newOptionen, setNewOptionen] = useState('');
|
||||
const [newPflicht, setNewPflicht] = useState(false);
|
||||
|
||||
const { data: eigenschaften = [] } = useQuery({
|
||||
queryKey: ['ausruestungsanfrage', 'eigenschaften', artikelId],
|
||||
queryFn: () => ausruestungsanfrageApi.getArtikelEigenschaften(artikelId!),
|
||||
enabled: artikelId != null,
|
||||
});
|
||||
|
||||
const upsertMut = useMutation({
|
||||
mutationFn: (data: { eigenschaft_id?: number; name: string; typ: string; optionen?: string[]; pflicht?: boolean; sort_order?: number }) =>
|
||||
ausruestungsanfrageApi.upsertArtikelEigenschaft(artikelId!, data),
|
||||
onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['ausruestungsanfrage', 'eigenschaften', artikelId] }); showSuccess('Eigenschaft gespeichert'); },
|
||||
onError: () => showError('Fehler beim Speichern'),
|
||||
});
|
||||
|
||||
const deleteMut = useMutation({
|
||||
mutationFn: (id: number) => ausruestungsanfrageApi.deleteArtikelEigenschaft(id),
|
||||
onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['ausruestungsanfrage', 'eigenschaften', artikelId] }); showSuccess('Eigenschaft gelöscht'); },
|
||||
onError: () => showError('Fehler beim Löschen'),
|
||||
});
|
||||
|
||||
const handleAdd = () => {
|
||||
if (!newName.trim()) return;
|
||||
const optionen = newTyp === 'options' ? newOptionen.split(',').map(s => s.trim()).filter(Boolean) : undefined;
|
||||
upsertMut.mutate({
|
||||
name: newName.trim(),
|
||||
typ: newTyp,
|
||||
optionen,
|
||||
pflicht: newPflicht,
|
||||
sort_order: eigenschaften.length,
|
||||
});
|
||||
setNewName('');
|
||||
setNewOptionen('');
|
||||
setNewPflicht(false);
|
||||
};
|
||||
|
||||
if (artikelId == null) return <Typography variant="body2" color="text.secondary" sx={{ mt: 1 }}>Bitte speichern Sie den Artikel zuerst, bevor Sie Eigenschaften hinzufügen.</Typography>;
|
||||
|
||||
return (
|
||||
<Box sx={{ mt: 1 }}>
|
||||
<Typography variant="subtitle2" sx={{ mb: 1 }}>Eigenschaften</Typography>
|
||||
{eigenschaften.map(e => (
|
||||
<Box key={e.id} sx={{ display: 'flex', gap: 1, alignItems: 'center', mb: 0.5, pl: 1, borderLeft: '2px solid', borderColor: 'divider' }}>
|
||||
<Typography variant="body2" sx={{ flexGrow: 1 }}>
|
||||
{e.name} ({e.typ === 'options' ? `Auswahl: ${e.optionen?.join(', ')}` : 'Freitext'})
|
||||
{e.pflicht && <Chip label="Pflicht" size="small" sx={{ ml: 0.5 }} />}
|
||||
</Typography>
|
||||
<IconButton size="small" color="error" onClick={() => deleteMut.mutate(e.id)}><DeleteIcon fontSize="small" /></IconButton>
|
||||
</Box>
|
||||
))}
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1, mt: 1, p: 1, border: '1px dashed', borderColor: 'divider', borderRadius: 1 }}>
|
||||
<Box sx={{ display: 'flex', gap: 1, alignItems: 'center' }}>
|
||||
<TextField size="small" label="Name" value={newName} onChange={e => setNewName(e.target.value)} sx={{ flexGrow: 1 }} />
|
||||
<TextField
|
||||
select
|
||||
size="small"
|
||||
label="Typ"
|
||||
value={newTyp}
|
||||
onChange={e => setNewTyp(e.target.value as 'options' | 'freitext')}
|
||||
sx={{ minWidth: 120 }}
|
||||
>
|
||||
<MenuItem value="options">Auswahl</MenuItem>
|
||||
<MenuItem value="freitext">Freitext</MenuItem>
|
||||
</TextField>
|
||||
<FormControlLabel
|
||||
control={<Checkbox size="small" checked={newPflicht} onChange={e => setNewPflicht(e.target.checked)} />}
|
||||
label="Pflicht"
|
||||
/>
|
||||
</Box>
|
||||
{newTyp === 'options' && (
|
||||
<TextField
|
||||
size="small"
|
||||
label="Optionen (kommagetrennt)"
|
||||
value={newOptionen}
|
||||
onChange={e => setNewOptionen(e.target.value)}
|
||||
placeholder="S, M, L, XL"
|
||||
fullWidth
|
||||
/>
|
||||
)}
|
||||
<Button
|
||||
size="small"
|
||||
startIcon={<AddIcon />}
|
||||
onClick={handleAdd}
|
||||
disabled={!newName.trim() || upsertMut.isPending}
|
||||
>
|
||||
Eigenschaft hinzufügen
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Catalog Tab ────────────────────────────────────────────────────────────
|
||||
|
||||
function KatalogTab() {
|
||||
const { showSuccess, showError } = useNotification();
|
||||
const { hasPermission } = usePermissionContext();
|
||||
const queryClient = useQueryClient();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const canManage = hasPermission('ausruestungsanfrage:manage_catalog');
|
||||
const canManageCategories = hasPermission('ausruestungsanfrage:manage_categories');
|
||||
|
||||
const [filterKategorie, setFilterKategorie] = useState<number | ''>('');
|
||||
const [artikelDialogOpen, setArtikelDialogOpen] = useState(false);
|
||||
const [editArtikel, setEditArtikel] = useState<AusruestungArtikel | null>(null);
|
||||
const [artikelForm, setArtikelForm] = useState<AusruestungArtikelFormData>({ bezeichnung: '' });
|
||||
const [kategorieDialogOpen, setKategorieDialogOpen] = useState(false);
|
||||
|
||||
const { data: items = [], isLoading } = useQuery({
|
||||
@@ -285,9 +177,6 @@ function KatalogTab() {
|
||||
queryFn: () => ausruestungsanfrageApi.getKategorien(),
|
||||
});
|
||||
|
||||
const topKategorien = useMemo(() => kategorien.filter(k => !k.parent_id), [kategorien]);
|
||||
const subKategorienOf = useCallback((parentId: number) => kategorien.filter(k => k.parent_id === parentId), [kategorien]);
|
||||
|
||||
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 => {
|
||||
@@ -300,48 +189,12 @@ function KatalogTab() {
|
||||
return kategorien.map(k => ({ id: k.id, name: getDisplayName(k), isChild: !!k.parent_id }));
|
||||
}, [kategorien]);
|
||||
|
||||
const [artikelMainKat, setArtikelMainKat] = useState<number | ''>('');
|
||||
const artikelSubKats = useMemo(() => artikelMainKat ? subKategorienOf(artikelMainKat as number) : [], [artikelMainKat, subKategorienOf]);
|
||||
|
||||
const createItemMut = useMutation({
|
||||
mutationFn: (data: AusruestungArtikelFormData) => ausruestungsanfrageApi.createItem(data),
|
||||
onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['ausruestungsanfrage'] }); showSuccess('Artikel erstellt'); setArtikelDialogOpen(false); },
|
||||
onError: () => showError('Fehler beim Erstellen'),
|
||||
});
|
||||
const updateItemMut = useMutation({
|
||||
mutationFn: ({ id, data }: { id: number; data: Partial<AusruestungArtikelFormData> }) => ausruestungsanfrageApi.updateItem(id, data),
|
||||
onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['ausruestungsanfrage'] }); showSuccess('Artikel aktualisiert'); setArtikelDialogOpen(false); },
|
||||
onError: () => showError('Fehler beim Aktualisieren'),
|
||||
});
|
||||
const deleteItemMut = useMutation({
|
||||
mutationFn: (id: number) => ausruestungsanfrageApi.deleteItem(id),
|
||||
onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['ausruestungsanfrage'] }); showSuccess('Artikel gelöscht'); },
|
||||
onError: () => showError('Fehler beim Löschen'),
|
||||
onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['ausruestungsanfrage'] }); showSuccess('Artikel geloescht'); },
|
||||
onError: () => showError('Fehler beim Loeschen'),
|
||||
});
|
||||
|
||||
const openNewArtikel = () => {
|
||||
setEditArtikel(null);
|
||||
setArtikelForm({ bezeichnung: '' });
|
||||
setArtikelMainKat('');
|
||||
setArtikelDialogOpen(true);
|
||||
};
|
||||
const openEditArtikel = (a: AusruestungArtikel) => {
|
||||
setEditArtikel(a);
|
||||
setArtikelForm({ bezeichnung: a.bezeichnung, beschreibung: a.beschreibung, kategorie_id: a.kategorie_id ?? null });
|
||||
const kat = kategorien.find(k => k.id === a.kategorie_id);
|
||||
if (kat?.parent_id) {
|
||||
setArtikelMainKat(kat.parent_id);
|
||||
} else {
|
||||
setArtikelMainKat(a.kategorie_id || '');
|
||||
}
|
||||
setArtikelDialogOpen(true);
|
||||
};
|
||||
const saveArtikel = () => {
|
||||
if (!artikelForm.bezeichnung.trim()) return;
|
||||
if (editArtikel) updateItemMut.mutate({ id: editArtikel.id, data: artikelForm });
|
||||
else createItemMut.mutate(artikelForm);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Box sx={{ display: 'flex', gap: 2, mb: 3, alignItems: 'center', flexWrap: 'wrap' }}>
|
||||
@@ -382,7 +235,12 @@ function KatalogTab() {
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{items.map(item => (
|
||||
<TableRow key={item.id} hover>
|
||||
<TableRow
|
||||
key={item.id}
|
||||
hover
|
||||
sx={{ cursor: 'pointer' }}
|
||||
onClick={() => navigate(`/ausruestungsanfrage/artikel/${item.id}`)}
|
||||
>
|
||||
<TableCell>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
{item.bezeichnung}
|
||||
@@ -397,8 +255,7 @@ function KatalogTab() {
|
||||
</TableCell>
|
||||
{canManage && (
|
||||
<TableCell align="right">
|
||||
<IconButton size="small" onClick={() => openEditArtikel(item)}><EditIcon fontSize="small" /></IconButton>
|
||||
<IconButton size="small" color="error" onClick={() => deleteItemMut.mutate(item.id)}><DeleteIcon fontSize="small" /></IconButton>
|
||||
<IconButton size="small" color="error" onClick={(e) => { e.stopPropagation(); deleteItemMut.mutate(item.id); }}><DeleteIcon fontSize="small" /></IconButton>
|
||||
</TableCell>
|
||||
)}
|
||||
</TableRow>
|
||||
@@ -408,56 +265,10 @@ function KatalogTab() {
|
||||
</TableContainer>
|
||||
)}
|
||||
|
||||
<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: '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 }))} />
|
||||
<TextField
|
||||
select
|
||||
label="Hauptkategorie"
|
||||
value={artikelMainKat}
|
||||
onChange={e => {
|
||||
const val = e.target.value ? Number(e.target.value) : '';
|
||||
setArtikelMainKat(val);
|
||||
if (val) {
|
||||
const subs = subKategorienOf(val as number);
|
||||
setArtikelForm(f => ({ ...f, kategorie_id: subs.length === 0 ? (val as number) : null }));
|
||||
} else {
|
||||
setArtikelForm(f => ({ ...f, kategorie_id: null }));
|
||||
}
|
||||
}}
|
||||
fullWidth
|
||||
>
|
||||
<MenuItem value="">Keine</MenuItem>
|
||||
{topKategorien.map(k => <MenuItem key={k.id} value={k.id}>{k.name}</MenuItem>)}
|
||||
</TextField>
|
||||
{artikelMainKat && artikelSubKats.length > 0 && (
|
||||
<TextField
|
||||
select
|
||||
label="Unterkategorie"
|
||||
value={artikelForm.kategorie_id ?? ''}
|
||||
onChange={e => setArtikelForm(f => ({ ...f, kategorie_id: e.target.value ? Number(e.target.value) : (artikelMainKat as number) }))}
|
||||
fullWidth
|
||||
>
|
||||
<MenuItem value={artikelMainKat as number}>Keine (nur Hauptkategorie)</MenuItem>
|
||||
{artikelSubKats.map(k => <MenuItem key={k.id} value={k.id}>{k.name}</MenuItem>)}
|
||||
</TextField>
|
||||
)}
|
||||
{canManage && <EigenschaftenEditor artikelId={editArtikel?.id ?? null} />}
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={() => setArtikelDialogOpen(false)}>Abbrechen</Button>
|
||||
<Button variant="contained" onClick={saveArtikel} disabled={!artikelForm.bezeichnung.trim() || createItemMut.isPending || updateItemMut.isPending}>
|
||||
Speichern
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
|
||||
<KategorieDialog open={kategorieDialogOpen} onClose={() => setKategorieDialogOpen(false)} />
|
||||
|
||||
{canManage && (
|
||||
<ChatAwareFab onClick={openNewArtikel} aria-label="Artikel hinzufügen">
|
||||
<ChatAwareFab onClick={() => navigate('/ausruestungsanfrage/artikel/neu')} aria-label="Artikel hinzufuegen">
|
||||
<AddIcon />
|
||||
</ChatAwareFab>
|
||||
)}
|
||||
@@ -533,6 +344,7 @@ function MeineAnfragenTab() {
|
||||
<TableCell>Bezeichnung</TableCell>
|
||||
<TableCell>Status</TableCell>
|
||||
<TableCell>Positionen</TableCell>
|
||||
<TableCell>Geliefert</TableCell>
|
||||
<TableCell>Erstellt am</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
@@ -543,6 +355,11 @@ function MeineAnfragenTab() {
|
||||
<TableCell>{r.bezeichnung || '-'}</TableCell>
|
||||
<TableCell><Chip label={AUSRUESTUNG_STATUS_LABELS[r.status]} color={AUSRUESTUNG_STATUS_COLORS[r.status]} size="small" /></TableCell>
|
||||
<TableCell>{r.positionen_count ?? r.items_count ?? '-'}</TableCell>
|
||||
<TableCell>
|
||||
{r.positionen_count != null && r.positionen_count > 0
|
||||
? `${r.geliefert_count ?? 0}/${r.positionen_count}`
|
||||
: '-'}
|
||||
</TableCell>
|
||||
<TableCell>{new Date(r.erstellt_am).toLocaleDateString('de-AT')}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
|
||||
Reference in New Issue
Block a user