This commit is contained in:
seb
2026-09-18 06:44:12 +02:00
parent d5ade4923d
commit a537c2f72d
8 changed files with 308 additions and 31 deletions

View File

@@ -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;
}