diff --git a/src/api/storageManager.jsx b/src/api/storageManager.jsx
new file mode 100644
index 0000000..77195ce
--- /dev/null
+++ b/src/api/storageManager.jsx
@@ -0,0 +1,83 @@
+import { createContext, useContext, useRef } from 'react';
+
+export const StorageManagerContext = createContext(null);
+
+function createStorageManager() {
+ /** Fetch /articleIndex on construction and convert to the row shape the search page renders. */
+ const articleIndexPromise = fetch('/api/articleIndex')
+ .then(async (res) => {
+ const data = await res.json();
+ const articles = Array.isArray(data) ? data : (data?.articles ?? []);
+ console.log('[storageMgr] GET /articleIndex', {
+ status: res.status,
+ count: articles.length,
+ sampleKeys:
+ articles.slice(0, 3).map((a) => (a && typeof a === 'object' ? Object.keys(a) : null)),
+ });
+ // Map backend article rows to the recordset shape used by the result list.
+ // `categories` is a comma-joined string (formatCategoryList splits on ',').
+ return articles.map((a) => ({
+ id: a.id,
+ sku: a.sku,
+ name: a.name,
+ eans: a.eans ?? [],
+ categories: Array.isArray(a.categoryIds) ? a.categoryIds.join(',') : null,
+ }));
+ })
+ .catch((err) => {
+ console.error('[storageMgr] GET /articleIndex failed', err);
+ return [];
+ });
+
+ async function doSearch({ target, filter, offset, maxCount, onUpdate }) {
+ const articles = await articleIndexPromise;
+ const query = target.trim().toLowerCase();
+ const matches = articles.filter(
+ (p) =>
+ p.name.toLowerCase().includes(query) ||
+ p.sku.toLowerCase().includes(query) ||
+ (p.eans ?? []).some((ean) => String(ean).includes(query)),
+ );
+ onUpdate({
+ categoryFacets: {},
+ totalCount: matches.length,
+ recordset: matches,
+ error: null,
+ });
+ }
+
+ return {
+ doSearch,
+ // Expose so callers can await if they want the loaded index.
+ articleIndexPromise,
+ };
+}
+
+/**
+ * Provider — mount once near the top of the tree.
+ */
+export function StorageManagerProvider({ children }) {
+ const mgrRef = useRef(null);
+ if (!mgrRef.current) {
+ mgrRef.current = createStorageManager();
+ }
+
+ return (
+
+ {children}
+
+ );
+}
+
+/**
+ * Hook for function components that need the store manager.
+ */
+export function useStorageManager() {
+ const ctx = useContext(StorageManagerContext);
+ if (!ctx) {
+ throw new Error('useStorageManager must be used within a ');
+ }
+ return ctx;
+}
+
+export default StorageManagerContext;
diff --git a/src/components/article/boxes/ArticleViewName.jsx b/src/components/article/boxes/ArticleViewName.jsx
index 0a25557..23452b0 100644
--- a/src/components/article/boxes/ArticleViewName.jsx
+++ b/src/components/article/boxes/ArticleViewName.jsx
@@ -23,7 +23,7 @@ const articleNameFieldSx = {
const articleNameFieldHiddenSx = {
...articleNameFieldSx,
- pointerEvents: 'none',
+ userSelect: 'text',
'& .MuiOutlinedInput-notchedOutline': {
borderColor: 'transparent',
},
diff --git a/src/main.jsx b/src/main.jsx
index d1a2f83..98a3bc7 100644
--- a/src/main.jsx
+++ b/src/main.jsx
@@ -4,6 +4,7 @@ import { ThemeProvider } from '@mui/material/styles';
import { Provider } from 'react-redux';
import { SnackbarProvider } from 'notistack';
import AuthBootstrap from './components/AuthBootstrap';
+import { StorageManagerProvider } from './api/storageManager';
import App from './App';
import { store } from './store';
import { getActiveTheme } from './theme';
@@ -33,7 +34,9 @@ createRoot(document.getElementById('root')).render(
anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }}
>
-
+
+
+
diff --git a/src/pages/artikel/index.jsx b/src/pages/artikel/index.jsx
index eb79250..a914fae 100644
--- a/src/pages/artikel/index.jsx
+++ b/src/pages/artikel/index.jsx
@@ -9,13 +9,11 @@ import TextField from '@mui/material/TextField';
import Typography from '@mui/material/Typography';
import SearchIcon from '@mui/icons-material/Search';
import ClearIcon from '@mui/icons-material/Clear';
-import { article } from '../../api';
+import { StorageManagerContext } from '../../api/storageManager';
import BackendError from '../../components/BackendError';
-import { backendErrorFrom } from '../../utils/backendError';
import { formatPrice } from '../../utils/price';
import {
buildSearchUrl,
- clearArtikelQuery,
queryFromParams,
readArtikelQuery,
writeArtikelQuery,
@@ -82,6 +80,8 @@ function initialSearchState(searchParams) {
}
class ArtikelSearchPage extends Component {
+ static contextType = StorageManagerContext;
+
state = initialSearchState(this.props.searchParams);
listSlotRef = { current: null };
rowMeasureRef = { current: null };
@@ -145,7 +145,7 @@ class ArtikelSearchPage extends Component {
this.setState({ pageSize: next }, () => {
const target = this.props.searchParams.get('target') ?? '';
- if (target.trim().length >= 3) this.fetchResults();
+ if (target) this.fetchResults();
});
};
@@ -166,6 +166,16 @@ class ArtikelSearchPage extends Component {
componentDidMount() {
document.addEventListener('mousedown', preserveSearchFocus, true);
window.addEventListener('resize', this.handleWindowResize);
+
+ // Kick off initial search (empty input = show all products).
+ const { input } = this.state;
+ this.context.doSearch({
+ target: input,
+ filter: this.props.searchParams.get('filter') || undefined,
+ offset: 0,
+ maxCount: this.state.pageSize ?? 20,
+ onUpdate: (result) => this.setState(result),
+ });
}
componentWillUnmount() {
@@ -206,78 +216,35 @@ class ArtikelSearchPage extends Component {
handleInputChange = (event) => {
const value = event.target.value;
- const belowMin = value.trim().length < 3;
+ this.setState({ input: value });
- if (belowMin) {
- clearArtikelQuery();
- // Clear results in the same update that shortens the input — otherwise the
- // hint appears one paint before fetchResults (driven by URL) removes the list.
- this.setState({
- input: value,
- categoryFacets: null,
- totalCount: null,
- recordset: null,
- error: null,
- pageSize: null,
- });
- } else {
- this.setState({ input: value });
- }
-
- this.updateParams(value, this.props.searchParams.get('filter') ?? '', 0);
+ this.context.doSearch({
+ target: value,
+ filter: this.props.searchParams.get('filter') || undefined,
+ offset: 0,
+ maxCount: this.state.pageSize ?? 20,
+ onUpdate: (result) => this.setState(result),
+ });
};
- fetchResults = async () => {
+ fetchResults = () => {
const { target, filter, offset } = queryFromParams(this.props.searchParams);
const trimmed = target.trim();
const { pageSize } = this.state;
- if (trimmed.length < 3) {
- const { categoryFacets, totalCount, recordset, error, pageSize: ps } = this.state;
- if (
- categoryFacets != null
- || totalCount != null
- || recordset != null
- || error != null
- || ps != null
- ) {
- this.setState({
- categoryFacets: null,
- totalCount: null,
- recordset: null,
- error: null,
- pageSize: null,
- });
- }
- return;
- }
-
if (pageSize == null) return;
writeArtikelQuery({ target, filter, offset });
- try {
- const data = await article.search({
- target: trimmed,
- filter: filter || undefined,
- offset,
- maxCount: pageSize,
- });
-
- this.setState({
- categoryFacets: data.categories ?? {},
- totalCount: data.length ?? 0,
- recordset: data.recordset ?? [],
- error: null,
- });
- } catch (err) {
- this.setState({
- categoryFacets: null,
- totalCount: null,
- recordset: null,
- error: backendErrorFrom(err),
- });
- }
+ // Delegate fetch + debounce to the global StorageManager.
+ // The callback pushes results into local component state.
+ this.context.doSearch({
+ target: trimmed,
+ filter: filter || undefined,
+ offset,
+ maxCount: pageSize,
+ onUpdate: (result) => this.setState(result),
+ });
};
render() {
@@ -286,7 +253,7 @@ class ArtikelSearchPage extends Component {
const offset = Math.max(0, Number(this.props.searchParams.get('offset')) || 0);
const categories = parseFilter(filterParam);
// Drive UI from input so hint vs results switch in one paint (URL lags one tick).
- const searchActive = input.trim().length >= 3;
+ const searchActive = input.trim().length > 0 || (recordset ?? []).length > 0;
const rows = recordset ?? [];
const facetEntries = categoryFacets
@@ -339,12 +306,6 @@ class ArtikelSearchPage extends Component {
/>
- {input.trim().length < 3 && (
-
- Mindestens 3 Zeichen eingeben — mehrere Wörter werden mit UND verknüpft.
-
- )}
-
{error && (
- {rows.length > 0 && pageSize != null && (
+ {rows.length > 0 && (
{rows.map((row, index) => (