This commit is contained in:
seb
2026-09-21 20:21:57 +02:00
parent 2046f96768
commit 536e7b1e2a
20 changed files with 3047 additions and 672 deletions

View File

@@ -1,8 +1,25 @@
import { Component } from 'react';
import Box from '@mui/material/Box';
import CircularProgress from '@mui/material/CircularProgress';
import { StorageManagerContext } from '../../api/storageManagerContext';
import BackendError from '../../components/BackendError';
import { ArticleView } from '../../components/article';
import {
artikelAutomat,
createArticleSelector,
getArticle,
setLastArticleSku,
} from '../../automats';
import withSkuRoute from './withSkuRoute';
import { setLastArticleSku } from '../../automats';
/** Full-screen loading state while the shared article cache resolves. */
function ArticleLoading() {
return (
<Box sx={{ display: 'flex', justifyContent: 'center', py: 8 }}>
<CircularProgress size={32} />
</Box>
);
}
class ArtikelSkuPage extends Component {
static contextType = StorageManagerContext;
@@ -10,43 +27,66 @@ class ArtikelSkuPage extends Component {
constructor(props, context) {
super(props, context);
this.state = {
ready:
context.getArticle(props.sku) != null &&
context.getCategoryState() != null,
article: getArticle(props.sku),
ready: context.getCategoryState() != null,
error: null,
};
}
componentDidMount() {
this._mounted = true;
this.installEscape();
setLastArticleSku(this.props.sku);
if (!this.state.ready) void this.ensureArticle();
this.unsub = artikelAutomat.subscribe(this, (state) => ({
article: createArticleSelector(this.props.sku)(state),
}));
void this.ensureArticle();
}
componentDidUpdate(prevProps) {
if (prevProps.sku !== this.props.sku) {
setLastArticleSku(this.props.sku);
const ready =
this.context.getArticle(this.props.sku) != null &&
this.context.getCategoryState() != null;
if (ready !== this.state.ready) this.setState({ ready });
if (!ready) void this.ensureArticle();
this.setState({ article: getArticle(this.props.sku), error: null });
this.unsub?.();
this.unsub = artikelAutomat.subscribe(this, (state) => ({
article: createArticleSelector(this.props.sku)(state),
}));
void this.ensureArticle();
}
}
componentWillUnmount() {
this._mounted = false;
this.removeEscape();
this.unsub?.();
this.unsub = null;
}
/**
* Delegates the article fetch to the shared artikelAutomat cache; also waits
* for the category tree/company/session the view tiles depend on.
*/
async ensureArticle() {
await Promise.all([
this.context.loadArticle(this.props.sku),
this.context.categoryTreePromise,
this.context.companyPromise,
this.context.authMePromise,
]);
this.setState({ ready: true });
const sku = this.props.sku;
if (!sku) return;
try {
await Promise.all([
artikelAutomat.actions.loadArticle(sku),
this.context.categoryTreePromise,
this.context.companyPromise,
this.context.authMePromise,
]);
if (this._mounted) this.setState({ ready: true });
} catch (error) {
if (this._mounted) this.setState({ error });
}
}
handleRetry = () => {
this.setState({ error: null });
void this.ensureArticle();
};
installEscape() {
this._onKeyDown = (e) => {
if (e.key !== 'Escape') return;
@@ -65,7 +105,12 @@ class ArtikelSkuPage extends Component {
render() {
const { sku } = this.props;
if (!this.state.ready) return null;
const { article, ready, error } = this.state;
if (error) {
return <BackendError error={error} sx={{ mt: 2 }} onRetry={this.handleRetry} />;
}
if (!article || !ready) return <ArticleLoading />;
return <ArticleView sku={sku} />;
}
}

View File

@@ -20,7 +20,8 @@ import {
emptyLine,
lineFromApi,
manifestConfig,
TextEditorDialog,
TextEditorPanel,
PdfViewerPanel,
ArticleSearchDialog,
} from '../../components/manifest';
import ManifestPdfDialog from '../../components/manifest/ManifestPdfDialog';
@@ -32,7 +33,7 @@ import { notifyError } from '../../utils/notify';
import { generatePdfThumbnail } from '../../utils/pdfThumbnail';
import { extractPdfText } from '../../utils/pdfText';
import { getDeliveryEditorAutomat } from '../../automats/deliveryEditorAutomat';
import { setLastDeliveryPath } from '../../automats';
import { setLastDeliveryPath, userSettingsAutomat, setViewerPosition } from '../../automats';
import { delivery as deliveryApi } from '../../api';
class LieferungNeuPage extends PureComponent {
@@ -54,6 +55,9 @@ class LieferungNeuPage extends PureComponent {
pdfFileName: '',
textEditorOpen: false,
textEditorAttachment: null,
viewerPosition: userSettingsAutomat.state.viewerPosition,
pdfViewerOpen: false,
pdfViewerAttachment: null,
articleSearchDialogOpen: false,
articleSearchLine: null,
unmatchedSupplierName: null,
@@ -77,6 +81,8 @@ class LieferungNeuPage extends PureComponent {
this.handleAbortParse = this.handleAbortParse.bind(this);
this.handleAttachmentClick = this.handleAttachmentClick.bind(this);
this.handleCloseTextEditor = this.handleCloseTextEditor.bind(this);
this.handleTextEditorPositionChange = this.handleTextEditorPositionChange.bind(this);
this.handleClosePdfViewer = this.handleClosePdfViewer.bind(this);
this.handleSaveTextEditor = this.handleSaveTextEditor.bind(this);
this.handleTextEditorContentLoaded = this.handleTextEditorContentLoaded.bind(this);
this.ensureAttachmentFile = this.ensureAttachmentFile.bind(this);
@@ -90,6 +96,9 @@ class LieferungNeuPage extends PureComponent {
componentDidMount() {
this.unsubscribe = this.automat.subscribe(this);
this._settingsUnsub = userSettingsAutomat.subscribe(this, (state) => ({
viewerPosition: state.viewerPosition,
}));
this._cancelled = false;
if (this.props.docId != null) {
setLastDeliveryPath(`/lieferung/${this.props.docId}`);
@@ -131,6 +140,7 @@ class LieferungNeuPage extends PureComponent {
this._skuDebounceTimers = null;
}
this.unsubscribe?.();
this._settingsUnsub?.();
}
async loadPartiesOnly() {
@@ -862,30 +872,21 @@ class LieferungNeuPage extends PureComponent {
this.setState({
textEditorOpen: true,
textEditorAttachment: attachment,
pdfViewerOpen: false,
pdfViewerAttachment: null,
});
return;
}
if (isPdf) {
if (
(attachment.file instanceof Blob || attachment.file instanceof File) &&
attachment.file.size > 0
) {
const url = URL.createObjectURL(attachment.file);
window.open(url, '_blank');
return;
}
const docId = this.props.docId ?? attachment.manifestId ?? this.state.id;
const fileId = attachment.fileId ?? attachment.id;
if (docId != null && fileId != null) {
window.open(
`/api/delivery/${encodeURIComponent(docId)}/file/${encodeURIComponent(fileId)}`,
'_blank',
);
void this.ensureAttachmentFile(attachment);
return;
}
this.setState({
pdfViewerOpen: true,
pdfViewerAttachment: attachment,
textEditorOpen: false,
textEditorAttachment: null,
});
void this.ensureAttachmentFile(attachment);
return;
}
if (attachment.file instanceof Blob || attachment.file instanceof File) {
@@ -913,6 +914,88 @@ class LieferungNeuPage extends PureComponent {
});
}
handleClosePdfViewer() {
this.setState({
pdfViewerOpen: false,
pdfViewerAttachment: null,
});
}
handleTextEditorPositionChange(position) {
setViewerPosition(position);
}
renderViewerPanels(wide) {
const {
textEditorOpen,
textEditorAttachment,
pdfViewerOpen,
pdfViewerAttachment,
viewerPosition,
} = this.state;
const docId = this.props.docId ?? this.state.id;
const panels = [];
if (textEditorOpen && textEditorAttachment) {
panels.push(
<TextEditorPanel
key="text"
docId={docId}
attachment={textEditorAttachment}
position={viewerPosition}
wide={wide}
height={panels.length === 0 ? '60vh' : '40vh'}
onPositionChange={this.handleTextEditorPositionChange}
onClose={this.handleCloseTextEditor}
onSave={this.handleSaveTextEditor}
onContentLoad={this.handleTextEditorContentLoaded}
/>,
);
}
if (pdfViewerOpen && pdfViewerAttachment) {
panels.push(
<PdfViewerPanel
key="pdf"
docId={docId}
attachment={pdfViewerAttachment}
position={viewerPosition}
wide={wide}
height={panels.length === 0 ? '60vh' : '40vh'}
onPositionChange={this.handleTextEditorPositionChange}
onClose={this.handleClosePdfViewer}
/>,
);
}
if (panels.length === 0) return null;
return (
<Box
sx={{
gridColumn: { xs: '1 / -1', sm: wide >= 2 ? 'span 2' : 'span 1', md: `span ${wide}` },
display: 'flex',
flexDirection: 'column',
gap: 2,
minWidth: 0,
...(viewerPosition === 'right'
? { position: 'sticky', top: 16, alignSelf: 'flex-start' }
: {}),
}}
>
{panels}
</Box>
);
}
isViewerOpen() {
return Boolean(
(this.state.textEditorOpen && this.state.textEditorAttachment) ||
(this.state.pdfViewerOpen && this.state.pdfViewerAttachment),
);
}
handleSaveTextEditor(newText) {
const attachment = this.state.textEditorAttachment;
if (!attachment) return;
@@ -1067,6 +1150,7 @@ class LieferungNeuPage extends PureComponent {
onNumberChange={this.handleNumberChange}
onExternalNumberChange={this.handleExternalNumberChange}
/>
{this.state.viewerPosition === 'top' && this.renderViewerPanels(3)}
{partyId == null && (!lines || lines.length === 0) ? (
<PanelTile wide={3} minHeight={160} color="#e2e8f0">
{this.state.unmatchedSupplierName ? (
@@ -1120,6 +1204,11 @@ class LieferungNeuPage extends PureComponent {
columns={DELIVERY_LINE_COLUMNS}
lines={lines}
saving={saving}
wide={
this.state.viewerPosition === 'right' && this.isViewerOpen()
? 2
: 3
}
linesColor={cfg.linesColor}
supplierArticlesMap={this.state.supplierArticlesMap}
supplierArticlesByLineKey={this.state.supplierArticlesByLineKey}
@@ -1137,6 +1226,8 @@ class LieferungNeuPage extends PureComponent {
/>
</>
)}
{this.state.viewerPosition === 'right' && this.renderViewerPanels(2)}
{this.state.viewerPosition === 'below' && this.renderViewerPanels(3)}
{notes && notes.length > 0 && (
<PanelTile wide={3} color="#fffbeb">
<Typography variant="subtitle2" fontWeight={700} color="warning.dark" sx={{ mb: 1.5 }}>
@@ -1162,14 +1253,6 @@ class LieferungNeuPage extends PureComponent {
onAbort={this.handleAbortParse}
/>
<TextEditorDialog
open={this.state.textEditorOpen}
docId={this.props.docId ?? this.state.id}
attachment={this.state.textEditorAttachment}
onClose={this.handleCloseTextEditor}
onSave={this.handleSaveTextEditor}
onContentLoad={this.handleTextEditorContentLoaded}
/>
<ArticleSearchDialog
open={this.state.articleSearchDialogOpen}
line={this.state.articleSearchLine}