rework from modal to page
This commit is contained in:
232
frontend/src/pages/BestellungNeu.tsx
Normal file
232
frontend/src/pages/BestellungNeu.tsx
Normal file
@@ -0,0 +1,232 @@
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Typography,
|
||||
Paper,
|
||||
Button,
|
||||
TextField,
|
||||
IconButton,
|
||||
Autocomplete,
|
||||
Tooltip,
|
||||
} from '@mui/material';
|
||||
import {
|
||||
ArrowBack,
|
||||
Add as AddIcon,
|
||||
RemoveCircleOutline as RemoveIcon,
|
||||
} from '@mui/icons-material';
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import DashboardLayout from '../components/dashboard/DashboardLayout';
|
||||
import { useNotification } from '../contexts/NotificationContext';
|
||||
import { bestellungApi } from '../services/bestellung';
|
||||
import type { BestellungFormData, BestellpositionFormData, LieferantFormData, Lieferant } from '../types/bestellung.types';
|
||||
|
||||
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' };
|
||||
|
||||
export default function BestellungNeu() {
|
||||
const navigate = useNavigate();
|
||||
const queryClient = useQueryClient();
|
||||
const { showSuccess, showError } = useNotification();
|
||||
|
||||
const [orderForm, setOrderForm] = useState<BestellungFormData>({ ...emptyOrderForm });
|
||||
const [inlineVendorOpen, setInlineVendorOpen] = useState(false);
|
||||
const [inlineVendorForm, setInlineVendorForm] = useState<LieferantFormData>({ ...emptyVendorForm });
|
||||
|
||||
// ── Queries ──
|
||||
const { data: vendors = [] } = useQuery({
|
||||
queryKey: ['lieferanten'],
|
||||
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');
|
||||
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: Lieferant) => {
|
||||
queryClient.invalidateQueries({ queryKey: ['lieferanten'] });
|
||||
showSuccess('Lieferant erstellt');
|
||||
setOrderForm((f) => ({ ...f, lieferant_id: newVendor.id }));
|
||||
setInlineVendorOpen(false);
|
||||
setInlineVendorForm({ ...emptyVendorForm });
|
||||
},
|
||||
onError: () => showError('Fehler beim Erstellen des Lieferanten'),
|
||||
});
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (!orderForm.bezeichnung.trim()) return;
|
||||
createOrder.mutate(orderForm);
|
||||
};
|
||||
|
||||
return (
|
||||
<DashboardLayout>
|
||||
{/* ── Header ── */}
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 3 }}>
|
||||
<IconButton onClick={() => navigate('/bestellungen')}>
|
||||
<ArrowBack />
|
||||
</IconButton>
|
||||
<Typography variant="h5" fontWeight={700}>Neue Bestellung</Typography>
|
||||
</Box>
|
||||
|
||||
{/* ── Form ── */}
|
||||
<Paper sx={{ p: 3, display: 'flex', flexDirection: 'column', gap: 2.5 }}>
|
||||
<TextField
|
||||
label="Bezeichnung"
|
||||
required
|
||||
InputLabelProps={{ shrink: true }}
|
||||
value={orderForm.bezeichnung}
|
||||
onChange={(e) => setOrderForm((f) => ({ ...f, bezeichnung: e.target.value }))}
|
||||
/>
|
||||
|
||||
{/* Lieferant + inline create */}
|
||||
<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="Budget"
|
||||
type="number"
|
||||
InputLabelProps={{ shrink: true }}
|
||||
value={orderForm.budget ?? ''}
|
||||
onChange={(e) => setOrderForm((f) => ({ ...f, budget: e.target.value ? Number(e.target.value) : undefined }))}
|
||||
inputProps={{ min: 0, step: 0.01 }}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
label="Notizen"
|
||||
multiline
|
||||
rows={3}
|
||||
value={orderForm.notizen || ''}
|
||||
onChange={(e) => setOrderForm((f) => ({ ...f, notizen: e.target.value }))}
|
||||
/>
|
||||
|
||||
{/* ── Positionen ── */}
|
||||
<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>
|
||||
|
||||
{/* ── Submit ── */}
|
||||
<Box sx={{ display: 'flex', justifyContent: 'flex-end', gap: 1, mt: 2 }}>
|
||||
<Button onClick={() => navigate('/bestellungen')}>Abbrechen</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={handleSubmit}
|
||||
disabled={!orderForm.bezeichnung.trim() || createOrder.isPending}
|
||||
>
|
||||
Erstellen
|
||||
</Button>
|
||||
</Box>
|
||||
</Paper>
|
||||
</DashboardLayout>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user