file api
This commit is contained in:
183
.agents/skills/automat/SKILL.md
Normal file
183
.agents/skills/automat/SKILL.md
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
---
|
||||||
|
name: automat
|
||||||
|
description: >-
|
||||||
|
Lightweight out-of-tree state management for React PureComponent with zero hooks, HOCs, or Context Providers.
|
||||||
|
Use this skill whenever creating, modifying, or consuming Automat stores in React components, setting up
|
||||||
|
subscriptions or fine-grained selectors, configuring cascading invalidations, handling binary blobs/fetchers, or streaming via SSE.
|
||||||
|
---
|
||||||
|
|
||||||
|
# ⚙ react-automat
|
||||||
|
|
||||||
|
> Zero-dependency higher-order state container for React `PureComponent`. State lives in module scope (independent of mounting). Components read state synchronously in `constructor` and subscribe in `componentDidMount`. No Context, hooks, or HOCs.
|
||||||
|
|
||||||
|
[](https://www.npmjs.com/package/react-automat)
|
||||||
|
[](https://bundlephobia.com/package/react-automat)
|
||||||
|
[](https://sebgreen.net/automat/)
|
||||||
|
[](LICENSE)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install react-automat
|
||||||
|
# or: pnpm add react-automat | yarn add react-automat | bun add react-automat
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Complete Type Signatures
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import type { PureComponent } from 'react';
|
||||||
|
|
||||||
|
export interface AutomatOptions<T> {
|
||||||
|
name?: string | null; // window.__AUTOMATS__ key & Automat.get(name)
|
||||||
|
persist?: boolean; // false = window (default), true = IndexedDB
|
||||||
|
url?: string | null; // Auto-triggers fetcher(url) on init
|
||||||
|
fetcher?: (url: string, automat?: Automat<T, any>) => Promise<T>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Selector<T, S = any> = keyof T | (keyof T)[] | ((state: T) => S | null | undefined);
|
||||||
|
|
||||||
|
export interface ScopedSlice<S> {
|
||||||
|
readonly state: S;
|
||||||
|
subscribe(target: PureComponent | ((slice: S) => void)): () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BlobResource {
|
||||||
|
blob: Blob; url: string; size: number; type: string; loadedAt: string;
|
||||||
|
revoke: () => void; // Auto-called on setDirty() / dispose() to release memory
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SSEOptions<T = any, A = any> {
|
||||||
|
onMessage?: (data: any, event: string, automat: Automat<T, A>) => void;
|
||||||
|
events?: Record<string, (data: any, automat: Automat<T, A>) => void>;
|
||||||
|
onError?: (err: any, automat: Automat<T, A>) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Automat<T = any, A = Record<string, Function>> {
|
||||||
|
constructor(initialState?: T | null, actions?: A, options?: AutomatOptions<T>);
|
||||||
|
|
||||||
|
readonly state: T;
|
||||||
|
getState(): T;
|
||||||
|
setState(partial: Partial<T> | ((prev: T) => Partial<T>)): T;
|
||||||
|
readonly actions: A;
|
||||||
|
|
||||||
|
readonly isReady: boolean;
|
||||||
|
readonly isDirty: boolean;
|
||||||
|
readonly ready: Promise<T>; // Re-awaitable after setDirty()
|
||||||
|
readonly name: string | null;
|
||||||
|
readonly persist: boolean;
|
||||||
|
readonly url: string | null;
|
||||||
|
readonly subscriberCount: number;
|
||||||
|
|
||||||
|
subscribe(target: PureComponent | ((state: T, partial?: Partial<T>) => void), selector?: Selector<T>): () => void;
|
||||||
|
select<S = any>(selector: Selector<T, S>): ScopedSlice<S>;
|
||||||
|
unsubscribe(target: any): void;
|
||||||
|
|
||||||
|
subscribeTo<U = any>(
|
||||||
|
upstream: Automat<U, any>,
|
||||||
|
transform: (upstreamState: U, currentState: T) => Partial<T> | null | undefined,
|
||||||
|
options?: { cascadeDirty?: boolean | ((upstream: U, current: T) => boolean) }
|
||||||
|
): this;
|
||||||
|
|
||||||
|
invalidateWith<U = any>(upstream: Automat<U, any>, filterFn?: (upstream: U, current: T) => boolean): this;
|
||||||
|
onDirty(fn: (automat: this) => void): () => void;
|
||||||
|
|
||||||
|
setDirty(): Promise<T>; // Resets state to null/default, revokes blobs, cascades, defers if unmounted
|
||||||
|
reload(): Promise<T>; // Silent background re-fetch; preserves state & isReady=true
|
||||||
|
refresh(): Promise<T>; // Alias for reload()
|
||||||
|
read(): T; // For Suspense: throws ready promise if unready; triggers deferred load if dirty
|
||||||
|
|
||||||
|
clearPersistence(): Promise<void>;
|
||||||
|
dispose(): void; // Unsubs all, revokes blobs, deletes from window registry
|
||||||
|
|
||||||
|
static get<T = any, A = any>(name: string): Automat<T, A> | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetchers & SSE
|
||||||
|
export function fetchJson<T = any>(url: string, options?: RequestInit): Promise<T>;
|
||||||
|
export function fetchBlob(url: string, options?: RequestInit): Promise<BlobResource>;
|
||||||
|
export const fetchImageBlob: typeof fetchBlob;
|
||||||
|
export const fetchPdfBlob: typeof fetchBlob;
|
||||||
|
export function connectSSE<T, A>(automat: Automat<T, A>, url: string, options?: SSEOptions<T, A>): { eventSource: EventSource | null; close: () => void };
|
||||||
|
export function createSSEFetcher(sseUrl: string, baseFetcher?: Function, sseOptions?: SSEOptions): (url: string, automat?: any) => Promise<any>;
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2. Canonical Usage Patterns
|
||||||
|
|
||||||
|
### A. PureComponent Subscription
|
||||||
|
```jsx
|
||||||
|
import { PureComponent } from 'react';
|
||||||
|
import { Automat } from 'react-automat';
|
||||||
|
|
||||||
|
const store = new Automat({ count: 0 }, { inc: () => store.setState({ count: store.state.count + 1 }) });
|
||||||
|
|
||||||
|
class Counter extends PureComponent {
|
||||||
|
constructor(props) { super(props); this.state = store.state; } // Sync read, never stale
|
||||||
|
componentDidMount() { this.unsub = store.subscribe(this); } // Auto-calls this.setState(partial)
|
||||||
|
componentWillUnmount() { this.unsub(); }
|
||||||
|
render() { return <button onClick={store.actions.inc}>{this.state.count}</button>; }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### B. Fine-Grained Selectors (Shallow-guarded, 0 renders if slice unchanged)
|
||||||
|
```js
|
||||||
|
store.subscribe(this, 'count'); // Single key
|
||||||
|
store.subscribe(this, ['theme', 'locale']); // Multi key
|
||||||
|
store.subscribe(this, (s) => ({ items: s.list.length }));// Projection function
|
||||||
|
```
|
||||||
|
|
||||||
|
### C. Unconnected Component via Window (Zero imports of store file)
|
||||||
|
```js
|
||||||
|
const store = Automat.get('cart'); // or window.__AUTOMATS__?.get('cart')
|
||||||
|
this.state = { count: store?.state?.items?.length || 0 };
|
||||||
|
this.unsub = store?.subscribe(this, (s) => ({ count: s.items.length }));
|
||||||
|
```
|
||||||
|
|
||||||
|
### D. Backend API: React Suspense vs Standard
|
||||||
|
```jsx
|
||||||
|
const user = new Automat(null, {}, { url: '/api/user' });
|
||||||
|
|
||||||
|
// Suspense: throws promise while unready
|
||||||
|
const Profile = () => <div>{user.read().name}</div>;
|
||||||
|
|
||||||
|
// Standard: check isReady
|
||||||
|
class ProfileStandard extends PureComponent {
|
||||||
|
constructor(props) { super(props); this.state = { u: user.state }; }
|
||||||
|
componentDidMount() { this.unsub = user.subscribe(this, (u) => ({ u })); }
|
||||||
|
componentWillUnmount() { this.unsub(); }
|
||||||
|
render() { return !user.isReady ? <Spinner /> : <div>{this.state.u.name}</div>; }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### E. Cascading Invalidation, Blobs & Memory Freeing
|
||||||
|
```js
|
||||||
|
const all = new Automat(null, {}, { name: 'photos/all', url: '/api/photos' });
|
||||||
|
const p1 = new Automat(null, {}, { name: 'photo/1', url: '/api/photos/1', fetcher: fetchImageBlob });
|
||||||
|
p1.invalidateWith(all); // p1 cascades setDirty() when all.setDirty() is called
|
||||||
|
|
||||||
|
all.setDirty(); // p1 auto-revokes held Blob URL, resets state=null, defers reload if unmounted
|
||||||
|
```
|
||||||
|
|
||||||
|
### F. SSE Remote Trigger
|
||||||
|
```js
|
||||||
|
const live = new Automat({}, {
|
||||||
|
onSSETrigger(data, event, store) {
|
||||||
|
if (event === 'invalidate') store.setDirty();
|
||||||
|
if (event === 'reload') store.reload();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
connectSSE(live, '/api/stream');
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. Decision Matrix: `setDirty()` vs `reload()`
|
||||||
|
|
||||||
|
| Method | `isReady` | State during fetch | Suspense | Unmounted | Use Case |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| `setDirty()` | `false` | Resets to `null`/init | Throws fallback | Deferred until mount/read | Cache bust, logout, hard reset |
|
||||||
|
| `reload()` | `true` | Preserved | Never suspends | Immediate fetch | Polling, pull-to-refresh, sync |
|
||||||
|
|
||||||
|
## 4. Invariant Rules
|
||||||
|
- Read `store.state` synchronously in `constructor(props)` — never in render bodies.
|
||||||
|
- Subscribe in `componentDidMount()` & store the returned `unsub`; unsubscribe in `componentWillUnmount()`.
|
||||||
|
- Never mutate `store.state` directly (`store.state.x = 1` ❌); use `store.setState({ x })` or actions.
|
||||||
|
- Pass selectors to `subscribe(this, selector)` on multi-key stores to skip redundant renders.
|
||||||
|
- Never wrap components in Context Providers, HOCs, or custom hooks.
|
||||||
248
package-lock.json
generated
248
package-lock.json
generated
@@ -16,12 +16,12 @@
|
|||||||
"@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",
|
"pdfjs-dist": "^6.3.289",
|
||||||
"pdftotext-wasm": "file:../pdftotext-wasm",
|
"pdftotext-wasm": "file:../pdftotext-wasm",
|
||||||
"react": "^19.3.0",
|
"react": "^19.3.0",
|
||||||
|
"react-automat": "^0.1.0",
|
||||||
"react-dom": "^19.3.0",
|
"react-dom": "^19.3.0",
|
||||||
"react-redux": "^9.3.0",
|
"react-redux": "^9.3.0",
|
||||||
"react-router-dom": "^7.18.4",
|
"react-router-dom": "^7.18.4",
|
||||||
@@ -35,17 +35,6 @@
|
|||||||
"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": {
|
||||||
@@ -442,9 +431,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/parser": {
|
"node_modules/@babel/parser": {
|
||||||
"version": "7.29.8",
|
"version": "7.29.9",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.8.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.9.tgz",
|
||||||
"integrity": "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==",
|
"integrity": "sha512-CjXrNHTnvqBVqHgdBysY3vk2T8tpJHb5/RMeHJBTyVa9xgugCB0CJTx/3oO8RV2QRQP391RWpB7D6hLjm8V9uA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/types": "^7.29.8"
|
"@babel/types": "^7.29.8"
|
||||||
@@ -2377,9 +2366,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@oxc-project/types": {
|
"node_modules/@oxc-project/types": {
|
||||||
"version": "0.149.0",
|
"version": "0.150.0",
|
||||||
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.149.0.tgz",
|
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.150.0.tgz",
|
||||||
"integrity": "sha512-Efcc+iF0j3Bf67YjEqIqWXbX5XddXoK/Mw4K1/JuXwRCZ8N16VR7iT23nlCc9XrveFVh/E5Rqs2StT0V8v9LdA==",
|
"integrity": "sha512-rDS5/31E9HfPl/CIzGrn0DOlvBbXFseQ5URJ9sYMfstbKLD/c6Gm9vmRzRGDdAXyOIL4zmO37lc9RIwYqVruZw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"funding": {
|
"funding": {
|
||||||
@@ -2423,9 +2412,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-android-arm-eabi": {
|
"node_modules/@rolldown/binding-android-arm-eabi": {
|
||||||
"version": "1.2.8",
|
"version": "1.2.9",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.8.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.9.tgz",
|
||||||
"integrity": "sha512-tN5aztYkKCte4i5SIrrz5yK/HMjEuCqCSCJa418jOV8tZ1cBY3YF2otxB1ktPxzsLA1BeTqwapK0bfjxNvHJVw==",
|
"integrity": "sha512-tNISae1QEf/vkb3xkRcjV5SEdzPE97We5IVaa2Z8jSszQPZ8U60B/YCYpw4QI7VidYsBtKavczXf+DyDs9WGxw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
@@ -2440,9 +2429,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-android-arm64": {
|
"node_modules/@rolldown/binding-android-arm64": {
|
||||||
"version": "1.2.8",
|
"version": "1.2.9",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.8.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.9.tgz",
|
||||||
"integrity": "sha512-dIYTWl9XprMUiQFoc55KUyk/oS8SKYH3zFl0LTR7RT0Xj4hgSVyuJcroH8JUu8RcpF8fTB6E0aOwCkZoYPcDSQ==",
|
"integrity": "sha512-YC8YsI30o606GTZi0VyzYlsDKFP8W61i/QzayHDkLbNEz/IShqAmTa+hsJRj13xTHA0H+6fk4b2UmGn+Q/cMlg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -2457,9 +2446,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-darwin-arm64": {
|
"node_modules/@rolldown/binding-darwin-arm64": {
|
||||||
"version": "1.2.8",
|
"version": "1.2.9",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.8.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.9.tgz",
|
||||||
"integrity": "sha512-PCSDQGXD2IyTEFrcgPyBM8jJuGmrbCMuoIOXdbEGVemruKACXoLQJrb+A45Z0L5t1RQkdfJprAYPkikbh7dzdA==",
|
"integrity": "sha512-IwhlH3qK5urrY8hZiEgGkHKEFN901p/p2bjxCxJlr4GyNnF7wYpUvK+Y43uaRYuC4hpfjzbR3SJC3arX1jGvmw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -2474,9 +2463,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-darwin-x64": {
|
"node_modules/@rolldown/binding-darwin-x64": {
|
||||||
"version": "1.2.8",
|
"version": "1.2.9",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.8.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.9.tgz",
|
||||||
"integrity": "sha512-Uk7lRsGhPFHVX/sAUC6D5H9Ol30dFHd6iquokll2th3LpdJ3F5CzQB+7DHn0Ri2mG+U7k2zXiPHDrwZenXhwSA==",
|
"integrity": "sha512-XxpJfVzFh+jilRxIXUqcfYAYcunIc/XEzIizsOL1fcJee5Sf7H3mH8WlLmfHfluz5amqR88QQo9izKtmMlavAw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -2491,9 +2480,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-freebsd-x64": {
|
"node_modules/@rolldown/binding-freebsd-x64": {
|
||||||
"version": "1.2.8",
|
"version": "1.2.9",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.8.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.9.tgz",
|
||||||
"integrity": "sha512-DjszaTEVogPqA5bYzsEeqDCQxbcp2fexQwKcRspYji2yzR68fCf+e4fx6kBSRDwX5/brZaHw/hWS9+A/+/w9sQ==",
|
"integrity": "sha512-kSfvhmgeWyfkbT3p/1s5vSgboogoah2zkm9fX2zjg2hHxSV7T4KhMWRUUaRk4OXNqoD3QAUeRqLcs1aZOK4U1g==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -2508,9 +2497,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-linux-arm-gnueabihf": {
|
"node_modules/@rolldown/binding-linux-arm-gnueabihf": {
|
||||||
"version": "1.2.8",
|
"version": "1.2.9",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.8.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.9.tgz",
|
||||||
"integrity": "sha512-zmwa7FTmdzB6aaEEuuls18H6Ap5JmJPSoPTuXixeJZV6tG40SyLkApQtz1g8ptZtiEKqj9OM0oNLPh1AgvE31Q==",
|
"integrity": "sha512-1RVzG17pxqbTfYLC352JlLt6kKLG+6Hr30n8DlIJqsnV5luUDd2Qdx9Ayw1Cabfyb1K9k0jXEZ7evxkRoT+uiw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
@@ -2525,9 +2514,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-linux-arm64-gnu": {
|
"node_modules/@rolldown/binding-linux-arm64-gnu": {
|
||||||
"version": "1.2.8",
|
"version": "1.2.9",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.8.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.9.tgz",
|
||||||
"integrity": "sha512-KdYQDPHwJVnbFwdTGMgxsI9SqblBlz6STGM+w1We/d5B8OWWidYH0MwkU/uA1wM5fIpO2MkOVxXrNzzuZhw9ew==",
|
"integrity": "sha512-BXqPvZ2drqVD+/Z8UpKwcs4Mp7grM+eGFku4CAEKrEtcbAsUpzREphK1sogCRZGreVPiMkiiBtw0n3TPteuqvw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -2545,9 +2534,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-linux-arm64-musl": {
|
"node_modules/@rolldown/binding-linux-arm64-musl": {
|
||||||
"version": "1.2.8",
|
"version": "1.2.9",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.8.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.9.tgz",
|
||||||
"integrity": "sha512-jFJTifHnNPY+yzOoNZQfSIysrVyXzEQPhPnOUjmD1bcQGHH6s7c8cViKWar8YplQImE5N9JRqMCLrM2CdxOrZA==",
|
"integrity": "sha512-11vWvo8YDwLzukt27J3aYDWU+gg2P7J+ZOmiJ0hkF5BXZDW7pVya7r40MXDy6ya0i9KamoENSVKIugvJNgFXIA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -2565,9 +2554,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-linux-ppc64-gnu": {
|
"node_modules/@rolldown/binding-linux-ppc64-gnu": {
|
||||||
"version": "1.2.8",
|
"version": "1.2.9",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.8.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.9.tgz",
|
||||||
"integrity": "sha512-FhiOziBDWPBjbcmRzfLyIJnaP7AVMFXT7YCXPjXxj7wKU3vx24RjrCNN/zjvVa+N2vVoHJwCoUBvsrN/DG3zIA==",
|
"integrity": "sha512-a1tijMkdwsIARtc0F39ApURROkf3NwqinI6TOiSSWCTR7dT96dffNvMUtDHnq64wKNTIZOIlzKrFvvFUznJiyw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"ppc64"
|
"ppc64"
|
||||||
],
|
],
|
||||||
@@ -2585,9 +2574,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-linux-s390x-gnu": {
|
"node_modules/@rolldown/binding-linux-s390x-gnu": {
|
||||||
"version": "1.2.8",
|
"version": "1.2.9",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.8.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.9.tgz",
|
||||||
"integrity": "sha512-WnHfADMzOV2Y55wlx1hzzQnar/wDt/VdvWSD99r18Mz9ylNieIGOkRx3UV21h7m/eJvjySYJkO26VvGNFkwsIQ==",
|
"integrity": "sha512-x6SQNdAvv4c3hWqTMaWuawzMX9myaCs/yEmlGsxJzkdClnHW7FbrjQuSiRDhuSYzEYoEMhsaJy9qHG/XNemJPQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"s390x"
|
"s390x"
|
||||||
],
|
],
|
||||||
@@ -2605,9 +2594,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-linux-x64-gnu": {
|
"node_modules/@rolldown/binding-linux-x64-gnu": {
|
||||||
"version": "1.2.8",
|
"version": "1.2.9",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.8.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.9.tgz",
|
||||||
"integrity": "sha512-H9tRr5ibfXFVLxbPOseVewewFpl28zcEdjRDt2FTUZU7odxP0gEv1ki4/kGmcGOh78oRwZuuQllGLZ9zTJp84g==",
|
"integrity": "sha512-9s0AZ8BFK5/n7B/TBoa2yJE3gI3KURrbXcPBlsAsvjU4VeJKgE90y1YtNxyEUIcHPQkg6/yfF3qihUrcM/Kf0Q==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -2625,9 +2614,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-linux-x64-musl": {
|
"node_modules/@rolldown/binding-linux-x64-musl": {
|
||||||
"version": "1.2.8",
|
"version": "1.2.9",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.8.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.9.tgz",
|
||||||
"integrity": "sha512-UefiqfM3D6IVNlZ8tSGs9+Ejjud2T+oxO0IHADU45Y+lyEjD2dVFyZHbkfX0LUb5Zugo/oIv1eCO/KVYhgYJYA==",
|
"integrity": "sha512-P7VWAmV+WdJluH7ovnRGoiv2i8To7GAZ+kGzfGup635cyL7SyYl3lSUaA3Gp5THf0n/Co5EyEqb2zbqq+nMOHQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -2645,9 +2634,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-openharmony-arm64": {
|
"node_modules/@rolldown/binding-openharmony-arm64": {
|
||||||
"version": "1.2.8",
|
"version": "1.2.9",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.8.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.9.tgz",
|
||||||
"integrity": "sha512-637Ke4kWSy6rp9cxQ9gMOXlxPgIw/c1beASV4M//3+9I4uwBVOOl74G+e3zyU3u19U7RkRl/HuewixZ/Z6+Rjg==",
|
"integrity": "sha512-1qixtsE4BK8h+yS3BfmZ09UhA7O/N4IACva6YBr7EBvCJraByTuRcgOTaiA62Tm0vey3UcKXLOaoGHtYmNGEVg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -2662,9 +2651,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-win32-arm64-msvc": {
|
"node_modules/@rolldown/binding-win32-arm64-msvc": {
|
||||||
"version": "1.2.8",
|
"version": "1.2.9",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.8.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.9.tgz",
|
||||||
"integrity": "sha512-xWBkPOF1Q9k/Gv1nQXnVdLxKu74jXppuOM4Z3mnypVUJJJwLsMl7hNJGRAUJoG8A5MgOI1ACKM+wBFxSJzKy4A==",
|
"integrity": "sha512-ok8IQjcEPs1AKZfuEUznVBrJw+gK4soq+bx8b1X2XoMqVClarc1q5JDmVtWXY1xfr6ZuHTAsPXHTgTrqKTZeww==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -2679,9 +2668,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-win32-x64-msvc": {
|
"node_modules/@rolldown/binding-win32-x64-msvc": {
|
||||||
"version": "1.2.8",
|
"version": "1.2.9",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.8.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.9.tgz",
|
||||||
"integrity": "sha512-uz2ZvfgXbxqNwijjjbxrnvALwpyODDcgc1T1N8N3rf/DXKQmaFwmB4LX4yyjggpwN2obdQLb2rgirX5ffCWYng==",
|
"integrity": "sha512-Ip2mXoU0hM0boq3Rf+ekuT653OROSo6aSYcPT1VHE4q52KvyxgFkQgrgb/IEsxOuvQ2fZZbs8khJAyCEPM24/g==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -3455,10 +3444,6 @@
|
|||||||
"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",
|
||||||
@@ -3543,9 +3528,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/baseline-browser-mapping": {
|
"node_modules/baseline-browser-mapping": {
|
||||||
"version": "2.11.23",
|
"version": "2.11.25",
|
||||||
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.23.tgz",
|
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.25.tgz",
|
||||||
"integrity": "sha512-le521dGVfxM7yRX0EikCoSz+rOK+hHzdDt/E7mG1jOJB/6WAAUuwVroLwaB7ApaUsz5Q0kFlDXLSA9MheUIfRQ==",
|
"integrity": "sha512-gMmEShwwq7FJqMwvfRwvCl00v4kN+KOfJqXn+f4nrufak5gNHJOksd/60Dvjuz7sI8Y5WiSFBa8FEYr+zoyqCw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"bin": {
|
"bin": {
|
||||||
@@ -3569,9 +3554,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/browserslist": {
|
"node_modules/browserslist": {
|
||||||
"version": "4.28.9",
|
"version": "4.29.0",
|
||||||
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.9.tgz",
|
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.29.0.tgz",
|
||||||
"integrity": "sha512-EWazOblFYUvlGZcfGhPUPmYh3nikUxBVb+y9MJun5f3hBi812X+8MSQTujLBtgK3cf51fJWbWfOjyeO954d+Eg==",
|
"integrity": "sha512-3GSvyjvDI4Dur1Meg2BekJquu5uF+9R9a1+5M1Mde192eZoXbeXjzgOsgqPS2V8D5wrrip0gR5Hf/GhWQ9ZzaA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@@ -3589,11 +3574,11 @@
|
|||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"baseline-browser-mapping": "^2.11.20",
|
"baseline-browser-mapping": "^2.11.23",
|
||||||
"caniuse-lite": "^1.0.30001810",
|
"caniuse-lite": "^1.0.30001810",
|
||||||
"electron-to-chromium": "^1.5.420",
|
"electron-to-chromium": "^1.5.427",
|
||||||
"node-releases": "^2.0.54",
|
"node-releases": "^2.0.55",
|
||||||
"update-browserslist-db": "^1.3.2"
|
"update-browserslist-db": "^1.3.3"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"browserslist": "cli.js"
|
"browserslist": "cli.js"
|
||||||
@@ -4039,9 +4024,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/electron-to-chromium": {
|
"node_modules/electron-to-chromium": {
|
||||||
"version": "1.5.428",
|
"version": "1.5.433",
|
||||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.428.tgz",
|
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.433.tgz",
|
||||||
"integrity": "sha512-1JxbaFJj1bRKurj1uY3l4xxpU9kOUAUjcIgApj0qu1Pao5GhoIWI8iL0BeMYJ2njig1hBx0A7eKD9VGjH9wlHw==",
|
"integrity": "sha512-5lCAbyZBjtmUt/RAGHRqrL2q0oEFRThDAsZHHDn9XHa89Qw7gMYOeSicBTy+AHfvo0r6vwsZvqNJTQIQy1BLzA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
@@ -4285,9 +4270,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/fast-uri": {
|
"node_modules/fast-uri": {
|
||||||
"version": "3.1.7",
|
"version": "3.1.8",
|
||||||
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.7.tgz",
|
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.8.tgz",
|
||||||
"integrity": "sha512-dOvZVzjdZdz7phd9v6jCbwxrBW3fK6n8Rc0CtdmM4bumzMnxywBYhuph6J819RRw/ku+rLbelwfMunktuzVVHg==",
|
"integrity": "sha512-GZMtZUTNRpOVIECoXwLNZS5xUGE+mVNbTB8h/7Rwh2TFWcBQiPzTgyZi05BF9UMZKkLJv8XBRJTlU7zg8+ZfMg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@@ -4888,12 +4873,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/is-core-module": {
|
"node_modules/is-core-module": {
|
||||||
"version": "2.16.2",
|
"version": "2.17.0",
|
||||||
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz",
|
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.17.0.tgz",
|
||||||
"integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==",
|
"integrity": "sha512-J/vG0zBCbIKOQFfufSwyXdMrsohyJIUNkrnmo6WZGzoM7tr/lsbfW5b2BvisL6zsyMzK9UxV9L6c7AoFbyXHOA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"hasown": "^2.0.3"
|
"hasown": "^2.0.4"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
@@ -5745,9 +5730,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/node-releases": {
|
"node_modules/node-releases": {
|
||||||
"version": "2.0.55",
|
"version": "2.0.56",
|
||||||
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.55.tgz",
|
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.56.tgz",
|
||||||
"integrity": "sha512-mIrE/Cw9y+9Au6dS5vDKDhQza9YvG6w+ZrS6X+ZzA7yFW/soAeaups4Qzn1bL6g5FVy8WtP79+0j82oPIbqRjQ==",
|
"integrity": "sha512-x0InOIyzgdk+eyaWaRJFH5snEtiImgBgblZ2CyPrLmqqcuMQkEvcDPHbzqbD8eDsSeJbVOjn+crzyzHaM4D+/A==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -5977,9 +5962,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/path-scurry/node_modules/lru-cache": {
|
"node_modules/path-scurry/node_modules/lru-cache": {
|
||||||
"version": "11.5.2",
|
"version": "11.5.3",
|
||||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz",
|
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.3.tgz",
|
||||||
"integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==",
|
"integrity": "sha512-U4N8FgzmWxc8k1VH8Kr6lQg18U7Fjvby6wXHVRX/ZZ7IwWbRMgrRbP0Wrb5q5NVinryp4SQampHKdvtecItxUg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "BlueOak-1.0.0",
|
"license": "BlueOak-1.0.0",
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -6136,6 +6121,20 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-automat": {
|
||||||
|
"version": "0.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-automat/-/react-automat-0.1.0.tgz",
|
||||||
|
"integrity": "sha512-/lGKX2gXyBo52rRc6qC0ZGeThuD2C/LNjJWCChA/WC3JCJ4anyx7xCREfNmMOgRUBE0k4+LF0EbvzJwb4/RxfQ==",
|
||||||
|
"license": "0BSD",
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=16.8.0 || >=17.0.0 || >=18.0.0 || >=19.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react-dom": {
|
"node_modules/react-dom": {
|
||||||
"version": "19.3.0",
|
"version": "19.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.3.0.tgz",
|
||||||
@@ -6336,9 +6335,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/regjsparser": {
|
"node_modules/regjsparser": {
|
||||||
"version": "0.13.2",
|
"version": "0.13.3",
|
||||||
"resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.13.2.tgz",
|
"resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.13.3.tgz",
|
||||||
"integrity": "sha512-NgRBy2Nx/bE+9F27nVHnqcN5HjyLmecqsqx2PJHu3/IEtADD4WuxuXIVExD5PoSDFVrl78dOonfcOe5O+5nbzQ==",
|
"integrity": "sha512-ycwFAS14Jw4mppvmK4GR/J6u3WpWpjkEApehuHtLc/8VpPNpDMbQ4WjqwplXifGeyKOzHSFLmSPqzksDQE2Sfg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "BSD-2-Clause",
|
"license": "BSD-2-Clause",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -6412,13 +6411,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/rolldown": {
|
"node_modules/rolldown": {
|
||||||
"version": "1.2.8",
|
"version": "1.2.9",
|
||||||
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.8.tgz",
|
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.9.tgz",
|
||||||
"integrity": "sha512-Z67nTmhZe7anqnM/EjI392w5i/ANUinjip7QYsOyN37oayduxt3ksdX0hf5OOamkAd53BiIHfbfSzfUmzKFQqQ==",
|
"integrity": "sha512-hx/Pv0N1haXRb11qkfnK5MXB/iqr7i0yjWQqmO9uHqZpBgQSqzc8UsSnEpalsh+j1I8qQ2CkXAkJC8Br3dKSlg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@oxc-project/types": "=0.149.0",
|
"@oxc-project/types": "=0.150.0",
|
||||||
"@rolldown/pluginutils": "^1.0.0"
|
"@rolldown/pluginutils": "^1.0.0"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
@@ -6428,21 +6427,21 @@
|
|||||||
"node": "^20.19.0 || >=22.12.0"
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@rolldown/binding-android-arm-eabi": "1.2.8",
|
"@rolldown/binding-android-arm-eabi": "1.2.9",
|
||||||
"@rolldown/binding-android-arm64": "1.2.8",
|
"@rolldown/binding-android-arm64": "1.2.9",
|
||||||
"@rolldown/binding-darwin-arm64": "1.2.8",
|
"@rolldown/binding-darwin-arm64": "1.2.9",
|
||||||
"@rolldown/binding-darwin-x64": "1.2.8",
|
"@rolldown/binding-darwin-x64": "1.2.9",
|
||||||
"@rolldown/binding-freebsd-x64": "1.2.8",
|
"@rolldown/binding-freebsd-x64": "1.2.9",
|
||||||
"@rolldown/binding-linux-arm-gnueabihf": "1.2.8",
|
"@rolldown/binding-linux-arm-gnueabihf": "1.2.9",
|
||||||
"@rolldown/binding-linux-arm64-gnu": "1.2.8",
|
"@rolldown/binding-linux-arm64-gnu": "1.2.9",
|
||||||
"@rolldown/binding-linux-arm64-musl": "1.2.8",
|
"@rolldown/binding-linux-arm64-musl": "1.2.9",
|
||||||
"@rolldown/binding-linux-ppc64-gnu": "1.2.8",
|
"@rolldown/binding-linux-ppc64-gnu": "1.2.9",
|
||||||
"@rolldown/binding-linux-s390x-gnu": "1.2.8",
|
"@rolldown/binding-linux-s390x-gnu": "1.2.9",
|
||||||
"@rolldown/binding-linux-x64-gnu": "1.2.8",
|
"@rolldown/binding-linux-x64-gnu": "1.2.9",
|
||||||
"@rolldown/binding-linux-x64-musl": "1.2.8",
|
"@rolldown/binding-linux-x64-musl": "1.2.9",
|
||||||
"@rolldown/binding-openharmony-arm64": "1.2.8",
|
"@rolldown/binding-openharmony-arm64": "1.2.9",
|
||||||
"@rolldown/binding-win32-arm64-msvc": "1.2.8",
|
"@rolldown/binding-win32-arm64-msvc": "1.2.9",
|
||||||
"@rolldown/binding-win32-x64-msvc": "1.2.8"
|
"@rolldown/binding-win32-x64-msvc": "1.2.9"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/rollup": {
|
"node_modules/rollup": {
|
||||||
@@ -7095,19 +7094,18 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/typed-array-byte-offset": {
|
"node_modules/typed-array-byte-offset": {
|
||||||
"version": "1.0.4",
|
"version": "1.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.5.tgz",
|
||||||
"integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==",
|
"integrity": "sha512-0FHJvLPqZ7KJzp17O13jfsAjsqazgrxBu2zEK95PmUz8lv2+GjRuxUInCr2Rk9Dms3ihN21zJ929ZO43yJ95QQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"available-typed-arrays": "^1.0.7",
|
"available-typed-arrays": "^1.0.7",
|
||||||
"call-bind": "^1.0.8",
|
"call-bind": "^1.0.9",
|
||||||
"for-each": "^0.3.3",
|
"for-each": "^0.3.5",
|
||||||
"gopd": "^1.2.0",
|
"gopd": "^1.2.0",
|
||||||
"has-proto": "^1.2.0",
|
|
||||||
"is-typed-array": "^1.1.15",
|
"is-typed-array": "^1.1.15",
|
||||||
"reflect.getprototypeof": "^1.0.9"
|
"reflect.getprototypeof": "^1.0.10"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
@@ -7480,9 +7478,9 @@
|
|||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/which-typed-array": {
|
"node_modules/which-typed-array": {
|
||||||
"version": "1.1.22",
|
"version": "1.1.24",
|
||||||
"resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.22.tgz",
|
"resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.24.tgz",
|
||||||
"integrity": "sha512-fvO4ExWMFsqyhG3AiPAObMuY1lxaqgYcxbc49CNdWDDECOJNgQyvsOWVwbZc+qf3rzRtxojBK+CMEv0Ld5CYpw==",
|
"integrity": "sha512-wk4Mf4pR5mRP7eYuuTBCIQ9d0ud2Fv2jRLQpfgnRjbOxAFHmjKFValgTpitVKzJJS8ajnYQV2Du1SZ8j6b/EUQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
@@ -22,12 +22,12 @@
|
|||||||
"@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",
|
"pdfjs-dist": "^6.3.289",
|
||||||
"pdftotext-wasm": "file:../pdftotext-wasm",
|
"pdftotext-wasm": "file:../pdftotext-wasm",
|
||||||
"react": "^19.3.0",
|
"react": "^19.3.0",
|
||||||
|
"react-automat": "^0.1.0",
|
||||||
"react-dom": "^19.3.0",
|
"react-dom": "^19.3.0",
|
||||||
"react-redux": "^9.3.0",
|
"react-redux": "^9.3.0",
|
||||||
"react-router-dom": "^7.18.4",
|
"react-router-dom": "^7.18.4",
|
||||||
|
|||||||
@@ -105,6 +105,11 @@ export class ApiClient {
|
|||||||
update: (id, body) => this.#json('PATCH', `/api/delivery/${enc(id)}`, { body }),
|
update: (id, body) => this.#json('PATCH', `/api/delivery/${enc(id)}`, { body }),
|
||||||
/** DELETE /api/delivery/{id} → 204 */
|
/** DELETE /api/delivery/{id} → 204 */
|
||||||
delete: (id) => this.#empty('DELETE', `/api/delivery/${enc(id)}`),
|
delete: (id) => this.#empty('DELETE', `/api/delivery/${enc(id)}`),
|
||||||
|
/** GET /api/delivery/{id}/file/{fileId} → Blob (full binary content) */
|
||||||
|
getFile: (id, fileId, { download = false } = {}) =>
|
||||||
|
this.#blob('GET', `/api/delivery/${enc(id)}/file/${enc(fileId)}`, {
|
||||||
|
query: download ? { download: 'true' } : undefined,
|
||||||
|
}),
|
||||||
/**
|
/**
|
||||||
* POST /api/delivery/parse — multipart/form-data, field "file"
|
* POST /api/delivery/parse — multipart/form-data, field "file"
|
||||||
* Accepts PDF or text files (max 50 MB), detection via mimetype/filename
|
* Accepts PDF or text files (max 50 MB), detection via mimetype/filename
|
||||||
@@ -153,6 +158,9 @@ export class ApiClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
parsePdfSse(file) {
|
||||||
|
return this.parseSse(file);
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -682,6 +682,75 @@ export const delivery = {
|
|||||||
return http(`/api/delivery/${id}`, { method: 'DELETE' });
|
return http(`/api/delivery/${id}`, { method: 'DELETE' });
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /api/delivery/:id/file/:fileId → Blob
|
||||||
|
* Full binary content of attached file (PDF, text, etc.)
|
||||||
|
*/
|
||||||
|
async getFile(id, fileId, { download = false } = {}) {
|
||||||
|
const query = download ? '?download=true' : '';
|
||||||
|
const res = await fetch(`/api/delivery/${encodeURIComponent(id)}/file/${encodeURIComponent(fileId)}${query}`, {
|
||||||
|
method: 'GET',
|
||||||
|
credentials: 'include',
|
||||||
|
});
|
||||||
|
if (!res.ok) {
|
||||||
|
let message = res.statusText;
|
||||||
|
try {
|
||||||
|
const data = await res.json();
|
||||||
|
message = data.error || data.message || message;
|
||||||
|
} catch { /* ignore */ }
|
||||||
|
const err = new Error(message || `Request failed (${res.status})`);
|
||||||
|
err.status = res.status;
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
return res.blob();
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /api/delivery/parse — multipart/form-data, field "file"
|
||||||
|
* Accepts PDF, TXT or CSV document (max 50 MB)
|
||||||
|
* Streams SSE events: status, llm_call, tool_call, usage, progress, result, error
|
||||||
|
*/
|
||||||
|
async *parseSse(file) {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('file', file, file.name);
|
||||||
|
const res = await fetch('/api/delivery/parse', {
|
||||||
|
method: 'POST',
|
||||||
|
credentials: 'include',
|
||||||
|
body: formData,
|
||||||
|
});
|
||||||
|
if (!res.ok) {
|
||||||
|
let message = res.statusText;
|
||||||
|
try {
|
||||||
|
const data = await res.json();
|
||||||
|
message = data.error || data.message || message;
|
||||||
|
} catch { /* ignore */ }
|
||||||
|
const err = new Error(message || `Request failed (${res.status})`);
|
||||||
|
err.status = res.status;
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
const reader = res.body.getReader();
|
||||||
|
const decoder = new TextDecoder();
|
||||||
|
let buf = '';
|
||||||
|
let currentEvent = 'message';
|
||||||
|
while (true) {
|
||||||
|
const { done, value } = await reader.read();
|
||||||
|
if (done) break;
|
||||||
|
buf += decoder.decode(value, { stream: true });
|
||||||
|
const lines = buf.split('\n');
|
||||||
|
buf = lines.pop();
|
||||||
|
for (const line of lines) {
|
||||||
|
if (line.startsWith('event:')) {
|
||||||
|
currentEvent = line.slice(6).trim();
|
||||||
|
} else if (line.startsWith('data:')) {
|
||||||
|
const raw = line.slice(5).trim();
|
||||||
|
let data;
|
||||||
|
try { data = JSON.parse(raw); } catch { data = raw; }
|
||||||
|
yield { event: currentEvent, data };
|
||||||
|
currentEvent = 'message';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,7 +1,16 @@
|
|||||||
import { createContext, createElement, useContext, useRef } from 'react';
|
import { createContext, createElement, useContext, useRef } from 'react';
|
||||||
import { StorageManager } from './StorageManager';
|
import { StorageManager } from './StorageManager';
|
||||||
|
|
||||||
export const StorageManagerContext = createContext(null);
|
let defaultStorageManager = null;
|
||||||
|
|
||||||
|
export function getStorageManager() {
|
||||||
|
if (!defaultStorageManager) {
|
||||||
|
defaultStorageManager = new StorageManager();
|
||||||
|
}
|
||||||
|
return defaultStorageManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const StorageManagerContext = createContext(getStorageManager());
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provider — mount once near the top of the tree.
|
* Provider — mount once near the top of the tree.
|
||||||
@@ -9,7 +18,7 @@ export const StorageManagerContext = createContext(null);
|
|||||||
export function StorageManagerProvider({ children }) {
|
export function StorageManagerProvider({ children }) {
|
||||||
const mgrRef = useRef(null);
|
const mgrRef = useRef(null);
|
||||||
if (!mgrRef.current) {
|
if (!mgrRef.current) {
|
||||||
mgrRef.current = new StorageManager();
|
mgrRef.current = getStorageManager();
|
||||||
}
|
}
|
||||||
|
|
||||||
return createElement(StorageManagerContext.Provider, { value: mgrRef.current }, children);
|
return createElement(StorageManagerContext.Provider, { value: mgrRef.current }, children);
|
||||||
@@ -19,9 +28,6 @@ export function StorageManagerProvider({ children }) {
|
|||||||
* Hook for function components that need the store manager.
|
* Hook for function components that need the store manager.
|
||||||
*/
|
*/
|
||||||
export function useStorageManager() {
|
export function useStorageManager() {
|
||||||
const ctx = useContext(StorageManagerContext);
|
const ctx = useContext(StorageManagerContext) || getStorageManager();
|
||||||
if (!ctx) {
|
|
||||||
throw new Error('useStorageManager must be used within a <StorageManagerProvider>');
|
|
||||||
}
|
|
||||||
return ctx;
|
return ctx;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Automat } from 'automat';
|
import { Automat } from 'react-automat';
|
||||||
import { supplier as supplierApi } from '../api';
|
import { supplier as supplierApi } from '../api';
|
||||||
|
|
||||||
export function createEmptyLineitem(line = {}) {
|
export function createEmptyLineitem(line = {}) {
|
||||||
@@ -34,7 +34,7 @@ export function buildSupplierArticlesMap(articles = [], existingMap = {}) {
|
|||||||
map[artSku.toLowerCase()] = item;
|
map[artSku.toLowerCase()] = item;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Map by EANs
|
// Map by Eans
|
||||||
const eans = item.article?.eans ?? item.article?.articleean ?? [];
|
const eans = item.article?.eans ?? item.article?.articleean ?? [];
|
||||||
for (const ean of eans) {
|
for (const ean of eans) {
|
||||||
if (ean != null) {
|
if (ean != null) {
|
||||||
@@ -77,336 +77,367 @@ export const initialState = {
|
|||||||
...initialDelivery,
|
...initialDelivery,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const deliveryEditorAutomat = new Automat(initialState, {
|
export function createDeliveryEditorAutomat(options = {}) {
|
||||||
setId(id) {
|
const name = typeof options === 'string' ? options : options?.name ?? null;
|
||||||
deliveryEditorAutomat.setState({ id, deliveryId: id });
|
if (name) {
|
||||||
},
|
const existing = Automat.get(name);
|
||||||
|
if (existing) return existing;
|
||||||
|
}
|
||||||
|
|
||||||
setDeliveryEntities(deliveryEntities) {
|
let automat;
|
||||||
deliveryEditorAutomat.setState({ deliveryEntities: deliveryEntities ?? [] });
|
const actions = {
|
||||||
},
|
setId(id) {
|
||||||
|
automat.setState({ id, deliveryId: id });
|
||||||
|
},
|
||||||
|
|
||||||
setDelivery(delivery = {}) {
|
setDeliveryEntities(deliveryEntities) {
|
||||||
const rawLines = delivery.lines ?? delivery.lineitems ?? [];
|
automat.setState({ deliveryEntities: deliveryEntities ?? [] });
|
||||||
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({
|
setDelivery(delivery = {}) {
|
||||||
...initialDelivery,
|
const rawLines = delivery.lines ?? delivery.lineitems ?? [];
|
||||||
...delivery,
|
const lines = Array.isArray(rawLines) ? rawLines : [];
|
||||||
id,
|
const partyId = delivery.partyId ?? delivery.deliveryEntity ?? null;
|
||||||
deliveryId: id,
|
const extNum = delivery.externalNumber ?? delivery.external_number ?? null;
|
||||||
partyId,
|
const id = delivery.id ?? delivery.deliveryId ?? null;
|
||||||
deliveryEntity: partyId,
|
const notes = Array.isArray(delivery.notes)
|
||||||
externalNumber: extNum,
|
? delivery.notes
|
||||||
external_number: extNum,
|
: delivery.notes ? [delivery.notes] : [];
|
||||||
lines,
|
const rawAttachments = delivery.attachments ?? delivery.files ?? [];
|
||||||
lineitems: lines,
|
const attachments = Array.isArray(rawAttachments) ? rawAttachments : [];
|
||||||
notes,
|
|
||||||
attachments,
|
|
||||||
files: attachments,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (partyId && lines.length > 0) {
|
automat.setState({
|
||||||
deliveryEditorAutomat.actions.fetchSupplierArticles(partyId, lines);
|
...initialDelivery,
|
||||||
}
|
...delivery,
|
||||||
},
|
id,
|
||||||
|
deliveryId: id,
|
||||||
|
partyId,
|
||||||
|
deliveryEntity: partyId,
|
||||||
|
externalNumber: extNum,
|
||||||
|
external_number: extNum,
|
||||||
|
lines,
|
||||||
|
lineitems: lines,
|
||||||
|
notes,
|
||||||
|
attachments,
|
||||||
|
files: attachments,
|
||||||
|
});
|
||||||
|
|
||||||
setDate(date) {
|
if (partyId && lines.length > 0) {
|
||||||
deliveryEditorAutomat.setState({ date });
|
automat.actions.fetchSupplierArticles(partyId, lines);
|
||||||
},
|
}
|
||||||
|
},
|
||||||
|
|
||||||
setDeliveryEntity(entityId) {
|
setDate(date) {
|
||||||
const prevId = deliveryEditorAutomat.state.deliveryEntity ?? deliveryEditorAutomat.state.partyId;
|
automat.setState({ date });
|
||||||
const idChanged = prevId !== entityId;
|
},
|
||||||
deliveryEditorAutomat.setState({
|
|
||||||
deliveryEntity: entityId,
|
setDeliveryEntity(entityId) {
|
||||||
partyId: entityId,
|
const prevId = automat.state.deliveryEntity ?? automat.state.partyId;
|
||||||
...(idChanged
|
const idChanged = prevId !== entityId;
|
||||||
? {
|
automat.setState({
|
||||||
supplierArticles: [],
|
deliveryEntity: entityId,
|
||||||
supplierArticlesMap: {},
|
partyId: entityId,
|
||||||
supplierArticlesByLineKey: {},
|
...(idChanged
|
||||||
loadingLineKeys: {},
|
? {
|
||||||
|
supplierArticles: [],
|
||||||
|
supplierArticlesMap: {},
|
||||||
|
supplierArticlesByLineKey: {},
|
||||||
|
loadingLineKeys: {},
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (entityId && idChanged) {
|
||||||
|
automat.actions.fetchSupplierArticles(entityId);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
setPartyId(partyId) {
|
||||||
|
automat.actions.setDeliveryEntity(partyId);
|
||||||
|
},
|
||||||
|
|
||||||
|
setExternalNumber(externalNumber) {
|
||||||
|
automat.setState({
|
||||||
|
externalNumber,
|
||||||
|
external_number: externalNumber,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
setNumber(number) {
|
||||||
|
automat.setState({ number });
|
||||||
|
},
|
||||||
|
|
||||||
|
setType(type) {
|
||||||
|
automat.setState({ type });
|
||||||
|
},
|
||||||
|
|
||||||
|
setLines(lines) {
|
||||||
|
const cleanLines = lines ?? [];
|
||||||
|
automat.setState({ lines: cleanLines, lineitems: cleanLines });
|
||||||
|
},
|
||||||
|
|
||||||
|
setLineitems(lines) {
|
||||||
|
const cleanLines = lines ?? [];
|
||||||
|
automat.setState({ lines: cleanLines, lineitems: cleanLines });
|
||||||
|
},
|
||||||
|
|
||||||
|
setNotes(notes) {
|
||||||
|
const cleanNotes = Array.isArray(notes)
|
||||||
|
? notes
|
||||||
|
: notes ? [notes] : [];
|
||||||
|
automat.setState({ notes: cleanNotes });
|
||||||
|
},
|
||||||
|
|
||||||
|
addLineitem(line = {}) {
|
||||||
|
const item = createEmptyLineitem(line);
|
||||||
|
const nextLines = [...(automat.state.lines ?? []), item];
|
||||||
|
automat.setState({ lines: nextLines, lineitems: nextLines });
|
||||||
|
},
|
||||||
|
|
||||||
|
addLine(line = {}) {
|
||||||
|
automat.actions.addLineitem(line);
|
||||||
|
},
|
||||||
|
|
||||||
|
updateLineitem(payload) {
|
||||||
|
const { index, key, ...fields } = payload || {};
|
||||||
|
const currentLines = automat.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,
|
||||||
|
};
|
||||||
|
automat.setState({ lines: nextLines, lineitems: nextLines });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
updateLine(payload) {
|
||||||
|
automat.actions.updateLineitem(payload);
|
||||||
|
},
|
||||||
|
|
||||||
|
removeLineitem(payload) {
|
||||||
|
const currentLines = automat.state.lines ?? [];
|
||||||
|
let index = typeof payload === 'number' && !Number.isNaN(payload) ? payload : -1;
|
||||||
|
let targetKey = null;
|
||||||
|
if (index === -1 && payload != null) {
|
||||||
|
const key = typeof payload === 'string' ? payload : payload?.key;
|
||||||
|
if (key != null) {
|
||||||
|
index = currentLines.findIndex((l) => l.key === key);
|
||||||
|
targetKey = key;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (index >= 0 && index < currentLines.length) {
|
||||||
|
targetKey = targetKey ?? currentLines[index]?.key;
|
||||||
|
const nextLines = currentLines.filter((_, i) => i !== index);
|
||||||
|
const byLineKey = { ...(automat.state.supplierArticlesByLineKey || {}) };
|
||||||
|
if (targetKey != null) delete byLineKey[targetKey];
|
||||||
|
automat.setState({
|
||||||
|
lines: nextLines,
|
||||||
|
lineitems: nextLines,
|
||||||
|
supplierArticlesByLineKey: byLineKey,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
removeLine(payload) {
|
||||||
|
automat.actions.removeLineitem(payload);
|
||||||
|
},
|
||||||
|
|
||||||
|
async fetchSupplierArticles(targetEntityId, targetLines) {
|
||||||
|
const entityId = targetEntityId ?? automat.state.deliveryEntity ?? automat.state.partyId;
|
||||||
|
if (!entityId) {
|
||||||
|
automat.setState({
|
||||||
|
supplierArticles: [],
|
||||||
|
supplierArticlesMap: {},
|
||||||
|
supplierArticlesByLineKey: {},
|
||||||
|
supplierArticlesLoading: false,
|
||||||
|
loadingLineKeys: {},
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const lines = targetLines ?? automat.state.lines ?? [];
|
||||||
|
const skus = Array.from(
|
||||||
|
new Set(
|
||||||
|
lines
|
||||||
|
.map((l) => (l.sku != null ? String(l.sku).trim() : ''))
|
||||||
|
.filter(Boolean),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
automat.setState({
|
||||||
|
supplierArticlesLoading: true,
|
||||||
|
supplierArticlesError: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await supplierApi.getArticles(entityId, skus);
|
||||||
|
const articles = Array.isArray(res?.articles) ? res.articles : [];
|
||||||
|
|
||||||
|
const map = buildSupplierArticlesMap(articles);
|
||||||
|
const byLineKey = {};
|
||||||
|
for (const line of lines) {
|
||||||
|
if (line.key != null) {
|
||||||
|
const rawSku = line.sku != null ? String(line.sku).trim() : '';
|
||||||
|
byLineKey[line.key] = rawSku ? (map[rawSku.toLowerCase()] || map[rawSku] || null) : null;
|
||||||
}
|
}
|
||||||
: {}),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (entityId && idChanged) {
|
|
||||||
deliveryEditorAutomat.actions.fetchSupplierArticles(entityId);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
setPartyId(partyId) {
|
|
||||||
deliveryEditorAutomat.actions.setDeliveryEntity(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;
|
|
||||||
let targetKey = null;
|
|
||||||
if (index === -1 && payload != null) {
|
|
||||||
const key = typeof payload === 'string' ? payload : payload?.key;
|
|
||||||
if (key != null) {
|
|
||||||
index = currentLines.findIndex((l) => l.key === key);
|
|
||||||
targetKey = key;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (index >= 0 && index < currentLines.length) {
|
|
||||||
targetKey = targetKey ?? currentLines[index]?.key;
|
|
||||||
const nextLines = currentLines.filter((_, i) => i !== index);
|
|
||||||
const byLineKey = { ...(deliveryEditorAutomat.state.supplierArticlesByLineKey || {}) };
|
|
||||||
if (targetKey != null) delete byLineKey[targetKey];
|
|
||||||
deliveryEditorAutomat.setState({
|
|
||||||
lines: nextLines,
|
|
||||||
lineitems: nextLines,
|
|
||||||
supplierArticlesByLineKey: byLineKey,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
removeLine(payload) {
|
|
||||||
deliveryEditorAutomat.actions.removeLineitem(payload);
|
|
||||||
},
|
|
||||||
|
|
||||||
async fetchSupplierArticles(targetEntityId, targetLines) {
|
|
||||||
const entityId = targetEntityId ?? deliveryEditorAutomat.state.deliveryEntity ?? deliveryEditorAutomat.state.partyId;
|
|
||||||
if (!entityId) {
|
|
||||||
deliveryEditorAutomat.setState({
|
|
||||||
supplierArticles: [],
|
|
||||||
supplierArticlesMap: {},
|
|
||||||
supplierArticlesByLineKey: {},
|
|
||||||
supplierArticlesLoading: false,
|
|
||||||
loadingLineKeys: {},
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const lines = targetLines ?? deliveryEditorAutomat.state.lines ?? [];
|
|
||||||
const skus = Array.from(
|
|
||||||
new Set(
|
|
||||||
lines
|
|
||||||
.map((l) => (l.sku != null ? String(l.sku).trim() : ''))
|
|
||||||
.filter(Boolean),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
deliveryEditorAutomat.setState({
|
|
||||||
supplierArticlesLoading: true,
|
|
||||||
supplierArticlesError: null,
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
const res = await supplierApi.getArticles(entityId, skus);
|
|
||||||
const articles = Array.isArray(res?.articles) ? res.articles : [];
|
|
||||||
|
|
||||||
const map = buildSupplierArticlesMap(articles);
|
|
||||||
const byLineKey = {};
|
|
||||||
for (const line of lines) {
|
|
||||||
if (line.key != null) {
|
|
||||||
const rawSku = line.sku != null ? String(line.sku).trim() : '';
|
|
||||||
byLineKey[line.key] = rawSku ? (map[rawSku.toLowerCase()] || map[rawSku] || null) : null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
automat.setState({
|
||||||
|
supplierArticles: articles,
|
||||||
|
supplierArticlesMap: map,
|
||||||
|
supplierArticlesByLineKey: byLineKey,
|
||||||
|
supplierArticlesLoading: false,
|
||||||
|
loadingLineKeys: {},
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Fehler beim Abrufen der Lieferanten-Artikel:', err);
|
||||||
|
automat.setState({
|
||||||
|
supplierArticlesLoading: false,
|
||||||
|
supplierArticlesError: err?.message || 'Fehler beim Laden der Artikel',
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
|
||||||
deliveryEditorAutomat.setState({
|
async fetchSupplierArticleForSku(sku, lineKey) {
|
||||||
supplierArticles: articles,
|
const entityId = automat.state.deliveryEntity ?? automat.state.partyId;
|
||||||
supplierArticlesMap: map,
|
const trimmed = sku != null ? String(sku).trim() : '';
|
||||||
supplierArticlesByLineKey: byLineKey,
|
|
||||||
supplierArticlesLoading: false,
|
|
||||||
loadingLineKeys: {},
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Fehler beim Abrufen der Lieferanten-Artikel:', err);
|
|
||||||
deliveryEditorAutomat.setState({
|
|
||||||
supplierArticlesLoading: false,
|
|
||||||
supplierArticlesError: err?.message || 'Fehler beim Laden der Artikel',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
async fetchSupplierArticleForSku(sku, lineKey) {
|
if (!entityId || !trimmed) {
|
||||||
const entityId = deliveryEditorAutomat.state.deliveryEntity ?? deliveryEditorAutomat.state.partyId;
|
if (lineKey != null) {
|
||||||
const trimmed = sku != null ? String(sku).trim() : '';
|
const byLineKey = { ...(automat.state.supplierArticlesByLineKey || {}) };
|
||||||
|
byLineKey[lineKey] = null;
|
||||||
if (!entityId || !trimmed) {
|
automat.setState({ supplierArticlesByLineKey: byLineKey });
|
||||||
if (lineKey != null) {
|
|
||||||
const byLineKey = { ...(deliveryEditorAutomat.state.supplierArticlesByLineKey || {}) };
|
|
||||||
byLineKey[lineKey] = null;
|
|
||||||
deliveryEditorAutomat.setState({ supplierArticlesByLineKey: byLineKey });
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const prevLoading = deliveryEditorAutomat.state.loadingLineKeys || {};
|
|
||||||
deliveryEditorAutomat.setState({
|
|
||||||
loadingLineKeys: lineKey != null ? { ...prevLoading, [lineKey]: true } : prevLoading,
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
const res = await supplierApi.getArticles(entityId, [trimmed]);
|
|
||||||
const articles = Array.isArray(res?.articles) ? res.articles : [];
|
|
||||||
|
|
||||||
const nextMap = buildSupplierArticlesMap(articles, deliveryEditorAutomat.state.supplierArticlesMap);
|
|
||||||
const matched = nextMap[trimmed.toLowerCase()] || nextMap[trimmed] || articles[0] || null;
|
|
||||||
|
|
||||||
const byLineKey = { ...(deliveryEditorAutomat.state.supplierArticlesByLineKey || {}) };
|
|
||||||
if (lineKey != null) {
|
|
||||||
byLineKey[lineKey] = matched;
|
|
||||||
}
|
|
||||||
|
|
||||||
const currentList = deliveryEditorAutomat.state.supplierArticles || [];
|
|
||||||
const mergedList = [...currentList];
|
|
||||||
for (const a of articles) {
|
|
||||||
if (!mergedList.some((existing) => existing.id === a.id)) {
|
|
||||||
mergedList.push(a);
|
|
||||||
}
|
}
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const nextLoading = { ...(deliveryEditorAutomat.state.loadingLineKeys || {}) };
|
const prevLoading = automat.state.loadingLineKeys || {};
|
||||||
if (lineKey != null) delete nextLoading[lineKey];
|
automat.setState({
|
||||||
|
loadingLineKeys: lineKey != null ? { ...prevLoading, [lineKey]: true } : prevLoading,
|
||||||
deliveryEditorAutomat.setState({
|
|
||||||
supplierArticles: mergedList,
|
|
||||||
supplierArticlesMap: nextMap,
|
|
||||||
supplierArticlesByLineKey: byLineKey,
|
|
||||||
loadingLineKeys: nextLoading,
|
|
||||||
supplierArticlesLoading: Object.keys(nextLoading).length > 0,
|
|
||||||
});
|
});
|
||||||
} catch (err) {
|
|
||||||
console.error('Fehler beim Abrufen des Artikels für SKU:', err);
|
try {
|
||||||
const nextLoading = { ...(deliveryEditorAutomat.state.loadingLineKeys || {}) };
|
const res = await supplierApi.getArticles(entityId, [trimmed]);
|
||||||
if (lineKey != null) delete nextLoading[lineKey];
|
const articles = Array.isArray(res?.articles) ? res.articles : [];
|
||||||
deliveryEditorAutomat.setState({
|
|
||||||
loadingLineKeys: nextLoading,
|
const nextMap = buildSupplierArticlesMap(articles, automat.state.supplierArticlesMap);
|
||||||
supplierArticlesLoading: Object.keys(nextLoading).length > 0,
|
const matched = nextMap[trimmed.toLowerCase()] || nextMap[trimmed] || articles[0] || null;
|
||||||
|
|
||||||
|
const byLineKey = { ...(automat.state.supplierArticlesByLineKey || {}) };
|
||||||
|
if (lineKey != null) {
|
||||||
|
byLineKey[lineKey] = matched;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentList = automat.state.supplierArticles || [];
|
||||||
|
const mergedList = [...currentList];
|
||||||
|
for (const a of articles) {
|
||||||
|
if (!mergedList.some((existing) => existing.id === a.id)) {
|
||||||
|
mergedList.push(a);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextLoading = { ...(automat.state.loadingLineKeys || {}) };
|
||||||
|
if (lineKey != null) delete nextLoading[lineKey];
|
||||||
|
|
||||||
|
automat.setState({
|
||||||
|
supplierArticles: mergedList,
|
||||||
|
supplierArticlesMap: nextMap,
|
||||||
|
supplierArticlesByLineKey: byLineKey,
|
||||||
|
loadingLineKeys: nextLoading,
|
||||||
|
supplierArticlesLoading: Object.keys(nextLoading).length > 0,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Fehler beim Abrufen des Artikels für SKU:', err);
|
||||||
|
const nextLoading = { ...(automat.state.loadingLineKeys || {}) };
|
||||||
|
if (lineKey != null) delete nextLoading[lineKey];
|
||||||
|
automat.setState({
|
||||||
|
loadingLineKeys: nextLoading,
|
||||||
|
supplierArticlesLoading: Object.keys(nextLoading).length > 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
addAttachment(attachment) {
|
||||||
|
const id = attachment.id ?? `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
||||||
|
const item = { ...attachment, id };
|
||||||
|
const next = [...(automat.state.attachments ?? []), item];
|
||||||
|
automat.setState({ attachments: next, files: next });
|
||||||
|
},
|
||||||
|
|
||||||
|
setAttachments(attachments) {
|
||||||
|
const clean = Array.isArray(attachments) ? attachments : [];
|
||||||
|
automat.setState({ attachments: clean, files: clean });
|
||||||
|
},
|
||||||
|
|
||||||
|
updateAttachment(payload) {
|
||||||
|
const { id, ...fields } = payload || {};
|
||||||
|
const current = automat.state.attachments ?? [];
|
||||||
|
const idx = current.findIndex((a) => a.id === id || String(a.id) === String(id));
|
||||||
|
if (idx !== -1) {
|
||||||
|
const next = [...current];
|
||||||
|
next[idx] = { ...next[idx], ...fields };
|
||||||
|
automat.setState({ attachments: next, files: next });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
removeAttachment(idOrPayload) {
|
||||||
|
const current = automat.state.attachments ?? [];
|
||||||
|
const targetId =
|
||||||
|
typeof idOrPayload === 'object' && idOrPayload !== null
|
||||||
|
? (idOrPayload.id ?? idOrPayload.fileId)
|
||||||
|
: idOrPayload;
|
||||||
|
const hasMatch = current.some((a) => a.id === targetId || String(a.id) === String(targetId));
|
||||||
|
let next;
|
||||||
|
if (hasMatch) {
|
||||||
|
next = current.filter((a) => a.id !== targetId && String(a.id) !== String(targetId));
|
||||||
|
} else if (typeof idOrPayload === 'number' && idOrPayload >= 0 && idOrPayload < current.length) {
|
||||||
|
next = current.filter((_, idx) => idx !== idOrPayload);
|
||||||
|
} else {
|
||||||
|
next = current.filter((a) => a.id !== targetId && String(a.id) !== String(targetId));
|
||||||
|
}
|
||||||
|
automat.setState({ attachments: next, files: next });
|
||||||
|
},
|
||||||
|
|
||||||
|
resetDelivery() {
|
||||||
|
automat.setState({
|
||||||
|
...initialDelivery,
|
||||||
});
|
});
|
||||||
}
|
},
|
||||||
},
|
|
||||||
|
|
||||||
addAttachment(attachment) {
|
resetDeliveryEditor() {
|
||||||
const id = attachment.id ?? `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
automat.setState({
|
||||||
const item = { ...attachment, id };
|
...initialState,
|
||||||
const next = [...(deliveryEditorAutomat.state.attachments ?? []), item];
|
});
|
||||||
deliveryEditorAutomat.setState({ attachments: next, files: next });
|
},
|
||||||
},
|
};
|
||||||
|
|
||||||
setAttachments(attachments) {
|
automat = new Automat(initialState, actions, {
|
||||||
const clean = Array.isArray(attachments) ? attachments : [];
|
name,
|
||||||
deliveryEditorAutomat.setState({ attachments: clean, files: clean });
|
persist: false,
|
||||||
},
|
});
|
||||||
|
|
||||||
updateAttachment(payload) {
|
return automat;
|
||||||
const { id, ...fields } = payload || {};
|
}
|
||||||
const current = deliveryEditorAutomat.state.attachments ?? [];
|
|
||||||
const idx = current.findIndex((a) => a.id === id || String(a.id) === String(id));
|
|
||||||
if (idx !== -1) {
|
|
||||||
const next = [...current];
|
|
||||||
next[idx] = { ...next[idx], ...fields };
|
|
||||||
deliveryEditorAutomat.setState({ attachments: next, files: next });
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
removeAttachment(idOrPayload) {
|
export const deliveryDraftAutomat = createDeliveryEditorAutomat({ name: 'delivery-draft' });
|
||||||
const current = deliveryEditorAutomat.state.attachments ?? [];
|
export const deliveryEditorAutomat = deliveryDraftAutomat;
|
||||||
const targetId =
|
|
||||||
typeof idOrPayload === 'object' && idOrPayload !== null
|
|
||||||
? (idOrPayload.id ?? idOrPayload.fileId)
|
|
||||||
: idOrPayload;
|
|
||||||
const hasMatch = current.some((a) => a.id === targetId || String(a.id) === String(targetId));
|
|
||||||
let next;
|
|
||||||
if (hasMatch) {
|
|
||||||
next = current.filter((a) => a.id !== targetId && String(a.id) !== String(targetId));
|
|
||||||
} else if (typeof idOrPayload === 'number' && idOrPayload >= 0 && idOrPayload < current.length) {
|
|
||||||
next = current.filter((_, idx) => idx !== idOrPayload);
|
|
||||||
} else {
|
|
||||||
next = current.filter((a) => a.id !== targetId && String(a.id) !== String(targetId));
|
|
||||||
}
|
|
||||||
deliveryEditorAutomat.setState({ attachments: next, files: next });
|
|
||||||
},
|
|
||||||
|
|
||||||
resetDelivery() {
|
export function getDeliveryEditorAutomat(docId) {
|
||||||
deliveryEditorAutomat.setState({
|
if (docId == null) {
|
||||||
...initialDelivery,
|
return deliveryDraftAutomat;
|
||||||
});
|
}
|
||||||
},
|
const name = `delivery-${docId}`;
|
||||||
|
let automat = Automat.get(name);
|
||||||
resetDeliveryEditor() {
|
if (!automat) {
|
||||||
deliveryEditorAutomat.setState({
|
automat = createDeliveryEditorAutomat({ name });
|
||||||
...initialState,
|
}
|
||||||
});
|
return automat;
|
||||||
},
|
}
|
||||||
});
|
|
||||||
|
|
||||||
export const {
|
export const {
|
||||||
setId,
|
setId,
|
||||||
|
|||||||
@@ -31,8 +31,19 @@ export class Attachment extends PureComponent {
|
|||||||
if (onClick && attachment) {
|
if (onClick && attachment) {
|
||||||
onClick(attachment);
|
onClick(attachment);
|
||||||
} else if (attachment?.file instanceof Blob || attachment?.file instanceof File) {
|
} else if (attachment?.file instanceof Blob || attachment?.file instanceof File) {
|
||||||
const url = URL.createObjectURL(attachment.file);
|
if (attachment.file.size > 0) {
|
||||||
window.open(url, '_blank');
|
const url = URL.createObjectURL(attachment.file);
|
||||||
|
window.open(url, '_blank');
|
||||||
|
} else {
|
||||||
|
const docId = attachment.manifestId;
|
||||||
|
const fileId = attachment.fileId ?? attachment.id;
|
||||||
|
if (docId != null && fileId != null) {
|
||||||
|
window.open(
|
||||||
|
`/api/delivery/${encodeURIComponent(docId)}/file/${encodeURIComponent(fileId)}`,
|
||||||
|
'_blank',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -49,7 +60,8 @@ export class Attachment extends PureComponent {
|
|||||||
const name = attachment?.name || 'Dokument';
|
const name = attachment?.name || 'Dokument';
|
||||||
const size = attachment?.size;
|
const size = attachment?.size;
|
||||||
const isPdf = attachment?.type === 'application/pdf' || name.toLowerCase().endsWith('.pdf');
|
const isPdf = attachment?.type === 'application/pdf' || name.toLowerCase().endsWith('.pdf');
|
||||||
const isText = attachment?.type?.startsWith('text/') || name.toLowerCase().endsWith('.txt');
|
const isCsv = attachment?.type === 'text/csv' || name.toLowerCase().endsWith('.csv');
|
||||||
|
const isText = attachment?.type?.startsWith('text/') || name.toLowerCase().endsWith('.txt') || isCsv;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
@@ -125,7 +137,7 @@ export class Attachment extends PureComponent {
|
|||||||
letterSpacing: '0.05em',
|
letterSpacing: '0.05em',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
TXT
|
{isCsv ? 'CSV' : 'TXT'}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
<Typography
|
<Typography
|
||||||
|
|||||||
@@ -134,7 +134,7 @@ class ManifestHeader extends PureComponent {
|
|||||||
) : null}
|
) : null}
|
||||||
{onPdfUpload ? (
|
{onPdfUpload ? (
|
||||||
<FileDropzone
|
<FileDropzone
|
||||||
accept="application/pdf,.pdf"
|
accept="application/pdf,.pdf,text/plain,.txt,text/csv,.csv"
|
||||||
disabled={saving || pdfUploading}
|
disabled={saving || pdfUploading}
|
||||||
onFileDrop={onPdfUpload}
|
onFileDrop={onPdfUpload}
|
||||||
sx={{
|
sx={{
|
||||||
@@ -150,6 +150,7 @@ class ManifestHeader extends PureComponent {
|
|||||||
startIcon={pdfUploading ? <CircularProgress size={16} color="inherit" /> : <UploadFileIcon />}
|
startIcon={pdfUploading ? <CircularProgress size={16} color="inherit" /> : <UploadFileIcon />}
|
||||||
disabled={saving || pdfUploading}
|
disabled={saving || pdfUploading}
|
||||||
onClick={this.handleUploadClick}
|
onClick={this.handleUploadClick}
|
||||||
|
title="PDF, TXT oder CSV hochladen"
|
||||||
sx={{
|
sx={{
|
||||||
whiteSpace: 'nowrap',
|
whiteSpace: 'nowrap',
|
||||||
height: 40,
|
height: 40,
|
||||||
@@ -164,8 +165,8 @@ class ManifestHeader extends PureComponent {
|
|||||||
{pdfUploading
|
{pdfUploading
|
||||||
? 'Wird analysiert…'
|
? 'Wird analysiert…'
|
||||||
: isDragging
|
: isDragging
|
||||||
? 'PDF hier ablegen'
|
? 'Datei hier ablegen'
|
||||||
: 'PDF hochladen'}
|
: 'Datei hochladen'}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</FileDropzone>
|
</FileDropzone>
|
||||||
@@ -182,7 +183,7 @@ class ManifestHeader extends PureComponent {
|
|||||||
<input
|
<input
|
||||||
ref={this.fileInputRef}
|
ref={this.fileInputRef}
|
||||||
type="file"
|
type="file"
|
||||||
accept="application/pdf,.pdf"
|
accept="application/pdf,.pdf,text/plain,.txt,text/csv,.csv"
|
||||||
style={{ display: 'none' }}
|
style={{ display: 'none' }}
|
||||||
onChange={this.handleFileChange}
|
onChange={this.handleFileChange}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import CloseIcon from '@mui/icons-material/Close';
|
|||||||
import CheckCircleIcon from '@mui/icons-material/CheckCircle';
|
import CheckCircleIcon from '@mui/icons-material/CheckCircle';
|
||||||
import ErrorOutlinedIcon from '@mui/icons-material/ErrorOutlined';
|
import ErrorOutlinedIcon from '@mui/icons-material/ErrorOutlined';
|
||||||
import PictureAsPdfIcon from '@mui/icons-material/PictureAsPdf';
|
import PictureAsPdfIcon from '@mui/icons-material/PictureAsPdf';
|
||||||
|
import TextSnippetOutlinedIcon from '@mui/icons-material/TextSnippetOutlined';
|
||||||
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
||||||
import ExpandLessIcon from '@mui/icons-material/ExpandLess';
|
import ExpandLessIcon from '@mui/icons-material/ExpandLess';
|
||||||
|
|
||||||
@@ -97,6 +98,9 @@ export default function ManifestPdfDialog({ open, filename, data, onClose }) {
|
|||||||
const { streaming = false, messages = [], result = null, error = null } = data ?? {};
|
const { streaming = false, messages = [], result = null, error = null } = data ?? {};
|
||||||
const hasResult = Boolean(result);
|
const hasResult = Boolean(result);
|
||||||
|
|
||||||
|
const isTextDoc =
|
||||||
|
filename?.toLowerCase().endsWith('.txt') || filename?.toLowerCase().endsWith('.csv');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog
|
<Dialog
|
||||||
open={open}
|
open={open}
|
||||||
@@ -121,12 +125,18 @@ export default function ManifestPdfDialog({ open, filename, data, onClose }) {
|
|||||||
<ErrorOutlinedIcon color="error" />
|
<ErrorOutlinedIcon color="error" />
|
||||||
) : streaming ? (
|
) : streaming ? (
|
||||||
<CircularProgress size={22} />
|
<CircularProgress size={22} />
|
||||||
|
) : isTextDoc ? (
|
||||||
|
<TextSnippetOutlinedIcon color="action" />
|
||||||
) : (
|
) : (
|
||||||
<PictureAsPdfIcon color="action" />
|
<PictureAsPdfIcon color="action" />
|
||||||
)}
|
)}
|
||||||
<Box sx={{ flex: 1 }}>
|
<Box sx={{ flex: 1 }}>
|
||||||
<Typography variant="subtitle1" component="div" fontWeight={600}>
|
<Typography variant="subtitle1" component="div" fontWeight={600}>
|
||||||
{streaming ? 'PDF wird analysiert…' : error ? 'PDF-Analyse fehlgeschlagen' : 'PDF-Analyse abgeschlossen'}
|
{streaming
|
||||||
|
? (isTextDoc ? 'Dokument wird analysiert…' : 'PDF wird analysiert…')
|
||||||
|
: error
|
||||||
|
? 'Analyse fehlgeschlagen'
|
||||||
|
: (isTextDoc ? 'Dokument-Analyse abgeschlossen' : 'PDF-Analyse abgeschlossen')}
|
||||||
</Typography>
|
</Typography>
|
||||||
{filename ? (
|
{filename ? (
|
||||||
<Typography variant="caption" color="text.secondary">
|
<Typography variant="caption" color="text.secondary">
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { PureComponent } from 'react';
|
import { PureComponent } from 'react';
|
||||||
import Box from '@mui/material/Box';
|
import Box from '@mui/material/Box';
|
||||||
import Button from '@mui/material/Button';
|
import Button from '@mui/material/Button';
|
||||||
|
import CircularProgress from '@mui/material/CircularProgress';
|
||||||
import Dialog from '@mui/material/Dialog';
|
import Dialog from '@mui/material/Dialog';
|
||||||
import DialogActions from '@mui/material/DialogActions';
|
import DialogActions from '@mui/material/DialogActions';
|
||||||
import DialogContent from '@mui/material/DialogContent';
|
import DialogContent from '@mui/material/DialogContent';
|
||||||
@@ -12,10 +13,15 @@ import Chip from '@mui/material/Chip';
|
|||||||
import CloseIcon from '@mui/icons-material/Close';
|
import CloseIcon from '@mui/icons-material/Close';
|
||||||
import TextSnippetOutlinedIcon from '@mui/icons-material/TextSnippetOutlined';
|
import TextSnippetOutlinedIcon from '@mui/icons-material/TextSnippetOutlined';
|
||||||
import ContentCopyIcon from '@mui/icons-material/ContentCopy';
|
import ContentCopyIcon from '@mui/icons-material/ContentCopy';
|
||||||
|
import DownloadOutlinedIcon from '@mui/icons-material/DownloadOutlined';
|
||||||
import SaveOutlinedIcon from '@mui/icons-material/SaveOutlined';
|
import SaveOutlinedIcon from '@mui/icons-material/SaveOutlined';
|
||||||
import { enqueueSnackbar } from 'notistack';
|
import { enqueueSnackbar } from 'notistack';
|
||||||
|
import { StorageManagerContext } from '../../api/storageManagerContext';
|
||||||
|
import { delivery as deliveryApi } from '../../api';
|
||||||
|
|
||||||
export class TextEditorDialog extends PureComponent {
|
export class TextEditorDialog extends PureComponent {
|
||||||
|
static contextType = StorageManagerContext;
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = {
|
this.state = {
|
||||||
@@ -26,6 +32,7 @@ export class TextEditorDialog extends PureComponent {
|
|||||||
this.handleChange = this.handleChange.bind(this);
|
this.handleChange = this.handleChange.bind(this);
|
||||||
this.handleSave = this.handleSave.bind(this);
|
this.handleSave = this.handleSave.bind(this);
|
||||||
this.handleCopy = this.handleCopy.bind(this);
|
this.handleCopy = this.handleCopy.bind(this);
|
||||||
|
this.handleDownload = this.handleDownload.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
@@ -34,12 +41,16 @@ export class TextEditorDialog extends PureComponent {
|
|||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
componentDidUpdate(prevProps) {
|
||||||
if (this.props.attachment !== prevProps.attachment || (this.props.open && !prevProps.open)) {
|
if (this.props.attachment !== prevProps.attachment || (this.props.open && !prevProps.open)) {
|
||||||
this.loadContent();
|
if (this.props.open) {
|
||||||
|
this.loadContent();
|
||||||
|
} else {
|
||||||
|
this.setState({ content: '', initialContent: '', loading: false });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadContent() {
|
async loadContent() {
|
||||||
const { attachment, open } = this.props;
|
const { attachment, open, docId, onContentLoad } = this.props;
|
||||||
if (!open || !attachment) return;
|
if (!open || !attachment) return;
|
||||||
|
|
||||||
if (typeof attachment.text === 'string') {
|
if (typeof attachment.text === 'string') {
|
||||||
@@ -51,7 +62,10 @@ export class TextEditorDialog extends PureComponent {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (attachment.file instanceof Blob || attachment.file instanceof File) {
|
if (
|
||||||
|
(attachment.file instanceof Blob || attachment.file instanceof File) &&
|
||||||
|
attachment.file.size > 0
|
||||||
|
) {
|
||||||
this.setState({ loading: true });
|
this.setState({ loading: true });
|
||||||
try {
|
try {
|
||||||
const text = await attachment.file.text();
|
const text = await attachment.file.text();
|
||||||
@@ -60,11 +74,44 @@ export class TextEditorDialog extends PureComponent {
|
|||||||
initialContent: text,
|
initialContent: text,
|
||||||
loading: false,
|
loading: false,
|
||||||
});
|
});
|
||||||
|
return;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Fehler beim Lesen der Textdatei:', err);
|
console.error('Fehler beim Lesen der Textdatei:', err);
|
||||||
this.setState({ loading: false });
|
this.setState({ loading: false });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Attachment has no text and empty file — load from API (GET /delivery/{id}/file/{fileId})
|
||||||
|
const effectiveDocId = docId ?? attachment.manifestId;
|
||||||
|
const fileId = attachment.fileId ?? attachment.id;
|
||||||
|
if (effectiveDocId != null && fileId != null) {
|
||||||
|
this.setState({ loading: true });
|
||||||
|
try {
|
||||||
|
const getFile =
|
||||||
|
this.context?.delivery?.getFile?.bind(this.context.delivery) ||
|
||||||
|
deliveryApi.getFile;
|
||||||
|
const blob = await getFile(effectiveDocId, fileId);
|
||||||
|
const text = await blob.text();
|
||||||
|
this.setState({
|
||||||
|
content: text,
|
||||||
|
initialContent: text,
|
||||||
|
loading: false,
|
||||||
|
});
|
||||||
|
onContentLoad?.(attachment, text, blob);
|
||||||
|
return;
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Fehler beim Laden der Textdatei:', err);
|
||||||
|
enqueueSnackbar('Fehler beim Laden der Datei', { variant: 'error' });
|
||||||
|
this.setState({ loading: false });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
content: '',
|
||||||
|
initialContent: '',
|
||||||
|
loading: false,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
handleChange(e) {
|
handleChange(e) {
|
||||||
@@ -88,6 +135,22 @@ export class TextEditorDialog extends PureComponent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleDownload() {
|
||||||
|
const { attachment } = this.props;
|
||||||
|
const { content } = this.state;
|
||||||
|
if (!content) return;
|
||||||
|
const filename = attachment?.name || 'Dokument.txt';
|
||||||
|
const blob = new Blob([content], { type: 'text/plain;charset=utf-8;' });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = url;
|
||||||
|
link.download = filename;
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
document.body.removeChild(link);
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { open, onClose, attachment, readOnly = false } = this.props;
|
const { open, onClose, attachment, readOnly = false } = this.props;
|
||||||
if (!open) return null;
|
if (!open) return null;
|
||||||
@@ -152,8 +215,24 @@ export class TextEditorDialog extends PureComponent {
|
|||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
bgcolor: '#fafafa',
|
bgcolor: '#fafafa',
|
||||||
|
position: 'relative',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
{loading && (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
position: 'absolute',
|
||||||
|
inset: 0,
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
bgcolor: 'rgba(250, 250, 250, 0.7)',
|
||||||
|
zIndex: 2,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CircularProgress size={36} />
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
<TextField
|
<TextField
|
||||||
multiline
|
multiline
|
||||||
fullWidth
|
fullWidth
|
||||||
@@ -216,15 +295,26 @@ export class TextEditorDialog extends PureComponent {
|
|||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Button
|
<Box sx={{ display: 'flex', gap: 1 }}>
|
||||||
size="small"
|
<Button
|
||||||
variant="outlined"
|
size="small"
|
||||||
startIcon={<ContentCopyIcon />}
|
variant="outlined"
|
||||||
onClick={this.handleCopy}
|
startIcon={<ContentCopyIcon />}
|
||||||
disabled={loading || !content}
|
onClick={this.handleCopy}
|
||||||
>
|
disabled={loading || !content}
|
||||||
Kopieren
|
>
|
||||||
</Button>
|
Kopieren
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
variant="outlined"
|
||||||
|
startIcon={<DownloadOutlinedIcon />}
|
||||||
|
onClick={this.handleDownload}
|
||||||
|
disabled={loading || !content}
|
||||||
|
>
|
||||||
|
Download
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
<Box sx={{ display: 'flex', gap: 1 }}>
|
<Box sx={{ display: 'flex', gap: 1 }}>
|
||||||
<Button size="small" onClick={onClose}>
|
<Button size="small" onClick={onClose}>
|
||||||
{readOnly ? 'Schließen' : 'Abbrechen'}
|
{readOnly ? 'Schließen' : 'Abbrechen'}
|
||||||
|
|||||||
80
src/nav.js
80
src/nav.js
@@ -1,3 +1,5 @@
|
|||||||
|
import { PureComponent, createElement } from 'react';
|
||||||
|
import Badge from '@mui/material/Badge';
|
||||||
import Inventory2Icon from '@mui/icons-material/Inventory2';
|
import Inventory2Icon from '@mui/icons-material/Inventory2';
|
||||||
import LocalShippingIcon from '@mui/icons-material/LocalShipping';
|
import LocalShippingIcon from '@mui/icons-material/LocalShipping';
|
||||||
import AssignmentIcon from '@mui/icons-material/Assignment';
|
import AssignmentIcon from '@mui/icons-material/Assignment';
|
||||||
@@ -13,15 +15,89 @@ import StorefrontIcon from '@mui/icons-material/Storefront';
|
|||||||
import RequestQuoteIcon from '@mui/icons-material/RequestQuote';
|
import RequestQuoteIcon from '@mui/icons-material/RequestQuote';
|
||||||
import ShoppingCartIcon from '@mui/icons-material/ShoppingCart';
|
import ShoppingCartIcon from '@mui/icons-material/ShoppingCart';
|
||||||
import { isKategorienPath } from './pages/artikel/categoryRoute';
|
import { isKategorienPath } from './pages/artikel/categoryRoute';
|
||||||
|
import { deliveryEditorAutomat } from './automats/deliveryEditorAutomat';
|
||||||
|
|
||||||
|
class DeliveryNavIcon extends PureComponent {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
const state = deliveryEditorAutomat.state;
|
||||||
|
const isDraft = state.id == null && state.deliveryId == null;
|
||||||
|
this.state = {
|
||||||
|
hasDraft: isDraft && (state.lines?.length || 0) > 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.unsub = deliveryEditorAutomat.subscribe(this, (state) => {
|
||||||
|
const isDraft = state.id == null && state.deliveryId == null;
|
||||||
|
return {
|
||||||
|
hasDraft: isDraft && (state.lines?.length || 0) > 0,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
this.unsub?.();
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { hasDraft } = this.state;
|
||||||
|
const { sx, ...rest } = this.props;
|
||||||
|
return createElement(LocalShippingIcon, {
|
||||||
|
...rest,
|
||||||
|
sx: [
|
||||||
|
...(Array.isArray(sx) ? sx : sx ? [sx] : []),
|
||||||
|
(theme) => (hasDraft ? { color: theme.nav?.itemActiveAccent || '#2dd4bf' } : null),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export const NAV_ITEMS = [
|
export const NAV_ITEMS = [
|
||||||
{ path: '/artikel', label: 'Artikel', icon: Inventory2Icon },
|
{ path: '/artikel', label: 'Artikel', icon: Inventory2Icon },
|
||||||
{ path: '/lieferung', label: 'Lieferung', icon: LocalShippingIcon },
|
{ path: '/lieferung', label: 'Lieferung', icon: DeliveryNavIcon },
|
||||||
{ path: '/auftrag', label: 'Auftrag', icon: AssignmentIcon },
|
{ path: '/auftrag', label: 'Auftrag', icon: AssignmentIcon },
|
||||||
{ path: '/kunde', label: 'Kunde', icon: PeopleIcon },
|
{ path: '/kunde', label: 'Kunde', icon: PeopleIcon },
|
||||||
{ path: '/infos', label: 'Infos', icon: InfoIcon },
|
{ path: '/infos', label: 'Infos', icon: InfoIcon },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
class DeliveryNeuIcon extends PureComponent {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
const state = deliveryEditorAutomat.state;
|
||||||
|
const isDraft = state.id == null && state.deliveryId == null;
|
||||||
|
this.state = {
|
||||||
|
badgeCount: isDraft ? (state.lines?.length || 0) : 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.unsub = deliveryEditorAutomat.subscribe(this, (state) => {
|
||||||
|
const isDraft = state.id == null && state.deliveryId == null;
|
||||||
|
return {
|
||||||
|
badgeCount: isDraft ? (state.lines?.length || 0) : 0,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
this.unsub?.();
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return createElement(
|
||||||
|
Badge,
|
||||||
|
{
|
||||||
|
badgeContent: this.state.badgeCount,
|
||||||
|
invisible: !this.state.badgeCount,
|
||||||
|
color: 'primary',
|
||||||
|
max: 999,
|
||||||
|
},
|
||||||
|
createElement(AddBoxIcon, this.props)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export const SUB_NAV = {
|
export const SUB_NAV = {
|
||||||
'/artikel': [
|
'/artikel': [
|
||||||
{ segment: '', label: 'Liste', title: 'Artikel', icon: ViewListIcon },
|
{ segment: '', label: 'Liste', title: 'Artikel', icon: ViewListIcon },
|
||||||
@@ -30,7 +106,7 @@ export const SUB_NAV = {
|
|||||||
],
|
],
|
||||||
'/lieferung': [
|
'/lieferung': [
|
||||||
{ segment: '', label: 'Liste', title: 'Lieferungen', icon: ListAltIcon },
|
{ segment: '', label: 'Liste', title: 'Lieferungen', icon: ListAltIcon },
|
||||||
{ segment: 'neu', label: 'Neu', title: 'Neue Lieferung', icon: AddBoxIcon },
|
{ segment: 'neu', label: 'Neu', title: 'Neue Lieferung', icon: DeliveryNeuIcon },
|
||||||
{ segment: 'lieferanten', label: 'Lieferanten', icon: StorefrontIcon },
|
{ segment: 'lieferanten', label: 'Lieferanten', icon: StorefrontIcon },
|
||||||
],
|
],
|
||||||
'/auftrag': [
|
'/auftrag': [
|
||||||
|
|||||||
@@ -9,8 +9,7 @@ import Paper from '@mui/material/Paper';
|
|||||||
import TextField from '@mui/material/TextField';
|
import TextField from '@mui/material/TextField';
|
||||||
import Typography from '@mui/material/Typography';
|
import Typography from '@mui/material/Typography';
|
||||||
import SearchIcon from '@mui/icons-material/Search';
|
import SearchIcon from '@mui/icons-material/Search';
|
||||||
import ClearIcon from '@mui/icons-material/Clear';
|
import { StorageManagerContext, getStorageManager } from '../../api/storageManagerContext';
|
||||||
import { StorageManagerContext } from '../../api/storageManagerContext';
|
|
||||||
import BackendError from '../../components/BackendError';
|
import BackendError from '../../components/BackendError';
|
||||||
import { formatPrice } from '../../utils/price';
|
import { formatPrice } from '../../utils/price';
|
||||||
import {
|
import {
|
||||||
@@ -83,6 +82,10 @@ function initialSearchState(searchParams) {
|
|||||||
class ArtikelSearchPage extends Component {
|
class ArtikelSearchPage extends Component {
|
||||||
static contextType = StorageManagerContext;
|
static contextType = StorageManagerContext;
|
||||||
|
|
||||||
|
get storageManager() {
|
||||||
|
return this.context || getStorageManager();
|
||||||
|
}
|
||||||
|
|
||||||
state = initialSearchState(this.props.searchParams);
|
state = initialSearchState(this.props.searchParams);
|
||||||
listSlotRef = { current: null };
|
listSlotRef = { current: null };
|
||||||
rowMeasureRef = { current: null };
|
rowMeasureRef = { current: null };
|
||||||
@@ -171,7 +174,7 @@ class ArtikelSearchPage extends Component {
|
|||||||
|
|
||||||
// Kick off initial search (empty input = show all products).
|
// Kick off initial search (empty input = show all products).
|
||||||
const { input } = this.state;
|
const { input } = this.state;
|
||||||
this.context.doSearch({
|
this.storageManager?.doSearch({
|
||||||
target: input,
|
target: input,
|
||||||
filter: this.props.searchParams.get('filter') || undefined,
|
filter: this.props.searchParams.get('filter') || undefined,
|
||||||
offset: 0,
|
offset: 0,
|
||||||
@@ -220,7 +223,7 @@ class ArtikelSearchPage extends Component {
|
|||||||
const value = event.target.value;
|
const value = event.target.value;
|
||||||
this.setState({ input: value });
|
this.setState({ input: value });
|
||||||
|
|
||||||
this.context.doSearch({
|
this.storageManager?.doSearch({
|
||||||
target: value,
|
target: value,
|
||||||
filter: this.props.searchParams.get('filter') || undefined,
|
filter: this.props.searchParams.get('filter') || undefined,
|
||||||
offset: 0,
|
offset: 0,
|
||||||
@@ -240,7 +243,7 @@ class ArtikelSearchPage extends Component {
|
|||||||
|
|
||||||
// Delegate fetch + debounce to the global StorageManager.
|
// Delegate fetch + debounce to the global StorageManager.
|
||||||
// The callback pushes results into local component state.
|
// The callback pushes results into local component state.
|
||||||
this.context.doSearch({
|
this.storageManager?.doSearch({
|
||||||
target: trimmed,
|
target: trimmed,
|
||||||
filter: filter || undefined,
|
filter: filter || undefined,
|
||||||
offset,
|
offset,
|
||||||
|
|||||||
@@ -28,7 +28,8 @@ import { backendErrorFrom } from '../../utils/backendError';
|
|||||||
import { notifyError } from '../../utils/notify';
|
import { notifyError } from '../../utils/notify';
|
||||||
import { generatePdfThumbnail } from '../../utils/pdfThumbnail';
|
import { generatePdfThumbnail } from '../../utils/pdfThumbnail';
|
||||||
import { extractPdfText } from '../../utils/pdfText';
|
import { extractPdfText } from '../../utils/pdfText';
|
||||||
import { deliveryEditorAutomat } from '../../automats/deliveryEditorAutomat';
|
import { getDeliveryEditorAutomat } from '../../automats/deliveryEditorAutomat';
|
||||||
|
import { delivery as deliveryApi } from '../../api';
|
||||||
|
|
||||||
class LieferungNeuPage extends PureComponent {
|
class LieferungNeuPage extends PureComponent {
|
||||||
static contextType = StorageManagerContext;
|
static contextType = StorageManagerContext;
|
||||||
@@ -36,9 +37,10 @@ class LieferungNeuPage extends PureComponent {
|
|||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.cfg = manifestConfig('delivery');
|
this.cfg = manifestConfig('delivery');
|
||||||
|
this.automat = getDeliveryEditorAutomat(props.docId);
|
||||||
this.state = {
|
this.state = {
|
||||||
...deliveryEditorAutomat.state,
|
...this.automat.state,
|
||||||
status: 'loadingParties',
|
status: props.docId != null ? 'loading' : 'loadingParties',
|
||||||
error: null,
|
error: null,
|
||||||
partiesLoading: true,
|
partiesLoading: true,
|
||||||
saving: false,
|
saving: false,
|
||||||
@@ -66,32 +68,38 @@ class LieferungNeuPage extends PureComponent {
|
|||||||
this.handleAttachmentClick = this.handleAttachmentClick.bind(this);
|
this.handleAttachmentClick = this.handleAttachmentClick.bind(this);
|
||||||
this.handleCloseTextEditor = this.handleCloseTextEditor.bind(this);
|
this.handleCloseTextEditor = this.handleCloseTextEditor.bind(this);
|
||||||
this.handleSaveTextEditor = this.handleSaveTextEditor.bind(this);
|
this.handleSaveTextEditor = this.handleSaveTextEditor.bind(this);
|
||||||
|
this.handleTextEditorContentLoaded = this.handleTextEditorContentLoaded.bind(this);
|
||||||
|
this.ensureAttachmentFile = this.ensureAttachmentFile.bind(this);
|
||||||
this.handleSkuBlur = this.handleSkuBlur.bind(this);
|
this.handleSkuBlur = this.handleSkuBlur.bind(this);
|
||||||
this.handleCreateArticleForLine = this.handleCreateArticleForLine.bind(this);
|
this.handleCreateArticleForLine = this.handleCreateArticleForLine.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.unsubscribe = deliveryEditorAutomat.subscribe(this);
|
this.unsubscribe = this.automat.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 {
|
||||||
void this.loadPartiesOnly();
|
void this.loadPartiesOnly();
|
||||||
if (this.props.initialDelivery) {
|
if (this.props.initialDelivery) {
|
||||||
deliveryEditorAutomat.actions.setDelivery(this.props.initialDelivery);
|
this.automat.actions.setDelivery(this.props.initialDelivery);
|
||||||
} else {
|
|
||||||
deliveryEditorAutomat.actions.resetDelivery();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
componentDidUpdate(prevProps) {
|
||||||
if (prevProps.docId !== this.props.docId) {
|
if (prevProps.docId !== this.props.docId) {
|
||||||
|
this.unsubscribe?.();
|
||||||
|
this.automat = getDeliveryEditorAutomat(this.props.docId);
|
||||||
|
this.unsubscribe = this.automat.subscribe(this);
|
||||||
|
this.setState({ ...this.automat.state });
|
||||||
if (this.props.docId != null) {
|
if (this.props.docId != null) {
|
||||||
void this.loadEdit();
|
void this.loadEdit();
|
||||||
} else {
|
} else {
|
||||||
void this.loadPartiesOnly();
|
void this.loadPartiesOnly();
|
||||||
deliveryEditorAutomat.actions.resetDelivery();
|
if (this.props.initialDelivery) {
|
||||||
|
this.automat.actions.setDelivery(this.props.initialDelivery);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -110,7 +118,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;
|
||||||
deliveryEditorAutomat.actions.setDeliveryEntities(parties);
|
this.automat.actions.setDeliveryEntities(parties);
|
||||||
this.setState({
|
this.setState({
|
||||||
status: 'ready',
|
status: 'ready',
|
||||||
partiesLoading: false,
|
partiesLoading: false,
|
||||||
@@ -145,7 +153,7 @@ class LieferungNeuPage extends PureComponent {
|
|||||||
]);
|
]);
|
||||||
if (this._cancelled) return;
|
if (this._cancelled) return;
|
||||||
|
|
||||||
deliveryEditorAutomat.actions.setDeliveryEntities(parties);
|
this.automat.actions.setDeliveryEntities(parties);
|
||||||
|
|
||||||
requireObject(docRes, cfg.getPathLabel);
|
requireObject(docRes, cfg.getPathLabel);
|
||||||
requireObject(docRes[cfg.docField], cfg.docField);
|
requireObject(docRes[cfg.docField], cfg.docField);
|
||||||
@@ -154,17 +162,17 @@ class LieferungNeuPage extends PureComponent {
|
|||||||
const lines =
|
const lines =
|
||||||
apiLines.length > 0 ? apiLines.map(lineFromApi) : [emptyLine()];
|
apiLines.length > 0 ? apiLines.map(lineFromApi) : [emptyLine()];
|
||||||
|
|
||||||
deliveryEditorAutomat.actions.setId(doc.id ?? docId);
|
this.automat.actions.setId(doc.id ?? docId);
|
||||||
deliveryEditorAutomat.actions.setDate(dateInputValue(doc.date));
|
this.automat.actions.setDate(dateInputValue(doc.date));
|
||||||
if (doc[cfg.partyField] != null) deliveryEditorAutomat.actions.setDeliveryEntity(doc[cfg.partyField]);
|
if (doc[cfg.partyField] != null) this.automat.actions.setDeliveryEntity(doc[cfg.partyField]);
|
||||||
if (doc.number != null) deliveryEditorAutomat.actions.setNumber(doc.number);
|
if (doc.number != null) this.automat.actions.setNumber(doc.number);
|
||||||
if (doc.externalNumber != null) deliveryEditorAutomat.actions.setExternalNumber(String(doc.externalNumber));
|
if (doc.externalNumber != null) this.automat.actions.setExternalNumber(String(doc.externalNumber));
|
||||||
deliveryEditorAutomat.actions.setLineitems(lines);
|
this.automat.actions.setLineitems(lines);
|
||||||
|
|
||||||
// Restore attachments with thumbnails
|
// Restore attachments with thumbnails
|
||||||
const apiFiles = doc.files ?? [];
|
const apiFiles = doc.files ?? [];
|
||||||
const attachments = await restoreAttachmentsFromApi(apiFiles, this.state.attachments);
|
const attachments = await restoreAttachmentsFromApi(apiFiles, this.state.attachments);
|
||||||
deliveryEditorAutomat.actions.setAttachments(attachments);
|
this.automat.actions.setAttachments(attachments);
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
status: 'ready',
|
status: 'ready',
|
||||||
@@ -186,19 +194,19 @@ class LieferungNeuPage extends PureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handlePartyChange(partyId) {
|
handlePartyChange(partyId) {
|
||||||
deliveryEditorAutomat.actions.setDeliveryEntity(partyId);
|
this.automat.actions.setDeliveryEntity(partyId);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleDateChange(date) {
|
handleDateChange(date) {
|
||||||
deliveryEditorAutomat.actions.setDate(date);
|
this.automat.actions.setDate(date);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleNumberChange(number) {
|
handleNumberChange(number) {
|
||||||
deliveryEditorAutomat.actions.setNumber(number);
|
this.automat.actions.setNumber(number);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleExternalNumberChange(externalNumber) {
|
handleExternalNumberChange(externalNumber) {
|
||||||
deliveryEditorAutomat.actions.setExternalNumber(externalNumber);
|
this.automat.actions.setExternalNumber(externalNumber);
|
||||||
}
|
}
|
||||||
|
|
||||||
findLineIndex(key) {
|
findLineIndex(key) {
|
||||||
@@ -214,7 +222,7 @@ class LieferungNeuPage extends PureComponent {
|
|||||||
|
|
||||||
handleLineChange(key, field, value) {
|
handleLineChange(key, field, value) {
|
||||||
const index = this.findLineIndex(key);
|
const index = this.findLineIndex(key);
|
||||||
deliveryEditorAutomat.actions.updateLineitem({
|
this.automat.actions.updateLineitem({
|
||||||
index,
|
index,
|
||||||
key,
|
key,
|
||||||
[field]: value,
|
[field]: value,
|
||||||
@@ -229,7 +237,7 @@ class LieferungNeuPage extends PureComponent {
|
|||||||
}
|
}
|
||||||
this._skuDebounceTimers[key] = setTimeout(() => {
|
this._skuDebounceTimers[key] = setTimeout(() => {
|
||||||
if (this._skuDebounceTimers) delete this._skuDebounceTimers[key];
|
if (this._skuDebounceTimers) delete this._skuDebounceTimers[key];
|
||||||
deliveryEditorAutomat.actions.fetchSupplierArticleForSku(value, key);
|
this.automat.actions.fetchSupplierArticleForSku(value, key);
|
||||||
}, 350);
|
}, 350);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -239,7 +247,7 @@ class LieferungNeuPage extends PureComponent {
|
|||||||
clearTimeout(this._skuDebounceTimers[key]);
|
clearTimeout(this._skuDebounceTimers[key]);
|
||||||
delete this._skuDebounceTimers[key];
|
delete this._skuDebounceTimers[key];
|
||||||
}
|
}
|
||||||
deliveryEditorAutomat.actions.fetchSupplierArticleForSku(value, key);
|
this.automat.actions.fetchSupplierArticleForSku(value, key);
|
||||||
}
|
}
|
||||||
|
|
||||||
async handleCreateArticleForLine(line) {
|
async handleCreateArticleForLine(line) {
|
||||||
@@ -294,7 +302,7 @@ class LieferungNeuPage extends PureComponent {
|
|||||||
|
|
||||||
// Keep the supplier SKU in the delivery line, but refresh supplier article data
|
// Keep the supplier SKU in the delivery line, but refresh supplier article data
|
||||||
// using the supplier SKU (not the internal SKU)
|
// using the supplier SKU (not the internal SKU)
|
||||||
deliveryEditorAutomat.actions.fetchSupplierArticleForSku(supplierSku, line.key);
|
this.automat.actions.fetchSupplierArticleForSku(supplierSku, line.key);
|
||||||
|
|
||||||
enqueueSnackbar(`Artikel "${internalSku}" erstellt und Lieferanten-Artikel "${supplierSku}" zugeordnet.`, { variant: 'success' });
|
enqueueSnackbar(`Artikel "${internalSku}" erstellt und Lieferanten-Artikel "${supplierSku}" zugeordnet.`, { variant: 'success' });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -304,69 +312,105 @@ class LieferungNeuPage extends PureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleAddLine() {
|
handleAddLine() {
|
||||||
deliveryEditorAutomat.actions.addLineitem();
|
this.automat.actions.addLineitem();
|
||||||
}
|
}
|
||||||
|
|
||||||
handleRemoveLine(key) {
|
handleRemoveLine(key) {
|
||||||
const index = this.findLineIndex(key);
|
const index = this.findLineIndex(key);
|
||||||
deliveryEditorAutomat.actions.removeLineitem(index >= 0 ? index : key);
|
this.automat.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
|
const isPdf =
|
||||||
let thumbnail = null;
|
file.type === 'application/pdf' || file.name?.toLowerCase().endsWith('.pdf');
|
||||||
try {
|
const isCsv =
|
||||||
thumbnail = await generatePdfThumbnail(file);
|
file.type === 'text/csv' || file.name?.toLowerCase().endsWith('.csv');
|
||||||
} catch (err) {
|
|
||||||
console.error('Fehler beim Erstellen des PDF-Thumbnails:', err);
|
|
||||||
}
|
|
||||||
|
|
||||||
const pdfId = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
if (isPdf) {
|
||||||
const pdfAttachment = {
|
// 1. Generate thumbnail for PDF
|
||||||
id: pdfId,
|
let thumbnail = null;
|
||||||
file,
|
try {
|
||||||
name: file.name,
|
thumbnail = await generatePdfThumbnail(file);
|
||||||
size: file.size,
|
} catch (err) {
|
||||||
type: 'application/pdf',
|
console.error('Fehler beim Erstellen des PDF-Thumbnails:', err);
|
||||||
thumbnail,
|
}
|
||||||
uploadedAt: new Date().toISOString(),
|
|
||||||
};
|
|
||||||
deliveryEditorAutomat.actions.addAttachment(pdfAttachment);
|
|
||||||
|
|
||||||
// 2. Extract text using pdftotext-wasm to create 2nd file (textfile)
|
const pdfId = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
||||||
try {
|
const pdfAttachment = {
|
||||||
const text = await extractPdfText(file, { layout: true });
|
id: pdfId,
|
||||||
const txtName = file.name.replace(/\.pdf$/i, '') + '.txt';
|
file,
|
||||||
const txtFile = new File([text], txtName, { type: 'text/plain;charset=utf-8' });
|
name: file.name,
|
||||||
const txtAttachment = {
|
size: file.size,
|
||||||
id: `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
type: 'application/pdf',
|
||||||
file: txtFile,
|
thumbnail,
|
||||||
name: txtFile.name,
|
|
||||||
size: txtFile.size,
|
|
||||||
type: 'text/plain',
|
|
||||||
text,
|
|
||||||
parentPdfId: pdfId,
|
|
||||||
uploadedAt: new Date().toISOString(),
|
uploadedAt: new Date().toISOString(),
|
||||||
};
|
};
|
||||||
deliveryEditorAutomat.actions.addAttachment(txtAttachment);
|
this.automat.actions.addAttachment(pdfAttachment);
|
||||||
} catch (txtErr) {
|
|
||||||
console.error('Fehler beim Extrahieren von Text mit pdftotext-wasm:', txtErr);
|
// 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(),
|
||||||
|
};
|
||||||
|
this.automat.actions.addAttachment(txtAttachment);
|
||||||
|
} catch (txtErr) {
|
||||||
|
console.error('Fehler beim Extrahieren von Text mit pdftotext-wasm:', txtErr);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Direct text / CSV attachment
|
||||||
|
let text = '';
|
||||||
|
try {
|
||||||
|
text = await file.text();
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Fehler beim Lesen der Datei:', err);
|
||||||
|
}
|
||||||
|
const attachmentId = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
||||||
|
const textAttachment = {
|
||||||
|
id: attachmentId,
|
||||||
|
file,
|
||||||
|
name: file.name,
|
||||||
|
size: file.size,
|
||||||
|
type: isCsv ? 'text/csv' : 'text/plain',
|
||||||
|
text,
|
||||||
|
uploadedAt: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
this.automat.actions.addAttachment(textAttachment);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Run SSE parsing with backend
|
// 3. Run SSE parsing with backend (POST /api/delivery/parse)
|
||||||
await this.runPdfParse(file);
|
await this.runParse(file);
|
||||||
}
|
}
|
||||||
|
|
||||||
async handlePdfRescan(attachment) {
|
async handlePdfRescan(attachment) {
|
||||||
if (!attachment?.file || this.state.pdfUploading) return;
|
if (!attachment || this.state.pdfUploading) return;
|
||||||
|
|
||||||
|
let targetAttachment = attachment;
|
||||||
|
if (!targetAttachment.file || targetAttachment.file.size === 0) {
|
||||||
|
const file = await this.ensureAttachmentFile(targetAttachment);
|
||||||
|
if (!file || file.size === 0) {
|
||||||
|
enqueueSnackbar('Datei konnte nicht geladen werden', { variant: 'error' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
targetAttachment = { ...targetAttachment, file };
|
||||||
|
}
|
||||||
|
|
||||||
// Refresh textfile upon rescan for PDF
|
// Refresh textfile upon rescan for PDF
|
||||||
const isPdf = attachment.type === 'application/pdf' || attachment.name.toLowerCase().endsWith('.pdf');
|
const isPdf = targetAttachment.type === 'application/pdf' || targetAttachment.name.toLowerCase().endsWith('.pdf');
|
||||||
if (isPdf) {
|
if (isPdf) {
|
||||||
try {
|
try {
|
||||||
const text = await extractPdfText(attachment.file, { layout: true });
|
const text = await extractPdfText(targetAttachment.file, { layout: true });
|
||||||
const txtName = attachment.name.replace(/\.pdf$/i, '') + '.txt';
|
const txtName = attachment.name.replace(/\.pdf$/i, '') + '.txt';
|
||||||
const txtFile = new File([text], txtName, { type: 'text/plain;charset=utf-8' });
|
const txtFile = new File([text], txtName, { type: 'text/plain;charset=utf-8' });
|
||||||
|
|
||||||
@@ -374,7 +418,7 @@ class LieferungNeuPage extends PureComponent {
|
|||||||
(a) => a.parentPdfId === attachment.id || a.name === txtName,
|
(a) => a.parentPdfId === attachment.id || a.name === txtName,
|
||||||
);
|
);
|
||||||
if (existingTxt) {
|
if (existingTxt) {
|
||||||
deliveryEditorAutomat.actions.updateAttachment({
|
this.automat.actions.updateAttachment({
|
||||||
id: existingTxt.id,
|
id: existingTxt.id,
|
||||||
file: txtFile,
|
file: txtFile,
|
||||||
size: txtFile.size,
|
size: txtFile.size,
|
||||||
@@ -382,7 +426,7 @@ class LieferungNeuPage extends PureComponent {
|
|||||||
isDirty: true,
|
isDirty: true,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
deliveryEditorAutomat.actions.addAttachment({
|
this.automat.actions.addAttachment({
|
||||||
id: `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
id: `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
||||||
file: txtFile,
|
file: txtFile,
|
||||||
name: txtFile.name,
|
name: txtFile.name,
|
||||||
@@ -403,7 +447,7 @@ class LieferungNeuPage extends PureComponent {
|
|||||||
|
|
||||||
handlePdfDelete(attachment) {
|
handlePdfDelete(attachment) {
|
||||||
if (!attachment?.id) return;
|
if (!attachment?.id) return;
|
||||||
deliveryEditorAutomat.actions.removeAttachment(attachment.id);
|
this.automat.actions.removeAttachment(attachment.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
async runParse(file) {
|
async runParse(file) {
|
||||||
@@ -500,24 +544,24 @@ class LieferungNeuPage extends PureComponent {
|
|||||||
if (matched) supplierId = matched.id;
|
if (matched) supplierId = matched.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (docDate) deliveryEditorAutomat.actions.setDate(docDate);
|
if (docDate) this.automat.actions.setDate(docDate);
|
||||||
if (supplierId != null) deliveryEditorAutomat.actions.setDeliveryEntity(supplierId);
|
if (supplierId != null) this.automat.actions.setDeliveryEntity(supplierId);
|
||||||
if (extNum != null) {
|
if (extNum != null) {
|
||||||
deliveryEditorAutomat.actions.setExternalNumber(String(extNum));
|
this.automat.actions.setExternalNumber(String(extNum));
|
||||||
}
|
}
|
||||||
if (docType) deliveryEditorAutomat.actions.setType(docType);
|
if (docType) this.automat.actions.setType(docType);
|
||||||
if (newLines && newLines.length > 0) {
|
if (newLines && newLines.length > 0) {
|
||||||
deliveryEditorAutomat.actions.setLineitems(newLines);
|
this.automat.actions.setLineitems(newLines);
|
||||||
}
|
}
|
||||||
if (notes.length > 0) {
|
if (notes.length > 0) {
|
||||||
deliveryEditorAutomat.actions.setNotes(notes);
|
this.automat.actions.setNotes(notes);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Supply whole article list to GET /supplier/{id}/articles
|
// Supply whole article list to GET /supplier/{id}/articles
|
||||||
const effectiveSupplierId = supplierId ?? deliveryEditorAutomat.state.deliveryEntity ?? deliveryEditorAutomat.state.partyId;
|
const effectiveSupplierId = supplierId ?? this.automat.state.deliveryEntity ?? this.automat.state.partyId;
|
||||||
const effectiveLines = (newLines && newLines.length > 0) ? newLines : deliveryEditorAutomat.state.lines;
|
const effectiveLines = (newLines && newLines.length > 0) ? newLines : this.automat.state.lines;
|
||||||
if (effectiveSupplierId != null && effectiveLines && effectiveLines.length > 0) {
|
if (effectiveSupplierId != null && effectiveLines && effectiveLines.length > 0) {
|
||||||
deliveryEditorAutomat.actions.fetchSupplierArticles(effectiveSupplierId, effectiveLines);
|
this.automat.actions.fetchSupplierArticles(effectiveSupplierId, effectiveLines);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -567,6 +611,74 @@ class LieferungNeuPage extends PureComponent {
|
|||||||
this.setState({ pdfDialogOpen: false });
|
this.setState({ pdfDialogOpen: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async ensureAttachmentFile(attachment) {
|
||||||
|
if (!attachment) return null;
|
||||||
|
const hasContent =
|
||||||
|
(attachment.file instanceof Blob || attachment.file instanceof File) &&
|
||||||
|
attachment.file.size > 0;
|
||||||
|
if (hasContent) {
|
||||||
|
return attachment.file;
|
||||||
|
}
|
||||||
|
|
||||||
|
const docId = this.props.docId ?? attachment.manifestId ?? this.state.id;
|
||||||
|
const fileId = attachment.fileId ?? attachment.id;
|
||||||
|
if (docId == null || fileId == null) {
|
||||||
|
return attachment.file || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const getFile =
|
||||||
|
this.context?.delivery?.getFile?.bind(this.context.delivery) ||
|
||||||
|
deliveryApi.getFile;
|
||||||
|
const blob = await getFile(docId, fileId);
|
||||||
|
const filename = attachment.name || 'Dokument';
|
||||||
|
const mimetype = attachment.type || blob.type || 'application/octet-stream';
|
||||||
|
const file = new File([blob], filename, { type: mimetype });
|
||||||
|
|
||||||
|
let text = attachment.text;
|
||||||
|
const isPdf =
|
||||||
|
mimetype === 'application/pdf' || filename.toLowerCase().endsWith('.pdf');
|
||||||
|
const isText =
|
||||||
|
!isPdf &&
|
||||||
|
(mimetype.startsWith('text/') ||
|
||||||
|
filename.toLowerCase().endsWith('.txt') ||
|
||||||
|
filename.toLowerCase().endsWith('.csv'));
|
||||||
|
|
||||||
|
if (isText && typeof text !== 'string') {
|
||||||
|
try {
|
||||||
|
text = await blob.text();
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Fehler beim Dekodieren des Texts:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.automat.actions.updateAttachment({
|
||||||
|
id: attachment.id,
|
||||||
|
file,
|
||||||
|
size: blob.size,
|
||||||
|
...(typeof text === 'string' ? { text } : {}),
|
||||||
|
});
|
||||||
|
|
||||||
|
return file;
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Fehler beim Laden der Dateiinhalte:', err);
|
||||||
|
notifyError(err);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleTextEditorContentLoaded(attachment, text, blob) {
|
||||||
|
const file = new File([blob], attachment.name || 'Dokument.txt', {
|
||||||
|
type: attachment.type || 'text/plain;charset=utf-8',
|
||||||
|
});
|
||||||
|
this.automat.actions.updateAttachment({
|
||||||
|
id: attachment.id,
|
||||||
|
file,
|
||||||
|
size: blob.size,
|
||||||
|
text,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
handleAttachmentClick(attachment) {
|
handleAttachmentClick(attachment) {
|
||||||
if (!attachment) return;
|
if (!attachment) return;
|
||||||
const isPdf =
|
const isPdf =
|
||||||
@@ -575,19 +687,55 @@ class LieferungNeuPage extends PureComponent {
|
|||||||
const isText =
|
const isText =
|
||||||
!isPdf &&
|
!isPdf &&
|
||||||
(attachment.type?.startsWith('text/') ||
|
(attachment.type?.startsWith('text/') ||
|
||||||
attachment.name?.toLowerCase().endsWith('.txt'));
|
attachment.name?.toLowerCase().endsWith('.txt') ||
|
||||||
|
attachment.name?.toLowerCase().endsWith('.csv'));
|
||||||
|
|
||||||
if (isText) {
|
if (isText) {
|
||||||
this.setState({
|
this.setState({
|
||||||
textEditorOpen: true,
|
textEditorOpen: true,
|
||||||
textEditorAttachment: attachment,
|
textEditorAttachment: attachment,
|
||||||
});
|
});
|
||||||
} else if (attachment.file instanceof Blob || attachment.file instanceof File) {
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isPdf) {
|
||||||
|
if (
|
||||||
|
(attachment.file instanceof Blob || attachment.file instanceof File) &&
|
||||||
|
attachment.file.size > 0
|
||||||
|
) {
|
||||||
|
const url = URL.createObjectURL(attachment.file);
|
||||||
|
window.open(url, '_blank');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const docId = this.props.docId ?? attachment.manifestId ?? this.state.id;
|
||||||
|
const fileId = attachment.fileId ?? attachment.id;
|
||||||
|
if (docId != null && fileId != null) {
|
||||||
|
window.open(
|
||||||
|
`/api/delivery/${encodeURIComponent(docId)}/file/${encodeURIComponent(fileId)}`,
|
||||||
|
'_blank',
|
||||||
|
);
|
||||||
|
void this.ensureAttachmentFile(attachment);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (attachment.file instanceof Blob || attachment.file instanceof File) {
|
||||||
if (attachment.file.size > 0) {
|
if (attachment.file.size > 0) {
|
||||||
const url = URL.createObjectURL(attachment.file);
|
const url = URL.createObjectURL(attachment.file);
|
||||||
window.open(url, '_blank');
|
window.open(url, '_blank');
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const docId = this.props.docId ?? attachment.manifestId ?? this.state.id;
|
||||||
|
const fileId = attachment.fileId ?? attachment.id;
|
||||||
|
if (docId != null && fileId != null) {
|
||||||
|
window.open(
|
||||||
|
`/api/delivery/${encodeURIComponent(docId)}/file/${encodeURIComponent(fileId)}`,
|
||||||
|
'_blank',
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleCloseTextEditor() {
|
handleCloseTextEditor() {
|
||||||
@@ -605,7 +753,7 @@ class LieferungNeuPage extends PureComponent {
|
|||||||
type: 'text/plain;charset=utf-8',
|
type: 'text/plain;charset=utf-8',
|
||||||
});
|
});
|
||||||
|
|
||||||
deliveryEditorAutomat.actions.updateAttachment({
|
this.automat.actions.updateAttachment({
|
||||||
id: attachment.id,
|
id: attachment.id,
|
||||||
file: newFile,
|
file: newFile,
|
||||||
size: newFile.size,
|
size: newFile.size,
|
||||||
@@ -657,7 +805,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' });
|
||||||
deliveryEditorAutomat.actions.resetDelivery();
|
this.automat.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);
|
||||||
@@ -670,17 +818,17 @@ 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()];
|
||||||
|
|
||||||
deliveryEditorAutomat.actions.setId(doc.id ?? docId);
|
this.automat.actions.setId(doc.id ?? docId);
|
||||||
deliveryEditorAutomat.actions.setDate(dateInputValue(doc.date));
|
this.automat.actions.setDate(dateInputValue(doc.date));
|
||||||
if (doc[cfg.partyField] != null) deliveryEditorAutomat.actions.setDeliveryEntity(doc[cfg.partyField]);
|
if (doc[cfg.partyField] != null) this.automat.actions.setDeliveryEntity(doc[cfg.partyField]);
|
||||||
if (doc.number != null) deliveryEditorAutomat.actions.setNumber(doc.number);
|
if (doc.number != null) this.automat.actions.setNumber(doc.number);
|
||||||
if (doc.externalNumber != null) deliveryEditorAutomat.actions.setExternalNumber(String(doc.externalNumber));
|
if (doc.externalNumber != null) this.automat.actions.setExternalNumber(String(doc.externalNumber));
|
||||||
deliveryEditorAutomat.actions.setLineitems(lines);
|
this.automat.actions.setLineitems(lines);
|
||||||
|
|
||||||
// Restore attachments with thumbnails
|
// Restore attachments with thumbnails
|
||||||
const apiFiles = doc.files ?? [];
|
const apiFiles = doc.files ?? [];
|
||||||
const attachments = await restoreAttachmentsFromApi(apiFiles, this.state.attachments);
|
const attachments = await restoreAttachmentsFromApi(apiFiles, this.state.attachments);
|
||||||
deliveryEditorAutomat.actions.setAttachments(attachments);
|
this.automat.actions.setAttachments(attachments);
|
||||||
|
|
||||||
enqueueSnackbar(cfg.savedMsg, { variant: 'success' });
|
enqueueSnackbar(cfg.savedMsg, { variant: 'success' });
|
||||||
this.setState({ saving: false });
|
this.setState({ saving: false });
|
||||||
@@ -807,9 +955,11 @@ class LieferungNeuPage extends PureComponent {
|
|||||||
/>
|
/>
|
||||||
<TextEditorDialog
|
<TextEditorDialog
|
||||||
open={this.state.textEditorOpen}
|
open={this.state.textEditorOpen}
|
||||||
|
docId={this.props.docId ?? this.state.id}
|
||||||
attachment={this.state.textEditorAttachment}
|
attachment={this.state.textEditorAttachment}
|
||||||
onClose={this.handleCloseTextEditor}
|
onClose={this.handleCloseTextEditor}
|
||||||
onSave={this.handleSaveTextEditor}
|
onSave={this.handleSaveTextEditor}
|
||||||
|
onContentLoad={this.handleTextEditorContentLoaded}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user