feat(delivery): replace deliveryEditorSlice with automat and add PDF/text attachment handling
- replace Redux deliveryEditorSlice with deliveryEditorAutomat using automat - add Attachment and AttachmentList components in Kopfdaten - extract PDF text via pdftotext-wasm and render PDF thumbnails with pdfjs-dist - add TextEditorDialog to view and edit extracted text files (preformatted monospace, no wrap) - configure Vite server.fs.allow for pdftotext.wasm
This commit is contained in:
@@ -1,5 +1,4 @@
|
||||
import { PureComponent } from 'react';
|
||||
import { legacy_connect as connect } from 'react-redux';
|
||||
import Alert from '@mui/material/Alert';
|
||||
import Box from '@mui/material/Box';
|
||||
import CircularProgress from '@mui/material/CircularProgress';
|
||||
@@ -18,6 +17,7 @@ import {
|
||||
emptyLine,
|
||||
lineFromApi,
|
||||
manifestConfig,
|
||||
TextEditorDialog,
|
||||
} from '../../components/manifest';
|
||||
import ManifestPdfDialog from '../../components/manifest/ManifestPdfDialog';
|
||||
import { loadParties } from '../../components/manifest/manifestConfig';
|
||||
@@ -25,29 +25,9 @@ import { StorageManagerContext } from '../../api/storageManagerContext';
|
||||
import { requireArray, requireDefined, requireObject } from '../../utils/apiShape';
|
||||
import { backendErrorFrom } from '../../utils/backendError';
|
||||
import { notifyError } from '../../utils/notify';
|
||||
import {
|
||||
setId,
|
||||
setDeliveryEntities,
|
||||
setDate,
|
||||
setDeliveryEntity,
|
||||
setNumber,
|
||||
setExternalNumber,
|
||||
setType,
|
||||
setLineitems,
|
||||
setNotes,
|
||||
addLineitem,
|
||||
updateLineitem,
|
||||
removeLineitem,
|
||||
resetDelivery,
|
||||
selectDeliveryId,
|
||||
selectDeliveryEntities,
|
||||
selectDeliveryDate,
|
||||
selectDeliveryEntity,
|
||||
selectDeliveryNumber,
|
||||
selectDeliveryExternalNumber,
|
||||
selectDeliveryLineitems,
|
||||
selectDeliveryNotes,
|
||||
} from '../../store/deliveryEditorSlice';
|
||||
import { generatePdfThumbnail } from '../../utils/pdfThumbnail';
|
||||
import { extractPdfText } from '../../utils/pdfText';
|
||||
import { deliveryEditorAutomat } from '../../automats/deliveryEditorAutomat';
|
||||
|
||||
class LieferungNeuPage extends PureComponent {
|
||||
static contextType = StorageManagerContext;
|
||||
@@ -56,6 +36,7 @@ class LieferungNeuPage extends PureComponent {
|
||||
super(props);
|
||||
this.cfg = manifestConfig('delivery');
|
||||
this.state = {
|
||||
...deliveryEditorAutomat.state,
|
||||
status: 'loadingParties',
|
||||
error: null,
|
||||
partiesLoading: true,
|
||||
@@ -64,6 +45,8 @@ class LieferungNeuPage extends PureComponent {
|
||||
pdfDialogOpen: false,
|
||||
pdfDialogData: null,
|
||||
pdfFileName: '',
|
||||
textEditorOpen: false,
|
||||
textEditorAttachment: null,
|
||||
};
|
||||
this._cancelled = false;
|
||||
this.handleRetry = this.handleRetry.bind(this);
|
||||
@@ -76,16 +59,22 @@ class LieferungNeuPage extends PureComponent {
|
||||
this.handleRemoveLine = this.handleRemoveLine.bind(this);
|
||||
this.handleSave = this.handleSave.bind(this);
|
||||
this.handlePdfUpload = this.handlePdfUpload.bind(this);
|
||||
this.handlePdfRescan = this.handlePdfRescan.bind(this);
|
||||
this.handlePdfDelete = this.handlePdfDelete.bind(this);
|
||||
this.handleClosePdfDialog = this.handleClosePdfDialog.bind(this);
|
||||
this.handleAttachmentClick = this.handleAttachmentClick.bind(this);
|
||||
this.handleCloseTextEditor = this.handleCloseTextEditor.bind(this);
|
||||
this.handleSaveTextEditor = this.handleSaveTextEditor.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.unsubscribe = deliveryEditorAutomat.subscribe(this);
|
||||
this._cancelled = false;
|
||||
if (this.props.docId != null) {
|
||||
void this.loadEdit();
|
||||
} else {
|
||||
if (this.props.deliveryId != null || this.props.number != null) {
|
||||
this.props.resetDelivery();
|
||||
if (this.state.deliveryId != null || this.state.number != null) {
|
||||
deliveryEditorAutomat.actions.resetDelivery();
|
||||
}
|
||||
void this.loadPartiesOnly();
|
||||
}
|
||||
@@ -96,7 +85,7 @@ class LieferungNeuPage extends PureComponent {
|
||||
if (this.props.docId != null) {
|
||||
void this.loadEdit();
|
||||
} else {
|
||||
this.props.resetDelivery();
|
||||
deliveryEditorAutomat.actions.resetDelivery();
|
||||
void this.loadPartiesOnly();
|
||||
}
|
||||
}
|
||||
@@ -104,6 +93,7 @@ class LieferungNeuPage extends PureComponent {
|
||||
|
||||
componentWillUnmount() {
|
||||
this._cancelled = true;
|
||||
this.unsubscribe?.();
|
||||
}
|
||||
|
||||
async loadPartiesOnly() {
|
||||
@@ -111,7 +101,7 @@ class LieferungNeuPage extends PureComponent {
|
||||
try {
|
||||
const parties = await loadParties('delivery', this.context);
|
||||
if (this._cancelled) return;
|
||||
this.props.setDeliveryEntities(parties);
|
||||
deliveryEditorAutomat.actions.setDeliveryEntities(parties);
|
||||
this.setState({
|
||||
status: 'ready',
|
||||
partiesLoading: false,
|
||||
@@ -146,7 +136,7 @@ class LieferungNeuPage extends PureComponent {
|
||||
]);
|
||||
if (this._cancelled) return;
|
||||
|
||||
this.props.setDeliveryEntities(parties);
|
||||
deliveryEditorAutomat.actions.setDeliveryEntities(parties);
|
||||
|
||||
requireObject(docRes, cfg.getPathLabel);
|
||||
requireObject(docRes[cfg.docField], cfg.docField);
|
||||
@@ -155,12 +145,12 @@ class LieferungNeuPage extends PureComponent {
|
||||
const lines =
|
||||
apiLines.length > 0 ? apiLines.map(lineFromApi) : [emptyLine()];
|
||||
|
||||
this.props.setId(doc.id ?? docId);
|
||||
this.props.setDate(dateInputValue(doc.date));
|
||||
if (doc[cfg.partyField] != null) this.props.setDeliveryEntity(doc[cfg.partyField]);
|
||||
if (doc.number != null) this.props.setNumber(doc.number);
|
||||
if (doc.externalNumber != null) this.props.setExternalNumber(String(doc.externalNumber));
|
||||
this.props.setLineitems(lines);
|
||||
deliveryEditorAutomat.actions.setId(doc.id ?? docId);
|
||||
deliveryEditorAutomat.actions.setDate(dateInputValue(doc.date));
|
||||
if (doc[cfg.partyField] != null) deliveryEditorAutomat.actions.setDeliveryEntity(doc[cfg.partyField]);
|
||||
if (doc.number != null) deliveryEditorAutomat.actions.setNumber(doc.number);
|
||||
if (doc.externalNumber != null) deliveryEditorAutomat.actions.setExternalNumber(String(doc.externalNumber));
|
||||
deliveryEditorAutomat.actions.setLineitems(lines);
|
||||
|
||||
this.setState({
|
||||
status: 'ready',
|
||||
@@ -182,24 +172,24 @@ class LieferungNeuPage extends PureComponent {
|
||||
}
|
||||
|
||||
handlePartyChange(partyId) {
|
||||
this.props.setDeliveryEntity(partyId);
|
||||
deliveryEditorAutomat.actions.setDeliveryEntity(partyId);
|
||||
}
|
||||
|
||||
handleDateChange(date) {
|
||||
this.props.setDate(date);
|
||||
deliveryEditorAutomat.actions.setDate(date);
|
||||
}
|
||||
|
||||
handleNumberChange(number) {
|
||||
this.props.setNumber(number);
|
||||
deliveryEditorAutomat.actions.setNumber(number);
|
||||
}
|
||||
|
||||
handleExternalNumberChange(externalNumber) {
|
||||
this.props.setExternalNumber(externalNumber);
|
||||
deliveryEditorAutomat.actions.setExternalNumber(externalNumber);
|
||||
}
|
||||
|
||||
findLineIndex(key) {
|
||||
if (typeof key === 'number' && !Number.isNaN(key)) return key;
|
||||
const lines = this.props.lines;
|
||||
const lines = this.state.lines;
|
||||
if (Array.isArray(lines)) {
|
||||
const idx = lines.findIndex((l) => l.key === key);
|
||||
if (idx !== -1) return idx;
|
||||
@@ -210,7 +200,7 @@ class LieferungNeuPage extends PureComponent {
|
||||
|
||||
handleLineChange(key, field, value) {
|
||||
const index = this.findLineIndex(key);
|
||||
this.props.updateLineitem({
|
||||
deliveryEditorAutomat.actions.updateLineitem({
|
||||
index,
|
||||
key,
|
||||
[field]: value,
|
||||
@@ -220,16 +210,105 @@ class LieferungNeuPage extends PureComponent {
|
||||
}
|
||||
|
||||
handleAddLine() {
|
||||
this.props.addLineitem();
|
||||
deliveryEditorAutomat.actions.addLineitem();
|
||||
}
|
||||
|
||||
handleRemoveLine(key) {
|
||||
const index = this.findLineIndex(key);
|
||||
this.props.removeLineitem(index >= 0 ? index : key);
|
||||
deliveryEditorAutomat.actions.removeLineitem(index >= 0 ? index : key);
|
||||
}
|
||||
|
||||
async handlePdfUpload(file) {
|
||||
if (!file || this.state.pdfUploading) return;
|
||||
|
||||
// 1. Generate thumbnail for PDF
|
||||
let thumbnail = null;
|
||||
try {
|
||||
thumbnail = await generatePdfThumbnail(file);
|
||||
} catch (err) {
|
||||
console.error('Fehler beim Erstellen des PDF-Thumbnails:', err);
|
||||
}
|
||||
|
||||
const pdfId = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
||||
const pdfAttachment = {
|
||||
id: pdfId,
|
||||
file,
|
||||
name: file.name,
|
||||
size: file.size,
|
||||
type: 'application/pdf',
|
||||
thumbnail,
|
||||
uploadedAt: new Date().toISOString(),
|
||||
};
|
||||
deliveryEditorAutomat.actions.addAttachment(pdfAttachment);
|
||||
|
||||
// 2. Extract text using pdftotext-wasm to create 2nd file (textfile)
|
||||
try {
|
||||
const text = await extractPdfText(file, { layout: true });
|
||||
const txtName = file.name.replace(/\.pdf$/i, '') + '.txt';
|
||||
const txtFile = new File([text], txtName, { type: 'text/plain;charset=utf-8' });
|
||||
const txtAttachment = {
|
||||
id: `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
||||
file: txtFile,
|
||||
name: txtFile.name,
|
||||
size: txtFile.size,
|
||||
type: 'text/plain',
|
||||
text,
|
||||
parentPdfId: pdfId,
|
||||
uploadedAt: new Date().toISOString(),
|
||||
};
|
||||
deliveryEditorAutomat.actions.addAttachment(txtAttachment);
|
||||
} catch (txtErr) {
|
||||
console.error('Fehler beim Extrahieren von Text mit pdftotext-wasm:', txtErr);
|
||||
}
|
||||
|
||||
// 3. Run SSE parsing with backend
|
||||
await this.runPdfParse(file);
|
||||
}
|
||||
|
||||
async handlePdfRescan(attachment) {
|
||||
if (!attachment?.file || this.state.pdfUploading) return;
|
||||
|
||||
// Refresh textfile upon rescan
|
||||
try {
|
||||
const text = await extractPdfText(attachment.file, { layout: true });
|
||||
const txtName = attachment.name.replace(/\.pdf$/i, '') + '.txt';
|
||||
const txtFile = new File([text], txtName, { type: 'text/plain;charset=utf-8' });
|
||||
|
||||
const existingTxt = this.state.attachments?.find(
|
||||
(a) => a.parentPdfId === attachment.id || a.name === txtName,
|
||||
);
|
||||
if (existingTxt) {
|
||||
deliveryEditorAutomat.actions.updateAttachment({
|
||||
id: existingTxt.id,
|
||||
file: txtFile,
|
||||
size: txtFile.size,
|
||||
text,
|
||||
});
|
||||
} else {
|
||||
deliveryEditorAutomat.actions.addAttachment({
|
||||
id: `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
||||
file: txtFile,
|
||||
name: txtFile.name,
|
||||
size: txtFile.size,
|
||||
type: 'text/plain',
|
||||
text,
|
||||
parentPdfId: attachment.id,
|
||||
uploadedAt: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
} catch (txtErr) {
|
||||
console.error('Fehler beim Aktualisieren der Textdatei:', txtErr);
|
||||
}
|
||||
|
||||
await this.runPdfParse(attachment.file);
|
||||
}
|
||||
|
||||
handlePdfDelete(attachment) {
|
||||
if (!attachment?.id) return;
|
||||
deliveryEditorAutomat.actions.removeAttachment(attachment.id);
|
||||
}
|
||||
|
||||
async runPdfParse(file) {
|
||||
this.setState({
|
||||
pdfUploading: true,
|
||||
pdfDialogOpen: true,
|
||||
@@ -311,9 +390,9 @@ class LieferungNeuPage extends PureComponent {
|
||||
const notes = Array.isArray(result?.notes) ? result.notes : [];
|
||||
|
||||
// If supplierId is null, try matching supplier by name from loaded delivery entities
|
||||
if (supplierId == null && result?.basedata?.lieferantName && Array.isArray(this.props.deliveryEntities)) {
|
||||
if (supplierId == null && result?.basedata?.lieferantName && Array.isArray(this.state.deliveryEntities)) {
|
||||
const nameToMatch = result.basedata.lieferantName.trim().toLowerCase();
|
||||
const matched = this.props.deliveryEntities.find(
|
||||
const matched = this.state.deliveryEntities.find(
|
||||
(e) =>
|
||||
e.name &&
|
||||
(e.name.trim().toLowerCase() === nameToMatch ||
|
||||
@@ -323,17 +402,17 @@ class LieferungNeuPage extends PureComponent {
|
||||
if (matched) supplierId = matched.id;
|
||||
}
|
||||
|
||||
if (docDate) this.props.setDate(docDate);
|
||||
if (supplierId != null) this.props.setDeliveryEntity(supplierId);
|
||||
if (docDate) deliveryEditorAutomat.actions.setDate(docDate);
|
||||
if (supplierId != null) deliveryEditorAutomat.actions.setDeliveryEntity(supplierId);
|
||||
if (extNum != null) {
|
||||
this.props.setExternalNumber(String(extNum));
|
||||
deliveryEditorAutomat.actions.setExternalNumber(String(extNum));
|
||||
}
|
||||
if (docType) this.props.setType(docType);
|
||||
if (docType) deliveryEditorAutomat.actions.setType(docType);
|
||||
if (newLines && newLines.length > 0) {
|
||||
this.props.setLineitems(newLines);
|
||||
deliveryEditorAutomat.actions.setLineitems(newLines);
|
||||
}
|
||||
if (notes.length > 0) {
|
||||
this.props.setNotes(notes);
|
||||
deliveryEditorAutomat.actions.setNotes(notes);
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -375,6 +454,52 @@ class LieferungNeuPage extends PureComponent {
|
||||
this.setState({ pdfDialogOpen: false });
|
||||
}
|
||||
|
||||
handleAttachmentClick(attachment) {
|
||||
if (!attachment) return;
|
||||
const isText =
|
||||
attachment.type?.startsWith('text/') ||
|
||||
attachment.name?.toLowerCase().endsWith('.txt') ||
|
||||
Boolean(attachment.text);
|
||||
|
||||
if (isText) {
|
||||
this.setState({
|
||||
textEditorOpen: true,
|
||||
textEditorAttachment: attachment,
|
||||
});
|
||||
} else if (attachment.file instanceof Blob || attachment.file instanceof File) {
|
||||
const url = URL.createObjectURL(attachment.file);
|
||||
window.open(url, '_blank');
|
||||
}
|
||||
}
|
||||
|
||||
handleCloseTextEditor() {
|
||||
this.setState({
|
||||
textEditorOpen: false,
|
||||
textEditorAttachment: null,
|
||||
});
|
||||
}
|
||||
|
||||
handleSaveTextEditor(newText) {
|
||||
const attachment = this.state.textEditorAttachment;
|
||||
if (!attachment) return;
|
||||
|
||||
const newFile = new File([newText], attachment.name, {
|
||||
type: 'text/plain;charset=utf-8',
|
||||
});
|
||||
|
||||
deliveryEditorAutomat.actions.updateAttachment({
|
||||
id: attachment.id,
|
||||
file: newFile,
|
||||
size: newFile.size,
|
||||
text: newText,
|
||||
});
|
||||
|
||||
this.setState({
|
||||
textEditorOpen: false,
|
||||
textEditorAttachment: null,
|
||||
});
|
||||
}
|
||||
|
||||
async handleSave() {
|
||||
const { docId } = this.props;
|
||||
const cfg = this.cfg;
|
||||
@@ -385,11 +510,11 @@ class LieferungNeuPage extends PureComponent {
|
||||
try {
|
||||
body = buildManifestBody(
|
||||
{
|
||||
number: this.props.number ?? '',
|
||||
externalNumber: this.props.externalNumber ?? '',
|
||||
date: this.props.date ?? '',
|
||||
partyId: this.props.partyId,
|
||||
lines: this.props.lines,
|
||||
number: this.state.number ?? '',
|
||||
externalNumber: this.state.externalNumber ?? '',
|
||||
date: this.state.date ?? '',
|
||||
partyId: this.state.partyId,
|
||||
lines: this.state.lines,
|
||||
},
|
||||
{
|
||||
partyField: cfg.partyField,
|
||||
@@ -412,7 +537,7 @@ class LieferungNeuPage extends PureComponent {
|
||||
await this.context[cfg.updateCache]();
|
||||
if (this._cancelled) return;
|
||||
enqueueSnackbar(cfg.createdMsg, { variant: 'success' });
|
||||
this.props.resetDelivery();
|
||||
deliveryEditorAutomat.actions.resetDelivery();
|
||||
this.props.navigate(cfg.editPath(data[cfg.docField].id));
|
||||
} else {
|
||||
const data = await this.context[cfg.api].update(docId, body);
|
||||
@@ -425,12 +550,12 @@ class LieferungNeuPage extends PureComponent {
|
||||
const apiLines = requireArray(doc.lines, `${cfg.docField}.lines`);
|
||||
const lines = apiLines.length > 0 ? apiLines.map(lineFromApi) : [emptyLine()];
|
||||
|
||||
this.props.setId(doc.id ?? docId);
|
||||
this.props.setDate(dateInputValue(doc.date));
|
||||
if (doc[cfg.partyField] != null) this.props.setDeliveryEntity(doc[cfg.partyField]);
|
||||
if (doc.number != null) this.props.setNumber(doc.number);
|
||||
if (doc.externalNumber != null) this.props.setExternalNumber(String(doc.externalNumber));
|
||||
this.props.setLineitems(lines);
|
||||
deliveryEditorAutomat.actions.setId(doc.id ?? docId);
|
||||
deliveryEditorAutomat.actions.setDate(dateInputValue(doc.date));
|
||||
if (doc[cfg.partyField] != null) deliveryEditorAutomat.actions.setDeliveryEntity(doc[cfg.partyField]);
|
||||
if (doc.number != null) deliveryEditorAutomat.actions.setNumber(doc.number);
|
||||
if (doc.externalNumber != null) deliveryEditorAutomat.actions.setExternalNumber(String(doc.externalNumber));
|
||||
deliveryEditorAutomat.actions.setLineitems(lines);
|
||||
|
||||
enqueueSnackbar(cfg.savedMsg, { variant: 'success' });
|
||||
this.setState({ saving: false });
|
||||
@@ -453,8 +578,6 @@ class LieferungNeuPage extends PureComponent {
|
||||
pdfDialogOpen,
|
||||
pdfDialogData,
|
||||
pdfFileName,
|
||||
} = this.state;
|
||||
const {
|
||||
deliveryEntities,
|
||||
date,
|
||||
partyId,
|
||||
@@ -462,7 +585,8 @@ class LieferungNeuPage extends PureComponent {
|
||||
externalNumber,
|
||||
lines,
|
||||
notes,
|
||||
} = this.props;
|
||||
attachments,
|
||||
} = this.state;
|
||||
|
||||
if (status === 'loadingParties' || status === 'loading') {
|
||||
return (
|
||||
@@ -492,7 +616,11 @@ class LieferungNeuPage extends PureComponent {
|
||||
externalNumber={externalNumber ?? ''}
|
||||
saving={saving}
|
||||
pdfUploading={pdfUploading}
|
||||
attachments={attachments}
|
||||
onPdfUpload={this.handlePdfUpload}
|
||||
onPdfRescan={this.handlePdfRescan}
|
||||
onPdfDelete={this.handlePdfDelete}
|
||||
onAttachmentClick={this.handleAttachmentClick}
|
||||
onPartyChange={this.handlePartyChange}
|
||||
onDateChange={this.handleDateChange}
|
||||
onNumberChange={this.handleNumberChange}
|
||||
@@ -546,39 +674,15 @@ class LieferungNeuPage extends PureComponent {
|
||||
data={pdfDialogData}
|
||||
onClose={this.handleClosePdfDialog}
|
||||
/>
|
||||
<TextEditorDialog
|
||||
open={this.state.textEditorOpen}
|
||||
attachment={this.state.textEditorAttachment}
|
||||
onClose={this.handleCloseTextEditor}
|
||||
onSave={this.handleSaveTextEditor}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = (state) => ({
|
||||
deliveryId: selectDeliveryId(state),
|
||||
deliveryEntities: selectDeliveryEntities(state),
|
||||
date: selectDeliveryDate(state),
|
||||
partyId: selectDeliveryEntity(state),
|
||||
number: selectDeliveryNumber(state),
|
||||
externalNumber: selectDeliveryExternalNumber(state),
|
||||
lines: selectDeliveryLineitems(state),
|
||||
notes: selectDeliveryNotes(state),
|
||||
});
|
||||
|
||||
const mapDispatchToProps = {
|
||||
setId,
|
||||
setDeliveryEntities,
|
||||
setDate,
|
||||
setDeliveryEntity,
|
||||
setNumber,
|
||||
setExternalNumber,
|
||||
setType,
|
||||
setLineitems,
|
||||
setNotes,
|
||||
addLineitem,
|
||||
updateLineitem,
|
||||
removeLineitem,
|
||||
resetDelivery,
|
||||
};
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps,
|
||||
)(withNavigate(LieferungNeuPage));
|
||||
export default withNavigate(LieferungNeuPage);
|
||||
|
||||
Reference in New Issue
Block a user