/** * accounting-view.js * * Phase 1: Accounts Overview, Register, Reports * Phase 2 Lieferung 2: Expenses-Section + Auto-Sync-on-First-Open-of-Day */ 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) // ──────────────────────────────────────────────────────────────────── let allAccounts = []; let registerAccountId = null; let registerStartDate = null; let registerEndDate = null; let registerLoadSeq = 0; let plStartDate = null; let plEndDate = null; let plAccountingMethod = 'Accrual'; let bsAsOfDate = null; let bsAccountingMethod = 'Accrual'; let tsMonth = null; // 'YYYY-MM' — selected month for tax summary let tsAccountingMethod = 'Accrual'; let crStartDate = null; let crEndDate = null; let stPeriods = []; let stEditingPeriodId = null; // current period in detail dialog, null = new let stAccounts = []; // cached account list for pickers let expStartDate = null; let expEndDate = null; let expOnlyMine = false; // Auto-Sync nur einmal pro View-Mount let autoSyncDoneThisOpen = false; // ──────────────────────────────────────────────────────────────────── // 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(year, month) { const d = year != null ? new Date(year, month, 1) : new Date(); return year != null ? d.toISOString().split('T')[0] : new Date(d.getFullYear(), d.getMonth(), 1).toISOString().split('T')[0]; } function lastOfMonthISO(year, month) { const d = year != null ? new Date(year, month + 1, 0) : new Date(); return year != null ? d.toISOString().split('T')[0] : new Date(d.getFullYear(), d.getMonth() + 1, 0).toISOString().split('T')[0]; } function prevMonthISO() { const d = new Date(); const m = d.getMonth() === 0 ? 11 : d.getMonth() - 1; const y = d.getMonth() === 0 ? d.getFullYear() - 1 : d.getFullYear(); return `${y}-${String(m + 1).padStart(2, '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)}
`; } function makeCollapsible(headerText, contentId, startCollapsed = false) { return `

${escapeHtml(headerText)}

`; } export function toggleSection(contentId, headerEl) { const content = document.getElementById(contentId); if (!content) return; const isHidden = content.classList.toggle('hidden'); const arrow = headerEl.querySelector('svg'); if (arrow) arrow.classList.toggle('rotate-90', !isHidden); } // ──────────────────────────────────────────────────────────────────── // Toolbar // ──────────────────────────────────────────────────────────────────── export function injectToolbar() { const c = document.getElementById('accounting-toolbar'); if (!c) return; c.innerHTML = `

Accounting

