Files
invoice-system/public/js/views/salestax-view.js
2026-08-07 12:46:36 -05:00

495 lines
26 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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 = `
<div class="bg-white rounded-lg shadow-sm border border-gray-200 overflow-hidden">
<div class="px-4 py-3 border-b bg-gray-50 flex flex-wrap items-center gap-3">
<h3 class="font-semibold text-gray-800 text-sm">Period Overview</h3>
<div class="ml-auto flex gap-2">
<button onclick="window.salesTaxView.openNewTaxPeriod()"
class="px-3 py-1.5 bg-blue-600 text-white rounded-md text-sm font-medium hover:bg-blue-700">
+ New Period
</button>
</div>
</div>
<div id="sales-tax-periods-table"></div>
</div>
<div id="sales-tax-detail" class="mt-4 hidden"></div>`;
}
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 = `<div class="p-4 text-gray-500 text-sm">No sales tax periods recorded yet. Click "+ New Period" to get started.</div>`;
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' || pStatus === 'paid') { statusHtml = 'Paid'; statusColor = 'bg-green-100 text-green-800'; }
else { statusHtml = 'Open'; statusColor = 'bg-yellow-100 text-yellow-800'; }
const [py, pm] = String(p.period_start).split('T')[0].split('-');
const monthLabel = new Date(Number(py), Number(pm) - 1).toLocaleDateString('en-US', { year: 'numeric', month: 'long', timeZone: 'UTC' });
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' || pStatus === 'paid' || pStatus === 'booked')
? (p.booked_at ? formatDate(p.booked_at) : '—') : '—';
return `
<tr class="border-t hover:bg-gray-50">
<td class="px-3 py-2 text-sm font-medium text-gray-800">${monthLabel}</td>
<td class="px-3 py-2 text-sm">
<span class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-semibold ${statusColor}">${statusHtml}</span>
</td>
<td class="px-3 py-2 text-sm text-right">${fmtMoney(parseFloat(p.tax_collected) || 0)}</td>
<td class="px-3 py-2 text-sm text-right">${adjStr}</td>
<td class="px-3 py-2 text-sm text-right font-semibold">${fmtMoney(netPaid)}</td>
<td class="px-3 py-2 text-sm text-gray-500">${paidOn}</td>
<td class="px-3 py-2 text-sm text-center">
<button onclick="window.salesTaxView.openTaxPeriod(${p.id})"
class="text-blue-600 hover:text-blue-800 text-xs font-medium">View summary</button>
</td>
</tr>`;
}).join('');
el.innerHTML = `
<div class="overflow-x-auto">
<table class="min-w-full text-sm">
<thead class="bg-gray-50">
<tr>
<th class="px-3 py-2 text-left font-medium text-gray-700">Period</th>
<th class="px-3 py-2 text-left font-medium text-gray-700">Status</th>
<th class="px-3 py-2 text-right font-medium text-gray-700">Tax Amount</th>
<th class="px-3 py-2 text-right font-medium text-gray-700">Adjustment</th>
<th class="px-3 py-2 text-right font-medium text-gray-700">Net Due</th>
<th class="px-3 py-2 text-left font-medium text-gray-700">Paid On</th>
<th class="px-3 py-2 text-center font-medium text-gray-700">Actions</th>
</tr>
</thead>
<tbody>${rows}</tbody>
</table>
<div class="px-3 py-2 text-xs text-gray-500 border-t bg-gray-50">${stPeriods.length} period${stPeriods.length === 1 ? '' : 's'}</div>
</div>`;
}
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;
if (existingPeriod && existingPeriod.status === 'paid') {
const [py, pm] = String(existingPeriod.period_start).split('T')[0].split('-');
const MONTHS = ['January','February','March','April','May','June','July','August','September','October','November','December'];
const mLabel = MONTHS[Number(pm) - 1] + ' ' + py;
const adj = parseFloat(existingPeriod.adjustment_amount) || 0;
const adjStr = adj > 0 ? `${fmtMoney(adj)}` : adj < 0 ? `+$${Math.abs(adj).toFixed(2)}` : '—';
const paidOn = formatDate(existingPeriod.booked_at);
detailEl.innerHTML = `
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-4">
<div class="flex items-center justify-between mb-3">
<h3 class="font-semibold text-gray-800">${mLabel} — Sales Tax Detail (Paid)</h3>
<button onclick="window.salesTaxView.closeTaxPeriodDetail()" class="px-2 py-1 text-gray-400 hover:text-gray-600 text-lg leading-none">&times;</button>
</div>
<p class="text-sm text-green-700 font-semibold mb-4">✅ Paid on ${paidOn}</p>
<table class="min-w-full text-sm border border-gray-200 rounded">
<tbody>
<tr class="border-t"><td class="px-3 py-2 text-gray-600">Total Sales</td><td class="px-3 py-2 text-right font-medium">${fmtMoney(parseFloat(existingPeriod.total_sales) || 0)}</td></tr>
<tr class="border-t"><td class="px-3 py-2 text-gray-600">Nontaxable Sales</td><td class="px-3 py-2 text-right font-medium">${fmtMoney(parseFloat(existingPeriod.nontaxable_sales) || 0)}</td></tr>
<tr class="border-t"><td class="px-3 py-2 text-gray-600">Taxable Sales</td><td class="px-3 py-2 text-right font-medium">${fmtMoney(parseFloat(existingPeriod.taxable_sales) || 0)}</td></tr>
<tr class="border-t"><td class="px-3 py-2 text-gray-600">Tax Collected</td><td class="px-3 py-2 text-right font-medium">${fmtMoney(parseFloat(existingPeriod.tax_collected) || 0)}</td></tr>
<tr class="border-t"><td class="px-3 py-2 text-gray-600">Adjustment</td><td class="px-3 py-2 text-right font-medium ${adj !== 0 ? 'text-red-600' : ''}">${adjStr}</td></tr>
${existingPeriod.adjustment_reason ? `<tr class="border-t"><td class="px-3 py-2 text-gray-500 text-xs pl-8" colspan="2">Reason: ${escapeHtml(existingPeriod.adjustment_reason)}</td></tr>` : ''}
<tr class="border-t bg-gray-50 font-semibold"><td class="px-3 py-2">Net Paid</td><td class="px-3 py-2 text-right">${fmtMoney(parseFloat(existingPeriod.net_paid) || 0)}</td></tr>
</tbody>
</table>
</div>`;
detailEl.classList.remove('hidden');
detailEl.scrollIntoView({ behavior: 'smooth' });
return;
}
showLoading('sales-tax-detail', 'Loading tax summary from QBO…');
let taxData = null;
try {
taxData = await window.API.accounting.getTaxSummary(startDate, endDate, 'Accrual');
} catch (e) {
// Für eine gespeicherte Periode ist der Live-Abruf nur Beiwerk: die
// Kennzahlen stehen in der DB-Zeile, also weiter mit dem Snapshot.
if (!existingPeriod) {
showError('sales-tax-detail', e.message || 'Failed to load tax summary');
return;
}
console.error('Tax summary fetch failed, showing saved snapshot:', e.message);
}
// Gespeicherte Periode → DB-Snapshot ist maßgeblich; der Live-Abruf liefert
// nur die Distrikt-Zeilen. Neue Periode → live geladene Werte.
const liveParsed = parseTaxDataFromReport(taxData);
const useSnapshot = !!existingPeriod;
const parsed = useSnapshot ? snapshotFromPeriod(existingPeriod, liveParsed) : liveParsed;
const reportData = useSnapshot ? withSnapshotGrandTotal(taxData, parsed) : taxData;
const reportHtml = reportData?.Rows
? renderQboReport(reportData)
: `<p class="text-sm text-gray-500">No tax data for this period.</p>`;
const snapshotNote = useSnapshot
? `<p class="mt-2 text-xs text-gray-500">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.</p>`
: '';
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 = `
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-4">
<div class="flex items-center justify-between mb-3">
<h3 class="font-semibold text-gray-800">${monthLabel} — Sales Tax Detail</h3>
<button onclick="window.salesTaxView.closeTaxPeriodDetail()"
class="px-2 py-1 text-gray-400 hover:text-gray-600 text-lg leading-none">&times;</button>
</div>
${reportHtml}
${snapshotNote}
<div class="mt-4 border-t pt-4">
<div class="flex items-center gap-4 text-sm mb-3" id="st-summary-bar">
<span><strong>Tax Collected:</strong> ${fmtMoney(taxCollected)}${useSnapshot ? ' <span class="text-xs text-gray-400">(saved)</span>' : ''}</span>
<span id="st-adj-display" class="${adjustments !== 0 ? 'text-red-600' : ''}"><strong>Adjustment:</strong> ${adjustments > 0 ? `${fmtMoney(adjustments)}` : adjustments < 0 ? `+$${Math.abs(adjustments).toFixed(2)}` : '—'}</span>
<span id="st-net-due" class="text-lg font-bold text-gray-900"><strong>Net Due:</strong> ${fmtMoney(netPaid)}</span>
</div>
${isEditable ? `
<div class="grid grid-cols-1 md:grid-cols-3 gap-3 mb-3">
<div>
<label class="block text-xs font-medium text-gray-700 mb-1">Adjustment Amount</label>
<input type="number" step="0.01" id="st-adjustment" value="${adjustments !== 0 ? adjustments : ''}" placeholder="e.g. 6.15 (discount)"
oninput="window.salesTaxView.updateTaxPreview()"
class="w-full px-3 py-1.5 border border-gray-300 rounded-md text-sm">
<p class="text-xs text-gray-400 mt-0.5">Positive = discount (reduces net due)</p>
</div>
<div>
<label class="block text-xs font-medium text-gray-700 mb-1">Reason</label>
<input type="text" id="st-adjustment-reason" value="${escapeHtml(adjReason)}" placeholder="e.g. Timely filing discount"
class="w-full px-3 py-1.5 border border-gray-300 rounded-md text-sm">
</div>
<div>
<label class="block text-xs font-medium text-gray-700 mb-1">Paid Date (QBO)</label>
<input type="date" id="st-paid-date" value="${today}"
class="w-full px-3 py-1.5 border border-gray-300 rounded-md text-sm">
</div>
</div>
<div class="flex items-center justify-between border-t pt-3 mt-1">
<div id="st-je-lines">
<p class="text-sm"><strong>Summary:</strong></p>
<p class="text-xs text-gray-500">Tax Collected: ${fmtMoney(taxCollected)}</p>
${adjustments > 0 ? `<p class="text-xs text-gray-500">Discount: ${fmtMoney(adjustments)}</p>` : ''}
${adjustments < 0 ? `<p class="text-xs text-gray-500">Penalty: +$${Math.abs(adjustments).toFixed(2)}</p>` : ''}
<p class="text-xs text-gray-500 font-semibold">Net Due: ${fmtMoney(netPaid)}</p>
</div>
<div class="flex gap-2">
<button onclick="window.salesTaxView.saveTaxPeriodDraft()"
class="px-3 py-1.5 bg-gray-100 hover:bg-gray-200 text-gray-700 rounded-md text-sm font-medium border border-gray-300">
💾 Save Draft
</button>
<button onclick="window.salesTaxView.markPeriodPaid()"
class="px-4 py-2 bg-green-600 text-white rounded-md text-sm font-semibold hover:bg-green-700">
✅ Mark as Paid
</button>
</div>
</div>
` : periodStatus === 'booked' ? `
<div class="border-t pt-3 mt-1">
<p class="text-sm text-green-700 font-semibold">✅ Booked — Journal Entry #${escapeHtml(existingPeriod.qbo_journal_entry_id)} on ${formatDate(existingPeriod.booked_at)}</p>
</div>
` : `
<div class="border-t pt-3 mt-1">
<p class="text-sm text-green-700 font-semibold">✅ Paid on ${formatDate(existingPeriod.booked_at)}</p>
</div>
`}
</div>
</div>`;
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
? `<strong>Adjustment:</strong> ${fmtMoney(adj)}`
: adj < 0
? `<strong>Adjustment:</strong> +$${Math.abs(adj).toFixed(2)}`
: `<strong>Adjustment:</strong> —`;
}
const netDue = document.getElementById('st-net-due');
if (netDue) {
netDue.innerHTML = `<strong>Net Due:</strong> ${fmtMoney(netPaid)}`;
}
const jeLines = document.getElementById('st-je-lines');
if (jeLines) {
jeLines.innerHTML = `
<p class="text-sm"><strong>Summary:</strong></p>
<p class="text-xs text-gray-500">Tax Collected: ${fmtMoney(taxCollected)}</p>
${adj > 0 ? `<p class="text-xs text-gray-500">Discount: ${fmtMoney(adj)}</p>` : ''}
${adj < 0 ? `<p class="text-xs text-gray-500">Penalty: +$${Math.abs(adj).toFixed(2)}</p>` : ''}
<p class="text-xs text-gray-500 font-semibold">Net Due: ${fmtMoney(netPaid)}</p>`;
}
}
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
};