rework from modal to page

This commit is contained in:
Matthias Hochmeister
2026-03-25 09:37:16 +01:00
parent 4ed76fe20d
commit 4ad260ce66
9 changed files with 1714 additions and 1389 deletions

View File

@@ -24,9 +24,8 @@ import {
FormControl,
InputLabel,
Tooltip,
Autocomplete,
} from '@mui/material';
import { Add as AddIcon, Edit as EditIcon, Delete as DeleteIcon, RemoveCircleOutline as RemoveIcon } from '@mui/icons-material';
import { Add as AddIcon, Edit as EditIcon, Delete as DeleteIcon } from '@mui/icons-material';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { useNavigate, useSearchParams } from 'react-router-dom';
import DashboardLayout from '../components/dashboard/DashboardLayout';
@@ -35,7 +34,7 @@ import { useNotification } from '../contexts/NotificationContext';
import { usePermissionContext } from '../contexts/PermissionContext';
import { bestellungApi } from '../services/bestellung';
import { BESTELLUNG_STATUS_LABELS, BESTELLUNG_STATUS_COLORS } from '../types/bestellung.types';
import type { BestellungStatus, BestellungFormData, BestellpositionFormData, LieferantFormData, Lieferant } from '../types/bestellung.types';
import type { BestellungStatus, LieferantFormData, Lieferant } from '../types/bestellung.types';
// ── Helpers ──
@@ -61,9 +60,7 @@ const ALL_STATUSES: BestellungStatus[] = ['entwurf', 'erstellt', 'bestellt', 'te
// ── Empty form data ──
const emptyOrderForm: BestellungFormData = { bezeichnung: '', lieferant_id: undefined, besteller_id: '', budget: undefined, notizen: '', positionen: [] };
const emptyVendorForm: LieferantFormData = { name: '', kontakt_name: '', email: '', telefon: '', adresse: '', website: '', notizen: '' };
const emptyPosition: BestellpositionFormData = { bezeichnung: '', menge: 1, einheit: 'Stk' };
// ══════════════════════════════════════════════════════════════════════════════
// Component
@@ -88,10 +85,6 @@ export default function Bestellungen() {
// ── State ──
const [statusFilter, setStatusFilter] = useState<string>('');
const [orderDialogOpen, setOrderDialogOpen] = useState(false);
const [orderForm, setOrderForm] = useState<BestellungFormData>({ ...emptyOrderForm });
const [inlineVendorOpen, setInlineVendorOpen] = useState(false);
const [inlineVendorForm, setInlineVendorForm] = useState<LieferantFormData>({ ...emptyVendorForm });
const [vendorDialogOpen, setVendorDialogOpen] = useState(false);
const [vendorForm, setVendorForm] = useState<LieferantFormData>({ ...emptyVendorForm });
@@ -110,37 +103,13 @@ export default function Bestellungen() {
queryFn: bestellungApi.getVendors,
});
const { data: orderUsers = [] } = useQuery({
queryKey: ['bestellungen', 'order-users'],
queryFn: bestellungApi.getOrderUsers,
});
// ── Mutations ──
const createOrder = useMutation({
mutationFn: (data: BestellungFormData) => bestellungApi.createOrder(data),
onSuccess: (created) => {
queryClient.invalidateQueries({ queryKey: ['bestellungen'] });
showSuccess('Bestellung erstellt');
setOrderDialogOpen(false);
setOrderForm({ ...emptyOrderForm });
navigate(`/bestellungen/${created.id}`);
},
onError: (error: any) => showError(error?.response?.data?.message || 'Fehler beim Erstellen der Bestellung'),
});
const createVendor = useMutation({
mutationFn: (data: LieferantFormData) => bestellungApi.createVendor(data),
onSuccess: (newVendor) => {
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['lieferanten'] });
showSuccess('Lieferant erstellt');
// If inline vendor creation during order creation, auto-select the new vendor
if (inlineVendorOpen) {
setOrderForm((f) => ({ ...f, lieferant_id: newVendor.id }));
setInlineVendorOpen(false);
setInlineVendorForm({ ...emptyVendorForm });
} else {
closeVendorDialog();
}
closeVendorDialog();
},
onError: () => showError('Fehler beim Erstellen des Lieferanten'),
});
@@ -188,11 +157,6 @@ export default function Bestellungen() {
}
}
function handleOrderSave() {
if (!orderForm.bezeichnung.trim()) return;
createOrder.mutate(orderForm);
}
// ── Render ──
return (
@@ -271,7 +235,7 @@ export default function Bestellungen() {
</TableContainer>
{hasPermission('bestellungen:create') && (
<ChatAwareFab onClick={() => setOrderDialogOpen(true)} aria-label="Neue Bestellung">
<ChatAwareFab onClick={() => navigate('/bestellungen/neu')} aria-label="Neue Bestellung">
<AddIcon />
</ChatAwareFab>
)}
@@ -330,135 +294,6 @@ export default function Bestellungen() {
)}
</TabPanel>
{/* ── Create Order Dialog ── */}
<Dialog open={orderDialogOpen} onClose={() => setOrderDialogOpen(false)} maxWidth="md" fullWidth>
<DialogTitle>Neue Bestellung</DialogTitle>
<DialogContent sx={{ display: 'flex', flexDirection: 'column', gap: 2, pt: '16px !important' }}>
<TextField
label="Bezeichnung"
required
InputLabelProps={{ shrink: true }}
value={orderForm.bezeichnung}
onChange={(e) => setOrderForm((f) => ({ ...f, bezeichnung: e.target.value }))}
/>
<Box sx={{ display: 'flex', gap: 1, alignItems: 'flex-start' }}>
<Autocomplete
options={vendors}
getOptionLabel={(o) => o.name}
value={vendors.find((v) => v.id === orderForm.lieferant_id) || null}
onChange={(_e, v) => setOrderForm((f) => ({ ...f, lieferant_id: v?.id }))}
renderInput={(params) => <TextField {...params} label="Lieferant" />}
sx={{ flexGrow: 1 }}
/>
<Tooltip title="Neuen Lieferant anlegen">
<IconButton
onClick={() => setInlineVendorOpen(!inlineVendorOpen)}
color={inlineVendorOpen ? 'primary' : 'default'}
sx={{ mt: 1 }}
>
<AddIcon />
</IconButton>
</Tooltip>
</Box>
{inlineVendorOpen && (
<Paper variant="outlined" sx={{ p: 2, display: 'flex', flexDirection: 'column', gap: 1.5 }}>
<Typography variant="subtitle2">Neuer Lieferant</Typography>
<TextField size="small" label="Name *" value={inlineVendorForm.name} onChange={(e) => setInlineVendorForm(f => ({ ...f, name: e.target.value }))} />
<TextField size="small" label="Kontakt-Name" value={inlineVendorForm.kontakt_name || ''} onChange={(e) => setInlineVendorForm(f => ({ ...f, kontakt_name: e.target.value }))} />
<TextField size="small" label="E-Mail" value={inlineVendorForm.email || ''} onChange={(e) => setInlineVendorForm(f => ({ ...f, email: e.target.value }))} />
<TextField size="small" label="Telefon" value={inlineVendorForm.telefon || ''} onChange={(e) => setInlineVendorForm(f => ({ ...f, telefon: e.target.value }))} />
<Box sx={{ display: 'flex', gap: 1, justifyContent: 'flex-end' }}>
<Button size="small" onClick={() => { setInlineVendorOpen(false); setInlineVendorForm({ ...emptyVendorForm }); }}>Abbrechen</Button>
<Button size="small" variant="contained" onClick={() => createVendor.mutate(inlineVendorForm)} disabled={!inlineVendorForm.name.trim() || createVendor.isPending}>Anlegen</Button>
</Box>
</Paper>
)}
<Autocomplete
options={orderUsers}
getOptionLabel={(o) => o.name}
value={orderUsers.find((u) => u.id === orderForm.besteller_id) || null}
onChange={(_e, v) => setOrderForm((f) => ({ ...f, besteller_id: v?.id || '' }))}
renderInput={(params) => <TextField {...params} label="Besteller" />}
/>
<TextField
label="Notizen"
multiline
rows={3}
value={orderForm.notizen || ''}
onChange={(e) => setOrderForm((f) => ({ ...f, notizen: e.target.value }))}
/>
{/* ── Dynamic Items List ── */}
<Typography variant="subtitle2" sx={{ mt: 1 }}>Positionen</Typography>
{(orderForm.positionen || []).map((pos, idx) => (
<Box key={idx} sx={{ display: 'flex', gap: 1, alignItems: 'center' }}>
<TextField
label="Bezeichnung"
size="small"
required
InputLabelProps={{ shrink: true }}
value={pos.bezeichnung}
onChange={(e) => {
const next = [...(orderForm.positionen || [])];
next[idx] = { ...next[idx], bezeichnung: e.target.value };
setOrderForm((f) => ({ ...f, positionen: next }));
}}
sx={{ flexGrow: 1 }}
/>
<TextField
label="Menge"
size="small"
type="number"
InputLabelProps={{ shrink: true }}
value={pos.menge}
onChange={(e) => {
const next = [...(orderForm.positionen || [])];
next[idx] = { ...next[idx], menge: Math.max(1, Number(e.target.value) || 1) };
setOrderForm((f) => ({ ...f, positionen: next }));
}}
sx={{ width: 90 }}
inputProps={{ min: 1 }}
/>
<TextField
label="Einheit"
size="small"
InputLabelProps={{ shrink: true }}
value={pos.einheit || 'Stk'}
onChange={(e) => {
const next = [...(orderForm.positionen || [])];
next[idx] = { ...next[idx], einheit: e.target.value };
setOrderForm((f) => ({ ...f, positionen: next }));
}}
sx={{ width: 100 }}
/>
<IconButton
size="small"
color="error"
onClick={() => {
const next = (orderForm.positionen || []).filter((_, i) => i !== idx);
setOrderForm((f) => ({ ...f, positionen: next }));
}}
>
<RemoveIcon />
</IconButton>
</Box>
))}
<Button
size="small"
startIcon={<AddIcon />}
onClick={() => setOrderForm((f) => ({ ...f, positionen: [...(f.positionen || []), { ...emptyPosition }] }))}
>
Position hinzufügen
</Button>
</DialogContent>
<DialogActions>
<Button onClick={() => setOrderDialogOpen(false)}>Abbrechen</Button>
<Button variant="contained" onClick={handleOrderSave} disabled={!orderForm.bezeichnung.trim() || createOrder.isPending}>
Erstellen
</Button>
</DialogActions>
</Dialog>
{/* ── Create/Edit Vendor Dialog ── */}
<Dialog open={vendorDialogOpen} onClose={closeVendorDialog} maxWidth="sm" fullWidth>
<DialogTitle>{editingVendor ? 'Lieferant bearbeiten' : 'Neuer Lieferant'}</DialogTitle>