rework external order tracking system

This commit is contained in:
Matthias Hochmeister
2026-03-25 13:24:52 +01:00
parent e02ada8b95
commit 561334791b
7 changed files with 148 additions and 44 deletions

View File

@@ -187,13 +187,13 @@ class BestellungController {
res.status(400).json({ success: false, message: 'Ungültige ID' }); res.status(400).json({ success: false, message: 'Ungültige ID' });
return; return;
} }
const { status } = req.body; const { status, force } = req.body;
if (!status || typeof status !== 'string') { if (!status || typeof status !== 'string') {
res.status(400).json({ success: false, message: 'Status ist erforderlich' }); res.status(400).json({ success: false, message: 'Status ist erforderlich' });
return; return;
} }
try { try {
const order = await bestellungService.updateOrderStatus(id, status, req.user!.id); const order = await bestellungService.updateOrderStatus(id, status, req.user!.id, !!force);
if (!order) { if (!order) {
res.status(404).json({ success: false, message: 'Bestellung nicht gefunden' }); res.status(404).json({ success: false, message: 'Bestellung nicht gefunden' });
return; return;

View File

@@ -0,0 +1,3 @@
-- Add tax rate column to bestellungen
ALTER TABLE bestellungen
ADD COLUMN IF NOT EXISTS steuersatz NUMERIC(5,2) NOT NULL DEFAULT 20.00;

View File

@@ -120,7 +120,7 @@ router.delete(
router.patch( router.patch(
'/items/:itemId/received', '/items/:itemId/received',
authenticate, authenticate,
requirePermission('bestellungen:create'), requirePermission('bestellungen:manage_orders'),
bestellungController.updateReceivedQuantity.bind(bestellungController) bestellungController.updateReceivedQuantity.bind(bestellungController)
); );

View File

@@ -174,16 +174,16 @@ async function getOrderById(id: number) {
} }
} }
async function createOrder(data: { bezeichnung: string; lieferant_id?: number; besteller_id?: string; notizen?: string; budget?: number; positionen?: Array<{ bezeichnung: string; menge: number; einheit?: string }> }, userId: string) { async function createOrder(data: { bezeichnung: string; lieferant_id?: number; besteller_id?: string; notizen?: string; budget?: number; steuersatz?: number; positionen?: Array<{ bezeichnung: string; menge: number; einheit?: string }> }, userId: string) {
const client = await pool.connect(); const client = await pool.connect();
try { try {
await client.query('BEGIN'); await client.query('BEGIN');
const bestellerId = data.besteller_id && data.besteller_id.trim() ? data.besteller_id.trim() : null; const bestellerId = data.besteller_id && data.besteller_id.trim() ? data.besteller_id.trim() : null;
const result = await client.query( const result = await client.query(
`INSERT INTO bestellungen (bezeichnung, lieferant_id, besteller_id, notizen, budget, erstellt_von) `INSERT INTO bestellungen (bezeichnung, lieferant_id, besteller_id, notizen, budget, steuersatz, erstellt_von)
VALUES ($1, $2, $3, $4, $5, $6) VALUES ($1, $2, $3, $4, $5, $6, $7)
RETURNING *`, RETURNING *`,
[data.bezeichnung, data.lieferant_id || null, bestellerId, data.notizen || null, data.budget || null, userId] [data.bezeichnung, data.lieferant_id || null, bestellerId, data.notizen || null, data.budget || null, data.steuersatz ?? 20, userId]
); );
const order = result.rows[0]; const order = result.rows[0];
@@ -209,7 +209,7 @@ async function createOrder(data: { bezeichnung: string; lieferant_id?: number; b
} }
} }
async function updateOrder(id: number, data: { bezeichnung?: string; lieferant_id?: number; notizen?: string; budget?: number; status?: string }, userId: string) { async function updateOrder(id: number, data: { bezeichnung?: string; lieferant_id?: number; notizen?: string; budget?: number; status?: string; steuersatz?: number }, userId: string) {
try { try {
// Check current order for status change detection // Check current order for status change detection
const current = await pool.query(`SELECT * FROM bestellungen WHERE id = $1`, [id]); const current = await pool.query(`SELECT * FROM bestellungen WHERE id = $1`, [id]);
@@ -239,10 +239,11 @@ async function updateOrder(id: number, data: { bezeichnung?: string; lieferant_i
status = COALESCE($5, status), status = COALESCE($5, status),
bestellt_am = $6, bestellt_am = $6,
abgeschlossen_am = $7, abgeschlossen_am = $7,
steuersatz = COALESCE($8, steuersatz),
aktualisiert_am = NOW() aktualisiert_am = NOW()
WHERE id = $8 WHERE id = $9
RETURNING *`, RETURNING *`,
[data.bezeichnung, data.lieferant_id, data.notizen, data.budget, data.status, bestellt_am, abgeschlossen_am, id] [data.bezeichnung, data.lieferant_id, data.notizen, data.budget, data.status, bestellt_am, abgeschlossen_am, data.steuersatz, id]
); );
if (result.rows.length === 0) return null; if (result.rows.length === 0) return null;
@@ -251,6 +252,7 @@ async function updateOrder(id: number, data: { bezeichnung?: string; lieferant_i
if (data.lieferant_id) changes.push(`Lieferant geändert`); if (data.lieferant_id) changes.push(`Lieferant geändert`);
if (data.status && data.status !== oldStatus) changes.push(`Status: ${oldStatus}${data.status}`); if (data.status && data.status !== oldStatus) changes.push(`Status: ${oldStatus}${data.status}`);
if (data.budget) changes.push(`Budget geändert`); if (data.budget) changes.push(`Budget geändert`);
if (data.steuersatz != null) changes.push(`Steuersatz: ${data.steuersatz}%`);
await logAction(id, 'Bestellung aktualisiert', changes.join(', ') || 'Bestellung bearbeitet', userId); await logAction(id, 'Bestellung aktualisiert', changes.join(', ') || 'Bestellung bearbeitet', userId);
return result.rows[0]; return result.rows[0];
@@ -302,15 +304,17 @@ const VALID_STATUS_TRANSITIONS: Record<string, string[]> = {
abgeschlossen: [], abgeschlossen: [],
}; };
async function updateOrderStatus(id: number, status: string, userId: string) { async function updateOrderStatus(id: number, status: string, userId: string, force?: boolean) {
try { try {
const current = await pool.query(`SELECT status FROM bestellungen WHERE id = $1`, [id]); const current = await pool.query(`SELECT status FROM bestellungen WHERE id = $1`, [id]);
if (current.rows.length === 0) return null; if (current.rows.length === 0) return null;
const oldStatus = current.rows[0].status; const oldStatus = current.rows[0].status;
const allowed = VALID_STATUS_TRANSITIONS[oldStatus] || []; if (!force) {
if (!allowed.includes(status)) { const allowed = VALID_STATUS_TRANSITIONS[oldStatus] || [];
throw new Error(`Ungültiger Statusübergang: ${oldStatus}${status}`); if (!allowed.includes(status)) {
throw new Error(`Ungültiger Statusübergang: ${oldStatus}${status}`);
}
} }
const updates: string[] = ['status = $1', 'aktualisiert_am = NOW()']; const updates: string[] = ['status = $1', 'aktualisiert_am = NOW()'];
@@ -330,7 +334,7 @@ async function updateOrderStatus(id: number, status: string, userId: string) {
params params
); );
await logAction(id, 'Status geändert', `${oldStatus}${status}`, userId); await logAction(id, 'Status geändert', `${oldStatus}${status}${force ? ' (manuell)' : ''}`, userId);
return result.rows[0]; return result.rows[0];
} catch (error) { } catch (error) {
logger.error('BestellungService.updateOrderStatus failed', { error, id }); logger.error('BestellungService.updateOrderStatus failed', { error, id });

View File

@@ -37,6 +37,7 @@ import {
History, History,
Upload as UploadIcon, Upload as UploadIcon,
ArrowDropDown, ArrowDropDown,
MoreVert,
} from '@mui/icons-material'; } from '@mui/icons-material';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { useParams, useNavigate } from 'react-router-dom'; import { useParams, useNavigate } from 'react-router-dom';
@@ -97,7 +98,9 @@ export default function BestellungDetail() {
const [editingItemId, setEditingItemId] = useState<number | null>(null); const [editingItemId, setEditingItemId] = useState<number | null>(null);
const [editingItemData, setEditingItemData] = useState<Partial<BestellpositionFormData>>({}); const [editingItemData, setEditingItemData] = useState<Partial<BestellpositionFormData>>({});
const [statusConfirmTarget, setStatusConfirmTarget] = useState<BestellungStatus | null>(null); const [statusConfirmTarget, setStatusConfirmTarget] = useState<BestellungStatus | null>(null);
const [statusForce, setStatusForce] = useState(false);
const [statusMenuAnchor, setStatusMenuAnchor] = useState<null | HTMLElement>(null); const [statusMenuAnchor, setStatusMenuAnchor] = useState<null | HTMLElement>(null);
const [overrideMenuAnchor, setOverrideMenuAnchor] = useState<null | HTMLElement>(null);
const [deleteItemTarget, setDeleteItemTarget] = useState<number | null>(null); const [deleteItemTarget, setDeleteItemTarget] = useState<number | null>(null);
const [deleteFileTarget, setDeleteFileTarget] = useState<number | null>(null); const [deleteFileTarget, setDeleteFileTarget] = useState<number | null>(null);
@@ -118,21 +121,36 @@ export default function BestellungDetail() {
const erinnerungen = data?.erinnerungen ?? []; const erinnerungen = data?.erinnerungen ?? [];
const historie = data?.historie ?? []; const historie = data?.historie ?? [];
const canEdit = hasPermission('bestellungen:create'); const canCreate = hasPermission('bestellungen:create');
const canDelete = hasPermission('bestellungen:delete');
const canManageReminders = hasPermission('bestellungen:manage_reminders');
const validTransitions = bestellung ? STATUS_TRANSITIONS[bestellung.status] : []; const validTransitions = bestellung ? STATUS_TRANSITIONS[bestellung.status] : [];
// All statuses except current, for force override
const ALL_STATUSES: BestellungStatus[] = ['entwurf', 'erstellt', 'bestellt', 'teillieferung', 'vollstaendig', 'abgeschlossen'];
const overrideStatuses = bestellung ? ALL_STATUSES.filter((s) => s !== bestellung.status) : [];
// ── Mutations ── // ── Mutations ──
const updateStatus = useMutation({ const updateStatus = useMutation({
mutationFn: (status: string) => bestellungApi.updateStatus(orderId, status), mutationFn: ({ status, force }: { status: string; force?: boolean }) => bestellungApi.updateStatus(orderId, status, force),
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['bestellung', orderId] }); queryClient.invalidateQueries({ queryKey: ['bestellung', orderId] });
showSuccess('Status aktualisiert'); showSuccess('Status aktualisiert');
setStatusConfirmTarget(null); setStatusConfirmTarget(null);
setStatusForce(false);
}, },
onError: () => showError('Fehler beim Aktualisieren des Status'), onError: () => showError('Fehler beim Aktualisieren des Status'),
}); });
const updateOrder = useMutation({
mutationFn: (data: Partial<{ steuersatz: number }>) => bestellungApi.updateOrder(orderId, data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['bestellung', orderId] });
},
onError: () => showError('Fehler beim Aktualisieren'),
});
const addItem = useMutation({ const addItem = useMutation({
mutationFn: (data: BestellpositionFormData) => bestellungApi.addLineItem(orderId, data), mutationFn: (data: BestellpositionFormData) => bestellungApi.addLineItem(orderId, data),
onSuccess: () => { onSuccess: () => {
@@ -252,6 +270,9 @@ export default function BestellungDetail() {
// Compute totals (NUMERIC columns come as strings from PostgreSQL — parse to float) // Compute totals (NUMERIC columns come as strings from PostgreSQL — parse to float)
const totalCost = positionen.reduce((sum, p) => sum + (parseFloat(String(p.einzelpreis)) || 0) * (parseFloat(String(p.menge)) || 0), 0); const totalCost = positionen.reduce((sum, p) => sum + (parseFloat(String(p.einzelpreis)) || 0) * (parseFloat(String(p.menge)) || 0), 0);
const steuersatz = parseFloat(String(bestellung?.steuersatz ?? 20));
const taxAmount = totalCost * (steuersatz / 100);
const totalBrutto = totalCost + taxAmount;
const totalReceived = positionen.length > 0 const totalReceived = positionen.length > 0
? positionen.reduce((sum, p) => sum + (parseFloat(String(p.erhalten_menge)) || 0), 0) ? positionen.reduce((sum, p) => sum + (parseFloat(String(p.erhalten_menge)) || 0), 0)
: 0; : 0;
@@ -328,13 +349,13 @@ export default function BestellungDetail() {
</Grid> </Grid>
{/* ── Status Action ── */} {/* ── Status Action ── */}
{canEdit && validTransitions.length > 0 && ( {canCreate && (
<Box sx={{ mb: 3 }}> <Box sx={{ mb: 3, display: 'flex', gap: 1, alignItems: 'center' }}>
{validTransitions.length === 1 ? ( {validTransitions.length === 1 ? (
<Button variant="contained" onClick={() => setStatusConfirmTarget(validTransitions[0])}> <Button variant="contained" onClick={() => { setStatusForce(false); setStatusConfirmTarget(validTransitions[0]); }}>
Status ändern: {BESTELLUNG_STATUS_LABELS[validTransitions[0]]} Status ändern: {BESTELLUNG_STATUS_LABELS[validTransitions[0]]}
</Button> </Button>
) : ( ) : validTransitions.length > 1 ? (
<> <>
<Button <Button
variant="contained" variant="contained"
@@ -353,6 +374,41 @@ export default function BestellungDetail() {
key={s} key={s}
onClick={() => { onClick={() => {
setStatusMenuAnchor(null); setStatusMenuAnchor(null);
setStatusForce(false);
setStatusConfirmTarget(s);
}}
>
{BESTELLUNG_STATUS_LABELS[s]}
</MenuItem>
))}
</Menu>
</>
) : null}
{/* Manual override menu */}
{overrideStatuses.length > 0 && (
<>
<IconButton
size="small"
title="Status manuell setzen"
onClick={(e) => setOverrideMenuAnchor(e.currentTarget)}
>
<MoreVert />
</IconButton>
<Menu
anchorEl={overrideMenuAnchor}
open={Boolean(overrideMenuAnchor)}
onClose={() => setOverrideMenuAnchor(null)}
>
<MenuItem disabled dense>
<Typography variant="caption" color="text.secondary">Status manuell setzen</Typography>
</MenuItem>
{overrideStatuses.map((s) => (
<MenuItem
key={s}
onClick={() => {
setOverrideMenuAnchor(null);
setStatusForce(true);
setStatusConfirmTarget(s); setStatusConfirmTarget(s);
}} }}
> >
@@ -396,7 +452,7 @@ export default function BestellungDetail() {
<TableCell align="right">Einzelpreis</TableCell> <TableCell align="right">Einzelpreis</TableCell>
<TableCell align="right">Gesamt</TableCell> <TableCell align="right">Gesamt</TableCell>
<TableCell align="right">Erhalten</TableCell> <TableCell align="right">Erhalten</TableCell>
{canEdit && <TableCell align="right">Aktionen</TableCell>} {(canCreate || canDelete) && <TableCell align="right">Aktionen</TableCell>}
</TableRow> </TableRow>
</TableHead> </TableHead>
<TableBody> <TableBody>
@@ -434,7 +490,7 @@ export default function BestellungDetail() {
<TableCell align="right">{formatCurrency(p.einzelpreis)}</TableCell> <TableCell align="right">{formatCurrency(p.einzelpreis)}</TableCell>
<TableCell align="right">{formatCurrency((p.einzelpreis ?? 0) * p.menge)}</TableCell> <TableCell align="right">{formatCurrency((p.einzelpreis ?? 0) * p.menge)}</TableCell>
<TableCell align="right"> <TableCell align="right">
{canEdit ? ( {canCreate ? (
<TextField <TextField
size="small" size="small"
type="number" type="number"
@@ -447,10 +503,10 @@ export default function BestellungDetail() {
p.erhalten_menge p.erhalten_menge
)} )}
</TableCell> </TableCell>
{canEdit && ( {(canCreate || canDelete) && (
<TableCell align="right"> <TableCell align="right">
<IconButton size="small" onClick={() => startEditItem(p)}><EditIcon fontSize="small" /></IconButton> {canCreate && <IconButton size="small" onClick={() => startEditItem(p)}><EditIcon fontSize="small" /></IconButton>}
<IconButton size="small" color="error" onClick={() => setDeleteItemTarget(p.id)}><DeleteIcon fontSize="small" /></IconButton> {canDelete && <IconButton size="small" color="error" onClick={() => setDeleteItemTarget(p.id)}><DeleteIcon fontSize="small" /></IconButton>}
</TableCell> </TableCell>
)} )}
</TableRow> </TableRow>
@@ -458,7 +514,7 @@ export default function BestellungDetail() {
)} )}
{/* ── Add Item Row ── */} {/* ── Add Item Row ── */}
{canEdit && ( {canCreate && (
<TableRow> <TableRow>
<TableCell> <TableCell>
<TextField size="small" placeholder="Bezeichnung" value={newItem.bezeichnung} onChange={(e) => setNewItem((f) => ({ ...f, bezeichnung: e.target.value }))} /> <TextField size="small" placeholder="Bezeichnung" value={newItem.bezeichnung} onChange={(e) => setNewItem((f) => ({ ...f, bezeichnung: e.target.value }))} />
@@ -485,13 +541,47 @@ export default function BestellungDetail() {
</TableRow> </TableRow>
)} )}
{/* ── Totals Row ── */} {/* ── Totals Rows (Netto / MwSt / Brutto) ── */}
{positionen.length > 0 && ( {positionen.length > 0 && (
<TableRow> <>
<TableCell colSpan={5} align="right"><strong>Gesamtsumme</strong></TableCell> <TableRow>
<TableCell align="right"><strong>{formatCurrency(totalCost)}</strong></TableCell> <TableCell colSpan={5} align="right">Netto</TableCell>
<TableCell colSpan={canEdit ? 2 : 1} /> <TableCell align="right">{formatCurrency(totalCost)}</TableCell>
</TableRow> <TableCell colSpan={(canCreate || canDelete) ? 2 : 1} />
</TableRow>
<TableRow>
<TableCell colSpan={5} align="right">
<Box sx={{ display: 'inline-flex', alignItems: 'center', gap: 0.5 }}>
MwSt.
{canCreate ? (
<TextField
size="small"
type="number"
sx={{ width: 70 }}
value={steuersatz}
inputProps={{ min: 0, max: 100, step: 0.5 }}
onChange={(e) => {
const val = parseFloat(e.target.value);
if (!isNaN(val) && val >= 0 && val <= 100) {
updateOrder.mutate({ steuersatz: val });
}
}}
/>
) : (
<span>{steuersatz}</span>
)}
%
</Box>
</TableCell>
<TableCell align="right">{formatCurrency(taxAmount)}</TableCell>
<TableCell colSpan={(canCreate || canDelete) ? 2 : 1} />
</TableRow>
<TableRow>
<TableCell colSpan={5} align="right"><strong>Brutto</strong></TableCell>
<TableCell align="right"><strong>{formatCurrency(totalBrutto)}</strong></TableCell>
<TableCell colSpan={(canCreate || canDelete) ? 2 : 1} />
</TableRow>
</>
)} )}
</TableBody> </TableBody>
</Table> </Table>
@@ -505,7 +595,7 @@ export default function BestellungDetail() {
<Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}> <Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}>
<AttachFile sx={{ mr: 1 }} /> <AttachFile sx={{ mr: 1 }} />
<Typography variant="h6" sx={{ flexGrow: 1 }}>Dateien</Typography> <Typography variant="h6" sx={{ flexGrow: 1 }}>Dateien</Typography>
{canEdit && ( {canCreate && (
<> <>
<input ref={fileInputRef} type="file" hidden onChange={handleFileSelect} /> <input ref={fileInputRef} type="file" hidden onChange={handleFileSelect} />
<Button size="small" startIcon={<UploadIcon />} onClick={() => fileInputRef.current?.click()} disabled={uploadFile.isPending}> <Button size="small" startIcon={<UploadIcon />} onClick={() => fileInputRef.current?.click()} disabled={uploadFile.isPending}>
@@ -534,7 +624,7 @@ export default function BestellungDetail() {
<Typography variant="caption" color="text.secondary"> <Typography variant="caption" color="text.secondary">
{formatFileSize(d.dateigroesse)} &middot; {formatDate(d.hochgeladen_am)} {formatFileSize(d.dateigroesse)} &middot; {formatDate(d.hochgeladen_am)}
</Typography> </Typography>
{canEdit && ( {canDelete && (
<Box sx={{ mt: 0.5 }}> <Box sx={{ mt: 0.5 }}>
<IconButton size="small" color="error" onClick={() => setDeleteFileTarget(d.id)}> <IconButton size="small" color="error" onClick={() => setDeleteFileTarget(d.id)}>
<DeleteIcon fontSize="small" /> <DeleteIcon fontSize="small" />
@@ -556,7 +646,7 @@ export default function BestellungDetail() {
<Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}> <Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}>
<Alarm sx={{ mr: 1 }} /> <Alarm sx={{ mr: 1 }} />
<Typography variant="h6" sx={{ flexGrow: 1 }}>Erinnerungen</Typography> <Typography variant="h6" sx={{ flexGrow: 1 }}>Erinnerungen</Typography>
{canEdit && ( {canManageReminders && (
<Button size="small" startIcon={<AddIcon />} onClick={() => setReminderFormOpen(true)}> <Button size="small" startIcon={<AddIcon />} onClick={() => setReminderFormOpen(true)}>
Neue Erinnerung Neue Erinnerung
</Button> </Button>
@@ -570,7 +660,7 @@ export default function BestellungDetail() {
<Box key={r.id} sx={{ display: 'flex', alignItems: 'center', gap: 1, opacity: r.erledigt ? 0.5 : 1 }}> <Box key={r.id} sx={{ display: 'flex', alignItems: 'center', gap: 1, opacity: r.erledigt ? 0.5 : 1 }}>
<Checkbox <Checkbox
checked={r.erledigt} checked={r.erledigt}
disabled={r.erledigt || !canEdit} disabled={r.erledigt || !canManageReminders}
onChange={() => markReminderDone.mutate(r.id)} onChange={() => markReminderDone.mutate(r.id)}
size="small" size="small"
/> />
@@ -582,7 +672,7 @@ export default function BestellungDetail() {
Fällig: {formatDate(r.faellig_am)} Fällig: {formatDate(r.faellig_am)}
</Typography> </Typography>
</Box> </Box>
{canEdit && ( {canManageReminders && (
<IconButton size="small" color="error" onClick={() => setDeleteReminderTarget(r.id)}> <IconButton size="small" color="error" onClick={() => setDeleteReminderTarget(r.id)}>
<DeleteIcon fontSize="small" /> <DeleteIcon fontSize="small" />
</IconButton> </IconButton>
@@ -670,17 +760,22 @@ export default function BestellungDetail() {
{/* ══════════════════════════════════════════════════════════════════════ */} {/* ══════════════════════════════════════════════════════════════════════ */}
{/* Status Confirmation */} {/* Status Confirmation */}
<Dialog open={statusConfirmTarget != null} onClose={() => setStatusConfirmTarget(null)}> <Dialog open={statusConfirmTarget != null} onClose={() => { setStatusConfirmTarget(null); setStatusForce(false); }}>
<DialogTitle>Status ändern</DialogTitle> <DialogTitle>Status ändern{statusForce ? ' (manuell)' : ''}</DialogTitle>
<DialogContent> <DialogContent>
<Typography> <Typography>
Status von <strong>{BESTELLUNG_STATUS_LABELS[bestellung.status]}</strong> auf{' '} Status von <strong>{BESTELLUNG_STATUS_LABELS[bestellung.status]}</strong> auf{' '}
<strong>{statusConfirmTarget ? BESTELLUNG_STATUS_LABELS[statusConfirmTarget] : ''}</strong> ändern? <strong>{statusConfirmTarget ? BESTELLUNG_STATUS_LABELS[statusConfirmTarget] : ''}</strong> ändern?
</Typography> </Typography>
{statusForce && (
<Typography variant="body2" color="warning.main" sx={{ mt: 1 }}>
Dies ist ein manueller Statusübergang außerhalb des normalen Ablaufs.
</Typography>
)}
</DialogContent> </DialogContent>
<DialogActions> <DialogActions>
<Button onClick={() => setStatusConfirmTarget(null)}>Abbrechen</Button> <Button onClick={() => { setStatusConfirmTarget(null); setStatusForce(false); }}>Abbrechen</Button>
<Button variant="contained" onClick={() => statusConfirmTarget && updateStatus.mutate(statusConfirmTarget)} disabled={updateStatus.isPending}> <Button variant="contained" onClick={() => statusConfirmTarget && updateStatus.mutate({ status: statusConfirmTarget, force: statusForce || undefined })} disabled={updateStatus.isPending}>
Bestätigen Bestätigen
</Button> </Button>
</DialogActions> </DialogActions>

View File

@@ -58,8 +58,8 @@ export const bestellungApi = {
deleteOrder: async (id: number): Promise<void> => { deleteOrder: async (id: number): Promise<void> => {
await api.delete(`/api/bestellungen/${id}`); await api.delete(`/api/bestellungen/${id}`);
}, },
updateStatus: async (id: number, status: string): Promise<Bestellung> => { updateStatus: async (id: number, status: string, force?: boolean): Promise<Bestellung> => {
const r = await api.patch(`/api/bestellungen/${id}/status`, { status }); const r = await api.patch(`/api/bestellungen/${id}/status`, { status, force: force || undefined });
return r.data.data; return r.data.data;
}, },

View File

@@ -57,6 +57,7 @@ export interface Bestellung {
besteller_name?: string; besteller_name?: string;
status: BestellungStatus; status: BestellungStatus;
budget?: number; budget?: number;
steuersatz?: number;
notizen?: string; notizen?: string;
erstellt_von?: string; erstellt_von?: string;
erstellt_am: string; erstellt_am: string;
@@ -74,6 +75,7 @@ export interface BestellungFormData {
besteller_id?: string; besteller_id?: string;
status?: BestellungStatus; status?: BestellungStatus;
budget?: number; budget?: number;
steuersatz?: number;
notizen?: string; notizen?: string;
positionen?: BestellpositionFormData[]; positionen?: BestellpositionFormData[];
} }