/** * accounting-view.js — Phase 1, read-only * * Drei Bereiche: * 1) Accounts Overview — Cards mit Bank- und Credit-Card-Balances * 2) Register — read-only Liste der Transaktionen für ein Konto * 3) Reports — Profit & Loss + Balance Sheet */ import '../utils/api.js'; // ← NEU import { formatDate } from '../utils/helpers.js'; // ──────────────────────────────────────────────────────────────────── // State (modul-lokal) // ──────────────────────────────────────────────────────────────────── let allAccounts = []; // alle aktiven Accounts (gesamt, für Dropdown) let registerAccountId = null; // aktuell ausgewählter Account fürs Register let registerStartDate = null; let registerEndDate = null; // Reports let plStartDate = null; let plEndDate = null; let plAccountingMethod = 'Accrual'; let bsAsOfDate = null; let bsAccountingMethod = 'Accrual'; let registerLoadSeq = 0; // ──────────────────────────────────────────────────────────────────── // Helpers // ──────────────────────────────────────────────────────────────────── function fmtMoney(n) { if (n == null || isNaN(n)) return ''; return n.toLocaleString('en-US', { style: 'currency', currency: 'USD', minimumFractionDigits: 2, maximumFractionDigits: 2 }); } function todayISO() { return new Date().toISOString().split('T')[0]; } function firstOfMonthISO() { const d = new Date(); return new Date(d.getFullYear(), d.getMonth(), 1).toISOString().split('T')[0]; } function firstOfYearISO() { const d = new Date(); return new Date(d.getFullYear(), 0, 1).toISOString().split('T')[0]; } function escapeHtml(s) { if (s == null) return ''; return String(s) .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } function showError(slotId, message) { const el = document.getElementById(slotId); if (!el) return; el.innerHTML = `

QBO Error

${escapeHtml(message)}

`; } function showLoading(slotId, message = 'Loading…') { const el = document.getElementById(slotId); if (!el) return; el.innerHTML = `
${escapeHtml(message)}
`; } // ──────────────────────────────────────────────────────────────────── // Toolbar (top-of-tab heading + sync button) // ──────────────────────────────────────────────────────────────────── export function injectToolbar() { const c = document.getElementById('accounting-toolbar'); if (!c) return; c.innerHTML = `

Accounting

read-only
`; } // ──────────────────────────────────────────────────────────────────── // Accounts Overview // ──────────────────────────────────────────────────────────────────── export async function loadAccountsOverview() { const slot = 'accounting-accounts'; showLoading(slot, 'Loading accounts from QBO…'); try { // Wir laden alle aktiven Accounts in einem Rutsch und filtern client-seitig. const accounts = await window.API.accounting.getAccounts(null, true); if (accounts.error) { showError(slot, accounts.error); return; } allAccounts = accounts; // Bank/Credit Card Cards rendern const cards = accounts.filter(a => a.accountType === 'Bank' || a.accountType === 'Credit Card' ); const el = document.getElementById(slot); if (!cards.length) { el.innerHTML = `
No bank or credit card accounts found in QBO.
`; } else { el.innerHTML = `
${cards.map(a => renderAccountCard(a)).join('')}
`; } // Register-Dropdown füttern (nur Bank + Credit Card) populateRegisterAccountDropdown(cards); } catch (err) { console.error('Accounts load failed:', err); showError(slot, err.message || 'Failed to load accounts'); } } function renderAccountCard(a) { const isBank = a.accountType === 'Bank'; const accent = isBank ? 'border-blue-200 bg-blue-50' : 'border-purple-200 bg-purple-50'; const label = isBank ? 'Bank' : 'Credit Card'; const labelColor = isBank ? 'text-blue-700' : 'text-purple-700'; const bal = a.currentBalance; const balText = bal != null ? fmtMoney(bal) : '—'; // Bei Credit Cards ist ein positiver CurrentBalance i.d.R. eine Schuld; nur Hinweis, keine Vorzeichenakrobatik. return `
${label} #${a.id}
${escapeHtml(a.name)}
${balText}
${a.accountSubType ? `
${escapeHtml(a.accountSubType)}
` : ''}
`; } // ──────────────────────────────────────────────────────────────────── // Register // ──────────────────────────────────────────────────────────────────── function populateRegisterAccountDropdown(bankCardAccounts) { const sel = document.getElementById('reg-account'); if (!sel) return; const current = registerAccountId || sel.value; sel.innerHTML = `` + bankCardAccounts.map(a => `` ).join(''); if (current && bankCardAccounts.find(a => a.id === current)) { sel.value = current; } } export function injectRegisterControls() { const c = document.getElementById('accounting-register-controls'); if (!c) return; if (!registerStartDate) registerStartDate = firstOfMonthISO(); if (!registerEndDate) registerEndDate = todayISO(); c.innerHTML = `
`; } export function selectRegisterAccount(accountId) { registerAccountId = accountId; const sel = document.getElementById('reg-account'); if (sel) sel.value = accountId; loadRegister(); } export async function loadRegister() { const sel = document.getElementById('reg-account'); const start = document.getElementById('reg-start'); const end = document.getElementById('reg-end'); if (!sel || !sel.value) { const slot = document.getElementById('accounting-register-table'); if (slot) slot.innerHTML = `

Select an account to view the register.

