feat(delivery): replace deliveryEditorSlice with automat and add PDF/text attachment handling

- replace Redux deliveryEditorSlice with deliveryEditorAutomat using automat
- add Attachment and AttachmentList components in Kopfdaten
- extract PDF text via pdftotext-wasm and render PDF thumbnails with pdfjs-dist
- add TextEditorDialog to view and edit extracted text files (preformatted monospace, no wrap)
- configure Vite server.fs.allow for pdftotext.wasm
This commit is contained in:
seb
2026-09-18 01:49:59 +02:00
parent 795043fe99
commit 27f6dfdebf
15 changed files with 1369 additions and 246 deletions

View File

@@ -0,0 +1,257 @@
import { Automat } from 'automat';
export function createEmptyLineitem(line = {}) {
return {
key: line.key ?? `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
name: line.name ?? null,
sku: line.sku ?? null,
amount: line.amount ?? null,
pu: line.pu ?? null,
unit: line.unit ?? null,
netprice: line.netprice ?? null,
taxrate: line.taxrate ?? null,
};
}
export const initialDelivery = {
id: null,
deliveryId: null,
date: null,
deliveryEntity: null,
partyId: null,
externalNumber: null,
external_number: null,
number: null,
type: null,
lines: [],
lineitems: [],
notes: [],
attachments: [],
files: [],
};
export const initialState = {
deliveryEntities: [],
...initialDelivery,
};
export const deliveryEditorAutomat = new Automat(initialState, {
setId(id) {
deliveryEditorAutomat.setState({ id, deliveryId: id });
},
setDeliveryEntities(deliveryEntities) {
deliveryEditorAutomat.setState({ deliveryEntities: deliveryEntities ?? [] });
},
setDelivery(delivery = {}) {
const rawLines = delivery.lines ?? delivery.lineitems ?? [];
const lines = Array.isArray(rawLines) ? rawLines : [];
const partyId = delivery.partyId ?? delivery.deliveryEntity ?? null;
const extNum = delivery.externalNumber ?? delivery.external_number ?? null;
const id = delivery.id ?? delivery.deliveryId ?? null;
const notes = Array.isArray(delivery.notes)
? delivery.notes
: delivery.notes ? [delivery.notes] : [];
const rawAttachments = delivery.attachments ?? delivery.files ?? [];
const attachments = Array.isArray(rawAttachments) ? rawAttachments : [];
deliveryEditorAutomat.setState({
...initialDelivery,
...delivery,
id,
deliveryId: id,
partyId,
deliveryEntity: partyId,
externalNumber: extNum,
external_number: extNum,
lines,
lineitems: lines,
notes,
attachments,
files: attachments,
});
},
setDate(date) {
deliveryEditorAutomat.setState({ date });
},
setDeliveryEntity(entityId) {
deliveryEditorAutomat.setState({ deliveryEntity: entityId, partyId: entityId });
},
setPartyId(partyId) {
deliveryEditorAutomat.setState({ deliveryEntity: partyId, partyId });
},
setExternalNumber(externalNumber) {
deliveryEditorAutomat.setState({
externalNumber,
external_number: externalNumber,
});
},
setNumber(number) {
deliveryEditorAutomat.setState({ number });
},
setType(type) {
deliveryEditorAutomat.setState({ type });
},
setLines(lines) {
const cleanLines = lines ?? [];
deliveryEditorAutomat.setState({ lines: cleanLines, lineitems: cleanLines });
},
setLineitems(lines) {
const cleanLines = lines ?? [];
deliveryEditorAutomat.setState({ lines: cleanLines, lineitems: cleanLines });
},
setNotes(notes) {
const cleanNotes = Array.isArray(notes)
? notes
: notes ? [notes] : [];
deliveryEditorAutomat.setState({ notes: cleanNotes });
},
addLineitem(line = {}) {
const item = createEmptyLineitem(line);
const nextLines = [...(deliveryEditorAutomat.state.lines ?? []), item];
deliveryEditorAutomat.setState({ lines: nextLines, lineitems: nextLines });
},
addLine(line = {}) {
deliveryEditorAutomat.actions.addLineitem(line);
},
updateLineitem(payload) {
const { index, key, ...fields } = payload || {};
const currentLines = deliveryEditorAutomat.state.lines ?? [];
let targetIndex = typeof index === 'number' && !Number.isNaN(index) ? index : -1;
if (targetIndex === -1 && key != null) {
targetIndex = currentLines.findIndex((l) => l.key === key);
}
if (targetIndex >= 0 && targetIndex < currentLines.length) {
const nextLines = [...currentLines];
nextLines[targetIndex] = {
...nextLines[targetIndex],
...fields,
};
deliveryEditorAutomat.setState({ lines: nextLines, lineitems: nextLines });
}
},
updateLine(payload) {
deliveryEditorAutomat.actions.updateLineitem(payload);
},
removeLineitem(payload) {
const currentLines = deliveryEditorAutomat.state.lines ?? [];
let index = typeof payload === 'number' && !Number.isNaN(payload) ? payload : -1;
if (index === -1 && payload != null) {
const key = typeof payload === 'string' ? payload : payload?.key;
if (key != null) {
index = currentLines.findIndex((l) => l.key === key);
}
}
if (index >= 0 && index < currentLines.length) {
const nextLines = currentLines.filter((_, i) => i !== index);
deliveryEditorAutomat.setState({ lines: nextLines, lineitems: nextLines });
}
},
removeLine(payload) {
deliveryEditorAutomat.actions.removeLineitem(payload);
},
addAttachment(attachment) {
const id = attachment.id ?? `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
const item = { ...attachment, id };
const next = [...(deliveryEditorAutomat.state.attachments ?? []), item];
deliveryEditorAutomat.setState({ attachments: next, files: next });
},
setAttachments(attachments) {
const clean = Array.isArray(attachments) ? attachments : [];
deliveryEditorAutomat.setState({ attachments: clean, files: clean });
},
updateAttachment(payload) {
const { id, ...fields } = payload || {};
const current = deliveryEditorAutomat.state.attachments ?? [];
const idx = current.findIndex((a) => a.id === id);
if (idx !== -1) {
const next = [...current];
next[idx] = { ...next[idx], ...fields };
deliveryEditorAutomat.setState({ attachments: next, files: next });
}
},
removeAttachment(idOrIndex) {
const current = deliveryEditorAutomat.state.attachments ?? [];
let next;
if (typeof idOrIndex === 'number') {
next = current.filter((_, idx) => idx !== idOrIndex);
} else {
const id = typeof idOrIndex === 'string' ? idOrIndex : idOrIndex?.id;
next = current.filter((a) => a.id !== id);
}
deliveryEditorAutomat.setState({ attachments: next, files: next });
},
resetDelivery() {
deliveryEditorAutomat.setState({
...initialDelivery,
});
},
resetDeliveryEditor() {
deliveryEditorAutomat.setState({
...initialState,
});
},
});
export const {
setId,
setDeliveryEntities,
setDelivery,
setDate,
setDeliveryEntity,
setPartyId,
setExternalNumber,
setNumber,
setType,
setLines,
setLineitems,
setNotes,
addLineitem,
addLine,
updateLineitem,
updateLine,
removeLineitem,
removeLine,
addAttachment,
setAttachments,
updateAttachment,
removeAttachment,
resetDelivery,
resetDeliveryEditor,
} = deliveryEditorAutomat.actions;
// Selectors
export const selectDeliveryId = (state = deliveryEditorAutomat.state) => state.deliveryId ?? state.id;
export const selectDeliveryEntities = (state = deliveryEditorAutomat.state) => state.deliveryEntities;
export const selectDeliveryDate = (state = deliveryEditorAutomat.state) => state.date;
export const selectDeliveryEntity = (state = deliveryEditorAutomat.state) => state.deliveryEntity ?? state.partyId;
export const selectDeliveryExternalNumber = (state = deliveryEditorAutomat.state) => state.externalNumber ?? state.external_number;
export const selectDeliveryNumber = (state = deliveryEditorAutomat.state) => state.number;
export const selectDeliveryType = (state = deliveryEditorAutomat.state) => state.type;
export const selectDeliveryLineitems = (state = deliveryEditorAutomat.state) => state.lines ?? state.lineitems;
export const selectDeliveryNotes = (state = deliveryEditorAutomat.state) => state.notes;
export const selectDeliveryAttachments = (state = deliveryEditorAutomat.state) => state.attachments;
export default deliveryEditorAutomat;