435 lines
17 KiB
TypeScript
435 lines
17 KiB
TypeScript
import { useState, useEffect } from 'react';
|
||
import {
|
||
Box,
|
||
Tab,
|
||
Tabs,
|
||
Typography,
|
||
Table,
|
||
TableBody,
|
||
TableCell,
|
||
TableContainer,
|
||
TableHead,
|
||
TableRow,
|
||
Paper,
|
||
Chip,
|
||
IconButton,
|
||
Button,
|
||
Dialog,
|
||
DialogTitle,
|
||
DialogContent,
|
||
DialogActions,
|
||
TextField,
|
||
MenuItem,
|
||
Select,
|
||
FormControl,
|
||
InputLabel,
|
||
Tooltip,
|
||
Autocomplete,
|
||
} from '@mui/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';
|
||
import ChatAwareFab from '../components/shared/ChatAwareFab';
|
||
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, LieferantFormData, Lieferant } from '../types/bestellung.types';
|
||
|
||
// ── Helpers ──
|
||
|
||
const formatCurrency = (value?: number) =>
|
||
value != null ? new Intl.NumberFormat('de-AT', { style: 'currency', currency: 'EUR' }).format(value) : '–';
|
||
|
||
const formatDate = (iso?: string) =>
|
||
iso ? new Date(iso).toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' }) : '–';
|
||
|
||
// ── Tab Panel ──
|
||
|
||
interface TabPanelProps { children: React.ReactNode; index: number; value: number }
|
||
function TabPanel({ children, value, index }: TabPanelProps) {
|
||
if (value !== index) return null;
|
||
return <Box sx={{ pt: 3 }}>{children}</Box>;
|
||
}
|
||
|
||
const TAB_COUNT = 2;
|
||
|
||
// ── Status options for filter ──
|
||
|
||
const ALL_STATUSES: BestellungStatus[] = ['entwurf', 'erstellt', 'bestellt', 'teillieferung', 'vollstaendig', 'abgeschlossen'];
|
||
|
||
// ── Empty form data ──
|
||
|
||
const emptyOrderForm: BestellungFormData = { bezeichnung: '', lieferant_id: undefined, besteller_id: '', budget: undefined, notizen: '' };
|
||
const emptyVendorForm: LieferantFormData = { name: '', kontakt_name: '', email: '', telefon: '', adresse: '', website: '', notizen: '' };
|
||
|
||
// ══════════════════════════════════════════════════════════════════════════════
|
||
// Component
|
||
// ══════════════════════════════════════════════════════════════════════════════
|
||
|
||
export default function Bestellungen() {
|
||
const navigate = useNavigate();
|
||
const [searchParams] = useSearchParams();
|
||
const queryClient = useQueryClient();
|
||
const { showSuccess, showError } = useNotification();
|
||
const { hasPermission } = usePermissionContext();
|
||
|
||
// Tab from URL
|
||
const [tab, setTab] = useState(() => {
|
||
const t = Number(searchParams.get('tab'));
|
||
return t >= 0 && t < TAB_COUNT ? t : 0;
|
||
});
|
||
useEffect(() => {
|
||
const t = Number(searchParams.get('tab'));
|
||
if (t >= 0 && t < TAB_COUNT) setTab(t);
|
||
}, [searchParams]);
|
||
|
||
// ── State ──
|
||
const [statusFilter, setStatusFilter] = useState<string>('');
|
||
const [orderDialogOpen, setOrderDialogOpen] = useState(false);
|
||
const [orderForm, setOrderForm] = useState<BestellungFormData>({ ...emptyOrderForm });
|
||
|
||
const [vendorDialogOpen, setVendorDialogOpen] = useState(false);
|
||
const [vendorForm, setVendorForm] = useState<LieferantFormData>({ ...emptyVendorForm });
|
||
const [editingVendor, setEditingVendor] = useState<Lieferant | null>(null);
|
||
|
||
const [deleteVendorTarget, setDeleteVendorTarget] = useState<Lieferant | null>(null);
|
||
|
||
// ── Queries ──
|
||
const { data: orders = [], isLoading: ordersLoading } = useQuery({
|
||
queryKey: ['bestellungen', statusFilter],
|
||
queryFn: () => bestellungApi.getOrders(statusFilter ? { status: statusFilter } : undefined),
|
||
});
|
||
|
||
const { data: vendors = [], isLoading: vendorsLoading } = useQuery({
|
||
queryKey: ['lieferanten'],
|
||
queryFn: bestellungApi.getVendors,
|
||
});
|
||
|
||
// ── 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: () => showError('Fehler beim Erstellen der Bestellung'),
|
||
});
|
||
|
||
const createVendor = useMutation({
|
||
mutationFn: (data: LieferantFormData) => bestellungApi.createVendor(data),
|
||
onSuccess: () => {
|
||
queryClient.invalidateQueries({ queryKey: ['lieferanten'] });
|
||
showSuccess('Lieferant erstellt');
|
||
closeVendorDialog();
|
||
},
|
||
onError: () => showError('Fehler beim Erstellen des Lieferanten'),
|
||
});
|
||
|
||
const updateVendor = useMutation({
|
||
mutationFn: ({ id, data }: { id: number; data: LieferantFormData }) => bestellungApi.updateVendor(id, data),
|
||
onSuccess: () => {
|
||
queryClient.invalidateQueries({ queryKey: ['lieferanten'] });
|
||
showSuccess('Lieferant aktualisiert');
|
||
closeVendorDialog();
|
||
},
|
||
onError: () => showError('Fehler beim Aktualisieren des Lieferanten'),
|
||
});
|
||
|
||
const deleteVendor = useMutation({
|
||
mutationFn: (id: number) => bestellungApi.deleteVendor(id),
|
||
onSuccess: () => {
|
||
queryClient.invalidateQueries({ queryKey: ['lieferanten'] });
|
||
showSuccess('Lieferant gelöscht');
|
||
setDeleteVendorTarget(null);
|
||
},
|
||
onError: () => showError('Fehler beim Löschen des Lieferanten'),
|
||
});
|
||
|
||
// ── Dialog helpers ──
|
||
|
||
function openEditVendor(v: Lieferant) {
|
||
setEditingVendor(v);
|
||
setVendorForm({ name: v.name, kontakt_name: v.kontakt_name || '', email: v.email || '', telefon: v.telefon || '', adresse: v.adresse || '', website: v.website || '', notizen: v.notizen || '' });
|
||
setVendorDialogOpen(true);
|
||
}
|
||
|
||
function closeVendorDialog() {
|
||
setVendorDialogOpen(false);
|
||
setEditingVendor(null);
|
||
setVendorForm({ ...emptyVendorForm });
|
||
}
|
||
|
||
function handleVendorSave() {
|
||
if (!vendorForm.name.trim()) return;
|
||
if (editingVendor) {
|
||
updateVendor.mutate({ id: editingVendor.id, data: vendorForm });
|
||
} else {
|
||
createVendor.mutate(vendorForm);
|
||
}
|
||
}
|
||
|
||
function handleOrderSave() {
|
||
if (!orderForm.bezeichnung.trim()) return;
|
||
createOrder.mutate(orderForm);
|
||
}
|
||
|
||
// ── Render ──
|
||
|
||
return (
|
||
<DashboardLayout>
|
||
<Typography variant="h4" sx={{ mb: 3 }}>Bestellungen</Typography>
|
||
|
||
<Box sx={{ borderBottom: 1, borderColor: 'divider' }}>
|
||
<Tabs value={tab} onChange={(_e, v) => { setTab(v); navigate(`/bestellungen?tab=${v}`, { replace: true }); }}>
|
||
<Tab label="Bestellungen" />
|
||
<Tab label="Lieferanten" />
|
||
</Tabs>
|
||
</Box>
|
||
|
||
{/* ── Tab 0: Orders ── */}
|
||
<TabPanel value={tab} index={0}>
|
||
<Box sx={{ mb: 2, display: 'flex', gap: 2, alignItems: 'center' }}>
|
||
<FormControl size="small" sx={{ minWidth: 200 }}>
|
||
<InputLabel>Status Filter</InputLabel>
|
||
<Select
|
||
value={statusFilter}
|
||
label="Status Filter"
|
||
onChange={(e) => setStatusFilter(e.target.value)}
|
||
>
|
||
<MenuItem value="">Alle</MenuItem>
|
||
{ALL_STATUSES.map((s) => (
|
||
<MenuItem key={s} value={s}>{BESTELLUNG_STATUS_LABELS[s]}</MenuItem>
|
||
))}
|
||
</Select>
|
||
</FormControl>
|
||
</Box>
|
||
|
||
<TableContainer component={Paper}>
|
||
<Table size="small">
|
||
<TableHead>
|
||
<TableRow>
|
||
<TableCell>Bezeichnung</TableCell>
|
||
<TableCell>Lieferant</TableCell>
|
||
<TableCell>Besteller</TableCell>
|
||
<TableCell>Status</TableCell>
|
||
<TableCell align="right">Positionen</TableCell>
|
||
<TableCell align="right">Gesamtpreis</TableCell>
|
||
<TableCell>Erstellt am</TableCell>
|
||
</TableRow>
|
||
</TableHead>
|
||
<TableBody>
|
||
{ordersLoading ? (
|
||
<TableRow><TableCell colSpan={7} align="center">Laden...</TableCell></TableRow>
|
||
) : orders.length === 0 ? (
|
||
<TableRow><TableCell colSpan={7} align="center">Keine Bestellungen vorhanden</TableCell></TableRow>
|
||
) : (
|
||
orders.map((o) => (
|
||
<TableRow
|
||
key={o.id}
|
||
hover
|
||
sx={{ cursor: 'pointer' }}
|
||
onClick={() => navigate(`/bestellungen/${o.id}`)}
|
||
>
|
||
<TableCell>{o.bezeichnung}</TableCell>
|
||
<TableCell>{o.lieferant_name || '–'}</TableCell>
|
||
<TableCell>{o.besteller_name || '–'}</TableCell>
|
||
<TableCell>
|
||
<Chip
|
||
label={BESTELLUNG_STATUS_LABELS[o.status]}
|
||
color={BESTELLUNG_STATUS_COLORS[o.status]}
|
||
size="small"
|
||
/>
|
||
</TableCell>
|
||
<TableCell align="right">{o.items_count ?? 0}</TableCell>
|
||
<TableCell align="right">{formatCurrency(o.total_cost)}</TableCell>
|
||
<TableCell>{formatDate(o.erstellt_am)}</TableCell>
|
||
</TableRow>
|
||
))
|
||
)}
|
||
</TableBody>
|
||
</Table>
|
||
</TableContainer>
|
||
|
||
{hasPermission('bestellungen:create') && (
|
||
<ChatAwareFab onClick={() => setOrderDialogOpen(true)} aria-label="Neue Bestellung">
|
||
<AddIcon />
|
||
</ChatAwareFab>
|
||
)}
|
||
</TabPanel>
|
||
|
||
{/* ── Tab 1: Vendors ── */}
|
||
<TabPanel value={tab} index={1}>
|
||
<Box sx={{ mb: 2, display: 'flex', justifyContent: 'flex-end' }}>
|
||
{hasPermission('bestellungen:create') && (
|
||
<Button variant="contained" startIcon={<AddIcon />} onClick={() => setVendorDialogOpen(true)}>
|
||
Lieferant hinzufügen
|
||
</Button>
|
||
)}
|
||
</Box>
|
||
|
||
<TableContainer component={Paper}>
|
||
<Table size="small">
|
||
<TableHead>
|
||
<TableRow>
|
||
<TableCell>Name</TableCell>
|
||
<TableCell>Kontakt</TableCell>
|
||
<TableCell>E-Mail</TableCell>
|
||
<TableCell>Telefon</TableCell>
|
||
<TableCell>Website</TableCell>
|
||
<TableCell align="right">Aktionen</TableCell>
|
||
</TableRow>
|
||
</TableHead>
|
||
<TableBody>
|
||
{vendorsLoading ? (
|
||
<TableRow><TableCell colSpan={6} align="center">Laden...</TableCell></TableRow>
|
||
) : vendors.length === 0 ? (
|
||
<TableRow><TableCell colSpan={6} align="center">Keine Lieferanten vorhanden</TableCell></TableRow>
|
||
) : (
|
||
vendors.map((v) => (
|
||
<TableRow key={v.id}>
|
||
<TableCell>{v.name}</TableCell>
|
||
<TableCell>{v.kontakt_name || '–'}</TableCell>
|
||
<TableCell>{v.email ? <a href={`mailto:${v.email}`}>{v.email}</a> : '–'}</TableCell>
|
||
<TableCell>{v.telefon || '–'}</TableCell>
|
||
<TableCell>
|
||
{v.website ? (
|
||
<a href={v.website} target="_blank" rel="noopener noreferrer">{v.website}</a>
|
||
) : '–'}
|
||
</TableCell>
|
||
<TableCell align="right">
|
||
<Tooltip title="Bearbeiten">
|
||
<IconButton size="small" onClick={() => openEditVendor(v)}><EditIcon fontSize="small" /></IconButton>
|
||
</Tooltip>
|
||
<Tooltip title="Löschen">
|
||
<IconButton size="small" color="error" onClick={() => setDeleteVendorTarget(v)}><DeleteIcon fontSize="small" /></IconButton>
|
||
</Tooltip>
|
||
</TableCell>
|
||
</TableRow>
|
||
))
|
||
)}
|
||
</TableBody>
|
||
</Table>
|
||
</TableContainer>
|
||
</TabPanel>
|
||
|
||
{/* ── Create Order Dialog ── */}
|
||
<Dialog open={orderDialogOpen} onClose={() => setOrderDialogOpen(false)} maxWidth="sm" fullWidth>
|
||
<DialogTitle>Neue Bestellung</DialogTitle>
|
||
<DialogContent sx={{ display: 'flex', flexDirection: 'column', gap: 2, pt: '16px !important' }}>
|
||
<TextField
|
||
label="Bezeichnung"
|
||
required
|
||
value={orderForm.bezeichnung}
|
||
onChange={(e) => setOrderForm((f) => ({ ...f, bezeichnung: e.target.value }))}
|
||
/>
|
||
<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" />}
|
||
/>
|
||
<TextField
|
||
label="Besteller"
|
||
value={orderForm.besteller_id || ''}
|
||
onChange={(e) => setOrderForm((f) => ({ ...f, besteller_id: e.target.value }))}
|
||
/>
|
||
<TextField
|
||
label="Budget"
|
||
type="number"
|
||
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 }))}
|
||
/>
|
||
</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>
|
||
<DialogContent sx={{ display: 'flex', flexDirection: 'column', gap: 2, pt: '16px !important' }}>
|
||
<TextField
|
||
label="Name"
|
||
required
|
||
value={vendorForm.name}
|
||
onChange={(e) => setVendorForm((f) => ({ ...f, name: e.target.value }))}
|
||
/>
|
||
<TextField
|
||
label="Kontakt-Name"
|
||
value={vendorForm.kontakt_name || ''}
|
||
onChange={(e) => setVendorForm((f) => ({ ...f, kontakt_name: e.target.value }))}
|
||
/>
|
||
<TextField
|
||
label="E-Mail"
|
||
type="email"
|
||
value={vendorForm.email || ''}
|
||
onChange={(e) => setVendorForm((f) => ({ ...f, email: e.target.value }))}
|
||
/>
|
||
<TextField
|
||
label="Telefon"
|
||
value={vendorForm.telefon || ''}
|
||
onChange={(e) => setVendorForm((f) => ({ ...f, telefon: e.target.value }))}
|
||
/>
|
||
<TextField
|
||
label="Adresse"
|
||
value={vendorForm.adresse || ''}
|
||
onChange={(e) => setVendorForm((f) => ({ ...f, adresse: e.target.value }))}
|
||
/>
|
||
<TextField
|
||
label="Website"
|
||
value={vendorForm.website || ''}
|
||
onChange={(e) => setVendorForm((f) => ({ ...f, website: e.target.value }))}
|
||
/>
|
||
<TextField
|
||
label="Notizen"
|
||
multiline
|
||
rows={3}
|
||
value={vendorForm.notizen || ''}
|
||
onChange={(e) => setVendorForm((f) => ({ ...f, notizen: e.target.value }))}
|
||
/>
|
||
</DialogContent>
|
||
<DialogActions>
|
||
<Button onClick={closeVendorDialog}>Abbrechen</Button>
|
||
<Button variant="contained" onClick={handleVendorSave} disabled={!vendorForm.name.trim() || createVendor.isPending || updateVendor.isPending}>
|
||
{editingVendor ? 'Speichern' : 'Erstellen'}
|
||
</Button>
|
||
</DialogActions>
|
||
</Dialog>
|
||
|
||
{/* ── Delete Vendor Confirm ── */}
|
||
<Dialog open={!!deleteVendorTarget} onClose={() => setDeleteVendorTarget(null)}>
|
||
<DialogTitle>Lieferant löschen</DialogTitle>
|
||
<DialogContent>
|
||
<Typography>
|
||
Soll der Lieferant <strong>{deleteVendorTarget?.name}</strong> wirklich gelöscht werden?
|
||
</Typography>
|
||
</DialogContent>
|
||
<DialogActions>
|
||
<Button onClick={() => setDeleteVendorTarget(null)}>Abbrechen</Button>
|
||
<Button color="error" variant="contained" onClick={() => deleteVendorTarget && deleteVendor.mutate(deleteVendorTarget.id)} disabled={deleteVendor.isPending}>
|
||
Löschen
|
||
</Button>
|
||
</DialogActions>
|
||
</Dialog>
|
||
</DashboardLayout>
|
||
);
|
||
}
|