update expense

This commit is contained in:
2026-05-25 13:07:41 -05:00
parent dc3064acc6
commit f535f35eee
5 changed files with 243 additions and 17 deletions

View File

@@ -33,6 +33,7 @@ let isSaving = false;
let selectedFile = null; // File object aus <input type="file">
let attachmentLimits = null; // { maxBytes, maxMb, allowedMimeTypes, allowedExtensions }
let editingExpenseId = null; // null = Create-Modus, sonst = Purchase-ID im Edit-Modus
// ────────────────────────────────────────────────────────────────────
// Helpers
// ────────────────────────────────────────────────────────────────────
@@ -138,14 +139,14 @@ function renderFilePreview() {
// Public Entry
// ────────────────────────────────────────────────────────────────────
export async function openExpenseModal({ onSaved } = {}) {
export async function openExpenseModal({ onSaved, expense = null } = {}) {
onSavedCb = onSaved || null;
selectedVendorId = null;
selectedVendorName = '';
lineCount = 0;
isSaving = false;
editingExpenseId = expense ? expense.id : null;
// Lade Stammdaten parallel (alles aus dem Cache → schnell)
try {
[vendors, expenseAccounts, paymentAccounts, paymentMethods, attachmentLimits] = await Promise.all([
window.API.accounting.getVendors('', 1000),
@@ -155,7 +156,6 @@ export async function openExpenseModal({ onSaved } = {}) {
window.API.accounting.getAttachmentLimits()
]);
// Errors aus dem Backend abfangen
for (const [name, data] of [['vendors', vendors], ['expenseAccounts', expenseAccounts], ['paymentAccounts', paymentAccounts], ['paymentMethods', paymentMethods]]) {
if (data && data.error) {
alert(`Failed to load ${name}: ${data.error}\n\nTry "Sync from QBO" first.`);
@@ -168,6 +168,11 @@ export async function openExpenseModal({ onSaved } = {}) {
}
renderModal();
// Im Edit-Modus: Formular mit bestehenden Daten befüllen
if (expense) {
prefillForm(expense);
}
}
function closeModal() {
@@ -192,6 +197,43 @@ function resetForm() {
updateTotal();
}
function prefillForm(expense) {
// Vendor
if (expense.vendorId) {
selectVendor(expense.vendorId, expense.vendorName || expense.vendorId);
}
// Payment Account
if (expense.accountId) {
const pa = document.getElementById('exp-payment-account');
if (pa) pa.value = expense.accountId;
}
// Datum
if (expense.txnDate) {
document.getElementById('exp-date').value = expense.txnDate;
}
// Payment Method
if (expense.paymentMethodId) {
const pm = document.getElementById('exp-payment-method');
if (pm) pm.value = expense.paymentMethodId;
}
// Ref / Memo
if (expense.refNo) document.getElementById('exp-ref-no').value = expense.refNo;
if (expense.memo) document.getElementById('exp-memo').value = expense.memo;
// Lines — bestehende Zeilen ersetzen
document.getElementById('exp-lines-tbody').innerHTML = '';
lineCount = 0;
const lines = expense.lines && expense.lines.length ? expense.lines : [{}];
lines.forEach(l => {
addLine();
const rows = document.querySelectorAll('#exp-lines-tbody tr');
const tr = rows[rows.length - 1];
if (l.accountId) tr.querySelector('.exp-line-account').value = l.accountId;
if (l.description) tr.querySelector('.exp-line-desc').value = l.description;
if (l.amount != null) tr.querySelector('.exp-line-amount').value = l.amount;
});
updateTotal();
}
// ────────────────────────────────────────────────────────────────────
// Render
// ────────────────────────────────────────────────────────────────────
@@ -203,7 +245,7 @@ function renderModal() {
<div id="expense-modal" class="modal fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full flex items-start justify-center z-50">
<div class="relative mx-auto p-6 border w-full max-w-5xl shadow-lg rounded-lg bg-white my-8">
<div class="flex justify-between items-center mb-5">
<h3 class="text-2xl font-bold text-gray-900">📝 New Expense</h3>
<h3 class="text-2xl font-bold text-gray-900">${editingExpenseId ? '✏️ Edit Expense' : '📝 New Expense'}</h3>
<button onclick="window.expenseModal.close()" class="text-gray-400 hover:text-gray-600">
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
@@ -326,19 +368,19 @@ function renderModal() {
</div>
<div id="exp-error" class="hidden mb-4 p-3 bg-red-50 border border-red-200 rounded text-sm text-red-700"></div>
<div class="flex justify-end gap-3">
<button type="button" onclick="window.expenseModal.close()"
class="px-5 py-2 bg-gray-200 hover:bg-gray-300 text-gray-700 rounded-md">
Cancel
</button>
<button type="button" id="exp-save-new-btn" onclick="window.expenseModal.save('new')"
class="px-5 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-md font-medium">
Save &amp; New
</button>
${editingExpenseId ? '' : `
<button type="button" id="exp-save-new-btn" onclick="window.expenseModal.save('new')"
class="px-5 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-md font-medium">
Save &amp; New
</button>`}
<button type="button" id="exp-save-close-btn" onclick="window.expenseModal.save('close')"
class="px-5 py-2 bg-green-600 hover:bg-green-700 text-white rounded-md font-medium">
Save &amp; Close
${editingExpenseId ? 'Save Changes' : 'Save &amp; Close'}
</button>
</div>
</div>
@@ -676,11 +718,13 @@ async function save(mode) {
setButtonsDisabled(true);
try {
const result = await window.API.accounting.createExpense(payload);
if (result.error) {
showError(result.error);
return;
}
const result = editingExpenseId
? await window.API.accounting.updateExpense(editingExpenseId, payload)
: await window.API.accounting.createExpense(payload);
if (result.error) {
showError(result.error);
return;
}
// Attachment hochladen, falls vorhanden
if (selectedFile && result.id) {
@@ -728,7 +772,7 @@ function setButtonsDisabled(disabled, customText) {
if (b) b.disabled = disabled;
const text = customText || (disabled ? 'Saving…' : null);
if (a) a.textContent = text || 'Save & New';
if (b) b.textContent = text || 'Save & Close';
if (b) b.textContent = text || (editingExpenseId ? 'Save Changes' : 'Save & Close');
}
function showError(msg) {

View File

@@ -169,6 +169,12 @@ const API = {
body: JSON.stringify(data)
}).then(r => r.json()),
updateExpense: (id, data) => fetch(`/api/accounting/expenses/${id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
}).then(r => r.json()),
attachToExpense: (expenseId, file, note) => {
const fd = new FormData();
fd.append('file', file);

View File

@@ -623,6 +623,11 @@ function renderExpensesTable(expenses) {
</details>`
: escapeHtml(e.lines[0]?.accountName || '');
const editBtn = expOnlyMine
? `<button onclick='window.accountingView.editExpense(${JSON.stringify(JSON.stringify(e))})'
class="text-blue-600 hover:text-blue-800 text-xs font-medium">Edit</button>`
: `<span class="text-gray-300 text-xs" title="Only expenses created from this app can be edited">—</span>`;
return `
<tr class="border-t hover:bg-gray-50 align-top">
<td class="px-3 py-2 text-sm whitespace-nowrap">${escapeHtml(e.txnDate || '')}</td>
@@ -632,6 +637,7 @@ function renderExpensesTable(expenses) {
<td class="px-3 py-2 text-sm">${escapeHtml(e.refNo || '')}</td>
<td class="px-3 py-2 text-sm text-gray-500">${escapeHtml(e.memo || '')}</td>
<td class="px-3 py-2 text-sm text-right whitespace-nowrap text-red-600">${fmtMoney(e.totalAmt)}</td>
<td class="px-3 py-2 text-sm text-center">${editBtn}</td>
</tr>`;
}).join('');
@@ -647,6 +653,7 @@ function renderExpensesTable(expenses) {
<th class="px-3 py-2 text-left font-medium text-gray-700">Ref</th>
<th class="px-3 py-2 text-left font-medium text-gray-700">Memo</th>
<th class="px-3 py-2 text-right font-medium text-gray-700">Amount</th>
<th class="px-3 py-2 text-center font-medium text-gray-700">Action</th>
</tr>
</thead>
<tbody>${tbody}</tbody>
@@ -683,7 +690,19 @@ export function refreshAll() {
loadAccountsOverview();
if (registerAccountId) loadRegister();
}
export async function editExpense(expenseJson) {
let expense;
try {
expense = typeof expenseJson === 'string' ? JSON.parse(expenseJson) : expenseJson;
} catch (e) {
alert('Could not open expense for editing.');
return;
}
await openExpenseModal({
expense,
onSaved: () => loadExpenses()
});
}
window.accountingView = {
renderAccountingView,
refreshAll,
@@ -695,5 +714,6 @@ window.accountingView = {
loadBalanceSheet,
loadExpenses,
openNewExpense,
editExpense,
toggleSection
};