read-only registers · expense entry
`; } // ──────────────────────────────────────────────────────────────────── // Auto-Sync beim ersten Öffnen des Tages // ──────────────────────────────────────────────────────────────────── async function maybeAutoSyncCaches() { if (autoSyncDoneThisOpen) return; autoSyncDoneThisOpen = true; try { const status = await window.API.accounting.getSyncStatus(); const accStale = status.accounts && status.accounts.staleToday; const venStale = status.vendors && status.vendors.staleToday; if (!accStale && !venStale) { updateSyncStatusBadge(status); return; } const syncBadge = document.getElementById('accounting-sync-status'); if (syncBadge) syncBadge.textContent = '🔄 Syncing caches…'; const tasks = []; if (accStale) tasks.push(window.API.accounting.syncAccounts()); if (venStale) tasks.push(window.API.accounting.syncVendors()); await Promise.all(tasks); const newStatus = await window.API.accounting.getSyncStatus(); updateSyncStatusBadge(newStatus); console.log('✅ Auto-synced QBO caches (first open of day)'); } catch (err) { console.warn('Auto-sync failed:', err.message); const syncBadge = document.getElementById('accounting-sync-status'); if (syncBadge) syncBadge.textContent = '⚠️ Sync failed'; } } export async function manualSync() { const syncBadge = document.getElementById('accounting-sync-status'); if (syncBadge) syncBadge.textContent = '🔄 Syncing…'; try { await Promise.all([ window.API.accounting.syncAccounts(), window.API.accounting.syncVendors() ]); const status = await window.API.accounting.getSyncStatus(); updateSyncStatusBadge(status); } catch (err) { if (syncBadge) syncBadge.textContent = '⚠️ Sync failed'; alert('Sync failed: ' + err.message); } } function updateSyncStatusBadge(status) { const el = document.getElementById('accounting-sync-status'); if (!el) return; const a = status.accounts; const v = status.vendors; if (a?.last_synced_at && v?.last_synced_at) { const aTime = new Date(a.last_synced_at).toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' }); el.innerHTML = `Synced @ ${aTime} · ${a.last_sync_count} accts · ${v.last_sync_count} vendors`; } else { el.textContent = 'Not yet synced'; } } // ──────────────────────────────────────────────────────────────────── // Accounts Overview // ──────────────────────────────────────────────────────────────────── export async function loadAccountsOverview() { const slot = 'accounting-accounts'; showLoading(slot, 'Loading accounts from QBO…'); try { const accounts = await window.API.accounting.getAccounts(null, true); if (accounts.error) return showError(slot, accounts.error); allAccounts = accounts; 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.
`; } else { el.innerHTML = `
${cards.map(renderAccountCard).join('')}
`; } 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 balText = a.currentBalance != null ? fmtMoney(a.currentBalance) : '—'; 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 = ` ${makeCollapsible('Register', 'register-section-body')}
`; } 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; try { const result = await window.API.accounting.getRegister(registerAccountId, registerStartDate, registerEndDate); if (mySeq !== registerLoadSeq) return; if (result.error) return showError(slot, result.error); 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; let rows = (result.rows || []).slice().sort((a, b) => (b.date || '').localeCompare(a.date || '')); const meta = result.meta || {}; if (!rows.length) { el.innerHTML = `
No transactions in selected range.
`; return; } const tbody = rows.map(renderRegisterRow).join(''); el.innerHTML = `
${escapeHtml(meta.reportName || 'Transaction List')} ${meta.startPeriod ? '— ' + escapeHtml(meta.startPeriod) : ''} ${meta.endPeriod ? ' to ' + escapeHtml(meta.endPeriod) : ''}
${rows.length} ${rows.length === 1 ? 'transaction' : 'transactions'}
${tbody}
Date Type No. Payee Split / Category Memo Amount
`; } function renderRegisterRow(r) { const isSplit = r.splitAccount === '-Split-'; const splitContent = isSplit ? renderSplitCell(r) : escapeHtml(r.splitAccount || ''); return ` ${escapeHtml(r.date || '')} ${escapeHtml(r.type || '')} ${escapeHtml(r.docNum || '')} ${escapeHtml(r.payee || '')} ${splitContent} ${escapeHtml(r.memo || '')} ${r.amount != null ? fmtMoney(r.amount) : ''} `; } function renderSplitCell(r) { if (!r.splits || !r.splits.length) { const type = (r.type || '').toLowerCase(); if (type.includes('tax payment')) { return `-Split- (Sales Tax)`; } if (type.includes('paycheck') || type.includes('payroll')) { return `-Split- (Payroll)`; } return `-Split-`; } const lines = r.splits.map(s => `
${escapeHtml(s.account || '?')} ${s.amount != null ? fmtMoney(s.amount) : ''}
`).join(''); return `
${lines}
`; } // ──────────────────────────────────────────────────────────────────── // Reports // ──────────────────────────────────────────────────────────────────── export function injectReportsControls() { const c = document.getElementById('accounting-reports'); if (!c) return; if (!plStartDate) plStartDate = firstOfYearISO(); if (!plEndDate) plEndDate = todayISO(); if (!bsAsOfDate) bsAsOfDate = todayISO(); if (!tsMonth) tsMonth = prevMonthISO(); if (!crStartDate) crStartDate = firstOfYearISO(); if (!crEndDate) crEndDate = todayISO(); c.innerHTML = ` ${makeCollapsible('Reports', 'reports-section-body')}

Profit & Loss

Balance Sheet

Sales Tax (QBO)

Customer Revenue

