subnav
This commit is contained in:
@@ -15,6 +15,7 @@ import ManifestLines from './ManifestLines';
|
||||
import ManifestPdfDialog from './ManifestPdfDialog';
|
||||
import {
|
||||
buildManifestBody,
|
||||
restoreAttachmentsFromApi,
|
||||
dateInputValue,
|
||||
emptyLine,
|
||||
lineFromApi,
|
||||
@@ -38,6 +39,7 @@ class ManifestForm extends PureComponent {
|
||||
number: '',
|
||||
externalNumber: '',
|
||||
lines: [emptyLine()],
|
||||
attachments: [],
|
||||
saving: false,
|
||||
pdfUploading: false,
|
||||
pdfDialogOpen: false,
|
||||
@@ -125,6 +127,10 @@ class ManifestForm extends PureComponent {
|
||||
const lines =
|
||||
apiLines.length > 0 ? apiLines.map(lineFromApi) : [emptyLine()];
|
||||
|
||||
// Restore attachments if available
|
||||
const apiFiles = doc.files ?? [];
|
||||
const attachments = await restoreAttachmentsFromApi(apiFiles);
|
||||
|
||||
this.setState({
|
||||
status: 'ready',
|
||||
parties,
|
||||
@@ -135,6 +141,7 @@ class ManifestForm extends PureComponent {
|
||||
externalNumber:
|
||||
doc.externalNumber != null ? String(doc.externalNumber) : '',
|
||||
lines,
|
||||
attachments,
|
||||
});
|
||||
} catch (err) {
|
||||
if (this._cancelled) return;
|
||||
@@ -294,13 +301,14 @@ class ManifestForm extends PureComponent {
|
||||
const requireAtLeastOne = mode === 'create';
|
||||
let body;
|
||||
try {
|
||||
body = buildManifestBody(
|
||||
body = await buildManifestBody(
|
||||
{
|
||||
number: this.state.number,
|
||||
externalNumber: this.state.externalNumber,
|
||||
date: this.state.date,
|
||||
partyId: this.state.partyId,
|
||||
lines: this.state.lines,
|
||||
attachments: this.state.attachments,
|
||||
},
|
||||
{
|
||||
partyField: cfg.partyField,
|
||||
|
||||
@@ -14,5 +14,7 @@ export {
|
||||
emptyLine,
|
||||
lineFromApi,
|
||||
buildManifestBody,
|
||||
buildFilesForUpload,
|
||||
restoreAttachmentsFromApi,
|
||||
dateInputValue,
|
||||
} from './manifestConfig';
|
||||
|
||||
@@ -130,10 +130,225 @@ export function buildLines(lines, { requireAtLeastOne }) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {{ number: string, externalNumber?: string, date: string, partyId: number|null, lines: object[] }} form
|
||||
* Convert File objects to base64 for API upload
|
||||
* @param {Array<{file: File, name: string, type: string, thumbnail?: string, text?: string, parentPdfId?: string|number, isPersisted?: boolean, isDirty?: boolean}>} attachments
|
||||
* @param {{ onlyNew?: boolean }} [options]
|
||||
* @returns {Promise<Array<{mimetype: string, name: string, content: string, thumbavif?: string, meta?: object}>>}
|
||||
*/
|
||||
export async function buildFilesForUpload(attachments = [], { onlyNew = false } = {}) {
|
||||
const files = [];
|
||||
for (const attachment of attachments) {
|
||||
if (!attachment) continue;
|
||||
// Skip already persisted attachments in update mode unless modified
|
||||
if (onlyNew && attachment.isPersisted && !attachment.isDirty) {
|
||||
continue;
|
||||
}
|
||||
if (attachment.isPersisted && !attachment.isDirty && (!attachment.file || attachment.file.size === 0)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let fileToUpload = attachment.file;
|
||||
if (!fileToUpload || fileToUpload.size === 0) {
|
||||
if (attachment.text) {
|
||||
fileToUpload = new File([attachment.text], attachment.name || 'file.txt', {
|
||||
type: 'text/plain;charset=utf-8',
|
||||
});
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const base64 = await fileToBase64(fileToUpload);
|
||||
const filePayload = {
|
||||
mimetype: attachment.type || fileToUpload.type,
|
||||
name: attachment.name || fileToUpload.name,
|
||||
content: base64,
|
||||
};
|
||||
|
||||
if (attachment.thumbnail) {
|
||||
const thumbBase64 = attachment.thumbnail.includes(',')
|
||||
? attachment.thumbnail.split(',')[1]
|
||||
: attachment.thumbnail;
|
||||
filePayload.thumbavif = thumbBase64;
|
||||
}
|
||||
|
||||
const meta = {};
|
||||
if (attachment.thumbnail) {
|
||||
meta.thumbnail = attachment.thumbnail;
|
||||
}
|
||||
if (attachment.text) {
|
||||
meta.text = attachment.text;
|
||||
}
|
||||
if (attachment.parentPdfId) {
|
||||
meta.parentPdfId = String(attachment.parentPdfId);
|
||||
}
|
||||
if (Object.keys(meta).length > 0) {
|
||||
filePayload.meta = meta;
|
||||
}
|
||||
|
||||
files.push(filePayload);
|
||||
} catch (err) {
|
||||
console.error('Fehler beim Konvertieren von Datei zu Base64:', err);
|
||||
}
|
||||
}
|
||||
return files;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert File to base64 string
|
||||
* @param {File} file
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
function fileToBase64(file) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => {
|
||||
const result = reader.result;
|
||||
if (typeof result === 'string') {
|
||||
// Remove data URL prefix (e.g., "data:application/pdf;base64,")
|
||||
const base64 = result.split(',')[1] || result;
|
||||
resolve(base64);
|
||||
} else {
|
||||
reject(new Error('Invalid file reader result'));
|
||||
}
|
||||
};
|
||||
reader.onerror = () => reject(new Error('File reader error'));
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert base64 string to Blob
|
||||
* @param {string} base64
|
||||
* @param {string} mimeType
|
||||
* @returns {Blob}
|
||||
*/
|
||||
function base64ToBlob(base64, mimeType) {
|
||||
const clean = base64.includes(',') ? base64.split(',')[1] : base64;
|
||||
const byteCharacters = atob(clean);
|
||||
const byteArrays = [];
|
||||
const sliceSize = 512;
|
||||
|
||||
for (let offset = 0; offset < byteCharacters.length; offset += sliceSize) {
|
||||
const slice = byteCharacters.slice(offset, offset + sliceSize);
|
||||
const byteNumbers = new Array(slice.length);
|
||||
for (let i = 0; i < slice.length; i++) {
|
||||
byteNumbers[i] = byteCharacters.charCodeAt(i);
|
||||
}
|
||||
const byteArray = new Uint8Array(byteNumbers);
|
||||
byteArrays.push(byteArray);
|
||||
}
|
||||
|
||||
return new Blob(byteArrays, { type: mimeType });
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore attachments from API response
|
||||
* @param {Array<{id: number, fileId?: number, manifestId?: number, mimetype: string, name: string, thumbavif?: any, size: number, created?: string, meta?: object}>} apiFiles
|
||||
* @param {Array<object>} [existingAttachments]
|
||||
* @returns {Promise<Array<{id: number, fileId: number, manifestId: number, file: File, name: string, type: string, thumbnail: string|null, size: number, text: string|null, parentPdfId: string|null, isPersisted: boolean}>>}
|
||||
*/
|
||||
export async function restoreAttachmentsFromApi(apiFiles = [], existingAttachments = []) {
|
||||
const existingMap = new Map();
|
||||
for (const existing of existingAttachments) {
|
||||
if (!existing) continue;
|
||||
if (existing.id != null) existingMap.set(String(existing.id), existing);
|
||||
if (existing.fileId != null) existingMap.set(String(existing.fileId), existing);
|
||||
if (existing.name) existingMap.set(existing.name, existing);
|
||||
}
|
||||
|
||||
const attachments = [];
|
||||
for (const apiFile of apiFiles) {
|
||||
try {
|
||||
let file = null;
|
||||
let thumbnail = null;
|
||||
|
||||
// 1. Try to restore thumbnail from apiFile.thumbavif
|
||||
if (apiFile.thumbavif) {
|
||||
try {
|
||||
if (typeof apiFile.thumbavif === 'string') {
|
||||
if (apiFile.thumbavif.startsWith('data:')) {
|
||||
thumbnail = apiFile.thumbavif;
|
||||
} else {
|
||||
const thumbBlob = base64ToBlob(apiFile.thumbavif, 'image/avif');
|
||||
thumbnail = URL.createObjectURL(thumbBlob);
|
||||
}
|
||||
} else if (typeof apiFile.thumbavif === 'object') {
|
||||
const rawBytes = apiFile.thumbavif.data || apiFile.thumbavif;
|
||||
if (Array.isArray(rawBytes) || rawBytes instanceof Uint8Array) {
|
||||
const thumbBlob = new Blob([new Uint8Array(rawBytes)], { type: 'image/avif' });
|
||||
thumbnail = URL.createObjectURL(thumbBlob);
|
||||
}
|
||||
}
|
||||
} catch (thumbErr) {
|
||||
console.error('Fehler beim Dekodieren von thumbavif:', thumbErr);
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Check meta.thumbnail if thumbavif wasn't available
|
||||
if (!thumbnail && apiFile.meta?.thumbnail) {
|
||||
try {
|
||||
const metaThumb = apiFile.meta.thumbnail;
|
||||
if (typeof metaThumb === 'string') {
|
||||
if (metaThumb.startsWith('data:') || metaThumb.startsWith('blob:')) {
|
||||
thumbnail = metaThumb;
|
||||
} else {
|
||||
const thumbBlob = base64ToBlob(metaThumb, 'image/avif');
|
||||
thumbnail = URL.createObjectURL(thumbBlob);
|
||||
}
|
||||
}
|
||||
} catch (metaThumbErr) {
|
||||
console.error('Fehler beim Dekodieren von meta.thumbnail:', metaThumbErr);
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Fallback to existing matching attachment thumbnail
|
||||
const matchingExisting =
|
||||
existingMap.get(String(apiFile.id)) ||
|
||||
existingMap.get(String(apiFile.fileId)) ||
|
||||
existingMap.get(apiFile.name);
|
||||
|
||||
if (!thumbnail && matchingExisting?.thumbnail) {
|
||||
thumbnail = matchingExisting.thumbnail;
|
||||
}
|
||||
|
||||
const placeholderName = apiFile.name || 'file';
|
||||
const placeholderType = apiFile.mimetype || 'application/octet-stream';
|
||||
file =
|
||||
matchingExisting?.file instanceof File && matchingExisting.file.size > 0
|
||||
? matchingExisting.file
|
||||
: new File([''], placeholderName, { type: placeholderType });
|
||||
|
||||
const text = apiFile.meta?.text || matchingExisting?.text || null;
|
||||
const parentPdfId = apiFile.meta?.parentPdfId || matchingExisting?.parentPdfId || null;
|
||||
|
||||
attachments.push({
|
||||
id: apiFile.id,
|
||||
fileId: apiFile.fileId ?? apiFile.id,
|
||||
manifestId: apiFile.manifestId,
|
||||
isPersisted: true,
|
||||
file,
|
||||
name: apiFile.name,
|
||||
type: apiFile.mimetype,
|
||||
thumbnail,
|
||||
size: apiFile.size,
|
||||
text,
|
||||
parentPdfId,
|
||||
uploadedAt: apiFile.created,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Fehler beim Wiederherstellen von Datei:', err);
|
||||
}
|
||||
}
|
||||
return attachments;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {{ number: string, externalNumber?: string, date: string, partyId: number|null, lines: object[], attachments?: object[] }} form
|
||||
* @param {{ partyField: 'supplierId'|'entityId', requireAtLeastOne: boolean, externalNumber?: boolean }} opts
|
||||
*/
|
||||
export function buildManifestBody(
|
||||
export async function buildManifestBody(
|
||||
form,
|
||||
{ partyField, requireAtLeastOne, externalNumber: useExternalNumber },
|
||||
) {
|
||||
@@ -161,6 +376,16 @@ export function buildManifestBody(
|
||||
}
|
||||
if (form.partyId != null) body[partyField] = form.partyId;
|
||||
else if (!requireAtLeastOne) body[partyField] = null;
|
||||
|
||||
// Add files if attachments exist
|
||||
if (form.attachments && form.attachments.length > 0) {
|
||||
const isUpdate = !requireAtLeastOne;
|
||||
const filesToUpload = await buildFilesForUpload(form.attachments, { onlyNew: isUpdate });
|
||||
if (filesToUpload.length > 0) {
|
||||
body.files = filesToUpload;
|
||||
}
|
||||
}
|
||||
|
||||
return body;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user