update
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { useState, useRef } from 'react';
|
||||
import React, { useState, useRef } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Typography,
|
||||
@@ -100,12 +100,13 @@ const formatFileSize = (bytes?: number) => {
|
||||
|
||||
// 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: [],
|
||||
entwurf: ['wartet_auf_genehmigung'],
|
||||
wartet_auf_genehmigung: ['bereit_zur_bestellung', 'entwurf'],
|
||||
bereit_zur_bestellung: ['bestellt'],
|
||||
bestellt: ['teillieferung', 'lieferung_pruefen'],
|
||||
teillieferung: ['lieferung_pruefen'],
|
||||
lieferung_pruefen: ['abgeschlossen'],
|
||||
abgeschlossen: [],
|
||||
};
|
||||
|
||||
// Empty line item form
|
||||
@@ -129,7 +130,6 @@ export default function BestellungDetail() {
|
||||
const [newItem, setNewItem] = useState<BestellpositionFormData>({ ...emptyItem });
|
||||
const [statusConfirmTarget, setStatusConfirmTarget] = useState<BestellungStatus | null>(null);
|
||||
const [statusForce, setStatusForce] = useState(false);
|
||||
const [statusMenuAnchor, setStatusMenuAnchor] = useState<null | HTMLElement>(null);
|
||||
const [overrideMenuAnchor, setOverrideMenuAnchor] = useState<null | HTMLElement>(null);
|
||||
const [deleteItemTarget, setDeleteItemTarget] = useState<number | null>(null);
|
||||
const [deleteFileTarget, setDeleteFileTarget] = useState<number | null>(null);
|
||||
@@ -150,6 +150,7 @@ export default function BestellungDetail() {
|
||||
menge: number;
|
||||
einheit: string;
|
||||
einzelpreis?: number;
|
||||
spezifikationen: string[];
|
||||
}>>({});
|
||||
|
||||
const [isSavingAll, setIsSavingAll] = useState(false);
|
||||
@@ -187,11 +188,12 @@ export default function BestellungDetail() {
|
||||
const canDelete = hasPermission('bestellungen:delete');
|
||||
const canManageReminders = hasPermission('bestellungen:manage_reminders');
|
||||
const canManageOrders = hasPermission('bestellungen:manage_orders');
|
||||
const canApprove = hasPermission('bestellungen:approve');
|
||||
const canExport = hasPermission('bestellungen:export');
|
||||
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 ALL_STATUSES: BestellungStatus[] = ['entwurf', 'wartet_auf_genehmigung', 'bereit_zur_bestellung', 'bestellt', 'teillieferung', 'lieferung_pruefen', 'abgeschlossen'];
|
||||
const overrideStatuses = bestellung ? ALL_STATUSES.filter((s) => s !== bestellung.status) : [];
|
||||
|
||||
// ── Mutations ──
|
||||
@@ -302,6 +304,7 @@ export default function BestellungDetail() {
|
||||
menge: parseFloat(String(p.menge)) || 1,
|
||||
einheit: p.einheit,
|
||||
einzelpreis: p.einzelpreis != null ? parseFloat(String(p.einzelpreis)) : undefined,
|
||||
spezifikationen: p.spezifikationen || [],
|
||||
}]))
|
||||
);
|
||||
setEditMode(true);
|
||||
@@ -326,7 +329,10 @@ export default function BestellungDetail() {
|
||||
for (const item of positionen) {
|
||||
const itemEdit = editItemsData[item.id];
|
||||
if (itemEdit) {
|
||||
await bestellungApi.updateLineItem(item.id, itemEdit);
|
||||
await bestellungApi.updateLineItem(item.id, {
|
||||
...itemEdit,
|
||||
spezifikationen: itemEdit.spezifikationen,
|
||||
});
|
||||
}
|
||||
}
|
||||
await queryClient.invalidateQueries({ queryKey: ['bestellung', orderId] });
|
||||
@@ -455,18 +461,22 @@ export default function BestellungDetail() {
|
||||
const totalBrutto = totalNetto * (1 + steuersatz);
|
||||
|
||||
if (hasPrices) {
|
||||
const rows = positionen.map((p) => {
|
||||
const rows: (string | number)[][] = [];
|
||||
for (const p of positionen) {
|
||||
const ep = p.einzelpreis != null ? parseFloat(String(p.einzelpreis)) : undefined;
|
||||
const menge = parseFloat(String(p.menge)) || 0;
|
||||
const gesamt = ep != null ? ep * menge : undefined;
|
||||
return [
|
||||
rows.push([
|
||||
p.bezeichnung,
|
||||
p.artikelnummer || '',
|
||||
`${menge} ${p.einheit}`,
|
||||
ep != null ? formatCurrency(ep) : '–',
|
||||
gesamt != null ? formatCurrency(gesamt) : '–',
|
||||
];
|
||||
});
|
||||
]);
|
||||
for (const spec of p.spezifikationen || []) {
|
||||
rows.push([` • ${spec}`, '', '', '', '']);
|
||||
}
|
||||
}
|
||||
|
||||
autoTable(doc, {
|
||||
head: [['Bezeichnung', 'Art.-Nr.', 'Menge', 'Einzelpreis', 'Gesamt']],
|
||||
@@ -484,6 +494,16 @@ export default function BestellungDetail() {
|
||||
3: { cellWidth: 30, halign: 'right' },
|
||||
4: { cellWidth: 30, halign: 'right' },
|
||||
},
|
||||
didParseCell: (data: any) => {
|
||||
if (data.section === 'body') {
|
||||
const cell0 = String(data.row.raw[0] ?? '');
|
||||
if (cell0.startsWith(' •')) {
|
||||
data.cell.styles.fontSize = 8;
|
||||
data.cell.styles.textColor = [100, 100, 100];
|
||||
data.cell.styles.fillColor = [255, 255, 255];
|
||||
}
|
||||
}
|
||||
},
|
||||
foot: [
|
||||
['', '', '', 'Netto:', formatCurrency(totalNetto)],
|
||||
['', '', '', `Brutto (${bestellung.steuersatz ?? 20}% USt.):`, formatCurrency(totalBrutto)],
|
||||
@@ -492,10 +512,14 @@ export default function BestellungDetail() {
|
||||
didDrawPage: addPdfFooter(doc, settings),
|
||||
});
|
||||
} else {
|
||||
const rows = positionen.map((p) => {
|
||||
const rows: string[][] = [];
|
||||
for (const p of positionen) {
|
||||
const menge = parseFloat(String(p.menge)) || 0;
|
||||
return [p.bezeichnung, p.artikelnummer || '', `${menge} ${p.einheit}`];
|
||||
});
|
||||
rows.push([p.bezeichnung, p.artikelnummer || '', `${menge} ${p.einheit}`]);
|
||||
for (const spec of p.spezifikationen || []) {
|
||||
rows.push([` • ${spec}`, '', '']);
|
||||
}
|
||||
}
|
||||
|
||||
autoTable(doc, {
|
||||
head: [['Bezeichnung', 'Art.-Nr.', 'Menge']],
|
||||
@@ -511,6 +535,16 @@ export default function BestellungDetail() {
|
||||
1: { cellWidth: 40 },
|
||||
2: { cellWidth: 33, halign: 'right' },
|
||||
},
|
||||
didParseCell: (data: any) => {
|
||||
if (data.section === 'body') {
|
||||
const cell0 = String(data.row.raw[0] ?? '');
|
||||
if (cell0.startsWith(' •')) {
|
||||
data.cell.styles.fontSize = 8;
|
||||
data.cell.styles.textColor = [100, 100, 100];
|
||||
data.cell.styles.fillColor = [255, 255, 255];
|
||||
}
|
||||
}
|
||||
},
|
||||
didDrawPage: addPdfFooter(doc, settings),
|
||||
});
|
||||
}
|
||||
@@ -581,10 +615,16 @@ export default function BestellungDetail() {
|
||||
</IconButton>
|
||||
<Typography variant="h4" sx={{ flexGrow: 1 }}>{bestellung.bezeichnung}</Typography>
|
||||
{canExport && !editMode && (
|
||||
<Tooltip title="PDF Export">
|
||||
<IconButton onClick={generateBestellungDetailPdf} color="primary">
|
||||
<PdfIcon />
|
||||
</IconButton>
|
||||
<Tooltip title={bestellung.status === 'entwurf' || bestellung.status === 'wartet_auf_genehmigung' ? 'Export erst nach Genehmigung verfügbar' : 'PDF Export'}>
|
||||
<span>
|
||||
<IconButton
|
||||
onClick={generateBestellungDetailPdf}
|
||||
color="primary"
|
||||
disabled={bestellung.status === 'entwurf' || bestellung.status === 'wartet_auf_genehmigung'}
|
||||
>
|
||||
<PdfIcon />
|
||||
</IconButton>
|
||||
</span>
|
||||
</Tooltip>
|
||||
)}
|
||||
{canCreate && !editMode && (
|
||||
@@ -662,41 +702,39 @@ export default function BestellungDetail() {
|
||||
)}
|
||||
|
||||
{/* ── Status Action ── */}
|
||||
{canManageOrders && (
|
||||
{(canManageOrders || canCreate || canApprove) && (
|
||||
<Box sx={{ mb: 3, display: 'flex', gap: 1, alignItems: 'center' }}>
|
||||
{validTransitions.length === 1 ? (
|
||||
<Button variant="contained" onClick={() => { setStatusForce(false); setStatusConfirmTarget(validTransitions[0]); }}>
|
||||
Status ändern: {BESTELLUNG_STATUS_LABELS[validTransitions[0]]}
|
||||
</Button>
|
||||
) : validTransitions.length > 1 ? (
|
||||
<>
|
||||
<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);
|
||||
setStatusForce(false);
|
||||
setStatusConfirmTarget(s);
|
||||
}}
|
||||
>
|
||||
{BESTELLUNG_STATUS_LABELS[s]}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Menu>
|
||||
</>
|
||||
) : null}
|
||||
{validTransitions
|
||||
.filter((s) => {
|
||||
// Approve/reject transitions from wartet_auf_genehmigung require canApprove
|
||||
if (bestellung.status === 'wartet_auf_genehmigung') {
|
||||
return canApprove;
|
||||
}
|
||||
// Transition to bereit_zur_bestellung from other states also requires canApprove
|
||||
if (s === 'bereit_zur_bestellung') return canApprove;
|
||||
// All other transitions require canCreate or canManageOrders
|
||||
return canCreate || canManageOrders;
|
||||
})
|
||||
.map((s) => {
|
||||
const isApprove = bestellung.status === 'wartet_auf_genehmigung' && s === 'bereit_zur_bestellung';
|
||||
const isReject = bestellung.status === 'wartet_auf_genehmigung' && s === 'entwurf';
|
||||
const label = isApprove
|
||||
? 'Genehmigen'
|
||||
: isReject
|
||||
? 'Ablehnen'
|
||||
: `Status: ${BESTELLUNG_STATUS_LABELS[s]}`;
|
||||
const color = isApprove ? 'success' : isReject ? 'error' : 'primary';
|
||||
return (
|
||||
<Button
|
||||
key={s}
|
||||
variant="contained"
|
||||
color={color as 'success' | 'error' | 'primary'}
|
||||
onClick={() => { setStatusForce(false); setStatusConfirmTarget(s); }}
|
||||
>
|
||||
{label}
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* Manual override menu */}
|
||||
{overrideStatuses.length > 0 && canManageOrders && (
|
||||
@@ -771,29 +809,30 @@ export default function BestellungDetail() {
|
||||
<TableBody>
|
||||
{positionen.map((p) =>
|
||||
editMode ? (
|
||||
<TableRow key={p.id}>
|
||||
<React.Fragment key={p.id}>
|
||||
<TableRow key={`${p.id}-row`}>
|
||||
<TableCell>
|
||||
<TextField size="small" value={editItemsData[p.id]?.bezeichnung ?? p.bezeichnung}
|
||||
onChange={(e) => setEditItemsData(d => ({ ...d, [p.id]: { ...(d[p.id] || { bezeichnung: p.bezeichnung, artikelnummer: p.artikelnummer || '', menge: parseFloat(String(p.menge)) || 1, einheit: p.einheit, einzelpreis: p.einzelpreis != null ? parseFloat(String(p.einzelpreis)) : undefined }), bezeichnung: e.target.value } }))} />
|
||||
onChange={(e) => setEditItemsData(d => ({ ...d, [p.id]: { ...(d[p.id] || { bezeichnung: p.bezeichnung, artikelnummer: p.artikelnummer || '', menge: parseFloat(String(p.menge)) || 1, einheit: p.einheit, einzelpreis: p.einzelpreis != null ? parseFloat(String(p.einzelpreis)) : undefined, spezifikationen: p.spezifikationen || [] }), bezeichnung: e.target.value } }))} />
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<TextField size="small" value={editItemsData[p.id]?.artikelnummer ?? p.artikelnummer ?? ''}
|
||||
onChange={(e) => setEditItemsData(d => ({ ...d, [p.id]: { ...(d[p.id] || { bezeichnung: p.bezeichnung, artikelnummer: p.artikelnummer || '', menge: parseFloat(String(p.menge)) || 1, einheit: p.einheit, einzelpreis: p.einzelpreis != null ? parseFloat(String(p.einzelpreis)) : undefined }), artikelnummer: e.target.value } }))} />
|
||||
onChange={(e) => setEditItemsData(d => ({ ...d, [p.id]: { ...(d[p.id] || { bezeichnung: p.bezeichnung, artikelnummer: p.artikelnummer || '', menge: parseFloat(String(p.menge)) || 1, einheit: p.einheit, einzelpreis: p.einzelpreis != null ? parseFloat(String(p.einzelpreis)) : undefined, spezifikationen: p.spezifikationen || [] }), artikelnummer: e.target.value } }))} />
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<TextField size="small" type="number" sx={{ width: 80 }}
|
||||
value={editItemsData[p.id]?.menge ?? p.menge}
|
||||
onChange={(e) => setEditItemsData(d => ({ ...d, [p.id]: { ...(d[p.id] || { bezeichnung: p.bezeichnung, artikelnummer: p.artikelnummer || '', menge: parseFloat(String(p.menge)) || 1, einheit: p.einheit, einzelpreis: p.einzelpreis != null ? parseFloat(String(p.einzelpreis)) : undefined }), menge: Number(e.target.value) } }))} />
|
||||
onChange={(e) => setEditItemsData(d => ({ ...d, [p.id]: { ...(d[p.id] || { bezeichnung: p.bezeichnung, artikelnummer: p.artikelnummer || '', menge: parseFloat(String(p.menge)) || 1, einheit: p.einheit, einzelpreis: p.einzelpreis != null ? parseFloat(String(p.einzelpreis)) : undefined, spezifikationen: p.spezifikationen || [] }), menge: Number(e.target.value) } }))} />
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<TextField size="small" sx={{ width: 80 }}
|
||||
value={editItemsData[p.id]?.einheit ?? p.einheit}
|
||||
onChange={(e) => setEditItemsData(d => ({ ...d, [p.id]: { ...(d[p.id] || { bezeichnung: p.bezeichnung, artikelnummer: p.artikelnummer || '', menge: parseFloat(String(p.menge)) || 1, einheit: p.einheit, einzelpreis: p.einzelpreis != null ? parseFloat(String(p.einzelpreis)) : undefined }), einheit: e.target.value } }))} />
|
||||
onChange={(e) => setEditItemsData(d => ({ ...d, [p.id]: { ...(d[p.id] || { bezeichnung: p.bezeichnung, artikelnummer: p.artikelnummer || '', menge: parseFloat(String(p.menge)) || 1, einheit: p.einheit, einzelpreis: p.einzelpreis != null ? parseFloat(String(p.einzelpreis)) : undefined, spezifikationen: p.spezifikationen || [] }), einheit: e.target.value } }))} />
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<TextField size="small" type="number" sx={{ width: 100 }}
|
||||
value={editItemsData[p.id]?.einzelpreis ?? p.einzelpreis ?? ''}
|
||||
onChange={(e) => setEditItemsData(d => ({ ...d, [p.id]: { ...(d[p.id] || { bezeichnung: p.bezeichnung, artikelnummer: p.artikelnummer || '', menge: parseFloat(String(p.menge)) || 1, einheit: p.einheit, einzelpreis: p.einzelpreis != null ? parseFloat(String(p.einzelpreis)) : undefined }), einzelpreis: e.target.value ? Number(e.target.value) : undefined } }))} />
|
||||
onChange={(e) => setEditItemsData(d => ({ ...d, [p.id]: { ...(d[p.id] || { bezeichnung: p.bezeichnung, artikelnummer: p.artikelnummer || '', menge: parseFloat(String(p.menge)) || 1, einheit: p.einheit, einzelpreis: p.einzelpreis != null ? parseFloat(String(p.einzelpreis)) : undefined, spezifikationen: p.spezifikationen || [] }), einzelpreis: e.target.value ? Number(e.target.value) : undefined } }))} />
|
||||
</TableCell>
|
||||
<TableCell align="right">
|
||||
{formatCurrency((editItemsData[p.id]?.einzelpreis ?? parseFloat(String(p.einzelpreis ?? 0))) * (editItemsData[p.id]?.menge ?? parseFloat(String(p.menge))))}
|
||||
@@ -802,6 +841,7 @@ export default function BestellungDetail() {
|
||||
{canManageOrders ? (
|
||||
<TextField size="small" type="number" sx={{ width: 70 }} value={p.erhalten_menge}
|
||||
inputProps={{ min: 0, max: p.menge }}
|
||||
disabled={bestellung.status !== 'bestellt' && bestellung.status !== 'teillieferung'}
|
||||
onChange={(e) => updateReceived.mutate({ itemId: p.id, menge: Number(e.target.value) })} />
|
||||
) : p.erhalten_menge}
|
||||
</TableCell>
|
||||
@@ -815,9 +855,61 @@ export default function BestellungDetail() {
|
||||
</TableCell>
|
||||
)}
|
||||
</TableRow>
|
||||
{/* Specifications editor row */}
|
||||
<TableRow key={`${p.id}-specs`}>
|
||||
<TableCell colSpan={8} sx={{ pt: 0, pb: 1, pl: 4 }}>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.5 }}>
|
||||
{(editItemsData[p.id]?.spezifikationen || []).map((spec, specIdx) => (
|
||||
<Box key={specIdx} sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<TextField
|
||||
size="small"
|
||||
value={spec}
|
||||
placeholder="Spezifikation"
|
||||
sx={{ flexGrow: 1 }}
|
||||
onChange={(e) => setEditItemsData(d => {
|
||||
const cur = d[p.id]?.spezifikationen ? [...d[p.id].spezifikationen] : [];
|
||||
cur[specIdx] = e.target.value;
|
||||
return { ...d, [p.id]: { ...d[p.id], spezifikationen: cur } };
|
||||
})}
|
||||
/>
|
||||
<IconButton size="small" color="error" onClick={() => setEditItemsData(d => {
|
||||
const cur = d[p.id]?.spezifikationen ? [...d[p.id].spezifikationen] : [];
|
||||
cur.splice(specIdx, 1);
|
||||
return { ...d, [p.id]: { ...d[p.id], spezifikationen: cur } };
|
||||
})}>
|
||||
<DeleteIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Box>
|
||||
))}
|
||||
<Button
|
||||
size="small"
|
||||
startIcon={<AddIcon />}
|
||||
onClick={() => setEditItemsData(d => {
|
||||
const cur = d[p.id]?.spezifikationen ? [...d[p.id].spezifikationen] : [];
|
||||
return { ...d, [p.id]: { ...d[p.id], spezifikationen: [...cur, ''] } };
|
||||
})}
|
||||
sx={{ alignSelf: 'flex-start' }}
|
||||
>
|
||||
Spezifikation hinzufügen
|
||||
</Button>
|
||||
</Box>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</React.Fragment>
|
||||
) : (
|
||||
<TableRow key={p.id}>
|
||||
<TableCell>{p.bezeichnung}</TableCell>
|
||||
<TableCell>
|
||||
<Box>
|
||||
{p.bezeichnung}
|
||||
{p.spezifikationen && p.spezifikationen.length > 0 && (
|
||||
<Box sx={{ pl: 2, mt: 0.5 }}>
|
||||
{p.spezifikationen.map((spec, i) => (
|
||||
<Typography key={i} variant="caption" color="text.secondary" display="block">• {spec}</Typography>
|
||||
))}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
</TableCell>
|
||||
<TableCell>{p.artikelnummer || '–'}</TableCell>
|
||||
<TableCell align="right">{p.menge}</TableCell>
|
||||
<TableCell>{p.einheit}</TableCell>
|
||||
@@ -831,6 +923,7 @@ export default function BestellungDetail() {
|
||||
sx={{ width: 70 }}
|
||||
value={p.erhalten_menge}
|
||||
inputProps={{ min: 0, max: p.menge }}
|
||||
disabled={bestellung.status !== 'bestellt' && bestellung.status !== 'teillieferung'}
|
||||
onChange={(e) => updateReceived.mutate({ itemId: p.id, menge: Number(e.target.value) })}
|
||||
/>
|
||||
) : (
|
||||
|
||||
@@ -59,7 +59,7 @@ const TAB_COUNT = 2;
|
||||
|
||||
// ── Status options ──
|
||||
|
||||
const ALL_STATUSES: BestellungStatus[] = ['entwurf', 'erstellt', 'bestellt', 'teillieferung', 'vollstaendig', 'abgeschlossen'];
|
||||
const ALL_STATUSES: BestellungStatus[] = ['entwurf', 'wartet_auf_genehmigung', 'bereit_zur_bestellung', 'bestellt', 'teillieferung', 'lieferung_pruefen', 'abgeschlossen'];
|
||||
const DEFAULT_EXCLUDED_STATUSES: BestellungStatus[] = ['abgeschlossen'];
|
||||
|
||||
// ── Kennung formatter ──
|
||||
@@ -262,10 +262,10 @@ export default function Bestellungen() {
|
||||
{/* ── Summary Cards ── */}
|
||||
<Grid container spacing={2} sx={{ mb: 3 }}>
|
||||
{[
|
||||
{ label: 'Noch nicht bestellt', count: orders.filter(o => o.status === 'entwurf' || o.status === 'erstellt').length, color: 'text.secondary' },
|
||||
{ label: 'Bestellt', count: orders.filter(o => o.status === 'bestellt').length, color: 'primary.main' },
|
||||
{ label: 'Entwurf / Genehmigung', count: orders.filter(o => o.status === 'entwurf' || o.status === 'wartet_auf_genehmigung').length, color: 'text.secondary' },
|
||||
{ label: 'Bereit / Bestellt', count: orders.filter(o => o.status === 'bereit_zur_bestellung' || o.status === 'bestellt').length, color: 'primary.main' },
|
||||
{ label: 'Teillieferung', count: orders.filter(o => o.status === 'teillieferung').length, color: 'warning.main' },
|
||||
{ label: 'Vollständig', count: orders.filter(o => o.status === 'vollstaendig').length, color: 'success.main' },
|
||||
{ label: 'Lieferung prüfen', count: orders.filter(o => o.status === 'lieferung_pruefen').length, color: 'secondary.main' },
|
||||
{ label: 'Gesamt', count: orders.length, color: 'text.primary' },
|
||||
].map(({ label, count, color }) => (
|
||||
<Grid item xs={6} sm={4} md={2} key={label}>
|
||||
|
||||
@@ -1732,12 +1732,36 @@ export default function Kalender() {
|
||||
setCalLoading(true);
|
||||
setCalError(null);
|
||||
try {
|
||||
const firstDay = new Date(viewMonth.year, viewMonth.month, 1);
|
||||
const dayOfWeek = (firstDay.getDay() + 6) % 7;
|
||||
const gridStart = new Date(firstDay);
|
||||
gridStart.setDate(gridStart.getDate() - dayOfWeek);
|
||||
const gridEnd = new Date(gridStart);
|
||||
gridEnd.setDate(gridStart.getDate() + 41);
|
||||
let gridStart: Date;
|
||||
let gridEnd: Date;
|
||||
|
||||
if (viewMode === 'day') {
|
||||
// Fetch the full month containing currentDate (plus padding)
|
||||
const monthStart = startOfMonth(currentDate);
|
||||
const dayOfWeek = (monthStart.getDay() + 6) % 7;
|
||||
gridStart = subDays(monthStart, dayOfWeek);
|
||||
gridEnd = addDays(gridStart, 41);
|
||||
} else if (viewMode === 'week') {
|
||||
// Fetch the month containing the current week
|
||||
const weekStart = startOfWeek(currentDate, { weekStartsOn: 1 });
|
||||
const weekEnd = endOfWeek(currentDate, { weekStartsOn: 1 });
|
||||
const monthStart = startOfMonth(weekStart);
|
||||
const monthEnd = endOfMonth(weekEnd);
|
||||
gridStart = subDays(monthStart, 7);
|
||||
gridEnd = addDays(monthEnd, 7);
|
||||
} else if (viewMode === 'list') {
|
||||
// Fetch from listFrom to listTo, with padding
|
||||
gridStart = subDays(parseISO(listFrom), 1);
|
||||
gridEnd = addDays(parseISO(listTo), 1);
|
||||
} else {
|
||||
// Month view: 42-day grid based on viewMonth
|
||||
const firstDay = new Date(viewMonth.year, viewMonth.month, 1);
|
||||
const dayOfWeek = (firstDay.getDay() + 6) % 7;
|
||||
gridStart = new Date(firstDay);
|
||||
gridStart.setDate(gridStart.getDate() - dayOfWeek);
|
||||
gridEnd = new Date(gridStart);
|
||||
gridEnd.setDate(gridStart.getDate() + 41);
|
||||
}
|
||||
|
||||
const [trainData, eventData] = await Promise.all([
|
||||
trainingApi.getCalendarRange(gridStart, gridEnd),
|
||||
@@ -1750,7 +1774,7 @@ export default function Kalender() {
|
||||
} finally {
|
||||
setCalLoading(false);
|
||||
}
|
||||
}, [viewMonth]);
|
||||
}, [viewMonth, viewMode, currentDate, listFrom, listTo]);
|
||||
|
||||
// Load kategorien + groups once
|
||||
useEffect(() => {
|
||||
|
||||
Reference in New Issue
Block a user