This commit is contained in:
seb
2026-10-05 17:57:33 +02:00
parent f627ff5b5a
commit 0a1002227c
9 changed files with 548 additions and 69 deletions

53
package-lock.json generated
View File

@@ -18,6 +18,7 @@
"ag-grid-community": "^36.2.0",
"ag-grid-react": "^36.2.0",
"dashgui": "file:../dashgui",
"flexsearch": "^0.8.212",
"jsbarcode": "^3.12.3",
"notistack": "^3.0.2",
"pdfconvert-wasm": "^0.3.0",
@@ -92,23 +93,23 @@
"ag-grid-react": "^36.2.0",
"marked": "^18.0.14",
"prismjs": "^1.30.0",
"react": "^19.2.8",
"react": "^19.3.0",
"react-automat": "file:../automat",
"react-dom": "^19.2.8"
"react-dom": "^19.3.0"
},
"devDependencies": {
"@eslint/js": "^10.0.1",
"@types/node": "^24.13.3",
"@types/react": "^19.2.18",
"@types/react-dom": "^19.2.7",
"@vitejs/plugin-react": "^6.1.1",
"eslint": "^10.10.0",
"@types/node": "^26.6.4",
"@types/react": "^19.3.0",
"@types/react-dom": "^19.3.0",
"@vitejs/plugin-react": "^6.1.2",
"eslint": "^10.12.0",
"eslint-plugin-react-hooks": "^7.1.1",
"eslint-plugin-react-refresh": "^0.5.6",
"globals": "^17.12.0",
"typescript": "~6.0.2",
"typescript-eslint": "^8.69.0",
"vite": "^8.3.0"
"eslint-plugin-react-refresh": "^0.5.7",
"globals": "^17.13.0",
"typescript": "~7.0.2",
"typescript-eslint": "^8.71.0",
"vite": "^8.3.2"
}
},
"node_modules/@babel/code-frame": {
@@ -5277,6 +5278,34 @@
"dev": true,
"license": "ISC"
},
"node_modules/flexsearch": {
"version": "0.8.212",
"resolved": "https://registry.npmjs.org/flexsearch/-/flexsearch-0.8.212.tgz",
"integrity": "sha512-wSyJr1GUWoOOIISRu+X2IXiOcVfg9qqBRyCPRUdLMIGJqPzMo+jMRlvE83t14v1j0dRMEaBbER/adQjp6Du2pw==",
"funding": [
{
"type": "github",
"url": "https://github.com/ts-thomas"
},
{
"type": "paypal",
"url": "https://www.paypal.com/donate/?hosted_button_id=GEVR88FC9BWRW"
},
{
"type": "opencollective",
"url": "https://opencollective.com/flexsearch"
},
{
"type": "patreon",
"url": "https://patreon.com/user?u=96245532"
},
{
"type": "liberapay",
"url": "https://liberapay.com/ts-thomas"
}
],
"license": "Apache-2.0"
},
"node_modules/for-each": {
"version": "0.3.5",
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz",

View File

@@ -25,6 +25,7 @@
"ag-grid-community": "^36.2.0",
"ag-grid-react": "^36.2.0",
"dashgui": "file:../dashgui",
"flexsearch": "^0.8.212",
"jsbarcode": "^3.12.3",
"notistack": "^3.0.2",
"pdfconvert-wasm": "^0.3.0",
@@ -40,7 +41,7 @@
},
"devDependencies": {
"@eslint/js": "^10.0.1",
"@vitejs/plugin-react": "6.1.1",
"@vitejs/plugin-react": "6.1.2",
"eslint": "^10.12.0",
"eslint-plugin-react-hooks": "^7.1.1",
"globals": "^17.13.0",

View File

@@ -11,6 +11,7 @@
*/
import { Automat } from 'react-automat';
import FlexSearch from 'flexsearch';
import articleIndexAutomat from './articleIndexAutomat';
import categoryAutomat from './categoryAutomat';
import { parseFilter } from '../components/article/searchFormat';
@@ -61,28 +62,101 @@ const initialState = {
let articleSearchAutomat;
const { Document, Charset } = FlexSearch;
let flexDocument = null;
let lastIndexedArticles = null;
let articleByInternalId = new Map();
/**
* Matches one index record against a search query (checks tokens in name, sku, brand, eans).
* Lazily constructs and maintains the FlexSearch Document index for the given articles array.
*
* @param {object} row Article index record.
* @param {string} query Lowercased search term.
* @returns {boolean} Whether the record matches.
* @param {Array<object>} articles Current article index items.
* @returns {{ doc: any, byId: Map<number, object> }}
*/
function matches(row, query) {
if (!query) return true;
const tokens = query.split(/\s+/).filter(Boolean);
if (tokens.length === 0) return true;
const name = String(row.name ?? '').toLowerCase();
const sku = String(row.sku ?? '').toLowerCase();
const brand = String(row.brand?.name || row.brandName || '').toLowerCase();
const eans = (row.eans ?? []).map((e) => String(e).toLowerCase());
return tokens.every(
(token) =>
name.includes(token) ||
sku.includes(token) ||
brand.includes(token) ||
eans.some((ean) => ean.includes(token)),
);
function getOrCreateFlexIndex(articles) {
if (flexDocument && lastIndexedArticles === articles) {
return { doc: flexDocument, byId: articleByInternalId };
}
const doc = new Document({
document: {
id: 'id',
index: [
{ field: 'all', tokenize: 'forward', resolution: 9 },
{ field: 'sku', tokenize: 'full', resolution: 9 },
{ field: 'name', tokenize: 'forward', resolution: 9 },
{ field: 'brand', tokenize: 'forward', resolution: 5 },
{ field: 'eans', tokenize: 'forward', resolution: 9 },
],
},
tokenize: 'forward',
charset: Charset.LatinAdvanced,
});
const byId = new Map();
for (let i = 0; i < articles.length; i++) {
const article = articles[i];
const docId = i;
byId.set(docId, article);
const sku = String(article.sku ?? '');
const name = String(article.name ?? '');
const brand = String(article.brand?.name || article.brandName || '');
const eans = (article.eans ?? []).join(' ');
const all = `${sku} ${name} ${brand} ${eans}`.trim();
doc.add({
id: docId,
sku,
name,
brand,
eans,
all,
});
}
flexDocument = doc;
lastIndexedArticles = articles;
articleByInternalId = byId;
return { doc, byId };
}
/**
* Searches the FlexSearch index with the query string.
*
* @param {string} query Search query.
* @param {Array<object>} articles Full articles collection.
* @returns {Array<object>} Articles matching the search query.
*/
function queryFlexSearch(query, articles) {
const q = String(query ?? '').trim();
if (!q) return articles;
const { doc, byId } = getOrCreateFlexIndex(articles);
let rawResults;
try {
rawResults = doc.search(q);
} catch {
rawResults = [];
}
const matchedIds = new Set();
for (const fieldResult of rawResults) {
if (Array.isArray(fieldResult.result)) {
for (const id of fieldResult.result) {
matchedIds.add(id);
}
}
}
const matchesList = [];
for (const id of matchedIds) {
const art = byId.get(id);
if (art) {
matchesList.push(art);
}
}
return matchesList;
}
/**
@@ -180,11 +254,12 @@ export function searchArticlesIndex(query, options = {}) {
}
const articles = articleIndexAutomat.state.items ?? [];
const q = String(query ?? '').trim().toLowerCase();
const q = String(query ?? '').trim();
const rawMatches = q ? queryFlexSearch(q, articles) : articles;
const excludedSet = new Set((excludeSkus ?? []).map((s) => String(s).toLowerCase()));
let pool = articles;
let pool = rawMatches;
if (!allowParent) {
pool = pool.filter((a) => !a.isParent && !a.is_parent);
}
@@ -192,7 +267,7 @@ export function searchArticlesIndex(query, options = {}) {
pool = pool.filter((a) => a.sku && !excludedSet.has(String(a.sku).toLowerCase()));
}
let matchesList = q ? pool.filter((row) => matches(row, q)) : pool;
let matchesList = pool;
if (categories && categories.length > 0) {
const categoriesMap = categoryAutomat.state?.categories ?? {};
@@ -390,11 +465,11 @@ const actions = {
const offset = params.offset !== undefined ? Math.max(0, Number(params.offset) || 0) : currentState.offset;
const articles = articleIndexAutomat.state.items ?? [];
const query = target.trim().toLowerCase();
const query = target.trim();
// Filter by text query across all tokens
// Query candidates using FlexSearch
const textMatches = query
? articles.filter((row) => matches(row, query))
? queryFlexSearch(query, articles)
: articles;
// Build facets for all text matches (before category filtering)
@@ -506,8 +581,11 @@ const actions = {
return actions.runSearch({ offset });
},
/** Reset the search state. */
/** Reset the search state and clear FlexSearch index. */
reset() {
flexDocument = null;
lastIndexedArticles = null;
articleByInternalId = new Map();
articleSearchAutomat.setState(initialState);
},
};

View File

@@ -27,7 +27,8 @@ export default class ArticleSearchDialog extends PureComponent {
}
handleSelect = async (article) => {
const { onAssign, line } = this.props;
const { onAssign, line, kind = 'delivery' } = this.props;
const isDelivery = kind === 'delivery';
if (!onAssign || !article?.sku) return;
if (article.isParent || article.is_parent) {
@@ -41,14 +42,19 @@ export default class ArticleSearchDialog extends PureComponent {
} catch (err) {
this.setState({
assigningSku: null,
error: err?.message || 'Fehler beim Verknüpfen des Lieferanten-Artikels.',
error:
err?.message ||
(isDelivery
? 'Fehler beim Verknüpfen des Lieferanten-Artikels.'
: 'Fehler beim Zuordnen des Artikels.'),
});
}
};
render() {
const { open, onClose, line } = this.props;
const { open, onClose, line, kind = 'delivery' } = this.props;
const { assigningSku, error } = this.state;
const isDelivery = kind === 'delivery';
const rawSku = line?.sku != null ? String(line.sku).trim() : '';
return (
@@ -93,10 +99,10 @@ export default class ArticleSearchDialog extends PureComponent {
>
<Box>
<Typography variant="h6" component="div" fontWeight={600}>
Existierenden Artikel zuordnen
{line ? 'Existierenden Artikel zuordnen' : 'Artikel auswählen'}
</Typography>
<Typography variant="caption" color="text.secondary">
Lieferanten-SKU: <strong>{rawSku || '–'}</strong>
{isDelivery ? 'Lieferanten-SKU' : 'SKU'}: <strong>{rawSku || '–'}</strong>
{line?.name ? ` · ${line.name}` : ''}
</Typography>
</Box>

View File

@@ -13,6 +13,7 @@ import ManifestHeader from './ManifestHeader';
import ManifestLines from './ManifestLines';
import ManifestPdfDialog from './ManifestPdfDialog';
import ArticleSearchDialog from './ArticleSearchDialog';
import artikelAutomat, { getArticle } from '../../automats/artikelAutomat';
import {
buildManifestBody,
restoreAttachmentsFromApi,
@@ -55,6 +56,7 @@ class ManifestForm extends PureComponent {
this.handleExternalNumberChange =
this.handleExternalNumberChange.bind(this);
this.handleLineChange = this.handleLineChange.bind(this);
this.handleSkuBlur = this.handleSkuBlur.bind(this);
this.handleAddLine = this.handleAddLine.bind(this);
this.handleAddArticleLine = this.handleAddArticleLine.bind(this);
this.handleOpenArticleSearchForLine = this.handleOpenArticleSearchForLine.bind(this);
@@ -208,6 +210,47 @@ class ManifestForm extends PureComponent {
}));
}
async handleSkuBlur(key, sku) {
const trimmed = String(sku ?? '').trim();
if (!trimmed) return;
try {
const art = getArticle(trimmed) || (await artikelAutomat.actions.loadArticle(trimmed));
if (art && !this._cancelled) {
this.setState((prev) => ({
lines: prev.lines.map((l) => {
if (l.key !== key) return l;
const netVal =
art.price != null
? String(art.price)
: art.vknetto != null
? String(art.vknetto)
: art.net != null
? String(art.net)
: '';
const taxVal =
art.taxrate != null
? String(art.taxrate)
: art.tax != null
? String(art.tax)
: '';
return {
...l,
sku: art.sku || l.sku,
name: art.name || l.name || '',
tax: l.tax || taxVal,
taxrate: l.taxrate || taxVal,
net: l.net || netVal,
netprice: l.netprice || netVal,
unit: art.unit || l.unit || 'Stück',
};
}),
}));
}
} catch {
// not in catalog or load error; preserve user entry
}
}
handleAddLine() {
this.setState((prev) => ({ lines: [...prev.lines, emptyLine()] }));
}
@@ -266,7 +309,15 @@ class ManifestForm extends PureComponent {
}
handleAssignArticleForLine(article, targetLine) {
if (!article || !targetLine) return;
if (!article) return;
if (!targetLine) {
this.handleAddArticleLine(article);
this.setState({
articleSearchDialogOpen: false,
articleSearchLine: null,
});
return;
}
const netVal =
article.price != null
? String(article.price)
@@ -513,12 +564,14 @@ class ManifestForm extends PureComponent {
return (
<ManifestLines
kind={cfg.kind}
columns={cfg.columns}
lines={lines}
saving={saving}
wide={4}
linesColor={cfg.linesColor}
onLineChange={this.handleLineChange}
onSkuBlur={this.handleSkuBlur}
onAddLine={this.handleAddLine}
onAddArticleLine={this.handleAddArticleLine}
onSearchArticle={this.handleOpenArticleSearchForLine}
@@ -597,6 +650,7 @@ class ManifestForm extends PureComponent {
<ArticleSearchDialog
open={this.state.articleSearchDialogOpen}
line={this.state.articleSearchLine}
kind={kind}
onClose={this.handleCloseArticleSearch}
onAssign={this.handleAssignArticleForLine}
/>

View File

@@ -31,9 +31,10 @@ import {
class ManifestFormLine extends PureComponent {
constructor(props) {
super(props);
const sku = props.supplierArticle?.article?.sku;
const sku = this.getArticleSku(props);
this.state = {
cachedArticle: sku ? getArticle(sku) : null,
loadingArticle: false,
};
this.handleRemove = this.handleRemove.bind(this);
this.handleBlur = this.handleBlur.bind(this);
@@ -42,13 +43,20 @@ class ManifestFormLine extends PureComponent {
this.handleRemoveSupplierArticle = this.handleRemoveSupplierArticle.bind(this);
}
getArticleSku(props = this.props) {
if (props.kind === 'delivery') {
return props.supplierArticle?.article?.sku || null;
}
return props.line?.sku ? String(props.line.sku).trim() : null;
}
componentDidMount() {
this.subscribeArticle(this.props.supplierArticle?.article?.sku);
this.subscribeArticle(this.getArticleSku());
}
componentDidUpdate(prevProps) {
const prevSku = prevProps.supplierArticle?.article?.sku;
const nextSku = this.props.supplierArticle?.article?.sku;
const prevSku = this.getArticleSku(prevProps);
const nextSku = this.getArticleSku(this.props);
if (prevSku !== nextSku) {
this.subscribeArticle(nextSku);
}
@@ -62,19 +70,35 @@ class ManifestFormLine extends PureComponent {
this.unsubArticle?.();
this.unsubArticle = null;
if (!sku) {
if (this.state.cachedArticle) {
this.setState({ cachedArticle: null });
if (this.state.cachedArticle || this.state.loadingArticle) {
this.setState({ cachedArticle: null, loadingArticle: false });
}
return;
}
const current = getArticle(sku);
this.setState({ cachedArticle: current });
if (!current) {
artikelAutomat.actions.loadArticle(sku).catch(() => { });
if (current) {
this.setState({ cachedArticle: current, loadingArticle: false });
} else {
this.setState({ cachedArticle: null, loadingArticle: true });
artikelAutomat.actions.loadArticle(sku)
.then((art) => {
if (this.getArticleSku() === sku) {
this.setState({ cachedArticle: art, loadingArticle: false });
}
})
.catch(() => {
if (this.getArticleSku() === sku) {
this.setState({ cachedArticle: null, loadingArticle: false });
}
});
}
this.unsubArticle = artikelAutomat.subscribe(this, (state) => ({
cachedArticle: createArticleSelector(sku)(state),
}));
this.unsubArticle = artikelAutomat.subscribe(this, (state) => {
const art = createArticleSelector(sku)(state);
return {
cachedArticle: art ?? null,
...(art ? { loadingArticle: false } : {}),
};
});
}
handleRemove() {
@@ -251,8 +275,255 @@ class ManifestFormLine extends PureComponent {
}
renderSecondLine() {
const { kind = 'delivery' } = this.props;
const rawSku = this.props.line?.sku != null ? String(this.props.line.sku).trim() : '';
if (kind === 'delivery') {
return this.renderDeliverySecondLine(rawSku);
}
return this.renderArticleSecondLine(rawSku);
}
renderArticleSecondLine(rawSku) {
const { loading } = this.props;
const { cachedArticle, loadingArticle } = this.state;
if (loading || loadingArticle) {
return (
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, py: 0.25 }}>
<CircularProgress size={14} thickness={5} sx={{ color: 'primary.main' }} />
<Typography variant="caption" color="text.secondary" sx={{ fontStyle: 'italic', fontSize: '0.72rem' }}>
Artikeldaten werden geladen…
</Typography>
</Box>
);
}
if (cachedArticle) {
const photo = cachedArticle.photos?.[0] || cachedArticle.articlepic?.[0];
const fullHash = photo?.hash || photo?.fullHash;
const thumbHash = photo?.thumbhash || photo?.thumbHash;
const thumbW = photo?.thumbw || photo?.thumbW;
const thumbH = photo?.thumbh || photo?.thumbH;
const hasPhoto = fullHash != null || thumbHash != null;
const eans = cachedArticle.eans ?? cachedArticle.articleean ?? [];
const primaryEan = Array.isArray(eans) ? eans[0] : eans;
const vkPrice = primaryPrice(cachedArticle);
const vkNetto = vkPrice?.net;
const vkBrutto = vkPrice
? priceBrutto(vkPrice) ??
(vkPrice.net != null && vkPrice.taxrate != null
? vkPrice.net * (1 + vkPrice.taxrate / 100)
: null)
: null;
return (
<Box
sx={{
display: 'flex',
alignItems: 'center',
gap: 1.25,
py: 0.25,
flexWrap: 'wrap',
}}
>
{/* Article Photo Thumbnail */}
<Box
sx={{
width: 28,
height: 28,
minWidth: 28,
borderRadius: 0.75,
overflow: 'hidden',
bgcolor: BORDER,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
border: '1px solid',
borderColor: 'divider',
}}
>
{hasPhoto ? (
<ArticlePhoto
photo={{ fullHash, thumbHash, thumbW, thumbH }}
size={28}
/>
) : (
<Inventory2OutlinedIcon sx={{ fontSize: 16, color: 'text.secondary' }} />
)}
</Box>
{/* Internal Article SKU */}
{cachedArticle.sku && (
<Chip
component={RouterLink}
to={artikelDetailPath(cachedArticle)}
clickable
size="small"
label={`Art.-Nr.: ${cachedArticle.sku}`}
sx={{
height: 20,
fontSize: '0.7rem',
fontWeight: 600,
bgcolor: 'primary.50',
color: 'primary.dark',
border: '1px solid',
borderColor: 'primary.200',
cursor: 'pointer',
textDecoration: 'none',
'&:hover': {
bgcolor: 'primary.100',
},
}}
/>
)}
{/* Brand */}
{cachedArticle.brand?.name && (
<BrandLabel
brand={cachedArticle.brand}
size={16}
typographyProps={{
variant: 'caption',
color: 'text.secondary',
fontWeight: 500,
}}
/>
)}
{/* Article Name */}
{cachedArticle.name && (
<Typography
variant="caption"
fontWeight={600}
noWrap
sx={{ color: 'text.primary', maxWidth: { xs: 200, sm: 300, md: 400 } }}
title={cachedArticle.name}
>
{cachedArticle.name}
</Typography>
)}
{/* Primary EAN */}
{primaryEan && (
<Typography
variant="caption"
color="text.secondary"
sx={{
fontFamily: 'monospace',
fontSize: '0.68rem',
bgcolor: 'action.hover',
px: 0.5,
py: 0.1,
borderRadius: 0.5,
}}
>
EAN: {primaryEan}
</Typography>
)}
{/* VK Netto & VK Brutto */}
{(vkNetto != null || vkBrutto != null) && (
<Box
sx={{
display: 'flex',
alignItems: 'center',
gap: 1.5,
ml: 'auto',
whiteSpace: 'nowrap',
}}
>
{vkNetto != null && (
<Typography
variant="caption"
fontWeight={600}
sx={{ color: 'text.secondary', fontSize: '0.72rem' }}
>
VK Netto: {formatPrice(vkNetto)}
</Typography>
)}
{vkBrutto != null && (
<Typography
variant="caption"
fontWeight={600}
sx={{ color: 'primary.dark', fontSize: '0.72rem' }}
>
VK Brutto: {formatPrice(vkBrutto)}
</Typography>
)}
</Box>
)}
</Box>
);
}
if (rawSku) {
return (
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.75, py: 0.25 }}>
{this.props.onSearchArticle && (
<Button
size="small"
variant="text"
startIcon={<SearchIcon sx={{ fontSize: 14 }} />}
onClick={this.handleSearchArticle}
sx={{
fontSize: '0.7rem',
fontWeight: 600,
textTransform: 'none',
minWidth: 'auto',
py: 0.25,
px: 1,
color: 'primary.main',
'&:hover': {
bgcolor: 'primary.50',
},
}}
>
Artikel suchen
</Button>
)}
<HelpOutlineIcon sx={{ fontSize: 15, color: 'warning.main' }} />
<Typography variant="caption" color="text.secondary" sx={{ fontSize: '0.72rem' }}>
Kein Artikel für SKU &bdquo;{rawSku}&ldquo; gefunden.
</Typography>
</Box>
);
}
if (this.props.onSearchArticle) {
return (
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.75, py: 0.25 }}>
<Button
size="small"
variant="text"
startIcon={<SearchIcon sx={{ fontSize: 14 }} />}
onClick={this.handleSearchArticle}
sx={{
fontSize: '0.7rem',
fontWeight: 600,
textTransform: 'none',
minWidth: 'auto',
py: 0.25,
px: 1,
color: 'primary.main',
'&:hover': {
bgcolor: 'primary.50',
},
}}
>
Artikel suchen
</Button>
</Box>
);
}
return null;
}
renderDeliverySecondLine(rawSku) {
const { line, supplierArticle, loading } = this.props;
const rawSku = line?.sku != null ? String(line.sku).trim() : '';
if (loading) {
return (

View File

@@ -9,6 +9,7 @@ import TableContainer from '@mui/material/TableContainer';
import TableHead from '@mui/material/TableHead';
import TableRow from '@mui/material/TableRow';
import AddIcon from '@mui/icons-material/Add';
import SearchIcon from '@mui/icons-material/Search';
import DeleteSweepOutlinedIcon from '@mui/icons-material/DeleteSweepOutlined';
import { PanelTile } from 'dashgui/panels';
import { LINE_COLUMNS, hasArticleLines } from './manifestConfig';
@@ -17,6 +18,7 @@ import ManifestFormLine from './ManifestFormLine';
class ManifestLines extends PureComponent {
render() {
const {
kind = 'delivery',
lines,
saving,
linesColor,
@@ -86,17 +88,21 @@ class ManifestLines extends PureComponent {
lines.map((line, index) => {
const lineKey = line.key ?? index;
const rawSku = line.sku != null ? String(line.sku).trim() : '';
const matched =
supplierArticlesByLineKey[lineKey] ||
(rawSku ? supplierArticlesMap[rawSku.toLowerCase()] || supplierArticlesMap[rawSku] : null);
const isLineLoading =
loadingLineKeys[lineKey] ||
(supplierArticlesLoading && !matched);
const isDelivery = kind === 'delivery';
const matched = isDelivery
? supplierArticlesByLineKey[lineKey] ||
(rawSku ? supplierArticlesMap[rawSku.toLowerCase()] || supplierArticlesMap[rawSku] : null)
: null;
const isLineLoading = isDelivery
? loadingLineKeys[lineKey] ||
(supplierArticlesLoading && !matched)
: false;
const ledgerLine = line.id != null ? ledgerLinesById[line.id] : null;
return (
<ManifestFormLine
key={lineKey}
kind={kind}
line={line.key != null ? line : { ...line, key: index }}
columns={columns}
saving={saving}
@@ -126,6 +132,16 @@ class ManifestLines extends PureComponent {
>
Zeile
</Button>
{onSearchArticle && (
<Button
size="small"
startIcon={<SearchIcon />}
disabled={saving}
onClick={() => onSearchArticle(null)}
>
Artikel suchen
</Button>
)}
<Button
size="small"
color="error"

View File

@@ -1,3 +1,4 @@
// #region Imports
import { Component, createRef } from 'react';
import FillSpace from 'dashgui/FillSpace';
import Box from '@mui/material/Box';
@@ -47,7 +48,9 @@ import {
categoryAutomat,
} from '../automats';
import { BrandLabel } from '../components/brand';
// #endregion
// #region Styles & Measure Sample
const resultRowSx = {
alignItems: 'center',
minHeight: 64,
@@ -106,7 +109,9 @@ function ResultRowMeasureSample() {
</ListItemButton>
);
}
// #endregion
// #region Helpers
function initialSearchState(searchParams) {
const query = queryFromParams(searchParams);
const stored = readArtikelQuery();
@@ -155,14 +160,22 @@ function getCategoryTreeOrder(categoriesMap, rootCategoryId) {
return order;
}
// #endregion
// #region ArtikelSearchPage
class ArtikelSearchPage extends Component {
state = initialSearchState(this.props.searchParams);
paginationRef = createRef();
filterHeaderRef = createRef();
fillSpaceRef = createRef();
categoryFillSpaceRef = createRef();
// #region Constructor & State
constructor(props) {
super(props);
this.state = initialSearchState(props.searchParams);
this.paginationRef = createRef();
this.filterHeaderRef = createRef();
this.fillSpaceRef = createRef();
this.categoryFillSpaceRef = createRef();
}
// #endregion
// #region Measurement & Query Persistence
toggleCategoryFilter = () => {
this.setState((prev) => ({ categoryFilterOpen: !prev.categoryFilterOpen }));
};
@@ -190,7 +203,9 @@ class ArtikelSearchPage extends Component {
offset: query.offset,
});
};
// #endregion
// #region Lifecycle
componentDidMount() {
document.addEventListener('mouseup', preserveSearchFocus, true);
setLastArticleSku(null);
@@ -263,7 +278,9 @@ class ArtikelSearchPage extends Component {
this.fetchResults();
}
}
// #endregion
// #region Search & Navigation Handlers
updateParams = (nextTarget, nextFilter, nextOffset = 0) => {
writeArtikelQuery({
target: nextTarget,
@@ -329,7 +346,9 @@ class ArtikelSearchPage extends Component {
const filterParam = this.props.searchParams.get('filter') ?? '';
this.updateParams(this.state.input, filterParam, nextOffset);
};
// #endregion
// #region Render
render() {
const {
input,
@@ -635,6 +654,9 @@ class ArtikelSearchPage extends Component {
</>
);
}
// #endregion
}
// #endregion
export default withSearchParams(ArtikelSearchPage);

View File

@@ -1321,6 +1321,7 @@ class LieferungNeuPage extends PureComponent {
</PanelTile>
)}
<ManifestLines
kind="delivery"
columns={DELIVERY_LINE_COLUMNS}
lines={lines}
saving={saving}
@@ -1379,6 +1380,7 @@ class LieferungNeuPage extends PureComponent {
<ArticleSearchDialog
open={this.state.articleSearchDialogOpen}
line={this.state.articleSearchLine}
kind="delivery"
onClose={this.handleCloseArticleSearch}
onAssign={this.handleAssignArticleForLine}
/>