diff --git a/public/js/modals/expense-modal.js b/public/js/modals/expense-modal.js index 1248f69..a703853 100644 --- a/public/js/modals/expense-modal.js +++ b/public/js/modals/expense-modal.js @@ -31,6 +31,8 @@ let lineCount = 0; let isSaving = false; +let selectedFile = null; // File object aus +let attachmentLimits = null; // { maxBytes, maxMb, allowedMimeTypes, allowedExtensions } // ──────────────────────────────────────────────────────────────────── // Helpers // ──────────────────────────────────────────────────────────────────── @@ -55,6 +57,83 @@ function fmtMoney(n) { return n.toLocaleString('en-US', { style: 'currency', currency: 'USD' }); } +function onFileSelected(e) { + const f = e.target.files && e.target.files[0]; + if (!f) return; + handleFile(f); +} + +function onFileDrop(e) { + e.preventDefault(); + const zone = document.getElementById('exp-attach-zone'); + if (zone) zone.classList.remove('border-blue-500', 'bg-blue-50'); + const f = e.dataTransfer.files && e.dataTransfer.files[0]; + if (!f) return; + handleFile(f); +} + +function handleFile(file) { + // Validate type + const allowed = attachmentLimits?.allowedMimeTypes || + ['image/png','image/jpeg','image/jpg','image/heic','image/heif','application/pdf']; + if (!allowed.includes(file.type)) { + alert(`Unsupported file type: ${file.type || 'unknown'}\nAllowed: PNG, JPG, HEIC, PDF`); + return; + } + + const maxBytes = attachmentLimits?.maxBytes || (5 * 1024 * 1024); + if (file.size > maxBytes) { + const maxMb = attachmentLimits?.maxMb || 5; + alert(`File too large: ${(file.size/1024/1024).toFixed(2)} MB\nMax: ${maxMb} MB`); + return; + } + + selectedFile = file; + renderFilePreview(); +} + +function clearFile() { + selectedFile = null; + const input = document.getElementById('exp-attach-input'); + if (input) input.value = ''; + renderFilePreview(); +} + +function renderFilePreview() { + const el = document.getElementById('exp-attach-preview'); + if (!el) return; + + if (!selectedFile) { + el.classList.add('hidden'); + el.innerHTML = ''; + return; + } + + const sizeMb = (selectedFile.size / 1024 / 1024).toFixed(2); + const isImage = selectedFile.type.startsWith('image/') && selectedFile.type !== 'image/heic' && selectedFile.type !== 'image/heif'; + + let thumbHtml = ''; + if (isImage) { + const url = URL.createObjectURL(selectedFile); + thumbHtml = ``; + } else if (selectedFile.type === 'application/pdf') { + thumbHtml = `
PDF
`; + } else { + thumbHtml = `
${selectedFile.type.split('/')[1] || 'file'}
`; + } + + el.classList.remove('hidden'); + el.innerHTML = ` +
+ ${thumbHtml} +
+

${escapeHtml(selectedFile.name)}

+

${sizeMb} MB · ${selectedFile.type || 'unknown'}

+
+ +
`; +} // ──────────────────────────────────────────────────────────────────── // Public Entry // ──────────────────────────────────────────────────────────────────── @@ -68,11 +147,12 @@ export async function openExpenseModal({ onSaved } = {}) { // Lade Stammdaten parallel (alles aus dem Cache → schnell) try { - [vendors, expenseAccounts, paymentAccounts, paymentMethods] = await Promise.all([ + [vendors, expenseAccounts, paymentAccounts, paymentMethods, attachmentLimits] = await Promise.all([ window.API.accounting.getVendors('', 1000), window.API.accounting.getExpenseAccounts(), window.API.accounting.getPaymentAccounts(), - window.API.accounting.getPaymentMethods() + window.API.accounting.getPaymentMethods(), + window.API.accounting.getAttachmentLimits() ]); // Errors aus dem Backend abfangen @@ -105,6 +185,7 @@ function resetForm() { document.getElementById('exp-ref-no').value = ''; document.getElementById('exp-memo').value = ''; document.getElementById('exp-date').value = todayISO(); + clearFile(); document.getElementById('exp-lines-tbody').innerHTML = ''; lineCount = 0; addLine(); @@ -213,6 +294,30 @@ function renderModal() { + +
+ +
+

+ 📎 Drop file here or click to select +

+

+ ${attachmentLimits ? `PNG, JPG, HEIC, PDF · max ${attachmentLimits.maxMb} MB` : 'Loading limits…'} +

+
+ + +
+