rework from modal to page

This commit is contained in:
Matthias Hochmeister
2026-03-25 10:23:28 +01:00
parent 4ad260ce66
commit feb39d234f
14 changed files with 698 additions and 280 deletions

View File

@@ -22,6 +22,8 @@ import {
CardContent,
LinearProgress,
Checkbox,
Menu,
MenuItem,
} from '@mui/material';
import {
ArrowBack,
@@ -34,6 +36,7 @@ import {
Alarm,
History,
Upload as UploadIcon,
ArrowDropDown,
} from '@mui/icons-material';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { useParams, useNavigate } from 'react-router-dom';
@@ -62,13 +65,15 @@ const formatFileSize = (bytes?: number) => {
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
};
// Status flow
const STATUS_FLOW: BestellungStatus[] = ['entwurf', 'erstellt', 'bestellt', 'teillieferung', 'vollstaendig', 'abgeschlossen'];
function getNextStatus(current: BestellungStatus): BestellungStatus | null {
const idx = STATUS_FLOW.indexOf(current);
return idx >= 0 && idx < STATUS_FLOW.length - 1 ? STATUS_FLOW[idx + 1] : null;
}
// Valid status transitions (must match backend VALID_STATUS_TRANSITIONS)
const STATUS_TRANSITIONS: Record<BestellungStatus, BestellungStatus[]> = {
entwurf: ['erstellt', 'bestellt'],
erstellt: ['bestellt'],
bestellt: ['teillieferung', 'vollstaendig'],
teillieferung: ['vollstaendig'],
vollstaendig: ['abgeschlossen'],
abgeschlossen: [],
};
// Empty line item form
const emptyItem: BestellpositionFormData = { bezeichnung: '', artikelnummer: '', menge: 1, einheit: 'Stk', einzelpreis: undefined };
@@ -91,7 +96,8 @@ export default function BestellungDetail() {
const [newItem, setNewItem] = useState<BestellpositionFormData>({ ...emptyItem });
const [editingItemId, setEditingItemId] = useState<number | null>(null);
const [editingItemData, setEditingItemData] = useState<Partial<BestellpositionFormData>>({});
const [statusConfirmOpen, setStatusConfirmOpen] = useState(false);
const [statusConfirmTarget, setStatusConfirmTarget] = useState<BestellungStatus | null>(null);
const [statusMenuAnchor, setStatusMenuAnchor] = useState<null | HTMLElement>(null);
const [deleteItemTarget, setDeleteItemTarget] = useState<number | null>(null);
const [deleteFileTarget, setDeleteFileTarget] = useState<number | null>(null);
@@ -113,7 +119,7 @@ export default function BestellungDetail() {
const historie = data?.historie ?? [];
const canEdit = hasPermission('bestellungen:create');
const nextStatus = bestellung ? getNextStatus(bestellung.status) : null;
const validTransitions = bestellung ? STATUS_TRANSITIONS[bestellung.status] : [];
// ── Mutations ──
@@ -122,7 +128,7 @@ export default function BestellungDetail() {
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['bestellung', orderId] });
showSuccess('Status aktualisiert');
setStatusConfirmOpen(false);
setStatusConfirmTarget(null);
},
onError: () => showError('Fehler beim Aktualisieren des Status'),
});
@@ -322,11 +328,40 @@ export default function BestellungDetail() {
</Grid>
{/* ── Status Action ── */}
{canEdit && nextStatus && (
{canEdit && validTransitions.length > 0 && (
<Box sx={{ mb: 3 }}>
<Button variant="contained" onClick={() => setStatusConfirmOpen(true)}>
Status ändern: {BESTELLUNG_STATUS_LABELS[nextStatus]}
</Button>
{validTransitions.length === 1 ? (
<Button variant="contained" onClick={() => setStatusConfirmTarget(validTransitions[0])}>
Status ändern: {BESTELLUNG_STATUS_LABELS[validTransitions[0]]}
</Button>
) : (
<>
<Button
variant="contained"
endIcon={<ArrowDropDown />}
onClick={(e) => setStatusMenuAnchor(e.currentTarget)}
>
Status ändern
</Button>
<Menu
anchorEl={statusMenuAnchor}
open={Boolean(statusMenuAnchor)}
onClose={() => setStatusMenuAnchor(null)}
>
{validTransitions.map((s) => (
<MenuItem
key={s}
onClick={() => {
setStatusMenuAnchor(null);
setStatusConfirmTarget(s);
}}
>
{BESTELLUNG_STATUS_LABELS[s]}
</MenuItem>
))}
</Menu>
</>
)}
</Box>
)}
@@ -630,17 +665,17 @@ export default function BestellungDetail() {
{/* ══════════════════════════════════════════════════════════════════════ */}
{/* Status Confirmation */}
<Dialog open={statusConfirmOpen} onClose={() => setStatusConfirmOpen(false)}>
<Dialog open={statusConfirmTarget != null} onClose={() => setStatusConfirmTarget(null)}>
<DialogTitle>Status ändern</DialogTitle>
<DialogContent>
<Typography>
Status von <strong>{BESTELLUNG_STATUS_LABELS[bestellung.status]}</strong> auf{' '}
<strong>{nextStatus ? BESTELLUNG_STATUS_LABELS[nextStatus] : ''}</strong> ändern?
<strong>{statusConfirmTarget ? BESTELLUNG_STATUS_LABELS[statusConfirmTarget] : ''}</strong> ändern?
</Typography>
</DialogContent>
<DialogActions>
<Button onClick={() => setStatusConfirmOpen(false)}>Abbrechen</Button>
<Button variant="contained" onClick={() => nextStatus && updateStatus.mutate(nextStatus)} disabled={updateStatus.isPending}>
<Button onClick={() => setStatusConfirmTarget(null)}>Abbrechen</Button>
<Button variant="contained" onClick={() => statusConfirmTarget && updateStatus.mutate(statusConfirmTarget)} disabled={updateStatus.isPending}>
Bestätigen
</Button>
</DialogActions>