This commit is contained in:
seb
2026-10-04 21:04:40 +02:00
parent 669954b496
commit f765b87336
59 changed files with 1491 additions and 2045 deletions

View File

@@ -10,7 +10,7 @@ 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 { StorageManagerContext, getStorageManager } from '../api/storageManagerContext';
import articleSearchAutomat from '../automats/articleSearchAutomat';
import BackendError from '../components/BackendError';
import { formatPrice } from '../utils/price';
import {
@@ -83,12 +83,6 @@ function initialSearchState(searchParams) {
}
class ArtikelSearchPage extends Component {
static contextType = StorageManagerContext;
get storageManager() {
return this.context || getStorageManager();
}
state = initialSearchState(this.props.searchParams);
listSlotRef = { current: null };
rowMeasureRef = { current: null };
@@ -176,20 +170,18 @@ class ArtikelSearchPage extends Component {
window.addEventListener('resize', this.handleWindowResize);
setLastArticleSku(null);
// Push search results into local component state.
this.unsubSearch = articleSearchAutomat.subscribe(this, (state) => state.result);
// Kick off initial search (empty input = show all products).
const { input } = this.state;
this.storageManager?.doSearch({
target: input,
filter: this.props.searchParams.get('filter') || undefined,
offset: 0,
maxCount: this.state.pageSize ?? 20,
onUpdate: (result) => this.setState(result),
});
void articleSearchAutomat.actions.search(this.state.input).catch(() => {});
}
componentWillUnmount() {
document.removeEventListener('mouseup', preserveSearchFocus, true);
window.removeEventListener('resize', this.handleWindowResize);
this.unsubSearch?.();
this.unsubSearch = null;
this.persistQuery();
}
@@ -226,34 +218,18 @@ class ArtikelSearchPage extends Component {
handleInputChange = (event) => {
const value = event.target.value;
this.setState({ input: value });
this.storageManager?.doSearch({
target: value,
filter: this.props.searchParams.get('filter') || undefined,
offset: 0,
maxCount: this.state.pageSize ?? 20,
onUpdate: (result) => this.setState(result),
});
void articleSearchAutomat.actions.search(value).catch(() => {});
};
fetchResults = () => {
const { target, filter, offset } = queryFromParams(this.props.searchParams);
const trimmed = target.trim();
const { pageSize } = this.state;
if (pageSize == null) return;
writeArtikelQuery({ target, filter, offset });
// Delegate fetch + debounce to the global StorageManager.
// The callback pushes results into local component state.
this.storageManager?.doSearch({
target: trimmed,
filter: filter || undefined,
offset,
maxCount: pageSize,
onUpdate: (result) => this.setState(result),
});
void articleSearchAutomat.actions.search(target.trim()).catch(() => {});
};
render() {

View File

@@ -1,7 +1,9 @@
import { Component } from 'react';
import Box from '@mui/material/Box';
import CircularProgress from '@mui/material/CircularProgress';
import { StorageManagerContext } from '../../api/storageManagerContext';
import { getCategoryState, whenCategoryTreeReady } from '../../automats/categoryAutomat';
import companyAutomat from '../../automats/companyAutomat';
import { bootstrapAuthMe } from '../../automats/authAutomat';
import BackendError from '../../components/BackendError';
import { ArticleView } from '../../components/article';
import {
@@ -22,13 +24,11 @@ function ArticleLoading() {
}
class ArtikelSkuPage extends Component {
static contextType = StorageManagerContext;
constructor(props, context) {
super(props, context);
constructor(props) {
super(props);
this.state = {
article: getArticle(props.sku),
ready: context.getCategoryState() != null,
ready: getCategoryState() != null,
error: null,
};
}
@@ -72,9 +72,9 @@ class ArtikelSkuPage extends Component {
try {
await Promise.all([
artikelAutomat.actions.loadArticle(sku),
this.context.categoryTreePromise,
this.context.companyPromise,
this.context.authMePromise,
whenCategoryTreeReady(),
companyAutomat.actions.load(),
bootstrapAuthMe(),
]);
if (this._mounted) this.setState({ ready: true });
} catch (error) {

View File

@@ -2,7 +2,9 @@ import { PureComponent } from 'react';
import { enqueueSnackbar } from 'notistack';
import BackendError from '../components/BackendError';
import { RecordList } from 'dashgui/lists';
import { StorageManagerContext } from '../api/storageManagerContext';
import { entity as entityApi } from '../api';
import entityAutomat from '../automats/entityAutomat';
import customergroupAutomat from '../automats/customergroupAutomat';
import { router } from '../appRouter';
import { backendErrorFrom } from '../utils/backendError';
import { notifyError } from '../utils/notify';
@@ -21,8 +23,6 @@ function formatDate(value) {
}
class KundeListePage extends PureComponent {
static contextType = StorageManagerContext;
constructor(props) {
super(props);
this.state = {
@@ -50,8 +50,8 @@ class KundeListePage extends PureComponent {
this.setState({ status: 'loading', error: null });
try {
const [entities, groups] = await Promise.all([
this.context.entities ?? this.context.entityPromise,
this.context.customergroups ?? this.context.customergroupPromise,
entityAutomat.actions.ensureLoaded(),
customergroupAutomat.actions.ensureLoaded(),
]);
if (this._cancelled) return;
const groupsById = Object.fromEntries(groups.map((row) => [row.id, row]));
@@ -89,8 +89,8 @@ class KundeListePage extends PureComponent {
this.setState({ busyId: entity.id });
try {
await this.context.entity.delete(entity.id);
const list = await this.context.updateEntity();
await entityApi.delete(entity.id);
const list = await entityAutomat.actions.reload();
if (this._cancelled) return;
this.setState({
entities: sortEntities(list),

View File

@@ -11,7 +11,9 @@ import { enqueueSnackbar } from 'notistack';
import BackendError from '../../components/BackendError';
import SingleLineTextEdit from 'dashgui/SingleLineTextEdit';
import { PanelTile, PanelTileGrid } from 'dashgui/panels';
import { StorageManagerContext } from '../../api/storageManagerContext';
import { entity as entityApi } from '../../api';
import entityAutomat from '../../automats/entityAutomat';
import customergroupAutomat from '../../automats/customergroupAutomat';
import { requireArray, requireDefined, requireObject } from '../../utils/apiShape';
import { backendErrorFrom } from '../../utils/backendError';
import { notifyError } from '../../utils/notify';
@@ -72,8 +74,6 @@ function emptyContactDraft() {
}
class KundeEditPage extends PureComponent {
static contextType = StorageManagerContext;
constructor(props) {
super(props);
this.state = {
@@ -128,10 +128,10 @@ class KundeEditPage extends PureComponent {
this.setState({ status: 'loading', error: null });
try {
const [entityRes, addressRes, contactRes, groups] = await Promise.all([
this.context.entity.get(entityId),
this.context.entity.listAddresses(entityId),
this.context.entity.listContacts(entityId),
this.context.customergroups ?? this.context.customergroupPromise,
entityApi.get(entityId),
entityApi.listAddresses(entityId),
entityApi.listContacts(entityId),
customergroupAutomat.actions.ensureLoaded(),
]);
if (this._cancelled) return;
@@ -186,14 +186,14 @@ class KundeEditPage extends PureComponent {
this.setState({ savingEntity: true });
try {
const data = await this.context.entity.update(entityId, {
const data = await entityApi.update(entityId, {
name: trimmedName,
firstName: firstName.trim() === '' ? null : firstName.trim(),
});
requireObject(data, 'PATCH /api/entity/:id');
requireObject(data.entity, 'entity');
requireDefined(data.entity.id, 'entity.id');
await this.context.updateEntity();
await entityAutomat.actions.reload();
if (this._cancelled) return;
this.setState({
entity: data.entity,
@@ -230,14 +230,14 @@ class KundeEditPage extends PureComponent {
this.setState({ busyAddressKey: address.id });
try {
const data = await this.context.entity.updateAddress(
const data = await entityApi.updateAddress(
entityId,
address.id,
body,
);
requireObject(data, 'PATCH /api/entity/:id/address/:id');
requireObject(data.address, 'address');
const listRes = await this.context.entity.listAddresses(entityId);
const listRes = await entityApi.listAddresses(entityId);
requireObject(listRes, 'GET /api/entity/:id/address');
const addresses = requireArray(listRes.addresses, 'addresses');
if (this._cancelled) return;
@@ -268,11 +268,11 @@ class KundeEditPage extends PureComponent {
this.setState({ busyAddressKey: 'new' });
try {
const data = await this.context.entity.createAddress(entityId, body);
const data = await entityApi.createAddress(entityId, body);
requireObject(data, 'POST /api/entity/:id/address');
requireObject(data.address, 'address');
requireDefined(data.address.id, 'address.id');
const listRes = await this.context.entity.listAddresses(entityId);
const listRes = await entityApi.listAddresses(entityId);
requireObject(listRes, 'GET /api/entity/:id/address');
const addresses = requireArray(listRes.addresses, 'addresses');
if (this._cancelled) return;
@@ -299,8 +299,8 @@ class KundeEditPage extends PureComponent {
this.setState({ busyAddressKey: address.id });
try {
await this.context.entity.deleteAddress(entityId, address.id);
const listRes = await this.context.entity.listAddresses(entityId);
await entityApi.deleteAddress(entityId, address.id);
const listRes = await entityApi.listAddresses(entityId);
requireObject(listRes, 'GET /api/entity/:id/address');
const addresses = requireArray(listRes.addresses, 'addresses');
if (this._cancelled) return;
@@ -341,13 +341,13 @@ class KundeEditPage extends PureComponent {
this.setState({ busyContactKey: contact.id });
try {
const data = await this.context.entity.updateContact(entityId, contact.id, {
const data = await entityApi.updateContact(entityId, contact.id, {
contactType,
contact: value,
});
requireObject(data, 'PATCH /api/entity/:id/contact/:id');
requireObject(data.contact, 'contact');
const listRes = await this.context.entity.listContacts(entityId);
const listRes = await entityApi.listContacts(entityId);
requireObject(listRes, 'GET /api/entity/:id/contact');
const contacts = requireArray(listRes.contacts, 'contacts');
if (this._cancelled) return;
@@ -382,14 +382,14 @@ class KundeEditPage extends PureComponent {
this.setState({ busyContactKey: 'new' });
try {
const data = await this.context.entity.createContact(entityId, {
const data = await entityApi.createContact(entityId, {
contactType,
contact: value,
});
requireObject(data, 'POST /api/entity/:id/contact');
requireObject(data.contact, 'contact');
requireDefined(data.contact.id, 'contact.id');
const listRes = await this.context.entity.listContacts(entityId);
const listRes = await entityApi.listContacts(entityId);
requireObject(listRes, 'GET /api/entity/:id/contact');
const contacts = requireArray(listRes.contacts, 'contacts');
if (this._cancelled) return;
@@ -419,8 +419,8 @@ class KundeEditPage extends PureComponent {
this.setState({ busyContactKey: contact.id });
try {
await this.context.entity.deleteContact(entityId, contact.id);
const listRes = await this.context.entity.listContacts(entityId);
await entityApi.deleteContact(entityId, contact.id);
const listRes = await entityApi.listContacts(entityId);
requireObject(listRes, 'GET /api/entity/:id/contact');
const contacts = requireArray(listRes.contacts, 'contacts');
if (this._cancelled) return;

View File

@@ -15,7 +15,8 @@ import { enqueueSnackbar } from 'notistack';
import BackendError from '../../components/BackendError';
import SingleLineTextEdit from 'dashgui/SingleLineTextEdit';
import { PanelTile, PanelTileGrid } from 'dashgui/panels';
import { StorageManagerContext } from '../../api/storageManagerContext';
import { customergroup as customergroupApi } from '../../api';
import customergroupAutomat from '../../automats/customergroupAutomat';
import { requireArray, requireDefined, requireObject } from '../../utils/apiShape';
import { backendErrorFrom } from '../../utils/backendError';
import { notifyError } from '../../utils/notify';
@@ -43,8 +44,6 @@ function requireGroupListItem(row, path) {
}
class KundeGruppenPage extends PureComponent {
static contextType = StorageManagerContext;
constructor(props) {
super(props);
this.state = {
@@ -85,9 +84,7 @@ class KundeGruppenPage extends PureComponent {
async load() {
this.setState({ status: 'loading', error: null });
try {
const list =
this.context.customergroups ??
(await this.context.customergroupPromise);
const list = await customergroupAutomat.actions.ensureLoaded();
if (this._cancelled) return;
this.applyGroups(list);
} catch (err) {
@@ -101,7 +98,7 @@ class KundeGruppenPage extends PureComponent {
}
async refreshFromServer() {
const list = await this.context.updateCustomergroup();
const list = await customergroupAutomat.actions.reload();
if (this._cancelled) return;
this.applyGroups(list);
}
@@ -112,7 +109,7 @@ class KundeGruppenPage extends PureComponent {
this.setState({ busyKey: 'new' });
try {
const data = await this.context.customergroup.create({ name });
const data = await customergroupApi.create({ name });
requireObject(data, 'POST /api/customergroup');
requireObject(data.customergroup, 'customergroup');
requireDefined(data.customergroup.id, 'customergroup.id');
@@ -141,7 +138,7 @@ class KundeGruppenPage extends PureComponent {
this.setState({ busyKey: group.id });
try {
const data = await this.context.customergroup.update(group.id, {
const data = await customergroupApi.update(group.id, {
name: draft,
});
requireObject(data, 'PATCH /api/customergroup/:id');
@@ -177,7 +174,7 @@ class KundeGruppenPage extends PureComponent {
this.setState({ busyKey: group.id });
try {
const data = await this.context.customergroup.order({ customergroupIds });
const data = await customergroupApi.order({ customergroupIds });
requireObject(data, 'PUT /api/customergroup/order');
requireArray(data.customergroupIds, 'customergroupIds');
await this.refreshFromServer();
@@ -204,7 +201,7 @@ class KundeGruppenPage extends PureComponent {
this.setState({ busyKey: group.id });
try {
await this.context.customergroup.delete(group.id);
await customergroupApi.delete(group.id);
await this.refreshFromServer();
if (this._cancelled) return;
this.setState({ busyKey: null });

View File

@@ -8,17 +8,17 @@ import TextField from '@mui/material/TextField';
import { enqueueSnackbar } from 'notistack';
import SingleLineTextEdit from 'dashgui/SingleLineTextEdit';
import { PanelTile, PanelTileGrid } from 'dashgui/panels';
import { StorageManagerContext } from '../../api/storageManagerContext';
import { entity as entityApi } from '../../api';
import entityAutomat from '../../automats/entityAutomat';
import customergroupAutomat from '../../automats/customergroupAutomat';
import { router } from '../../appRouter';
import { requireDefined, requireObject } from '../../utils/apiShape';
import { notifyError } from '../../utils/notify';
class KundeNeuPage extends PureComponent {
static contextType = StorageManagerContext;
constructor(props, context) {
super(props, context);
const groups = this.context.customergroups ?? [];
constructor(props) {
super(props);
const groups = customergroupAutomat.state.items ?? [];
this.state = {
customergroupId: groups.length > 0 ? groups[0].id : null,
number: '',
@@ -36,7 +36,7 @@ class KundeNeuPage extends PureComponent {
async loadNumber() {
try {
const res = await this.context.entity.nextNumber();
const res = await entityApi.nextNumber();
requireObject(res, 'GET /api/entity/next-number');
requireDefined(res.number, 'number');
this.setState({ number: res.number });
@@ -73,11 +73,11 @@ class KundeNeuPage extends PureComponent {
this.setState({ saving: true });
try {
const data = await this.context.entity.create(body);
const data = await entityApi.create(body);
requireObject(data, 'POST /api/entity');
requireObject(data.entity, 'entity');
requireDefined(data.entity.id, 'entity.id');
await this.context.updateEntity();
await entityAutomat.actions.reload();
enqueueSnackbar('Kunde angelegt.', { variant: 'success' });
void router.navigate(`/kunde/${data.entity.id}`);
} catch (err) {
@@ -88,7 +88,7 @@ class KundeNeuPage extends PureComponent {
render() {
const { customergroupId, number, name, firstName, saving } = this.state;
const groups = this.context.customergroups ?? [];
const groups = customergroupAutomat.state.items ?? [];
const canSave =
customergroupId != null && name.trim() !== '' && !saving;

View File

@@ -37,8 +37,9 @@ import {
BookDeliveryDialog,
} from '../../components/manifest';
import ManifestPdfDialog from '../../components/manifest/ManifestPdfDialog';
import { loadParties } from '../../components/manifest/manifestConfig';
import { StorageManagerContext } from '../../api/storageManagerContext';
import { loadParties, apiFor } from '../../components/manifest/manifestConfig';
import { article as articleApi, delivery as deliveryApi } from '../../api';
import { getCategoryState } from '../../automats/categoryAutomat';
import { requireArray, requireDefined, requireObject } from '../../utils/apiShape';
import { backendErrorFrom } from '../../utils/backendError';
import { notifyError } from '../../utils/notify';
@@ -47,12 +48,10 @@ import { generateTextThumbnail } from '../../utils/textThumbnail';
import { extractPdfText } from '../../utils/pdfText';
import { getDeliveryEditorAutomat } from '../../automats/deliveryEditorAutomat';
import { setLastDeliveryPath, userSettingsAutomat, setViewerPosition } from '../../automats';
import { delivery as deliveryApi, ledger as defaultLedgerApi } from '../../api';
import { fetchDeliveryLedgerSummary } from '../../utils/deliveryLedger';
class LieferungNeuPage extends PureComponent {
static contextType = StorageManagerContext;
constructor(props) {
super(props);
this.cfg = manifestConfig('delivery');
@@ -168,7 +167,7 @@ class LieferungNeuPage extends PureComponent {
async loadPartiesOnly() {
this.setState({ status: 'loadingParties', error: null, partiesLoading: true });
try {
const parties = await loadParties('delivery', this.context);
const parties = await loadParties('delivery');
if (this._cancelled) return;
this.automat.actions.setDeliveryEntities(parties);
this.setState({
@@ -200,8 +199,8 @@ class LieferungNeuPage extends PureComponent {
this.setState({ status: 'loading', error: null, partiesLoading: true });
try {
const [docRes, parties] = await Promise.all([
this.context[cfg.api].get(docId),
loadParties('delivery', this.context),
apiFor(cfg.kind).get(docId),
loadParties('delivery'),
]);
if (this._cancelled) return;
@@ -228,12 +227,7 @@ class LieferungNeuPage extends PureComponent {
let ledgerSummary = null;
try {
const ledgerClient = this.context?.ledger || defaultLedgerApi;
const deliveryClient = this.context?.[cfg.api] || deliveryApi;
ledgerSummary = await fetchDeliveryLedgerSummary(docId, {
ledgerApi: ledgerClient,
deliveryApi: deliveryClient,
});
ledgerSummary = await fetchDeliveryLedgerSummary(docId);
} catch (lErr) {
console.error('[neu] Failed to load ledger summary:', lErr);
}
@@ -272,12 +266,7 @@ class LieferungNeuPage extends PureComponent {
const { docId } = this.props;
if (docId == null) return;
try {
const ledgerClient = this.context?.ledger || defaultLedgerApi;
const deliveryClient = this.context?.[this.cfg.api] || deliveryApi;
const ledgerSummary = await fetchDeliveryLedgerSummary(docId, {
ledgerApi: ledgerClient,
deliveryApi: deliveryClient,
});
const ledgerSummary = await fetchDeliveryLedgerSummary(docId);
if (this._cancelled) return;
this.setState({ ledgerSummary });
} catch (err) {
@@ -381,7 +370,7 @@ class LieferungNeuPage extends PureComponent {
try {
// Get root category ID
const categoryState = this.context.getCategoryState();
const categoryState = getCategoryState();
if (!categoryState) {
enqueueSnackbar('Kategorie-Daten nicht verfügbar.', { variant: 'error' });
return;
@@ -394,7 +383,7 @@ class LieferungNeuPage extends PureComponent {
}
// Create article in root category
const articleRes = await this.context.article.create({
const articleRes = await articleApi.create({
parentId: rootCategoryId,
name: categoryName,
});
@@ -407,7 +396,7 @@ class LieferungNeuPage extends PureComponent {
const supplierSku = line.sku;
// Add supplier relationship with the supplier's SKU
await this.context.article.addSupplier(internalSku, {
await articleApi.addSupplier(internalSku, {
entityId: partyId,
sku: supplierSku || undefined,
pu: line.pu ? Number(line.pu) : undefined,
@@ -457,7 +446,7 @@ class LieferungNeuPage extends PureComponent {
const supplierSku = line?.sku;
try {
await this.context.article.addSupplier(internalSku, {
await articleApi.addSupplier(internalSku, {
entityId: partyId,
sku: supplierSku || undefined,
pu: line?.pu ? Number(line.pu) : undefined,
@@ -699,10 +688,7 @@ class LieferungNeuPage extends PureComponent {
};
try {
const parser = this.context?.delivery?.parseSse;
if (!parser) throw new Error('parseSse nicht verfügbar.');
for await (const { event, data } of parser(file, { signal: abortController.signal })) {
for await (const { event, data } of deliveryApi.parseSse(file, { signal: abortController.signal })) {
if (this._cancelled || abortController.signal.aborted) return;
switch (event) {
case 'status':
@@ -914,10 +900,7 @@ class LieferungNeuPage extends PureComponent {
}
try {
const getFile =
this.context?.delivery?.getFile?.bind(this.context.delivery) ||
deliveryApi.getFile;
const blob = await getFile(docId, fileId);
const blob = await deliveryApi.getFile(docId, fileId);
const filename = attachment.name || 'Dokument';
const mimetype = attachment.type || blob.type || 'application/octet-stream';
const file = new File([blob], filename, { type: mimetype });
@@ -1164,23 +1147,24 @@ class LieferungNeuPage extends PureComponent {
}
this.setState({ saving: true });
const api = apiFor(cfg.kind);
try {
if (isCreate) {
const data = await this.context[cfg.api].create(body);
const data = await api.create(body);
requireObject(data, cfg.postPathLabel);
requireObject(data[cfg.docField], cfg.docField);
requireDefined(data[cfg.docField].id, `${cfg.docField}.id`);
await this.context[cfg.updateCache]();
await cfg.listAutomat.actions.reload();
if (this._cancelled) return;
enqueueSnackbar(cfg.createdMsg, { variant: 'success' });
this.automat.actions.resetDelivery();
this.props.navigate(cfg.editPath(data[cfg.docField].id));
} else {
const data = await this.context[cfg.api].update(docId, body);
const data = await api.update(docId, body);
requireObject(data, cfg.patchPathLabel);
requireObject(data[cfg.docField], cfg.docField);
requireDefined(data[cfg.docField].id, `${cfg.docField}.id`);
await this.context[cfg.updateCache]();
await cfg.listAutomat.actions.reload();
if (this._cancelled) return;
const doc = data[cfg.docField];
const apiLines = requireArray(doc.lines, `${cfg.docField}.lines`);