file api
This commit is contained in:
@@ -105,6 +105,11 @@ export class ApiClient {
|
||||
update: (id, body) => this.#json('PATCH', `/api/delivery/${enc(id)}`, { body }),
|
||||
/** DELETE /api/delivery/{id} → 204 */
|
||||
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"
|
||||
* 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' });
|
||||
},
|
||||
|
||||
/**
|
||||
* 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 { 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.
|
||||
@@ -9,7 +18,7 @@ export const StorageManagerContext = createContext(null);
|
||||
export function StorageManagerProvider({ children }) {
|
||||
const mgrRef = useRef(null);
|
||||
if (!mgrRef.current) {
|
||||
mgrRef.current = new StorageManager();
|
||||
mgrRef.current = getStorageManager();
|
||||
}
|
||||
|
||||
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.
|
||||
*/
|
||||
export function useStorageManager() {
|
||||
const ctx = useContext(StorageManagerContext);
|
||||
if (!ctx) {
|
||||
throw new Error('useStorageManager must be used within a <StorageManagerProvider>');
|
||||
}
|
||||
const ctx = useContext(StorageManagerContext) || getStorageManager();
|
||||
return ctx;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Automat } from 'automat';
|
||||
import { Automat } from 'react-automat';
|
||||
import { supplier as supplierApi } from '../api';
|
||||
|
||||
export function createEmptyLineitem(line = {}) {
|
||||
@@ -34,7 +34,7 @@ export function buildSupplierArticlesMap(articles = [], existingMap = {}) {
|
||||
map[artSku.toLowerCase()] = item;
|
||||
}
|
||||
}
|
||||
// Map by EANs
|
||||
// Map by Eans
|
||||
const eans = item.article?.eans ?? item.article?.articleean ?? [];
|
||||
for (const ean of eans) {
|
||||
if (ean != null) {
|
||||
@@ -77,336 +77,367 @@ export const initialState = {
|
||||
...initialDelivery,
|
||||
};
|
||||
|
||||
export const deliveryEditorAutomat = new Automat(initialState, {
|
||||
setId(id) {
|
||||
deliveryEditorAutomat.setState({ id, deliveryId: id });
|
||||
},
|
||||
export function createDeliveryEditorAutomat(options = {}) {
|
||||
const name = typeof options === 'string' ? options : options?.name ?? null;
|
||||
if (name) {
|
||||
const existing = Automat.get(name);
|
||||
if (existing) return existing;
|
||||
}
|
||||
|
||||
setDeliveryEntities(deliveryEntities) {
|
||||
deliveryEditorAutomat.setState({ deliveryEntities: deliveryEntities ?? [] });
|
||||
},
|
||||
let automat;
|
||||
const actions = {
|
||||
setId(id) {
|
||||
automat.setState({ id, deliveryId: id });
|
||||
},
|
||||
|
||||
setDelivery(delivery = {}) {
|
||||
const rawLines = delivery.lines ?? delivery.lineitems ?? [];
|
||||
const lines = Array.isArray(rawLines) ? rawLines : [];
|
||||
const partyId = delivery.partyId ?? delivery.deliveryEntity ?? null;
|
||||
const extNum = delivery.externalNumber ?? delivery.external_number ?? null;
|
||||
const id = delivery.id ?? delivery.deliveryId ?? null;
|
||||
const notes = Array.isArray(delivery.notes)
|
||||
? delivery.notes
|
||||
: delivery.notes ? [delivery.notes] : [];
|
||||
const rawAttachments = delivery.attachments ?? delivery.files ?? [];
|
||||
const attachments = Array.isArray(rawAttachments) ? rawAttachments : [];
|
||||
setDeliveryEntities(deliveryEntities) {
|
||||
automat.setState({ deliveryEntities: deliveryEntities ?? [] });
|
||||
},
|
||||
|
||||
deliveryEditorAutomat.setState({
|
||||
...initialDelivery,
|
||||
...delivery,
|
||||
id,
|
||||
deliveryId: id,
|
||||
partyId,
|
||||
deliveryEntity: partyId,
|
||||
externalNumber: extNum,
|
||||
external_number: extNum,
|
||||
lines,
|
||||
lineitems: lines,
|
||||
notes,
|
||||
attachments,
|
||||
files: attachments,
|
||||
});
|
||||
setDelivery(delivery = {}) {
|
||||
const rawLines = delivery.lines ?? delivery.lineitems ?? [];
|
||||
const lines = Array.isArray(rawLines) ? rawLines : [];
|
||||
const partyId = delivery.partyId ?? delivery.deliveryEntity ?? null;
|
||||
const extNum = delivery.externalNumber ?? delivery.external_number ?? null;
|
||||
const id = delivery.id ?? delivery.deliveryId ?? null;
|
||||
const notes = Array.isArray(delivery.notes)
|
||||
? delivery.notes
|
||||
: delivery.notes ? [delivery.notes] : [];
|
||||
const rawAttachments = delivery.attachments ?? delivery.files ?? [];
|
||||
const attachments = Array.isArray(rawAttachments) ? rawAttachments : [];
|
||||
|
||||
if (partyId && lines.length > 0) {
|
||||
deliveryEditorAutomat.actions.fetchSupplierArticles(partyId, lines);
|
||||
}
|
||||
},
|
||||
automat.setState({
|
||||
...initialDelivery,
|
||||
...delivery,
|
||||
id,
|
||||
deliveryId: id,
|
||||
partyId,
|
||||
deliveryEntity: partyId,
|
||||
externalNumber: extNum,
|
||||
external_number: extNum,
|
||||
lines,
|
||||
lineitems: lines,
|
||||
notes,
|
||||
attachments,
|
||||
files: attachments,
|
||||
});
|
||||
|
||||
setDate(date) {
|
||||
deliveryEditorAutomat.setState({ date });
|
||||
},
|
||||
if (partyId && lines.length > 0) {
|
||||
automat.actions.fetchSupplierArticles(partyId, lines);
|
||||
}
|
||||
},
|
||||
|
||||
setDeliveryEntity(entityId) {
|
||||
const prevId = deliveryEditorAutomat.state.deliveryEntity ?? deliveryEditorAutomat.state.partyId;
|
||||
const idChanged = prevId !== entityId;
|
||||
deliveryEditorAutomat.setState({
|
||||
deliveryEntity: entityId,
|
||||
partyId: entityId,
|
||||
...(idChanged
|
||||
? {
|
||||
supplierArticles: [],
|
||||
supplierArticlesMap: {},
|
||||
supplierArticlesByLineKey: {},
|
||||
loadingLineKeys: {},
|
||||
setDate(date) {
|
||||
automat.setState({ date });
|
||||
},
|
||||
|
||||
setDeliveryEntity(entityId) {
|
||||
const prevId = automat.state.deliveryEntity ?? automat.state.partyId;
|
||||
const idChanged = prevId !== entityId;
|
||||
automat.setState({
|
||||
deliveryEntity: entityId,
|
||||
partyId: entityId,
|
||||
...(idChanged
|
||||
? {
|
||||
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({
|
||||
supplierArticles: articles,
|
||||
supplierArticlesMap: map,
|
||||
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) {
|
||||
const entityId = automat.state.deliveryEntity ?? automat.state.partyId;
|
||||
const trimmed = sku != null ? String(sku).trim() : '';
|
||||
|
||||
async fetchSupplierArticleForSku(sku, lineKey) {
|
||||
const entityId = deliveryEditorAutomat.state.deliveryEntity ?? deliveryEditorAutomat.state.partyId;
|
||||
const trimmed = sku != null ? String(sku).trim() : '';
|
||||
|
||||
if (!entityId || !trimmed) {
|
||||
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);
|
||||
if (!entityId || !trimmed) {
|
||||
if (lineKey != null) {
|
||||
const byLineKey = { ...(automat.state.supplierArticlesByLineKey || {}) };
|
||||
byLineKey[lineKey] = null;
|
||||
automat.setState({ supplierArticlesByLineKey: byLineKey });
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const nextLoading = { ...(deliveryEditorAutomat.state.loadingLineKeys || {}) };
|
||||
if (lineKey != null) delete nextLoading[lineKey];
|
||||
|
||||
deliveryEditorAutomat.setState({
|
||||
supplierArticles: mergedList,
|
||||
supplierArticlesMap: nextMap,
|
||||
supplierArticlesByLineKey: byLineKey,
|
||||
loadingLineKeys: nextLoading,
|
||||
supplierArticlesLoading: Object.keys(nextLoading).length > 0,
|
||||
const prevLoading = automat.state.loadingLineKeys || {};
|
||||
automat.setState({
|
||||
loadingLineKeys: lineKey != null ? { ...prevLoading, [lineKey]: true } : prevLoading,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Fehler beim Abrufen des Artikels für SKU:', err);
|
||||
const nextLoading = { ...(deliveryEditorAutomat.state.loadingLineKeys || {}) };
|
||||
if (lineKey != null) delete nextLoading[lineKey];
|
||||
deliveryEditorAutomat.setState({
|
||||
loadingLineKeys: nextLoading,
|
||||
supplierArticlesLoading: Object.keys(nextLoading).length > 0,
|
||||
|
||||
try {
|
||||
const res = await supplierApi.getArticles(entityId, [trimmed]);
|
||||
const articles = Array.isArray(res?.articles) ? res.articles : [];
|
||||
|
||||
const nextMap = buildSupplierArticlesMap(articles, automat.state.supplierArticlesMap);
|
||||
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) {
|
||||
const id = attachment.id ?? `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
||||
const item = { ...attachment, id };
|
||||
const next = [...(deliveryEditorAutomat.state.attachments ?? []), item];
|
||||
deliveryEditorAutomat.setState({ attachments: next, files: next });
|
||||
},
|
||||
resetDeliveryEditor() {
|
||||
automat.setState({
|
||||
...initialState,
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
setAttachments(attachments) {
|
||||
const clean = Array.isArray(attachments) ? attachments : [];
|
||||
deliveryEditorAutomat.setState({ attachments: clean, files: clean });
|
||||
},
|
||||
automat = new Automat(initialState, actions, {
|
||||
name,
|
||||
persist: false,
|
||||
});
|
||||
|
||||
updateAttachment(payload) {
|
||||
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 });
|
||||
}
|
||||
},
|
||||
return automat;
|
||||
}
|
||||
|
||||
removeAttachment(idOrPayload) {
|
||||
const current = deliveryEditorAutomat.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));
|
||||
}
|
||||
deliveryEditorAutomat.setState({ attachments: next, files: next });
|
||||
},
|
||||
export const deliveryDraftAutomat = createDeliveryEditorAutomat({ name: 'delivery-draft' });
|
||||
export const deliveryEditorAutomat = deliveryDraftAutomat;
|
||||
|
||||
resetDelivery() {
|
||||
deliveryEditorAutomat.setState({
|
||||
...initialDelivery,
|
||||
});
|
||||
},
|
||||
|
||||
resetDeliveryEditor() {
|
||||
deliveryEditorAutomat.setState({
|
||||
...initialState,
|
||||
});
|
||||
},
|
||||
});
|
||||
export function getDeliveryEditorAutomat(docId) {
|
||||
if (docId == null) {
|
||||
return deliveryDraftAutomat;
|
||||
}
|
||||
const name = `delivery-${docId}`;
|
||||
let automat = Automat.get(name);
|
||||
if (!automat) {
|
||||
automat = createDeliveryEditorAutomat({ name });
|
||||
}
|
||||
return automat;
|
||||
}
|
||||
|
||||
export const {
|
||||
setId,
|
||||
|
||||
@@ -31,8 +31,19 @@ export class Attachment extends PureComponent {
|
||||
if (onClick && attachment) {
|
||||
onClick(attachment);
|
||||
} else if (attachment?.file instanceof Blob || attachment?.file instanceof File) {
|
||||
const url = URL.createObjectURL(attachment.file);
|
||||
window.open(url, '_blank');
|
||||
if (attachment.file.size > 0) {
|
||||
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 size = attachment?.size;
|
||||
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 (
|
||||
<Box
|
||||
@@ -125,7 +137,7 @@ export class Attachment extends PureComponent {
|
||||
letterSpacing: '0.05em',
|
||||
}}
|
||||
>
|
||||
TXT
|
||||
{isCsv ? 'CSV' : 'TXT'}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Typography
|
||||
|
||||
@@ -134,7 +134,7 @@ class ManifestHeader extends PureComponent {
|
||||
) : null}
|
||||
{onPdfUpload ? (
|
||||
<FileDropzone
|
||||
accept="application/pdf,.pdf"
|
||||
accept="application/pdf,.pdf,text/plain,.txt,text/csv,.csv"
|
||||
disabled={saving || pdfUploading}
|
||||
onFileDrop={onPdfUpload}
|
||||
sx={{
|
||||
@@ -150,6 +150,7 @@ class ManifestHeader extends PureComponent {
|
||||
startIcon={pdfUploading ? <CircularProgress size={16} color="inherit" /> : <UploadFileIcon />}
|
||||
disabled={saving || pdfUploading}
|
||||
onClick={this.handleUploadClick}
|
||||
title="PDF, TXT oder CSV hochladen"
|
||||
sx={{
|
||||
whiteSpace: 'nowrap',
|
||||
height: 40,
|
||||
@@ -164,8 +165,8 @@ class ManifestHeader extends PureComponent {
|
||||
{pdfUploading
|
||||
? 'Wird analysiert…'
|
||||
: isDragging
|
||||
? 'PDF hier ablegen'
|
||||
: 'PDF hochladen'}
|
||||
? 'Datei hier ablegen'
|
||||
: 'Datei hochladen'}
|
||||
</Button>
|
||||
)}
|
||||
</FileDropzone>
|
||||
@@ -182,7 +183,7 @@ class ManifestHeader extends PureComponent {
|
||||
<input
|
||||
ref={this.fileInputRef}
|
||||
type="file"
|
||||
accept="application/pdf,.pdf"
|
||||
accept="application/pdf,.pdf,text/plain,.txt,text/csv,.csv"
|
||||
style={{ display: 'none' }}
|
||||
onChange={this.handleFileChange}
|
||||
/>
|
||||
|
||||
@@ -15,6 +15,7 @@ import CloseIcon from '@mui/icons-material/Close';
|
||||
import CheckCircleIcon from '@mui/icons-material/CheckCircle';
|
||||
import ErrorOutlinedIcon from '@mui/icons-material/ErrorOutlined';
|
||||
import PictureAsPdfIcon from '@mui/icons-material/PictureAsPdf';
|
||||
import TextSnippetOutlinedIcon from '@mui/icons-material/TextSnippetOutlined';
|
||||
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
||||
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 hasResult = Boolean(result);
|
||||
|
||||
const isTextDoc =
|
||||
filename?.toLowerCase().endsWith('.txt') || filename?.toLowerCase().endsWith('.csv');
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
@@ -121,12 +125,18 @@ export default function ManifestPdfDialog({ open, filename, data, onClose }) {
|
||||
<ErrorOutlinedIcon color="error" />
|
||||
) : streaming ? (
|
||||
<CircularProgress size={22} />
|
||||
) : isTextDoc ? (
|
||||
<TextSnippetOutlinedIcon color="action" />
|
||||
) : (
|
||||
<PictureAsPdfIcon color="action" />
|
||||
)}
|
||||
<Box sx={{ flex: 1 }}>
|
||||
<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>
|
||||
{filename ? (
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { PureComponent } from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
import Button from '@mui/material/Button';
|
||||
import CircularProgress from '@mui/material/CircularProgress';
|
||||
import Dialog from '@mui/material/Dialog';
|
||||
import DialogActions from '@mui/material/DialogActions';
|
||||
import DialogContent from '@mui/material/DialogContent';
|
||||
@@ -12,10 +13,15 @@ import Chip from '@mui/material/Chip';
|
||||
import CloseIcon from '@mui/icons-material/Close';
|
||||
import TextSnippetOutlinedIcon from '@mui/icons-material/TextSnippetOutlined';
|
||||
import ContentCopyIcon from '@mui/icons-material/ContentCopy';
|
||||
import DownloadOutlinedIcon from '@mui/icons-material/DownloadOutlined';
|
||||
import SaveOutlinedIcon from '@mui/icons-material/SaveOutlined';
|
||||
import { enqueueSnackbar } from 'notistack';
|
||||
import { StorageManagerContext } from '../../api/storageManagerContext';
|
||||
import { delivery as deliveryApi } from '../../api';
|
||||
|
||||
export class TextEditorDialog extends PureComponent {
|
||||
static contextType = StorageManagerContext;
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
@@ -26,6 +32,7 @@ export class TextEditorDialog extends PureComponent {
|
||||
this.handleChange = this.handleChange.bind(this);
|
||||
this.handleSave = this.handleSave.bind(this);
|
||||
this.handleCopy = this.handleCopy.bind(this);
|
||||
this.handleDownload = this.handleDownload.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
@@ -34,12 +41,16 @@ export class TextEditorDialog extends PureComponent {
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
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() {
|
||||
const { attachment, open } = this.props;
|
||||
const { attachment, open, docId, onContentLoad } = this.props;
|
||||
if (!open || !attachment) return;
|
||||
|
||||
if (typeof attachment.text === 'string') {
|
||||
@@ -51,7 +62,10 @@ export class TextEditorDialog extends PureComponent {
|
||||
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 });
|
||||
try {
|
||||
const text = await attachment.file.text();
|
||||
@@ -60,11 +74,44 @@ export class TextEditorDialog extends PureComponent {
|
||||
initialContent: text,
|
||||
loading: false,
|
||||
});
|
||||
return;
|
||||
} catch (err) {
|
||||
console.error('Fehler beim Lesen der Textdatei:', err);
|
||||
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) {
|
||||
@@ -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() {
|
||||
const { open, onClose, attachment, readOnly = false } = this.props;
|
||||
if (!open) return null;
|
||||
@@ -152,8 +215,24 @@ export class TextEditorDialog extends PureComponent {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
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
|
||||
multiline
|
||||
fullWidth
|
||||
@@ -216,15 +295,26 @@ export class TextEditorDialog extends PureComponent {
|
||||
justifyContent: 'space-between',
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
size="small"
|
||||
variant="outlined"
|
||||
startIcon={<ContentCopyIcon />}
|
||||
onClick={this.handleCopy}
|
||||
disabled={loading || !content}
|
||||
>
|
||||
Kopieren
|
||||
</Button>
|
||||
<Box sx={{ display: 'flex', gap: 1 }}>
|
||||
<Button
|
||||
size="small"
|
||||
variant="outlined"
|
||||
startIcon={<ContentCopyIcon />}
|
||||
onClick={this.handleCopy}
|
||||
disabled={loading || !content}
|
||||
>
|
||||
Kopieren
|
||||
</Button>
|
||||
<Button
|
||||
size="small"
|
||||
variant="outlined"
|
||||
startIcon={<DownloadOutlinedIcon />}
|
||||
onClick={this.handleDownload}
|
||||
disabled={loading || !content}
|
||||
>
|
||||
Download
|
||||
</Button>
|
||||
</Box>
|
||||
<Box sx={{ display: 'flex', gap: 1 }}>
|
||||
<Button size="small" onClick={onClose}>
|
||||
{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 LocalShippingIcon from '@mui/icons-material/LocalShipping';
|
||||
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 ShoppingCartIcon from '@mui/icons-material/ShoppingCart';
|
||||
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 = [
|
||||
{ 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: '/kunde', label: 'Kunde', icon: PeopleIcon },
|
||||
{ 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 = {
|
||||
'/artikel': [
|
||||
{ segment: '', label: 'Liste', title: 'Artikel', icon: ViewListIcon },
|
||||
@@ -30,7 +106,7 @@ export const SUB_NAV = {
|
||||
],
|
||||
'/lieferung': [
|
||||
{ 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 },
|
||||
],
|
||||
'/auftrag': [
|
||||
|
||||
@@ -9,8 +9,7 @@ import Paper from '@mui/material/Paper';
|
||||
import TextField from '@mui/material/TextField';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import SearchIcon from '@mui/icons-material/Search';
|
||||
import ClearIcon from '@mui/icons-material/Clear';
|
||||
import { StorageManagerContext } from '../../api/storageManagerContext';
|
||||
import { StorageManagerContext, getStorageManager } from '../../api/storageManagerContext';
|
||||
import BackendError from '../../components/BackendError';
|
||||
import { formatPrice } from '../../utils/price';
|
||||
import {
|
||||
@@ -83,6 +82,10 @@ function initialSearchState(searchParams) {
|
||||
class ArtikelSearchPage extends Component {
|
||||
static contextType = StorageManagerContext;
|
||||
|
||||
get storageManager() {
|
||||
return this.context || getStorageManager();
|
||||
}
|
||||
|
||||
state = initialSearchState(this.props.searchParams);
|
||||
listSlotRef = { current: null };
|
||||
rowMeasureRef = { current: null };
|
||||
@@ -171,7 +174,7 @@ class ArtikelSearchPage extends Component {
|
||||
|
||||
// Kick off initial search (empty input = show all products).
|
||||
const { input } = this.state;
|
||||
this.context.doSearch({
|
||||
this.storageManager?.doSearch({
|
||||
target: input,
|
||||
filter: this.props.searchParams.get('filter') || undefined,
|
||||
offset: 0,
|
||||
@@ -220,7 +223,7 @@ class ArtikelSearchPage extends Component {
|
||||
const value = event.target.value;
|
||||
this.setState({ input: value });
|
||||
|
||||
this.context.doSearch({
|
||||
this.storageManager?.doSearch({
|
||||
target: value,
|
||||
filter: this.props.searchParams.get('filter') || undefined,
|
||||
offset: 0,
|
||||
@@ -240,7 +243,7 @@ class ArtikelSearchPage extends Component {
|
||||
|
||||
// Delegate fetch + debounce to the global StorageManager.
|
||||
// The callback pushes results into local component state.
|
||||
this.context.doSearch({
|
||||
this.storageManager?.doSearch({
|
||||
target: trimmed,
|
||||
filter: filter || undefined,
|
||||
offset,
|
||||
|
||||
@@ -28,7 +28,8 @@ import { backendErrorFrom } from '../../utils/backendError';
|
||||
import { notifyError } from '../../utils/notify';
|
||||
import { generatePdfThumbnail } from '../../utils/pdfThumbnail';
|
||||
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 {
|
||||
static contextType = StorageManagerContext;
|
||||
@@ -36,9 +37,10 @@ class LieferungNeuPage extends PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.cfg = manifestConfig('delivery');
|
||||
this.automat = getDeliveryEditorAutomat(props.docId);
|
||||
this.state = {
|
||||
...deliveryEditorAutomat.state,
|
||||
status: 'loadingParties',
|
||||
...this.automat.state,
|
||||
status: props.docId != null ? 'loading' : 'loadingParties',
|
||||
error: null,
|
||||
partiesLoading: true,
|
||||
saving: false,
|
||||
@@ -66,32 +68,38 @@ class LieferungNeuPage extends PureComponent {
|
||||
this.handleAttachmentClick = this.handleAttachmentClick.bind(this);
|
||||
this.handleCloseTextEditor = this.handleCloseTextEditor.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.handleCreateArticleForLine = this.handleCreateArticleForLine.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.unsubscribe = deliveryEditorAutomat.subscribe(this);
|
||||
this.unsubscribe = this.automat.subscribe(this);
|
||||
this._cancelled = false;
|
||||
if (this.props.docId != null) {
|
||||
void this.loadEdit();
|
||||
} else {
|
||||
void this.loadPartiesOnly();
|
||||
if (this.props.initialDelivery) {
|
||||
deliveryEditorAutomat.actions.setDelivery(this.props.initialDelivery);
|
||||
} else {
|
||||
deliveryEditorAutomat.actions.resetDelivery();
|
||||
this.automat.actions.setDelivery(this.props.initialDelivery);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
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) {
|
||||
void this.loadEdit();
|
||||
} else {
|
||||
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 {
|
||||
const parties = await loadParties('delivery', this.context);
|
||||
if (this._cancelled) return;
|
||||
deliveryEditorAutomat.actions.setDeliveryEntities(parties);
|
||||
this.automat.actions.setDeliveryEntities(parties);
|
||||
this.setState({
|
||||
status: 'ready',
|
||||
partiesLoading: false,
|
||||
@@ -145,7 +153,7 @@ class LieferungNeuPage extends PureComponent {
|
||||
]);
|
||||
if (this._cancelled) return;
|
||||
|
||||
deliveryEditorAutomat.actions.setDeliveryEntities(parties);
|
||||
this.automat.actions.setDeliveryEntities(parties);
|
||||
|
||||
requireObject(docRes, cfg.getPathLabel);
|
||||
requireObject(docRes[cfg.docField], cfg.docField);
|
||||
@@ -154,17 +162,17 @@ class LieferungNeuPage extends PureComponent {
|
||||
const lines =
|
||||
apiLines.length > 0 ? apiLines.map(lineFromApi) : [emptyLine()];
|
||||
|
||||
deliveryEditorAutomat.actions.setId(doc.id ?? docId);
|
||||
deliveryEditorAutomat.actions.setDate(dateInputValue(doc.date));
|
||||
if (doc[cfg.partyField] != null) deliveryEditorAutomat.actions.setDeliveryEntity(doc[cfg.partyField]);
|
||||
if (doc.number != null) deliveryEditorAutomat.actions.setNumber(doc.number);
|
||||
if (doc.externalNumber != null) deliveryEditorAutomat.actions.setExternalNumber(String(doc.externalNumber));
|
||||
deliveryEditorAutomat.actions.setLineitems(lines);
|
||||
this.automat.actions.setId(doc.id ?? docId);
|
||||
this.automat.actions.setDate(dateInputValue(doc.date));
|
||||
if (doc[cfg.partyField] != null) this.automat.actions.setDeliveryEntity(doc[cfg.partyField]);
|
||||
if (doc.number != null) this.automat.actions.setNumber(doc.number);
|
||||
if (doc.externalNumber != null) this.automat.actions.setExternalNumber(String(doc.externalNumber));
|
||||
this.automat.actions.setLineitems(lines);
|
||||
|
||||
// Restore attachments with thumbnails
|
||||
const apiFiles = doc.files ?? [];
|
||||
const attachments = await restoreAttachmentsFromApi(apiFiles, this.state.attachments);
|
||||
deliveryEditorAutomat.actions.setAttachments(attachments);
|
||||
this.automat.actions.setAttachments(attachments);
|
||||
|
||||
this.setState({
|
||||
status: 'ready',
|
||||
@@ -186,19 +194,19 @@ class LieferungNeuPage extends PureComponent {
|
||||
}
|
||||
|
||||
handlePartyChange(partyId) {
|
||||
deliveryEditorAutomat.actions.setDeliveryEntity(partyId);
|
||||
this.automat.actions.setDeliveryEntity(partyId);
|
||||
}
|
||||
|
||||
handleDateChange(date) {
|
||||
deliveryEditorAutomat.actions.setDate(date);
|
||||
this.automat.actions.setDate(date);
|
||||
}
|
||||
|
||||
handleNumberChange(number) {
|
||||
deliveryEditorAutomat.actions.setNumber(number);
|
||||
this.automat.actions.setNumber(number);
|
||||
}
|
||||
|
||||
handleExternalNumberChange(externalNumber) {
|
||||
deliveryEditorAutomat.actions.setExternalNumber(externalNumber);
|
||||
this.automat.actions.setExternalNumber(externalNumber);
|
||||
}
|
||||
|
||||
findLineIndex(key) {
|
||||
@@ -214,7 +222,7 @@ class LieferungNeuPage extends PureComponent {
|
||||
|
||||
handleLineChange(key, field, value) {
|
||||
const index = this.findLineIndex(key);
|
||||
deliveryEditorAutomat.actions.updateLineitem({
|
||||
this.automat.actions.updateLineitem({
|
||||
index,
|
||||
key,
|
||||
[field]: value,
|
||||
@@ -229,7 +237,7 @@ class LieferungNeuPage extends PureComponent {
|
||||
}
|
||||
this._skuDebounceTimers[key] = setTimeout(() => {
|
||||
if (this._skuDebounceTimers) delete this._skuDebounceTimers[key];
|
||||
deliveryEditorAutomat.actions.fetchSupplierArticleForSku(value, key);
|
||||
this.automat.actions.fetchSupplierArticleForSku(value, key);
|
||||
}, 350);
|
||||
}
|
||||
}
|
||||
@@ -239,7 +247,7 @@ class LieferungNeuPage extends PureComponent {
|
||||
clearTimeout(this._skuDebounceTimers[key]);
|
||||
delete this._skuDebounceTimers[key];
|
||||
}
|
||||
deliveryEditorAutomat.actions.fetchSupplierArticleForSku(value, key);
|
||||
this.automat.actions.fetchSupplierArticleForSku(value, key);
|
||||
}
|
||||
|
||||
async handleCreateArticleForLine(line) {
|
||||
@@ -294,7 +302,7 @@ class LieferungNeuPage extends PureComponent {
|
||||
|
||||
// Keep the supplier SKU in the delivery line, but refresh supplier article data
|
||||
// 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' });
|
||||
} catch (err) {
|
||||
@@ -304,69 +312,105 @@ class LieferungNeuPage extends PureComponent {
|
||||
}
|
||||
|
||||
handleAddLine() {
|
||||
deliveryEditorAutomat.actions.addLineitem();
|
||||
this.automat.actions.addLineitem();
|
||||
}
|
||||
|
||||
handleRemoveLine(key) {
|
||||
const index = this.findLineIndex(key);
|
||||
deliveryEditorAutomat.actions.removeLineitem(index >= 0 ? index : key);
|
||||
this.automat.actions.removeLineitem(index >= 0 ? index : key);
|
||||
}
|
||||
|
||||
async handlePdfUpload(file) {
|
||||
if (!file || this.state.pdfUploading) return;
|
||||
|
||||
// 1. Generate thumbnail for PDF
|
||||
let thumbnail = null;
|
||||
try {
|
||||
thumbnail = await generatePdfThumbnail(file);
|
||||
} catch (err) {
|
||||
console.error('Fehler beim Erstellen des PDF-Thumbnails:', err);
|
||||
}
|
||||
const isPdf =
|
||||
file.type === 'application/pdf' || file.name?.toLowerCase().endsWith('.pdf');
|
||||
const isCsv =
|
||||
file.type === 'text/csv' || file.name?.toLowerCase().endsWith('.csv');
|
||||
|
||||
const pdfId = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
||||
const pdfAttachment = {
|
||||
id: pdfId,
|
||||
file,
|
||||
name: file.name,
|
||||
size: file.size,
|
||||
type: 'application/pdf',
|
||||
thumbnail,
|
||||
uploadedAt: new Date().toISOString(),
|
||||
};
|
||||
deliveryEditorAutomat.actions.addAttachment(pdfAttachment);
|
||||
if (isPdf) {
|
||||
// 1. Generate thumbnail for PDF
|
||||
let thumbnail = null;
|
||||
try {
|
||||
thumbnail = await generatePdfThumbnail(file);
|
||||
} catch (err) {
|
||||
console.error('Fehler beim Erstellen des PDF-Thumbnails:', err);
|
||||
}
|
||||
|
||||
// 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,
|
||||
const pdfId = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
||||
const pdfAttachment = {
|
||||
id: pdfId,
|
||||
file,
|
||||
name: file.name,
|
||||
size: file.size,
|
||||
type: 'application/pdf',
|
||||
thumbnail,
|
||||
uploadedAt: new Date().toISOString(),
|
||||
};
|
||||
deliveryEditorAutomat.actions.addAttachment(txtAttachment);
|
||||
} catch (txtErr) {
|
||||
console.error('Fehler beim Extrahieren von Text mit pdftotext-wasm:', txtErr);
|
||||
this.automat.actions.addAttachment(pdfAttachment);
|
||||
|
||||
// 2. Extract text using pdftotext-wasm to create 2nd file (textfile)
|
||||
try {
|
||||
const text = await extractPdfText(file, { layout: true });
|
||||
const txtName = file.name.replace(/\.pdf$/i, '') + '.txt';
|
||||
const txtFile = new File([text], txtName, { type: 'text/plain;charset=utf-8' });
|
||||
const txtAttachment = {
|
||||
id: `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
||||
file: txtFile,
|
||||
name: txtFile.name,
|
||||
size: txtFile.size,
|
||||
type: 'text/plain',
|
||||
text,
|
||||
parentPdfId: pdfId,
|
||||
uploadedAt: new Date().toISOString(),
|
||||
};
|
||||
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
|
||||
await this.runPdfParse(file);
|
||||
// 3. Run SSE parsing with backend (POST /api/delivery/parse)
|
||||
await this.runParse(file);
|
||||
}
|
||||
|
||||
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
|
||||
const isPdf = attachment.type === 'application/pdf' || attachment.name.toLowerCase().endsWith('.pdf');
|
||||
const isPdf = targetAttachment.type === 'application/pdf' || targetAttachment.name.toLowerCase().endsWith('.pdf');
|
||||
if (isPdf) {
|
||||
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 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,
|
||||
);
|
||||
if (existingTxt) {
|
||||
deliveryEditorAutomat.actions.updateAttachment({
|
||||
this.automat.actions.updateAttachment({
|
||||
id: existingTxt.id,
|
||||
file: txtFile,
|
||||
size: txtFile.size,
|
||||
@@ -382,7 +426,7 @@ class LieferungNeuPage extends PureComponent {
|
||||
isDirty: true,
|
||||
});
|
||||
} else {
|
||||
deliveryEditorAutomat.actions.addAttachment({
|
||||
this.automat.actions.addAttachment({
|
||||
id: `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
||||
file: txtFile,
|
||||
name: txtFile.name,
|
||||
@@ -403,7 +447,7 @@ class LieferungNeuPage extends PureComponent {
|
||||
|
||||
handlePdfDelete(attachment) {
|
||||
if (!attachment?.id) return;
|
||||
deliveryEditorAutomat.actions.removeAttachment(attachment.id);
|
||||
this.automat.actions.removeAttachment(attachment.id);
|
||||
}
|
||||
|
||||
async runParse(file) {
|
||||
@@ -500,24 +544,24 @@ class LieferungNeuPage extends PureComponent {
|
||||
if (matched) supplierId = matched.id;
|
||||
}
|
||||
|
||||
if (docDate) deliveryEditorAutomat.actions.setDate(docDate);
|
||||
if (supplierId != null) deliveryEditorAutomat.actions.setDeliveryEntity(supplierId);
|
||||
if (docDate) this.automat.actions.setDate(docDate);
|
||||
if (supplierId != null) this.automat.actions.setDeliveryEntity(supplierId);
|
||||
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) {
|
||||
deliveryEditorAutomat.actions.setLineitems(newLines);
|
||||
this.automat.actions.setLineitems(newLines);
|
||||
}
|
||||
if (notes.length > 0) {
|
||||
deliveryEditorAutomat.actions.setNotes(notes);
|
||||
this.automat.actions.setNotes(notes);
|
||||
}
|
||||
|
||||
// Supply whole article list to GET /supplier/{id}/articles
|
||||
const effectiveSupplierId = supplierId ?? deliveryEditorAutomat.state.deliveryEntity ?? deliveryEditorAutomat.state.partyId;
|
||||
const effectiveLines = (newLines && newLines.length > 0) ? newLines : deliveryEditorAutomat.state.lines;
|
||||
const effectiveSupplierId = supplierId ?? this.automat.state.deliveryEntity ?? this.automat.state.partyId;
|
||||
const effectiveLines = (newLines && newLines.length > 0) ? newLines : this.automat.state.lines;
|
||||
if (effectiveSupplierId != null && effectiveLines && effectiveLines.length > 0) {
|
||||
deliveryEditorAutomat.actions.fetchSupplierArticles(effectiveSupplierId, effectiveLines);
|
||||
this.automat.actions.fetchSupplierArticles(effectiveSupplierId, effectiveLines);
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -567,6 +611,74 @@ class LieferungNeuPage extends PureComponent {
|
||||
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) {
|
||||
if (!attachment) return;
|
||||
const isPdf =
|
||||
@@ -575,19 +687,55 @@ class LieferungNeuPage extends PureComponent {
|
||||
const isText =
|
||||
!isPdf &&
|
||||
(attachment.type?.startsWith('text/') ||
|
||||
attachment.name?.toLowerCase().endsWith('.txt'));
|
||||
attachment.name?.toLowerCase().endsWith('.txt') ||
|
||||
attachment.name?.toLowerCase().endsWith('.csv'));
|
||||
|
||||
if (isText) {
|
||||
this.setState({
|
||||
textEditorOpen: true,
|
||||
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) {
|
||||
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',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
handleCloseTextEditor() {
|
||||
@@ -605,7 +753,7 @@ class LieferungNeuPage extends PureComponent {
|
||||
type: 'text/plain;charset=utf-8',
|
||||
});
|
||||
|
||||
deliveryEditorAutomat.actions.updateAttachment({
|
||||
this.automat.actions.updateAttachment({
|
||||
id: attachment.id,
|
||||
file: newFile,
|
||||
size: newFile.size,
|
||||
@@ -657,7 +805,7 @@ class LieferungNeuPage extends PureComponent {
|
||||
await this.context[cfg.updateCache]();
|
||||
if (this._cancelled) return;
|
||||
enqueueSnackbar(cfg.createdMsg, { variant: 'success' });
|
||||
deliveryEditorAutomat.actions.resetDelivery();
|
||||
this.automat.actions.resetDelivery();
|
||||
this.props.navigate(cfg.editPath(data[cfg.docField].id));
|
||||
} else {
|
||||
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 lines = apiLines.length > 0 ? apiLines.map(lineFromApi) : [emptyLine()];
|
||||
|
||||
deliveryEditorAutomat.actions.setId(doc.id ?? docId);
|
||||
deliveryEditorAutomat.actions.setDate(dateInputValue(doc.date));
|
||||
if (doc[cfg.partyField] != null) deliveryEditorAutomat.actions.setDeliveryEntity(doc[cfg.partyField]);
|
||||
if (doc.number != null) deliveryEditorAutomat.actions.setNumber(doc.number);
|
||||
if (doc.externalNumber != null) deliveryEditorAutomat.actions.setExternalNumber(String(doc.externalNumber));
|
||||
deliveryEditorAutomat.actions.setLineitems(lines);
|
||||
this.automat.actions.setId(doc.id ?? docId);
|
||||
this.automat.actions.setDate(dateInputValue(doc.date));
|
||||
if (doc[cfg.partyField] != null) this.automat.actions.setDeliveryEntity(doc[cfg.partyField]);
|
||||
if (doc.number != null) this.automat.actions.setNumber(doc.number);
|
||||
if (doc.externalNumber != null) this.automat.actions.setExternalNumber(String(doc.externalNumber));
|
||||
this.automat.actions.setLineitems(lines);
|
||||
|
||||
// Restore attachments with thumbnails
|
||||
const apiFiles = doc.files ?? [];
|
||||
const attachments = await restoreAttachmentsFromApi(apiFiles, this.state.attachments);
|
||||
deliveryEditorAutomat.actions.setAttachments(attachments);
|
||||
this.automat.actions.setAttachments(attachments);
|
||||
|
||||
enqueueSnackbar(cfg.savedMsg, { variant: 'success' });
|
||||
this.setState({ saving: false });
|
||||
@@ -807,9 +955,11 @@ class LieferungNeuPage extends PureComponent {
|
||||
/>
|
||||
<TextEditorDialog
|
||||
open={this.state.textEditorOpen}
|
||||
docId={this.props.docId ?? this.state.id}
|
||||
attachment={this.state.textEditorAttachment}
|
||||
onClose={this.handleCloseTextEditor}
|
||||
onSave={this.handleSaveTextEditor}
|
||||
onContentLoad={this.handleTextEditorContentLoaded}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user