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:
306
package-lock.json
generated
306
package-lock.json
generated
@@ -16,8 +16,11 @@
|
|||||||
"@mui/material": "^9.4.0",
|
"@mui/material": "^9.4.0",
|
||||||
"@reduxjs/toolkit": "^2.12.0",
|
"@reduxjs/toolkit": "^2.12.0",
|
||||||
"@shopify/draggable": "^1.2.1",
|
"@shopify/draggable": "^1.2.1",
|
||||||
|
"automat": "file:../automat",
|
||||||
"dashgui": "file:../dashgui",
|
"dashgui": "file:../dashgui",
|
||||||
"notistack": "^3.0.2",
|
"notistack": "^3.0.2",
|
||||||
|
"pdfjs-dist": "^6.3.289",
|
||||||
|
"pdftotext-wasm": "file:../pdftotext-wasm",
|
||||||
"react": "^19.3.0",
|
"react": "^19.3.0",
|
||||||
"react-dom": "^19.3.0",
|
"react-dom": "^19.3.0",
|
||||||
"react-redux": "^9.3.0",
|
"react-redux": "^9.3.0",
|
||||||
@@ -32,6 +35,17 @@
|
|||||||
"vite-plugin-pwa": "^1.3.0"
|
"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": {
|
"../dashgui": {
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -54,6 +68,13 @@
|
|||||||
"react-dom": "^19.0.0"
|
"react-dom": "^19.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"../pdftotext-wasm": {
|
||||||
|
"version": "0.1.0",
|
||||||
|
"license": "GPLv3",
|
||||||
|
"bin": {
|
||||||
|
"pdftotext-wasm": "bin.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@apideck/better-ajv-errors": {
|
"node_modules/@apideck/better-ajv-errors": {
|
||||||
"version": "0.3.7",
|
"version": "0.3.7",
|
||||||
"resolved": "https://registry.npmjs.org/@apideck/better-ajv-errors/-/better-ajv-errors-0.3.7.tgz",
|
"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": {
|
"node_modules/@napi-rs/lzma-linux-x64-gnu": {
|
||||||
"version": "1.5.1",
|
"version": "1.5.1",
|
||||||
"resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz",
|
"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": ">= 4.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/automat": {
|
||||||
|
"resolved": "../automat",
|
||||||
|
"link": true
|
||||||
|
},
|
||||||
"node_modules/available-typed-arrays": {
|
"node_modules/available-typed-arrays": {
|
||||||
"version": "1.0.7",
|
"version": "1.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz",
|
||||||
@@ -5705,6 +5995,22 @@
|
|||||||
"node": ">=8"
|
"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": {
|
"node_modules/picocolors": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||||
|
|||||||
@@ -22,8 +22,11 @@
|
|||||||
"@mui/material": "^9.4.0",
|
"@mui/material": "^9.4.0",
|
||||||
"@reduxjs/toolkit": "^2.12.0",
|
"@reduxjs/toolkit": "^2.12.0",
|
||||||
"@shopify/draggable": "^1.2.1",
|
"@shopify/draggable": "^1.2.1",
|
||||||
"notistack": "^3.0.2",
|
"automat": "file:../automat",
|
||||||
"dashgui": "file:../dashgui",
|
"dashgui": "file:../dashgui",
|
||||||
|
"notistack": "^3.0.2",
|
||||||
|
"pdfjs-dist": "^6.3.289",
|
||||||
|
"pdftotext-wasm": "file:../pdftotext-wasm",
|
||||||
"react": "^19.3.0",
|
"react": "^19.3.0",
|
||||||
"react-dom": "^19.3.0",
|
"react-dom": "^19.3.0",
|
||||||
"react-redux": "^9.3.0",
|
"react-redux": "^9.3.0",
|
||||||
|
|||||||
257
src/automats/deliveryEditorAutomat.js
Normal file
257
src/automats/deliveryEditorAutomat.js
Normal file
@@ -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;
|
||||||
1
src/automats/index.js
Normal file
1
src/automats/index.js
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from './deliveryEditorAutomat';
|
||||||
243
src/components/manifest/Attachment.jsx
Normal file
243
src/components/manifest/Attachment.jsx
Normal file
@@ -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 (
|
||||||
|
<Box
|
||||||
|
component="div"
|
||||||
|
role="button"
|
||||||
|
tabIndex={disabled ? -1 : 0}
|
||||||
|
onClick={disabled ? undefined : this.handleTileClick}
|
||||||
|
sx={{
|
||||||
|
...tileBaseSx,
|
||||||
|
width: TILE_SIZE,
|
||||||
|
height: TILE_SIZE,
|
||||||
|
p: 0,
|
||||||
|
gap: 0,
|
||||||
|
position: 'relative',
|
||||||
|
overflow: 'hidden',
|
||||||
|
cursor: disabled ? 'default' : 'pointer',
|
||||||
|
bgcolor: isText ? '#fcfcfc' : '#f8fafc',
|
||||||
|
border: '1px solid',
|
||||||
|
borderColor: 'divider',
|
||||||
|
...(disabled ? { opacity: 0.6, pointerEvents: 'none' } : {}),
|
||||||
|
...sx,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Thumbnail, Text preview, or Fallback Icon */}
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
position: 'absolute',
|
||||||
|
inset: 0,
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
bgcolor: isText ? '#fafafa' : '#f1f5f9',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{thumbnail ? (
|
||||||
|
<Box
|
||||||
|
component="img"
|
||||||
|
src={thumbnail}
|
||||||
|
alt={name}
|
||||||
|
sx={{
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
objectFit: 'contain',
|
||||||
|
display: 'block',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : isText ? (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
p: 1.25,
|
||||||
|
bgcolor: '#fafafa',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
justifyContent: 'flex-start',
|
||||||
|
overflow: 'hidden',
|
||||||
|
position: 'relative',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5, mb: 0.5 }}>
|
||||||
|
<TextSnippetOutlinedIcon sx={{ fontSize: 18, color: 'primary.main' }} />
|
||||||
|
<Typography
|
||||||
|
component="span"
|
||||||
|
sx={{
|
||||||
|
fontSize: '0.62rem',
|
||||||
|
fontWeight: 700,
|
||||||
|
color: 'primary.dark',
|
||||||
|
bgcolor: 'action.selected',
|
||||||
|
px: 0.5,
|
||||||
|
py: 0.1,
|
||||||
|
borderRadius: 0.5,
|
||||||
|
letterSpacing: '0.05em',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
TXT
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
<Typography
|
||||||
|
component="pre"
|
||||||
|
sx={{
|
||||||
|
m: 0,
|
||||||
|
fontFamily: 'monospace',
|
||||||
|
fontSize: '0.45rem',
|
||||||
|
lineHeight: 1.2,
|
||||||
|
color: 'text.secondary',
|
||||||
|
whiteSpace: 'pre-wrap',
|
||||||
|
wordBreak: 'break-all',
|
||||||
|
overflow: 'hidden',
|
||||||
|
opacity: 0.8,
|
||||||
|
userSelect: 'none',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{attachment?.text ? attachment.text.slice(0, 300) : 'Textdatei'}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
) : (
|
||||||
|
<PictureAsPdfIcon sx={{ fontSize: 48, color: 'error.main' }} />
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Action buttons: Rescan (for PDF) and Delete */}
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: 4,
|
||||||
|
right: 4,
|
||||||
|
display: 'flex',
|
||||||
|
gap: 0.5,
|
||||||
|
zIndex: 2,
|
||||||
|
}}
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
{isPdf ? (
|
||||||
|
<Tooltip title="Neu analysieren">
|
||||||
|
<span>
|
||||||
|
<IconButton
|
||||||
|
size="small"
|
||||||
|
disabled={disabled}
|
||||||
|
onClick={this.handleRescan}
|
||||||
|
sx={{
|
||||||
|
bgcolor: 'rgba(0, 0, 0, 0.6)',
|
||||||
|
color: '#fff',
|
||||||
|
p: 0.5,
|
||||||
|
backdropFilter: 'blur(2px)',
|
||||||
|
'&:hover': { bgcolor: 'rgba(0, 0, 0, 0.85)' },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SyncIcon sx={{ fontSize: 16 }} />
|
||||||
|
</IconButton>
|
||||||
|
</span>
|
||||||
|
</Tooltip>
|
||||||
|
) : null}
|
||||||
|
<Tooltip title="Löschen">
|
||||||
|
<span>
|
||||||
|
<IconButton
|
||||||
|
size="small"
|
||||||
|
disabled={disabled}
|
||||||
|
onClick={this.handleDelete}
|
||||||
|
sx={{
|
||||||
|
bgcolor: 'rgba(0, 0, 0, 0.6)',
|
||||||
|
color: '#fff',
|
||||||
|
p: 0.5,
|
||||||
|
backdropFilter: 'blur(2px)',
|
||||||
|
'&:hover': { bgcolor: 'error.main' },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DeleteOutlineOutlinedIcon sx={{ fontSize: 16 }} />
|
||||||
|
</IconButton>
|
||||||
|
</span>
|
||||||
|
</Tooltip>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Bottom file metadata overlay */}
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
position: 'absolute',
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: 0,
|
||||||
|
px: 1,
|
||||||
|
py: 0.5,
|
||||||
|
background: 'linear-gradient(transparent, rgba(0,0,0,0.75) 40%, rgba(0,0,0,0.85))',
|
||||||
|
textAlign: 'left',
|
||||||
|
pointerEvents: 'none',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography
|
||||||
|
variant="caption"
|
||||||
|
fontWeight={600}
|
||||||
|
noWrap
|
||||||
|
sx={{ color: '#fff', display: 'block', fontSize: '0.72rem', lineHeight: 1.2 }}
|
||||||
|
>
|
||||||
|
{name}
|
||||||
|
</Typography>
|
||||||
|
{size ? (
|
||||||
|
<Typography
|
||||||
|
variant="caption"
|
||||||
|
noWrap
|
||||||
|
sx={{ color: 'rgba(255,255,255,0.8)', fontSize: '0.65rem' }}
|
||||||
|
>
|
||||||
|
{this.formatFileSize(size)}
|
||||||
|
</Typography>
|
||||||
|
) : null}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Attachment;
|
||||||
41
src/components/manifest/AttachmentList.jsx
Normal file
41
src/components/manifest/AttachmentList.jsx
Normal file
@@ -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 (
|
||||||
|
<Box sx={{ mt: 2.5, pt: 1.5, borderTop: '1px solid', borderColor: 'divider' }}>
|
||||||
|
<Typography
|
||||||
|
variant="caption"
|
||||||
|
color="text.secondary"
|
||||||
|
fontWeight={600}
|
||||||
|
sx={{ mb: 1, display: 'block', textTransform: 'uppercase', letterSpacing: '0.05em' }}
|
||||||
|
>
|
||||||
|
Dateianhänge ({attachments.length})
|
||||||
|
</Typography>
|
||||||
|
<Stack direction="row" spacing={1.5} sx={{ flexWrap: 'wrap', gap: 1.5 }}>
|
||||||
|
{attachments.map((attachment, index) => (
|
||||||
|
<Attachment
|
||||||
|
key={attachment.id ?? index}
|
||||||
|
attachment={attachment}
|
||||||
|
onRescan={onRescan}
|
||||||
|
onDelete={onDelete}
|
||||||
|
onClick={onClick}
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AttachmentList;
|
||||||
@@ -8,6 +8,7 @@ import UploadFileIcon from '@mui/icons-material/UploadFile';
|
|||||||
import SingleLineTextEdit from 'dashgui/SingleLineTextEdit';
|
import SingleLineTextEdit from 'dashgui/SingleLineTextEdit';
|
||||||
import { PanelTile } from 'dashgui/panels';
|
import { PanelTile } from 'dashgui/panels';
|
||||||
import FileDropzone from '../FileDropzone';
|
import FileDropzone from '../FileDropzone';
|
||||||
|
import AttachmentList from './AttachmentList';
|
||||||
import { manifestConfig, partyDisplayName } from './manifestConfig';
|
import { manifestConfig, partyDisplayName } from './manifestConfig';
|
||||||
|
|
||||||
class ManifestHeader extends PureComponent {
|
class ManifestHeader extends PureComponent {
|
||||||
@@ -48,7 +49,12 @@ class ManifestHeader extends PureComponent {
|
|||||||
externalNumber,
|
externalNumber,
|
||||||
saving,
|
saving,
|
||||||
pdfUploading,
|
pdfUploading,
|
||||||
|
attachments,
|
||||||
onPdfUpload,
|
onPdfUpload,
|
||||||
|
onPdfRescan,
|
||||||
|
onPdfDelete,
|
||||||
|
onPdfClick,
|
||||||
|
onAttachmentClick,
|
||||||
onDateChange,
|
onDateChange,
|
||||||
onNumberChange,
|
onNumberChange,
|
||||||
onExternalNumberChange,
|
onExternalNumberChange,
|
||||||
@@ -165,6 +171,13 @@ class ManifestHeader extends PureComponent {
|
|||||||
</FileDropzone>
|
</FileDropzone>
|
||||||
) : null}
|
) : null}
|
||||||
</Stack>
|
</Stack>
|
||||||
|
<AttachmentList
|
||||||
|
attachments={attachments}
|
||||||
|
onRescan={onPdfRescan}
|
||||||
|
onDelete={onPdfDelete}
|
||||||
|
onClick={onAttachmentClick ?? onPdfClick}
|
||||||
|
disabled={saving || pdfUploading}
|
||||||
|
/>
|
||||||
{onPdfUpload ? (
|
{onPdfUpload ? (
|
||||||
<input
|
<input
|
||||||
ref={this.fileInputRef}
|
ref={this.fileInputRef}
|
||||||
|
|||||||
250
src/components/manifest/TextEditorDialog.jsx
Normal file
250
src/components/manifest/TextEditorDialog.jsx
Normal file
@@ -0,0 +1,250 @@
|
|||||||
|
import { PureComponent } from 'react';
|
||||||
|
import Box from '@mui/material/Box';
|
||||||
|
import Button from '@mui/material/Button';
|
||||||
|
import Dialog from '@mui/material/Dialog';
|
||||||
|
import DialogActions from '@mui/material/DialogActions';
|
||||||
|
import DialogContent from '@mui/material/DialogContent';
|
||||||
|
import DialogTitle from '@mui/material/DialogTitle';
|
||||||
|
import IconButton from '@mui/material/IconButton';
|
||||||
|
import TextField from '@mui/material/TextField';
|
||||||
|
import Typography from '@mui/material/Typography';
|
||||||
|
import Chip from '@mui/material/Chip';
|
||||||
|
import CloseIcon from '@mui/icons-material/Close';
|
||||||
|
import TextSnippetOutlinedIcon from '@mui/icons-material/TextSnippetOutlined';
|
||||||
|
import ContentCopyIcon from '@mui/icons-material/ContentCopy';
|
||||||
|
import SaveOutlinedIcon from '@mui/icons-material/SaveOutlined';
|
||||||
|
import { enqueueSnackbar } from 'notistack';
|
||||||
|
|
||||||
|
export class TextEditorDialog extends PureComponent {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
content: '',
|
||||||
|
initialContent: '',
|
||||||
|
loading: false,
|
||||||
|
};
|
||||||
|
this.handleChange = this.handleChange.bind(this);
|
||||||
|
this.handleSave = this.handleSave.bind(this);
|
||||||
|
this.handleCopy = this.handleCopy.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.loadContent();
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps) {
|
||||||
|
if (this.props.attachment !== prevProps.attachment || (this.props.open && !prevProps.open)) {
|
||||||
|
this.loadContent();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadContent() {
|
||||||
|
const { attachment, open } = this.props;
|
||||||
|
if (!open || !attachment) return;
|
||||||
|
|
||||||
|
if (typeof attachment.text === 'string') {
|
||||||
|
this.setState({
|
||||||
|
content: attachment.text,
|
||||||
|
initialContent: attachment.text,
|
||||||
|
loading: false,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (attachment.file instanceof Blob || attachment.file instanceof File) {
|
||||||
|
this.setState({ loading: true });
|
||||||
|
try {
|
||||||
|
const text = await attachment.file.text();
|
||||||
|
this.setState({
|
||||||
|
content: text,
|
||||||
|
initialContent: text,
|
||||||
|
loading: false,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Fehler beim Lesen der Textdatei:', err);
|
||||||
|
this.setState({ loading: false });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleChange(e) {
|
||||||
|
this.setState({ content: e.target.value });
|
||||||
|
}
|
||||||
|
|
||||||
|
handleSave() {
|
||||||
|
const { onSave, onClose } = this.props;
|
||||||
|
const { content } = this.state;
|
||||||
|
onSave?.(content);
|
||||||
|
enqueueSnackbar('Textdatei gespeichert', { variant: 'success' });
|
||||||
|
onClose?.();
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleCopy() {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(this.state.content);
|
||||||
|
enqueueSnackbar('In die Zwischenablage kopiert', { variant: 'info' });
|
||||||
|
} catch {
|
||||||
|
enqueueSnackbar('Kopieren fehlgeschlagen', { variant: 'error' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { open, onClose, attachment, readOnly = false } = this.props;
|
||||||
|
if (!open) return null;
|
||||||
|
|
||||||
|
const { content, initialContent, loading } = this.state;
|
||||||
|
const filename = attachment?.name || 'Dokument.txt';
|
||||||
|
const isDirty = content !== initialContent;
|
||||||
|
const linesCount = content ? content.split('\n').length : 0;
|
||||||
|
const charCount = content ? content.length : 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog
|
||||||
|
open={open}
|
||||||
|
onClose={onClose}
|
||||||
|
maxWidth="lg"
|
||||||
|
fullWidth
|
||||||
|
slotProps={{
|
||||||
|
paper: {
|
||||||
|
sx: {
|
||||||
|
height: '85vh',
|
||||||
|
maxHeight: 850,
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DialogTitle
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
py: 1.5,
|
||||||
|
px: 2.5,
|
||||||
|
borderBottom: '1px solid',
|
||||||
|
borderColor: 'divider',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5, minWidth: 0 }}>
|
||||||
|
<TextSnippetOutlinedIcon color="primary" />
|
||||||
|
<Box sx={{ minWidth: 0 }}>
|
||||||
|
<Typography variant="subtitle1" fontWeight={600} noWrap>
|
||||||
|
{filename}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="caption" color="text.secondary">
|
||||||
|
{linesCount} Zeilen · {charCount} Zeichen
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
{isDirty && (
|
||||||
|
<Chip label="Ungespeichert" size="small" color="warning" sx={{ height: 20, fontSize: '0.7rem' }} />
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
<IconButton size="small" onClick={onClose} sx={{ color: 'text.secondary' }}>
|
||||||
|
<CloseIcon fontSize="small" />
|
||||||
|
</IconButton>
|
||||||
|
</DialogTitle>
|
||||||
|
|
||||||
|
<DialogContent
|
||||||
|
sx={{
|
||||||
|
p: 2,
|
||||||
|
flex: 1,
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
bgcolor: '#fafafa',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<TextField
|
||||||
|
multiline
|
||||||
|
fullWidth
|
||||||
|
value={content}
|
||||||
|
disabled={loading}
|
||||||
|
onChange={this.handleChange}
|
||||||
|
inputProps={{
|
||||||
|
wrap: 'off',
|
||||||
|
spellCheck: false,
|
||||||
|
}}
|
||||||
|
slotProps={{
|
||||||
|
htmlInput: {
|
||||||
|
wrap: 'off',
|
||||||
|
spellCheck: false,
|
||||||
|
},
|
||||||
|
input: {
|
||||||
|
readOnly,
|
||||||
|
sx: {
|
||||||
|
height: '100%',
|
||||||
|
alignItems: 'flex-start',
|
||||||
|
fontFamily: '"SFMono-Regular", Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace',
|
||||||
|
fontSize: '0.72rem',
|
||||||
|
lineHeight: 1.45,
|
||||||
|
tabSize: 4,
|
||||||
|
bgcolor: '#ffffff',
|
||||||
|
p: 1.5,
|
||||||
|
'& textarea': {
|
||||||
|
height: '100% !important',
|
||||||
|
whiteSpace: 'pre !important',
|
||||||
|
overflowWrap: 'normal !important',
|
||||||
|
wordWrap: 'normal !important',
|
||||||
|
wordBreak: 'normal !important',
|
||||||
|
overflowX: 'auto !important',
|
||||||
|
overflowY: 'auto !important',
|
||||||
|
fontFamily: 'inherit',
|
||||||
|
fontSize: 'inherit',
|
||||||
|
lineHeight: 'inherit',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
sx={{
|
||||||
|
flex: 1,
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
'& .MuiInputBase-root': {
|
||||||
|
flex: 1,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</DialogContent>
|
||||||
|
|
||||||
|
<DialogActions
|
||||||
|
sx={{
|
||||||
|
px: 2.5,
|
||||||
|
py: 1.5,
|
||||||
|
borderTop: '1px solid',
|
||||||
|
borderColor: 'divider',
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
variant="outlined"
|
||||||
|
startIcon={<ContentCopyIcon />}
|
||||||
|
onClick={this.handleCopy}
|
||||||
|
disabled={loading || !content}
|
||||||
|
>
|
||||||
|
Kopieren
|
||||||
|
</Button>
|
||||||
|
<Box sx={{ display: 'flex', gap: 1 }}>
|
||||||
|
<Button size="small" onClick={onClose}>
|
||||||
|
{readOnly ? 'Schließen' : 'Abbrechen'}
|
||||||
|
</Button>
|
||||||
|
{!readOnly && (
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
variant="contained"
|
||||||
|
startIcon={<SaveOutlinedIcon />}
|
||||||
|
onClick={this.handleSave}
|
||||||
|
disabled={loading || !isDirty}
|
||||||
|
>
|
||||||
|
Speichern
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TextEditorDialog;
|
||||||
@@ -3,6 +3,9 @@ export { default as ManifestForm } from './ManifestForm';
|
|||||||
export { default as ManifestHeader } from './ManifestHeader';
|
export { default as ManifestHeader } from './ManifestHeader';
|
||||||
export { default as ManifestLines } from './ManifestLines';
|
export { default as ManifestLines } from './ManifestLines';
|
||||||
export { default as ManifestFormLine } from './ManifestFormLine';
|
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 { withManifestId } from './withManifestId';
|
||||||
export {
|
export {
|
||||||
manifestConfig,
|
manifestConfig,
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { PureComponent } from 'react';
|
import { PureComponent } from 'react';
|
||||||
import { legacy_connect as connect } from 'react-redux';
|
|
||||||
import Alert from '@mui/material/Alert';
|
import Alert from '@mui/material/Alert';
|
||||||
import Box from '@mui/material/Box';
|
import Box from '@mui/material/Box';
|
||||||
import CircularProgress from '@mui/material/CircularProgress';
|
import CircularProgress from '@mui/material/CircularProgress';
|
||||||
@@ -18,6 +17,7 @@ import {
|
|||||||
emptyLine,
|
emptyLine,
|
||||||
lineFromApi,
|
lineFromApi,
|
||||||
manifestConfig,
|
manifestConfig,
|
||||||
|
TextEditorDialog,
|
||||||
} from '../../components/manifest';
|
} from '../../components/manifest';
|
||||||
import ManifestPdfDialog from '../../components/manifest/ManifestPdfDialog';
|
import ManifestPdfDialog from '../../components/manifest/ManifestPdfDialog';
|
||||||
import { loadParties } from '../../components/manifest/manifestConfig';
|
import { loadParties } from '../../components/manifest/manifestConfig';
|
||||||
@@ -25,29 +25,9 @@ import { StorageManagerContext } from '../../api/storageManagerContext';
|
|||||||
import { requireArray, requireDefined, requireObject } from '../../utils/apiShape';
|
import { requireArray, requireDefined, requireObject } from '../../utils/apiShape';
|
||||||
import { backendErrorFrom } from '../../utils/backendError';
|
import { backendErrorFrom } from '../../utils/backendError';
|
||||||
import { notifyError } from '../../utils/notify';
|
import { notifyError } from '../../utils/notify';
|
||||||
import {
|
import { generatePdfThumbnail } from '../../utils/pdfThumbnail';
|
||||||
setId,
|
import { extractPdfText } from '../../utils/pdfText';
|
||||||
setDeliveryEntities,
|
import { deliveryEditorAutomat } from '../../automats/deliveryEditorAutomat';
|
||||||
setDate,
|
|
||||||
setDeliveryEntity,
|
|
||||||
setNumber,
|
|
||||||
setExternalNumber,
|
|
||||||
setType,
|
|
||||||
setLineitems,
|
|
||||||
setNotes,
|
|
||||||
addLineitem,
|
|
||||||
updateLineitem,
|
|
||||||
removeLineitem,
|
|
||||||
resetDelivery,
|
|
||||||
selectDeliveryId,
|
|
||||||
selectDeliveryEntities,
|
|
||||||
selectDeliveryDate,
|
|
||||||
selectDeliveryEntity,
|
|
||||||
selectDeliveryNumber,
|
|
||||||
selectDeliveryExternalNumber,
|
|
||||||
selectDeliveryLineitems,
|
|
||||||
selectDeliveryNotes,
|
|
||||||
} from '../../store/deliveryEditorSlice';
|
|
||||||
|
|
||||||
class LieferungNeuPage extends PureComponent {
|
class LieferungNeuPage extends PureComponent {
|
||||||
static contextType = StorageManagerContext;
|
static contextType = StorageManagerContext;
|
||||||
@@ -56,6 +36,7 @@ class LieferungNeuPage extends PureComponent {
|
|||||||
super(props);
|
super(props);
|
||||||
this.cfg = manifestConfig('delivery');
|
this.cfg = manifestConfig('delivery');
|
||||||
this.state = {
|
this.state = {
|
||||||
|
...deliveryEditorAutomat.state,
|
||||||
status: 'loadingParties',
|
status: 'loadingParties',
|
||||||
error: null,
|
error: null,
|
||||||
partiesLoading: true,
|
partiesLoading: true,
|
||||||
@@ -64,6 +45,8 @@ class LieferungNeuPage extends PureComponent {
|
|||||||
pdfDialogOpen: false,
|
pdfDialogOpen: false,
|
||||||
pdfDialogData: null,
|
pdfDialogData: null,
|
||||||
pdfFileName: '',
|
pdfFileName: '',
|
||||||
|
textEditorOpen: false,
|
||||||
|
textEditorAttachment: null,
|
||||||
};
|
};
|
||||||
this._cancelled = false;
|
this._cancelled = false;
|
||||||
this.handleRetry = this.handleRetry.bind(this);
|
this.handleRetry = this.handleRetry.bind(this);
|
||||||
@@ -76,16 +59,22 @@ class LieferungNeuPage extends PureComponent {
|
|||||||
this.handleRemoveLine = this.handleRemoveLine.bind(this);
|
this.handleRemoveLine = this.handleRemoveLine.bind(this);
|
||||||
this.handleSave = this.handleSave.bind(this);
|
this.handleSave = this.handleSave.bind(this);
|
||||||
this.handlePdfUpload = this.handlePdfUpload.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.handleClosePdfDialog = this.handleClosePdfDialog.bind(this);
|
||||||
|
this.handleAttachmentClick = this.handleAttachmentClick.bind(this);
|
||||||
|
this.handleCloseTextEditor = this.handleCloseTextEditor.bind(this);
|
||||||
|
this.handleSaveTextEditor = this.handleSaveTextEditor.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
|
this.unsubscribe = deliveryEditorAutomat.subscribe(this);
|
||||||
this._cancelled = false;
|
this._cancelled = false;
|
||||||
if (this.props.docId != null) {
|
if (this.props.docId != null) {
|
||||||
void this.loadEdit();
|
void this.loadEdit();
|
||||||
} else {
|
} else {
|
||||||
if (this.props.deliveryId != null || this.props.number != null) {
|
if (this.state.deliveryId != null || this.state.number != null) {
|
||||||
this.props.resetDelivery();
|
deliveryEditorAutomat.actions.resetDelivery();
|
||||||
}
|
}
|
||||||
void this.loadPartiesOnly();
|
void this.loadPartiesOnly();
|
||||||
}
|
}
|
||||||
@@ -96,7 +85,7 @@ class LieferungNeuPage extends PureComponent {
|
|||||||
if (this.props.docId != null) {
|
if (this.props.docId != null) {
|
||||||
void this.loadEdit();
|
void this.loadEdit();
|
||||||
} else {
|
} else {
|
||||||
this.props.resetDelivery();
|
deliveryEditorAutomat.actions.resetDelivery();
|
||||||
void this.loadPartiesOnly();
|
void this.loadPartiesOnly();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -104,6 +93,7 @@ class LieferungNeuPage extends PureComponent {
|
|||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
this._cancelled = true;
|
this._cancelled = true;
|
||||||
|
this.unsubscribe?.();
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadPartiesOnly() {
|
async loadPartiesOnly() {
|
||||||
@@ -111,7 +101,7 @@ class LieferungNeuPage extends PureComponent {
|
|||||||
try {
|
try {
|
||||||
const parties = await loadParties('delivery', this.context);
|
const parties = await loadParties('delivery', this.context);
|
||||||
if (this._cancelled) return;
|
if (this._cancelled) return;
|
||||||
this.props.setDeliveryEntities(parties);
|
deliveryEditorAutomat.actions.setDeliveryEntities(parties);
|
||||||
this.setState({
|
this.setState({
|
||||||
status: 'ready',
|
status: 'ready',
|
||||||
partiesLoading: false,
|
partiesLoading: false,
|
||||||
@@ -146,7 +136,7 @@ class LieferungNeuPage extends PureComponent {
|
|||||||
]);
|
]);
|
||||||
if (this._cancelled) return;
|
if (this._cancelled) return;
|
||||||
|
|
||||||
this.props.setDeliveryEntities(parties);
|
deliveryEditorAutomat.actions.setDeliveryEntities(parties);
|
||||||
|
|
||||||
requireObject(docRes, cfg.getPathLabel);
|
requireObject(docRes, cfg.getPathLabel);
|
||||||
requireObject(docRes[cfg.docField], cfg.docField);
|
requireObject(docRes[cfg.docField], cfg.docField);
|
||||||
@@ -155,12 +145,12 @@ class LieferungNeuPage extends PureComponent {
|
|||||||
const lines =
|
const lines =
|
||||||
apiLines.length > 0 ? apiLines.map(lineFromApi) : [emptyLine()];
|
apiLines.length > 0 ? apiLines.map(lineFromApi) : [emptyLine()];
|
||||||
|
|
||||||
this.props.setId(doc.id ?? docId);
|
deliveryEditorAutomat.actions.setId(doc.id ?? docId);
|
||||||
this.props.setDate(dateInputValue(doc.date));
|
deliveryEditorAutomat.actions.setDate(dateInputValue(doc.date));
|
||||||
if (doc[cfg.partyField] != null) this.props.setDeliveryEntity(doc[cfg.partyField]);
|
if (doc[cfg.partyField] != null) deliveryEditorAutomat.actions.setDeliveryEntity(doc[cfg.partyField]);
|
||||||
if (doc.number != null) this.props.setNumber(doc.number);
|
if (doc.number != null) deliveryEditorAutomat.actions.setNumber(doc.number);
|
||||||
if (doc.externalNumber != null) this.props.setExternalNumber(String(doc.externalNumber));
|
if (doc.externalNumber != null) deliveryEditorAutomat.actions.setExternalNumber(String(doc.externalNumber));
|
||||||
this.props.setLineitems(lines);
|
deliveryEditorAutomat.actions.setLineitems(lines);
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
status: 'ready',
|
status: 'ready',
|
||||||
@@ -182,24 +172,24 @@ class LieferungNeuPage extends PureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handlePartyChange(partyId) {
|
handlePartyChange(partyId) {
|
||||||
this.props.setDeliveryEntity(partyId);
|
deliveryEditorAutomat.actions.setDeliveryEntity(partyId);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleDateChange(date) {
|
handleDateChange(date) {
|
||||||
this.props.setDate(date);
|
deliveryEditorAutomat.actions.setDate(date);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleNumberChange(number) {
|
handleNumberChange(number) {
|
||||||
this.props.setNumber(number);
|
deliveryEditorAutomat.actions.setNumber(number);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleExternalNumberChange(externalNumber) {
|
handleExternalNumberChange(externalNumber) {
|
||||||
this.props.setExternalNumber(externalNumber);
|
deliveryEditorAutomat.actions.setExternalNumber(externalNumber);
|
||||||
}
|
}
|
||||||
|
|
||||||
findLineIndex(key) {
|
findLineIndex(key) {
|
||||||
if (typeof key === 'number' && !Number.isNaN(key)) return key;
|
if (typeof key === 'number' && !Number.isNaN(key)) return key;
|
||||||
const lines = this.props.lines;
|
const lines = this.state.lines;
|
||||||
if (Array.isArray(lines)) {
|
if (Array.isArray(lines)) {
|
||||||
const idx = lines.findIndex((l) => l.key === key);
|
const idx = lines.findIndex((l) => l.key === key);
|
||||||
if (idx !== -1) return idx;
|
if (idx !== -1) return idx;
|
||||||
@@ -210,7 +200,7 @@ class LieferungNeuPage extends PureComponent {
|
|||||||
|
|
||||||
handleLineChange(key, field, value) {
|
handleLineChange(key, field, value) {
|
||||||
const index = this.findLineIndex(key);
|
const index = this.findLineIndex(key);
|
||||||
this.props.updateLineitem({
|
deliveryEditorAutomat.actions.updateLineitem({
|
||||||
index,
|
index,
|
||||||
key,
|
key,
|
||||||
[field]: value,
|
[field]: value,
|
||||||
@@ -220,16 +210,105 @@ class LieferungNeuPage extends PureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleAddLine() {
|
handleAddLine() {
|
||||||
this.props.addLineitem();
|
deliveryEditorAutomat.actions.addLineitem();
|
||||||
}
|
}
|
||||||
|
|
||||||
handleRemoveLine(key) {
|
handleRemoveLine(key) {
|
||||||
const index = this.findLineIndex(key);
|
const index = this.findLineIndex(key);
|
||||||
this.props.removeLineitem(index >= 0 ? index : key);
|
deliveryEditorAutomat.actions.removeLineitem(index >= 0 ? index : key);
|
||||||
}
|
}
|
||||||
|
|
||||||
async handlePdfUpload(file) {
|
async handlePdfUpload(file) {
|
||||||
if (!file || this.state.pdfUploading) return;
|
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({
|
this.setState({
|
||||||
pdfUploading: true,
|
pdfUploading: true,
|
||||||
pdfDialogOpen: true,
|
pdfDialogOpen: true,
|
||||||
@@ -311,9 +390,9 @@ class LieferungNeuPage extends PureComponent {
|
|||||||
const notes = Array.isArray(result?.notes) ? result.notes : [];
|
const notes = Array.isArray(result?.notes) ? result.notes : [];
|
||||||
|
|
||||||
// If supplierId is null, try matching supplier by name from loaded delivery entities
|
// 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 nameToMatch = result.basedata.lieferantName.trim().toLowerCase();
|
||||||
const matched = this.props.deliveryEntities.find(
|
const matched = this.state.deliveryEntities.find(
|
||||||
(e) =>
|
(e) =>
|
||||||
e.name &&
|
e.name &&
|
||||||
(e.name.trim().toLowerCase() === nameToMatch ||
|
(e.name.trim().toLowerCase() === nameToMatch ||
|
||||||
@@ -323,17 +402,17 @@ class LieferungNeuPage extends PureComponent {
|
|||||||
if (matched) supplierId = matched.id;
|
if (matched) supplierId = matched.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (docDate) this.props.setDate(docDate);
|
if (docDate) deliveryEditorAutomat.actions.setDate(docDate);
|
||||||
if (supplierId != null) this.props.setDeliveryEntity(supplierId);
|
if (supplierId != null) deliveryEditorAutomat.actions.setDeliveryEntity(supplierId);
|
||||||
if (extNum != null) {
|
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) {
|
if (newLines && newLines.length > 0) {
|
||||||
this.props.setLineitems(newLines);
|
deliveryEditorAutomat.actions.setLineitems(newLines);
|
||||||
}
|
}
|
||||||
if (notes.length > 0) {
|
if (notes.length > 0) {
|
||||||
this.props.setNotes(notes);
|
deliveryEditorAutomat.actions.setNotes(notes);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -375,6 +454,52 @@ class LieferungNeuPage extends PureComponent {
|
|||||||
this.setState({ pdfDialogOpen: false });
|
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() {
|
async handleSave() {
|
||||||
const { docId } = this.props;
|
const { docId } = this.props;
|
||||||
const cfg = this.cfg;
|
const cfg = this.cfg;
|
||||||
@@ -385,11 +510,11 @@ class LieferungNeuPage extends PureComponent {
|
|||||||
try {
|
try {
|
||||||
body = buildManifestBody(
|
body = buildManifestBody(
|
||||||
{
|
{
|
||||||
number: this.props.number ?? '',
|
number: this.state.number ?? '',
|
||||||
externalNumber: this.props.externalNumber ?? '',
|
externalNumber: this.state.externalNumber ?? '',
|
||||||
date: this.props.date ?? '',
|
date: this.state.date ?? '',
|
||||||
partyId: this.props.partyId,
|
partyId: this.state.partyId,
|
||||||
lines: this.props.lines,
|
lines: this.state.lines,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
partyField: cfg.partyField,
|
partyField: cfg.partyField,
|
||||||
@@ -412,7 +537,7 @@ class LieferungNeuPage extends PureComponent {
|
|||||||
await this.context[cfg.updateCache]();
|
await this.context[cfg.updateCache]();
|
||||||
if (this._cancelled) return;
|
if (this._cancelled) return;
|
||||||
enqueueSnackbar(cfg.createdMsg, { variant: 'success' });
|
enqueueSnackbar(cfg.createdMsg, { variant: 'success' });
|
||||||
this.props.resetDelivery();
|
deliveryEditorAutomat.actions.resetDelivery();
|
||||||
this.props.navigate(cfg.editPath(data[cfg.docField].id));
|
this.props.navigate(cfg.editPath(data[cfg.docField].id));
|
||||||
} else {
|
} else {
|
||||||
const data = await this.context[cfg.api].update(docId, body);
|
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 apiLines = requireArray(doc.lines, `${cfg.docField}.lines`);
|
||||||
const lines = apiLines.length > 0 ? apiLines.map(lineFromApi) : [emptyLine()];
|
const lines = apiLines.length > 0 ? apiLines.map(lineFromApi) : [emptyLine()];
|
||||||
|
|
||||||
this.props.setId(doc.id ?? docId);
|
deliveryEditorAutomat.actions.setId(doc.id ?? docId);
|
||||||
this.props.setDate(dateInputValue(doc.date));
|
deliveryEditorAutomat.actions.setDate(dateInputValue(doc.date));
|
||||||
if (doc[cfg.partyField] != null) this.props.setDeliveryEntity(doc[cfg.partyField]);
|
if (doc[cfg.partyField] != null) deliveryEditorAutomat.actions.setDeliveryEntity(doc[cfg.partyField]);
|
||||||
if (doc.number != null) this.props.setNumber(doc.number);
|
if (doc.number != null) deliveryEditorAutomat.actions.setNumber(doc.number);
|
||||||
if (doc.externalNumber != null) this.props.setExternalNumber(String(doc.externalNumber));
|
if (doc.externalNumber != null) deliveryEditorAutomat.actions.setExternalNumber(String(doc.externalNumber));
|
||||||
this.props.setLineitems(lines);
|
deliveryEditorAutomat.actions.setLineitems(lines);
|
||||||
|
|
||||||
enqueueSnackbar(cfg.savedMsg, { variant: 'success' });
|
enqueueSnackbar(cfg.savedMsg, { variant: 'success' });
|
||||||
this.setState({ saving: false });
|
this.setState({ saving: false });
|
||||||
@@ -453,8 +578,6 @@ class LieferungNeuPage extends PureComponent {
|
|||||||
pdfDialogOpen,
|
pdfDialogOpen,
|
||||||
pdfDialogData,
|
pdfDialogData,
|
||||||
pdfFileName,
|
pdfFileName,
|
||||||
} = this.state;
|
|
||||||
const {
|
|
||||||
deliveryEntities,
|
deliveryEntities,
|
||||||
date,
|
date,
|
||||||
partyId,
|
partyId,
|
||||||
@@ -462,7 +585,8 @@ class LieferungNeuPage extends PureComponent {
|
|||||||
externalNumber,
|
externalNumber,
|
||||||
lines,
|
lines,
|
||||||
notes,
|
notes,
|
||||||
} = this.props;
|
attachments,
|
||||||
|
} = this.state;
|
||||||
|
|
||||||
if (status === 'loadingParties' || status === 'loading') {
|
if (status === 'loadingParties' || status === 'loading') {
|
||||||
return (
|
return (
|
||||||
@@ -492,7 +616,11 @@ class LieferungNeuPage extends PureComponent {
|
|||||||
externalNumber={externalNumber ?? ''}
|
externalNumber={externalNumber ?? ''}
|
||||||
saving={saving}
|
saving={saving}
|
||||||
pdfUploading={pdfUploading}
|
pdfUploading={pdfUploading}
|
||||||
|
attachments={attachments}
|
||||||
onPdfUpload={this.handlePdfUpload}
|
onPdfUpload={this.handlePdfUpload}
|
||||||
|
onPdfRescan={this.handlePdfRescan}
|
||||||
|
onPdfDelete={this.handlePdfDelete}
|
||||||
|
onAttachmentClick={this.handleAttachmentClick}
|
||||||
onPartyChange={this.handlePartyChange}
|
onPartyChange={this.handlePartyChange}
|
||||||
onDateChange={this.handleDateChange}
|
onDateChange={this.handleDateChange}
|
||||||
onNumberChange={this.handleNumberChange}
|
onNumberChange={this.handleNumberChange}
|
||||||
@@ -546,39 +674,15 @@ class LieferungNeuPage extends PureComponent {
|
|||||||
data={pdfDialogData}
|
data={pdfDialogData}
|
||||||
onClose={this.handleClosePdfDialog}
|
onClose={this.handleClosePdfDialog}
|
||||||
/>
|
/>
|
||||||
|
<TextEditorDialog
|
||||||
|
open={this.state.textEditorOpen}
|
||||||
|
attachment={this.state.textEditorAttachment}
|
||||||
|
onClose={this.handleCloseTextEditor}
|
||||||
|
onSave={this.handleSaveTextEditor}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const mapStateToProps = (state) => ({
|
export default withNavigate(LieferungNeuPage);
|
||||||
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));
|
|
||||||
|
|||||||
@@ -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;
|
|
||||||
@@ -5,7 +5,6 @@ import categoryReducer from './categorySlice';
|
|||||||
import companyReducer, { companyLoadListener } from './companySlice';
|
import companyReducer, { companyLoadListener } from './companySlice';
|
||||||
import posterminalReducer, { posterminalLoadListener } from './posterminalSlice';
|
import posterminalReducer, { posterminalLoadListener } from './posterminalSlice';
|
||||||
import unitReducer, { unitLoadListener } from './unitSlice';
|
import unitReducer, { unitLoadListener } from './unitSlice';
|
||||||
import deliveryEditorReducer from './deliveryEditorSlice';
|
|
||||||
|
|
||||||
const listenerMiddleware = createListenerMiddleware();
|
const listenerMiddleware = createListenerMiddleware();
|
||||||
companyLoadListener(listenerMiddleware);
|
companyLoadListener(listenerMiddleware);
|
||||||
@@ -53,7 +52,6 @@ export const store = configureStore({
|
|||||||
brand: brandReducer,
|
brand: brandReducer,
|
||||||
posterminal: posterminalReducer,
|
posterminal: posterminalReducer,
|
||||||
unit: unitReducer,
|
unit: unitReducer,
|
||||||
deliveryEditor: deliveryEditorReducer,
|
|
||||||
},
|
},
|
||||||
middleware: (getDefaultMiddleware) =>
|
middleware: (getDefaultMiddleware) =>
|
||||||
getDefaultMiddleware()
|
getDefaultMiddleware()
|
||||||
|
|||||||
7
src/utils/pdfText.js
Normal file
7
src/utils/pdfText.js
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { pdfToText } from 'pdftotext-wasm';
|
||||||
|
|
||||||
|
export async function extractPdfText(pdfSource, options = { layout: true }) {
|
||||||
|
return pdfToText(pdfSource, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { pdfToText };
|
||||||
36
src/utils/pdfThumbnail.js
Normal file
36
src/utils/pdfThumbnail.js
Normal file
@@ -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);
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import fs from 'node:fs';
|
import fs from 'node:fs';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import { fileURLToPath } from 'node:url';
|
import { fileURLToPath } from 'node:url';
|
||||||
import { defineConfig } from 'vite';
|
import { defineConfig, searchForWorkspaceRoot } from 'vite';
|
||||||
import react from '@vitejs/plugin-react';
|
import react from '@vitejs/plugin-react';
|
||||||
import { VitePWA } from 'vite-plugin-pwa';
|
import { VitePWA } from 'vite-plugin-pwa';
|
||||||
|
|
||||||
@@ -90,6 +90,12 @@ const sharedServer = {
|
|||||||
port: useHttps ? 4308 : 4209,
|
port: useHttps ? 4308 : 4209,
|
||||||
https: httpsOptions(),
|
https: httpsOptions(),
|
||||||
allowedHosts: true,
|
allowedHosts: true,
|
||||||
|
fs: {
|
||||||
|
allow: [
|
||||||
|
searchForWorkspaceRoot(process.cwd()),
|
||||||
|
path.resolve(__dirname, '..'),
|
||||||
|
],
|
||||||
|
},
|
||||||
proxy: {
|
proxy: {
|
||||||
'/api': {
|
'/api': {
|
||||||
target: 'http://localhost:4208',
|
target: 'http://localhost:4208',
|
||||||
|
|||||||
Reference in New Issue
Block a user