diff --git a/package-lock.json b/package-lock.json
index 9d7c407..a123c4a 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -16,8 +16,11 @@
"@mui/material": "^9.4.0",
"@reduxjs/toolkit": "^2.12.0",
"@shopify/draggable": "^1.2.1",
+ "automat": "file:../automat",
"dashgui": "file:../dashgui",
"notistack": "^3.0.2",
+ "pdfjs-dist": "^6.3.289",
+ "pdftotext-wasm": "file:../pdftotext-wasm",
"react": "^19.3.0",
"react-dom": "^19.3.0",
"react-redux": "^9.3.0",
@@ -32,6 +35,17 @@
"vite-plugin-pwa": "^1.3.0"
}
},
+ "../automat": {
+ "version": "0.1.0",
+ "dependencies": {
+ "react": "^19.3.0",
+ "react-dom": "^19.3.0"
+ },
+ "devDependencies": {
+ "@vitejs/plugin-react": "^6.1.1",
+ "vite": "^8.3.0"
+ }
+ },
"../dashgui": {
"version": "0.1.0",
"devDependencies": {
@@ -54,6 +68,13 @@
"react-dom": "^19.0.0"
}
},
+ "../pdftotext-wasm": {
+ "version": "0.1.0",
+ "license": "GPLv3",
+ "bin": {
+ "pdftotext-wasm": "bin.js"
+ }
+ },
"node_modules/@apideck/better-ajv-errors": {
"version": "0.3.7",
"resolved": "https://registry.npmjs.org/@apideck/better-ajv-errors/-/better-ajv-errors-0.3.7.tgz",
@@ -2070,6 +2091,271 @@
}
}
},
+ "node_modules/@napi-rs/canvas": {
+ "version": "1.0.9",
+ "resolved": "https://registry.npmjs.org/@napi-rs/canvas/-/canvas-1.0.9.tgz",
+ "integrity": "sha512-QviPdJImDi/jMAvBfqaw+19BndMd/sizXVW3NnpMd3VJGz++QXkOHcP9kWR/smHG0hNjHeyuHFyrx/5lD0oNcQ==",
+ "license": "MIT",
+ "optional": true,
+ "workspaces": [
+ "e2e/*"
+ ],
+ "engines": {
+ "node": ">= 10"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/Brooooooklyn"
+ },
+ "optionalDependencies": {
+ "@napi-rs/canvas-android-arm64": "1.0.9",
+ "@napi-rs/canvas-darwin-arm64": "1.0.9",
+ "@napi-rs/canvas-darwin-x64": "1.0.9",
+ "@napi-rs/canvas-linux-arm-gnueabihf": "1.0.9",
+ "@napi-rs/canvas-linux-arm64-gnu": "1.0.9",
+ "@napi-rs/canvas-linux-arm64-musl": "1.0.9",
+ "@napi-rs/canvas-linux-riscv64-gnu": "1.0.9",
+ "@napi-rs/canvas-linux-x64-gnu": "1.0.9",
+ "@napi-rs/canvas-linux-x64-musl": "1.0.9",
+ "@napi-rs/canvas-win32-arm64-msvc": "1.0.9",
+ "@napi-rs/canvas-win32-x64-msvc": "1.0.9"
+ }
+ },
+ "node_modules/@napi-rs/canvas-android-arm64": {
+ "version": "1.0.9",
+ "resolved": "https://registry.npmjs.org/@napi-rs/canvas-android-arm64/-/canvas-android-arm64-1.0.9.tgz",
+ "integrity": "sha512-4LGXk2/0HVzE29K8SzML5WubgCp++B1FH3qgl35XmSZE+lLdr6P9VRQEnZ0MCLZMTSuJP41yyhtoiVNEuvrTIA==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">= 10"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/Brooooooklyn"
+ }
+ },
+ "node_modules/@napi-rs/canvas-darwin-arm64": {
+ "version": "1.0.9",
+ "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-arm64/-/canvas-darwin-arm64-1.0.9.tgz",
+ "integrity": "sha512-YNdfLBzY0W/Pep9fo2L6RmoNlNksnn05LRnX66W63R3ij58S25QOTcjdtEt2v8+PnCESzqZsYzUo+QPeIR44NA==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 10"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/Brooooooklyn"
+ }
+ },
+ "node_modules/@napi-rs/canvas-darwin-x64": {
+ "version": "1.0.9",
+ "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-x64/-/canvas-darwin-x64-1.0.9.tgz",
+ "integrity": "sha512-ceZQSknTEcy3dOXoekv59LTCkXjvnLsq+VW5PeNNDHEPQbRS5Ervkm1EaDa7WLAjiYWMLuSQTRTHao1dEX4prg==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 10"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/Brooooooklyn"
+ }
+ },
+ "node_modules/@napi-rs/canvas-linux-arm-gnueabihf": {
+ "version": "1.0.9",
+ "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm-gnueabihf/-/canvas-linux-arm-gnueabihf-1.0.9.tgz",
+ "integrity": "sha512-XhfI0Wwv4llhd6nnWDtY3kQKjq0r+y1i91PlJlJI24ag2U9WrnwbG1qS3+fDLEyouwEVFchiKkTEHozK+5iUNA==",
+ "cpu": [
+ "arm"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/Brooooooklyn"
+ }
+ },
+ "node_modules/@napi-rs/canvas-linux-arm64-gnu": {
+ "version": "1.0.9",
+ "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-gnu/-/canvas-linux-arm64-gnu-1.0.9.tgz",
+ "integrity": "sha512-012oiYtKaE7i9oxc8q7nraT7kDOpLcaCmFLzVe9Ty34RHDdoDzbWLrVh827CNxYh/EADX1eSikA3ymLjo/nNuw==",
+ "cpu": [
+ "arm64"
+ ],
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/Brooooooklyn"
+ }
+ },
+ "node_modules/@napi-rs/canvas-linux-arm64-musl": {
+ "version": "1.0.9",
+ "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-musl/-/canvas-linux-arm64-musl-1.0.9.tgz",
+ "integrity": "sha512-Ls5UWYFFn63casTZEczbeyEg3vRDRkv9lscuGwfchtY5yLLQhgOB8SN4YGxmfJ5vTaBwZ2YUxBS3NtmjJmFXdA==",
+ "cpu": [
+ "arm64"
+ ],
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/Brooooooklyn"
+ }
+ },
+ "node_modules/@napi-rs/canvas-linux-riscv64-gnu": {
+ "version": "1.0.9",
+ "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-riscv64-gnu/-/canvas-linux-riscv64-gnu-1.0.9.tgz",
+ "integrity": "sha512-hLKEGxV7ZiRHqndePTokgDMdBlo/rDfzg7P4p4QIv9pUhuYobnu3R2NIFLCRghG0nwfo+s2sw+c1xZFeCmEAsw==",
+ "cpu": [
+ "riscv64"
+ ],
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/Brooooooklyn"
+ }
+ },
+ "node_modules/@napi-rs/canvas-linux-x64-gnu": {
+ "version": "1.0.9",
+ "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-gnu/-/canvas-linux-x64-gnu-1.0.9.tgz",
+ "integrity": "sha512-6kaz3w0QMy77PDWk6rJ1ksIihdad3qzEyX2o2oGT8GwCaypfT5mhjr8buOO5hstyLxcWXDScuz56RsINLtBPIQ==",
+ "cpu": [
+ "x64"
+ ],
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/Brooooooklyn"
+ }
+ },
+ "node_modules/@napi-rs/canvas-linux-x64-musl": {
+ "version": "1.0.9",
+ "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-musl/-/canvas-linux-x64-musl-1.0.9.tgz",
+ "integrity": "sha512-xrGvmS3v55hmZ86ls/kBLVNMUTYio3f6Ik0DireemG994VfPAwiA3ZXA0Uf1bByctkB3NQ1Sfb+H5bkdUnnzfQ==",
+ "cpu": [
+ "x64"
+ ],
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/Brooooooklyn"
+ }
+ },
+ "node_modules/@napi-rs/canvas-win32-arm64-msvc": {
+ "version": "1.0.9",
+ "resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-arm64-msvc/-/canvas-win32-arm64-msvc-1.0.9.tgz",
+ "integrity": "sha512-yjmVS3ArZeRVCP7jqbPq4rpZa/BhTeI7ELE2XqJg3snICQBDevLZyArxswHkiTnT34KRic33/4fLirrHI+SY8A==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 10"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/Brooooooklyn"
+ }
+ },
+ "node_modules/@napi-rs/canvas-win32-x64-msvc": {
+ "version": "1.0.9",
+ "resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-x64-msvc/-/canvas-win32-x64-msvc-1.0.9.tgz",
+ "integrity": "sha512-QlSYQdMQslB81nlABo9wNfQ6npFhE7/O+saCZdqVGueGanyRk4jCogD5EwQenfP3kIq9e+mm6GreQBjX5MrA8g==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 10"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/Brooooooklyn"
+ }
+ },
"node_modules/@napi-rs/lzma-linux-x64-gnu": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz",
@@ -3169,6 +3455,10 @@
"node": ">= 4.0.0"
}
},
+ "node_modules/automat": {
+ "resolved": "../automat",
+ "link": true
+ },
"node_modules/available-typed-arrays": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz",
@@ -5705,6 +5995,22 @@
"node": ">=8"
}
},
+ "node_modules/pdfjs-dist": {
+ "version": "6.3.289",
+ "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-6.3.289.tgz",
+ "integrity": "sha512-ZHjSVpDa3D6izMq8/04lvkhkATUmL9px6ChPaXc1k6nU2Mrhlg1/7F0bdUqCwUjw3NsPTfPZsMDUU6ZIcRaeQw==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=22.13.0 || >=24"
+ },
+ "optionalDependencies": {
+ "@napi-rs/canvas": "^1.0.0"
+ }
+ },
+ "node_modules/pdftotext-wasm": {
+ "resolved": "../pdftotext-wasm",
+ "link": true
+ },
"node_modules/picocolors": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
diff --git a/package.json b/package.json
index 0508c1f..4cf4899 100644
--- a/package.json
+++ b/package.json
@@ -22,8 +22,11 @@
"@mui/material": "^9.4.0",
"@reduxjs/toolkit": "^2.12.0",
"@shopify/draggable": "^1.2.1",
- "notistack": "^3.0.2",
+ "automat": "file:../automat",
"dashgui": "file:../dashgui",
+ "notistack": "^3.0.2",
+ "pdfjs-dist": "^6.3.289",
+ "pdftotext-wasm": "file:../pdftotext-wasm",
"react": "^19.3.0",
"react-dom": "^19.3.0",
"react-redux": "^9.3.0",
diff --git a/src/automats/deliveryEditorAutomat.js b/src/automats/deliveryEditorAutomat.js
new file mode 100644
index 0000000..3bbce4a
--- /dev/null
+++ b/src/automats/deliveryEditorAutomat.js
@@ -0,0 +1,257 @@
+import { Automat } from 'automat';
+
+export function createEmptyLineitem(line = {}) {
+ return {
+ key: line.key ?? `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
+ name: line.name ?? null,
+ sku: line.sku ?? null,
+ amount: line.amount ?? null,
+ pu: line.pu ?? null,
+ unit: line.unit ?? null,
+ netprice: line.netprice ?? null,
+ taxrate: line.taxrate ?? null,
+ };
+}
+
+export const initialDelivery = {
+ id: null,
+ deliveryId: null,
+ date: null,
+ deliveryEntity: null,
+ partyId: null,
+ externalNumber: null,
+ external_number: null,
+ number: null,
+ type: null,
+ lines: [],
+ lineitems: [],
+ notes: [],
+ attachments: [],
+ files: [],
+};
+
+export const initialState = {
+ deliveryEntities: [],
+ ...initialDelivery,
+};
+
+export const deliveryEditorAutomat = new Automat(initialState, {
+ setId(id) {
+ deliveryEditorAutomat.setState({ id, deliveryId: id });
+ },
+
+ setDeliveryEntities(deliveryEntities) {
+ deliveryEditorAutomat.setState({ deliveryEntities: deliveryEntities ?? [] });
+ },
+
+ setDelivery(delivery = {}) {
+ const rawLines = delivery.lines ?? delivery.lineitems ?? [];
+ const lines = Array.isArray(rawLines) ? rawLines : [];
+ const partyId = delivery.partyId ?? delivery.deliveryEntity ?? null;
+ const extNum = delivery.externalNumber ?? delivery.external_number ?? null;
+ const id = delivery.id ?? delivery.deliveryId ?? null;
+ const notes = Array.isArray(delivery.notes)
+ ? delivery.notes
+ : delivery.notes ? [delivery.notes] : [];
+ const rawAttachments = delivery.attachments ?? delivery.files ?? [];
+ const attachments = Array.isArray(rawAttachments) ? rawAttachments : [];
+
+ deliveryEditorAutomat.setState({
+ ...initialDelivery,
+ ...delivery,
+ id,
+ deliveryId: id,
+ partyId,
+ deliveryEntity: partyId,
+ externalNumber: extNum,
+ external_number: extNum,
+ lines,
+ lineitems: lines,
+ notes,
+ attachments,
+ files: attachments,
+ });
+ },
+
+ setDate(date) {
+ deliveryEditorAutomat.setState({ date });
+ },
+
+ setDeliveryEntity(entityId) {
+ deliveryEditorAutomat.setState({ deliveryEntity: entityId, partyId: entityId });
+ },
+
+ setPartyId(partyId) {
+ deliveryEditorAutomat.setState({ deliveryEntity: partyId, partyId });
+ },
+
+ setExternalNumber(externalNumber) {
+ deliveryEditorAutomat.setState({
+ externalNumber,
+ external_number: externalNumber,
+ });
+ },
+
+ setNumber(number) {
+ deliveryEditorAutomat.setState({ number });
+ },
+
+ setType(type) {
+ deliveryEditorAutomat.setState({ type });
+ },
+
+ setLines(lines) {
+ const cleanLines = lines ?? [];
+ deliveryEditorAutomat.setState({ lines: cleanLines, lineitems: cleanLines });
+ },
+
+ setLineitems(lines) {
+ const cleanLines = lines ?? [];
+ deliveryEditorAutomat.setState({ lines: cleanLines, lineitems: cleanLines });
+ },
+
+ setNotes(notes) {
+ const cleanNotes = Array.isArray(notes)
+ ? notes
+ : notes ? [notes] : [];
+ deliveryEditorAutomat.setState({ notes: cleanNotes });
+ },
+
+ addLineitem(line = {}) {
+ const item = createEmptyLineitem(line);
+ const nextLines = [...(deliveryEditorAutomat.state.lines ?? []), item];
+ deliveryEditorAutomat.setState({ lines: nextLines, lineitems: nextLines });
+ },
+
+ addLine(line = {}) {
+ deliveryEditorAutomat.actions.addLineitem(line);
+ },
+
+ updateLineitem(payload) {
+ const { index, key, ...fields } = payload || {};
+ const currentLines = deliveryEditorAutomat.state.lines ?? [];
+ let targetIndex = typeof index === 'number' && !Number.isNaN(index) ? index : -1;
+ if (targetIndex === -1 && key != null) {
+ targetIndex = currentLines.findIndex((l) => l.key === key);
+ }
+ if (targetIndex >= 0 && targetIndex < currentLines.length) {
+ const nextLines = [...currentLines];
+ nextLines[targetIndex] = {
+ ...nextLines[targetIndex],
+ ...fields,
+ };
+ deliveryEditorAutomat.setState({ lines: nextLines, lineitems: nextLines });
+ }
+ },
+
+ updateLine(payload) {
+ deliveryEditorAutomat.actions.updateLineitem(payload);
+ },
+
+ removeLineitem(payload) {
+ const currentLines = deliveryEditorAutomat.state.lines ?? [];
+ let index = typeof payload === 'number' && !Number.isNaN(payload) ? payload : -1;
+ if (index === -1 && payload != null) {
+ const key = typeof payload === 'string' ? payload : payload?.key;
+ if (key != null) {
+ index = currentLines.findIndex((l) => l.key === key);
+ }
+ }
+ if (index >= 0 && index < currentLines.length) {
+ const nextLines = currentLines.filter((_, i) => i !== index);
+ deliveryEditorAutomat.setState({ lines: nextLines, lineitems: nextLines });
+ }
+ },
+
+ removeLine(payload) {
+ deliveryEditorAutomat.actions.removeLineitem(payload);
+ },
+
+ addAttachment(attachment) {
+ const id = attachment.id ?? `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
+ const item = { ...attachment, id };
+ const next = [...(deliveryEditorAutomat.state.attachments ?? []), item];
+ deliveryEditorAutomat.setState({ attachments: next, files: next });
+ },
+
+ setAttachments(attachments) {
+ const clean = Array.isArray(attachments) ? attachments : [];
+ deliveryEditorAutomat.setState({ attachments: clean, files: clean });
+ },
+
+ updateAttachment(payload) {
+ const { id, ...fields } = payload || {};
+ const current = deliveryEditorAutomat.state.attachments ?? [];
+ const idx = current.findIndex((a) => a.id === id);
+ if (idx !== -1) {
+ const next = [...current];
+ next[idx] = { ...next[idx], ...fields };
+ deliveryEditorAutomat.setState({ attachments: next, files: next });
+ }
+ },
+
+ removeAttachment(idOrIndex) {
+ const current = deliveryEditorAutomat.state.attachments ?? [];
+ let next;
+ if (typeof idOrIndex === 'number') {
+ next = current.filter((_, idx) => idx !== idOrIndex);
+ } else {
+ const id = typeof idOrIndex === 'string' ? idOrIndex : idOrIndex?.id;
+ next = current.filter((a) => a.id !== id);
+ }
+ deliveryEditorAutomat.setState({ attachments: next, files: next });
+ },
+
+ resetDelivery() {
+ deliveryEditorAutomat.setState({
+ ...initialDelivery,
+ });
+ },
+
+ resetDeliveryEditor() {
+ deliveryEditorAutomat.setState({
+ ...initialState,
+ });
+ },
+});
+
+export const {
+ setId,
+ setDeliveryEntities,
+ setDelivery,
+ setDate,
+ setDeliveryEntity,
+ setPartyId,
+ setExternalNumber,
+ setNumber,
+ setType,
+ setLines,
+ setLineitems,
+ setNotes,
+ addLineitem,
+ addLine,
+ updateLineitem,
+ updateLine,
+ removeLineitem,
+ removeLine,
+ addAttachment,
+ setAttachments,
+ updateAttachment,
+ removeAttachment,
+ resetDelivery,
+ resetDeliveryEditor,
+} = deliveryEditorAutomat.actions;
+
+// Selectors
+export const selectDeliveryId = (state = deliveryEditorAutomat.state) => state.deliveryId ?? state.id;
+export const selectDeliveryEntities = (state = deliveryEditorAutomat.state) => state.deliveryEntities;
+export const selectDeliveryDate = (state = deliveryEditorAutomat.state) => state.date;
+export const selectDeliveryEntity = (state = deliveryEditorAutomat.state) => state.deliveryEntity ?? state.partyId;
+export const selectDeliveryExternalNumber = (state = deliveryEditorAutomat.state) => state.externalNumber ?? state.external_number;
+export const selectDeliveryNumber = (state = deliveryEditorAutomat.state) => state.number;
+export const selectDeliveryType = (state = deliveryEditorAutomat.state) => state.type;
+export const selectDeliveryLineitems = (state = deliveryEditorAutomat.state) => state.lines ?? state.lineitems;
+export const selectDeliveryNotes = (state = deliveryEditorAutomat.state) => state.notes;
+export const selectDeliveryAttachments = (state = deliveryEditorAutomat.state) => state.attachments;
+
+export default deliveryEditorAutomat;
diff --git a/src/automats/index.js b/src/automats/index.js
new file mode 100644
index 0000000..a606173
--- /dev/null
+++ b/src/automats/index.js
@@ -0,0 +1 @@
+export * from './deliveryEditorAutomat';
diff --git a/src/components/manifest/Attachment.jsx b/src/components/manifest/Attachment.jsx
new file mode 100644
index 0000000..f93e599
--- /dev/null
+++ b/src/components/manifest/Attachment.jsx
@@ -0,0 +1,243 @@
+import { PureComponent } from 'react';
+import Box from '@mui/material/Box';
+import IconButton from '@mui/material/IconButton';
+import Tooltip from '@mui/material/Tooltip';
+import Typography from '@mui/material/Typography';
+import TextSnippetOutlinedIcon from '@mui/icons-material/TextSnippetOutlined';
+import PictureAsPdfIcon from '@mui/icons-material/PictureAsPdf';
+import SyncIcon from '@mui/icons-material/Sync';
+import DeleteOutlineOutlinedIcon from '@mui/icons-material/DeleteOutlineOutlined';
+import { tileBaseSx, TILE_SIZE } from 'dashgui/tiles';
+
+export class Attachment extends PureComponent {
+ handleRescan = (e) => {
+ e?.stopPropagation?.();
+ const { attachment, onRescan } = this.props;
+ if (attachment && onRescan) {
+ onRescan(attachment);
+ }
+ };
+
+ handleDelete = (e) => {
+ e?.stopPropagation?.();
+ const { attachment, onDelete } = this.props;
+ if (attachment && onDelete) {
+ onDelete(attachment);
+ }
+ };
+
+ handleTileClick = (e) => {
+ const { attachment, onClick } = this.props;
+ if (onClick && attachment) {
+ onClick(attachment);
+ } else if (attachment?.file instanceof Blob || attachment?.file instanceof File) {
+ const url = URL.createObjectURL(attachment.file);
+ window.open(url, '_blank');
+ }
+ };
+
+ formatFileSize(bytes) {
+ if (bytes == null || Number.isNaN(bytes)) return '';
+ if (bytes < 1024) return `${bytes} B`;
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
+ }
+
+ render() {
+ const { attachment, disabled, sx } = this.props;
+ const thumbnail = attachment?.thumbnail;
+ const name = attachment?.name || 'Dokument';
+ const size = attachment?.size;
+ const isPdf = attachment?.type === 'application/pdf' || name.toLowerCase().endsWith('.pdf');
+ const isText = attachment?.type?.startsWith('text/') || name.toLowerCase().endsWith('.txt');
+
+ return (
+
+ {/* Thumbnail, Text preview, or Fallback Icon */}
+
+ {thumbnail ? (
+
+ ) : isText ? (
+
+
+
+
+ TXT
+
+
+
+ {attachment?.text ? attachment.text.slice(0, 300) : 'Textdatei'}
+
+
+ ) : (
+
+ )}
+
+
+ {/* Action buttons: Rescan (for PDF) and Delete */}
+ e.stopPropagation()}
+ >
+ {isPdf ? (
+
+
+
+
+
+
+
+ ) : null}
+
+
+
+
+
+
+
+
+
+ {/* Bottom file metadata overlay */}
+
+
+ {name}
+
+ {size ? (
+
+ {this.formatFileSize(size)}
+
+ ) : null}
+
+
+ );
+ }
+}
+
+export default Attachment;
diff --git a/src/components/manifest/AttachmentList.jsx b/src/components/manifest/AttachmentList.jsx
new file mode 100644
index 0000000..246b9f1
--- /dev/null
+++ b/src/components/manifest/AttachmentList.jsx
@@ -0,0 +1,41 @@
+import { PureComponent } from 'react';
+import Box from '@mui/material/Box';
+import Stack from '@mui/material/Stack';
+import Typography from '@mui/material/Typography';
+import Attachment from './Attachment';
+
+export class AttachmentList extends PureComponent {
+ render() {
+ const { attachments, onRescan, onDelete, onClick, disabled } = this.props;
+ if (!Array.isArray(attachments) || attachments.length === 0) {
+ return null;
+ }
+
+ return (
+
+
+ Dateianhänge ({attachments.length})
+
+
+ {attachments.map((attachment, index) => (
+
+ ))}
+
+
+ );
+ }
+}
+
+export default AttachmentList;
diff --git a/src/components/manifest/ManifestHeader.jsx b/src/components/manifest/ManifestHeader.jsx
index 293e572..42c4a5a 100644
--- a/src/components/manifest/ManifestHeader.jsx
+++ b/src/components/manifest/ManifestHeader.jsx
@@ -8,6 +8,7 @@ import UploadFileIcon from '@mui/icons-material/UploadFile';
import SingleLineTextEdit from 'dashgui/SingleLineTextEdit';
import { PanelTile } from 'dashgui/panels';
import FileDropzone from '../FileDropzone';
+import AttachmentList from './AttachmentList';
import { manifestConfig, partyDisplayName } from './manifestConfig';
class ManifestHeader extends PureComponent {
@@ -48,7 +49,12 @@ class ManifestHeader extends PureComponent {
externalNumber,
saving,
pdfUploading,
+ attachments,
onPdfUpload,
+ onPdfRescan,
+ onPdfDelete,
+ onPdfClick,
+ onAttachmentClick,
onDateChange,
onNumberChange,
onExternalNumberChange,
@@ -165,6 +171,13 @@ class ManifestHeader extends PureComponent {
) : null}
+
{onPdfUpload ? (
+
+
+
+
+
+ {filename}
+
+
+ {linesCount} Zeilen · {charCount} Zeichen
+
+
+ {isDirty && (
+
+ )}
+
+
+
+
+
+
+
+
+
+
+
+ }
+ onClick={this.handleCopy}
+ disabled={loading || !content}
+ >
+ Kopieren
+
+
+
+ {!readOnly && (
+ }
+ onClick={this.handleSave}
+ disabled={loading || !isDirty}
+ >
+ Speichern
+
+ )}
+
+
+
+ );
+ }
+}
+
+export default TextEditorDialog;
diff --git a/src/components/manifest/index.js b/src/components/manifest/index.js
index 3d729a2..60723f6 100644
--- a/src/components/manifest/index.js
+++ b/src/components/manifest/index.js
@@ -3,6 +3,9 @@ export { default as ManifestForm } from './ManifestForm';
export { default as ManifestHeader } from './ManifestHeader';
export { default as ManifestLines } from './ManifestLines';
export { default as ManifestFormLine } from './ManifestFormLine';
+export { default as Attachment } from './Attachment';
+export { default as AttachmentList } from './AttachmentList';
+export { default as TextEditorDialog } from './TextEditorDialog';
export { withManifestId } from './withManifestId';
export {
manifestConfig,
diff --git a/src/pages/lieferung/neu.jsx b/src/pages/lieferung/neu.jsx
index 9e8827d..f9e0cc6 100644
--- a/src/pages/lieferung/neu.jsx
+++ b/src/pages/lieferung/neu.jsx
@@ -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}
/>
+
>
);
}
}
-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);
diff --git a/src/store/deliveryEditorSlice.js b/src/store/deliveryEditorSlice.js
deleted file mode 100644
index e66c55f..0000000
--- a/src/store/deliveryEditorSlice.js
+++ /dev/null
@@ -1,145 +0,0 @@
-import { createSlice } from '@reduxjs/toolkit';
-
-
-export function createEmptyLineitem(line = {}) {
- return {
- key: line.key ?? `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
- name: line.name ?? null,
- sku: line.sku ?? null,
- amount: line.amount ?? null,
- pu: line.pu ?? null,
- unit: line.unit ?? null,
- netprice: line.netprice ?? null,
- taxrate: line.taxrate ?? null,
- };
-}
-
-const initialDelivery = {
- id: null,
- date: null,
- deliveryEntity: null,
- external_number: null,
- number: null,
- type: null,
- lineitems: [],
- notes: [],
-};
-
-const initialState = {
- // system now delivery entity list
- deliveryEntities: [],
- // delivery being edited
- delivery: initialDelivery,
-};
-
-const deliveryEditorSlice = createSlice({
- name: 'deliveryEditor',
- initialState,
- reducers: {
- setId(state, action) {
- state.delivery.id = action.payload;
- },
- setDeliveryEntities(state, action) {
- state.deliveryEntities = action.payload ?? [];
- },
- setDelivery(state, action) {
- state.delivery = {
- ...initialDelivery,
- ...action.payload,
- };
- },
- setDate(state, action) {
- state.delivery.date = action.payload;
- },
- setDeliveryEntity(state, action) {
- state.delivery.deliveryEntity = action.payload;
- },
- setExternalNumber(state, action) {
- state.delivery.external_number = action.payload;
- },
- setNumber(state, action) {
- state.delivery.number = action.payload;
- },
- setType(state, action) {
- state.delivery.type = action.payload;
- },
- setLineitems(state, action) {
- state.delivery.lineitems = action.payload ?? [];
- },
- setNotes(state, action) {
- state.delivery.notes = Array.isArray(action.payload)
- ? action.payload
- : action.payload ? [action.payload] : [];
- },
- addLineitem(state, action) {
- state.delivery.lineitems.push(createEmptyLineitem(action.payload));
- },
- updateLineitem(state, action) {
- const { index, key, ...fields } = action.payload;
- let targetIndex = typeof index === 'number' && !Number.isNaN(index) ? index : -1;
- if (targetIndex === -1 && key != null) {
- targetIndex = state.delivery.lineitems.findIndex((l) => l.key === key);
- }
- if (targetIndex >= 0 && targetIndex < state.delivery.lineitems.length) {
- state.delivery.lineitems[targetIndex] = {
- ...state.delivery.lineitems[targetIndex],
- ...fields,
- };
- }
- },
- removeLineitem(state, action) {
- const payload = action.payload;
- let index = typeof payload === 'number' && !Number.isNaN(payload) ? payload : -1;
- if (index === -1 && payload != null) {
- const key = typeof payload === 'string' ? payload : payload?.key;
- if (key != null) {
- index = state.delivery.lineitems.findIndex((l) => l.key === key);
- }
- }
- if (index >= 0 && index < state.delivery.lineitems.length) {
- state.delivery.lineitems.splice(index, 1);
- }
- },
- resetDelivery(state) {
- state.delivery = { ...initialDelivery };
- },
- resetDeliveryEditor() {
- return {
- ...initialState,
- delivery: { ...initialDelivery },
- };
- },
- },
-});
-
-export const {
- setId,
- setDeliveryEntities,
- setDelivery,
- setDate,
- setDeliveryEntity,
- setExternalNumber,
- setNumber,
- setType,
- setLineitems,
- setNotes,
- addLineitem,
- updateLineitem,
- removeLineitem,
- resetDelivery,
- resetDeliveryEditor,
-} = deliveryEditorSlice.actions;
-
-// Selectors
-export const selectDeliveryId = (state) => state.deliveryEditor.delivery.id;
-export const selectDeliveryEntities = (state) => state.deliveryEditor.deliveryEntities;
-export const selectDelivery = (state) => state.deliveryEditor.delivery;
-export const selectDeliveryDate = (state) => state.deliveryEditor.delivery.date;
-export const selectDeliveryEntity = (state) => state.deliveryEditor.delivery.deliveryEntity;
-export const selectDeliveryExternalNumber = (state) => state.deliveryEditor.delivery.external_number;
-export const selectDeliveryNumber = (state) => state.deliveryEditor.delivery.number;
-export const selectDeliveryType = (state) => state.deliveryEditor.delivery.type;
-export const selectDeliveryLineitems = (state) => state.deliveryEditor.delivery.lineitems;
-export const selectDeliveryNotes = (state) => state.deliveryEditor.delivery.notes;
-
-export default deliveryEditorSlice.reducer;
diff --git a/src/store/index.js b/src/store/index.js
index a0e78f4..3b358cf 100644
--- a/src/store/index.js
+++ b/src/store/index.js
@@ -5,7 +5,6 @@ import categoryReducer from './categorySlice';
import companyReducer, { companyLoadListener } from './companySlice';
import posterminalReducer, { posterminalLoadListener } from './posterminalSlice';
import unitReducer, { unitLoadListener } from './unitSlice';
-import deliveryEditorReducer from './deliveryEditorSlice';
const listenerMiddleware = createListenerMiddleware();
companyLoadListener(listenerMiddleware);
@@ -53,7 +52,6 @@ export const store = configureStore({
brand: brandReducer,
posterminal: posterminalReducer,
unit: unitReducer,
- deliveryEditor: deliveryEditorReducer,
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware()
diff --git a/src/utils/pdfText.js b/src/utils/pdfText.js
new file mode 100644
index 0000000..47c352f
--- /dev/null
+++ b/src/utils/pdfText.js
@@ -0,0 +1,7 @@
+import { pdfToText } from 'pdftotext-wasm';
+
+export async function extractPdfText(pdfSource, options = { layout: true }) {
+ return pdfToText(pdfSource, options);
+}
+
+export { pdfToText };
diff --git a/src/utils/pdfThumbnail.js b/src/utils/pdfThumbnail.js
new file mode 100644
index 0000000..06d2d2f
--- /dev/null
+++ b/src/utils/pdfThumbnail.js
@@ -0,0 +1,36 @@
+import * as pdfjsLib from 'pdfjs-dist';
+
+// Tells Vite/Webpack to emit the worker script as an asset file locally
+pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
+ 'pdfjs-dist/build/pdf.worker.mjs',
+ import.meta.url
+).toString();
+
+export async function generatePdfThumbnail(pdfSource, targetWidth = 200) {
+ let loadingTask;
+ if (pdfSource instanceof File || pdfSource instanceof Blob) {
+ const arrayBuffer = await pdfSource.arrayBuffer();
+ loadingTask = pdfjsLib.getDocument({ data: arrayBuffer });
+ } else {
+ loadingTask = pdfjsLib.getDocument(pdfSource);
+ }
+
+ const pdf = await loadingTask.promise;
+ const page = await pdf.getPage(1);
+
+ const unscaledViewport = page.getViewport({ scale: 1 });
+ const scale = targetWidth / unscaledViewport.width;
+ const viewport = page.getViewport({ scale });
+
+ const canvas = document.createElement('canvas');
+ const context = canvas.getContext('2d');
+ canvas.width = Math.floor(viewport.width);
+ canvas.height = Math.floor(viewport.height);
+
+ await page.render({
+ canvasContext: context,
+ viewport: viewport,
+ }).promise;
+
+ return canvas.toDataURL('image/jpeg', 0.85);
+}
diff --git a/vite.config.js b/vite.config.js
index 29cf599..7e3760a 100644
--- a/vite.config.js
+++ b/vite.config.js
@@ -1,7 +1,7 @@
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
-import { defineConfig } from 'vite';
+import { defineConfig, searchForWorkspaceRoot } from 'vite';
import react from '@vitejs/plugin-react';
import { VitePWA } from 'vite-plugin-pwa';
@@ -90,6 +90,12 @@ const sharedServer = {
port: useHttps ? 4308 : 4209,
https: httpsOptions(),
allowedHosts: true,
+ fs: {
+ allow: [
+ searchForWorkspaceRoot(process.cwd()),
+ path.resolve(__dirname, '..'),
+ ],
+ },
proxy: {
'/api': {
target: 'http://localhost:4208',