catalog search/sort, edit-page characteristics, preferred vendor per article
This commit is contained in:
@@ -2,6 +2,7 @@ import { useState, useMemo, useCallback } from 'react';
|
||||
import {
|
||||
Box, Typography, Paper, Button, TextField, IconButton,
|
||||
Chip, MenuItem, Divider, Checkbox, FormControlLabel, LinearProgress,
|
||||
Autocomplete,
|
||||
} from '@mui/material';
|
||||
import {
|
||||
ArrowBack, Edit as EditIcon, Delete as DeleteIcon,
|
||||
@@ -13,6 +14,8 @@ import DashboardLayout from '../components/dashboard/DashboardLayout';
|
||||
import { useNotification } from '../contexts/NotificationContext';
|
||||
import { usePermissionContext } from '../contexts/PermissionContext';
|
||||
import { ausruestungsanfrageApi } from '../services/ausruestungsanfrage';
|
||||
import { bestellungApi } from '../services/bestellung';
|
||||
import type { Lieferant } from '../types/bestellung.types';
|
||||
import type {
|
||||
AusruestungArtikel, AusruestungArtikelFormData,
|
||||
AusruestungEigenschaft, AusruestungKategorie,
|
||||
@@ -29,6 +32,13 @@ function EigenschaftenEditor({ artikelId }: { artikelId: number | null }) {
|
||||
const [newOptionen, setNewOptionen] = useState('');
|
||||
const [newPflicht, setNewPflicht] = useState(false);
|
||||
|
||||
// Inline edit state
|
||||
const [editingId, setEditingId] = useState<number | null>(null);
|
||||
const [editName, setEditName] = useState('');
|
||||
const [editTyp, setEditTyp] = useState<'options' | 'freitext'>('options');
|
||||
const [editOptionen, setEditOptionen] = useState('');
|
||||
const [editPflicht, setEditPflicht] = useState(false);
|
||||
|
||||
const { data: eigenschaften = [] } = useQuery({
|
||||
queryKey: ['ausruestungsanfrage', 'eigenschaften', artikelId],
|
||||
queryFn: () => ausruestungsanfrageApi.getArtikelEigenschaften(artikelId!),
|
||||
@@ -38,7 +48,14 @@ function EigenschaftenEditor({ artikelId }: { artikelId: number | null }) {
|
||||
const upsertMut = useMutation({
|
||||
mutationFn: (data: { eigenschaft_id?: number; name: string; typ: string; optionen?: string[]; pflicht?: boolean; sort_order?: number }) =>
|
||||
ausruestungsanfrageApi.upsertArtikelEigenschaft(artikelId!, data),
|
||||
onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['ausruestungsanfrage', 'eigenschaften', artikelId] }); showSuccess('Eigenschaft gespeichert'); },
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['ausruestungsanfrage', 'eigenschaften', artikelId] });
|
||||
showSuccess('Eigenschaft gespeichert');
|
||||
setNewName('');
|
||||
setNewOptionen('');
|
||||
setNewPflicht(false);
|
||||
setEditingId(null);
|
||||
},
|
||||
onError: () => showError('Fehler beim Speichern'),
|
||||
});
|
||||
|
||||
@@ -58,9 +75,26 @@ function EigenschaftenEditor({ artikelId }: { artikelId: number | null }) {
|
||||
pflicht: newPflicht,
|
||||
sort_order: eigenschaften.length,
|
||||
});
|
||||
setNewName('');
|
||||
setNewOptionen('');
|
||||
setNewPflicht(false);
|
||||
};
|
||||
|
||||
const startEditEigenschaft = (e: AusruestungEigenschaft) => {
|
||||
setEditingId(e.id);
|
||||
setEditName(e.name);
|
||||
setEditTyp(e.typ);
|
||||
setEditOptionen(e.optionen?.join(', ') || '');
|
||||
setEditPflicht(e.pflicht);
|
||||
};
|
||||
|
||||
const handleSaveEdit = (e: AusruestungEigenschaft) => {
|
||||
if (!editName.trim()) return;
|
||||
upsertMut.mutate({
|
||||
eigenschaft_id: e.id,
|
||||
name: editName.trim(),
|
||||
typ: editTyp,
|
||||
optionen: editTyp === 'options' ? editOptionen.split(',').map(s => s.trim()).filter(Boolean) : undefined,
|
||||
pflicht: editPflicht,
|
||||
sort_order: e.sort_order,
|
||||
});
|
||||
};
|
||||
|
||||
if (artikelId == null) return <Typography variant="body2" color="text.secondary" sx={{ mt: 1 }}>Bitte speichern Sie den Artikel zuerst, bevor Sie Eigenschaften hinzufuegen.</Typography>;
|
||||
@@ -69,12 +103,42 @@ function EigenschaftenEditor({ artikelId }: { artikelId: number | null }) {
|
||||
<Box sx={{ mt: 1 }}>
|
||||
<Typography variant="subtitle2" sx={{ mb: 1 }}>Eigenschaften</Typography>
|
||||
{eigenschaften.map(e => (
|
||||
<Box key={e.id} sx={{ display: 'flex', gap: 1, alignItems: 'center', mb: 0.5, pl: 1, borderLeft: '2px solid', borderColor: 'divider' }}>
|
||||
<Typography variant="body2" sx={{ flexGrow: 1 }}>
|
||||
{e.name} ({e.typ === 'options' ? `Auswahl: ${e.optionen?.join(', ')}` : 'Freitext'})
|
||||
{e.pflicht && <Chip label="Pflicht" size="small" sx={{ ml: 0.5 }} />}
|
||||
</Typography>
|
||||
<IconButton size="small" color="error" onClick={() => deleteMut.mutate(e.id)}><DeleteIcon fontSize="small" /></IconButton>
|
||||
<Box key={e.id} sx={{ mb: 0.5, pl: 1, borderLeft: '2px solid', borderColor: 'divider' }}>
|
||||
{editingId === e.id ? (
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1, p: 1, border: '1px dashed', borderColor: 'divider', borderRadius: 1 }}>
|
||||
<Box sx={{ display: 'flex', gap: 1, alignItems: 'center' }}>
|
||||
<TextField size="small" label="Name" value={editName} onChange={ev => setEditName(ev.target.value)} sx={{ flexGrow: 1 }} />
|
||||
<TextField
|
||||
select size="small" label="Typ" value={editTyp}
|
||||
onChange={ev => setEditTyp(ev.target.value as 'options' | 'freitext')}
|
||||
sx={{ minWidth: 120 }}
|
||||
>
|
||||
<MenuItem value="options">Auswahl</MenuItem>
|
||||
<MenuItem value="freitext">Freitext</MenuItem>
|
||||
</TextField>
|
||||
<FormControlLabel
|
||||
control={<Checkbox size="small" checked={editPflicht} onChange={ev => setEditPflicht(ev.target.checked)} />}
|
||||
label="Pflicht"
|
||||
/>
|
||||
</Box>
|
||||
{editTyp === 'options' && (
|
||||
<TextField size="small" label="Optionen (kommagetrennt)" value={editOptionen} onChange={ev => setEditOptionen(ev.target.value)} fullWidth />
|
||||
)}
|
||||
<Box sx={{ display: 'flex', gap: 0.5 }}>
|
||||
<Button size="small" variant="contained" onClick={() => handleSaveEdit(e)} disabled={!editName.trim() || upsertMut.isPending}>Speichern</Button>
|
||||
<Button size="small" onClick={() => setEditingId(null)}>Abbrechen</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
) : (
|
||||
<Box sx={{ display: 'flex', gap: 1, alignItems: 'center' }}>
|
||||
<Typography variant="body2" sx={{ flexGrow: 1 }}>
|
||||
{e.name} ({e.typ === 'options' ? `Auswahl: ${e.optionen?.join(', ')}` : 'Freitext'})
|
||||
{e.pflicht && <Chip label="Pflicht" size="small" sx={{ ml: 0.5 }} />}
|
||||
</Typography>
|
||||
<IconButton size="small" onClick={() => startEditEigenschaft(e)}><EditIcon fontSize="small" /></IconButton>
|
||||
<IconButton size="small" color="error" onClick={() => deleteMut.mutate(e.id)}><DeleteIcon fontSize="small" /></IconButton>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
))}
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1, mt: 1, p: 1, border: '1px dashed', borderColor: 'divider', borderRadius: 1 }}>
|
||||
@@ -158,6 +222,12 @@ export default function AusruestungsanfrageArtikelDetail() {
|
||||
enabled: artikelId != null,
|
||||
});
|
||||
|
||||
const { data: lieferanten = [] } = useQuery<Lieferant[]>({
|
||||
queryKey: ['bestellungen', 'lieferanten'],
|
||||
queryFn: () => bestellungApi.getVendors(),
|
||||
enabled: editing || isCreate,
|
||||
});
|
||||
|
||||
const topKategorien = useMemo(() => kategorien.filter(k => !k.parent_id), [kategorien]);
|
||||
const subKategorienOf = useCallback((parentId: number) => kategorien.filter(k => k.parent_id === parentId), [kategorien]);
|
||||
const subKats = useMemo(() => mainKat ? subKategorienOf(mainKat as number) : [], [mainKat, subKategorienOf]);
|
||||
@@ -213,6 +283,7 @@ export default function AusruestungsanfrageArtikelDetail() {
|
||||
bezeichnung: artikel.bezeichnung,
|
||||
beschreibung: artikel.beschreibung,
|
||||
kategorie_id: artikel.kategorie_id ?? null,
|
||||
bevorzugter_lieferant_id: artikel.bevorzugter_lieferant_id ?? null,
|
||||
});
|
||||
const kat = kategorien.find(k => k.id === artikel.kategorie_id);
|
||||
if (kat?.parent_id) {
|
||||
@@ -321,6 +392,15 @@ export default function AusruestungsanfrageArtikelDetail() {
|
||||
</TextField>
|
||||
)}
|
||||
|
||||
<Autocomplete
|
||||
options={lieferanten}
|
||||
getOptionLabel={l => l.name}
|
||||
value={lieferanten.find(l => l.id === form.bevorzugter_lieferant_id) || null}
|
||||
onChange={(_, v) => setForm(f => ({ ...f, bevorzugter_lieferant_id: v?.id ?? null }))}
|
||||
renderInput={params => <TextField {...params} label="Bevorzugter Lieferant (optional)" />}
|
||||
fullWidth
|
||||
/>
|
||||
|
||||
{canManage && <EigenschaftenEditor artikelId={artikelId} />}
|
||||
|
||||
<Divider />
|
||||
@@ -363,6 +443,12 @@ export default function AusruestungsanfrageArtikelDetail() {
|
||||
<Typography variant="caption" color="text.secondary">Erstellt am</Typography>
|
||||
<Typography variant="body2">{new Date(artikel.erstellt_am).toLocaleDateString('de-AT')}</Typography>
|
||||
</Box>
|
||||
{artikel.bevorzugter_lieferant_name && (
|
||||
<Box>
|
||||
<Typography variant="caption" color="text.secondary">Bevorzugter Lieferant</Typography>
|
||||
<Typography variant="body2">{artikel.bevorzugter_lieferant_name}</Typography>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
</Paper>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user