From 51d01a502b402bb3a47b4bffae7f0bd8e42109dc Mon Sep 17 00:00:00 2001 From: seb Date: Tue, 22 Sep 2026 07:59:00 +0200 Subject: [PATCH] but --- src/api/StorageManager.js | 180 ++++++++++++++-------- src/components/PageLayout.jsx | 86 +++++++++-- src/components/manifest/ManifestForm.jsx | 14 ++ src/components/manifest/ManifestLines.jsx | 9 -- src/pages/lieferung/neu.jsx | 11 ++ 5 files changed, 220 insertions(+), 80 deletions(-) diff --git a/src/api/StorageManager.js b/src/api/StorageManager.js index 7d77b06..36e5625 100644 --- a/src/api/StorageManager.js +++ b/src/api/StorageManager.js @@ -79,7 +79,7 @@ function createCache(label, load) { export class StorageManager extends ApiClient { /** @type {EventSource | null} */ #events = null; - /** @type {ReturnType | null} */ + /** @type {ReturnType | null} */ #articleIndexCache = null; /** @type {Map void>>} sku → article listeners */ #articleWatchers = new Map(); @@ -386,14 +386,24 @@ export class StorageManager extends ApiClient { #initCaches() { const authMeCache = createCache('GET /auth/me', () => this.#loadAuthMe()); - const deliveryCache = createCache('GET /delivery', () => this.#loadDeliveries()); - const customergroupCache = createCache('GET /customergroup', () => + const deliveryCache = this.#createDeferredCache('GET /delivery', () => + this.#loadDeliveries(), + ); + const customergroupCache = this.#createDeferredCache('GET /customergroup', () => this.#loadCustomergroups(), ); - const entityCache = createCache('GET /entity', () => this.#loadEntities()); - const numberCache = createCache('GET /number', () => this.#loadNumbers()); - const quoteCache = createCache('GET /quote', () => this.#loadQuotes()); - const orderCache = createCache('GET /order', () => this.#loadOrders()); + const entityCache = this.#createDeferredCache('GET /entity', () => + this.#loadEntities(), + ); + const numberCache = this.#createDeferredCache('GET /number', () => + this.#loadNumbers(), + ); + const quoteCache = this.#createDeferredCache('GET /quote', () => + this.#loadQuotes(), + ); + const orderCache = this.#createDeferredCache('GET /order', () => + this.#loadOrders(), + ); // articleIndex is auth-gated — create a deferred cache that only loads after auth this.#articleIndexCache = this.#createDeferredCache( @@ -478,7 +488,7 @@ export class StorageManager extends ApiClient { /** Settled GET /category/tree body, or undefined. */ categoryTree: { get: () => categoryTreeCache.value }, - // ── Remaining non-auth-gated caches ───────────────────────────────────── + // ── Deferred caches (auth-gated) ───────────────────────────────────────── deliveryPromise: { get: () => deliveryCache.promise }, updateDelivery: { value: deliveryCache.update }, /** Settled GET /delivery list, or undefined. */ @@ -513,58 +523,132 @@ export class StorageManager extends ApiClient { reconnectEvents: { value: () => this.#reconnectEvents() }, }); - // Trigger articleIndex load once auth is established - authAutomat.subscribe((auth) => { - if (auth.status === 'authed' && auth.user?.selectedCompanyId) { - if (!articleIndexCache.value) { - void articleIndexCache.update?.().catch(() => {}); + let lastCompanyId = null; + + const syncWithAuth = (auth) => { + const companyId = + auth.status === 'authed' && auth.user?.selectedCompanyId != null + ? String(auth.user.selectedCompanyId) + : null; + + if (companyId === lastCompanyId) return; + const prevCompanyId = lastCompanyId; + lastCompanyId = companyId; + + if (!companyId) { + // Unauthenticated or logged out + this.#disconnectEvents(); + articleIndexCache.clear(); + categoryTreeCache.clear(); + deliveryCache.clear(); + customergroupCache.clear(); + entityCache.clear(); + numberCache.clear(); + quoteCache.clear(); + orderCache.clear(); + artikelAutomat.actions.clear(); + } else { + // Company established or switched + if (prevCompanyId) { + articleIndexCache.clear(); + categoryTreeCache.clear(); + deliveryCache.clear(); + customergroupCache.clear(); + entityCache.clear(); + numberCache.clear(); + quoteCache.clear(); + orderCache.clear(); + artikelAutomat.actions.clear(); } + this.#reconnectEvents(); + void articleIndexCache.trigger().catch(() => {}); + void deliveryCache.trigger().catch(() => {}); + void customergroupCache.trigger().catch(() => {}); + void entityCache.trigger().catch(() => {}); + void numberCache.trigger().catch(() => {}); + void quoteCache.trigger().catch(() => {}); + void orderCache.trigger().catch(() => {}); } - }); + }; + + authAutomat.subscribe(syncWithAuth); + syncWithAuth(authAutomat.state); } /** - * Create a cache that does NOT load immediately but waits for an explicit trigger. - * Differs from createCache in that the initial load promise is not started. + * Create a cache that does NOT load immediately but waits for an explicit trigger + * or loads on-demand once authenticated. */ #createDeferredCache(label, load) { let current; - let resolveInitial; - let rejectInitial; - let promise = new Promise((res, rej) => { - resolveInitial = res; - rejectInitial = rej; - }); + let loadPromise = null; - async function update() { + const trigger = () => { + if (loadPromise) return loadPromise; + loadPromise = (async () => { + try { + const data = await load(); + current = data; + return data; + } catch (err) { + console.error(`[storageMgr] ${label} failed`, err); + loadPromise = null; + throw err; + } + })(); + return loadPromise; + }; + + const update = async () => { try { const data = await load(); current = data; - promise = Promise.resolve(data); - resolveInitial?.(data); - resolveInitial = null; + loadPromise = Promise.resolve(data); return data; } catch (err) { console.error(`[storageMgr] update ${label} failed`, err); - rejectInitial?.(err); - rejectInitial = null; throw err; } - } + }; - function replace(data) { + const replace = (data) => { current = data; - promise = Promise.resolve(data); - resolveInitial?.(data); - resolveInitial = null; + loadPromise = Promise.resolve(data); return data; - } + }; + + const clear = () => { + current = undefined; + loadPromise = null; + }; return { update, replace, - get promise() { return promise; }, - get value() { return current; }, + clear, + trigger, + get promise() { + if (!loadPromise) { + if (authAutomat.state.status === 'authed' && authAutomat.state.user?.selectedCompanyId) { + return trigger(); + } + return new Promise((resolve, reject) => { + const unsub = authAutomat.subscribe((auth) => { + if (auth.status === 'authed' && auth.user?.selectedCompanyId) { + unsub(); + trigger().then(resolve, reject); + } else if (auth.status === 'anon') { + unsub(); + reject(new Error(`Cannot load ${label}: unauthenticated`)); + } + }); + }); + } + return loadPromise; + }, + get value() { + return current; + }, }; } @@ -652,24 +736,6 @@ export class StorageManager extends ApiClient { })); } - async #loadBrands() { - const data = await this.brand.list(); - requireObject(data, 'GET /api/brand'); - return requireArray(data.brands, 'brands'); - } - - async #loadUnits() { - const data = await this.unit.list(); - requireObject(data, 'GET /api/unit'); - return requireArray(data.units, 'units'); - } - - async #loadCompanies() { - const data = await this.company.list(); - requireObject(data, 'GET /api/company'); - return requireArray(data.companies, 'companies'); - } - async #loadCategoryTree() { const data = await this.category.tree(); requireObject(data, 'GET /api/category/tree'); @@ -679,12 +745,6 @@ export class StorageManager extends ApiClient { return data; } - async #loadPosterminals() { - const data = await this.posterminal.list(); - requireObject(data, 'GET /api/posterminal'); - return requireArray(data.posterminals, 'posterminals'); - } - async #loadDeliveries() { const data = await this.delivery.list(); requireObject(data, 'GET /api/delivery'); diff --git a/src/components/PageLayout.jsx b/src/components/PageLayout.jsx index 633f35b..e1971f2 100644 --- a/src/components/PageLayout.jsx +++ b/src/components/PageLayout.jsx @@ -1,23 +1,87 @@ +import { createContext, useContext, useState, useLayoutEffect } from 'react'; +import { createPortal } from 'react-dom'; import Box from '@mui/material/Box'; import Typography from '@mui/material/Typography'; +import { GRID_COLUMNS } from 'dashgui/panels'; + +export const PageTitleRightContext = createContext(null); + +export function PageTitleRight({ children }) { + const contextNode = useContext(PageTitleRightContext); + const [domNode, setDomNode] = useState( + () => (typeof document !== 'undefined' ? document.getElementById('page-title-right') : null) + ); + + useLayoutEffect(() => { + if (!contextNode && typeof document !== 'undefined') { + const el = document.getElementById('page-title-right'); + if (el && el !== domNode) { + setDomNode(el); + } + } + }, [contextNode, domNode]); + + const target = contextNode || domNode; + if (!target) return null; + return createPortal(children, target); +} export default function PageLayout({ title, showTitle = true, titleAction, + titleRightAction, + titleWide = 3, children, }) { + const [rightNode, setRightNode] = useState(null); + return ( - - {showTitle ? ( - - - {title} - - {titleAction} - - ) : null} - {children} - + + + {showTitle ? ( + + = 2 ? 'span 2' : 'span 1', + md: `span ${Math.min(Math.max(titleWide, 1), GRID_COLUMNS)}`, + }, + display: 'flex', + alignItems: 'center', + gap: 1.5, + minWidth: 0, + }} + > + + {title} + + {titleAction} + + {titleRightAction} + + + + ) : null} + {children} + + ); } + diff --git a/src/components/manifest/ManifestForm.jsx b/src/components/manifest/ManifestForm.jsx index 527a9e7..1dd791e 100644 --- a/src/components/manifest/ManifestForm.jsx +++ b/src/components/manifest/ManifestForm.jsx @@ -1,9 +1,11 @@ import { PureComponent } from 'react'; import Box from '@mui/material/Box'; +import Button from '@mui/material/Button'; import CircularProgress from '@mui/material/CircularProgress'; import Typography from '@mui/material/Typography'; import { enqueueSnackbar } from 'notistack'; import BackendError from '../BackendError'; +import { PageTitleRight } from '../PageLayout'; import { PanelTile, PanelTileGrid } from 'dashgui/panels'; import { StorageManagerContext } from '../../api/storageManagerContext'; import { requireArray, requireDefined, requireObject } from '../../utils/apiShape'; @@ -473,6 +475,18 @@ class ManifestForm extends PureComponent { return ( <> + {status === 'ready' && ( + + + + )} Zeile hinzufügen - - diff --git a/src/pages/lieferung/neu.jsx b/src/pages/lieferung/neu.jsx index cb4d737..b1f04b1 100644 --- a/src/pages/lieferung/neu.jsx +++ b/src/pages/lieferung/neu.jsx @@ -10,6 +10,7 @@ import { enqueueSnackbar } from 'notistack'; import { PanelTile, PanelTileGrid } from 'dashgui/panels'; import BackendError from '../../components/BackendError'; import withNavigate from '../../components/withNavigate'; +import { PageTitleRight } from '../../components/PageLayout'; import { ManifestHeader, ManifestLines, @@ -1158,6 +1159,16 @@ class LieferungNeuPage extends PureComponent { return ( <> + + +