rework external order tracking system
This commit is contained in:
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -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)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -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 });
|
||||||
|
|||||||
@@ -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)} · {formatDate(d.hochgeladen_am)}
|
{formatFileSize(d.dateigroesse)} · {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>
|
||||||
|
|||||||
@@ -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;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -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[];
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user