`; return; } registerAccountId = sel.value; registerStartDate = start.value; registerEndDate = end.value; const slot = 'accounting-register-table'; showLoading(slot, 'Loading register from QBO…'); const mySeq = ++registerLoadSeq; // ← merken, welcher Call das ist try { const result = await window.API.accounting.getRegister( registerAccountId, registerStartDate, registerEndDate ); // Wenn inzwischen ein neuerer Call gestartet wurde → Result verwerfen if (mySeq !== registerLoadSeq) return; if (result.error) { showError(slot, result.error); return; } renderRegisterTable(result); } catch (err) { if (mySeq !== registerLoadSeq) return; console.error('Register load failed:', err); showError(slot, err.message || 'Failed to load register'); } } function renderRegisterTable(result) { const el = document.getElementById('accounting-register-table'); if (!el) return; const rows = result.rows || []; const meta = result.meta || {}; if (!rows.length) { el.innerHTML = `
No transactions in selected range.
`; return; } const tbody = rows.map(r => ` ${escapeHtml(r.date || '')} ${escapeHtml(r.type || '')} ${escapeHtml(r.docNum || '')} ${escapeHtml(r.payee || '')} ${escapeHtml(r.splitAccount || '')} ${escapeHtml((r.memo || '').slice(0, 60))} ${r.amount != null ? fmtMoney(r.amount) : ''} `).join(''); el.innerHTML = `
${escapeHtml(meta.reportName || 'Transaction List')} ${meta.startPeriod ? '— ' + escapeHtml(meta.startPeriod) : ''} ${meta.endPeriod ? ' to ' + escapeHtml(meta.endPeriod) : ''} · ${rows.length} rows
${tbody}
Date Type No. Payee Split / Category Memo Amount
`; } // ──────────────────────────────────────────────────────────────────── // Reports — P&L + Balance Sheet // ──────────────────────────────────────────────────────────────────── export function injectReportsControls() { const c = document.getElementById('accounting-reports'); if (!c) return; if (!plStartDate) plStartDate = firstOfYearISO(); if (!plEndDate) plEndDate = todayISO(); if (!bsAsOfDate) bsAsOfDate = todayISO(); c.innerHTML = `

Profit & Loss

Balance Sheet

`; } export async function loadProfitLoss() { plStartDate = document.getElementById('pl-start').value; plEndDate = document.getElementById('pl-end').value; plAccountingMethod = document.getElementById('pl-method').value; showLoading('pl-result', 'Loading P&L from QBO…'); try { const data = await window.API.accounting.getProfitAndLoss( plStartDate, plEndDate, plAccountingMethod ); if (data.error) return showError('pl-result', data.error); document.getElementById('pl-result').innerHTML = renderQboReport(data); } catch (err) { showError('pl-result', err.message || 'Failed to load P&L'); } } export async function loadBalanceSheet() { bsAsOfDate = document.getElementById('bs-asof').value; bsAccountingMethod = document.getElementById('bs-method').value; showLoading('bs-result', 'Loading Balance Sheet from QBO…'); try { const data = await window.API.accounting.getBalanceSheet( bsAsOfDate, bsAccountingMethod ); if (data.error) return showError('bs-result', data.error); document.getElementById('bs-result').innerHTML = renderQboReport(data); } catch (err) { showError('bs-result', err.message || 'Failed to load Balance Sheet'); } } // ──────────────────────────────────────────────────────────────────── // Generic QBO Report Renderer (P&L + Balance Sheet) // QBO Reports haben rekursive Section/Row-Bäume mit Summary-Zeilen. // Wir rendern sie als verschachtelte HTML-Tabelle. // ──────────────────────────────────────────────────────────────────── function renderQboReport(report) { if (!report || !report.Header) { return `

No report data.

`; } const cols = (report.Columns && report.Columns.Column) || []; const colCount = cols.length; let body = ''; if (report.Rows && report.Rows.Row) { body = renderReportRows(report.Rows.Row, 0, colCount); } const headerRow = cols.map(c => `${escapeHtml(c.ColTitle || '')}` ).join(''); return `
${escapeHtml(report.Header.ReportName || '')} ${report.Header.StartPeriod ? '· ' + escapeHtml(report.Header.StartPeriod) + ' – ' + escapeHtml(report.Header.EndPeriod) : ''} ${report.Header.ReportBasis ? '· ' + escapeHtml(report.Header.ReportBasis) : ''}
${headerRow}${body}
`; } function renderReportRows(rows, depth, colCount) { if (!rows) return ''; const arr = Array.isArray(rows) ? rows : [rows]; let html = ''; for (const row of arr) { const isSection = row.type === 'Section' || row.Rows || row.Summary; const indentPx = depth * 16; if (row.Header && row.Header.ColData) { // Section header row const headerCells = row.Header.ColData.map((c, i) => { if (i === 0) { return `${escapeHtml(c.value || '')}`; } return ``; }).join(''); html += `${headerCells}`; } if (isSection && row.Rows && row.Rows.Row) { html += renderReportRows(row.Rows.Row, depth + 1, colCount); } if (row.Summary && row.Summary.ColData) { const sumCells = row.Summary.ColData.map((c, i) => { if (i === 0) { return `${escapeHtml(c.value || '')}`; } return `${escapeHtml(c.value || '')}`; }).join(''); html += `${sumCells}`; } if (!isSection && row.ColData) { // Plain data row const cells = row.ColData.map((c, i) => { if (i === 0) { return `${escapeHtml(c.value || '')}`; } return `${escapeHtml(c.value || '')}`; }).join(''); html += `${cells}`; } } return html; } // ──────────────────────────────────────────────────────────────────── // Init / Public Entry Points // ──────────────────────────────────────────────────────────────────── export function renderAccountingView() { injectToolbar(); injectRegisterControls(); injectReportsControls(); loadAccountsOverview(); } export function refreshAll() { loadAccountsOverview(); if (registerAccountId) loadRegister(); } // Expose for onclick handlers window.accountingView = { renderAccountingView, refreshAll, loadAccountsOverview, loadRegister, selectRegisterAccount, loadProfitLoss, loadBalanceSheet };