but
This commit is contained in:
@@ -79,7 +79,7 @@ function createCache(label, load) {
|
||||
export class StorageManager extends ApiClient {
|
||||
/** @type {EventSource | null} */
|
||||
#events = null;
|
||||
/** @type {ReturnType<typeof createCache> | null} */
|
||||
/** @type {ReturnType<StorageManager['#createDeferredCache']> | null} */
|
||||
#articleIndexCache = null;
|
||||
/** @type {Map<string, Set<(article: object) => 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');
|
||||
|
||||
@@ -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 (
|
||||
<Box sx={{ minWidth: 0 }}>
|
||||
{showTitle ? (
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5, mb: 2 }}>
|
||||
<Typography variant="h5" sx={{ fontWeight: 700 }}>
|
||||
{title}
|
||||
</Typography>
|
||||
{titleAction}
|
||||
</Box>
|
||||
) : null}
|
||||
{children}
|
||||
</Box>
|
||||
<PageTitleRightContext.Provider value={rightNode}>
|
||||
<Box sx={{ minWidth: 0 }}>
|
||||
{showTitle ? (
|
||||
<Box
|
||||
sx={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: {
|
||||
xs: '1fr',
|
||||
sm: 'repeat(2, 1fr)',
|
||||
md: `repeat(${GRID_COLUMNS}, 1fr)`,
|
||||
},
|
||||
gap: 2,
|
||||
mb: 2,
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
gridColumn: {
|
||||
xs: '1 / -1',
|
||||
sm: titleWide >= 2 ? 'span 2' : 'span 1',
|
||||
md: `span ${Math.min(Math.max(titleWide, 1), GRID_COLUMNS)}`,
|
||||
},
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 1.5,
|
||||
minWidth: 0,
|
||||
}}
|
||||
>
|
||||
<Typography variant="h5" sx={{ fontWeight: 700 }}>
|
||||
{title}
|
||||
</Typography>
|
||||
{titleAction}
|
||||
<Box
|
||||
id="page-title-right"
|
||||
ref={setRightNode}
|
||||
sx={{ ml: 'auto', display: 'flex', alignItems: 'center', gap: 1 }}
|
||||
>
|
||||
{titleRightAction}
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
) : null}
|
||||
{children}
|
||||
</Box>
|
||||
</PageTitleRightContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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' && (
|
||||
<PageTitleRight>
|
||||
<Button
|
||||
variant="contained"
|
||||
size="small"
|
||||
disabled={saving}
|
||||
onClick={this.handleSave}
|
||||
>
|
||||
{saving ? 'Speichern…' : 'Speichern'}
|
||||
</Button>
|
||||
</PageTitleRight>
|
||||
)}
|
||||
<PanelTileGrid>
|
||||
<ManifestHeader
|
||||
kind={kind}
|
||||
|
||||
@@ -101,15 +101,6 @@ class ManifestLines extends PureComponent {
|
||||
>
|
||||
Zeile hinzufügen
|
||||
</Button>
|
||||
<Box sx={{ flex: 1 }} />
|
||||
<Button
|
||||
variant="contained"
|
||||
size="small"
|
||||
disabled={saving}
|
||||
onClick={onSave}
|
||||
>
|
||||
{saving ? 'Speichern…' : 'Speichern'}
|
||||
</Button>
|
||||
</Box>
|
||||
</Stack>
|
||||
</PanelTile>
|
||||
|
||||
@@ -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 (
|
||||
<>
|
||||
<PageTitleRight>
|
||||
<Button
|
||||
variant="contained"
|
||||
size="small"
|
||||
disabled={saving}
|
||||
onClick={this.handleSave}
|
||||
>
|
||||
{saving ? 'Speichern…' : 'Speichern'}
|
||||
</Button>
|
||||
</PageTitleRight>
|
||||
<PanelTileGrid>
|
||||
<ManifestHeader
|
||||
kind="delivery"
|
||||
|
||||
Reference in New Issue
Block a user