`; } 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'); } } export async function loadTaxSummary() { tsMonth = document.getElementById('ts-month').value; tsAccountingMethod = document.getElementById('ts-method').value; if (!tsMonth) return showError('ts-result', 'Please select a month.'); const [y, m] = tsMonth.split('-').map(Number); const startDate = firstOfMonthISO(y, m - 1); const endDate = lastOfMonthISO(y, m - 1); showLoading('ts-result', 'Loading Sales Tax Liability from QBO…'); try { const data = await window.API.accounting.getTaxSummary(startDate, endDate, tsAccountingMethod); if (data.error) return showError('ts-result', data.error); document.getElementById('ts-result').innerHTML = renderQboReport(data); } catch (err) { showError('ts-result', err.message || 'Failed to load Tax Summary'); } } export async function loadCustomerRevenue() { crStartDate = document.getElementById('cr-start').value; crEndDate = document.getElementById('cr-end').value; const anonymize = document.getElementById('cr-anonymize')?.checked || false; if (!crStartDate || !crEndDate) return showError('cr-result', 'Please select both start and end dates.'); showLoading('cr-result', 'Loading customer revenue...'); const maskName = (name) => anonymize ? name.charAt(0) : name; try { const data = await window.API.accounting.getCustomerRevenue(crStartDate, crEndDate); if (data.error) return showError('cr-result', data.error); if (!data.length) { document.getElementById('cr-result').innerHTML = '

No invoices found in this period.

'; return; } const grandTotal = parseFloat(data[0].grand_total) || 0; const totalInvoices = data.reduce((s, r) => s + parseInt(r.invoice_count), 0); let rowsHtml = ''; let rank = 0; for (const r of data) { rank++; const rev = parseFloat(r.total_revenue) || 0; const pct = grandTotal > 0 ? ((rev / grandTotal) * 100).toFixed(1) : '0.0'; rowsHtml += ` ${rank}. ${escapeHtml(maskName(r.customer_name))} ${r.invoice_count} ${fmtMoney(rev)} ${pct}% `; } rowsHtml += ` TOTAL (${data.length} customers) ${totalInvoices} ${fmtMoney(grandTotal)} 100.0% `; document.getElementById('cr-result').innerHTML = `
${rowsHtml}
Customer Invoices Revenue (net) % of Total
`; } catch (err) { showError('cr-result', err.message || 'Failed to load revenue report'); } } export function exportCustomerRevenuePdf() { const startEl = document.getElementById('cr-start'); const endEl = document.getElementById('cr-end'); const anonymize = document.getElementById('cr-anonymize')?.checked || false; if (!startEl?.value || !endEl?.value) return alert('Please select start and end dates first.'); let url = `/api/accounting/reports/customer-revenue/pdf?startDate=${startEl.value}&endDate=${endEl.value}`; if (anonymize) url += '&anonymize=true'; window.open(url, '_blank'); } function renderQboReport(report) { if (!report || !report.Header) return `

No report data.

