diff --git a/public/js/utils/api.js b/public/js/utils/api.js index a283702..80e9abc 100644 --- a/public/js/utils/api.js +++ b/public/js/utils/api.js @@ -139,6 +139,10 @@ const API = { const params = new URLSearchParams({ asOfDate, accountingMethod }); return fetch('/api/accounting/reports/balance-sheet?' + params.toString()).then(r => r.json()); }, + getTaxSummary: (startDate, endDate, accountingMethod = 'Accrual') => { + const params = new URLSearchParams({ startDate, endDate, accountingMethod }); + return fetch('/api/accounting/reports/tax-summary?' + params.toString()).then(r => r.json()); + }, // Phase 2 Lieferung 1 — Sync + Cache-Reads syncAccounts: () => fetch('/api/accounting/sync-accounts', { method: 'POST' }).then(r => r.json()), diff --git a/public/js/views/accounting-view.js b/public/js/views/accounting-view.js index db19678..8926193 100644 --- a/public/js/views/accounting-view.js +++ b/public/js/views/accounting-view.js @@ -27,6 +27,9 @@ let plAccountingMethod = 'Accrual'; let bsAsOfDate = null; let bsAccountingMethod = 'Accrual'; +let tsMonth = null; // 'YYYY-MM' — selected month for tax summary +let tsAccountingMethod = 'Accrual'; + let expStartDate = null; let expEndDate = null; let expOnlyMine = false; @@ -47,9 +50,19 @@ function fmtMoney(n) { } function todayISO() { return new Date().toISOString().split('T')[0]; } -function firstOfMonthISO() { +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(); - return new Date(d.getFullYear(), d.getMonth(), 1).toISOString().split('T')[0]; + 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(); @@ -416,6 +429,7 @@ export function injectReportsControls() { if (!plStartDate) plStartDate = firstOfYearISO(); if (!plEndDate) plEndDate = todayISO(); if (!bsAsOfDate) bsAsOfDate = todayISO(); + if (!tsMonth) tsMonth = prevMonthISO(); c.innerHTML = ` ${makeCollapsible('Reports', 'reports-section-body')} @@ -455,6 +469,22 @@ export function injectReportsControls() {
+
+

Sales Tax (QBO)

+
+
+
+
+
+
+ +
+
+
+
`; } @@ -482,6 +512,21 @@ export async function loadBalanceSheet() { } 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'); } +} + function renderQboReport(report) { if (!report || !report.Header) return `

No report data.

`; const cols = (report.Columns && report.Columns.Column) || []; @@ -724,6 +769,7 @@ window.accountingView = { loadRegister, loadProfitLoss, loadBalanceSheet, + loadTaxSummary, loadExpenses, openNewExpense, openNewRefund, diff --git a/src/routes/accounting.js b/src/routes/accounting.js index 1c318e7..5dbbed1 100644 --- a/src/routes/accounting.js +++ b/src/routes/accounting.js @@ -85,6 +85,16 @@ router.get('/reports/balance-sheet', async (req, res) => { } catch (err) { handleQboError(err, res, 'balance-sheet'); } }); +router.get('/reports/tax-summary', async (req, res) => { + try { + res.json(await accountingService.getTaxSummary({ + startDate: req.query.startDate, + endDate: req.query.endDate, + accountingMethod: req.query.accountingMethod || 'Accrual' + })); + } catch (err) { handleQboError(err, res, 'tax-summary'); } +}); + // ════════════════════════════════════════════════════════════════════ // Phase 2 Lieferung 1 — Sync + Cache-Reads // ════════════════════════════════════════════════════════════════════ diff --git a/src/services/accounting-service.js b/src/services/accounting-service.js index c5b8028..38a0903 100644 --- a/src/services/accounting-service.js +++ b/src/services/accounting-service.js @@ -367,6 +367,14 @@ async function getBalanceSheet({ asOfDate, accountingMethod = 'Accrual' } = {}) return data; } +async function getTaxSummary({ startDate, endDate, accountingMethod = 'Accrual' } = {}) { + const url = buildReportUrl('TaxSummary', { start_date: startDate, end_date: endDate, accounting_method: accountingMethod }); + const response = await makeQboApiCall({ url, method: 'GET' }); + const data = getJson(response); + throwIfFault(data, 'TaxSummary report'); + return data; +} + // ════════════════════════════════════════════════════════════════════ // Phase 2 Lieferung 1 — Caches und Sync // ════════════════════════════════════════════════════════════════════ @@ -1418,6 +1426,7 @@ module.exports = { getRegister, getProfitAndLoss, getBalanceSheet, + getTaxSummary, normalizeTransactionListReport, // Phase 2 Lieferung 1 — Sync