attachments
This commit is contained in:
@@ -31,6 +31,8 @@ let lineCount = 0;
|
||||
|
||||
let isSaving = false;
|
||||
|
||||
let selectedFile = null; // File object aus <input type="file">
|
||||
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 = `<img src="${url}" class="h-16 w-16 object-cover rounded border border-gray-200" onload="URL.revokeObjectURL(this.src)">`;
|
||||
} else if (selectedFile.type === 'application/pdf') {
|
||||
thumbHtml = `<div class="h-16 w-16 bg-red-50 border border-red-200 rounded flex items-center justify-center text-red-600 font-bold text-xs">PDF</div>`;
|
||||
} else {
|
||||
thumbHtml = `<div class="h-16 w-16 bg-gray-100 border border-gray-200 rounded flex items-center justify-center text-gray-500 text-xs">${selectedFile.type.split('/')[1] || 'file'}</div>`;
|
||||
}
|
||||
|
||||
el.classList.remove('hidden');
|
||||
el.innerHTML = `
|
||||
<div class="flex items-center gap-3 p-2 bg-gray-50 border border-gray-200 rounded">
|
||||
${thumbHtml}
|
||||
<div class="flex-1 min-w-0">
|
||||
<p class="text-sm font-medium text-gray-900 truncate">${escapeHtml(selectedFile.name)}</p>
|
||||
<p class="text-xs text-gray-500">${sizeMb} MB · ${selectedFile.type || 'unknown'}</p>
|
||||
</div>
|
||||
<button type="button" onclick="window.expenseModal.clearFile()"
|
||||
class="text-red-500 hover:text-red-700 text-sm px-2">Remove</button>
|
||||
</div>`;
|
||||
}
|
||||
// ────────────────────────────────────────────────────────────────────
|
||||
// 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() {
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Attachment -->
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">
|
||||
Attachment <span class="text-gray-400 text-xs font-normal">(optional — receipt screenshot or PDF)</span>
|
||||
</label>
|
||||
<div id="exp-attach-zone"
|
||||
class="border-2 border-dashed border-gray-300 rounded-md p-4 text-center cursor-pointer hover:border-blue-400 hover:bg-blue-50 transition"
|
||||
onclick="document.getElementById('exp-attach-input').click()"
|
||||
ondragover="event.preventDefault(); this.classList.add('border-blue-500','bg-blue-50');"
|
||||
ondragleave="this.classList.remove('border-blue-500','bg-blue-50');"
|
||||
ondrop="window.expenseModal.onFileDrop(event)">
|
||||
<p class="text-sm text-gray-500">
|
||||
📎 Drop file here or click to select
|
||||
</p>
|
||||
<p class="text-xs text-gray-400 mt-1">
|
||||
${attachmentLimits ? `PNG, JPG, HEIC, PDF · max ${attachmentLimits.maxMb} MB` : 'Loading limits…'}
|
||||
</p>
|
||||
</div>
|
||||
<input type="file" id="exp-attach-input" class="hidden"
|
||||
accept=".png,.jpg,.jpeg,.heic,.heif,.pdf,image/png,image/jpeg,image/heic,application/pdf"
|
||||
onchange="window.expenseModal.onFileSelected(event)">
|
||||
<div id="exp-attach-preview" class="hidden mt-2"></div>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Memo</label>
|
||||
<textarea id="exp-memo" rows="2"
|
||||
@@ -576,6 +681,25 @@ async function save(mode) {
|
||||
showError(result.error);
|
||||
return;
|
||||
}
|
||||
|
||||
// Attachment hochladen, falls vorhanden
|
||||
if (selectedFile && result.id) {
|
||||
try {
|
||||
setButtonsDisabled(true, 'Uploading attachment…');
|
||||
const attachResult = await window.API.accounting.attachToExpense(
|
||||
result.id,
|
||||
selectedFile
|
||||
);
|
||||
if (attachResult.error) {
|
||||
// Expense ist schon erstellt — Fehler nur anzeigen, nicht blockieren
|
||||
alert(`⚠️ Expense saved (Purchase #${result.id}) but attachment failed:\n${attachResult.error}\n\nYou can attach the file directly in QBO.`);
|
||||
} else {
|
||||
console.log(`📎 Attachment uploaded: ${attachResult.id}`);
|
||||
}
|
||||
} catch (attachErr) {
|
||||
alert(`⚠️ Expense saved (Purchase #${result.id}) but attachment failed:\n${attachErr.message}\n\nYou can attach the file directly in QBO.`);
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof onSavedCb === 'function') {
|
||||
try { onSavedCb(result); } catch (e) { console.warn('onSaved cb threw:', e); }
|
||||
@@ -597,13 +721,14 @@ async function save(mode) {
|
||||
}
|
||||
}
|
||||
|
||||
function setButtonsDisabled(disabled) {
|
||||
function setButtonsDisabled(disabled, customText) {
|
||||
const a = document.getElementById('exp-save-new-btn');
|
||||
const b = document.getElementById('exp-save-close-btn');
|
||||
if (a) a.disabled = disabled;
|
||||
if (b) b.disabled = disabled;
|
||||
if (a) a.textContent = disabled ? 'Saving…' : 'Save & New';
|
||||
if (b) b.textContent = disabled ? 'Saving…' : 'Save & Close';
|
||||
const text = customText || (disabled ? 'Saving…' : null);
|
||||
if (a) a.textContent = text || 'Save & New';
|
||||
if (b) b.textContent = text || 'Save & Close';
|
||||
}
|
||||
|
||||
function showError(msg) {
|
||||
@@ -645,5 +770,8 @@ window.expenseModal = {
|
||||
removeLine,
|
||||
clearLines,
|
||||
updateTotal,
|
||||
save
|
||||
save,
|
||||
onFileSelected,
|
||||
onFileDrop,
|
||||
clearFile
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user