`; const cols = (report.Columns && report.Columns.Column) || []; const headerRow = cols.map(c => `${escapeHtml(c.ColTitle || '')}`).join(''); const body = report.Rows && report.Rows.Row ? renderReportRows(report.Rows.Row, 0) : ''; 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) { 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 indent = depth * 16; if (row.Header && row.Header.ColData) { const cells = row.Header.ColData.map((c, i) => i === 0 ? `${escapeHtml(c.value || '')}` : `` ).join(''); html += `${cells}`; } if (isSection && row.Rows && row.Rows.Row) html += renderReportRows(row.Rows.Row, depth + 1); if (row.Summary && row.Summary.ColData) { const cells = row.Summary.ColData.map((c, i) => i === 0 ? `${escapeHtml(c.value || '')}` : `${escapeHtml(c.value || '')}` ).join(''); html += `${cells}`; } if (!isSection && row.ColData) { const cells = row.ColData.map((c, i) => i === 0 ? `${escapeHtml(c.value || '')}` : `${escapeHtml(c.value || '')}` ).join(''); html += `${cells}`; } } return html; } // ════════════════════════════════════════════════════════════════════ // Phase 2 Lieferung 2 — Expenses Section // ════════════════════════════════════════════════════════════════════ export function injectExpensesSection() { const c = document.getElementById('accounting-expenses'); if (!c) return; if (!expStartDate) expStartDate = firstOfMonthISO(); if (!expEndDate) expEndDate = todayISO(); c.innerHTML = ` ${makeCollapsible('Expenses', 'expenses-section-body')}
`; } export async function loadExpenses() { const startEl = document.getElementById('exp-start'); const endEl = document.getElementById('exp-end'); const onlyEl = document.getElementById('exp-only-mine'); expStartDate = startEl.value; expEndDate = endEl.value; expOnlyMine = onlyEl.checked; const slot = 'accounting-expenses-table'; showLoading(slot, 'Loading expenses from QBO…'); try { const list = await window.API.accounting.listExpenses(expStartDate, expEndDate, expOnlyMine); if (list.error) return showError(slot, list.error); renderExpensesTable(list); } catch (err) { showError(slot, err.message || 'Failed to load expenses'); } } function renderExpensesTable(expenses) { const el = document.getElementById('accounting-expenses-table'); if (!el) return; if (!expenses.length) { el.innerHTML = `
No expenses in selected range${expOnlyMine ? ' (created from this app)' : ''}.
`; return; } const sorted = expenses.slice().sort((a, b) => (b.txnDate || '').localeCompare(a.txnDate || '')); const tbody = sorted.map(e => { const splitsHtml = e.lines && e.lines.length > 1 ? `
${e.lines.length} lines
${e.lines.map(l => `
${escapeHtml(l.accountName || '?')} ${l.amount != null ? fmtMoney(l.amount) : ''}
`).join('')}
` : escapeHtml(e.lines[0]?.accountName || ''); const editBtn = expOnlyMine ? `` : ``; return ` ${escapeHtml(e.txnDate || '')} ${escapeHtml(e.vendorName || '')} ${escapeHtml(e.accountName || '')} ${splitsHtml} ${escapeHtml(e.refNo || '')} ${escapeHtml(e.memo || '')} ${fmtMoney(e.totalAmt)} ${editBtn} `; }).join(''); el.innerHTML = `
${tbody}
Date Vendor Payment Account Category Ref Memo Amount Action
${sorted.length} expense${sorted.length === 1 ? '' : 's'}
`; } // ──────────────────────────────────────────────────────────────────── // Sales Tax Periods // ──────────────────────────────────────────────────────────────────── export function injectSalesTaxSection() { const c = document.getElementById('accounting-sales-tax'); if (!c) return; c.innerHTML = ` ${makeCollapsible('Sales Tax', 'sales-tax-section-body')}

Period Overview

`; } export async function loadTaxPeriods() { try { stPeriods = await window.API.accounting.getTaxPeriods() || []; } catch (e) { stPeriods = []; console.error('Failed to load tax periods:', e.message); } renderTaxPeriodsTable(); } function renderTaxPeriodsTable() { const el = document.getElementById('sales-tax-periods-table'); if (!el) return; if (!stPeriods.length) { el.innerHTML = `
No sales tax periods recorded yet. Click "+ New Period" to get started.
`; return; } const rows = stPeriods.map(p => { const pStatus = p.status || (p.qbo_journal_entry_id ? 'booked' : 'open'); let statusHtml, statusColor; if (pStatus === 'booked') { statusHtml = 'Booked'; statusColor = 'bg-green-100 text-green-800'; } else if (pStatus === 'external') { statusHtml = 'External'; statusColor = 'bg-blue-100 text-blue-800'; } else { statusHtml = 'Open'; statusColor = 'bg-yellow-100 text-yellow-800'; } const monthLabel = new Date(p.period_start).toLocaleDateString('en-US', { year: 'numeric', month: 'long' }); const adj = parseFloat(p.adjustment_amount) || 0; const adjStr = adj !== 0 ? (adj > 0 ? `−$${adj.toFixed(2)}` : `+$${Math.abs(adj).toFixed(2)}`) : '—'; const netPaid = parseFloat(p.net_paid) || parseFloat(p.tax_collected) || 0; const paidOn = pStatus === 'external' ? 'Paid in QBO' : (p.booked_at ? formatDate(p.booked_at) : '—'); return ` ${monthLabel} ${statusHtml} ${fmtMoney(parseFloat(p.tax_collected) || 0)} ${adjStr} ${fmtMoney(netPaid)} ${paidOn} `; }).join(''); el.innerHTML = `
${rows}
Period Status Tax Amount Adjustment Net Due Paid On Actions
${stPeriods.length} period${stPeriods.length === 1 ? '' : 's'}
`; } export async function openNewTaxPeriod() { const [y, m] = prevMonthISO().split('-').map(Number); const startDate = firstOfMonthISO(y, m - 1); const endDate = lastOfMonthISO(y, m - 1); await openTaxPeriodDetail(startDate, endDate, null); } export async function openTaxPeriod(periodId) { const period = stPeriods.find(p => p.id === periodId); if (!period) return alert('Period not found.'); await openTaxPeriodDetail(period.period_start, period.period_end, period); } async function openTaxPeriodDetail(startDate, endDate, existingPeriod) { const detailEl = document.getElementById('sales-tax-detail'); if (!detailEl) return; stEditingPeriodId = existingPeriod ? existingPeriod.id : null; showLoading('sales-tax-detail', 'Loading tax summary from QBO…'); let taxData; try { taxData = await window.API.accounting.getTaxSummary(startDate, endDate, 'Accrual'); } catch (e) { showError('sales-tax-detail', e.message || 'Failed to load tax summary'); return; } if (!stAccounts.length) { try { const accts = await window.API.accounting.getAccounts() || []; stAccounts = Array.isArray(accts) ? accts : []; } catch (e) { stAccounts = []; } } const reportHtml = taxData?.Rows ? renderQboReport(taxData) : `

