diff --git a/public/index.html b/public/index.html index e121bf7..d6c0cd1 100644 --- a/public/index.html +++ b/public/index.html @@ -30,6 +30,7 @@ + @@ -138,15 +139,15 @@
-${escapeHtml(title)}
+${escapeHtml(message)}
+No report data.
`; + const cols = (report.Columns && report.Columns.Column) || []; + const headerRow = cols.map(c => `QBO Error
-${escapeHtml(message)}
-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 += `| Customer | -Invoices | -Revenue (net) | -% of Total | -
|---|
No report data.
`; - const cols = (report.Columns && report.Columns.Column) || []; - const headerRow = cols.map(c => `| Period | -Status | -Tax Amount | -Adjustment | -Net Due | -Paid On | -Actions | -
|---|
\u2705 Paid on ${paidOn}
-| Total Sales | ${fmtMoney(parseFloat(existingPeriod.total_sales) || 0)} |
| Nontaxable Sales | ${fmtMoney(parseFloat(existingPeriod.nontaxable_sales) || 0)} |
| Taxable Sales | ${fmtMoney(parseFloat(existingPeriod.taxable_sales) || 0)} |
| Tax Collected | ${fmtMoney(parseFloat(existingPeriod.tax_collected) || 0)} |
| Adjustment | ${adjStr} |
| Reason: ${escapeHtml(existingPeriod.adjustment_reason)} | |
| Net Paid | ${fmtMoney(parseFloat(existingPeriod.net_paid) || 0)} |
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 today = todayISO(); - const [y, m] = startDate.split('-').map(Number); - const monthLabel = new Date(y, m - 1).toLocaleDateString('en-US', { year: 'numeric', month: 'long', timeZone: 'UTC' }); - - detailEl.innerHTML = ` -Positive = discount (reduces net due)
-Summary:
-Tax Collected: ${fmtMoney(taxCollected)}
- ${adjustments > 0 ? `Discount: \u2212${fmtMoney(adjustments)}
` : ''} - ${adjustments < 0 ? `Penalty: +$${Math.abs(adjustments).toFixed(2)}
` : ''} -Net Due: ${fmtMoney(netPaid)}
-✅ Booked — Journal Entry #${escapeHtml(existingPeriod.qbo_journal_entry_id)} on ${formatDate(existingPeriod.booked_at)}
-✅ Paid on ${formatDate(existingPeriod.booked_at)}
-Summary:
-Tax Collected: ${fmtMoney(taxCollected)}
- ${adj > 0 ? `Discount: \u2212${fmtMoney(adj)}
` : ''} - ${adj < 0 ? `Penalty: +$${Math.abs(adj).toFixed(2)}
` : ''} -Net Due: ${fmtMoney(netPaid)}
`; - } -} - -export async function saveTaxPeriodDraft() { - const adjAmount = parseFloat(document.getElementById('st-adjustment').value) || 0; - const adjReason = document.getElementById('st-adjustment-reason').value.trim(); - - 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, - net_paid: stCurrentTaxData.taxCollected - adjAmount, - status: 'open' - }); - 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 markPeriodPaid() { - const adjAmount = parseFloat(document.getElementById('st-adjustment').value) || 0; - const adjReason = document.getElementById('st-adjustment-reason').value.trim(); - const paidDate = document.getElementById('st-paid-date').value; - if (!paidDate) return alert('Please select a paid date.'); - - if (!stCurrentTaxData) return alert('No tax data loaded.'); - if (!stEditingPeriodId) return alert('Please save the period first (Save Draft).'); - - const netPaid = stCurrentTaxData.taxCollected - adjAmount; - - if (!confirm(`Mark this period as paid?\n\nPaid Date: ${paidDate}\nTax Collected: $${stCurrentTaxData.taxCollected.toFixed(2)}\nAdjustment: $${adjAmount.toFixed(2)}\nNet Paid: $${netPaid.toFixed(2)}\n\nThis does NOT write to QBO — record the payment in QBO first.`)) return; - - 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, - net_paid: netPaid, - status: 'open' - }); - } catch (e) { - return alert('Failed to save period: ' + e.message); - } - - try { - await window.API.accounting.markTaxPaidExternal(stEditingPeriodId, paidDate); - 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() @@ -1219,10 +496,7 @@ export function renderAccountingView() { autoSyncDoneThisOpen = false; injectToolbar(); injectRegisterControls(); - injectReportsControls(); injectExpensesSection(); - injectSalesTaxSection(); - loadTaxPeriods(); maybeAutoSyncCaches().then(() => { loadAccountsOverview(); @@ -1260,26 +534,10 @@ window.accountingView = { manualSync, loadAccountsOverview, loadRegister, - loadProfitLoss, - loadBalanceSheet, - loadTaxSummary, loadExpenses, openNewExpense, openNewRefund, editExpense, selectRegisterAccount, - toggleSection, - injectSalesTaxSection, - loadTaxPeriods, - openNewTaxPeriod, - openTaxPeriod, - closeTaxPeriodDetail, - updateTaxPreview, - saveTaxPeriodDraft, - markPeriodPaid, - loadCustomerRevenue, - exportCustomerRevenuePdf, - exportProfitLossPdf, - exportBalanceSheetPdf, - exportTaxSummaryPdf + toggleSection }; \ No newline at end of file diff --git a/public/js/views/reports-view.js b/public/js/views/reports-view.js index 423e02f..0a5da26 100644 --- a/public/js/views/reports-view.js +++ b/public/js/views/reports-view.js @@ -12,7 +12,13 @@ * formatiert. Ins CSV gehen die Rohwerte, damit Excel sauber rechnen kann. */ +import '../utils/api.js'; import { formatDate } from '../utils/helpers.js'; +import { + fmtMoney, escapeHtml, showError, showLoading, + todayISO, firstOfMonthISO, lastOfMonthISO, prevMonthISO, firstOfYearISO, + renderQboReport, initAlpineTree +} from '../utils/report-helpers.js'; // ── State ─────────────────────────────────────────────────────────── const today = new Date().toISOString().split('T')[0]; @@ -28,6 +34,20 @@ let revAnonymize = false; let revGroupByMonth = false; let revData = null; +// State der aus accounting-view.js übernommenen Reports (1:1) +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; + const AR_BUCKETS = [ { key: 'current', label: 'Current' }, { key: 'd1_30', label: '1–30 days' }, @@ -37,47 +57,12 @@ const AR_BUCKETS = [ ]; const BUCKET_LABEL = Object.fromEntries(AR_BUCKETS.map(b => [b.key, b.label])); -// ── Local helpers (Stil wie accounting-view.js) ───────────────────── +// ── Local helpers ─────────────────────────────────────────────────── +// fmtMoney/escapeHtml/showError/showLoading kommen aus report-helpers.js. -/** numeric-String → "$1,234.56". Kein Float-Zwischenschritt außer zur Anzeige. */ -function fmtMoney(v) { - if (v == null || v === '') return '—'; - const n = parseFloat(v); - if (isNaN(n)) return '—'; - const fixed = n.toFixed(2); - const [intPart, decPart] = fixed.replace('-', '').split('.'); - const grouped = intPart.replace(/\B(?=(\d{3})+(?!\d))/g, ','); - return `${n < 0 ? '-' : ''}$${grouped}.${decPart}`; -} - -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 = ` -Report Error
-${escapeHtml(message)}
-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 += `| Customer | +Invoices | +Revenue (net) | +% of Total | +
|---|
| Period | +Status | +Tax Amount | +Adjustment | +Net Due | +Paid On | +Actions | +
|---|
✅ Paid on ${paidOn}
+| Total Sales | ${fmtMoney(parseFloat(existingPeriod.total_sales) || 0)} |
| Nontaxable Sales | ${fmtMoney(parseFloat(existingPeriod.nontaxable_sales) || 0)} |
| Taxable Sales | ${fmtMoney(parseFloat(existingPeriod.taxable_sales) || 0)} |
| Tax Collected | ${fmtMoney(parseFloat(existingPeriod.tax_collected) || 0)} |
| Adjustment | ${adjStr} |
| Reason: ${escapeHtml(existingPeriod.adjustment_reason)} | |
| Net Paid | ${fmtMoney(parseFloat(existingPeriod.net_paid) || 0)} |
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 today = todayISO(); + const [y, m] = startDate.split('-').map(Number); + const monthLabel = new Date(y, m - 1).toLocaleDateString('en-US', { year: 'numeric', month: 'long', timeZone: 'UTC' }); + + detailEl.innerHTML = ` +Positive = discount (reduces net due)
+Summary:
+Tax Collected: ${fmtMoney(taxCollected)}
+ ${adjustments > 0 ? `Discount: −${fmtMoney(adjustments)}
` : ''} + ${adjustments < 0 ? `Penalty: +$${Math.abs(adjustments).toFixed(2)}
` : ''} +Net Due: ${fmtMoney(netPaid)}
+✅ Booked — Journal Entry #${escapeHtml(existingPeriod.qbo_journal_entry_id)} on ${formatDate(existingPeriod.booked_at)}
+✅ Paid on ${formatDate(existingPeriod.booked_at)}
+Summary:
+Tax Collected: ${fmtMoney(taxCollected)}
+ ${adj > 0 ? `Discount: −${fmtMoney(adj)}
` : ''} + ${adj < 0 ? `Penalty: +$${Math.abs(adj).toFixed(2)}
` : ''} +Net Due: ${fmtMoney(netPaid)}
`; + } +} + +export async function saveTaxPeriodDraft() { + const adjAmount = parseFloat(document.getElementById('st-adjustment').value) || 0; + const adjReason = document.getElementById('st-adjustment-reason').value.trim(); + + 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, + net_paid: stCurrentTaxData.taxCollected - adjAmount, + status: 'open' + }); + 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 markPeriodPaid() { + const adjAmount = parseFloat(document.getElementById('st-adjustment').value) || 0; + const adjReason = document.getElementById('st-adjustment-reason').value.trim(); + const paidDate = document.getElementById('st-paid-date').value; + if (!paidDate) return alert('Please select a paid date.'); + + if (!stCurrentTaxData) return alert('No tax data loaded.'); + if (!stEditingPeriodId) return alert('Please save the period first (Save Draft).'); + + const netPaid = stCurrentTaxData.taxCollected - adjAmount; + + if (!confirm(`Mark this period as paid?\n\nPaid Date: ${paidDate}\nTax Collected: $${stCurrentTaxData.taxCollected.toFixed(2)}\nAdjustment: $${adjAmount.toFixed(2)}\nNet Paid: $${netPaid.toFixed(2)}\n\nThis does NOT write to QBO — record the payment in QBO first.`)) return; + + 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, + net_paid: netPaid, + status: 'open' + }); + } catch (e) { + return alert('Failed to save period: ' + e.message); + } + + try { + await window.API.accounting.markTaxPaidExternal(stEditingPeriodId, paidDate); + 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 }; +} + +// ──────────────────────────────────────────────────────────────────── +// Init / Public Entry Point +// ──────────────────────────────────────────────────────────────────── + +export function renderSalesTaxView() { + injectSalesTaxSection(); + loadTaxPeriods(); +} + +window.salesTaxView = { + renderSalesTaxView, + injectSalesTaxSection, + loadTaxPeriods, + openNewTaxPeriod, + openTaxPeriod, + closeTaxPeriodDetail, + updateTaxPreview, + saveTaxPeriodDraft, + markPeriodPaid +};