Files
dashboard/frontend/src/pages/LieferantDetail.tsx
2026-03-27 17:27:55 +01:00

321 lines
11 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, useEffect } from 'react';
import {
Box,
Typography,
Paper,
Button,
TextField,
IconButton,
Dialog,
DialogTitle,
DialogContent,
DialogActions,
Grid,
Card,
CardContent,
Skeleton,
} from '@mui/material';
import { ArrowBack, Edit as EditIcon, Delete as DeleteIcon, Save as SaveIcon, Close as CloseIcon } from '@mui/icons-material';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { useParams, useNavigate } from 'react-router-dom';
import DashboardLayout from '../components/dashboard/DashboardLayout';
import { useNotification } from '../contexts/NotificationContext';
import { usePermissionContext } from '../contexts/PermissionContext';
import { bestellungApi } from '../services/bestellung';
import type { LieferantFormData } from '../types/bestellung.types';
const emptyForm: LieferantFormData = { name: '', kontakt_name: '', email: '', telefon: '', adresse: '', website: '', notizen: '' };
function ensureUrl(url: string): string {
if (!url) return url;
if (url.startsWith('http://') || url.startsWith('https://')) return url;
return `https://${url}`;
}
export default function LieferantDetail() {
const { id } = useParams<{ id: string }>();
const navigate = useNavigate();
const queryClient = useQueryClient();
const { showSuccess, showError } = useNotification();
const { hasPermission } = usePermissionContext();
const isNew = id === 'neu';
const vendorId = isNew ? 0 : Number(id);
const canManage = hasPermission('bestellungen:manage_vendors');
const [editMode, setEditMode] = useState(isNew);
const [form, setForm] = useState<LieferantFormData>({ ...emptyForm });
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
// Sync edit mode when navigating between /neu and /:id within the same component instance
useEffect(() => {
if (isNew) {
setEditMode(true);
setForm({ ...emptyForm });
}
}, [isNew]);
// ── Query ──
const { data: vendor, isPending, isLoading, isError } = useQuery({
queryKey: ['lieferant', vendorId],
queryFn: () => bestellungApi.getVendor(vendorId),
enabled: !isNew && !!vendorId,
});
// Sync form with loaded vendor data
useEffect(() => {
if (vendor) {
setForm({
name: vendor.name,
kontakt_name: vendor.kontakt_name || '',
email: vendor.email || '',
telefon: vendor.telefon || '',
adresse: vendor.adresse || '',
website: vendor.website || '',
notizen: vendor.notizen || '',
});
}
}, [vendor]);
// ── Mutations ──
const createVendor = useMutation({
mutationFn: (data: LieferantFormData) => bestellungApi.createVendor(data),
onSuccess: (created) => {
queryClient.invalidateQueries({ queryKey: ['lieferanten'] });
showSuccess('Lieferant erstellt');
navigate(`/bestellungen/lieferanten/${created.id}`, { replace: true });
},
onError: () => showError('Fehler beim Erstellen des Lieferanten'),
});
const updateVendor = useMutation({
mutationFn: (data: LieferantFormData) => bestellungApi.updateVendor(vendorId, data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['lieferant', vendorId] });
queryClient.invalidateQueries({ queryKey: ['lieferanten'] });
showSuccess('Lieferant aktualisiert');
setEditMode(false);
},
onError: () => showError('Fehler beim Aktualisieren des Lieferanten'),
});
const deleteVendor = useMutation({
mutationFn: () => bestellungApi.deleteVendor(vendorId),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['lieferanten'] });
showSuccess('Lieferant gelöscht');
navigate('/bestellungen?tab=1');
},
onError: () => showError('Fehler beim Löschen des Lieferanten'),
});
function handleSave() {
if (!form.name.trim()) return;
if (isNew) {
createVendor.mutate(form);
} else {
updateVendor.mutate(form);
}
}
function handleCancel() {
if (isNew) {
navigate('/bestellungen?tab=1');
} else if (vendor) {
setForm({
name: vendor.name,
kontakt_name: vendor.kontakt_name || '',
email: vendor.email || '',
telefon: vendor.telefon || '',
adresse: vendor.adresse || '',
website: vendor.website || '',
notizen: vendor.notizen || '',
});
setEditMode(false);
}
}
// ── Loading / Error ──
if (!isNew && isPending) {
return (
<DashboardLayout>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mb: 3 }}>
<IconButton onClick={() => navigate('/bestellungen?tab=1')}><ArrowBack /></IconButton>
<Skeleton width={300} height={40} />
</Box>
<Paper sx={{ p: 3 }}>
<Skeleton height={40} />
<Skeleton height={40} />
<Skeleton height={40} />
</Paper>
</DashboardLayout>
);
}
if (!isNew && !isPending && (isError || !vendor)) {
return (
<DashboardLayout>
<Box sx={{ p: 4, textAlign: 'center' }}>
<Typography color="error">Lieferant nicht gefunden.</Typography>
<Button sx={{ mt: 2 }} onClick={() => navigate('/bestellungen?tab=1')}>Zurück</Button>
</Box>
</DashboardLayout>
);
}
const isSaving = createVendor.isPending || updateVendor.isPending;
return (
<DashboardLayout>
{/* ── Header ── */}
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mb: 3 }}>
<IconButton onClick={() => navigate('/bestellungen?tab=1')}>
<ArrowBack />
</IconButton>
<Typography variant="h4" sx={{ flexGrow: 1 }}>
{isNew ? 'Neuer Lieferant' : vendor!.name}
</Typography>
{!isNew && canManage && !editMode && (
<>
<Button startIcon={<EditIcon />} onClick={() => setEditMode(true)}>
Bearbeiten
</Button>
<Button startIcon={<DeleteIcon />} color="error" onClick={() => setDeleteDialogOpen(true)}>
Löschen
</Button>
</>
)}
{editMode && (
<>
<Button
variant="contained"
startIcon={<SaveIcon />}
onClick={handleSave}
disabled={!form.name.trim() || isSaving}
>
Speichern
</Button>
<Button startIcon={<CloseIcon />} onClick={handleCancel}>
Abbrechen
</Button>
</>
)}
</Box>
{/* ── Content ── */}
{editMode ? (
<Paper sx={{ p: 3 }}>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
<TextField
label="Name"
required
value={form.name}
onChange={(e) => setForm((f) => ({ ...f, name: e.target.value }))}
/>
<TextField
label="Kontakt-Name"
value={form.kontakt_name || ''}
onChange={(e) => setForm((f) => ({ ...f, kontakt_name: e.target.value }))}
/>
<TextField
label="E-Mail"
type="email"
value={form.email || ''}
onChange={(e) => setForm((f) => ({ ...f, email: e.target.value }))}
/>
<TextField
label="Telefon"
value={form.telefon || ''}
onChange={(e) => setForm((f) => ({ ...f, telefon: e.target.value }))}
/>
<TextField
label="Adresse"
value={form.adresse || ''}
onChange={(e) => setForm((f) => ({ ...f, adresse: e.target.value }))}
/>
<TextField
label="Website"
value={form.website || ''}
onChange={(e) => setForm((f) => ({ ...f, website: e.target.value }))}
/>
<TextField
label="Notizen"
multiline
rows={4}
value={form.notizen || ''}
onChange={(e) => setForm((f) => ({ ...f, notizen: e.target.value }))}
/>
</Box>
</Paper>
) : (
<Grid container spacing={2}>
<Grid item xs={12} sm={6} md={4}>
<Card variant="outlined"><CardContent>
<Typography variant="caption" color="text.secondary">Name</Typography>
<Typography>{vendor!.name}</Typography>
</CardContent></Card>
</Grid>
<Grid item xs={12} sm={6} md={4}>
<Card variant="outlined"><CardContent>
<Typography variant="caption" color="text.secondary">Kontakt</Typography>
<Typography>{vendor!.kontakt_name || ''}</Typography>
</CardContent></Card>
</Grid>
<Grid item xs={12} sm={6} md={4}>
<Card variant="outlined"><CardContent>
<Typography variant="caption" color="text.secondary">E-Mail</Typography>
<Typography>
{vendor!.email ? <a href={`mailto:${vendor!.email}`}>{vendor!.email}</a> : ''}
</Typography>
</CardContent></Card>
</Grid>
<Grid item xs={12} sm={6} md={4}>
<Card variant="outlined"><CardContent>
<Typography variant="caption" color="text.secondary">Telefon</Typography>
<Typography>{vendor!.telefon || ''}</Typography>
</CardContent></Card>
</Grid>
<Grid item xs={12} sm={6} md={4}>
<Card variant="outlined"><CardContent>
<Typography variant="caption" color="text.secondary">Website</Typography>
<Typography>
{vendor!.website ? <a href={ensureUrl(vendor!.website)} target="_blank" rel="noopener noreferrer">{vendor!.website}</a> : ''}
</Typography>
</CardContent></Card>
</Grid>
<Grid item xs={12} sm={6} md={4}>
<Card variant="outlined"><CardContent>
<Typography variant="caption" color="text.secondary">Adresse</Typography>
<Typography>{vendor!.adresse || ''}</Typography>
</CardContent></Card>
</Grid>
{vendor!.notizen && (
<Grid item xs={12}>
<Card variant="outlined"><CardContent>
<Typography variant="caption" color="text.secondary">Notizen</Typography>
<Typography sx={{ whiteSpace: 'pre-wrap' }}>{vendor!.notizen}</Typography>
</CardContent></Card>
</Grid>
)}
</Grid>
)}
{/* ── Delete Dialog ── */}
<Dialog open={deleteDialogOpen} onClose={() => setDeleteDialogOpen(false)}>
<DialogTitle>Lieferant löschen</DialogTitle>
<DialogContent>
<Typography>
Soll der Lieferant <strong>{vendor?.name}</strong> wirklich gelöscht werden?
</Typography>
</DialogContent>
<DialogActions>
<Button onClick={() => setDeleteDialogOpen(false)}>Abbrechen</Button>
<Button color="error" variant="contained" onClick={() => deleteVendor.mutate()} disabled={deleteVendor.isPending}>
Löschen
</Button>
</DialogActions>
</Dialog>
</DashboardLayout>
);
}