/** * salestax-view.js — manuelle Sales-Tax-Verwaltung * * Aus accounting-view.js herausgelöst (vormals Sektion "Sales Tax" dort). * Logik unverändert übernommen; angepasst wurden ausschließlich: * - der onclick-Namespace: window.accountingView.X() → window.salesTaxView.X() * - Helper kommen jetzt aus ../utils/report-helpers.js statt modul-lokal * - der Sektions-Klapper makeCollapsible('Sales Tax', …) entfällt ersatzlos * * Hinweis: openTaxPeriodDetail() ruft API.accounting.getTaxSummary() auf — * dieselbe QBO-Schnittstelle, die der Report "Sales Tax (QBO)" auf dem * Reports-Tab nutzt. Die Abhängigkeit liegt auf API-Ebene, die Trennung der * beiden Oberflächen berührt sie nicht. * * Maßgeblichkeit der Beträge: Sobald eine Periode in sales_tax_periods steht, * ist der dort gespeicherte QBO-Snapshot (tax_collected & Co.) der angezeigte * Grundbetrag. Der Live-Abruf liefert dann nur noch die Distrikt-Aufschlüsselung. * Nur für eine noch nicht gespeicherte Periode kommen die Kennzahlen live. */ import '../utils/api.js'; import { formatDate } from '../utils/helpers.js'; import { fmtMoney, escapeHtml, showError, showLoading, todayISO, firstOfMonthISO, lastOfMonthISO, prevMonthISO, renderQboReport } from '../utils/report-helpers.js'; // ──────────────────────────────────────────────────────────────────── // State (modul-lokal) // ──────────────────────────────────────────────────────────────────── let stPeriods = []; let stEditingPeriodId = null; // current period in detail dialog, null = new let stCurrentTaxData = null; // parsed data for the open detail dialog // ──────────────────────────────────────────────────────────────────── // Sales Tax Periods // ──────────────────────────────────────────────────────────────────── export function injectSalesTaxSection() { const c = document.getElementById('salestax-content'); if (!c) return; c.innerHTML = `
| 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 snapshotNote = useSnapshot ? `Grand total shown is the saved QBO snapshot for this period${existingPeriod.updated_at ? ` (last saved ${formatDate(existingPeriod.updated_at)})` : ''}. The district rows above are recalculated live and may differ in detail.
` : ''; 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 }; } /** numeric aus der DB (pg liefert NUMERIC als String) — sonst Fallback. */ function numOr(value, fallback) { if (value == null || value === '') return fallback; const n = parseFloat(value); return Number.isFinite(n) ? n : fallback; } /** * Kennzahlen einer gespeicherten Periode aus der DB-Zeile. * Fällt spaltenweise auf den Live-Wert zurück, falls die Spalte leer ist * (Altbestände ohne vollständigen Snapshot). */ function snapshotFromPeriod(period, fallback) { return { totalSales: numOr(period.total_sales, fallback.totalSales), nontaxableSales: numOr(period.nontaxable_sales, fallback.nontaxableSales), taxableSales: numOr(period.taxable_sales, fallback.taxableSales), taxCollected: numOr(period.tax_collected, fallback.taxCollected) }; } /** * Ersetzt im live geladenen Report die GRAND-TOTAL-Zeile durch den gespeicherten * Snapshot, damit die Tabelle nicht einen anderen Gesamtbetrag zeigt als die * Summary-Leiste darunter. Die Distrikt-Zeilen bleiben unberührt (Aufschlüsselung). * Arbeitet auf einer flachen Kopie — taxData selbst wird nicht mutiert. */ function withSnapshotGrandTotal(taxData, snapshot) { if (!taxData?.Rows?.Row || !Array.isArray(taxData.Rows.Row)) return taxData; const rows = taxData.Rows.Row.map(row => { const label = row?.Header?.ColData?.[0]?.value; if (row?.type !== 'Section' || !label || !label.toLowerCase().includes('grand total')) return row; const cd = row.Summary?.ColData || []; return { ...row, Summary: { ColData: [ { value: cd[0]?.value || 'Total' }, { value: snapshot.totalSales.toFixed(2) }, { value: snapshot.nontaxableSales.toFixed(2) }, { value: snapshot.taxableSales.toFixed(2) }, { value: snapshot.taxCollected.toFixed(2) }, { value: cd[5]?.value || '' } ] } }; }); return { ...taxData, Rows: { ...taxData.Rows, Row: rows } }; } // ──────────────────────────────────────────────────────────────────── // Init / Public Entry Point // ──────────────────────────────────────────────────────────────────── export function renderSalesTaxView() { injectSalesTaxSection(); loadTaxPeriods(); } window.salesTaxView = { renderSalesTaxView, injectSalesTaxSection, loadTaxPeriods, openNewTaxPeriod, openTaxPeriod, closeTaxPeriodDetail, updateTaxPreview, saveTaxPeriodDraft, markPeriodPaid };