342 lines
10 KiB
JavaScript
342 lines
10 KiB
JavaScript
import { useEffect, useState } from 'react';
|
|
import { Link as RouterLink, useNavigate, useParams } from 'react-router-dom';
|
|
import Alert from '@mui/material/Alert';
|
|
import Box from '@mui/material/Box';
|
|
import Chip from '@mui/material/Chip';
|
|
import CircularProgress from '@mui/material/CircularProgress';
|
|
import Link from '@mui/material/Link';
|
|
import Paper from '@mui/material/Paper';
|
|
import Table from '@mui/material/Table';
|
|
import TableBody from '@mui/material/TableBody';
|
|
import TableCell from '@mui/material/TableCell';
|
|
import TableHead from '@mui/material/TableHead';
|
|
import TableRow from '@mui/material/TableRow';
|
|
import Typography from '@mui/material/Typography';
|
|
import { article } from '../../api';
|
|
import { formatPrice, getBrutto } from '../../utils/price';
|
|
import { formatStock } from './searchFormat';
|
|
|
|
function DetailRow({ label, children }) {
|
|
if (children == null || children === '') return null;
|
|
return (
|
|
<Box sx={{ display: 'grid', gridTemplateColumns: '160px 1fr', gap: 1, py: 0.75 }}>
|
|
<Typography variant="body2" color="text.secondary">
|
|
{label}
|
|
</Typography>
|
|
<Typography variant="body2" component="div">
|
|
{children}
|
|
</Typography>
|
|
</Box>
|
|
);
|
|
}
|
|
|
|
function Section({ title, children }) {
|
|
if (!children) return null;
|
|
return (
|
|
<Box sx={{ mt: 3 }}>
|
|
<Typography variant="subtitle1" sx={{ fontWeight: 600, mb: 1 }}>
|
|
{title}
|
|
</Typography>
|
|
{children}
|
|
</Box>
|
|
);
|
|
}
|
|
|
|
export function ArticleView({ article: a }) {
|
|
const brutto =
|
|
a.fVKNEtto != null ? getBrutto(a.fVKNEtto, a.kSteuerklasse) : null;
|
|
|
|
return (
|
|
<Box>
|
|
<Box sx={{ display: 'flex', flexWrap: 'wrap', alignItems: 'center', gap: 1, mb: 2 }}>
|
|
<Typography variant="h6" sx={{ fontWeight: 700, mr: 1 }}>
|
|
{a.cName}
|
|
</Typography>
|
|
<Chip
|
|
size="small"
|
|
label={a.cAktiv === 'Y' ? 'Aktiv' : 'Inaktiv'}
|
|
color={a.cAktiv === 'Y' ? 'success' : 'default'}
|
|
variant="outlined"
|
|
/>
|
|
</Box>
|
|
|
|
<Paper variant="outlined" sx={{ p: 2 }}>
|
|
<DetailRow label="Artikelnr.">{a.cArtNr}</DetailRow>
|
|
<DetailRow label="Barcode">{a.cBarcode}</DetailRow>
|
|
<DetailRow label="Hersteller">{a.cHersteller}</DetailRow>
|
|
<DetailRow label="VK Netto">
|
|
{a.fVKNEtto != null ? formatPrice(a.fVKNEtto) : null}
|
|
</DetailRow>
|
|
<DetailRow label="VK Brutto">
|
|
{brutto != null ? formatPrice(brutto) : null}
|
|
</DetailRow>
|
|
<DetailRow label="UVP">{a.fUVP != null ? formatPrice(a.fUVP) : null}</DetailRow>
|
|
<DetailRow label="Bestand">{formatStock(a.fBestand)}</DetailRow>
|
|
<DetailRow label="Garage">{a.garage != null ? formatStock(a.garage) : null}</DetailRow>
|
|
<DetailRow label="Keller">{a.keller != null ? formatStock(a.keller) : null}</DetailRow>
|
|
<DetailRow label="Versandklasse">{a.tVersandklassecName}</DetailRow>
|
|
<DetailRow label="Gewicht">
|
|
{a.fGewicht != null
|
|
? `${new Intl.NumberFormat('de-DE').format(a.fGewicht)} kg`
|
|
: null}
|
|
</DetailRow>
|
|
{a.vater?.cArtNr && (
|
|
<DetailRow label="Vaterartikel">
|
|
<Link component={RouterLink} to={`/artikel/${encodeURIComponent(a.vater.cArtNr)}`}>
|
|
{a.vater.cArtNr}
|
|
</Link>
|
|
</DetailRow>
|
|
)}
|
|
{(a.cEigenschaftName || a.cEigenschaftWertName) && (
|
|
<DetailRow label="Variante">
|
|
{[a.cEigenschaftName, a.cEigenschaftWertName].filter(Boolean).join(': ')}
|
|
</DetailRow>
|
|
)}
|
|
</Paper>
|
|
|
|
{a.cKurzBeschreibung && (
|
|
<Section title="Kurzbeschreibung">
|
|
<Typography variant="body2">{a.cKurzBeschreibung}</Typography>
|
|
</Section>
|
|
)}
|
|
|
|
{a.cBeschreibung && (
|
|
<Section title="Beschreibung">
|
|
<Box
|
|
sx={{ typography: 'body2', '& p': { mt: 0 } }}
|
|
dangerouslySetInnerHTML={{ __html: a.cBeschreibung }}
|
|
/>
|
|
</Section>
|
|
)}
|
|
|
|
{a.EANs?.length > 0 && (
|
|
<Section title="EANs">
|
|
<Typography variant="body2">
|
|
{a.EANs.map((row) => row.cEAN).filter(Boolean).join(', ')}
|
|
</Typography>
|
|
</Section>
|
|
)}
|
|
|
|
{a.KATs?.length > 0 && (
|
|
<Section title="Kategorien">
|
|
<Typography variant="body2">
|
|
{a.KATs.map((row) => row.kKategorie).join(', ')}
|
|
</Typography>
|
|
</Section>
|
|
)}
|
|
|
|
{a.staffel?.length > 0 && (
|
|
<Section title="Staffelpreise">
|
|
<Table size="small">
|
|
<TableHead>
|
|
<TableRow>
|
|
<TableCell>Menge</TableCell>
|
|
<TableCell align="right">VK Brutto</TableCell>
|
|
</TableRow>
|
|
</TableHead>
|
|
<TableBody>
|
|
{a.staffel.map((row, i) => (
|
|
<TableRow key={i}>
|
|
<TableCell>{row.menge}</TableCell>
|
|
<TableCell align="right">{row.fVKBrutto}</TableCell>
|
|
</TableRow>
|
|
))}
|
|
</TableBody>
|
|
</Table>
|
|
</Section>
|
|
)}
|
|
|
|
{a.lief?.length > 0 && (
|
|
<Section title="Lieferanten">
|
|
<Table size="small">
|
|
<TableHead>
|
|
<TableRow>
|
|
<TableCell>Firma</TableCell>
|
|
<TableCell>Lief.-Artikelnr.</TableCell>
|
|
<TableCell align="right">EK Netto</TableCell>
|
|
<TableCell align="right">Lager</TableCell>
|
|
</TableRow>
|
|
</TableHead>
|
|
<TableBody>
|
|
{a.lief.map((row, i) => (
|
|
<TableRow key={i}>
|
|
<TableCell>
|
|
{row.cFirma}
|
|
{row.nStandard ? ' ★' : ''}
|
|
</TableCell>
|
|
<TableCell>{row.cLiefArtNr || '—'}</TableCell>
|
|
<TableCell align="right">
|
|
{row.fEKNetto != null ? formatPrice(row.fEKNetto) : '—'}
|
|
</TableCell>
|
|
<TableCell align="right">{formatStock(row.fLagerbestand)}</TableCell>
|
|
</TableRow>
|
|
))}
|
|
</TableBody>
|
|
</Table>
|
|
</Section>
|
|
)}
|
|
|
|
{a.merkmale?.length > 0 && (
|
|
<Section title="Merkmale">
|
|
<Paper variant="outlined" sx={{ p: 2 }}>
|
|
{a.merkmale.map((row, i) => (
|
|
<DetailRow key={i} label={row.cName}>
|
|
{row.cWert}
|
|
</DetailRow>
|
|
))}
|
|
</Paper>
|
|
</Section>
|
|
)}
|
|
|
|
{a.children?.length > 0 && (
|
|
<Section title="Varianten">
|
|
<Table size="small">
|
|
<TableHead>
|
|
<TableRow>
|
|
<TableCell>Artikelnr.</TableCell>
|
|
<TableCell>Name</TableCell>
|
|
</TableRow>
|
|
</TableHead>
|
|
<TableBody>
|
|
{a.children.map((row) => (
|
|
<TableRow key={row.cArtNr || row.kArtikel} hover>
|
|
<TableCell>
|
|
{row.cArtNr ? (
|
|
<Link
|
|
component={RouterLink}
|
|
to={`/artikel/${encodeURIComponent(row.cArtNr)}`}
|
|
>
|
|
{row.cArtNr}
|
|
</Link>
|
|
) : (
|
|
'—'
|
|
)}
|
|
</TableCell>
|
|
<TableCell>{row.cName || '—'}</TableCell>
|
|
</TableRow>
|
|
))}
|
|
</TableBody>
|
|
</Table>
|
|
</Section>
|
|
)}
|
|
|
|
{a.liste?.length > 0 && (
|
|
<Section title="Stückliste">
|
|
<Table size="small">
|
|
<TableHead>
|
|
<TableRow>
|
|
<TableCell>Artikelnr.</TableCell>
|
|
<TableCell>Name</TableCell>
|
|
<TableCell align="right">Menge</TableCell>
|
|
</TableRow>
|
|
</TableHead>
|
|
<TableBody>
|
|
{a.liste.map((row, i) => (
|
|
<TableRow key={i}>
|
|
<TableCell>
|
|
{row.cArtNr ? (
|
|
<Link
|
|
component={RouterLink}
|
|
to={`/artikel/${encodeURIComponent(row.cArtNr)}`}
|
|
>
|
|
{row.cArtNr}
|
|
</Link>
|
|
) : (
|
|
'—'
|
|
)}
|
|
</TableCell>
|
|
<TableCell>{row.cName || '—'}</TableCell>
|
|
<TableCell align="right">{formatStock(row.fAnzahl ?? row.menge)}</TableCell>
|
|
</TableRow>
|
|
))}
|
|
</TableBody>
|
|
</Table>
|
|
</Section>
|
|
)}
|
|
|
|
{a.cAnmerkung && (
|
|
<Section title="Interne Anmerkung">
|
|
<Typography variant="body2" sx={{ whiteSpace: 'pre-wrap' }}>
|
|
{a.cAnmerkung}
|
|
</Typography>
|
|
</Section>
|
|
)}
|
|
</Box>
|
|
);
|
|
}
|
|
|
|
export default function ArtikelSkuPage() {
|
|
const { sku } = useParams();
|
|
const navigate = useNavigate();
|
|
const target = sku ? decodeURIComponent(sku) : '';
|
|
const [data, setData] = useState(null);
|
|
const [error, setError] = useState(null);
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
useEffect(() => {
|
|
function onKeyDown(e) {
|
|
if (e.key !== 'Escape') return;
|
|
e.preventDefault();
|
|
navigate(-1);
|
|
}
|
|
|
|
document.addEventListener('keydown', onKeyDown);
|
|
return () => document.removeEventListener('keydown', onKeyDown);
|
|
}, [navigate]);
|
|
|
|
useEffect(() => {
|
|
let cancelled = false;
|
|
|
|
async function load() {
|
|
if (!target) {
|
|
setError('Keine Artikelnr. angegeben');
|
|
setData(null);
|
|
setLoading(false);
|
|
return;
|
|
}
|
|
|
|
setLoading(true);
|
|
setError(null);
|
|
try {
|
|
const res = await article.get({ target });
|
|
if (!cancelled) {
|
|
setData(res.article ?? null);
|
|
if (!res.article) setError('Artikel nicht gefunden');
|
|
}
|
|
} catch (err) {
|
|
if (!cancelled) {
|
|
setData(null);
|
|
setError(err.message || 'Artikel konnte nicht geladen werden');
|
|
}
|
|
} finally {
|
|
if (!cancelled) setLoading(false);
|
|
}
|
|
}
|
|
|
|
load();
|
|
return () => {
|
|
cancelled = true;
|
|
};
|
|
}, [target]);
|
|
|
|
if (loading) {
|
|
return (
|
|
<Box sx={{ display: 'flex', justifyContent: 'center', py: 6 }}>
|
|
<CircularProgress />
|
|
</Box>
|
|
);
|
|
}
|
|
|
|
if (error) {
|
|
return (
|
|
<Alert severity="error" sx={{ maxWidth: 640 }}>
|
|
{error}
|
|
</Alert>
|
|
);
|
|
}
|
|
|
|
if (!data) return null;
|
|
|
|
return <ArticleView article={data} />;
|
|
}
|