shared catalog in Bestellungen, catalog picker in line items, Ersatzbeschaffung flag, vendor detail flash fix
This commit is contained in:
@@ -4,7 +4,7 @@ import {
|
||||
Table, TableBody, TableCell, TableHead, TableRow,
|
||||
Dialog, DialogTitle, DialogContent, DialogActions, TextField,
|
||||
MenuItem, Select, FormControl, InputLabel, Autocomplete,
|
||||
Checkbox, LinearProgress,
|
||||
Checkbox, LinearProgress, Switch, FormControlLabel, Alert,
|
||||
} from '@mui/material';
|
||||
import {
|
||||
ArrowBack, Add as AddIcon, Delete as DeleteIcon, Edit as EditIcon,
|
||||
@@ -123,6 +123,15 @@ export default function AusruestungsanfrageDetail() {
|
||||
onError: () => showError('Fehler beim Aktualisieren'),
|
||||
});
|
||||
|
||||
const zurueckgegebenMut = useMutation({
|
||||
mutationFn: ({ positionId, zurueckgegeben }: { positionId: number; zurueckgegeben: boolean }) =>
|
||||
ausruestungsanfrageApi.updatePositionZurueckgegeben(positionId, zurueckgegeben),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['ausruestungsanfrage'] });
|
||||
},
|
||||
onError: () => showError('Fehler beim Aktualisieren'),
|
||||
});
|
||||
|
||||
// ── Edit helpers ──
|
||||
const startEditing = () => {
|
||||
if (!detail) return;
|
||||
@@ -134,6 +143,7 @@ export default function AusruestungsanfrageDetail() {
|
||||
menge: p.menge,
|
||||
notizen: p.notizen,
|
||||
eigenschaften: p.eigenschaften?.map(e => ({ eigenschaft_id: e.eigenschaft_id, wert: e.wert })),
|
||||
ist_ersatz: p.ist_ersatz || false,
|
||||
})));
|
||||
const initVals: Record<number, Record<number, string>> = {};
|
||||
detail.positionen.forEach((p, idx) => {
|
||||
@@ -290,6 +300,23 @@ export default function AusruestungsanfrageDetail() {
|
||||
))}
|
||||
</Box>
|
||||
)}
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.5, ml: 1 }}>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
size="small"
|
||||
checked={item.ist_ersatz || false}
|
||||
onChange={(e) => updateEditItem(idx, 'ist_ersatz', e.target.checked)}
|
||||
/>
|
||||
}
|
||||
label="Ersatzbeschaffung"
|
||||
/>
|
||||
{item.ist_ersatz && (
|
||||
<Alert severity="info" sx={{ py: 0, fontSize: '0.8rem' }}>
|
||||
Altes Gerät muss zurückgegeben werden
|
||||
</Alert>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
))}
|
||||
<Button size="small" startIcon={<AddIcon />} onClick={addEditItem}>
|
||||
@@ -376,19 +403,33 @@ export default function AusruestungsanfrageDetail() {
|
||||
)}
|
||||
<TableCell>
|
||||
<Typography variant="body2" fontWeight={500} sx={p.geliefert ? { textDecoration: 'line-through' } : undefined}>{p.bezeichnung}</Typography>
|
||||
{p.eigenschaften && p.eigenschaften.length > 0 && (
|
||||
<Box sx={{ display: 'flex', gap: 0.5, flexWrap: 'wrap', mt: 0.5 }}>
|
||||
{p.eigenschaften.map(e => (
|
||||
<Chip key={e.eigenschaft_id} label={`${e.eigenschaft_name}: ${e.wert}`} size="small" variant="outlined" />
|
||||
))}
|
||||
</Box>
|
||||
)}
|
||||
<Box sx={{ display: 'flex', gap: 0.5, flexWrap: 'wrap', mt: 0.5 }}>
|
||||
{p.ist_ersatz && (
|
||||
<Chip label="Ersatzbeschaffung" size="small" color="warning" variant="outlined" />
|
||||
)}
|
||||
{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" />
|
||||
))}
|
||||
</Box>
|
||||
</TableCell>
|
||||
<TableCell align="right">
|
||||
<Typography variant="body2" fontWeight={600}>{p.menge}x</Typography>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{p.notizen && <Typography variant="caption" color="text.secondary">{p.notizen}</Typography>}
|
||||
{p.notizen && <Typography variant="caption" color="text.secondary" display="block">{p.notizen}</Typography>}
|
||||
{p.ist_ersatz && (
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
size="small"
|
||||
checked={p.altes_geraet_zurueckgegeben}
|
||||
disabled={!showAdminActions || zurueckgegebenMut.isPending}
|
||||
onChange={(_, checked) => zurueckgegebenMut.mutate({ positionId: p.id, zurueckgegeben: checked })}
|
||||
/>
|
||||
}
|
||||
label={<Typography variant="caption">Altes Gerät zurückgegeben</Typography>}
|
||||
/>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
|
||||
@@ -194,6 +194,10 @@ export default function AusruestungsanfrageZuBestellung() {
|
||||
menge: p.menge,
|
||||
einheit: p.einheit,
|
||||
notizen: p.notizen,
|
||||
artikel_id: p.artikel_id,
|
||||
spezifikationen: p.eigenschaften?.length
|
||||
? p.eigenschaften.map(e => `${e.eigenschaft_name}: ${e.wert}`)
|
||||
: undefined,
|
||||
})),
|
||||
}));
|
||||
createOrdersMut.mutate({ orders });
|
||||
|
||||
@@ -51,10 +51,12 @@ import GermanDateField from '../components/shared/GermanDateField';
|
||||
import { useNotification } from '../contexts/NotificationContext';
|
||||
import { usePermissionContext } from '../contexts/PermissionContext';
|
||||
import { bestellungApi } from '../services/bestellung';
|
||||
import { ausruestungsanfrageApi } from '../services/ausruestungsanfrage';
|
||||
import { configApi } from '../services/config';
|
||||
import { addPdfHeader, addPdfFooter } from '../utils/pdfExport';
|
||||
import { BESTELLUNG_STATUS_LABELS, BESTELLUNG_STATUS_COLORS } from '../types/bestellung.types';
|
||||
import type { BestellungStatus, BestellpositionFormData, ErinnerungFormData } from '../types/bestellung.types';
|
||||
import type { AusruestungArtikel, AusruestungEigenschaft } from '../types/ausruestungsanfrage.types';
|
||||
|
||||
// ── Helpers ──
|
||||
|
||||
@@ -159,6 +161,11 @@ export default function BestellungDetail() {
|
||||
const [reminderFormOpen, setReminderFormOpen] = useState(false);
|
||||
const [deleteReminderTarget, setDeleteReminderTarget] = useState<number | null>(null);
|
||||
|
||||
// ── Catalog picker state ──
|
||||
const [selectedKatalogItem, setSelectedKatalogItem] = useState<AusruestungArtikel | null>(null);
|
||||
const [katalogEigenschaften, setKatalogEigenschaften] = useState<AusruestungEigenschaft[]>([]);
|
||||
const [eigenschaftValues, setEigenschaftValues] = useState<Record<number, string>>({});
|
||||
|
||||
// ── Query ──
|
||||
const { data, isLoading, isError, error, refetch } = useQuery({
|
||||
queryKey: ['bestellung', orderId],
|
||||
@@ -184,6 +191,13 @@ export default function BestellungDetail() {
|
||||
enabled: editMode,
|
||||
});
|
||||
|
||||
const { data: katalogItems = [] } = useQuery({
|
||||
queryKey: ['katalogItems'],
|
||||
queryFn: () => bestellungApi.getKatalogItems(),
|
||||
enabled: editMode,
|
||||
staleTime: 5 * 60 * 1000,
|
||||
});
|
||||
|
||||
const canCreate = hasPermission('bestellungen:create');
|
||||
const canDelete = hasPermission('bestellungen:delete');
|
||||
const canManageReminders = hasPermission('bestellungen:manage_reminders');
|
||||
@@ -214,6 +228,9 @@ export default function BestellungDetail() {
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['bestellung', orderId] });
|
||||
setNewItem({ ...emptyItem });
|
||||
setSelectedKatalogItem(null);
|
||||
setKatalogEigenschaften([]);
|
||||
setEigenschaftValues({});
|
||||
showSuccess('Position hinzugefügt');
|
||||
},
|
||||
onError: () => showError('Fehler beim Hinzufügen der Position'),
|
||||
@@ -348,7 +365,21 @@ export default function BestellungDetail() {
|
||||
|
||||
function handleAddItem() {
|
||||
if (!newItem.bezeichnung.trim()) return;
|
||||
addItem.mutate(newItem);
|
||||
// Merge characteristic values into spezifikationen
|
||||
const charSpecs = Object.entries(eigenschaftValues)
|
||||
.filter(([, v]) => v.trim())
|
||||
.map(([eid, v]) => {
|
||||
const eig = katalogEigenschaften.find(e => e.id === Number(eid));
|
||||
return eig ? `${eig.name}: ${v}` : v;
|
||||
});
|
||||
const mergedSpecs = [...(newItem.spezifikationen || []), ...charSpecs];
|
||||
addItem.mutate({
|
||||
...newItem,
|
||||
spezifikationen: mergedSpecs.length > 0 ? mergedSpecs : undefined,
|
||||
});
|
||||
setSelectedKatalogItem(null);
|
||||
setKatalogEigenschaften([]);
|
||||
setEigenschaftValues({});
|
||||
}
|
||||
|
||||
function handleFileSelect(e: React.ChangeEvent<HTMLInputElement>) {
|
||||
@@ -968,9 +999,52 @@ export default function BestellungDetail() {
|
||||
|
||||
{/* ── Add Item Row ── */}
|
||||
{editMode && canCreate && (
|
||||
<>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<TextField size="small" placeholder="Bezeichnung" value={newItem.bezeichnung} onChange={(e) => setNewItem((f) => ({ ...f, bezeichnung: e.target.value }))} />
|
||||
<Autocomplete<AusruestungArtikel, false, false, true>
|
||||
freeSolo
|
||||
size="small"
|
||||
options={katalogItems}
|
||||
getOptionLabel={(o) => typeof o === 'string' ? o : o.bezeichnung}
|
||||
value={selectedKatalogItem || newItem.bezeichnung || ''}
|
||||
onChange={async (_, v) => {
|
||||
if (typeof v === 'string') {
|
||||
setNewItem((f) => ({ ...f, bezeichnung: v, artikel_id: undefined }));
|
||||
setSelectedKatalogItem(null);
|
||||
setKatalogEigenschaften([]);
|
||||
setEigenschaftValues({});
|
||||
} else if (v) {
|
||||
setNewItem((f) => ({ ...f, bezeichnung: v.bezeichnung, artikel_id: v.id }));
|
||||
setSelectedKatalogItem(v);
|
||||
// Load eigenschaften
|
||||
try {
|
||||
const eigs = await ausruestungsanfrageApi.getArtikelEigenschaften(v.id);
|
||||
setKatalogEigenschaften(eigs || []);
|
||||
} catch { setKatalogEigenschaften([]); }
|
||||
setEigenschaftValues({});
|
||||
}
|
||||
}}
|
||||
onInputChange={(_, val, reason) => {
|
||||
if (reason === 'input') {
|
||||
setNewItem((f) => ({ ...f, bezeichnung: val, artikel_id: undefined }));
|
||||
setSelectedKatalogItem(null);
|
||||
setKatalogEigenschaften([]);
|
||||
setEigenschaftValues({});
|
||||
}
|
||||
}}
|
||||
renderInput={(params) => <TextField {...params} size="small" placeholder="Bezeichnung" />}
|
||||
renderOption={(props, option) => (
|
||||
<li {...props} key={option.id}>
|
||||
<Box>
|
||||
<Typography variant="body2">{option.bezeichnung}</Typography>
|
||||
{option.kategorie_name && <Typography variant="caption" color="text.secondary">{option.kategorie_name}</Typography>}
|
||||
</Box>
|
||||
</li>
|
||||
)}
|
||||
isOptionEqualToValue={(o, v) => o.id === (typeof v === 'object' ? v.id : undefined)}
|
||||
sx={{ minWidth: 200 }}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<TextField size="small" placeholder="Artikelnr." value={newItem.artikelnummer || ''} onChange={(e) => setNewItem((f) => ({ ...f, artikelnummer: e.target.value }))} />
|
||||
@@ -992,6 +1066,52 @@ export default function BestellungDetail() {
|
||||
</IconButton>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
{/* Characteristic fields when catalog item selected */}
|
||||
{katalogEigenschaften.length > 0 && (
|
||||
<TableRow>
|
||||
<TableCell colSpan={8} sx={{ pt: 0, pb: 1, borderBottom: 'none' }}>
|
||||
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1, ml: 1, pl: 1.5, borderLeft: '2px solid', borderColor: 'divider' }}>
|
||||
{katalogEigenschaften.map((e) =>
|
||||
e.typ === 'options' && e.optionen?.length ? (
|
||||
<TextField
|
||||
key={e.id}
|
||||
select
|
||||
size="small"
|
||||
label={e.name}
|
||||
required={e.pflicht}
|
||||
value={eigenschaftValues[e.id] || ''}
|
||||
onChange={(ev) => setEigenschaftValues((prev) => ({ ...prev, [e.id]: ev.target.value }))}
|
||||
sx={{ minWidth: 140 }}
|
||||
>
|
||||
<MenuItem value="">—</MenuItem>
|
||||
{e.optionen.map((opt) => <MenuItem key={opt} value={opt}>{opt}</MenuItem>)}
|
||||
</TextField>
|
||||
) : (
|
||||
<TextField
|
||||
key={e.id}
|
||||
size="small"
|
||||
label={e.name}
|
||||
required={e.pflicht}
|
||||
value={eigenschaftValues[e.id] || ''}
|
||||
onChange={(ev) => setEigenschaftValues((prev) => ({ ...prev, [e.id]: ev.target.value }))}
|
||||
sx={{ minWidth: 160 }}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
{selectedKatalogItem?.bevorzugter_lieferant_name && !bestellung?.lieferant_id && (
|
||||
<Chip
|
||||
size="small"
|
||||
label={`Bevorzugter Lieferant: ${selectedKatalogItem.bevorzugter_lieferant_name}`}
|
||||
color="info"
|
||||
variant="outlined"
|
||||
sx={{ alignSelf: 'center' }}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* ── Totals Rows (Netto / MwSt / Brutto) ── */}
|
||||
|
||||
@@ -23,8 +23,10 @@ import {
|
||||
FormGroup,
|
||||
LinearProgress,
|
||||
Divider,
|
||||
TextField,
|
||||
MenuItem,
|
||||
} from '@mui/material';
|
||||
import { Add as AddIcon, ExpandMore as ExpandMoreIcon, FilterList as FilterListIcon, PictureAsPdf as PdfIcon } from '@mui/icons-material';
|
||||
import { Add as AddIcon, ExpandMore as ExpandMoreIcon, FilterList as FilterListIcon, PictureAsPdf as PdfIcon, Search as SearchIcon } from '@mui/icons-material';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||
import DashboardLayout from '../components/dashboard/DashboardLayout';
|
||||
@@ -52,7 +54,7 @@ function TabPanel({ children, value, index }: TabPanelProps) {
|
||||
return <Box sx={{ pt: 3 }}>{children}</Box>;
|
||||
}
|
||||
|
||||
const TAB_COUNT = 2;
|
||||
const TAB_COUNT = 3;
|
||||
|
||||
// ── Status options ──
|
||||
|
||||
@@ -85,6 +87,7 @@ export default function Bestellungen() {
|
||||
const { hasPermission } = usePermissionContext();
|
||||
const canManageVendors = hasPermission('bestellungen:manage_vendors');
|
||||
const canExport = hasPermission('bestellungen:export');
|
||||
const canManageCatalog = hasPermission('ausruestungsanfrage:manage_catalog');
|
||||
|
||||
// Tab from URL
|
||||
const [tab, setTab] = useState(() => {
|
||||
@@ -113,6 +116,26 @@ export default function Bestellungen() {
|
||||
queryFn: bestellungApi.getVendors,
|
||||
});
|
||||
|
||||
// ── Katalog state ──
|
||||
const [katalogSearch, setKatalogSearch] = useState('');
|
||||
const [katalogKategorie, setKatalogKategorie] = useState('');
|
||||
|
||||
const { data: katalogItems = [], isLoading: katalogLoading } = useQuery({
|
||||
queryKey: ['katalogItems', katalogSearch, katalogKategorie],
|
||||
queryFn: () => bestellungApi.getKatalogItems({
|
||||
search: katalogSearch || undefined,
|
||||
kategorie: katalogKategorie || undefined,
|
||||
}),
|
||||
enabled: tab === 2,
|
||||
});
|
||||
|
||||
const { data: katalogKategorien = [] } = useQuery({
|
||||
queryKey: ['katalogKategorien'],
|
||||
queryFn: bestellungApi.getKatalogKategorien,
|
||||
enabled: tab === 2,
|
||||
staleTime: 5 * 60 * 1000,
|
||||
});
|
||||
|
||||
// ── Derive unique filter values from data ──
|
||||
const uniqueVendors = useMemo(() => {
|
||||
const map = new Map<string, string>();
|
||||
@@ -251,6 +274,7 @@ export default function Bestellungen() {
|
||||
<Tabs value={tab} onChange={(_e, v) => { setTab(v); navigate(`/bestellungen?tab=${v}`, { replace: true }); }} variant="scrollable" scrollButtons="auto">
|
||||
<Tab label="Bestellungen" />
|
||||
{canManageVendors && <Tab label="Lieferanten" />}
|
||||
<Tab label="Katalog" />
|
||||
</Tabs>
|
||||
</Box>
|
||||
|
||||
@@ -458,6 +482,82 @@ export default function Bestellungen() {
|
||||
</ChatAwareFab>
|
||||
</TabPanel>
|
||||
)}
|
||||
|
||||
{/* ── Tab 2: Katalog ── */}
|
||||
<TabPanel value={tab} index={canManageVendors ? 2 : 1}>
|
||||
<Box sx={{ display: 'flex', gap: 2, mb: 3 }}>
|
||||
<TextField
|
||||
size="small"
|
||||
placeholder="Suche..."
|
||||
value={katalogSearch}
|
||||
onChange={(e) => setKatalogSearch(e.target.value)}
|
||||
InputProps={{ startAdornment: <SearchIcon fontSize="small" sx={{ mr: 0.5, color: 'text.secondary' }} /> }}
|
||||
sx={{ flex: 1, maxWidth: 400 }}
|
||||
/>
|
||||
<TextField
|
||||
select
|
||||
size="small"
|
||||
label="Kategorie"
|
||||
value={katalogKategorie}
|
||||
onChange={(e) => setKatalogKategorie(e.target.value)}
|
||||
sx={{ minWidth: 180 }}
|
||||
>
|
||||
<MenuItem value="">Alle Kategorien</MenuItem>
|
||||
{katalogKategorien.map((k) => (
|
||||
<MenuItem key={k} value={k}>{k}</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
</Box>
|
||||
|
||||
<TableContainer component={Paper}>
|
||||
<Table size="small">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Bezeichnung</TableCell>
|
||||
<TableCell>Kategorie</TableCell>
|
||||
<TableCell align="right">Geschätzter Preis</TableCell>
|
||||
<TableCell>Bevorzugter Lieferant</TableCell>
|
||||
<TableCell align="right">Eigenschaften</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{katalogLoading ? (
|
||||
<TableRow><TableCell colSpan={5} align="center">Laden...</TableCell></TableRow>
|
||||
) : katalogItems.length === 0 ? (
|
||||
<TableRow><TableCell colSpan={5} align="center">Keine Artikel gefunden</TableCell></TableRow>
|
||||
) : (
|
||||
katalogItems.map((item) => (
|
||||
<TableRow
|
||||
key={item.id}
|
||||
hover
|
||||
sx={{ cursor: 'pointer' }}
|
||||
onClick={() => navigate(`/ausruestungsanfrage/artikel/${item.id}`)}
|
||||
>
|
||||
<TableCell>
|
||||
<Typography variant="body2" fontWeight={500}>{item.bezeichnung}</Typography>
|
||||
{item.beschreibung && (
|
||||
<Typography variant="caption" color="text.secondary" sx={{ display: 'block', maxWidth: 300, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
|
||||
{item.beschreibung}
|
||||
</Typography>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell>{item.kategorie_name || item.kategorie || '–'}</TableCell>
|
||||
<TableCell align="right">{item.geschaetzter_preis != null ? formatCurrency(item.geschaetzter_preis) : '–'}</TableCell>
|
||||
<TableCell>{item.bevorzugter_lieferant_name || '–'}</TableCell>
|
||||
<TableCell align="right">{item.eigenschaften_count ?? 0}</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
|
||||
{canManageCatalog && (
|
||||
<ChatAwareFab onClick={() => navigate('/ausruestungsanfrage/artikel/neu')} aria-label="Neuer Katalogartikel">
|
||||
<AddIcon />
|
||||
</ChatAwareFab>
|
||||
)}
|
||||
</TabPanel>
|
||||
</DashboardLayout>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@ export default function LieferantDetail() {
|
||||
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
||||
|
||||
// ── Query ──
|
||||
const { data: vendor, isLoading, isError } = useQuery({
|
||||
const { data: vendor, isPending, isLoading, isError } = useQuery({
|
||||
queryKey: ['lieferant', vendorId],
|
||||
queryFn: () => bestellungApi.getVendor(vendorId),
|
||||
enabled: !isNew && !!vendorId,
|
||||
@@ -128,7 +128,7 @@ export default function LieferantDetail() {
|
||||
}
|
||||
|
||||
// ── Loading / Error ──
|
||||
if (!isNew && isLoading) {
|
||||
if (!isNew && isPending) {
|
||||
return (
|
||||
<DashboardLayout>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mb: 3 }}>
|
||||
@@ -144,7 +144,7 @@ export default function LieferantDetail() {
|
||||
);
|
||||
}
|
||||
|
||||
if (!isNew && (isError || !vendor)) {
|
||||
if (!isNew && !isPending && (isError || !vendor)) {
|
||||
return (
|
||||
<DashboardLayout>
|
||||
<Box sx={{ p: 4, textAlign: 'center' }}>
|
||||
|
||||
@@ -122,6 +122,10 @@ export const ausruestungsanfrageApi = {
|
||||
await api.patch(`/api/ausruestungsanfragen/positionen/${positionId}/geliefert`, { geliefert });
|
||||
},
|
||||
|
||||
updatePositionZurueckgegeben: async (positionId: number, altes_geraet_zurueckgegeben: boolean): Promise<void> => {
|
||||
await api.patch(`/api/ausruestungsanfragen/positionen/${positionId}/zurueckgegeben`, { altes_geraet_zurueckgegeben });
|
||||
},
|
||||
|
||||
// ── Linking ──
|
||||
linkToOrder: async (anfrageId: number, bestellungId: number): Promise<void> => {
|
||||
await api.post(`/api/ausruestungsanfragen/requests/${anfrageId}/link`, { bestellung_id: bestellungId });
|
||||
|
||||
@@ -12,6 +12,7 @@ import type {
|
||||
ErinnerungFormData,
|
||||
BestellungHistorie,
|
||||
} from '../types/bestellung.types';
|
||||
import type { AusruestungArtikel, AusruestungEigenschaft } from '../types/ausruestungsanfrage.types';
|
||||
|
||||
export const bestellungApi = {
|
||||
// ── Vendors ──
|
||||
@@ -120,4 +121,21 @@ export const bestellungApi = {
|
||||
const r = await api.get('/api/permissions/users-with', { params: { permission: 'bestellungen:create' } });
|
||||
return r.data.data;
|
||||
},
|
||||
|
||||
// ── Catalog ──
|
||||
getKatalogItems: async (filters?: { search?: string; kategorie?: string }): Promise<AusruestungArtikel[]> => {
|
||||
const params = new URLSearchParams();
|
||||
if (filters?.search) params.set('search', filters.search);
|
||||
if (filters?.kategorie) params.set('kategorie', filters.kategorie);
|
||||
const r = await api.get(`/api/bestellungen/katalog/items?${params.toString()}`);
|
||||
return r.data.data;
|
||||
},
|
||||
getKatalogItem: async (id: number): Promise<AusruestungArtikel> => {
|
||||
const r = await api.get(`/api/bestellungen/katalog/items/${id}`);
|
||||
return r.data.data;
|
||||
},
|
||||
getKatalogKategorien: async (): Promise<string[]> => {
|
||||
const r = await api.get('/api/bestellungen/katalog/kategorien');
|
||||
return r.data.data;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -109,6 +109,8 @@ export interface AusruestungAnfragePosition {
|
||||
geliefert: boolean;
|
||||
erstellt_am: string;
|
||||
eigenschaften?: AusruestungPositionEigenschaft[];
|
||||
ist_ersatz: boolean;
|
||||
altes_geraet_zurueckgegeben: boolean;
|
||||
}
|
||||
|
||||
export interface AusruestungAnfrageFormItem {
|
||||
@@ -117,6 +119,7 @@ export interface AusruestungAnfrageFormItem {
|
||||
menge: number;
|
||||
notizen?: string;
|
||||
eigenschaften?: { eigenschaft_id: number; wert: string }[];
|
||||
ist_ersatz?: boolean;
|
||||
}
|
||||
|
||||
// ── API Response Types ──
|
||||
@@ -158,6 +161,8 @@ export interface CreateOrderPositionPayload {
|
||||
menge: number;
|
||||
einheit?: string;
|
||||
notizen?: string;
|
||||
artikel_id?: number;
|
||||
spezifikationen?: string[];
|
||||
}
|
||||
|
||||
export interface CreateOrderPayload {
|
||||
|
||||
@@ -108,6 +108,8 @@ export interface Bestellposition {
|
||||
erstellt_am: string;
|
||||
aktualisiert_am: string;
|
||||
spezifikationen?: string[];
|
||||
artikel_id?: number;
|
||||
artikel_bezeichnung?: string;
|
||||
}
|
||||
|
||||
export interface BestellpositionFormData {
|
||||
@@ -118,6 +120,7 @@ export interface BestellpositionFormData {
|
||||
einzelpreis?: number;
|
||||
notizen?: string;
|
||||
spezifikationen?: string[];
|
||||
artikel_id?: number;
|
||||
}
|
||||
|
||||
// ── File Attachments ──
|
||||
|
||||
Reference in New Issue
Block a user