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

@@ -20,13 +20,11 @@ import CloseIcon from '@mui/icons-material/Close';
import SearchIcon from '@mui/icons-material/Search';
import Inventory2OutlinedIcon from '@mui/icons-material/Inventory2Outlined';
import LinkIcon from '@mui/icons-material/Link';
import { StorageManagerContext } from '../../api/storageManagerContext';
import articleIndexAutomat from '../../automats/articleIndexAutomat';
import ArticlePhoto from '../article/ArticlePhoto';
import { BrandLabel } from '../brand';
export default class ArticleSearchDialog extends PureComponent {
static contextType = StorageManagerContext;
constructor(props) {
super(props);
this.state = {
@@ -68,13 +66,7 @@ export default class ArticleSearchDialog extends PureComponent {
async loadArticles() {
this.setState({ loading: true, error: null });
try {
let list = [];
if (this.context?.articleIndexPromise) {
list = await this.context.articleIndexPromise;
} else if (this.context?.article?.index) {
const res = await this.context.article.index();
list = res?.articles || [];
}
const list = await articleIndexAutomat.actions.ensureLoaded();
this.setState({ articles: Array.isArray(list) ? list : [], loading: false });
} catch (err) {
console.error('Fehler beim Laden des Artikel-Index:', err);

View File

@@ -27,7 +27,6 @@ import CloseIcon from '@mui/icons-material/Close';
import Inventory2OutlinedIcon from '@mui/icons-material/Inventory2Outlined';
import CheckCircleOutlineOutlinedIcon from '@mui/icons-material/CheckCircleOutlineOutlined';
import WarehouseIcon from '@mui/icons-material/Warehouse';
import { StorageManagerContext } from '../../api/storageManagerContext';
import { enqueueSnackbar } from 'notistack';
import { notifyError } from '../../utils/notify';
import { formatDate } from './manifestConfig';
@@ -38,8 +37,6 @@ import {
import warehouseAutomat from '../../automats/warehouseAutomat';
export default class BookDeliveryDialog extends PureComponent {
static contextType = StorageManagerContext;
constructor(props) {
super(props);
@@ -160,12 +157,7 @@ export default class BookDeliveryDialog extends PureComponent {
this.setState({ loading: true, error: null });
try {
const ledgerApi = this.context?.ledger;
const deliveryApi = this.context?.delivery;
const summary = await fetchDeliveryLedgerSummary(delivery.id, {
ledgerApi,
deliveryApi,
});
const summary = await fetchDeliveryLedgerSummary(delivery.id);
this.initAmountsFromSummary(summary, prefillLineId);
this.setState({ summary, loading: false });
} catch (err) {
@@ -255,14 +247,10 @@ export default class BookDeliveryDialog extends PureComponent {
this.setState({ isBooking: true, error: null, warehouseError: null });
try {
const ledgerApi = this.context?.ledger;
const deliveryApi = this.context?.delivery;
await bookDeliveryLines({
delivery: summary.delivery || delivery,
linesToBook,
warehouseId: selectedWarehouseId,
ledgerApi,
transactionText,
});
@@ -273,10 +261,7 @@ export default class BookDeliveryDialog extends PureComponent {
);
// Refresh summary
const updatedSummary = await fetchDeliveryLedgerSummary(delivery.id, {
ledgerApi,
deliveryApi,
});
const updatedSummary = await fetchDeliveryLedgerSummary(delivery.id);
if (onBookSuccess) {
onBookSuccess(updatedSummary);

View File

@@ -13,7 +13,7 @@ import DescriptionOutlinedIcon from '@mui/icons-material/DescriptionOutlined';
import { enqueueSnackbar } from 'notistack';
import BackendError from '../BackendError';
import { RecordList } from 'dashgui/lists';
import { StorageManagerContext } from '../../api/storageManagerContext';
import deliveryAutomat from '../../automats/deliveryAutomat';
import { delivery } from '../../api';
import { supplierAutomat } from '../../automats';
import { backendErrorFrom } from '../../utils/backendError';
@@ -39,8 +39,6 @@ function formatFileSize(bytes) {
}
export class DeliveryList extends PureComponent {
static contextType = StorageManagerContext;
constructor(props) {
super(props);
this.state = {
@@ -77,14 +75,7 @@ export class DeliveryList extends PureComponent {
}
async fetchDeliveries() {
if (this.context?.deliveries) {
return this.context.deliveries;
}
if (this.context?.deliveryPromise) {
return await this.context.deliveryPromise;
}
const res = await (this.context?.delivery?.list ? this.context.delivery.list() : delivery.list());
return res?.deliveries ?? res ?? [];
return deliveryAutomat.actions.ensureLoaded();
}
async load() {
@@ -103,12 +94,10 @@ export class DeliveryList extends PureComponent {
}
async handleRetry() {
if (this.context?.updateDelivery) {
try {
await this.context.updateDelivery();
} catch {
// Will be caught in load()
}
try {
await deliveryAutomat.actions.reload();
} catch {
// Will be caught in load()
}
void this.load();
}
@@ -140,15 +129,13 @@ export class DeliveryList extends PureComponent {
async handleBookSuccess() {
this.handleCloseBookDialog();
if (this.context?.updateDelivery) {
try {
const updated = await this.context.updateDelivery();
if (this._cancelled) return;
this.setState({ docs: sortDeliveries(updated) });
return;
} catch {
// Fall back to load()
}
try {
const updated = await deliveryAutomat.actions.reload();
if (this._cancelled) return;
this.setState({ docs: sortDeliveries(updated) });
return;
} catch {
// Fall back to load()
}
void this.load();
}
@@ -159,10 +146,8 @@ export class DeliveryList extends PureComponent {
this.setState({ busyId: doc.id });
try {
await (this.context?.delivery?.delete ? this.context.delivery.delete(doc.id) : delivery.delete(doc.id));
if (this.context?.updateDelivery) {
await this.context.updateDelivery();
}
await delivery.delete(doc.id);
await deliveryAutomat.actions.reload();
if (this._cancelled) return;
await this.load();
enqueueSnackbar('Lieferung gelöscht.', { variant: 'success' });

View File

@@ -4,7 +4,7 @@ import CircularProgress from '@mui/material/CircularProgress';
import { enqueueSnackbar } from 'notistack';
import BackendError from '../BackendError';
import { PanelTileGrid } from 'dashgui/panels';
import { StorageManagerContext } from '../../api/storageManagerContext';
import { delivery as deliveryApi } from '../../api';
import { requireArray, requireDefined, requireObject } from '../../utils/apiShape';
import { backendErrorFrom } from '../../utils/backendError';
import { notifyError } from '../../utils/notify';
@@ -20,11 +20,10 @@ import {
lineFromApi,
loadParties,
manifestConfig,
apiFor,
} from './manifestConfig';
class ManifestForm extends PureComponent {
static contextType = StorageManagerContext;
constructor(props) {
super(props);
this.cfg = manifestConfig(props.kind);
@@ -88,7 +87,7 @@ class ManifestForm extends PureComponent {
async loadPartiesOnly() {
this.setState({ status: 'loadingParties', error: null, partiesLoading: true });
try {
const parties = await loadParties(this.props.kind, this.context);
const parties = await loadParties(this.props.kind);
if (this._cancelled) return;
this.setState({
status: 'ready',
@@ -120,8 +119,8 @@ class ManifestForm extends PureComponent {
this.setState({ status: 'loading', error: null, partiesLoading: true });
try {
const [docRes, parties] = await Promise.all([
this.context[cfg.api].get(docId),
loadParties(kind, this.context),
apiFor(cfg).get(docId),
loadParties(kind),
]);
if (this._cancelled) return;
@@ -243,10 +242,10 @@ class ManifestForm extends PureComponent {
};
try {
const gen = this.context?.delivery?.parsePdfSse;
const gen = deliveryApi.parsePdfSse;
if (!gen) throw new Error('parsePdfSse nicht verfügbar.');
for await (const { event, data } of this.context.delivery.parsePdfSse(file, { signal: abortController.signal })) {
for await (const { event, data } of gen(file, { signal: abortController.signal })) {
if (this._cancelled || abortController.signal.aborted) return;
switch (event) {
case 'status':
@@ -372,24 +371,25 @@ class ManifestForm extends PureComponent {
}
this.setState({ saving: true });
const api = apiFor(cfg);
try {
if (mode === 'create') {
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.props.navigate(cfg.editPath(data[cfg.docField].id));
return;
}
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`);

View File

@@ -2,7 +2,6 @@ import { PureComponent } from 'react';
import { enqueueSnackbar } from 'notistack';
import BackendError from '../BackendError';
import { RecordList } from 'dashgui/lists';
import { StorageManagerContext } from '../../api/storageManagerContext';
import { backendErrorFrom } from '../../utils/backendError';
import { notifyError } from '../../utils/notify';
import withNavigate from '../withNavigate';
@@ -14,11 +13,10 @@ import {
numberDisplay,
partyDisplayName,
sortDocs,
apiFor,
} from './manifestConfig';
class ManifestList extends PureComponent {
static contextType = StorageManagerContext;
constructor(props) {
super(props);
this.cfg = manifestConfig(props.kind);
@@ -52,8 +50,8 @@ class ManifestList extends PureComponent {
this.setState({ status: 'loading', error: null });
try {
const [docs, parties] = await Promise.all([
cfg.listPromise(this.context),
loadParties(this.props.kind, this.context),
cfg.listAutomat.actions.ensureLoaded(),
loadParties(this.props.kind),
]);
if (this._cancelled) return;
const sortedDocs = sortDocs(docs);
@@ -96,8 +94,9 @@ class ManifestList extends PureComponent {
const cfg = this.cfg;
this.setState({ busyId: doc.id });
try {
await this.context[cfg.api].delete(doc.id);
const list = await this.context[cfg.updateCache]();
const api = apiFor(cfg.kind);
await api.delete(doc.id);
const list = await cfg.listAutomat.actions.reload();
if (this._cancelled) return;
this.setState({
docs: sortDocs(list),

View File

@@ -14,7 +14,6 @@ import VerticalAlignBottomIcon from '@mui/icons-material/VerticalAlignBottom';
import ViewSidebarIcon from '@mui/icons-material/ViewSidebar';
import { enqueueSnackbar } from 'notistack';
import { PanelTile } from 'dashgui/panels';
import { StorageManagerContext } from '../../api/storageManagerContext';
import { delivery as deliveryApi } from '../../api';
/**
@@ -24,8 +23,6 @@ import { delivery as deliveryApi } from '../../api';
* attachment is selected; unmounting closes it.
*/
export class PdfViewerPanel extends PureComponent {
static contextType = StorageManagerContext;
constructor(props) {
super(props);
this.state = {
@@ -77,10 +74,7 @@ export class PdfViewerPanel extends PureComponent {
if (effectiveDocId != null && fileId != null) {
this.setState({ loading: true, error: null });
try {
const getFile =
this.context?.delivery?.getFile?.bind(this.context.delivery) ||
deliveryApi.getFile;
const blob = await getFile(effectiveDocId, fileId);
const blob = await deliveryApi.getFile(effectiveDocId, fileId);
this._objectUrl = URL.createObjectURL(blob);
this.setState({ url: this._objectUrl, loading: false, error: null });
return;

View File

@@ -17,7 +17,6 @@ import VerticalAlignBottomIcon from '@mui/icons-material/VerticalAlignBottom';
import ViewSidebarIcon from '@mui/icons-material/ViewSidebar';
import { enqueueSnackbar } from 'notistack';
import { PanelTile } from 'dashgui/panels';
import { StorageManagerContext } from '../../api/storageManagerContext';
import { delivery as deliveryApi } from '../../api';
/**
@@ -26,8 +25,6 @@ import { delivery as deliveryApi } from '../../api';
* selected; unmounting closes it.
*/
export class TextEditorPanel extends PureComponent {
static contextType = StorageManagerContext;
constructor(props) {
super(props);
this.state = {
@@ -89,10 +86,7 @@ export class TextEditorPanel extends PureComponent {
if (effectiveDocId != null && fileId != null) {
this.setState({ loading: true });
try {
const getFile =
this.context?.delivery?.getFile?.bind(this.context.delivery) ||
deliveryApi.getFile;
const blob = await getFile(effectiveDocId, fileId);
const blob = await deliveryApi.getFile(effectiveDocId, fileId);
const text = await blob.text();
this.setState({
content: text,

View File

@@ -1,4 +1,14 @@
import {
delivery as deliveryApi,
quote as quoteApi,
order as orderApi,
} from '../../api';
import supplierAutomat from '../../automats/supplierAutomat';
import entityAutomat from '../../automats/entityAutomat';
import customergroupAutomat from '../../automats/customergroupAutomat';
import deliveryAutomat from '../../automats/deliveryAutomat';
import quoteAutomat from '../../automats/quoteAutomat';
import orderAutomat from '../../automats/orderAutomat';
/** Groups that are not sales customers (filter client-side). */
export const EXCLUDED_CUSTOMER_GROUP_NAMES = new Set(['Lieferanten', 'POS']);
@@ -481,12 +491,27 @@ export const DELIVERY_LINE_COLUMNS = [
/** @typedef {'delivery'|'quote'|'order'} ManifestKind */
/**
* Resolves the api namespace for a manifest kind.
*
* Delivery, quote, and order share one editor/list shape but talk to three
* different endpoints.
*
* @param {ManifestKind} kind Manifest kind.
* @returns {{get: Function, create: Function, update: Function, delete: Function}}
*/
export function apiFor(kind) {
if (kind === 'delivery') return deliveryApi;
if (kind === 'quote') return quoteApi;
if (kind === 'order') return orderApi;
throw new Error(`Unknown manifest kind: ${kind}`);
}
/**
* Load party options for Autocomplete / list name lookup.
* @param {ManifestKind} kind
* @param {object} ctx StorageManager instance
*/
export async function loadParties(kind, ctx) {
export async function loadParties(kind) {
if (kind === 'delivery') {
if (supplierAutomat.state.status === 'ready') {
return sortSuppliers(supplierAutomat.state.suppliers);
@@ -495,8 +520,8 @@ export async function loadParties(kind, ctx) {
return sortSuppliers(list);
}
const [entities, groups] = await Promise.all([
ctx.entities ?? ctx.entityPromise,
ctx.customergroups ?? ctx.customergroupPromise,
entityAutomat.actions.ensureLoaded(),
customergroupAutomat.actions.ensureLoaded(),
]);
return filterCustomerEntities(entities, groups);
}
@@ -538,8 +563,7 @@ export function manifestConfig(kind) {
headerColor: '#fed7aa',
linesColor: '#bbf7d0',
requirePartyBeforeLines: false,
updateCache: 'updateDelivery',
listPromise: (ctx) => ctx.deliveries ?? ctx.deliveryPromise,
listAutomat: deliveryAutomat,
partiesByIdFromList: (parties) =>
Object.fromEntries(parties.map((row) => [row.id, row])),
// Delivery uses a server-allocated read-only number plus an optional
@@ -575,8 +599,7 @@ export function manifestConfig(kind) {
headerColor: '#fde68a',
linesColor: '#bbf7d0',
requirePartyBeforeLines: false,
updateCache: 'updateQuote',
listPromise: (ctx) => ctx.quotes ?? ctx.quotePromise,
listAutomat: quoteAutomat,
partiesByIdFromList: (parties) =>
Object.fromEntries(parties.map((row) => [row.id, row])),
columns: LINE_COLUMNS,
@@ -609,8 +632,7 @@ export function manifestConfig(kind) {
headerColor: '#c4b5fd',
linesColor: '#bbf7d0',
requirePartyBeforeLines: false,
updateCache: 'updateOrder',
listPromise: (ctx) => ctx.orders ?? ctx.orderPromise,
listAutomat: orderAutomat,
partiesByIdFromList: (parties) =>
Object.fromEntries(parties.map((row) => [row.id, row])),
columns: LINE_COLUMNS,