No tax data for this period.

`; const parsed = parseTaxDataFromReport(taxData); stCurrentTaxData = { ...parsed, startDate, endDate }; const taxCollected = parsed.taxCollected; const totalSales = parsed.totalSales; const nontaxable = parsed.nontaxableSales; const taxable = parsed.taxableSales; const adjustments = existingPeriod?.adjustment_amount != null ? parseFloat(existingPeriod.adjustment_amount) || 0 : 0; const adjReason = existingPeriod?.adjustment_reason || ''; const netPaid = taxCollected - adjustments; const periodStatus = existingPeriod?.status || (existingPeriod?.qbo_journal_entry_id ? 'booked' : 'open'); const isOpen = periodStatus === 'open'; const isEditable = isOpen; const bankOpts = stAccounts .filter(a => a.accountType === 'Bank' || a.accountType === 'Credit Card') .map(a => ``).join(''); const liabilityOpts = stAccounts .filter(a => a.accountType === 'Other Current Liability') .map(a => ``).join(''); const adjustOpts = stAccounts .filter(a => a.accountType === 'Income' || a.accountType === 'Expense' || a.classification === 'Revenue' || a.classification === 'Expense') .map(a => { const sel = a.id === existingPeriod?.adjustment_account_id ? 'selected' : ''; const name = (a.fullyQualifiedName || a.name || ''); const isDiscount = name.toLowerCase().includes('discount'); return ``; }).join(''); const today = todayISO(); const [y, m] = startDate.split('-').map(Number); const monthLabel = new Date(y, m - 1).toLocaleDateString('en-US', { year: 'numeric', month: 'long' }); detailEl.innerHTML = `

${monthLabel} — Sales Tax Detail

${reportHtml}
Tax Collected: ${fmtMoney(taxCollected)} Adjustment: ${adjustments > 0 ? `−${fmtMoney(adjustments)}` : adjustments < 0 ? `+$${Math.abs(adjustments).toFixed(2)}` : '—'} Net Due: ${fmtMoney(netPaid)}

Positive = discount (reduces net due)

