diff --git a/public/js/modals/refund-modal.js b/public/js/modals/refund-modal.js
new file mode 100644
index 0000000..fd58e49
--- /dev/null
+++ b/public/js/modals/refund-modal.js
@@ -0,0 +1,282 @@
+/**
+ * refund-modal.js — Record Vendor Refund
+ *
+ * Erzeugt einen QBO Deposit gegen die ursprüngliche Expense-Kategorie.
+ * Für den Fall: Geld kam tatsächlich zurück aufs Bank-/Kreditkartenkonto.
+ */
+
+import '../utils/api.js';
+
+let modalEl = null;
+let onSavedCb = null;
+
+let vendors = [];
+let expenseAccounts = [];
+let paymentAccounts = [];
+
+let selectedVendorId = null;
+let selectedVendorName = '';
+let isSaving = false;
+
+function todayISO() { return new Date().toISOString().split('T')[0]; }
+
+function escapeHtml(s) {
+ if (s == null) return '';
+ return String(s)
+ .replace(/&/g, '&').replace(//g, '>')
+ .replace(/"/g, '"').replace(/'/g, ''');
+}
+function escapeAttr(s) {
+ return String(s || '')
+ .replace(/&/g, '&').replace(/"/g, '"')
+ .replace(//g, '>');
+}
+
+export async function openRefundModal({ onSaved } = {}) {
+ onSavedCb = onSaved || null;
+ selectedVendorId = null;
+ selectedVendorName = '';
+ isSaving = false;
+
+ try {
+ [vendors, expenseAccounts, paymentAccounts] = await Promise.all([
+ window.API.accounting.getVendors('', 1000),
+ window.API.accounting.getExpenseAccounts(),
+ window.API.accounting.getPaymentAccounts()
+ ]);
+ for (const [name, data] of [['vendors', vendors], ['expenseAccounts', expenseAccounts], ['paymentAccounts', paymentAccounts]]) {
+ if (data && data.error) {
+ alert(`Failed to load ${name}: ${data.error}\n\nTry "Sync from QBO" first.`);
+ return;
+ }
+ }
+ } catch (err) {
+ alert('Failed to load refund modal data: ' + err.message);
+ return;
+ }
+
+ renderModal();
+}
+
+function closeModal() {
+ if (modalEl) { modalEl.remove(); modalEl = null; }
+}
+
+function renderModal() {
+ closeModal();
+
+ const html = `
+
No vendor found.
`;
+ } else {
+ html = filtered.map(v => `
+
+
${escapeHtml(v.displayName)}
+ ${v.email ? `
${escapeHtml(v.email)}
` : ''}
+
`).join('');
+ }
+
+ dropdownEl.innerHTML = html;
+ dropdownEl.classList.remove('hidden');
+
+ if (!dropdownEl._outsideHandlerInstalled) {
+ dropdownEl._outsideHandlerInstalled = true;
+ document.addEventListener('click', (e) => {
+ if (!dropdownEl.contains(e.target) && e.target !== inputEl) {
+ dropdownEl.classList.add('hidden');
+ }
+ });
+ }
+}
+
+function selectVendor(id, name) {
+ selectedVendorId = id;
+ selectedVendorName = name;
+ document.getElementById('ref-vendor-search').value = name;
+ document.getElementById('ref-vendor-id').value = id;
+ document.getElementById('ref-vendor-dropdown').classList.add('hidden');
+}
+
+async function save() {
+ if (isSaving) return;
+ hideError();
+
+ if (!selectedVendorId) return showError('Please select a vendor.');
+
+ const depositAccountId = document.getElementById('ref-deposit-account').value;
+ if (!depositAccountId) return showError('Please select the deposit account.');
+
+ const categoryAccountId = document.getElementById('ref-category').value;
+ if (!categoryAccountId) return showError('Please select the original expense category.');
+
+ const txnDate = document.getElementById('ref-date').value;
+ if (!txnDate) return showError('Please enter a date.');
+
+ const amount = parseFloat(document.getElementById('ref-amount').value);
+ if (!isFinite(amount) || amount <= 0) {
+ return showError('Please enter a positive refund amount.');
+ }
+
+ const payload = {
+ vendorId: selectedVendorId,
+ depositAccountId,
+ categoryAccountId,
+ txnDate,
+ amount,
+ refNo: document.getElementById('ref-ref-no').value.trim() || null,
+ memo: document.getElementById('ref-memo').value.trim() || null
+ };
+
+ isSaving = true;
+ const btn = document.getElementById('ref-save-btn');
+ btn.disabled = true;
+ btn.textContent = 'Saving…';
+
+ try {
+ const result = await window.API.accounting.createRefund(payload);
+ if (result.error) {
+ showError(result.error);
+ return;
+ }
+ if (typeof onSavedCb === 'function') {
+ try { onSavedCb(result); } catch (e) { console.warn('onSaved cb threw:', e); }
+ }
+ closeModal();
+ } catch (err) {
+ showError(err.message || 'Save failed');
+ } finally {
+ isSaving = false;
+ if (btn) { btn.disabled = false; btn.textContent = 'Record Refund'; }
+ }
+}
+
+function showError(msg) {
+ const el = document.getElementById('ref-error');
+ if (el) { el.textContent = msg; el.classList.remove('hidden'); }
+}
+function hideError() {
+ const el = document.getElementById('ref-error');
+ if (el) el.classList.add('hidden');
+}
+
+window.refundModal = {
+ open: openRefundModal,
+ close: closeModal,
+ onVendorInput,
+ selectVendor,
+ save
+};
\ No newline at end of file
diff --git a/public/js/utils/api.js b/public/js/utils/api.js
index 0eab879..311c479 100644
--- a/public/js/utils/api.js
+++ b/public/js/utils/api.js
@@ -169,6 +169,12 @@ const API = {
body: JSON.stringify(data)
}).then(r => r.json()),
+ createRefund: (data) => fetch('/api/accounting/refunds', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(data)
+ }).then(r => r.json()),
+
updateExpense: (id, data) => fetch(`/api/accounting/expenses/${id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
diff --git a/public/js/views/accounting-view.js b/public/js/views/accounting-view.js
index 2684745..db19678 100644
--- a/public/js/views/accounting-view.js
+++ b/public/js/views/accounting-view.js
@@ -8,6 +8,7 @@
import '../utils/api.js';
import { formatDate } from '../utils/helpers.js';
import { openExpenseModal } from '../modals/expense-modal.js';
+import { openRefundModal } from '../modals/refund-modal.js';
// ────────────────────────────────────────────────────────────────────
// State (modul-lokal)
@@ -568,7 +569,11 @@ export function injectExpensesSection() {
class="px-3 py-1.5 bg-blue-600 text-white rounded-md text-sm font-medium hover:bg-blue-700">
Load
-
+
+