Major features: - Pixel-Perfect glassmorphism design (landing + POS + public catalog) - OEM/Local catalog toggle with Nexpart taxonomy (14 groups, 108 subgroups, 558 part types) - Marketplace B2B Phase 1 (bodegas, POs, status machine, WA+email notifications) - Peer-to-peer inventory (multi-instance, LAN discovery) - WhatsApp: photo→Vision AI, voice→Whisper, conversational quotations - Smart unified search (VIN/plate/part_number/keyword auto-detect) - Shop Supplies tab (vehicle-independent parts) - Chatbot AI fallback chain (5 models) + response cache - CSV inventory import tool + setup_instance.sh installer - Tablet-responsive CSS + sidebar toggle - Filters, export CSV, employee edit, business data save - Quotation system (WA→POS) with auto-print on confirmation - Live stats on landing page Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
510 lines
25 KiB
JavaScript
510 lines
25 KiB
JavaScript
// /home/Autopartes/pos/static/js/accounting.js
|
|
// Accounting module — wired to design-system HTML IDs
|
|
// Tabs: panel-cxc, panel-cxp, panel-balance, panel-resultados, panel-flujo, panel-conciliacion, panel-cierre
|
|
|
|
const Accounting = (() => {
|
|
const API = '/pos/api/accounting';
|
|
|
|
function token() {
|
|
return localStorage.getItem('pos_token') || '';
|
|
}
|
|
|
|
function headers() {
|
|
return { 'Authorization': `Bearer ${token()}`, 'Content-Type': 'application/json' };
|
|
}
|
|
|
|
async function api(path, opts = {}) {
|
|
const res = await fetch(`${API}${path}`, { headers: headers(), ...opts });
|
|
if (!res.ok) {
|
|
const err = await res.json().catch(() => ({ error: res.statusText }));
|
|
throw new Error(err.error || 'Request failed');
|
|
}
|
|
return res.json();
|
|
}
|
|
|
|
function fmt(n) {
|
|
return parseFloat(n || 0).toLocaleString('es-MX', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
|
}
|
|
|
|
// ---- Auth check ----
|
|
function checkAuth() {
|
|
if (!token()) {
|
|
window.location.href = '/pos/login';
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// ---- Tab switching (matches design system onclick="switchTab('xxx')") ----
|
|
function switchTab(name) {
|
|
document.querySelectorAll('.tab-btn').forEach(btn => {
|
|
btn.classList.remove('is-active');
|
|
btn.setAttribute('aria-selected', 'false');
|
|
});
|
|
document.querySelectorAll('.tab-panel').forEach(p => p.classList.remove('is-active'));
|
|
|
|
// Activate button
|
|
const tabBtn = document.querySelector(`.tab-btn[onclick*="'${name}'"]`);
|
|
if (tabBtn) {
|
|
tabBtn.classList.add('is-active');
|
|
tabBtn.setAttribute('aria-selected', 'true');
|
|
}
|
|
|
|
// Activate panel
|
|
const panel = document.getElementById(`panel-${name}`);
|
|
if (panel) panel.classList.add('is-active');
|
|
|
|
// Load data
|
|
if (name === 'cxc') loadAging();
|
|
if (name === 'cxp') loadAccountsPayable();
|
|
if (name === 'balance') loadBalanceSheet();
|
|
if (name === 'resultados') loadIncomeStatement();
|
|
if (name === 'flujo') loadCashFlow();
|
|
if (name === 'conciliacion') loadReconciliation();
|
|
if (name === 'cierre') loadPeriodClose();
|
|
}
|
|
|
|
// ---- Badge helper ----
|
|
function statusBadge(status, label) {
|
|
const map = {
|
|
pending: 'badge--pending',
|
|
vigente: 'badge--pending',
|
|
overdue: 'badge--overdue',
|
|
vencida: 'badge--overdue',
|
|
partial: 'badge--partial',
|
|
parcial: 'badge--partial',
|
|
ok: 'badge--ok',
|
|
pagada: 'badge--ok',
|
|
open: 'badge--pending',
|
|
closed: 'badge--ok',
|
|
};
|
|
return `<span class="badge ${map[status] || ''}">${label || status}</span>`;
|
|
}
|
|
|
|
// ---- Tab 1: Cuentas por Cobrar (Aging) ----
|
|
async function loadAging() {
|
|
const panel = document.getElementById('panel-cxc');
|
|
if (!panel) return;
|
|
const tbody = panel.querySelector('.data-table tbody');
|
|
if (!tbody) return;
|
|
|
|
try {
|
|
const res = await api('/aging');
|
|
const rows = res.data || [];
|
|
if (!rows.length) {
|
|
tbody.innerHTML = '<tr><td colspan="9" style="text-align:center;padding:var(--space-6);color:var(--color-text-muted);">No hay cuentas por cobrar.</td></tr>';
|
|
return;
|
|
}
|
|
|
|
tbody.innerHTML = rows.map(r => {
|
|
const status = r.days_overdue > 0 ? 'overdue' : r.paid > 0 && r.balance > 0 ? 'partial' : r.balance <= 0 ? 'ok' : 'pending';
|
|
const label = status === 'overdue' ? 'Vencida' : status === 'partial' ? 'Parcial' : status === 'ok' ? 'Pagada' : 'Vigente';
|
|
return `<tr>
|
|
<td class="td--mono">${r.invoice || r.folio || '-'}</td>
|
|
<td class="td--primary">${r.name || r.customer_name || '-'}</td>
|
|
<td>${r.issue_date ? new Date(r.issue_date).toLocaleDateString('es-MX') : '-'}</td>
|
|
<td>${r.due_date ? new Date(r.due_date).toLocaleDateString('es-MX') : '-'}</td>
|
|
<td class="td--amount">$${fmt(r.total)}</td>
|
|
<td class="td--amount">$${fmt(r.paid || 0)}</td>
|
|
<td class="td--amount">$${fmt(r.balance || r.total)}</td>
|
|
<td>${statusBadge(status, label)}</td>
|
|
<td><button class="btn btn--ghost btn--sm">${r.balance > 0 ? 'Cobrar' : 'Ver'}</button></td>
|
|
</tr>`;
|
|
}).join('');
|
|
|
|
// Update pagination text
|
|
const pagSpan = panel.querySelector('.pagination span');
|
|
if (pagSpan) pagSpan.textContent = `Mostrando 1-${rows.length} de ${res.totals?.count || rows.length} registros`;
|
|
|
|
// Update summary card badge count
|
|
updateBadgeCount('cxc', rows.length);
|
|
} catch (e) {
|
|
tbody.innerHTML = `<tr><td colspan="9" style="color:var(--color-error);padding:var(--space-4);">Error: ${e.message}</td></tr>`;
|
|
}
|
|
}
|
|
|
|
// ---- Tab 2: Cuentas por Pagar ----
|
|
async function loadAccountsPayable() {
|
|
const panel = document.getElementById('panel-cxp');
|
|
if (!panel) return;
|
|
const tbody = panel.querySelector('.data-table tbody');
|
|
if (!tbody) return;
|
|
|
|
try {
|
|
// Use accounts endpoint filtered for payables or a dedicated endpoint
|
|
const res = await api('/aging?type=payable');
|
|
const rows = res.data || [];
|
|
if (!rows.length) {
|
|
tbody.innerHTML = '<tr><td colspan="9" style="text-align:center;padding:var(--space-6);color:var(--color-text-muted);">No hay cuentas por pagar.</td></tr>';
|
|
return;
|
|
}
|
|
|
|
tbody.innerHTML = rows.map(r => {
|
|
const status = r.days_overdue > 0 ? 'overdue' : r.paid > 0 && r.balance > 0 ? 'partial' : r.balance <= 0 ? 'ok' : 'pending';
|
|
const label = status === 'overdue' ? 'Vencida' : status === 'partial' ? 'Parcial' : status === 'ok' ? 'Pagada' : 'Vigente';
|
|
return `<tr>
|
|
<td class="td--mono">${r.invoice || r.folio || '-'}</td>
|
|
<td class="td--primary">${r.name || r.vendor_name || '-'}</td>
|
|
<td>${r.receipt_date ? new Date(r.receipt_date).toLocaleDateString('es-MX') : '-'}</td>
|
|
<td>${r.due_date ? new Date(r.due_date).toLocaleDateString('es-MX') : '-'}</td>
|
|
<td class="td--amount">$${fmt(r.total)}</td>
|
|
<td class="td--amount">$${fmt(r.paid || 0)}</td>
|
|
<td class="td--amount">$${fmt(r.balance || r.total)}</td>
|
|
<td>${statusBadge(status, label)}</td>
|
|
<td><button class="btn btn--ghost btn--sm">${r.balance > 0 ? 'Pagar' : 'Ver'}</button></td>
|
|
</tr>`;
|
|
}).join('');
|
|
|
|
const pagSpan = panel.querySelector('.pagination span');
|
|
if (pagSpan) pagSpan.textContent = `Mostrando 1-${rows.length} de ${res.totals?.count || rows.length} registros`;
|
|
} catch (e) {
|
|
tbody.innerHTML = `<tr><td colspan="9" style="color:var(--color-error);padding:var(--space-4);">Error: ${e.message}</td></tr>`;
|
|
}
|
|
}
|
|
|
|
// ---- Tab 3: Balance General ----
|
|
async function loadBalanceSheet() {
|
|
const panel = document.getElementById('panel-balance');
|
|
if (!panel) return;
|
|
const grid = panel.querySelector('.finance-grid');
|
|
if (!grid) return;
|
|
|
|
try {
|
|
const now = new Date();
|
|
const res = await api(`/balance-sheet?date=${now.toISOString().slice(0, 10)}`);
|
|
|
|
// Build assets card
|
|
const activoCard = grid.querySelector('.finance-card:first-child');
|
|
const pasivoCard = grid.querySelector('.finance-card:last-child');
|
|
|
|
if (activoCard) {
|
|
let html = `<div class="finance-card__header"><div class="finance-card__title">Activos</div></div>`;
|
|
if (res.activo && res.activo.items) {
|
|
for (const item of res.activo.items) {
|
|
const isNeg = item.balance < 0;
|
|
html += `<div class="finance-card__row finance-card__row--indent">
|
|
<span class="finance-card__row-label">${item.code ? item.code + ' ' : ''}${item.name}</span>
|
|
<span class="finance-card__row-value ${isNeg ? 'finance-card__row-value--negative' : ''}">$${fmt(item.balance)}</span>
|
|
</div>`;
|
|
}
|
|
}
|
|
html += `<div class="finance-card__row finance-card__row--total">
|
|
<span class="finance-card__row-label">Total Activos</span>
|
|
<span class="finance-card__row-value">$${fmt(res.activo?.total || 0)}</span>
|
|
</div>`;
|
|
activoCard.innerHTML = html;
|
|
}
|
|
|
|
if (pasivoCard) {
|
|
let html = `<div class="finance-card__header"><div class="finance-card__title">Pasivo + Capital</div></div>`;
|
|
// Pasivo
|
|
if (res.pasivo && res.pasivo.items) {
|
|
html += `<div class="finance-card__row finance-card__row--section"><span class="finance-card__row-label">Pasivo</span><span></span></div>`;
|
|
for (const item of res.pasivo.items) {
|
|
html += `<div class="finance-card__row finance-card__row--indent">
|
|
<span class="finance-card__row-label">${item.code ? item.code + ' ' : ''}${item.name}</span>
|
|
<span class="finance-card__row-value">$${fmt(item.balance)}</span>
|
|
</div>`;
|
|
}
|
|
html += `<div class="finance-card__row"><span class="finance-card__row-label" style="font-weight:var(--font-weight-semibold)">Total Pasivo</span><span class="finance-card__row-value">$${fmt(res.pasivo.total)}</span></div>`;
|
|
}
|
|
// Capital
|
|
if (res.capital && res.capital.items) {
|
|
html += `<div class="finance-card__row finance-card__row--section"><span class="finance-card__row-label">Capital Contable</span><span></span></div>`;
|
|
for (const item of res.capital.items) {
|
|
const isPos = item.balance > 0;
|
|
html += `<div class="finance-card__row finance-card__row--indent">
|
|
<span class="finance-card__row-label">${item.code ? item.code + ' ' : ''}${item.name}</span>
|
|
<span class="finance-card__row-value ${isPos ? 'finance-card__row-value--positive' : ''}">$${fmt(item.balance)}</span>
|
|
</div>`;
|
|
}
|
|
html += `<div class="finance-card__row"><span class="finance-card__row-label" style="font-weight:var(--font-weight-semibold)">Total Capital</span><span class="finance-card__row-value">$${fmt(res.capital.total)}</span></div>`;
|
|
}
|
|
html += `<div class="finance-card__row finance-card__row--total">
|
|
<span class="finance-card__row-label">Total Pasivo + Capital</span>
|
|
<span class="finance-card__row-value">$${fmt((res.pasivo?.total || 0) + (res.capital?.total || 0))}</span>
|
|
</div>`;
|
|
pasivoCard.innerHTML = html;
|
|
}
|
|
|
|
// Update period selector text
|
|
const sel = panel.querySelector('.select-filter');
|
|
if (sel && res.as_of) {
|
|
sel.innerHTML = `<option>Al ${res.as_of}</option>`;
|
|
}
|
|
} catch (e) {
|
|
grid.innerHTML = `<p style="color:var(--color-error);padding:var(--space-4);">Error: ${e.message}</p>`;
|
|
}
|
|
}
|
|
|
|
// ---- Tab 4: Estado de Resultados ----
|
|
async function loadIncomeStatement() {
|
|
const panel = document.getElementById('panel-resultados');
|
|
if (!panel) return;
|
|
const grid = panel.querySelector('.finance-grid');
|
|
if (!grid) return;
|
|
|
|
try {
|
|
const now = new Date();
|
|
const res = await api(`/income-statement?year=${now.getFullYear()}&month=${now.getMonth() + 1}`);
|
|
|
|
const card = grid.querySelector('.finance-card');
|
|
if (!card) return;
|
|
|
|
let html = `<div class="finance-card__header"><div class="finance-card__title">Estado de Resultados</div></div>`;
|
|
|
|
// Ingresos
|
|
html += `<div class="finance-card__row finance-card__row--section"><span class="finance-card__row-label">Ingresos</span><span></span></div>`;
|
|
if (res.ingresos && res.ingresos.items) {
|
|
for (const item of res.ingresos.items) {
|
|
html += `<div class="finance-card__row finance-card__row--indent">
|
|
<span class="finance-card__row-label">${item.name}</span>
|
|
<span class="finance-card__row-value">$${fmt(item.amount)}</span>
|
|
</div>`;
|
|
}
|
|
}
|
|
html += `<div class="finance-card__row"><span class="finance-card__row-label" style="font-weight:var(--font-weight-semibold)">Total Ingresos</span><span class="finance-card__row-value">$${fmt(res.ingresos?.total || 0)}</span></div>`;
|
|
|
|
// Costos
|
|
html += `<div class="finance-card__row finance-card__row--section"><span class="finance-card__row-label">Costo de Ventas</span><span></span></div>`;
|
|
if (res.costos && res.costos.items) {
|
|
for (const item of res.costos.items) {
|
|
html += `<div class="finance-card__row finance-card__row--indent">
|
|
<span class="finance-card__row-label">${item.name}</span>
|
|
<span class="finance-card__row-value finance-card__row-value--negative">-$${fmt(Math.abs(item.amount))}</span>
|
|
</div>`;
|
|
}
|
|
}
|
|
html += `<div class="finance-card__row"><span class="finance-card__row-label" style="font-weight:var(--font-weight-semibold)">Utilidad Bruta</span><span class="finance-card__row-value finance-card__row-value--positive">$${fmt(res.utilidad_bruta || 0)}</span></div>`;
|
|
|
|
// Gastos
|
|
html += `<div class="finance-card__row finance-card__row--section"><span class="finance-card__row-label">Gastos de Operacion</span><span></span></div>`;
|
|
if (res.gastos && res.gastos.items) {
|
|
for (const item of res.gastos.items) {
|
|
html += `<div class="finance-card__row finance-card__row--indent">
|
|
<span class="finance-card__row-label">${item.name}</span>
|
|
<span class="finance-card__row-value finance-card__row-value--negative">-$${fmt(Math.abs(item.amount))}</span>
|
|
</div>`;
|
|
}
|
|
}
|
|
html += `<div class="finance-card__row"><span class="finance-card__row-label" style="font-weight:var(--font-weight-semibold)">Total Gastos Operacion</span><span class="finance-card__row-value finance-card__row-value--negative">-$${fmt(Math.abs(res.gastos?.total || 0))}</span></div>`;
|
|
|
|
// Utilidad neta
|
|
const netColor = (res.utilidad_neta || 0) >= 0 ? 'finance-card__row-value--positive' : 'finance-card__row-value--negative';
|
|
html += `<div class="finance-card__row finance-card__row--total">
|
|
<span class="finance-card__row-label">Utilidad Neta</span>
|
|
<span class="finance-card__row-value ${netColor}">$${fmt(res.utilidad_neta || 0)}</span>
|
|
</div>`;
|
|
|
|
card.innerHTML = html;
|
|
} catch (e) {
|
|
grid.innerHTML = `<p style="color:var(--color-error);padding:var(--space-4);">Error: ${e.message}</p>`;
|
|
}
|
|
}
|
|
|
|
// ---- Tab 5: Flujo de Efectivo ----
|
|
async function loadCashFlow() {
|
|
// Flujo de Efectivo currently has no dedicated API endpoint, keep demo data
|
|
// Future: wire to /pos/api/accounting/cash-flow
|
|
}
|
|
|
|
// ---- Tab 6: Conciliacion Bancaria ----
|
|
async function loadReconciliation() {
|
|
// Bank reconciliation currently has no dedicated API endpoint, keep demo data
|
|
// Future: wire to /pos/api/accounting/reconciliation
|
|
}
|
|
|
|
// ---- Tab 7: Cierre de Mes ----
|
|
async function loadPeriodClose() {
|
|
const panel = document.getElementById('panel-cierre');
|
|
if (!panel) return;
|
|
|
|
// Wire the "Ejecutar Cierre" button
|
|
const closeBtn = panel.querySelector('.btn--primary');
|
|
if (closeBtn && !closeBtn.dataset.wired) {
|
|
closeBtn.dataset.wired = 'true';
|
|
closeBtn.addEventListener('click', async () => {
|
|
const now = new Date();
|
|
const year = now.getFullYear();
|
|
const month = now.getMonth() + 1;
|
|
if (!confirm(`Cerrar periodo ${month}/${year}? Esta accion no se puede revertir.`)) return;
|
|
try {
|
|
await api('/periods/close', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ year, month }),
|
|
});
|
|
alert('Periodo cerrado exitosamente.');
|
|
} catch (e) {
|
|
alert('Error: ' + e.message);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
// ---- Summary cards update ----
|
|
async function loadSummaryCards() {
|
|
const cards = document.querySelectorAll('.summary-card');
|
|
if (cards.length < 4) return;
|
|
|
|
try {
|
|
// Load trial balance for overall numbers
|
|
const now = new Date();
|
|
const tb = await api(`/trial-balance?year=${now.getFullYear()}&month=${now.getMonth() + 1}`);
|
|
// The summary cards will keep their structure, just update values if API returns data
|
|
// This is best-effort; if API doesn't support summary data, demo values remain
|
|
} catch (_) {
|
|
// Non-critical, keep demo values
|
|
}
|
|
}
|
|
|
|
// ---- Helper: update tab badge counts ----
|
|
function updateBadgeCount(tabName, count) {
|
|
const btn = document.querySelector(`.tab-btn[onclick*="'${tabName}'"]`);
|
|
if (!btn) return;
|
|
const badge = btn.querySelector('.tab-btn__badge');
|
|
if (badge) badge.textContent = count;
|
|
}
|
|
|
|
// ---- Clock ----
|
|
function startClock() {
|
|
const el = document.getElementById('live-clock');
|
|
if (!el) return;
|
|
const tick = () => {
|
|
const now = new Date();
|
|
el.textContent = now.toLocaleTimeString('es-MX', { hour: '2-digit', minute: '2-digit' });
|
|
};
|
|
tick();
|
|
setInterval(tick, 1000);
|
|
}
|
|
|
|
// ---- Init ----
|
|
function init() {
|
|
if (!checkAuth()) return;
|
|
startClock();
|
|
loadSummaryCards();
|
|
// Load initial tab data (cxc is active by default)
|
|
loadAging();
|
|
}
|
|
|
|
document.addEventListener('DOMContentLoaded', init);
|
|
|
|
// ---- Exportar placeholder ----
|
|
function exportarContabilidad() {
|
|
// Find the first visible table in the active accounting tab and export as CSV
|
|
var tables = document.querySelectorAll('table');
|
|
var table = null;
|
|
for (var i = 0; i < tables.length; i++) {
|
|
if (tables[i].offsetParent !== null && tables[i].querySelector('tbody tr')) {
|
|
table = tables[i];
|
|
break;
|
|
}
|
|
}
|
|
if (!table) {
|
|
alert('No hay datos para exportar en la vista actual.');
|
|
return;
|
|
}
|
|
var rows = [];
|
|
var ths = table.querySelectorAll('thead th');
|
|
if (ths.length) {
|
|
rows.push(Array.from(ths).map(function(th) { return '"' + th.textContent.trim().replace(/"/g, '""') + '"'; }).join(','));
|
|
}
|
|
table.querySelectorAll('tbody tr').forEach(function(tr) {
|
|
var cells = tr.querySelectorAll('td');
|
|
rows.push(Array.from(cells).map(function(td) { return '"' + td.textContent.trim().replace(/"/g, '""') + '"'; }).join(','));
|
|
});
|
|
if (rows.length <= 1) { alert('Sin datos para exportar.'); return; }
|
|
var csv = rows.join('\n');
|
|
var blob = new Blob(['\uFEFF' + csv], { type: 'text/csv;charset=utf-8;' });
|
|
var url = URL.createObjectURL(blob);
|
|
var a = document.createElement('a');
|
|
a.href = url;
|
|
a.download = 'contabilidad_nexus_' + new Date().toISOString().slice(0, 10) + '.csv';
|
|
a.click();
|
|
URL.revokeObjectURL(url);
|
|
}
|
|
|
|
// ---- Nueva Poliza modal ----
|
|
function showNewEntryModal() {
|
|
const overlay = document.getElementById('newEntryModalOverlay');
|
|
if (!overlay) return;
|
|
// Set default date to today
|
|
const dateInput = overlay.querySelector('#entryDate');
|
|
if (dateInput && !dateInput.value) {
|
|
dateInput.value = new Date().toISOString().slice(0, 10);
|
|
}
|
|
document.getElementById('entryResult').innerHTML = '';
|
|
overlay.style.display = 'flex';
|
|
}
|
|
|
|
function closeNewEntryModal() {
|
|
const overlay = document.getElementById('newEntryModalOverlay');
|
|
if (overlay) overlay.style.display = 'none';
|
|
}
|
|
|
|
function addEntryLine() {
|
|
const container = document.getElementById('entryLines');
|
|
if (!container) return;
|
|
const line = document.createElement('div');
|
|
line.className = 'entry-line';
|
|
line.style.cssText = 'display:grid;grid-template-columns:2fr 1fr 1fr auto;gap:var(--space-2);margin-bottom:var(--space-2);align-items:center;';
|
|
line.innerHTML =
|
|
'<input type="text" placeholder="Cuenta contable" class="entry-account" style="padding:var(--space-2) var(--space-3);border:1px solid var(--color-border);border-radius:var(--radius-md);background:var(--color-surface-2);color:var(--color-text-primary);font-size:var(--text-body-sm);" />' +
|
|
'<input type="number" placeholder="Debe" class="entry-debit" step="0.01" style="padding:var(--space-2) var(--space-3);border:1px solid var(--color-border);border-radius:var(--radius-md);background:var(--color-surface-2);color:var(--color-text-primary);font-size:var(--text-body-sm);" />' +
|
|
'<input type="number" placeholder="Haber" class="entry-credit" step="0.01" style="padding:var(--space-2) var(--space-3);border:1px solid var(--color-border);border-radius:var(--radius-md);background:var(--color-surface-2);color:var(--color-text-primary);font-size:var(--text-body-sm);" />' +
|
|
'<button class="btn btn--ghost btn--sm" onclick="this.closest(\'.entry-line\').remove()">×</button>';
|
|
container.appendChild(line);
|
|
}
|
|
|
|
async function submitNewEntry() {
|
|
const date = document.getElementById('entryDate').value;
|
|
const type = document.getElementById('entryType').value;
|
|
const description = document.getElementById('entryDescription').value.trim();
|
|
const resultEl = document.getElementById('entryResult');
|
|
|
|
if (!date || !description) {
|
|
resultEl.innerHTML = '<span style="color:var(--color-error);">Fecha y descripcion son obligatorios.</span>';
|
|
return;
|
|
}
|
|
|
|
const lines = [];
|
|
document.querySelectorAll('#entryLines .entry-line').forEach(row => {
|
|
const account = row.querySelector('.entry-account').value.trim();
|
|
const debit = parseFloat(row.querySelector('.entry-debit').value) || 0;
|
|
const credit = parseFloat(row.querySelector('.entry-credit').value) || 0;
|
|
if (account && (debit || credit)) {
|
|
lines.push({ account, debit, credit });
|
|
}
|
|
});
|
|
|
|
if (!lines.length) {
|
|
resultEl.innerHTML = '<span style="color:var(--color-error);">Agregue al menos una partida.</span>';
|
|
return;
|
|
}
|
|
|
|
try {
|
|
await api('/entries', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ date, type, description, lines }),
|
|
});
|
|
resultEl.innerHTML = '<span style="color:var(--color-success);">Poliza creada exitosamente.</span>';
|
|
setTimeout(() => closeNewEntryModal(), 1200);
|
|
} catch (e) {
|
|
resultEl.innerHTML = '<span style="color:var(--color-error);">Error: ' + e.message + '</span>';
|
|
}
|
|
}
|
|
|
|
// Expose switchTab globally for onclick handlers in HTML
|
|
window.switchTab = switchTab;
|
|
window.exportarContabilidad = exportarContabilidad;
|
|
window.showNewEntryModal = showNewEntryModal;
|
|
window.closeNewEntryModal = closeNewEntryModal;
|
|
window.addEntryLine = addEntryLine;
|
|
window.submitNewEntry = submitNewEntry;
|
|
|
|
return {
|
|
switchTab, loadAging, loadAccountsPayable, loadBalanceSheet,
|
|
loadIncomeStatement, loadCashFlow, loadReconciliation, loadPeriodClose,
|
|
exportarContabilidad, showNewEntryModal, closeNewEntryModal, addEntryLine, submitNewEntry,
|
|
};
|
|
})();
|