${isEditable ? `

Journal Entry lines:

Debit Sales Tax Payable: ${fmtMoney(taxCollected)}

${adjustments > 0 ? `

Credit Discount: ${fmtMoney(adjustments)}

` : ''} ${adjustments < 0 ? `

Debit Penalty: ${fmtMoney(Math.abs(adjustments))}

` : ''}

Credit Bank: ${fmtMoney(netPaid)}

` : periodStatus === 'booked' ? `

✅ Booked — Journal Entry #${escapeHtml(existingPeriod.qbo_journal_entry_id)} on ${formatDate(existingPeriod.booked_at)}

` : `

📋 Marked as paid externally on ${formatDate(existingPeriod.booked_at)}

`}
`; detailEl.classList.remove('hidden'); detailEl.scrollIntoView({ behavior: 'smooth' }); } export function closeTaxPeriodDetail() { const el = document.getElementById('sales-tax-detail'); if (el) el.classList.add('hidden'); stEditingPeriodId = null; } export function updateTaxPreview() { if (!stCurrentTaxData) return; const adjInput = document.getElementById('st-adjustment'); const adj = parseFloat(adjInput?.value) || 0; const taxCollected = stCurrentTaxData.taxCollected; const netPaid = taxCollected - adj; const adjDisplay = document.getElementById('st-adj-display'); if (adjDisplay) { adjDisplay.className = adj !== 0 ? 'text-red-600' : ''; adjDisplay.innerHTML = adj > 0 ? `Adjustment: −${fmtMoney(adj)}` : adj < 0 ? `Adjustment: +$${Math.abs(adj).toFixed(2)}` : `Adjustment: —`; } const netDue = document.getElementById('st-net-due'); if (netDue) { netDue.innerHTML = `Net Due: ${fmtMoney(netPaid)}`; } const jeLines = document.getElementById('st-je-lines'); if (jeLines) { jeLines.innerHTML = `

Journal Entry lines:

Debit Sales Tax Payable: ${fmtMoney(taxCollected)}

${adj > 0 ? `

Credit Discount: ${fmtMoney(adj)}

` : ''} ${adj < 0 ? `

Debit Penalty: ${fmtMoney(Math.abs(adj))}

` : ''}

Credit Bank: ${fmtMoney(netPaid)}

