Merge branch 'main' into desarrollo_hector
This commit is contained in:
@@ -84,13 +84,15 @@ const Dashboard = (() => {
|
||||
});
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Period selector (placeholder for future use)
|
||||
// Period selector
|
||||
// -------------------------------------------------------------------------
|
||||
function setPeriod(btn) {
|
||||
btn.closest('.period-selector').querySelectorAll('.period-btn').forEach(function(b) {
|
||||
b.classList.remove('active');
|
||||
});
|
||||
btn.classList.add('active');
|
||||
const period = btn.textContent.trim().toLowerCase();
|
||||
loadChart(period);
|
||||
}
|
||||
window.setPeriod = setPeriod;
|
||||
|
||||
@@ -205,6 +207,50 @@ const Dashboard = (() => {
|
||||
return data;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// 1b. Historical sales KPIs (imported data)
|
||||
// -------------------------------------------------------------------------
|
||||
async function loadHistoricalSummary() {
|
||||
try {
|
||||
const now = new Date();
|
||||
const firstDay = new Date(now.getFullYear(), now.getMonth(), 1).toISOString().slice(0, 10);
|
||||
const lastDay = new Date(now.getFullYear(), now.getMonth() + 1, 0).toISOString().slice(0, 10);
|
||||
|
||||
// All historical sales
|
||||
const all = await apiFetch('/pos/api/historical-sales?per_page=1');
|
||||
const totalRecords = all.pagination ? all.pagination.total : 0;
|
||||
|
||||
// Current month historical sales
|
||||
const month = await apiFetch(`/pos/api/historical-sales?date_from=${firstDay}&date_to=${lastDay}&per_page=200`);
|
||||
const monthRows = month.data || [];
|
||||
const monthTotal = monthRows.reduce((a, r) => a + (r.total || 0), 0);
|
||||
|
||||
const totalEl = document.getElementById('kpi-historico-total-value');
|
||||
const totalMetaEl = document.getElementById('kpi-historico-total-meta');
|
||||
if (totalEl) totalEl.textContent = fmt(monthTotal);
|
||||
if (totalMetaEl) totalMetaEl.innerHTML = `<span class="kpi-meta-text">${fmtInt(totalRecords)} tickets importados</span>`;
|
||||
|
||||
const mesEl = document.getElementById('kpi-historico-mes-value');
|
||||
const mesMetaEl = document.getElementById('kpi-historico-mes-meta');
|
||||
if (mesEl) mesEl.textContent = fmt(monthTotal);
|
||||
if (mesMetaEl) mesMetaEl.innerHTML = `<span class="kpi-meta-text">${monthRows.length} tickets este mes</span>`;
|
||||
|
||||
const countEl = document.getElementById('kpi-historico-count-value');
|
||||
const countMetaEl = document.getElementById('kpi-historico-count-meta');
|
||||
if (countEl) countEl.textContent = fmtInt(totalRecords);
|
||||
if (countMetaEl) countMetaEl.innerHTML = `<span class="kpi-meta-text">Registros históricos</span>`;
|
||||
|
||||
} catch (err) {
|
||||
console.error('Error loading historical summary:', err);
|
||||
const ids = [
|
||||
['kpi-historico-total-value', 'kpi-historico-total-meta'],
|
||||
['kpi-historico-mes-value', 'kpi-historico-mes-meta'],
|
||||
['kpi-historico-count-value', 'kpi-historico-count-meta'],
|
||||
];
|
||||
ids.forEach(([v, m]) => setKpiError(v, m));
|
||||
}
|
||||
}
|
||||
|
||||
function setKpiError(valueId, metaId) {
|
||||
const v = document.getElementById(valueId);
|
||||
const m = document.getElementById(metaId);
|
||||
@@ -396,39 +442,159 @@ const Dashboard = (() => {
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// 5. Weekly bar chart (last 7 days)
|
||||
// Helpers for chart grouping
|
||||
// -------------------------------------------------------------------------
|
||||
async function loadWeeklyChart() {
|
||||
function isoWeek(date) {
|
||||
const tmp = new Date(date.valueOf());
|
||||
const dayNum = (date.getDay() + 6) % 7;
|
||||
tmp.setDate(tmp.getDate() - dayNum + 3);
|
||||
const firstThursday = tmp.valueOf();
|
||||
tmp.setMonth(0, 1);
|
||||
if (tmp.getDay() !== 4) {
|
||||
tmp.setMonth(0, 1 + ((4 - tmp.getDay()) + 7) % 7);
|
||||
}
|
||||
return 1 + Math.ceil((firstThursday - tmp) / 604800000);
|
||||
}
|
||||
|
||||
function weekLabel(date) {
|
||||
return `Sem ${isoWeek(date)}`;
|
||||
}
|
||||
|
||||
function monthLabel(date) {
|
||||
return MONTH_NAMES[date.getMonth()].slice(0, 3);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// 5. Sales chart (today / week / month / year)
|
||||
// -------------------------------------------------------------------------
|
||||
async function loadChart(period) {
|
||||
const chartEl = document.getElementById('bar-chart');
|
||||
const totalEl = document.getElementById('chart-week-total');
|
||||
const legendEl = document.getElementById('chart-legend');
|
||||
const titleEl = document.querySelector('.chart-header .section-title');
|
||||
if (!chartEl) return;
|
||||
|
||||
// Fetch daily summary for each of last 7 days
|
||||
const days = [];
|
||||
for (let i = 6; i >= 0; i--) {
|
||||
days.push(daysAgo(i));
|
||||
period = period || 'semana';
|
||||
|
||||
let dateFrom, dateTo, labels = [], buckets = {}, labelOrder = [];
|
||||
const now = new Date();
|
||||
|
||||
if (period === 'hoy') {
|
||||
dateFrom = dateTo = todayStr();
|
||||
labelOrder = ['Hoy'];
|
||||
buckets['Hoy'] = 0;
|
||||
} else if (period === 'semana') {
|
||||
const days = [];
|
||||
for (let i = 6; i >= 0; i--) { days.push(daysAgo(i)); }
|
||||
dateFrom = days[0];
|
||||
dateTo = days[6];
|
||||
days.forEach(d => {
|
||||
const date = new Date(d + 'T12:00:00');
|
||||
const label = DAY_NAMES_SHORT[date.getDay()];
|
||||
labelOrder.push(label);
|
||||
buckets[label] = { total: 0, date: d };
|
||||
});
|
||||
} else if (period === 'mes') {
|
||||
const year = now.getFullYear();
|
||||
const month = now.getMonth();
|
||||
const lastDay = new Date(year, month + 1, 0).getDate();
|
||||
dateFrom = new Date(year, month, 1).toISOString().slice(0, 10);
|
||||
dateTo = new Date(year, month, lastDay).toISOString().slice(0, 10);
|
||||
for (let i = 1; i <= 4; i++) {
|
||||
const label = `Sem ${i}`;
|
||||
labelOrder.push(label);
|
||||
buckets[label] = { total: 0, week: i };
|
||||
}
|
||||
} else if (period === 'año') {
|
||||
for (let i = 11; i >= 0; i--) {
|
||||
const d = new Date(now.getFullYear(), now.getMonth() - i, 1);
|
||||
const label = monthLabel(d);
|
||||
labelOrder.push(label);
|
||||
buckets[label] = 0;
|
||||
}
|
||||
dateFrom = new Date(now.getFullYear(), now.getMonth() - 11, 1).toISOString().slice(0, 10);
|
||||
dateTo = todayStr();
|
||||
}
|
||||
|
||||
const summaries = await Promise.all(
|
||||
days.map(d => apiFetch(`/pos/api/register/daily-summary?date=${d}`))
|
||||
);
|
||||
// Fetch normal sales for short periods
|
||||
let normalByKey = {};
|
||||
if (period === 'hoy' || period === 'semana') {
|
||||
const days = period === 'hoy' ? [todayStr()] : (function() {
|
||||
const arr = [];
|
||||
for (let i = 6; i >= 0; i--) arr.push(daysAgo(i));
|
||||
return arr;
|
||||
})();
|
||||
const summaries = await Promise.all(
|
||||
days.map(d => apiFetch(`/pos/api/register/daily-summary?date=${d}`))
|
||||
);
|
||||
days.forEach((d, i) => {
|
||||
const date = new Date(d + 'T12:00:00');
|
||||
const key = period === 'hoy' ? 'Hoy' : DAY_NAMES_SHORT[date.getDay()];
|
||||
normalByKey[key] = summaries[i] ? (summaries[i].total_sales || 0) : 0;
|
||||
});
|
||||
}
|
||||
|
||||
let weekTotal = 0;
|
||||
const dayData = days.map((dateStr, i) => {
|
||||
const s = summaries[i];
|
||||
const total = s ? (s.total_sales || 0) : 0;
|
||||
weekTotal += total;
|
||||
const d = new Date(dateStr + 'T12:00:00');
|
||||
return {
|
||||
label: DAY_NAMES_SHORT[d.getDay()],
|
||||
total: total,
|
||||
isToday: dateStr === todayStr(),
|
||||
};
|
||||
// Fetch historical sales for the range
|
||||
let histRows = [];
|
||||
try {
|
||||
const perPage = period === 'año' ? 2000 : 1000;
|
||||
const histData = await apiFetch(`/pos/api/historical-sales?date_from=${dateFrom}&date_to=${dateTo}&per_page=${perPage}`);
|
||||
histRows = histData.data || [];
|
||||
const totalPages = histData.pagination ? histData.pagination.total_pages : 1;
|
||||
for (let p = 2; p <= totalPages && p <= 20; p++) {
|
||||
const more = await apiFetch(`/pos/api/historical-sales?date_from=${dateFrom}&date_to=${dateTo}&per_page=${perPage}&page=${p}`);
|
||||
histRows = histRows.concat(more.data || []);
|
||||
}
|
||||
} catch (e) {
|
||||
histRows = [];
|
||||
}
|
||||
|
||||
// Group historical sales
|
||||
histRows.forEach(r => {
|
||||
if (!r.sale_date) return;
|
||||
const date = new Date(r.sale_date + 'T12:00:00');
|
||||
let key;
|
||||
if (period === 'hoy') key = 'Hoy';
|
||||
else if (period === 'semana') key = DAY_NAMES_SHORT[date.getDay()];
|
||||
else if (period === 'mes') {
|
||||
const day = date.getDate();
|
||||
const weekNum = day <= 7 ? 1 : day <= 14 ? 2 : day <= 21 ? 3 : 4;
|
||||
key = `Sem ${weekNum}`;
|
||||
} else {
|
||||
key = monthLabel(date);
|
||||
}
|
||||
if (key) {
|
||||
if (typeof buckets[key] === 'object') buckets[key].total += (r.total || 0);
|
||||
else buckets[key] = (buckets[key] || 0) + (r.total || 0);
|
||||
}
|
||||
});
|
||||
|
||||
// Update week total
|
||||
// Build chart data
|
||||
let chartTotal = 0;
|
||||
const dayData = labelOrder.map(label => {
|
||||
let normalTotal = normalByKey[label] || 0;
|
||||
let histTotal = 0;
|
||||
if (typeof buckets[label] === 'object') {
|
||||
histTotal = buckets[label].total;
|
||||
} else {
|
||||
histTotal = buckets[label] || 0;
|
||||
}
|
||||
const total = normalTotal + histTotal;
|
||||
chartTotal += total;
|
||||
const isToday = period === 'hoy' || (typeof buckets[label] === 'object' && buckets[label].date === todayStr());
|
||||
return { label, total, isToday };
|
||||
});
|
||||
|
||||
// Update labels
|
||||
const titles = { hoy: 'Ventas de Hoy', semana: 'Ventas Semanales', mes: 'Ventas del Mes', año: 'Ventas del Año' };
|
||||
const legends = { hoy: 'Total del día', semana: 'Ventas brutas (7 días)', mes: 'Ventas brutas (4 semanas)', año: 'Ventas brutas (12 meses)' };
|
||||
if (titleEl) titleEl.textContent = titles[period] || 'Ventas';
|
||||
if (totalEl) {
|
||||
totalEl.innerHTML = `Total semana: <strong style="color:var(--color-primary);font-family:var(--font-mono);">${fmt(weekTotal)}</strong>`;
|
||||
const periodLabel = period === 'hoy' ? 'Total día' : period === 'semana' ? 'Total semana' : period === 'mes' ? 'Total mes' : 'Total año';
|
||||
totalEl.innerHTML = `${periodLabel}: <strong style="color:var(--color-primary);font-family:var(--font-mono);">${fmt(chartTotal)}</strong>`;
|
||||
}
|
||||
if (legendEl) {
|
||||
legendEl.innerHTML = `<div class="legend-item"><div class="legend-dot"></div>${legends[period]}</div>`;
|
||||
}
|
||||
|
||||
const maxVal = Math.max(...dayData.map(d => d.total), 1);
|
||||
@@ -533,9 +699,10 @@ const Dashboard = (() => {
|
||||
|
||||
// Load all data in parallel
|
||||
loadDailySummary();
|
||||
loadHistoricalSummary();
|
||||
loadAlerts();
|
||||
loadTopProducts();
|
||||
loadWeeklyChart();
|
||||
loadChart('semana');
|
||||
loadRecentSales();
|
||||
|
||||
// Auto-refresh every 2 minutes
|
||||
|
||||
@@ -478,16 +478,140 @@
|
||||
// PURCHASE / ENTRADA (purchaseModal)
|
||||
// =====================================================================
|
||||
|
||||
let purchaseSearchTimeout = null;
|
||||
let purchaseSelectedItem = null;
|
||||
|
||||
function showPurchaseModal() {
|
||||
document.getElementById('purchaseModal').classList.add('is-open');
|
||||
setTimeout(function() {
|
||||
var el = document.getElementById('purchaseItemSearch');
|
||||
if (el) el.focus();
|
||||
}, 100);
|
||||
}
|
||||
function showPurchaseModalForItem(itemId) {
|
||||
document.getElementById('purchaseItemId').value = itemId;
|
||||
// Pre-fill by fetching item details
|
||||
apiFetch(API + '/items?page=1&per_page=1').then(function() {
|
||||
// We just need the item detail; use the existing list or fetch by id
|
||||
apiFetch(API + '/items?page=1&per_page=1').then(function() {});
|
||||
});
|
||||
selectPurchaseItem({id: itemId, name: 'Producto #' + itemId});
|
||||
showPurchaseModal();
|
||||
}
|
||||
function closePurchaseModal() {
|
||||
document.getElementById('purchaseModal').classList.remove('is-open');
|
||||
document.getElementById('purchaseResult').innerHTML = '';
|
||||
clearPurchaseSelection();
|
||||
}
|
||||
|
||||
function clearPurchaseSelection() {
|
||||
purchaseSelectedItem = null;
|
||||
var ids = ['purchaseItemId','purchaseItemSearch','purchaseQty','purchaseCost','purchaseInvoice','purchaseNotes'];
|
||||
ids.forEach(function(id) {
|
||||
var el = document.getElementById(id);
|
||||
if (el) el.value = '';
|
||||
});
|
||||
var results = document.getElementById('purchaseItemResults');
|
||||
if (results) results.style.display = 'none';
|
||||
var selected = document.getElementById('purchaseItemSelected');
|
||||
if (selected) selected.textContent = '';
|
||||
}
|
||||
|
||||
function selectPurchaseItem(item) {
|
||||
purchaseSelectedItem = item;
|
||||
document.getElementById('purchaseItemId').value = item.id;
|
||||
document.getElementById('purchaseItemSearch').value = item.name || item.part_number || item.barcode || ('#' + item.id);
|
||||
document.getElementById('purchaseItemResults').style.display = 'none';
|
||||
document.getElementById('purchaseItemSelected').innerHTML =
|
||||
'<strong>' + esc(item.name || '') + '</strong>' +
|
||||
(item.part_number ? ' · No. parte: ' + esc(item.part_number) : '') +
|
||||
(item.barcode ? ' · Barcode: ' + esc(item.barcode) : '');
|
||||
document.getElementById('purchaseQty').focus();
|
||||
}
|
||||
|
||||
function searchPurchaseItems(query) {
|
||||
var resultsEl = document.getElementById('purchaseItemResults');
|
||||
if (!query || query.length < 2) {
|
||||
resultsEl.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
apiFetch(API + '/items?q=' + encodeURIComponent(query) + '&per_page=10').then(function(res) {
|
||||
var items = (res && res.data) || [];
|
||||
if (!items.length) {
|
||||
resultsEl.innerHTML = '<div style="padding:var(--space-3);color:var(--color-text-muted);font-size:var(--text-caption);">Sin resultados</div>';
|
||||
resultsEl.style.display = 'block';
|
||||
return;
|
||||
}
|
||||
resultsEl.innerHTML = items.map(function(it) {
|
||||
return '<div class="purchase-search-result" style="padding:var(--space-3);cursor:pointer;border-bottom:1px solid var(--color-border);" ' +
|
||||
'data-id="' + it.id + '">' +
|
||||
'<div style="font-weight:var(--font-weight-semibold);">' + esc(it.name) + '</div>' +
|
||||
'<div style="font-size:var(--text-caption);color:var(--color-text-muted);">' +
|
||||
(it.part_number ? 'No. parte: ' + esc(it.part_number) + ' · ' : '') +
|
||||
(it.barcode ? 'Barcode: ' + esc(it.barcode) + ' · ' : '') +
|
||||
'Stock: ' + (it.stock || 0) +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
}).join('');
|
||||
resultsEl.querySelectorAll('.purchase-search-result').forEach(function(row) {
|
||||
row.onclick = function() {
|
||||
var id = parseInt(row.dataset.id);
|
||||
var item = items.find(function(x) { return x.id === id; });
|
||||
if (item) selectPurchaseItem(item);
|
||||
};
|
||||
});
|
||||
resultsEl.style.display = 'block';
|
||||
}).catch(function() {
|
||||
resultsEl.style.display = 'none';
|
||||
});
|
||||
}
|
||||
|
||||
function wirePurchaseSearch() {
|
||||
var input = document.getElementById('purchaseItemSearch');
|
||||
var resultsEl = document.getElementById('purchaseItemResults');
|
||||
if (!input) return;
|
||||
|
||||
input.addEventListener('input', function() {
|
||||
if (purchaseSelectedItem && input.value !== purchaseSelectedItem.name) {
|
||||
purchaseSelectedItem = null;
|
||||
document.getElementById('purchaseItemId').value = '';
|
||||
document.getElementById('purchaseItemSelected').textContent = '';
|
||||
}
|
||||
clearTimeout(purchaseSearchTimeout);
|
||||
purchaseSearchTimeout = setTimeout(function() {
|
||||
searchPurchaseItems(input.value.trim());
|
||||
}, 250);
|
||||
});
|
||||
|
||||
input.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
// Try exact barcode match first
|
||||
var query = input.value.trim();
|
||||
if (!query) return;
|
||||
apiFetch(API + '/items?q=' + encodeURIComponent(query) + '&per_page=20').then(function(res) {
|
||||
var items = (res && res.data) || [];
|
||||
var exact = items.find(function(it) {
|
||||
return (it.barcode || '').toLowerCase() === query.toLowerCase() ||
|
||||
(it.part_number || '').toLowerCase() === query.toLowerCase();
|
||||
});
|
||||
if (exact) {
|
||||
selectPurchaseItem(exact);
|
||||
} else if (items.length === 1) {
|
||||
selectPurchaseItem(items[0]);
|
||||
} else {
|
||||
searchPurchaseItems(query);
|
||||
}
|
||||
});
|
||||
} else if (e.key === 'Escape') {
|
||||
if (resultsEl) resultsEl.style.display = 'none';
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('click', function(e) {
|
||||
if (resultsEl && !input.contains(e.target) && !resultsEl.contains(e.target)) {
|
||||
resultsEl.style.display = 'none';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function recordPurchase() {
|
||||
@@ -506,10 +630,6 @@
|
||||
if (result && result.operation_id) {
|
||||
document.getElementById('purchaseResult').innerHTML = '<span style="color:var(--color-success);">Compra registrada (op #' + result.operation_id + ')</span>';
|
||||
closePurchaseModal();
|
||||
['purchaseItemId','purchaseQty','purchaseCost','purchaseInvoice','purchaseNotes'].forEach(function(id) {
|
||||
var el = document.getElementById(id);
|
||||
if (el) el.value = '';
|
||||
});
|
||||
if (window.loadInventoryStats) window.loadInventoryStats();
|
||||
loadItems(currentPage);
|
||||
} else {
|
||||
@@ -2009,4 +2129,5 @@
|
||||
|
||||
loadItems(1);
|
||||
renderSavedFilters();
|
||||
wirePurchaseSearch();
|
||||
})();
|
||||
|
||||
@@ -62,6 +62,7 @@ const Invoicing = (() => {
|
||||
if (name === 'notas') loadNotas();
|
||||
if (name === 'complementos') loadComplementos();
|
||||
if (name === 'cancelaciones') loadCancelaciones();
|
||||
if (name === 'config') loadFacturapiStatus();
|
||||
}
|
||||
|
||||
// ---- Badge helpers ----
|
||||
@@ -259,6 +260,75 @@ const Invoicing = (() => {
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Facturapi status (config tab) ----
|
||||
async function loadFacturapiStatus() {
|
||||
const container = document.getElementById('facturapi-status');
|
||||
if (!container) return;
|
||||
container.innerHTML = 'Cargando...';
|
||||
try {
|
||||
const status = await api('/facturapi/status');
|
||||
|
||||
if (!status.has_key) {
|
||||
container.innerHTML = `<p style="color:var(--color-error);">Falta la llave API de Facturapi. Configura <code>cfdi_facturapi_key</code> o la variable <code>FACTURAPI_USER_KEY</code>.</p>`;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!status.has_org_id) {
|
||||
container.innerHTML = `
|
||||
<p style="color:var(--color-warning);margin-bottom:var(--space-3);">No hay organización Facturapi vinculada.</p>
|
||||
<button class="btn btn--primary" onclick="Invoicing.setupFacturapi(this)">Crear / Vincular Organización</button>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
let csdHtml = status.has_csd
|
||||
? '<span style="color:var(--color-success);">Activo</span>'
|
||||
: '<span style="color:var(--color-error);">Pendiente</span>';
|
||||
|
||||
let pendingHtml = '';
|
||||
if (status.pending_steps && status.pending_steps.length) {
|
||||
pendingHtml = '<ul style="margin:var(--space-2) 0 0 0;padding-left:var(--space-5);color:var(--color-warning);">' +
|
||||
status.pending_steps.map(s => `<li>${s.description || s.type}</li>`).join('') +
|
||||
'</ul>';
|
||||
}
|
||||
|
||||
container.innerHTML = `
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:var(--space-4);">
|
||||
<div>
|
||||
<div style="font-size:var(--text-caption);color:var(--color-text-muted);">Organización</div>
|
||||
<div style="font-weight:var(--font-weight-semibold);">${escapeHtml(status.legal_name) || '-'}</div>
|
||||
<div style="font-family:var(--font-mono);font-size:var(--text-body-sm);">${status.tax_id || ''}</div>
|
||||
<div style="font-family:var(--font-mono);font-size:var(--text-caption);color:var(--color-text-muted);">${status.org_id || ''}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style="font-size:var(--text-caption);color:var(--color-text-muted);">CSD</div>
|
||||
<div style="font-weight:var(--font-weight-semibold);">${csdHtml}</div>
|
||||
<div style="font-size:var(--text-caption);color:var(--color-text-muted);margin-top:var(--space-2);">Pasos pendientes</div>
|
||||
${pendingHtml || '<span style="color:var(--color-success);">Ninguno</span>'}
|
||||
</div>
|
||||
</div>
|
||||
${status.error ? `<p style="color:var(--color-error);margin-top:var(--space-3);">Error: ${escapeHtml(status.error)}</p>` : ''}
|
||||
`;
|
||||
} catch (e) {
|
||||
container.innerHTML = `<p style="color:var(--color-error);">Error: ${e.message}</p>`;
|
||||
}
|
||||
}
|
||||
|
||||
async function setupFacturapi(btn) {
|
||||
if (!btn) return;
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'Configurando...';
|
||||
try {
|
||||
const res = await api('/facturapi/setup', { method: 'POST' });
|
||||
alert('Organización vinculada: ' + res.org_id);
|
||||
loadFacturapiStatus();
|
||||
} catch (e) {
|
||||
alert('Error: ' + e.message);
|
||||
btn.disabled = false;
|
||||
btn.textContent = 'Crear / Vincular Organización';
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Detail modal (uses modalDetalleOverlay) ----
|
||||
async function showDetail(cfdiId) {
|
||||
const overlay = document.getElementById('modalDetalleOverlay');
|
||||
@@ -300,10 +370,17 @@ const Invoicing = (() => {
|
||||
</div>
|
||||
</div>
|
||||
${item.error_message ? `<p style="color:var(--color-error);margin-bottom:var(--space-3);"><strong>Error:</strong> ${escapeHtml(item.error_message)}</p>` : ''}
|
||||
${(item.xml_signed || item.xml_unsigned) ? `
|
||||
<div style="font-size:var(--text-caption); color:var(--color-text-muted); text-transform:uppercase; letter-spacing:var(--tracking-widest); margin-bottom:var(--space-2);">Vista previa XML</div>
|
||||
<pre style="background:var(--color-surface-3); border:1px solid var(--color-border); border-radius:var(--radius-md); padding:var(--space-4); font-family:var(--font-mono); font-size:11px; color:var(--color-text-secondary); overflow-x:auto; max-height:200px; line-height:1.6;">${escapeHtml(item.xml_signed || item.xml_unsigned)}</pre>
|
||||
` : ''}`;
|
||||
${(item.xml_signed || item.payload_unsigned) ? `
|
||||
<div style="font-size:var(--text-caption); color:var(--color-text-muted); text-transform:uppercase; letter-spacing:var(--tracking-widest); margin-bottom:var(--space-2);">${item.xml_signed ? 'Vista previa XML' : 'Payload Facturapi'}</div>
|
||||
<pre style="background:var(--color-surface-3); border:1px solid var(--color-border); border-radius:var(--radius-md); padding:var(--space-4); font-family:var(--font-mono); font-size:11px; color:var(--color-text-secondary); overflow-x:auto; max-height:200px; line-height:1.6;">${escapeHtml(item.xml_signed || item.payload_unsigned)}</pre>
|
||||
` : ''}
|
||||
${item.status === 'stamped' && item.external_id ? `
|
||||
<div style="display:flex;gap:var(--space-3);margin-top:var(--space-4);">
|
||||
<a class="btn btn--ghost btn--sm" href="${API}/facturapi/download/${item.id}/xml" target="_blank">Descargar XML</a>
|
||||
<a class="btn btn--ghost btn--sm" href="${API}/facturapi/download/${item.id}/pdf" target="_blank">Descargar PDF</a>
|
||||
</div>
|
||||
` : ''}
|
||||
`;
|
||||
}
|
||||
|
||||
// Wire the cancel button inside modal footer
|
||||
@@ -531,10 +608,10 @@ const Invoicing = (() => {
|
||||
window.notaCreditoPlaceholder = notaCreditoPlaceholder;
|
||||
|
||||
return {
|
||||
switchTab, loadFacturas, loadNotas, loadComplementos, loadCancelaciones,
|
||||
switchTab, loadFacturas, loadNotas, loadComplementos, loadCancelaciones, loadFacturapiStatus,
|
||||
showDetail, showCancelModal, confirmCancel, processQueue,
|
||||
showNewInvoiceModal, closeNewInvoiceModal, submitNewInvoice, notaCreditoPlaceholder,
|
||||
openGlobalInvoiceModal, previewGlobalInvoice, generateGlobalInvoice,
|
||||
openGlobalInvoiceModal, previewGlobalInvoice, generateGlobalInvoice, setupFacturapi,
|
||||
};
|
||||
// Register Cmd+K items
|
||||
if (typeof registerCmdKItem === "function") {
|
||||
|
||||
@@ -54,7 +54,7 @@ const Reports = (() => {
|
||||
}
|
||||
|
||||
// Track which tabs have been loaded
|
||||
var loaded = { ventas: false, inventario: false, clientes: false, financieros: false };
|
||||
var loaded = { ventas: false, inventario: false, clientes: false, financieros: false, historico: false };
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Theme switcher
|
||||
@@ -85,6 +85,7 @@ const Reports = (() => {
|
||||
else if (id === 'inventario') loadInventario();
|
||||
else if (id === 'clientes') loadClientes();
|
||||
else if (id === 'financieros') loadFinancieros();
|
||||
else if (id === 'historico') loadHistorico();
|
||||
}
|
||||
}
|
||||
window.switchTab = switchTab;
|
||||
@@ -289,6 +290,85 @@ const Reports = (() => {
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// TAB 5: HISTÓRICO
|
||||
// =========================================================================
|
||||
async function loadHistorico() {
|
||||
loaded.historico = true;
|
||||
var dateFrom = document.getElementById('historico-date-from').value;
|
||||
var dateTo = document.getElementById('historico-date-to').value;
|
||||
var customer = document.getElementById('historico-customer').value.trim();
|
||||
|
||||
var params = new URLSearchParams();
|
||||
if (dateFrom) params.set('date_from', dateFrom);
|
||||
if (dateTo) params.set('date_to', dateTo);
|
||||
if (customer) params.set('customer', customer);
|
||||
params.set('per_page', '200');
|
||||
|
||||
var kpiEl = document.getElementById('historico-kpis');
|
||||
var detalleEl = document.getElementById('historico-detalle');
|
||||
kpiEl.innerHTML = spinner();
|
||||
detalleEl.innerHTML = spinner();
|
||||
|
||||
try {
|
||||
var allRows = [];
|
||||
var page = 1;
|
||||
var totalPages = 1;
|
||||
|
||||
while (page <= totalPages) {
|
||||
params.set('page', page);
|
||||
var json = await apiFetch('/pos/api/historical-sales?' + params.toString());
|
||||
allRows = allRows.concat(json.data || []);
|
||||
totalPages = json.pagination ? json.pagination.total_pages : 1;
|
||||
page++;
|
||||
if (page > 50) break;
|
||||
}
|
||||
|
||||
var total = allRows.reduce(function(a, r) { return a + r.total; }, 0);
|
||||
var subtotal = allRows.reduce(function(a, r) { return a + r.subtotal; }, 0);
|
||||
var balance = allRows.reduce(function(a, r) { return a + r.balance; }, 0);
|
||||
|
||||
kpiEl.innerHTML =
|
||||
kpiCard('Total Histórico', '$' + fmt(total), allRows.length + ' registros') +
|
||||
kpiCard('Subtotal', '$' + fmt(subtotal), '') +
|
||||
kpiCard('Saldo Pendiente', '$' + fmt(balance), '') +
|
||||
kpiCard('Tickets', fmtInt(allRows.length), '');
|
||||
|
||||
var html = '<div class="table-card__header"><span class="table-card__title">Ventas Históricas Importadas</span>' +
|
||||
'<span class="pill pill--muted">' + allRows.length + ' registros</span></div>';
|
||||
html += '<div class="table-wrap"><table class="data-table"><thead><tr>' +
|
||||
'<th>Fecha</th><th>Documento</th><th>Cliente</th><th>Pago</th>' +
|
||||
'<th class="align-right">Subtotal</th><th class="align-right">Total</th>' +
|
||||
'<th class="align-right">Pagado</th><th class="align-right">Saldo</th>' +
|
||||
'</tr></thead><tbody>';
|
||||
allRows.slice(0, 200).forEach(function(r) {
|
||||
html += '<tr>' +
|
||||
'<td>' + fmtDate(r.sale_date) + '</td>' +
|
||||
'<td class="td-mono">' + esc(r.document_no || r.external_document_id || '--') + '</td>' +
|
||||
'<td>' + esc(r.customer_name || '--') + '</td>' +
|
||||
'<td><span class="pill pill--muted">' + esc(r.payment_method || '--') + '</span></td>' +
|
||||
'<td class="align-right td-mono">$' + fmt(r.subtotal) + '</td>' +
|
||||
'<td class="align-right td-mono-accent">$' + fmt(r.total) + '</td>' +
|
||||
'<td class="align-right td-mono">$' + fmt(r.amount_paid) + '</td>' +
|
||||
'<td class="align-right td-mono">$' + fmt(r.balance) + '</td>' +
|
||||
'</tr>';
|
||||
});
|
||||
html += '</tbody></table></div>';
|
||||
detalleEl.innerHTML = html;
|
||||
|
||||
} catch (err) {
|
||||
kpiEl.innerHTML = errorMsg('Error cargando histórico: ' + err.message);
|
||||
detalleEl.innerHTML = '';
|
||||
}
|
||||
}
|
||||
|
||||
function esc(s) {
|
||||
if (s == null) return '';
|
||||
return String(s).replace(/[&<>"']/g, function(c) {
|
||||
return { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c];
|
||||
});
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// TAB 2: INVENTARIO
|
||||
// =========================================================================
|
||||
@@ -712,7 +792,7 @@ const Reports = (() => {
|
||||
|
||||
return {
|
||||
init, setTheme, switchTab,
|
||||
loadVentas, loadInventario, loadClientes, loadFinancieros, fmt
|
||||
loadVentas, loadInventario, loadClientes, loadFinancieros, loadHistorico, fmt
|
||||
};
|
||||
// Register Cmd+K items
|
||||
if (typeof registerCmdKItem === "function") {
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
// The fetch handler normalizes static asset URLs (strips ?v= query strings)
|
||||
// so templates can use cache-busting query params freely.
|
||||
|
||||
const CACHE_NAME = 'nexus-pos-v17';
|
||||
const CACHE_NAME = 'nexus-pos-v18';
|
||||
|
||||
const APP_SHELL = [
|
||||
'/pos/static/css/tokens.css',
|
||||
|
||||
Reference in New Issue
Block a user