From 27f6dfdebfa58c0760636bd7ef7417427599a5d2 Mon Sep 17 00:00:00 2001 From: seb Date: Fri, 18 Sep 2026 01:49:59 +0200 Subject: [PATCH] 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 --- package-lock.json | 306 +++++++++++++++++++ package.json | 5 +- src/automats/deliveryEditorAutomat.js | 257 ++++++++++++++++ src/automats/index.js | 1 + src/components/manifest/Attachment.jsx | 243 +++++++++++++++ src/components/manifest/AttachmentList.jsx | 41 +++ src/components/manifest/ManifestHeader.jsx | 13 + src/components/manifest/TextEditorDialog.jsx | 250 +++++++++++++++ src/components/manifest/index.js | 3 + src/pages/lieferung/neu.jsx | 298 ++++++++++++------ src/store/deliveryEditorSlice.js | 145 --------- src/store/index.js | 2 - src/utils/pdfText.js | 7 + src/utils/pdfThumbnail.js | 36 +++ vite.config.js | 8 +- 15 files changed, 1369 insertions(+), 246 deletions(-) create mode 100644 src/automats/deliveryEditorAutomat.js create mode 100644 src/automats/index.js create mode 100644 src/components/manifest/Attachment.jsx create mode 100644 src/components/manifest/AttachmentList.jsx create mode 100644 src/components/manifest/TextEditorDialog.jsx delete mode 100644 src/store/deliveryEditorSlice.js create mode 100644 src/utils/pdfText.js create mode 100644 src/utils/pdfThumbnail.js 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 && ( + + )} + + + + + + + + + + + + + + + {!readOnly && ( + + )} + + + + ); + } +} + +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',