add linking between internal and external orders
This commit is contained in:
@@ -33,6 +33,7 @@ import BestellungNeu from './pages/BestellungNeu';
|
||||
import LieferantDetail from './pages/LieferantDetail';
|
||||
import Ausruestungsanfrage from './pages/Ausruestungsanfrage';
|
||||
import AusruestungsanfrageDetail from './pages/AusruestungsanfrageDetail';
|
||||
import AusruestungsanfrageZuBestellung from './pages/AusruestungsanfrageZuBestellung';
|
||||
import AusruestungsanfrageArtikelDetail from './pages/AusruestungsanfrageArtikelDetail';
|
||||
import AusruestungsanfrageNeu from './pages/AusruestungsanfrageNeu';
|
||||
import Issues from './pages/Issues';
|
||||
@@ -324,6 +325,14 @@ function App() {
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/ausruestungsanfragen/:id/bestellen"
|
||||
element={
|
||||
<ProtectedRoute>
|
||||
<AusruestungsanfrageZuBestellung />
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/issues"
|
||||
element={
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
} from '@mui/material';
|
||||
import {
|
||||
ArrowBack, Add as AddIcon, Delete as DeleteIcon, Edit as EditIcon,
|
||||
Check as CheckIcon, Close as CloseIcon, Link as LinkIcon,
|
||||
Check as CheckIcon, Close as CloseIcon, ShoppingCart as ShoppingCartIcon,
|
||||
} from '@mui/icons-material';
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { useParams, useNavigate } from 'react-router-dom';
|
||||
@@ -17,13 +17,11 @@ 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 {
|
||||
AusruestungAnfrage, AusruestungAnfrageDetailResponse,
|
||||
AusruestungAnfrageFormItem, AusruestungAnfrageStatus,
|
||||
} from '../types/ausruestungsanfrage.types';
|
||||
import type { Bestellung } from '../types/bestellung.types';
|
||||
|
||||
// ── Helpers ──
|
||||
|
||||
@@ -58,8 +56,6 @@ export default function AusruestungsanfrageDetail() {
|
||||
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);
|
||||
|
||||
// Permissions
|
||||
const showAdminActions = hasPermission('ausruestungsanfrage:approve');
|
||||
@@ -80,12 +76,6 @@ export default function AusruestungsanfrageDetail() {
|
||||
enabled: editing,
|
||||
});
|
||||
|
||||
const { data: bestellungen = [] } = useQuery({
|
||||
queryKey: ['bestellungen'],
|
||||
queryFn: () => bestellungApi.getOrders(),
|
||||
enabled: linkDialog,
|
||||
});
|
||||
|
||||
// ── Mutations ──
|
||||
const updateMut = useMutation({
|
||||
mutationFn: (data: { bezeichnung?: string; notizen?: string; items?: AusruestungAnfrageFormItem[] }) =>
|
||||
@@ -111,17 +101,6 @@ export default function AusruestungsanfrageDetail() {
|
||||
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 geliefertMut = useMutation({
|
||||
mutationFn: ({ positionId, geliefert }: { positionId: number; geliefert: boolean }) =>
|
||||
ausruestungsanfrageApi.updatePositionGeliefert(positionId, geliefert),
|
||||
@@ -414,11 +393,12 @@ export default function AusruestungsanfrageDetail() {
|
||||
)}
|
||||
{showAdminActions && anfrage && anfrage.status === 'genehmigt' && canLink && (
|
||||
<Button
|
||||
variant="outlined"
|
||||
startIcon={<LinkIcon />}
|
||||
onClick={() => setLinkDialog(true)}
|
||||
variant="contained"
|
||||
color="primary"
|
||||
startIcon={<ShoppingCartIcon />}
|
||||
onClick={() => navigate(`/ausruestungsanfragen/${requestId}/bestellen`)}
|
||||
>
|
||||
Verknüpfen
|
||||
Bestellungen erstellen
|
||||
</Button>
|
||||
)}
|
||||
{canEdit && !editing && (
|
||||
@@ -459,29 +439,6 @@ export default function AusruestungsanfrageDetail() {
|
||||
</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: '20px !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>
|
||||
</DashboardLayout>
|
||||
);
|
||||
}
|
||||
|
||||
426
frontend/src/pages/AusruestungsanfrageZuBestellung.tsx
Normal file
426
frontend/src/pages/AusruestungsanfrageZuBestellung.tsx
Normal file
@@ -0,0 +1,426 @@
|
||||
import { useState, useMemo, useEffect } from 'react';
|
||||
import {
|
||||
Box, Typography, Paper, Button, IconButton, Chip,
|
||||
Table, TableBody, TableCell, TableHead, TableRow,
|
||||
Autocomplete, TextField, Alert, CircularProgress,
|
||||
Dialog, DialogTitle, DialogContent, DialogActions,
|
||||
LinearProgress,
|
||||
} from '@mui/material';
|
||||
import {
|
||||
ArrowBack,
|
||||
Add as AddIcon,
|
||||
ShoppingCart as ShoppingCartIcon,
|
||||
} from '@mui/icons-material';
|
||||
import { useParams, useNavigate } from 'react-router-dom';
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import DashboardLayout from '../components/dashboard/DashboardLayout';
|
||||
import { useNotification } from '../contexts/NotificationContext';
|
||||
import { usePermissionContext } from '../contexts/PermissionContext';
|
||||
import { ausruestungsanfrageApi } from '../services/ausruestungsanfrage';
|
||||
import { bestellungApi } from '../services/bestellung';
|
||||
import { AUSRUESTUNG_STATUS_LABELS } from '../types/ausruestungsanfrage.types';
|
||||
import type {
|
||||
AusruestungAnfrage,
|
||||
AusruestungAnfragePosition,
|
||||
CreateOrdersRequest,
|
||||
} from '../types/ausruestungsanfrage.types';
|
||||
import type { Lieferant } 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}`;
|
||||
}
|
||||
|
||||
interface VendorAssignment {
|
||||
lieferantId: number;
|
||||
lieferantName: string;
|
||||
}
|
||||
|
||||
interface VendorGroup {
|
||||
lieferantId: number;
|
||||
lieferantName: string;
|
||||
positionen: AusruestungAnfragePosition[];
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════════════════════════════════════
|
||||
// Component
|
||||
// ══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
export default function AusruestungsanfrageZuBestellung() {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const navigate = useNavigate();
|
||||
const queryClient = useQueryClient();
|
||||
const { showSuccess, showError } = useNotification();
|
||||
const { hasPermission } = usePermissionContext();
|
||||
|
||||
const requestId = Number(id);
|
||||
const canManageVendors = hasPermission('bestellungen:manage_vendors');
|
||||
|
||||
// ── Data ──
|
||||
const { data: detail, isLoading, isError } = useQuery({
|
||||
queryKey: ['ausruestungsanfrage', requestId],
|
||||
queryFn: () => ausruestungsanfrageApi.getRequest(requestId),
|
||||
enabled: !!requestId,
|
||||
});
|
||||
|
||||
const { data: vendors = [] } = useQuery({
|
||||
queryKey: ['bestellung-vendors'],
|
||||
queryFn: () => bestellungApi.getVendors(),
|
||||
});
|
||||
|
||||
const anfrage = detail?.anfrage;
|
||||
const positionen: AusruestungAnfragePosition[] = detail?.positionen ?? [];
|
||||
|
||||
// ── State ──
|
||||
const [assignments, setAssignments] = useState<Record<number, VendorAssignment | null>>({});
|
||||
const [orderNames, setOrderNames] = useState<Record<number, string>>({});
|
||||
|
||||
// New vendor dialog
|
||||
const [newVendorDialog, setNewVendorDialog] = useState(false);
|
||||
const [newVendorName, setNewVendorName] = useState('');
|
||||
const [newVendorKontakt, setNewVendorKontakt] = useState('');
|
||||
const [newVendorEmail, setNewVendorEmail] = useState('');
|
||||
const [newVendorTelefon, setNewVendorTelefon] = useState('');
|
||||
// Track which position triggered the new-vendor dialog
|
||||
const [newVendorTargetPosId, setNewVendorTargetPosId] = useState<number | null>(null);
|
||||
|
||||
// ── Derived: vendor groups ──
|
||||
const vendorGroups: VendorGroup[] = useMemo(() => {
|
||||
const map = new Map<number, VendorGroup>();
|
||||
positionen.forEach(pos => {
|
||||
const a = assignments[pos.id];
|
||||
if (!a) return;
|
||||
if (!map.has(a.lieferantId)) {
|
||||
map.set(a.lieferantId, { lieferantId: a.lieferantId, lieferantName: a.lieferantName, positionen: [] });
|
||||
}
|
||||
map.get(a.lieferantId)!.positionen.push(pos);
|
||||
});
|
||||
return [...map.values()];
|
||||
}, [assignments, positionen]);
|
||||
|
||||
// ── Auto-fill order names when a new vendor group appears ──
|
||||
useEffect(() => {
|
||||
if (!anfrage) return;
|
||||
const label = formatOrderId(anfrage);
|
||||
vendorGroups.forEach(g => {
|
||||
setOrderNames(prev => {
|
||||
if (prev[g.lieferantId]) return prev;
|
||||
return { ...prev, [g.lieferantId]: `Anfrage ${label} – ${g.lieferantName}` };
|
||||
});
|
||||
});
|
||||
}, [vendorGroups, anfrage]);
|
||||
|
||||
// ── Derived: progress ──
|
||||
const assignedCount = positionen.filter(p => assignments[p.id] != null).length;
|
||||
const allAssigned = positionen.length > 0 && assignedCount === positionen.length;
|
||||
|
||||
// ── Mutations ──
|
||||
const createOrdersMut = useMutation({
|
||||
mutationFn: (payload: CreateOrdersRequest) =>
|
||||
ausruestungsanfrageApi.createOrders(requestId, payload),
|
||||
onSuccess: (result) => {
|
||||
showSuccess(`${result.created_bestellungen.length} Bestellung(en) erfolgreich erstellt`);
|
||||
queryClient.invalidateQueries({ queryKey: ['ausruestungsanfrage', requestId] });
|
||||
navigate('/bestellungen');
|
||||
},
|
||||
onError: () => showError('Bestellungen konnten nicht erstellt werden'),
|
||||
});
|
||||
|
||||
const createVendorMut = useMutation({
|
||||
mutationFn: (data: { name: string; kontakt_name?: string; email?: string; telefon?: string }) =>
|
||||
bestellungApi.createVendor(data),
|
||||
onSuccess: (newVendor: Lieferant) => {
|
||||
queryClient.invalidateQueries({ queryKey: ['bestellung-vendors'] });
|
||||
// Auto-assign to the position that triggered the dialog
|
||||
if (newVendorTargetPosId != null) {
|
||||
setAssignments(prev => ({
|
||||
...prev,
|
||||
[newVendorTargetPosId]: { lieferantId: newVendor.id, lieferantName: newVendor.name },
|
||||
}));
|
||||
}
|
||||
setNewVendorDialog(false);
|
||||
setNewVendorName('');
|
||||
setNewVendorKontakt('');
|
||||
setNewVendorEmail('');
|
||||
setNewVendorTelefon('');
|
||||
setNewVendorTargetPosId(null);
|
||||
showSuccess(`Lieferant "${newVendor.name}" erstellt`);
|
||||
},
|
||||
onError: () => showError('Lieferant konnte nicht erstellt werden'),
|
||||
});
|
||||
|
||||
// ── Handlers ──
|
||||
const handleSubmit = () => {
|
||||
if (!allAssigned) return;
|
||||
const orders = vendorGroups.map(g => ({
|
||||
lieferant_id: g.lieferantId,
|
||||
bezeichnung: orderNames[g.lieferantId] || `Anfrage – ${g.lieferantName}`,
|
||||
positionen: g.positionen.map(p => ({
|
||||
position_id: p.id,
|
||||
bezeichnung: p.bezeichnung,
|
||||
menge: p.menge,
|
||||
einheit: p.einheit,
|
||||
notizen: p.notizen,
|
||||
})),
|
||||
}));
|
||||
createOrdersMut.mutate({ orders });
|
||||
};
|
||||
|
||||
const handleCreateVendor = () => {
|
||||
if (!newVendorName.trim()) return;
|
||||
createVendorMut.mutate({
|
||||
name: newVendorName.trim(),
|
||||
kontakt_name: newVendorKontakt.trim() || undefined,
|
||||
email: newVendorEmail.trim() || undefined,
|
||||
telefon: newVendorTelefon.trim() || undefined,
|
||||
});
|
||||
};
|
||||
|
||||
// ── Loading / error states ──
|
||||
if (isLoading) {
|
||||
return (
|
||||
<DashboardLayout>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'center', mt: 8 }}>
|
||||
<CircularProgress />
|
||||
</Box>
|
||||
</DashboardLayout>
|
||||
);
|
||||
}
|
||||
|
||||
if (isError || !anfrage) {
|
||||
return (
|
||||
<DashboardLayout>
|
||||
<Alert severity="error" sx={{ m: 3 }}>Anfrage konnte nicht geladen werden.</Alert>
|
||||
</DashboardLayout>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Render ──
|
||||
return (
|
||||
<DashboardLayout>
|
||||
<Box sx={{ maxWidth: 960, mx: 'auto', px: 2, py: 3 }}>
|
||||
|
||||
{/* ── Header ── */}
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5, mb: 3 }}>
|
||||
<IconButton onClick={() => navigate(`/ausruestungsanfrage/${requestId}`)} size="small">
|
||||
<ArrowBack />
|
||||
</IconButton>
|
||||
<Box sx={{ flex: 1 }}>
|
||||
<Typography variant="h5" fontWeight={600}>
|
||||
Bestellungen erstellen
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{anfrage.bezeichnung || `Anfrage ${formatOrderId(anfrage)}`}
|
||||
{' · '}
|
||||
<Chip
|
||||
label={AUSRUESTUNG_STATUS_LABELS[anfrage.status]}
|
||||
size="small"
|
||||
sx={{ height: 18, fontSize: 11 }}
|
||||
/>
|
||||
</Typography>
|
||||
</Box>
|
||||
<Chip
|
||||
label={`${assignedCount} / ${positionen.length} zugewiesen`}
|
||||
color={allAssigned ? 'success' : 'default'}
|
||||
variant={allAssigned ? 'filled' : 'outlined'}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{/* ── Progress bar ── */}
|
||||
<LinearProgress
|
||||
variant="determinate"
|
||||
value={positionen.length > 0 ? (assignedCount / positionen.length) * 100 : 0}
|
||||
sx={{ mb: 3, borderRadius: 2, height: 6 }}
|
||||
color={allAssigned ? 'success' : 'primary'}
|
||||
/>
|
||||
|
||||
{/* ── Items table ── */}
|
||||
<Paper sx={{ mb: 3 }}>
|
||||
<Box sx={{ px: 2, py: 1.5, borderBottom: '1px solid', borderColor: 'divider' }}>
|
||||
<Typography variant="subtitle2">Lieferanten zuweisen</Typography>
|
||||
</Box>
|
||||
<Table size="small">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Artikel</TableCell>
|
||||
<TableCell align="right" sx={{ width: 80 }}>Menge</TableCell>
|
||||
<TableCell sx={{ width: 320 }}>Lieferant</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{positionen.map(pos => {
|
||||
const assignment = assignments[pos.id];
|
||||
const selectedVendor = assignment
|
||||
? vendors.find(v => v.id === assignment.lieferantId) ?? null
|
||||
: null;
|
||||
return (
|
||||
<TableRow key={pos.id} sx={assignment ? {} : { bgcolor: 'warning.light', opacity: 0.85 }}>
|
||||
<TableCell>
|
||||
<Typography variant="body2" fontWeight={500}>{pos.bezeichnung}</Typography>
|
||||
{pos.notizen && (
|
||||
<Typography variant="caption" color="text.secondary">{pos.notizen}</Typography>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell align="right">
|
||||
<Typography variant="body2">{pos.menge} {pos.einheit ?? 'Stk'}</Typography>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
|
||||
<Autocomplete<Lieferant>
|
||||
size="small"
|
||||
sx={{ flex: 1 }}
|
||||
options={vendors}
|
||||
getOptionLabel={o => o.name}
|
||||
value={selectedVendor}
|
||||
onChange={(_, v) => {
|
||||
setAssignments(prev => ({
|
||||
...prev,
|
||||
[pos.id]: v ? { lieferantId: v.id, lieferantName: v.name } : null,
|
||||
}));
|
||||
}}
|
||||
renderInput={params => (
|
||||
<TextField
|
||||
{...params}
|
||||
placeholder="Lieferant wählen…"
|
||||
size="small"
|
||||
/>
|
||||
)}
|
||||
isOptionEqualToValue={(o, v) => o.id === v.id}
|
||||
/>
|
||||
{canManageVendors && (
|
||||
<IconButton
|
||||
size="small"
|
||||
title="Neuen Lieferanten anlegen"
|
||||
onClick={() => {
|
||||
setNewVendorTargetPosId(pos.id);
|
||||
setNewVendorDialog(true);
|
||||
}}
|
||||
>
|
||||
<AddIcon fontSize="small" />
|
||||
</IconButton>
|
||||
)}
|
||||
</Box>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</Paper>
|
||||
|
||||
{/* ── Preview section ── */}
|
||||
{vendorGroups.length > 0 && (
|
||||
<Box sx={{ mb: 3 }}>
|
||||
<Typography variant="subtitle2" sx={{ mb: 1.5 }}>
|
||||
Vorschau — {vendorGroups.length} Bestellung{vendorGroups.length !== 1 ? 'en' : ''} werden erstellt
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', gap: 2, flexWrap: 'wrap' }}>
|
||||
{vendorGroups.map(g => (
|
||||
<Paper
|
||||
key={g.lieferantId}
|
||||
variant="outlined"
|
||||
sx={{ p: 2, minWidth: 240, flex: '1 1 240px', maxWidth: 360 }}
|
||||
>
|
||||
<Typography variant="subtitle2" color="primary" sx={{ mb: 0.5 }}>
|
||||
{g.lieferantName}
|
||||
</Typography>
|
||||
<Typography variant="caption" color="text.secondary" sx={{ mb: 1.5, display: 'block' }}>
|
||||
{g.positionen.length} Artikel
|
||||
</Typography>
|
||||
<Box sx={{ mb: 1.5 }}>
|
||||
{g.positionen.map(p => (
|
||||
<Typography key={p.id} variant="body2" sx={{ py: 0.25 }}>
|
||||
· {p.bezeichnung} ×{p.menge}
|
||||
</Typography>
|
||||
))}
|
||||
</Box>
|
||||
<TextField
|
||||
size="small"
|
||||
fullWidth
|
||||
label="Bezeichnung der Bestellung"
|
||||
value={orderNames[g.lieferantId] ?? ''}
|
||||
onChange={e =>
|
||||
setOrderNames(prev => ({ ...prev, [g.lieferantId]: e.target.value }))
|
||||
}
|
||||
/>
|
||||
</Paper>
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* ── Bottom action bar ── */}
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, justifyContent: 'flex-end' }}>
|
||||
{!allAssigned && positionen.length > 0 && (
|
||||
<Alert severity="warning" sx={{ flex: 1, py: 0.5 }}>
|
||||
{positionen.length - assignedCount} Artikel {positionen.length - assignedCount === 1 ? 'hat' : 'haben'} noch keinen Lieferanten.
|
||||
</Alert>
|
||||
)}
|
||||
<Button
|
||||
variant="contained"
|
||||
size="large"
|
||||
startIcon={<ShoppingCartIcon />}
|
||||
disabled={!allAssigned || createOrdersMut.isPending}
|
||||
onClick={handleSubmit}
|
||||
>
|
||||
{createOrdersMut.isPending ? 'Erstelle…' : `${vendorGroups.length || ''} Bestellung${vendorGroups.length !== 1 ? 'en' : ''} erstellen`}
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* ── New vendor dialog ── */}
|
||||
<Dialog
|
||||
open={newVendorDialog}
|
||||
onClose={() => { setNewVendorDialog(false); setNewVendorTargetPosId(null); }}
|
||||
maxWidth="sm"
|
||||
fullWidth
|
||||
>
|
||||
<DialogTitle>Neuen Lieferanten anlegen</DialogTitle>
|
||||
<DialogContent sx={{ display: 'flex', flexDirection: 'column', gap: 2, pt: '20px !important' }}>
|
||||
<TextField
|
||||
label="Name *"
|
||||
value={newVendorName}
|
||||
onChange={e => setNewVendorName(e.target.value)}
|
||||
autoFocus
|
||||
fullWidth
|
||||
/>
|
||||
<TextField
|
||||
label="Kontaktperson"
|
||||
value={newVendorKontakt}
|
||||
onChange={e => setNewVendorKontakt(e.target.value)}
|
||||
fullWidth
|
||||
/>
|
||||
<TextField
|
||||
label="E-Mail"
|
||||
value={newVendorEmail}
|
||||
onChange={e => setNewVendorEmail(e.target.value)}
|
||||
fullWidth
|
||||
type="email"
|
||||
/>
|
||||
<TextField
|
||||
label="Telefon"
|
||||
value={newVendorTelefon}
|
||||
onChange={e => setNewVendorTelefon(e.target.value)}
|
||||
fullWidth
|
||||
/>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={() => { setNewVendorDialog(false); setNewVendorTargetPosId(null); }}>
|
||||
Abbrechen
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
disabled={!newVendorName.trim() || createVendorMut.isPending}
|
||||
onClick={handleCreateVendor}
|
||||
>
|
||||
Anlegen
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</DashboardLayout>
|
||||
);
|
||||
}
|
||||
@@ -9,6 +9,8 @@ import type {
|
||||
AusruestungEigenschaft,
|
||||
AusruestungAnfrage,
|
||||
AusruestungWidgetOverview,
|
||||
CreateOrdersRequest,
|
||||
CreateOrdersResponse,
|
||||
} from '../types/ausruestungsanfrage.types';
|
||||
|
||||
export const ausruestungsanfrageApi = {
|
||||
@@ -127,6 +129,12 @@ export const ausruestungsanfrageApi = {
|
||||
await api.delete(`/api/ausruestungsanfragen/requests/${anfrageId}/link/${bestellungId}`);
|
||||
},
|
||||
|
||||
// ── Create orders from request ──
|
||||
createOrders: async (anfrageId: number, payload: CreateOrdersRequest): Promise<CreateOrdersResponse> => {
|
||||
const r = await api.post(`/api/ausruestungsanfragen/requests/${anfrageId}/create-orders`, payload);
|
||||
return r.data.data;
|
||||
},
|
||||
|
||||
// ── Overview ──
|
||||
getOverview: async (): Promise<AusruestungOverview> => {
|
||||
const r = await api.get('/api/ausruestungsanfragen/overview');
|
||||
|
||||
@@ -101,6 +101,7 @@ export interface AusruestungAnfragePosition {
|
||||
artikel_id?: number;
|
||||
bezeichnung: string;
|
||||
menge: number;
|
||||
einheit?: string;
|
||||
notizen?: string;
|
||||
geliefert: boolean;
|
||||
erstellt_am: string;
|
||||
@@ -145,3 +146,33 @@ export interface AusruestungWidgetOverview {
|
||||
approved_count: number;
|
||||
unhandled_count: number;
|
||||
}
|
||||
|
||||
// ── Create-Orders Wizard ──
|
||||
|
||||
export interface CreateOrderPositionPayload {
|
||||
position_id: number;
|
||||
bezeichnung: string;
|
||||
menge: number;
|
||||
einheit?: string;
|
||||
notizen?: string;
|
||||
}
|
||||
|
||||
export interface CreateOrderPayload {
|
||||
lieferant_id: number;
|
||||
bezeichnung: string;
|
||||
positionen: CreateOrderPositionPayload[];
|
||||
}
|
||||
|
||||
export interface CreateOrdersRequest {
|
||||
orders: CreateOrderPayload[];
|
||||
}
|
||||
|
||||
export interface CreatedBestellung {
|
||||
id: number;
|
||||
bezeichnung: string;
|
||||
lieferant_name: string;
|
||||
}
|
||||
|
||||
export interface CreateOrdersResponse {
|
||||
created_bestellungen: CreatedBestellung[];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user