`; } } export async function saveTaxPeriodDraft() { const adjAmount = parseFloat(document.getElementById('st-adjustment').value) || 0; const adjReason = document.getElementById('st-adjustment-reason').value.trim(); const adjAccountEl = document.getElementById('st-adjustment-account'); const adjAccountId = adjAccountEl.value; const adjAccountName = adjAccountId ? adjAccountEl.options[adjAccountEl.selectedIndex]?.text : ''; const bankEl = document.getElementById('st-bank-account'); const bankId = bankEl.value; const bankName = bankId ? bankEl.options[bankEl.selectedIndex]?.text : ''; const payableEl = document.getElementById('st-payable-account'); const payableId = payableEl.value; const payableName = payableId ? payableEl.options[payableEl.selectedIndex]?.text : ''; if (!stCurrentTaxData) return alert('No tax data loaded.'); try { const period = await window.API.accounting.upsertTaxPeriod({ period_start: stCurrentTaxData.startDate, period_end: stCurrentTaxData.endDate, total_sales: stCurrentTaxData.totalSales, nontaxable_sales: stCurrentTaxData.nontaxableSales, taxable_sales: stCurrentTaxData.taxableSales, tax_collected: stCurrentTaxData.taxCollected, adjustment_amount: adjAmount, adjustment_reason: adjReason || null, adjustment_account_id: adjAccountId || null, adjustment_account_name: adjAccountName || null, net_paid: stCurrentTaxData.taxCollected - adjAmount, bank_account_id: bankId || null, bank_account_name: bankName || null, sales_tax_payable_id: payableId || null, sales_tax_payable_name: payableName || null }); await loadTaxPeriods(); if (period?.period_start) { await openTaxPeriodDetail(period.period_start, period.period_end, period); } } catch (e) { alert('Failed to save: ' + e.message); } } export async function recordTaxPayment() { const adjAmount = parseFloat(document.getElementById('st-adjustment').value) || 0; const adjReason = document.getElementById('st-adjustment-reason').value.trim(); const adjAccountEl = document.getElementById('st-adjustment-account'); const adjAccountId = adjAccountEl.value; const adjAccountName = adjAccountId ? adjAccountEl.options[adjAccountEl.selectedIndex]?.text : ''; const bankEl = document.getElementById('st-bank-account'); const bankId = bankEl.value; const bankName = bankId ? bankEl.options[bankEl.selectedIndex]?.text : ''; const payableEl = document.getElementById('st-payable-account'); const payableId = payableEl.value; const payableName = payableId ? payableEl.options[payableEl.selectedIndex]?.text : ''; const txnDate = document.getElementById('st-payment-date').value; if (!txnDate) return alert('Please select a payment date.'); if (!stCurrentTaxData) return alert('No tax data loaded.'); const taxCollected = stCurrentTaxData.taxCollected; const netPaid = taxCollected - adjAmount; if (!payableId) return alert('Please select the Sales Tax Payable account.'); if (!bankId) return alert('Please select a bank account.'); const preview = `Journal Entry on ${txnDate}:\n\n` + `Debit Sales Tax Payable: $${taxCollected.toFixed(2)}\n` + (adjAmount > 0 ? `Credit Discount: $${adjAmount.toFixed(2)} (${adjReason || 'Adjustment'})\n` : '') + (adjAmount < 0 ? `Debit Penalty: $${Math.abs(adjAmount).toFixed(2)} (${adjReason || 'Adjustment'})\n` : '') + `Credit Bank: $${netPaid.toFixed(2)}\n\n` + (adjAmount > 0 ? `Debits (${taxCollected.toFixed(2)}) = Credits (${adjAmount.toFixed(2)} + ${netPaid.toFixed(2)} = ${(adjAmount + netPaid).toFixed(2)})` : adjAmount < 0 ? `Debits (${taxCollected.toFixed(2)} + ${Math.abs(adjAmount).toFixed(2)} = ${(taxCollected + Math.abs(adjAmount)).toFixed(2)}) = Credits (${netPaid.toFixed(2)})` : `Debits = Credits = ${taxCollected.toFixed(2)}`) + '\n\nRecord this in QBO?'; if (!confirm(preview)) return; let periodId = stEditingPeriodId; try { const period = await window.API.accounting.upsertTaxPeriod({ period_start: stCurrentTaxData.startDate, period_end: stCurrentTaxData.endDate, total_sales: stCurrentTaxData.totalSales, nontaxable_sales: stCurrentTaxData.nontaxableSales, taxable_sales: stCurrentTaxData.taxableSales, tax_collected: stCurrentTaxData.taxCollected, adjustment_amount: adjAmount, adjustment_reason: adjReason || null, adjustment_account_id: adjAccountId || null, adjustment_account_name: adjAccountName || null, net_paid: netPaid, bank_account_id: bankId || null, bank_account_name: bankName || null, sales_tax_payable_id: payableId || null, sales_tax_payable_name: payableName || null }); periodId = period.id; } catch (e) { return alert('Failed to save period before payment: ' + e.message); } if (typeof showSpinner === 'function') showSpinner('Recording tax payment in QBO…'); try { const result = await window.API.accounting.recordTaxPayment(periodId, { txnDate, taxCollected, adjustmentAmount: adjAmount, netPaid, salesTaxPayableId: payableId, salesTaxPayableName: payableName, adjustmentAccountId: adjAccountId || null, adjustmentAccountName: adjAccountName || null, adjustmentReason: adjReason || null, bankAccountId: bankId, bankAccountName: bankName }); if (result.error) return alert('Failed: ' + result.error); await loadTaxPeriods(); const updated = stPeriods.find(p => p.id === periodId); if (updated) { await openTaxPeriodDetail(updated.period_start, updated.period_end, updated); } } catch (e) { alert('QBO payment failed: ' + e.message); } finally { if (typeof hideSpinner === 'function') hideSpinner(); } } export async function markTaxPaidExternal() { if (!stEditingPeriodId) return alert('Please save the period first (Save Draft).'); const adjAmount = parseFloat(document.getElementById('st-adjustment').value) || 0; const adjReason = document.getElementById('st-adjustment-reason').value.trim(); const adjAccountEl = document.getElementById('st-adjustment-account'); const adjAccountId = adjAccountEl.value; const adjAccountName = adjAccountId ? adjAccountEl.options[adjAccountEl.selectedIndex]?.text : ''; const bankEl = document.getElementById('st-bank-account'); const bankId = bankEl.value; const bankName = bankId ? bankEl.options[bankEl.selectedIndex]?.text : ''; const payableEl = document.getElementById('st-payable-account'); const payableId = payableEl.value; const payableName = payableId ? payableEl.options[payableEl.selectedIndex]?.text : ''; if (!confirm(`Mark this period as already paid in QBO (external)?\n\nNo Journal Entry will be created — this only records the status in the app.\n\nTax Collected: $${stCurrentTaxData.taxCollected.toFixed(2)}\nAdjustment: $${adjAmount.toFixed(2)}\nNet Due: $${(stCurrentTaxData.taxCollected - adjAmount).toFixed(2)}`)) return; if (!stCurrentTaxData) return alert('No tax data loaded.'); try { await window.API.accounting.upsertTaxPeriod({ period_start: stCurrentTaxData.startDate, period_end: stCurrentTaxData.endDate, total_sales: stCurrentTaxData.totalSales, nontaxable_sales: stCurrentTaxData.nontaxableSales, taxable_sales: stCurrentTaxData.taxableSales, tax_collected: stCurrentTaxData.taxCollected, adjustment_amount: adjAmount, adjustment_reason: adjReason || null, adjustment_account_id: adjAccountId || null, adjustment_account_name: adjAccountName || null, net_paid: stCurrentTaxData.taxCollected - adjAmount, bank_account_id: bankId || null, bank_account_name: bankName || null, sales_tax_payable_id: payableId || null, sales_tax_payable_name: payableName || null, status: 'open' }); } catch (e) { return alert('Failed to save period: ' + e.message); } try { const result = await window.API.accounting.markTaxPaidExternal(stEditingPeriodId); await loadTaxPeriods(); const updated = stPeriods.find(p => p.id === stEditingPeriodId); if (updated) { await openTaxPeriodDetail(updated.period_start, updated.period_end, updated); } } catch (e) { alert('Failed: ' + e.message); } } function parseTaxDataFromReport(taxData) { if (!taxData?.Rows?.Row) return { totalSales: 0, nontaxableSales: 0, taxableSales: 0, taxCollected: 0 }; const rows = Array.isArray(taxData.Rows.Row) ? taxData.Rows.Row : []; for (const row of rows) { if (row.type === 'Section' && row.Header?.ColData?.[0]?.value?.toLowerCase().includes('grand total')) { const cd = row.Summary?.ColData || []; return { totalSales: parseFloat(cd[1]?.value || '0') || 0, nontaxableSales: parseFloat(cd[2]?.value || '0') || 0, taxableSales: parseFloat(cd[3]?.value || '0') || 0, taxCollected: parseFloat(cd[4]?.value || '0') || 0 }; } } return { totalSales: 0, nontaxableSales: 0, taxableSales: 0, taxCollected: 0 }; } let stCurrentTaxData = null; // parsed data for the open detail dialog export async function openNewExpense() { await openExpenseModal({ onSaved: () => loadExpenses() }); } // ──────────────────────────────────────────────────────────────────── // Init / Public Entry Points // ──────────────────────────────────────────────────────────────────── export function renderAccountingView() { autoSyncDoneThisOpen = false; injectToolbar(); injectRegisterControls(); injectReportsControls(); injectExpensesSection(); injectSalesTaxSection(); loadTaxPeriods(); maybeAutoSyncCaches().then(() => { loadAccountsOverview(); }); } 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() }); } export async function openNewRefund() { await openRefundModal({ onSaved: (result) => { alert(`✅ Refund recorded: ${fmtMoney(result.totalAmt)} from ${result.vendorName}\nDeposit #${result.id} — booked to ${result.categoryName}`); loadExpenses(); } }); } window.accountingView = { renderAccountingView, refreshAll, manualSync, loadAccountsOverview, loadRegister, loadProfitLoss, loadBalanceSheet, loadTaxSummary, loadExpenses, openNewExpense, openNewRefund, editExpense, selectRegisterAccount, toggleSection, injectSalesTaxSection, loadTaxPeriods, openNewTaxPeriod, openTaxPeriod, closeTaxPeriodDetail, updateTaxPreview, saveTaxPeriodDraft, recordTaxPayment, markTaxPaidExternal, loadCustomerRevenue, exportCustomerRevenuePdf };