fix(ausruestung): show untracked assignments, item traits in order wizard, receipt
gate for completion, PDF phone + last-row line
This commit is contained in:
@@ -190,6 +190,7 @@ async function getOrderById(id: number) {
|
|||||||
l.adresse AS lieferant_adresse,
|
l.adresse AS lieferant_adresse,
|
||||||
COALESCE(u.name, u.preferred_username, u.email) AS besteller_name,
|
COALESCE(u.name, u.preferred_username, u.email) AS besteller_name,
|
||||||
u.email AS besteller_email,
|
u.email AS besteller_email,
|
||||||
|
COALESCE(mp.telefon_mobil, mp.telefon_privat) AS besteller_telefon,
|
||||||
mp.dienstgrad AS besteller_dienstgrad
|
mp.dienstgrad AS besteller_dienstgrad
|
||||||
FROM bestellungen b
|
FROM bestellungen b
|
||||||
LEFT JOIN lieferanten l ON l.id = b.lieferant_id
|
LEFT JOIN lieferanten l ON l.id = b.lieferant_id
|
||||||
|
|||||||
@@ -429,6 +429,15 @@ export default function AusruestungsanfrageDetail() {
|
|||||||
{p.geliefert && detail?.im_haus && (
|
{p.geliefert && detail?.im_haus && (
|
||||||
<Chip label="Im Haus" size="small" color="success" />
|
<Chip label="Im Haus" size="small" color="success" />
|
||||||
)}
|
)}
|
||||||
|
{p.geliefert && p.zuweisung_typ === 'keine' && (
|
||||||
|
<Chip label="Nicht verfolgt" size="small" color="default" variant="outlined" />
|
||||||
|
)}
|
||||||
|
{p.geliefert && p.zuweisung_typ === 'persoenlich' && (
|
||||||
|
<Chip label="Persönlich" size="small" color="primary" variant="outlined" />
|
||||||
|
)}
|
||||||
|
{p.geliefert && p.zuweisung_typ === 'ausruestung' && (
|
||||||
|
<Chip label="Ausrüstung" size="small" color="success" variant="outlined" />
|
||||||
|
)}
|
||||||
{p.eigenschaften && p.eigenschaften.length > 0 && p.eigenschaften.map(e => (
|
{p.eigenschaften && p.eigenschaften.length > 0 && p.eigenschaften.map(e => (
|
||||||
<Chip key={e.eigenschaft_id} label={`${e.eigenschaft_name}: ${e.wert}`} size="small" variant="outlined" />
|
<Chip key={e.eigenschaft_id} label={`${e.eigenschaft_name}: ${e.wert}`} size="small" variant="outlined" />
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import {
|
|||||||
Table, TableBody, TableCell, TableHead, TableRow,
|
Table, TableBody, TableCell, TableHead, TableRow,
|
||||||
Autocomplete, TextField, Alert, CircularProgress,
|
Autocomplete, TextField, Alert, CircularProgress,
|
||||||
Dialog, DialogTitle, DialogContent, DialogActions,
|
Dialog, DialogTitle, DialogContent, DialogActions,
|
||||||
LinearProgress,
|
LinearProgress, FormControlLabel, Switch,
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import {
|
import {
|
||||||
ArrowBack,
|
ArrowBack,
|
||||||
@@ -85,6 +85,7 @@ export default function AusruestungsanfrageZuBestellung() {
|
|||||||
const [assignments, setAssignments] = useState<Record<number, VendorAssignment | null>>({});
|
const [assignments, setAssignments] = useState<Record<number, VendorAssignment | null>>({});
|
||||||
const [orderNames, setOrderNames] = useState<Record<number, string>>({});
|
const [orderNames, setOrderNames] = useState<Record<number, string>>({});
|
||||||
const [quantities, setQuantities] = useState<Record<number, number>>({});
|
const [quantities, setQuantities] = useState<Record<number, number>>({});
|
||||||
|
const [istErsatzOverrides, setIstErsatzOverrides] = useState<Record<number, boolean>>({});
|
||||||
|
|
||||||
// New vendor dialog
|
// New vendor dialog
|
||||||
const [newVendorDialog, setNewVendorDialog] = useState(false);
|
const [newVendorDialog, setNewVendorDialog] = useState(false);
|
||||||
@@ -301,6 +302,24 @@ export default function AusruestungsanfrageZuBestellung() {
|
|||||||
{pos.notizen && (
|
{pos.notizen && (
|
||||||
<Typography variant="caption" color="text.secondary">{pos.notizen}</Typography>
|
<Typography variant="caption" color="text.secondary">{pos.notizen}</Typography>
|
||||||
)}
|
)}
|
||||||
|
{pos.eigenschaften && pos.eigenschaften.length > 0 && (
|
||||||
|
<Box sx={{ display: 'flex', gap: 0.5, flexWrap: 'wrap', mt: 0.5 }}>
|
||||||
|
{pos.eigenschaften.map(e => (
|
||||||
|
<Chip key={e.eigenschaft_id} label={`${e.eigenschaft_name}: ${e.wert}`} size="small" variant="outlined" />
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
<FormControlLabel
|
||||||
|
sx={{ mt: 0.5 }}
|
||||||
|
control={
|
||||||
|
<Switch
|
||||||
|
size="small"
|
||||||
|
checked={istErsatzOverrides[pos.id] ?? pos.ist_ersatz ?? false}
|
||||||
|
onChange={(e) => setIstErsatzOverrides(prev => ({ ...prev, [pos.id]: e.target.checked }))}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label={<Typography variant="caption">Ersatzbeschaffung</Typography>}
|
||||||
|
/>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell align="right">
|
<TableCell align="right">
|
||||||
<Typography variant="body2">{pos.menge} {pos.einheit ?? 'Stk'}</Typography>
|
<Typography variant="body2">{pos.menge} {pos.einheit ?? 'Stk'}</Typography>
|
||||||
@@ -385,7 +404,10 @@ export default function AusruestungsanfrageZuBestellung() {
|
|||||||
<Box sx={{ mb: 1.5 }}>
|
<Box sx={{ mb: 1.5 }}>
|
||||||
{g.positionen.map(p => (
|
{g.positionen.map(p => (
|
||||||
<Typography key={p.id} variant="body2" sx={{ py: 0.25 }}>
|
<Typography key={p.id} variant="body2" sx={{ py: 0.25 }}>
|
||||||
· {p.bezeichnung} ×{p.menge}
|
· {p.bezeichnung} ×{quantities[p.id] ?? p.menge}
|
||||||
|
{(istErsatzOverrides[p.id] ?? p.ist_ersatz) && (
|
||||||
|
<Chip label="Ersatz" size="small" color="warning" sx={{ ml: 0.5, height: 16, fontSize: 10 }} />
|
||||||
|
)}
|
||||||
</Typography>
|
</Typography>
|
||||||
))}
|
))}
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -203,6 +203,7 @@ export default function BestellungDetail() {
|
|||||||
const canExport = hasPermission('bestellungen:export');
|
const canExport = hasPermission('bestellungen:export');
|
||||||
const validTransitions = bestellung ? STATUS_TRANSITIONS[bestellung.status] : [];
|
const validTransitions = bestellung ? STATUS_TRANSITIONS[bestellung.status] : [];
|
||||||
const allCostsEntered = positionen.length === 0 || positionen.every(p => p.einzelpreis != null && Number(p.einzelpreis) > 0);
|
const allCostsEntered = positionen.length === 0 || positionen.every(p => p.einzelpreis != null && Number(p.einzelpreis) > 0);
|
||||||
|
const allItemsReceived = positionen.length === 0 || positionen.every(p => Number(p.erhalten_menge) >= Number(p.menge));
|
||||||
|
|
||||||
// All statuses except current, for force override
|
// All statuses except current, for force override
|
||||||
const ALL_STATUSES: BestellungStatus[] = ['entwurf', 'wartet_auf_genehmigung', 'bereit_zur_bestellung', 'bestellt', 'teillieferung', 'lieferung_pruefen', 'abgeschlossen'];
|
const ALL_STATUSES: BestellungStatus[] = ['entwurf', 'wartet_auf_genehmigung', 'bereit_zur_bestellung', 'bestellt', 'teillieferung', 'lieferung_pruefen', 'abgeschlossen'];
|
||||||
@@ -475,6 +476,7 @@ export default function BestellungDetail() {
|
|||||||
: bestellung.besteller_name;
|
: bestellung.besteller_name;
|
||||||
row('Name', nameWithRank);
|
row('Name', nameWithRank);
|
||||||
if (bestellung.besteller_email) row('E-Mail', bestellung.besteller_email);
|
if (bestellung.besteller_email) row('E-Mail', bestellung.besteller_email);
|
||||||
|
if (bestellung.besteller_telefon) row('Telefon', bestellung.besteller_telefon);
|
||||||
curY += 3;
|
curY += 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -563,6 +565,17 @@ export default function BestellungDetail() {
|
|||||||
data.cell.y,
|
data.cell.y,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
// Line at bottom of last row
|
||||||
|
if (data.row.index === rows.length - 1) {
|
||||||
|
data.doc.setDrawColor(200, 200, 200);
|
||||||
|
data.doc.setLineWidth(0.2);
|
||||||
|
data.doc.line(
|
||||||
|
data.settings.margin.left,
|
||||||
|
data.cell.y + data.cell.height,
|
||||||
|
data.doc.internal.pageSize.width - data.settings.margin.right,
|
||||||
|
data.cell.y + data.cell.height,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
foot: [
|
foot: [
|
||||||
@@ -616,6 +629,17 @@ export default function BestellungDetail() {
|
|||||||
data.cell.y,
|
data.cell.y,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
// Line at bottom of last row
|
||||||
|
if (data.row.index === rows.length - 1) {
|
||||||
|
data.doc.setDrawColor(200, 200, 200);
|
||||||
|
data.doc.setLineWidth(0.2);
|
||||||
|
data.doc.line(
|
||||||
|
data.settings.margin.left,
|
||||||
|
data.cell.y + data.cell.height,
|
||||||
|
data.doc.internal.pageSize.width - data.settings.margin.right,
|
||||||
|
data.cell.y + data.cell.height,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
didDrawPage: addPdfFooter(doc, settings),
|
didDrawPage: addPdfFooter(doc, settings),
|
||||||
@@ -786,6 +810,9 @@ export default function BestellungDetail() {
|
|||||||
{validTransitions.includes('abgeschlossen' as BestellungStatus) && !allCostsEntered && (
|
{validTransitions.includes('abgeschlossen' as BestellungStatus) && !allCostsEntered && (
|
||||||
<Alert severity="warning" sx={{ mb: 2 }}>Nicht alle Positionen haben Kosten. Bitte Einzelpreise eintragen, bevor die Bestellung abgeschlossen wird.</Alert>
|
<Alert severity="warning" sx={{ mb: 2 }}>Nicht alle Positionen haben Kosten. Bitte Einzelpreise eintragen, bevor die Bestellung abgeschlossen wird.</Alert>
|
||||||
)}
|
)}
|
||||||
|
{validTransitions.includes('abgeschlossen' as BestellungStatus) && !allItemsReceived && (
|
||||||
|
<Alert severity="warning" sx={{ mb: 2 }}>Nicht alle Positionen wurden vollständig empfangen. Bitte Eingangsmenge prüfen, bevor die Bestellung abgeschlossen wird.</Alert>
|
||||||
|
)}
|
||||||
<Box sx={{ display: 'flex', gap: 1, alignItems: 'center' }}>
|
<Box sx={{ display: 'flex', gap: 1, alignItems: 'center' }}>
|
||||||
{validTransitions
|
{validTransitions
|
||||||
.filter((s) => {
|
.filter((s) => {
|
||||||
@@ -813,7 +840,7 @@ export default function BestellungDetail() {
|
|||||||
key={s}
|
key={s}
|
||||||
variant="contained"
|
variant="contained"
|
||||||
color={color as 'success' | 'error' | 'primary'}
|
color={color as 'success' | 'error' | 'primary'}
|
||||||
disabled={isAbgeschlossen && !allCostsEntered}
|
disabled={isAbgeschlossen && (!allCostsEntered || !allItemsReceived)}
|
||||||
onClick={() => { setStatusForce(false); setStatusConfirmTarget(s); }}
|
onClick={() => { setStatusForce(false); setStatusConfirmTarget(s); }}
|
||||||
>
|
>
|
||||||
{label}
|
{label}
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ export interface Bestellung {
|
|||||||
besteller_id?: string;
|
besteller_id?: string;
|
||||||
besteller_name?: string;
|
besteller_name?: string;
|
||||||
besteller_email?: string;
|
besteller_email?: string;
|
||||||
|
besteller_telefon?: string;
|
||||||
besteller_dienstgrad?: string;
|
besteller_dienstgrad?: string;
|
||||||
status: BestellungStatus;
|
status: BestellungStatus;
|
||||||
budget?: number;
|
budget?: number;
|
||||||
|
|||||||
Reference in New Issue
Block a user