Files
dashboard/frontend/src/pages/Ausruestungsanfrage.tsx
2026-03-24 08:41:24 +01:00

1286 lines
54 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState, useMemo, useEffect, useCallback } from 'react';
import {
Box, Tab, Tabs, Typography, Grid, Button, Chip,
Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Paper,
Dialog, DialogTitle, DialogContent, DialogActions, TextField, IconButton,
MenuItem, Select, FormControl, InputLabel, Autocomplete,
Divider, Checkbox, FormControlLabel, Tooltip,
} from '@mui/material';
import {
Add as AddIcon, Delete as DeleteIcon, Edit as EditIcon,
Check as CheckIcon, Close as CloseIcon, Link as LinkIcon, Settings as SettingsIcon,
} from '@mui/icons-material';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { useSearchParams } from 'react-router-dom';
import DashboardLayout from '../components/dashboard/DashboardLayout';
import ChatAwareFab from '../components/shared/ChatAwareFab';
import { useNotification } from '../contexts/NotificationContext';
import { usePermissionContext } from '../contexts/PermissionContext';
import { useAuth } from '../contexts/AuthContext';
import { ausruestungsanfrageApi } from '../services/ausruestungsanfrage';
import { bestellungApi } from '../services/bestellung';
import { AUSRUESTUNG_STATUS_LABELS, AUSRUESTUNG_STATUS_COLORS } from '../types/ausruestungsanfrage.types';
import type {
AusruestungArtikel, AusruestungArtikelFormData, AusruestungAnfrageFormItem,
AusruestungAnfrageDetailResponse, AusruestungAnfrageStatus, AusruestungAnfrage,
AusruestungOverview, AusruestungEigenschaft,
} from '../types/ausruestungsanfrage.types';
import type { Bestellung } from '../types/bestellung.types';
// ─── Helpers ─────────────────────────────────────────────────────────────────
function formatOrderId(r: AusruestungAnfrage): string {
if (r.bestell_jahr && r.bestell_nummer) {
return `${r.bestell_jahr}/${String(r.bestell_nummer).padStart(3, '0')}`;
}
return `#${r.id}`;
}
const ACTIVE_STATUSES: AusruestungAnfrageStatus[] = ['offen', 'genehmigt', 'bestellt'];
// ─── Eigenschaft Fields Component ────────────────────────────────────────────
interface EigenschaftFieldsProps {
eigenschaften: AusruestungEigenschaft[];
values: Record<number, string>;
onChange: (eigenschaftId: number, wert: string) => void;
}
function EigenschaftFields({ eigenschaften, values, onChange }: EigenschaftFieldsProps) {
if (eigenschaften.length === 0) return null;
return (
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1, ml: 2, mt: 0.5 }}>
{eigenschaften.map(e => (
<Box key={e.id} sx={{ display: 'flex', gap: 1, alignItems: 'center' }}>
{e.typ === 'options' && e.optionen && e.optionen.length > 0 ? (
<FormControl size="small" sx={{ minWidth: 160 }} required={e.pflicht}>
<InputLabel>{e.name}</InputLabel>
<Select
value={values[e.id] || ''}
label={e.name}
onChange={ev => onChange(e.id, ev.target.value)}
>
<MenuItem value=""></MenuItem>
{e.optionen.map(opt => (
<MenuItem key={opt} value={opt}>{opt}</MenuItem>
))}
</Select>
</FormControl>
) : (
<TextField
size="small"
label={e.name}
value={values[e.id] || ''}
onChange={ev => onChange(e.id, ev.target.value)}
required={e.pflicht}
sx={{ minWidth: 160 }}
/>
)}
</Box>
))}
</Box>
);
}
// ─── Category Management Dialog ──────────────────────────────────────────────
interface KategorieDialogProps {
open: boolean;
onClose: () => void;
}
function KategorieDialog({ open, onClose }: KategorieDialogProps) {
const { showSuccess, showError } = useNotification();
const queryClient = useQueryClient();
const [newName, setNewName] = useState('');
const [editId, setEditId] = useState<number | null>(null);
const [editName, setEditName] = useState('');
const { data: kategorien = [] } = useQuery({
queryKey: ['ausruestungsanfrage', 'kategorien'],
queryFn: () => ausruestungsanfrageApi.getKategorien(),
enabled: open,
});
const createMut = useMutation({
mutationFn: (name: string) => ausruestungsanfrageApi.createKategorie(name),
onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['ausruestungsanfrage'] }); showSuccess('Kategorie erstellt'); setNewName(''); },
onError: () => showError('Fehler beim Erstellen'),
});
const updateMut = useMutation({
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'),
});
const deleteMut = useMutation({
mutationFn: (id: number) => ausruestungsanfrageApi.deleteKategorie(id),
onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['ausruestungsanfrage'] }); showSuccess('Kategorie gelöscht'); },
onError: () => showError('Fehler beim Löschen'),
});
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 }}>
<TextField
size="small"
label="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()); }}
/>
<Button
variant="contained"
size="small"
onClick={() => { if (newName.trim()) createMut.mutate(newName.trim()); }}
disabled={!newName.trim() || createMut.isPending}
>
Erstellen
</Button>
</Box>
<Divider />
{kategorien.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>
))
)}
</DialogContent>
<DialogActions>
<Button onClick={onClose}>Schließen</Button>
</DialogActions>
</Dialog>
);
}
// ─── 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 }} />
<FormControl size="small" sx={{ minWidth: 120 }}>
<InputLabel>Typ</InputLabel>
<Select value={newTyp} label="Typ" onChange={e => setNewTyp(e.target.value as 'options' | 'freitext')}>
<MenuItem value="options">Auswahl</MenuItem>
<MenuItem value="freitext">Freitext</MenuItem>
</Select>
</FormControl>
<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>
);
}
// ─── Detail Modal ─────────────────────────────────────────────────────────────
interface DetailModalProps {
requestId: number | null;
onClose: () => void;
showAdminActions?: boolean;
showEditButton?: boolean;
canEditAny?: boolean;
currentUserId?: string;
}
function DetailModal({ requestId, onClose, showAdminActions, showEditButton, canEditAny, currentUserId }: DetailModalProps) {
const { showSuccess, showError } = useNotification();
const queryClient = useQueryClient();
const { hasPermission } = usePermissionContext();
const [editing, setEditing] = useState(false);
const [editBezeichnung, setEditBezeichnung] = useState('');
const [editNotizen, setEditNotizen] = useState('');
const [editItems, setEditItems] = useState<AusruestungAnfrageFormItem[]>([]);
// Admin action state
const [actionDialog, setActionDialog] = useState<{ action: 'genehmigt' | 'abgelehnt' } | null>(null);
const [adminNotizen, setAdminNotizen] = useState('');
const [statusChangeValue, setStatusChangeValue] = useState('');
const [linkDialog, setLinkDialog] = useState(false);
const [selectedBestellung, setSelectedBestellung] = useState<Bestellung | null>(null);
const { data: detail, isLoading } = useQuery<AusruestungAnfrageDetailResponse>({
queryKey: ['ausruestungsanfrage', 'request', requestId],
queryFn: () => ausruestungsanfrageApi.getRequest(requestId!),
enabled: requestId != null,
});
const { data: catalogItems = [] } = useQuery({
queryKey: ['ausruestungsanfrage', 'items-for-edit'],
queryFn: () => ausruestungsanfrageApi.getItems({ aktiv: true }),
enabled: editing,
});
const { data: bestellungen = [] } = useQuery({
queryKey: ['bestellungen'],
queryFn: () => bestellungApi.getOrders(),
enabled: linkDialog,
});
const updateMut = useMutation({
mutationFn: (data: { bezeichnung?: string; notizen?: string; items?: AusruestungAnfrageFormItem[] }) =>
ausruestungsanfrageApi.updateRequest(requestId!, data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['ausruestungsanfrage'] });
showSuccess('Anfrage aktualisiert');
setEditing(false);
},
onError: () => showError('Fehler beim Aktualisieren'),
});
const statusMut = useMutation({
mutationFn: ({ status, notes }: { status: string; notes?: string }) =>
ausruestungsanfrageApi.updateRequestStatus(requestId!, status, notes),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['ausruestungsanfrage'] });
showSuccess('Status aktualisiert');
setActionDialog(null);
setAdminNotizen('');
setStatusChangeValue('');
},
onError: () => showError('Fehler beim Aktualisieren'),
});
const linkMut = useMutation({
mutationFn: (bestellungId: number) => ausruestungsanfrageApi.linkToOrder(requestId!, bestellungId),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['ausruestungsanfrage'] });
showSuccess('Verknüpfung erstellt');
setLinkDialog(false);
setSelectedBestellung(null);
},
onError: () => showError('Fehler beim Verknüpfen'),
});
const startEditing = () => {
if (!detail) return;
setEditBezeichnung(detail.anfrage.bezeichnung || '');
setEditNotizen(detail.anfrage.notizen || '');
setEditItems(detail.positionen.map(p => ({
artikel_id: p.artikel_id,
bezeichnung: p.bezeichnung,
menge: p.menge,
notizen: p.notizen,
eigenschaften: p.eigenschaften?.map(e => ({ eigenschaft_id: e.eigenschaft_id, wert: e.wert })),
})));
setEditing(true);
};
const handleSaveEdit = () => {
if (editItems.length === 0) return;
updateMut.mutate({
bezeichnung: editBezeichnung || undefined,
notizen: editNotizen || undefined,
items: editItems,
});
};
const addEditItem = () => {
setEditItems(prev => [...prev, { bezeichnung: '', menge: 1 }]);
};
const removeEditItem = (idx: number) => {
setEditItems(prev => prev.filter((_, i) => i !== idx));
};
const updateEditItem = (idx: number, field: string, value: unknown) => {
setEditItems(prev => prev.map((item, i) => i === idx ? { ...item, [field]: value } : item));
};
if (!requestId) return null;
const anfrage = detail?.anfrage;
const canEdit = anfrage && (
canEditAny ||
(anfrage.anfrager_id === currentUserId && anfrage.status === 'offen')
);
return (
<>
<Dialog open={requestId != null} onClose={onClose} maxWidth="md" fullWidth>
<DialogTitle sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<span>
Anfrage {anfrage ? formatOrderId(anfrage) : '...'}
{anfrage?.bezeichnung && `${anfrage.bezeichnung}`}
</span>
{anfrage && (
<Chip
label={AUSRUESTUNG_STATUS_LABELS[anfrage.status]}
color={AUSRUESTUNG_STATUS_COLORS[anfrage.status]}
size="small"
/>
)}
</DialogTitle>
<DialogContent sx={{ display: 'flex', flexDirection: 'column', gap: 2, pt: '16px !important' }}>
{isLoading ? (
<Typography color="text.secondary">Lade Details...</Typography>
) : !detail ? (
<Typography color="text.secondary">Anfrage nicht gefunden.</Typography>
) : editing ? (
/* ── Edit Mode ── */
<>
<TextField
label="Bezeichnung (optional)"
value={editBezeichnung}
onChange={e => setEditBezeichnung(e.target.value)}
fullWidth
/>
<TextField
label="Notizen (optional)"
value={editNotizen}
onChange={e => setEditNotizen(e.target.value)}
multiline
rows={2}
fullWidth
/>
<Typography variant="subtitle2" sx={{ mt: 1 }}>Positionen</Typography>
{editItems.map((item, idx) => (
<Box key={idx} sx={{ display: 'flex', gap: 1, alignItems: 'center' }}>
<Autocomplete
freeSolo
options={catalogItems}
getOptionLabel={o => typeof o === 'string' ? o : o.bezeichnung}
value={item.artikel_id ? catalogItems.find(c => c.id === item.artikel_id) || item.bezeichnung : item.bezeichnung}
onChange={(_, v) => {
if (typeof v === 'string') {
updateEditItem(idx, 'bezeichnung', v);
updateEditItem(idx, 'artikel_id', undefined);
} else if (v) {
setEditItems(prev => prev.map((it, i) => i === idx ? { ...it, artikel_id: v.id, bezeichnung: v.bezeichnung } : it));
}
}}
onInputChange={(_, val, reason) => {
if (reason === 'input') {
setEditItems(prev => prev.map((it, i) => i === idx ? { ...it, bezeichnung: val, artikel_id: undefined } : it));
}
}}
renderInput={params => <TextField {...params} label="Artikel" size="small" />}
sx={{ flexGrow: 1 }}
/>
<TextField
size="small"
type="number"
label="Menge"
value={item.menge}
onChange={e => updateEditItem(idx, 'menge', Math.max(1, Number(e.target.value)))}
sx={{ width: 90 }}
inputProps={{ min: 1 }}
/>
<IconButton size="small" onClick={() => removeEditItem(idx)}>
<DeleteIcon fontSize="small" />
</IconButton>
</Box>
))}
<Button size="small" startIcon={<AddIcon />} onClick={addEditItem}>
Position hinzufügen
</Button>
</>
) : (
/* ── View Mode ── */
<>
{anfrage!.anfrager_name && (
<Typography variant="body2" color="text.secondary">
Anfrager: {anfrage!.anfrager_name}
</Typography>
)}
{anfrage!.notizen && (
<Typography variant="body2">Notizen: {anfrage!.notizen}</Typography>
)}
{anfrage!.admin_notizen && (
<Typography variant="body2">Admin Notizen: {anfrage!.admin_notizen}</Typography>
)}
<Typography variant="body2" color="text.secondary">
Erstellt am: {new Date(anfrage!.erstellt_am).toLocaleDateString('de-AT')}
</Typography>
<Divider />
<Typography variant="subtitle2">Positionen</Typography>
{detail.positionen.map(p => (
<Box key={p.id}>
<Typography variant="body2">
- {p.menge}x {p.bezeichnung}{p.notizen ? ` (${p.notizen})` : ''}
</Typography>
{p.eigenschaften && p.eigenschaften.length > 0 && (
<Box sx={{ ml: 2, mt: 0.25 }}>
{p.eigenschaften.map(e => (
<Typography key={e.eigenschaft_id} variant="caption" color="text.secondary" display="block">
{e.eigenschaft_name}: {e.wert}
</Typography>
))}
</Box>
)}
</Box>
))}
{detail.linked_bestellungen && detail.linked_bestellungen.length > 0 && (
<>
<Divider />
<Typography variant="subtitle2">Verknüpfte Bestellungen</Typography>
<Box sx={{ display: 'flex', gap: 0.5, flexWrap: 'wrap' }}>
{detail.linked_bestellungen.map(b => (
<Chip key={b.id} label={`#${b.id} ${b.bezeichnung}`} size="small" />
))}
</Box>
</>
)}
</>
)}
</DialogContent>
<DialogActions>
{editing ? (
<>
<Button onClick={() => setEditing(false)}>Abbrechen</Button>
<Button
variant="contained"
onClick={handleSaveEdit}
disabled={updateMut.isPending || editItems.length === 0 || editItems.some(i => !i.bezeichnung.trim())}
>
Speichern
</Button>
</>
) : (
<>
{/* Admin actions */}
{showAdminActions && anfrage && anfrage.status === 'offen' && (
<>
<Button
color="success"
variant="outlined"
startIcon={<CheckIcon />}
onClick={() => { setActionDialog({ action: 'genehmigt' }); setAdminNotizen(''); }}
>
Genehmigen
</Button>
<Button
color="error"
variant="outlined"
startIcon={<CloseIcon />}
onClick={() => { setActionDialog({ action: 'abgelehnt' }); setAdminNotizen(''); }}
>
Ablehnen
</Button>
</>
)}
{showAdminActions && anfrage && hasPermission('ausruestungsanfrage:approve') && (
<Box sx={{ display: 'flex', gap: 1, alignItems: 'center' }}>
<FormControl size="small" sx={{ minWidth: 140 }}>
<InputLabel>Status ändern</InputLabel>
<Select
value={statusChangeValue}
label="Status ändern"
onChange={e => {
const val = e.target.value;
setStatusChangeValue(val);
if (val) statusMut.mutate({ status: val });
}}
>
{(Object.keys(AUSRUESTUNG_STATUS_LABELS) as AusruestungAnfrageStatus[])
.filter(s => s !== anfrage.status)
.map(s => (
<MenuItem key={s} value={s}>{AUSRUESTUNG_STATUS_LABELS[s]}</MenuItem>
))}
</Select>
</FormControl>
</Box>
)}
{showAdminActions && anfrage && anfrage.status === 'genehmigt' && hasPermission('ausruestungsanfrage:link_orders') && (
<Button
variant="outlined"
startIcon={<LinkIcon />}
onClick={() => setLinkDialog(true)}
>
Verknüpfen
</Button>
)}
{(showEditButton || canEditAny) && canEdit && !editing && (
<Button startIcon={<EditIcon />} onClick={startEditing}>
Bearbeiten
</Button>
)}
<Button onClick={onClose}>Schließen</Button>
</>
)}
</DialogActions>
</Dialog>
{/* 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' }}>
<TextField
fullWidth
label="Admin Notizen (optional)"
value={adminNotizen}
onChange={e => setAdminNotizen(e.target.value)}
multiline
rows={2}
/>
</DialogContent>
<DialogActions>
<Button onClick={() => setActionDialog(null)}>Abbrechen</Button>
<Button
variant="contained"
color={actionDialog?.action === 'genehmigt' ? 'success' : 'error'}
onClick={() => {
if (!actionDialog) return;
statusMut.mutate({ status: actionDialog.action, notes: adminNotizen || undefined });
}}
disabled={statusMut.isPending}
>
{actionDialog?.action === 'genehmigt' ? 'Genehmigen' : 'Ablehnen'}
</Button>
</DialogActions>
</Dialog>
{/* 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' }}>
<Autocomplete
options={bestellungen}
getOptionLabel={(o) => `#${o.id} ${o.bezeichnung}`}
value={selectedBestellung}
onChange={(_, v) => setSelectedBestellung(v)}
renderInput={params => <TextField {...params} label="Bestellung auswählen" />}
/>
</DialogContent>
<DialogActions>
<Button onClick={() => { setLinkDialog(false); setSelectedBestellung(null); }}>Abbrechen</Button>
<Button
variant="contained"
disabled={!selectedBestellung || linkMut.isPending}
onClick={() => { if (selectedBestellung) linkMut.mutate(selectedBestellung.id); }}
>
Verknüpfen
</Button>
</DialogActions>
</Dialog>
</>
);
}
// ─── Catalog Tab ────────────────────────────────────────────────────────────
function KatalogTab() {
const { showSuccess, showError } = useNotification();
const { hasPermission } = usePermissionContext();
const queryClient = useQueryClient();
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({
queryKey: ['ausruestungsanfrage', 'items', filterKategorie],
queryFn: () => ausruestungsanfrageApi.getItems(filterKategorie ? { kategorie_id: filterKategorie as number } : undefined),
});
const { data: kategorien = [] } = useQuery({
queryKey: ['ausruestungsanfrage', 'kategorien'],
queryFn: () => ausruestungsanfrageApi.getKategorien(),
});
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'),
});
const openNewArtikel = () => {
setEditArtikel(null);
setArtikelForm({ bezeichnung: '' });
setArtikelDialogOpen(true);
};
const openEditArtikel = (a: AusruestungArtikel) => {
setEditArtikel(a);
setArtikelForm({ bezeichnung: a.bezeichnung, beschreibung: a.beschreibung, kategorie_id: a.kategorie_id ?? null });
setArtikelDialogOpen(true);
};
const saveArtikel = () => {
if (!artikelForm.bezeichnung.trim()) return;
if (editArtikel) updateItemMut.mutate({ id: editArtikel.id, data: artikelForm });
else createItemMut.mutate(artikelForm);
};
return (
<Box>
{/* Filter */}
<Box sx={{ display: 'flex', gap: 2, mb: 3, alignItems: 'center', flexWrap: 'wrap' }}>
<FormControl size="small" sx={{ minWidth: 200 }}>
<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>)}
</Select>
</FormControl>
{canManageCategories && (
<Tooltip title="Kategorien verwalten">
<IconButton size="small" onClick={() => setKategorieDialogOpen(true)}>
<SettingsIcon fontSize="small" />
</IconButton>
</Tooltip>
)}
</Box>
{/* Catalog table */}
{isLoading ? (
<Typography color="text.secondary">Lade Katalog...</Typography>
) : items.length === 0 ? (
<Typography color="text.secondary">Keine Artikel vorhanden.</Typography>
) : (
<TableContainer component={Paper} variant="outlined">
<Table size="small">
<TableHead>
<TableRow>
<TableCell>Bezeichnung</TableCell>
<TableCell>Kategorie</TableCell>
<TableCell>Beschreibung</TableCell>
{canManage && <TableCell align="right">Aktionen</TableCell>}
</TableRow>
</TableHead>
<TableBody>
{items.map(item => (
<TableRow key={item.id} hover>
<TableCell>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
{item.bezeichnung}
{(item.eigenschaften_count ?? 0) > 0 && (
<Chip label={`${item.eigenschaften_count} Eig.`} size="small" variant="outlined" />
)}
</Box>
</TableCell>
<TableCell>{item.kategorie_name || item.kategorie || '-'}</TableCell>
<TableCell sx={{ maxWidth: 300, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
{item.beschreibung || '-'}
</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>
</TableCell>
)}
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
)}
{/* 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' }}>
<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>
<InputLabel>Kategorie</InputLabel>
<Select
value={artikelForm.kategorie_id ?? ''}
label="Kategorie"
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>)}
</Select>
</FormControl>
{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>
{/* Kategorie management dialog */}
<KategorieDialog open={kategorieDialogOpen} onClose={() => setKategorieDialogOpen(false)} />
{/* FAB for new catalog item */}
{canManage && (
<ChatAwareFab onClick={openNewArtikel} aria-label="Artikel hinzufügen">
<AddIcon />
</ChatAwareFab>
)}
</Box>
);
}
// ─── My Requests Tab ────────────────────────────────────────────────────────
function MeineAnfragenTab() {
const { hasPermission } = usePermissionContext();
const { user } = useAuth();
const queryClient = useQueryClient();
const { showSuccess, showError } = useNotification();
const canCreate = hasPermission('ausruestungsanfrage:create_request');
const canOrderForUser = hasPermission('ausruestungsanfrage:order_for_user');
const canEditAny = hasPermission('ausruestungsanfrage:edit');
const [detailId, setDetailId] = useState<number | null>(null);
const [statusFilter, setStatusFilter] = useState<AusruestungAnfrageStatus[]>(ACTIVE_STATUSES);
const [createDialogOpen, setCreateDialogOpen] = useState(false);
const [newBezeichnung, setNewBezeichnung] = useState('');
const [newNotizen, setNewNotizen] = useState('');
const [newFuerBenutzer, setNewFuerBenutzer] = useState<{ id: string; name: string } | null>(null);
const [newItems, setNewItems] = useState<AusruestungAnfrageFormItem[]>([{ bezeichnung: '', menge: 1 }]);
// Track loaded eigenschaften per item row (by artikel_id)
const [itemEigenschaften, setItemEigenschaften] = useState<Record<number, AusruestungEigenschaft[]>>({});
// Track eigenschaft values per item row index
const [itemEigenschaftValues, setItemEigenschaftValues] = useState<Record<number, Record<number, string>>>({});
const { data: requests = [], isLoading } = useQuery({
queryKey: ['ausruestungsanfrage', 'myRequests'],
queryFn: () => ausruestungsanfrageApi.getMyRequests(),
});
const { data: catalogItems = [] } = useQuery({
queryKey: ['ausruestungsanfrage', 'items-for-create'],
queryFn: () => ausruestungsanfrageApi.getItems({ aktiv: true }),
enabled: createDialogOpen,
});
const { data: orderUsers = [] } = useQuery({
queryKey: ['ausruestungsanfrage', 'orderUsers'],
queryFn: () => ausruestungsanfrageApi.getOrderUsers(),
enabled: createDialogOpen && canOrderForUser,
});
const createMut = useMutation({
mutationFn: (args: { items: AusruestungAnfrageFormItem[]; notizen?: string; bezeichnung?: string; fuer_benutzer_id?: string }) =>
ausruestungsanfrageApi.createRequest(args.items, args.notizen, args.bezeichnung, args.fuer_benutzer_id),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['ausruestungsanfrage'] });
showSuccess('Anfrage erstellt');
setCreateDialogOpen(false);
resetCreateForm();
},
onError: () => showError('Fehler beim Erstellen der Anfrage'),
});
const resetCreateForm = () => {
setNewBezeichnung('');
setNewNotizen('');
setNewFuerBenutzer(null);
setNewItems([{ bezeichnung: '', menge: 1 }]);
setItemEigenschaften({});
setItemEigenschaftValues({});
};
const loadEigenschaften = useCallback(async (artikelId: number) => {
if (itemEigenschaften[artikelId]) return;
try {
const eigs = await ausruestungsanfrageApi.getArtikelEigenschaften(artikelId);
setItemEigenschaften(prev => ({ ...prev, [artikelId]: eigs }));
} catch { /* ignore */ }
}, [itemEigenschaften]);
const handleCreateSubmit = () => {
const validItems = newItems.filter(i => i.bezeichnung.trim()).map((item, idx) => {
const vals = itemEigenschaftValues[idx] || {};
const eigenschaften = Object.entries(vals)
.filter(([, wert]) => wert.trim())
.map(([eid, wert]) => ({ eigenschaft_id: Number(eid), wert }));
return { ...item, eigenschaften: eigenschaften.length > 0 ? eigenschaften : undefined };
});
if (validItems.length === 0) return;
// Check required eigenschaften
for (let idx = 0; idx < newItems.length; idx++) {
const item = newItems[idx];
if (!item.bezeichnung.trim()) continue;
if (item.artikel_id && itemEigenschaften[item.artikel_id]) {
for (const e of itemEigenschaften[item.artikel_id]) {
if (e.pflicht && !(itemEigenschaftValues[idx]?.[e.id]?.trim())) {
showError(`Pflichtfeld "${e.name}" für "${item.bezeichnung}" fehlt`);
return;
}
}
}
}
createMut.mutate({
items: validItems,
notizen: newNotizen || undefined,
bezeichnung: newBezeichnung || undefined,
fuer_benutzer_id: newFuerBenutzer?.id,
});
};
const filteredRequests = useMemo(() => {
if (statusFilter.length === 0) return requests;
return requests.filter(r => statusFilter.includes(r.status));
}, [requests, statusFilter]);
const handleStatusFilterChange = (value: string) => {
if (value === 'all') {
setStatusFilter([]);
} else if (value === 'active') {
setStatusFilter(ACTIVE_STATUSES);
} else {
setStatusFilter([value as AusruestungAnfrageStatus]);
}
};
const currentFilterValue = useMemo(() => {
if (statusFilter.length === 0) return 'all';
if (statusFilter.length === ACTIVE_STATUSES.length && ACTIVE_STATUSES.every(s => statusFilter.includes(s))) return 'active';
return statusFilter[0] || 'all';
}, [statusFilter]);
if (isLoading) return <Typography color="text.secondary">Lade Anfragen...</Typography>;
return (
<Box>
<Box sx={{ display: 'flex', gap: 2, mb: 2, alignItems: 'center' }}>
<FormControl size="small" sx={{ minWidth: 200 }}>
<InputLabel>Status</InputLabel>
<Select value={currentFilterValue} label="Status" onChange={e => handleStatusFilterChange(e.target.value)}>
<MenuItem value="active">Aktive Anfragen</MenuItem>
<MenuItem value="all">Alle</MenuItem>
{(Object.keys(AUSRUESTUNG_STATUS_LABELS) as AusruestungAnfrageStatus[]).map(s => (
<MenuItem key={s} value={s}>{AUSRUESTUNG_STATUS_LABELS[s]}</MenuItem>
))}
</Select>
</FormControl>
</Box>
{filteredRequests.length === 0 ? (
<Typography color="text.secondary" sx={{ mb: 2 }}>Keine Anfragen vorhanden.</Typography>
) : (
<TableContainer component={Paper} variant="outlined">
<Table size="small">
<TableHead>
<TableRow>
<TableCell>Anfrage</TableCell>
<TableCell>Bezeichnung</TableCell>
<TableCell>Status</TableCell>
<TableCell>Positionen</TableCell>
<TableCell>Erstellt am</TableCell>
</TableRow>
</TableHead>
<TableBody>
{filteredRequests.map(r => (
<TableRow key={r.id} hover sx={{ cursor: 'pointer' }} onClick={() => setDetailId(r.id)}>
<TableCell>{formatOrderId(r)}</TableCell>
<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>{new Date(r.erstellt_am).toLocaleDateString('de-AT')}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
)}
{/* Detail Modal */}
<DetailModal
requestId={detailId}
onClose={() => setDetailId(null)}
showEditButton
canEditAny={canEditAny}
currentUserId={user?.id}
/>
{/* 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' }}>
<TextField
label="Bezeichnung (optional)"
value={newBezeichnung}
onChange={e => setNewBezeichnung(e.target.value)}
fullWidth
/>
{canOrderForUser && (
<Autocomplete
options={orderUsers}
getOptionLabel={o => o.name}
value={newFuerBenutzer}
onChange={(_, v) => setNewFuerBenutzer(v)}
renderInput={params => <TextField {...params} label="Für wen (optional)" />}
/>
)}
<TextField
label="Notizen (optional)"
value={newNotizen}
onChange={e => setNewNotizen(e.target.value)}
multiline
rows={2}
fullWidth
/>
<Divider />
<Typography variant="subtitle2">Positionen</Typography>
{newItems.map((item, idx) => (
<Box key={idx}>
<Box sx={{ display: 'flex', gap: 1, alignItems: 'center' }}>
<Autocomplete
freeSolo
options={catalogItems}
getOptionLabel={o => typeof o === 'string' ? o : o.bezeichnung}
value={item.artikel_id ? catalogItems.find(c => c.id === item.artikel_id) || item.bezeichnung : item.bezeichnung}
onChange={(_, v) => {
if (typeof v === 'string') {
setNewItems(prev => prev.map((it, i) => i === idx ? { ...it, bezeichnung: v, artikel_id: undefined } : it));
// Clear eigenschaften for this row
setItemEigenschaftValues(prev => { const n = { ...prev }; delete n[idx]; return n; });
} else if (v) {
setNewItems(prev => prev.map((it, i) => i === idx ? { ...it, artikel_id: v.id, bezeichnung: v.bezeichnung } : it));
loadEigenschaften(v.id);
}
}}
onInputChange={(_, val, reason) => {
if (reason === 'input') {
setNewItems(prev => prev.map((it, i) => i === idx ? { ...it, bezeichnung: val, artikel_id: undefined } : it));
}
}}
renderInput={params => <TextField {...params} label="Artikel" size="small" />}
sx={{ flexGrow: 1 }}
/>
<TextField
size="small"
type="number"
label="Menge"
value={item.menge}
onChange={e => setNewItems(prev => prev.map((it, i) => i === idx ? { ...it, menge: Math.max(1, Number(e.target.value)) } : it))}
sx={{ width: 90 }}
inputProps={{ min: 1 }}
/>
<IconButton size="small" onClick={() => setNewItems(prev => prev.filter((_, i) => i !== idx))} disabled={newItems.length <= 1}>
<DeleteIcon fontSize="small" />
</IconButton>
</Box>
{/* Eigenschaft fields for this item */}
{item.artikel_id && itemEigenschaften[item.artikel_id] && itemEigenschaften[item.artikel_id].length > 0 && (
<EigenschaftFields
eigenschaften={itemEigenschaften[item.artikel_id]}
values={itemEigenschaftValues[idx] || {}}
onChange={(eid, wert) => setItemEigenschaftValues(prev => ({
...prev,
[idx]: { ...(prev[idx] || {}), [eid]: wert },
}))}
/>
)}
</Box>
))}
<Button size="small" startIcon={<AddIcon />} onClick={() => setNewItems(prev => [...prev, { bezeichnung: '', menge: 1 }])}>
Position hinzufügen
</Button>
</DialogContent>
<DialogActions>
<Button onClick={() => { setCreateDialogOpen(false); resetCreateForm(); }}>Abbrechen</Button>
<Button
variant="contained"
onClick={handleCreateSubmit}
disabled={createMut.isPending || newItems.every(i => !i.bezeichnung.trim())}
>
Anfrage erstellen
</Button>
</DialogActions>
</Dialog>
{/* FAB for creating new request */}
{canCreate && (
<ChatAwareFab onClick={() => setCreateDialogOpen(true)} aria-label="Neue Anfrage erstellen">
<AddIcon />
</ChatAwareFab>
)}
</Box>
);
}
// ─── Admin All Requests Tab (merged with overview) ──────────────────────────
function AlleAnfragenTab() {
const { hasPermission } = usePermissionContext();
const { user } = useAuth();
const [statusFilter, setStatusFilter] = useState<string>('');
const [detailId, setDetailId] = useState<number | null>(null);
const canEditAny = hasPermission('ausruestungsanfrage:edit');
const { data: requests = [], isLoading } = useQuery({
queryKey: ['ausruestungsanfrage', 'requests', statusFilter],
queryFn: () => ausruestungsanfrageApi.getRequests(statusFilter ? { status: statusFilter } : undefined),
});
const { data: overview } = useQuery<AusruestungOverview>({
queryKey: ['ausruestungsanfrage', 'overview'],
queryFn: () => ausruestungsanfrageApi.getOverview(),
});
if (isLoading) return <Typography color="text.secondary">Lade Anfragen...</Typography>;
return (
<Box>
{/* Summary cards */}
<Grid container spacing={2} sx={{ mb: 3 }}>
<Grid item xs={6} sm={3}>
<Paper variant="outlined" sx={{ p: 2, textAlign: 'center' }}>
<Typography variant="h4" fontWeight={700}>{overview?.pending_count ?? '-'}</Typography>
<Typography variant="body2" color="text.secondary">Offene</Typography>
</Paper>
</Grid>
<Grid item xs={6} sm={3}>
<Paper variant="outlined" sx={{ p: 2, textAlign: 'center' }}>
<Typography variant="h4" fontWeight={700}>{overview?.approved_count ?? '-'}</Typography>
<Typography variant="body2" color="text.secondary">Genehmigte</Typography>
</Paper>
</Grid>
<Grid item xs={6} sm={3}>
<Paper variant="outlined" sx={{ p: 2, textAlign: 'center' }}>
<Typography variant="h4" fontWeight={700} color="warning.main">{overview?.unhandled_count ?? '-'}</Typography>
<Typography variant="body2" color="text.secondary">Neue (unbearbeitet)</Typography>
</Paper>
</Grid>
<Grid item xs={6} sm={3}>
<Paper variant="outlined" sx={{ p: 2, textAlign: 'center' }}>
<Typography variant="h4" fontWeight={700}>{overview?.total_items ?? '-'}</Typography>
<Typography variant="body2" color="text.secondary">Gesamt Artikel</Typography>
</Paper>
</Grid>
</Grid>
<FormControl size="small" sx={{ minWidth: 200, mb: 2 }}>
<InputLabel>Status Filter</InputLabel>
<Select value={statusFilter} label="Status Filter" onChange={e => setStatusFilter(e.target.value)}>
<MenuItem value="">Alle</MenuItem>
{(Object.keys(AUSRUESTUNG_STATUS_LABELS) as AusruestungAnfrageStatus[]).map(s => (
<MenuItem key={s} value={s}>{AUSRUESTUNG_STATUS_LABELS[s]}</MenuItem>
))}
</Select>
</FormControl>
{requests.length === 0 ? (
<Typography color="text.secondary">Keine Anfragen vorhanden.</Typography>
) : (
<TableContainer component={Paper} variant="outlined">
<Table size="small">
<TableHead>
<TableRow>
<TableCell>Anfrage</TableCell>
<TableCell>Bezeichnung</TableCell>
<TableCell>Anfrager</TableCell>
<TableCell>Status</TableCell>
<TableCell>Positionen</TableCell>
<TableCell>Erstellt am</TableCell>
</TableRow>
</TableHead>
<TableBody>
{requests.map(r => (
<TableRow key={r.id} hover sx={{ cursor: 'pointer' }} onClick={() => setDetailId(r.id)}>
<TableCell>{formatOrderId(r)}</TableCell>
<TableCell>{r.bezeichnung || '-'}</TableCell>
<TableCell>{r.anfrager_name || r.anfrager_id}</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>{new Date(r.erstellt_am).toLocaleDateString('de-AT')}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
)}
{/* Detail Modal with admin actions */}
<DetailModal
requestId={detailId}
onClose={() => setDetailId(null)}
showAdminActions
showEditButton
canEditAny={canEditAny}
currentUserId={user?.id}
/>
</Box>
);
}
// ─── Main Page ──────────────────────────────────────────────────────────────
export default function Ausruestungsanfrage() {
const [searchParams, setSearchParams] = useSearchParams();
const { hasPermission } = usePermissionContext();
const canView = hasPermission('ausruestungsanfrage:view');
const canCreate = hasPermission('ausruestungsanfrage:create_request');
const canApprove = hasPermission('ausruestungsanfrage:approve');
const tabCount = 1 + (canCreate ? 1 : 0) + (canApprove ? 1 : 0);
const [activeTab, setActiveTab] = useState(() => {
const t = Number(searchParams.get('tab'));
return t >= 0 && t < tabCount ? t : 0;
});
// Sync tab from URL changes (e.g. sidebar navigation)
useEffect(() => {
const t = Number(searchParams.get('tab'));
if (t >= 0 && t < tabCount) setActiveTab(t);
}, [searchParams, tabCount]);
const handleTabChange = (_: React.SyntheticEvent, val: number) => {
setActiveTab(val);
setSearchParams({ tab: String(val) }, { replace: true });
};
const tabIndex = useMemo(() => {
const map: Record<string, number> = {};
let next = 0;
if (canCreate) { map.meine = next; next++; }
if (canApprove) { map.alle = next; next++; }
map.katalog = next;
return map;
}, [canCreate, canApprove]);
if (!canView) {
return (
<DashboardLayout>
<Typography>Keine Berechtigung.</Typography>
</DashboardLayout>
);
}
return (
<DashboardLayout>
<Typography variant="h5" fontWeight={700} sx={{ mb: 2 }}>Interne Bestellungen</Typography>
<Box sx={{ borderBottom: 1, borderColor: 'divider', mb: 3 }}>
<Tabs value={activeTab} onChange={handleTabChange} variant="scrollable" scrollButtons="auto">
{canCreate && <Tab label="Meine Anfragen" />}
{canApprove && <Tab label="Alle Anfragen" />}
<Tab label="Katalog" />
</Tabs>
</Box>
{canCreate && activeTab === tabIndex.meine && <MeineAnfragenTab />}
{canApprove && activeTab === tabIndex.alle && <AlleAnfragenTab />}
{activeTab === tabIndex.katalog && <KatalogTab />}
</DashboardLayout>
);
}