feat(pos): dashboard funcional con datos reales — ventas, alertas, cajas
Replace all hardcoded/demo data with live API calls: - KPIs from /pos/api/register/daily-summary (ventas, tickets, promedio, cajas) - Inventory alerts from /pos/api/inventory/alerts (zero, low, over stock) - Top products aggregated from /pos/api/sales detail items - Weekly bar chart from 7-day daily summaries - Recent sales table with real items from sale detail endpoint - Auto-refresh every 2 minutes, manual refresh button - Dynamic greeting with time-of-day and JWT user name Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
// /home/Autopartes/pos/static/js/dashboard.js
|
||||
// Dashboard module: KPIs, charts, summary data
|
||||
// Dashboard module: fetches real data from POS APIs
|
||||
|
||||
const Dashboard = (() => {
|
||||
function token() {
|
||||
@@ -19,9 +19,30 @@ const Dashboard = (() => {
|
||||
}
|
||||
|
||||
function fmt(n) {
|
||||
return parseFloat(n || 0).toLocaleString('es-MX', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
||||
return '$' + parseFloat(n || 0).toLocaleString('es-MX', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
||||
}
|
||||
|
||||
function fmtInt(n) {
|
||||
return parseInt(n || 0).toLocaleString('es-MX');
|
||||
}
|
||||
|
||||
/** Today as YYYY-MM-DD */
|
||||
function todayStr() {
|
||||
return new Date().toISOString().slice(0, 10);
|
||||
}
|
||||
|
||||
/** Return a date N days ago as YYYY-MM-DD */
|
||||
function daysAgo(n) {
|
||||
const d = new Date();
|
||||
d.setDate(d.getDate() - n);
|
||||
return d.toISOString().slice(0, 10);
|
||||
}
|
||||
|
||||
const DAY_NAMES_SHORT = ['Dom', 'Lun', 'Mar', 'Mie', 'Jue', 'Vie', 'Sab'];
|
||||
const MONTH_NAMES = ['enero', 'febrero', 'marzo', 'abril', 'mayo', 'junio',
|
||||
'julio', 'agosto', 'septiembre', 'octubre', 'noviembre', 'diciembre'];
|
||||
const DAY_NAMES_LONG = ['Domingo', 'Lunes', 'Martes', 'Miercoles', 'Jueves', 'Viernes', 'Sabado'];
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Theme switcher
|
||||
// -------------------------------------------------------------------------
|
||||
@@ -63,7 +84,7 @@ const Dashboard = (() => {
|
||||
});
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Period selector
|
||||
// Period selector (placeholder for future use)
|
||||
// -------------------------------------------------------------------------
|
||||
function setPeriod(btn) {
|
||||
btn.closest('.period-selector').querySelectorAll('.period-btn').forEach(function(b) {
|
||||
@@ -74,18 +95,405 @@ const Dashboard = (() => {
|
||||
window.setPeriod = setPeriod;
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Placeholder API calls
|
||||
// Generic fetch helper
|
||||
// -------------------------------------------------------------------------
|
||||
async function loadSalesSummary() {
|
||||
// TODO: call /pos/api/cashregister/summary or similar
|
||||
async function apiFetch(url) {
|
||||
try {
|
||||
const resp = await fetch(url, { headers: headers() });
|
||||
if (resp.status === 401) {
|
||||
window.location.href = '/pos/login';
|
||||
return null;
|
||||
}
|
||||
if (!resp.ok) return null;
|
||||
return await resp.json();
|
||||
} catch (err) {
|
||||
console.error('Dashboard fetch error:', url, err);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Set header greeting with real date and user info
|
||||
// -------------------------------------------------------------------------
|
||||
function setGreeting() {
|
||||
const now = new Date();
|
||||
const h = now.getHours();
|
||||
let saludo = 'Buenos dias';
|
||||
if (h >= 12 && h < 19) saludo = 'Buenas tardes';
|
||||
else if (h >= 19) saludo = 'Buenas noches';
|
||||
|
||||
// Try to get user name from JWT payload
|
||||
let userName = '';
|
||||
try {
|
||||
const payload = JSON.parse(atob(token().split('.')[1]));
|
||||
userName = payload.name || payload.sub || '';
|
||||
} catch(e) {}
|
||||
|
||||
const greetEl = document.getElementById('header-greeting');
|
||||
if (greetEl) greetEl.textContent = userName ? `${saludo}, ${userName}` : saludo;
|
||||
|
||||
const subEl = document.getElementById('header-subtitle');
|
||||
if (subEl) {
|
||||
const dayName = DAY_NAMES_LONG[now.getDay()];
|
||||
const day = now.getDate();
|
||||
const month = MONTH_NAMES[now.getMonth()];
|
||||
const year = now.getFullYear();
|
||||
subEl.textContent = `${dayName}, ${day} de ${month} de ${year}`;
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// 1. Ventas de hoy KPIs (daily summary)
|
||||
// -------------------------------------------------------------------------
|
||||
async function loadDailySummary() {
|
||||
const today = todayStr();
|
||||
const data = await apiFetch(`/pos/api/register/daily-summary?date=${today}`);
|
||||
if (!data) {
|
||||
setKpiError('kpi-ventas-total', 'kpi-ventas-meta');
|
||||
setKpiError('kpi-tickets-count', 'kpi-tickets-meta');
|
||||
setKpiError('kpi-promedio-value', 'kpi-promedio-meta');
|
||||
return null;
|
||||
}
|
||||
|
||||
// Ventas Hoy
|
||||
const totalEl = document.getElementById('kpi-ventas-total');
|
||||
const metaEl = document.getElementById('kpi-ventas-meta');
|
||||
if (totalEl) totalEl.textContent = fmt(data.total_sales);
|
||||
if (metaEl) {
|
||||
const methods = Object.entries(data.sales_by_method || {})
|
||||
.map(([m, v]) => `${capitalize(m)}: ${fmt(v.amount)}`)
|
||||
.join(' | ');
|
||||
metaEl.innerHTML = methods
|
||||
? `<span class="kpi-meta-text">${methods}</span>`
|
||||
: '<span class="kpi-meta-text">Sin ventas aun</span>';
|
||||
}
|
||||
|
||||
// Tickets Hoy
|
||||
const ticketsEl = document.getElementById('kpi-tickets-count');
|
||||
const ticketsMetaEl = document.getElementById('kpi-tickets-meta');
|
||||
if (ticketsEl) ticketsEl.textContent = fmtInt(data.total_sales_count);
|
||||
if (ticketsMetaEl) {
|
||||
const cancelled = data.cancelled_count || 0;
|
||||
ticketsMetaEl.innerHTML = cancelled > 0
|
||||
? `<span class="kpi-tag kpi-tag--neutral">${cancelled} cancelada${cancelled > 1 ? 's' : ''}</span><span class="kpi-meta-text">de ${data.total_sales_count + cancelled} totales</span>`
|
||||
: `<span class="kpi-meta-text">${data.total_sales_count} completada${data.total_sales_count !== 1 ? 's' : ''}</span>`;
|
||||
}
|
||||
|
||||
// Ticket Promedio
|
||||
const promedioEl = document.getElementById('kpi-promedio-value');
|
||||
const promedioMetaEl = document.getElementById('kpi-promedio-meta');
|
||||
const avg = data.total_sales_count > 0 ? data.total_sales / data.total_sales_count : 0;
|
||||
if (promedioEl) promedioEl.textContent = fmt(avg);
|
||||
if (promedioMetaEl) {
|
||||
promedioMetaEl.innerHTML = `<span class="kpi-meta-text">sobre ${fmtInt(data.total_sales_count)} ventas</span>`;
|
||||
}
|
||||
|
||||
// Cajas del Día KPI
|
||||
const cajasCountEl = document.getElementById('kpi-cajas-count');
|
||||
const cajasMetaEl = document.getElementById('kpi-cajas-meta');
|
||||
const regs = data.registers || [];
|
||||
const activeRegs = regs.filter(r => r.status === 'open');
|
||||
const closedRegs = regs.filter(r => r.status === 'closed');
|
||||
if (cajasCountEl) cajasCountEl.textContent = regs.length;
|
||||
if (cajasMetaEl) {
|
||||
cajasMetaEl.innerHTML = `<span class="kpi-tag kpi-tag--up">${activeRegs.length} abierta${activeRegs.length !== 1 ? 's' : ''}</span><span class="kpi-meta-text">${closedRegs.length} cerrada${closedRegs.length !== 1 ? 's' : ''}</span>`;
|
||||
}
|
||||
|
||||
// Also populate registers list
|
||||
renderRegisters(regs);
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
function setKpiError(valueId, metaId) {
|
||||
const v = document.getElementById(valueId);
|
||||
const m = document.getElementById(metaId);
|
||||
if (v) v.textContent = '--';
|
||||
if (m) m.innerHTML = '<span class="kpi-meta-text" style="color:var(--color-error)">Error al cargar</span>';
|
||||
}
|
||||
|
||||
function capitalize(s) {
|
||||
if (!s) return '';
|
||||
return s.charAt(0).toUpperCase() + s.slice(1);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// 2. Registers list
|
||||
// -------------------------------------------------------------------------
|
||||
function renderRegisters(registers) {
|
||||
const container = document.getElementById('registers-list');
|
||||
if (!container) return;
|
||||
|
||||
if (!registers || registers.length === 0) {
|
||||
container.innerHTML = '<div class="rank-item" style="justify-content:center;color:var(--color-text-muted);font-size:var(--text-caption);">Sin cajas registradas hoy</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
const maxSale = Math.max(...registers.map(r => r.sale_total || 0), 1);
|
||||
container.innerHTML = registers.map((r, i) => {
|
||||
const statusClass = r.status === 'open' ? 'kpi-tag--up' : 'kpi-tag--neutral';
|
||||
const statusLabel = r.status === 'open' ? 'Abierta' : 'Cerrada';
|
||||
const pct = maxSale > 0 ? Math.round((r.sale_total / maxSale) * 100) : 0;
|
||||
return `
|
||||
<div class="rank-item">
|
||||
<div class="rank-num ${i === 0 ? 'rank-num--1' : i === 1 ? 'rank-num--2' : ''}">${i + 1}</div>
|
||||
<div class="rank-item__info">
|
||||
<div class="rank-item__name">Caja ${r.register_number || r.id}</div>
|
||||
<div class="rank-item__sub">${r.employee_name || 'Sin cajero'} · ${r.sale_count || 0} ventas <span class="kpi-tag ${statusClass}" style="font-size:9px;padding:1px 6px;">${statusLabel}</span></div>
|
||||
<div class="rank-item__bar-bg">
|
||||
<div class="rank-item__bar-fill" style="width:${pct}%"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="rank-item__value">${fmt(r.sale_total)}</div>
|
||||
</div>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// 3. Inventory alerts
|
||||
// -------------------------------------------------------------------------
|
||||
async function loadAlerts() {
|
||||
const data = await apiFetch('/pos/api/inventory/alerts');
|
||||
const container = document.getElementById('alerts-grid');
|
||||
if (!container) return;
|
||||
|
||||
if (!data || !data.data || data.data.length === 0) {
|
||||
container.innerHTML = `
|
||||
<div class="alert-item" style="border-left:4px solid var(--color-success);justify-content:center;">
|
||||
<div class="alert-icon-wrap" style="background-color:rgba(34,197,94,0.1);color:var(--color-success);">
|
||||
<svg width="18" height="18" viewBox="0 0 18 18" fill="none"><circle cx="9" cy="9" r="7" stroke="currentColor" stroke-width="1.5"/><path d="M6 9l2 2 4-4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
||||
</div>
|
||||
<div class="alert-content">
|
||||
<span class="alert-title">Sin alertas</span>
|
||||
<div class="alert-desc">Todo el inventario dentro de parametros normales.</div>
|
||||
</div>
|
||||
</div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
const alerts = data.data;
|
||||
// Group by type
|
||||
const zero = alerts.filter(a => a.type === 'zero');
|
||||
const low = alerts.filter(a => a.type === 'low');
|
||||
const over = alerts.filter(a => a.type === 'over');
|
||||
|
||||
let html = '';
|
||||
|
||||
if (zero.length > 0) {
|
||||
const items = zero.slice(0, 5).map(a => `${a.name}: <strong>${a.stock} uds</strong>`).join(' · ');
|
||||
html += buildAlertCard('error', 'Stock Agotado', `${zero.length} items`, items,
|
||||
'<svg width="18" height="18" viewBox="0 0 18 18" fill="none"><path d="M9 3L16.5 15H1.5L9 3z" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/><path d="M9 8v3M9 12.5v.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>');
|
||||
}
|
||||
|
||||
if (low.length > 0) {
|
||||
const items = low.slice(0, 5).map(a => `${a.name}: <strong>${a.stock} uds</strong> (min ${a.min_stock})`).join(' · ');
|
||||
html += buildAlertCard('warning', 'Stock Bajo', `${low.length} items`, items,
|
||||
'<svg width="18" height="18" viewBox="0 0 18 18" fill="none"><path d="M9 3L16.5 15H1.5L9 3z" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/><path d="M9 8v3M9 12.5v.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>');
|
||||
}
|
||||
|
||||
if (over.length > 0) {
|
||||
const items = over.slice(0, 5).map(a => `${a.name}: <strong>${a.stock} uds</strong> (max ${a.max_stock})`).join(' · ');
|
||||
html += buildAlertCard('orange', 'Sobrestock', `${over.length} items`, items,
|
||||
'<svg width="18" height="18" viewBox="0 0 18 18" fill="none"><rect x="2" y="2" width="14" height="14" rx="2" stroke="currentColor" stroke-width="1.5"/><path d="M9 6v3M9 11v.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>');
|
||||
}
|
||||
|
||||
container.innerHTML = html || '<div class="alert-item" style="border-left:none;justify-content:center;color:var(--color-text-muted);">Sin alertas</div>';
|
||||
}
|
||||
|
||||
function buildAlertCard(severity, title, badge, desc, iconSvg) {
|
||||
return `
|
||||
<div class="alert-item alert-item--${severity}">
|
||||
<div class="alert-icon-wrap alert-icon-wrap--${severity}">${iconSvg}</div>
|
||||
<div class="alert-content">
|
||||
<div style="display:flex;align-items:center;gap:var(--space-2);margin-bottom:3px;">
|
||||
<span class="alert-title">${title}</span>
|
||||
<span class="alert-badge alert-badge--${severity}">${badge}</span>
|
||||
</div>
|
||||
<div class="alert-desc">${desc}</div>
|
||||
</div>
|
||||
<a href="/pos/inventory#alertas" class="alert-action" style="text-decoration:none;">Ver</a>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// 4. Top Products (from today's sales detail)
|
||||
// -------------------------------------------------------------------------
|
||||
async function loadTopProducts() {
|
||||
// TODO: call /pos/api/inventory/products?sort=sold
|
||||
const today = todayStr();
|
||||
// Fetch all today's sales with pagination
|
||||
const data = await apiFetch(`/pos/api/sales?date_from=${today}&date_to=${today}&status=completed&per_page=200`);
|
||||
const container = document.getElementById('top-products-list');
|
||||
if (!container) return;
|
||||
|
||||
if (!data || !data.data || data.data.length === 0) {
|
||||
container.innerHTML = '<div class="rank-item" style="justify-content:center;color:var(--color-text-muted);font-size:var(--text-caption);">Sin ventas hoy</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
// Fetch detail for each sale to get items (up to 20 sales for performance)
|
||||
const salesToFetch = data.data.slice(0, 20);
|
||||
const details = await Promise.all(
|
||||
salesToFetch.map(s => apiFetch(`/pos/api/sales/${s.id}`))
|
||||
);
|
||||
|
||||
// Aggregate items
|
||||
const productMap = {};
|
||||
for (const sale of details) {
|
||||
if (!sale || !sale.items) continue;
|
||||
for (const item of sale.items) {
|
||||
const key = item.part_number || item.name;
|
||||
if (!productMap[key]) {
|
||||
productMap[key] = { name: item.name, part_number: item.part_number || '', qty: 0, revenue: 0 };
|
||||
}
|
||||
productMap[key].qty += item.quantity || 0;
|
||||
productMap[key].revenue += item.subtotal || 0;
|
||||
}
|
||||
}
|
||||
|
||||
const sorted = Object.values(productMap).sort((a, b) => b.revenue - a.revenue).slice(0, 5);
|
||||
|
||||
if (sorted.length === 0) {
|
||||
container.innerHTML = '<div class="rank-item" style="justify-content:center;color:var(--color-text-muted);font-size:var(--text-caption);">Sin productos vendidos</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
const maxRev = sorted[0].revenue || 1;
|
||||
container.innerHTML = sorted.map((p, i) => {
|
||||
const pct = Math.round((p.revenue / maxRev) * 100);
|
||||
return `
|
||||
<div class="rank-item">
|
||||
<div class="rank-num ${i === 0 ? 'rank-num--1' : i === 1 ? 'rank-num--2' : ''}">${i + 1}</div>
|
||||
<div class="rank-item__info">
|
||||
<div class="rank-item__name">${escHtml(p.name)}</div>
|
||||
<div class="rank-item__sub">${escHtml(p.part_number)} · ${p.qty} pzas vendidas</div>
|
||||
<div class="rank-item__bar-bg">
|
||||
<div class="rank-item__bar-fill" style="width:${pct}%"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="rank-item__value">${fmt(p.revenue)}</div>
|
||||
</div>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function escHtml(s) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = s || '';
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// 5. Weekly bar chart (last 7 days)
|
||||
// -------------------------------------------------------------------------
|
||||
async function loadWeeklyChart() {
|
||||
const chartEl = document.getElementById('bar-chart');
|
||||
const totalEl = document.getElementById('chart-week-total');
|
||||
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));
|
||||
}
|
||||
|
||||
const summaries = await Promise.all(
|
||||
days.map(d => apiFetch(`/pos/api/register/daily-summary?date=${d}`))
|
||||
);
|
||||
|
||||
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(),
|
||||
};
|
||||
});
|
||||
|
||||
// Update week total
|
||||
if (totalEl) {
|
||||
totalEl.innerHTML = `Total semana: <strong style="color:var(--color-primary);font-family:var(--font-mono);">${fmt(weekTotal)}</strong>`;
|
||||
}
|
||||
|
||||
const maxVal = Math.max(...dayData.map(d => d.total), 1);
|
||||
|
||||
chartEl.innerHTML = dayData.map(d => {
|
||||
const heightPct = Math.max(Math.round((d.total / maxVal) * 100), 2);
|
||||
const todayClass = d.isToday ? ' today' : '';
|
||||
const todayLabelStyle = d.isToday ? ' style="color:var(--color-primary);font-weight:700;"' : '';
|
||||
const tooltip = d.isToday ? `${fmt(d.total)} ← Hoy` : fmt(d.total);
|
||||
return `
|
||||
<div class="bar-chart__col">
|
||||
<div class="bar-chart__bar-wrap">
|
||||
<div class="bar-chart__bar${todayClass}" style="height:${heightPct}%" data-value="${tooltip}"></div>
|
||||
</div>
|
||||
<span class="bar-chart__label"${todayLabelStyle}>${d.label}</span>
|
||||
</div>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// 6. Recent sales table
|
||||
// -------------------------------------------------------------------------
|
||||
async function loadRecentSales() {
|
||||
// TODO: call /pos/api/cashregister/recent
|
||||
const today = todayStr();
|
||||
const data = await apiFetch(`/pos/api/sales?date_from=${today}&date_to=${today}&per_page=10`);
|
||||
const tbody = document.getElementById('recent-sales-tbody');
|
||||
if (!tbody) return;
|
||||
|
||||
if (!data || !data.data || data.data.length === 0) {
|
||||
tbody.innerHTML = '<tr><td colspan="5" style="text-align:center;color:var(--color-text-muted);font-size:var(--text-caption);">Sin ventas hoy</td></tr>';
|
||||
return;
|
||||
}
|
||||
|
||||
// Fetch items for first 5 sales
|
||||
const salesToShow = data.data.slice(0, 5);
|
||||
const details = await Promise.all(
|
||||
salesToShow.map(s => apiFetch(`/pos/api/sales/${s.id}`))
|
||||
);
|
||||
|
||||
tbody.innerHTML = salesToShow.map((sale, idx) => {
|
||||
const detail = details[idx];
|
||||
const time = sale.created_at ? sale.created_at.slice(11, 16) : '--:--';
|
||||
const client = sale.customer_name || 'Publico General';
|
||||
const total = sale.total || 0;
|
||||
const method = sale.payment_method || 'efectivo';
|
||||
|
||||
// Build products summary from detail items
|
||||
let productsSummary = '';
|
||||
if (detail && detail.items && detail.items.length > 0) {
|
||||
productsSummary = detail.items.slice(0, 3).map(it =>
|
||||
`${escHtml(it.name)}${it.quantity > 1 ? ' (x' + it.quantity + ')' : ''}`
|
||||
).join(', ');
|
||||
if (detail.items.length > 3) productsSummary += '...';
|
||||
}
|
||||
|
||||
const methodClass = getPaymentBadgeClass(method);
|
||||
const methodLabel = capitalize(method);
|
||||
|
||||
const statusTag = sale.status === 'cancelled'
|
||||
? ' <span style="color:var(--color-error);font-size:10px;font-weight:700;">[CANCELADA]</span>' : '';
|
||||
|
||||
return `
|
||||
<tr>
|
||||
<td><span class="td-time">${time}</span></td>
|
||||
<td><span class="td-client">${escHtml(client)}</span>${statusTag}</td>
|
||||
<td class="td-products">${productsSummary}</td>
|
||||
<td><span class="td-mono">${fmt(total)}</span></td>
|
||||
<td><span class="pago-badge pago-badge--${methodClass}">${methodLabel}</span></td>
|
||||
</tr>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function getPaymentBadgeClass(method) {
|
||||
const m = (method || '').toLowerCase();
|
||||
if (m.includes('efectivo') || m === 'cash') return 'efectivo';
|
||||
if (m.includes('tarjeta') || m === 'card') return 'tarjeta';
|
||||
if (m.includes('transferencia') || m === 'transfer') return 'transferencia';
|
||||
if (m.includes('credito') || m === 'credit') return 'credito';
|
||||
return 'efectivo';
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
@@ -102,12 +510,23 @@ const Dashboard = (() => {
|
||||
}
|
||||
} catch(e) {}
|
||||
|
||||
loadSalesSummary();
|
||||
setGreeting();
|
||||
|
||||
// Load all data in parallel
|
||||
loadDailySummary();
|
||||
loadAlerts();
|
||||
loadTopProducts();
|
||||
loadWeeklyChart();
|
||||
loadRecentSales();
|
||||
|
||||
// Auto-refresh every 2 minutes
|
||||
setInterval(() => {
|
||||
loadDailySummary();
|
||||
loadRecentSales();
|
||||
}, 120000);
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
|
||||
return { init, loadSalesSummary, loadTopProducts, loadRecentSales, setTheme };
|
||||
return { init, setTheme };
|
||||
})();
|
||||
|
||||
@@ -1422,8 +1422,8 @@
|
||||
</svg>
|
||||
</button>
|
||||
<div class="header__greeting">
|
||||
<div class="header__title">Buenos días, Carlos</div>
|
||||
<div class="header__subtitle">Miércoles, 1 de abril de 2026 · Sucursal Centro</div>
|
||||
<div class="header__title" id="header-greeting">Buenos días</div>
|
||||
<div class="header__subtitle" id="header-subtitle">Cargando...</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="header__right">
|
||||
@@ -1440,7 +1440,7 @@
|
||||
</svg>
|
||||
<span class="notif-dot"></span>
|
||||
</button>
|
||||
<button class="icon-btn" title="Refrescar datos">
|
||||
<button class="icon-btn" title="Refrescar datos" onclick="Dashboard.init()">
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
|
||||
<path d="M13.5 8a5.5 5.5 0 11-1.5-3.8" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/>
|
||||
<path d="M14 2v4h-4" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
@@ -1464,7 +1464,7 @@
|
||||
<div class="kpi-grid">
|
||||
|
||||
<!-- Card 1: Ventas Hoy -->
|
||||
<div class="kpi-card">
|
||||
<div class="kpi-card" id="kpi-ventas-hoy">
|
||||
<div class="kpi-card__accent-bar"></div>
|
||||
<div class="kpi-card__label">
|
||||
Ventas Hoy
|
||||
@@ -1474,47 +1474,14 @@
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
<div class="kpi-card__value">$45,230</div>
|
||||
<div class="kpi-card__meta">
|
||||
<span class="kpi-tag kpi-tag--up">
|
||||
<svg width="9" height="9" viewBox="0 0 9 9" fill="none">
|
||||
<path d="M4.5 7V2M2 4.5L4.5 2 7 4.5" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
+12%
|
||||
</span>
|
||||
<span class="kpi-meta-text">vs ayer $40,382</span>
|
||||
<div class="kpi-card__value" id="kpi-ventas-total">--</div>
|
||||
<div class="kpi-card__meta" id="kpi-ventas-meta">
|
||||
<span class="kpi-tag kpi-tag--neutral">Cargando...</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Card 2: Ventas Mes -->
|
||||
<div class="kpi-card">
|
||||
<div class="kpi-card__accent-bar"></div>
|
||||
<div class="kpi-card__label">
|
||||
Ventas Mes
|
||||
<span class="kpi-card__icon">
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none">
|
||||
<rect x="1" y="2" width="12" height="11" rx="1.5" stroke="currentColor" stroke-width="1.4"/>
|
||||
<path d="M4 1v2M10 1v2M1 6h12" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/>
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
<div class="kpi-card__value">$892,150</div>
|
||||
<div class="kpi-card__meta">
|
||||
<span class="kpi-meta-text">Meta: $1,310,000</span>
|
||||
</div>
|
||||
<div class="kpi-progress">
|
||||
<div class="kpi-progress__track">
|
||||
<div class="kpi-progress__fill" style="width: 68%"></div>
|
||||
</div>
|
||||
<div class="kpi-progress__label">
|
||||
<span>68% de meta</span>
|
||||
<span>$417,850 restante</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Card 3: Tickets Hoy -->
|
||||
<div class="kpi-card">
|
||||
<!-- Card 2: Tickets Hoy -->
|
||||
<div class="kpi-card" id="kpi-tickets">
|
||||
<div class="kpi-card__accent-bar"></div>
|
||||
<div class="kpi-card__label">
|
||||
Tickets Hoy
|
||||
@@ -1525,28 +1492,44 @@
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
<div class="kpi-card__value">23</div>
|
||||
<div class="kpi-card__meta">
|
||||
<span class="kpi-tag kpi-tag--neutral">Promedio</span>
|
||||
<span class="kpi-meta-text">$1,966 por ticket</span>
|
||||
<div class="kpi-card__value" id="kpi-tickets-count">--</div>
|
||||
<div class="kpi-card__meta" id="kpi-tickets-meta">
|
||||
<span class="kpi-tag kpi-tag--neutral">Cargando...</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Card 4: Utilidad Mes -->
|
||||
<div class="kpi-card">
|
||||
<!-- Card 3: Ticket Promedio -->
|
||||
<div class="kpi-card" id="kpi-promedio">
|
||||
<div class="kpi-card__accent-bar"></div>
|
||||
<div class="kpi-card__label">
|
||||
Utilidad Mes
|
||||
Ticket Promedio
|
||||
<span class="kpi-card__icon">
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none">
|
||||
<rect x="1" y="2" width="12" height="11" rx="1.5" stroke="currentColor" stroke-width="1.4"/>
|
||||
<path d="M4 1v2M10 1v2M1 6h12" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/>
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
<div class="kpi-card__value" id="kpi-promedio-value">--</div>
|
||||
<div class="kpi-card__meta" id="kpi-promedio-meta">
|
||||
<span class="kpi-tag kpi-tag--neutral">Cargando...</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Card 4: Cajas Activas -->
|
||||
<div class="kpi-card" id="kpi-cajas">
|
||||
<div class="kpi-card__accent-bar"></div>
|
||||
<div class="kpi-card__label">
|
||||
Cajas del Día
|
||||
<span class="kpi-card__icon">
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none">
|
||||
<path d="M2 11L5 8l2.5 2L10 5l2.5 2.5" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
<div class="kpi-card__value">$178,430</div>
|
||||
<div class="kpi-card__meta">
|
||||
<span class="kpi-tag kpi-tag--up">20%</span>
|
||||
<span class="kpi-meta-text">margen neto</span>
|
||||
<div class="kpi-card__value" id="kpi-cajas-count">--</div>
|
||||
<div class="kpi-card__meta" id="kpi-cajas-meta">
|
||||
<span class="kpi-tag kpi-tag--neutral">Cargando...</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1562,8 +1545,8 @@
|
||||
<div class="chart-header">
|
||||
<div>
|
||||
<div class="section-title">Ventas Semanales</div>
|
||||
<div style="font-size:var(--text-caption);color:var(--color-text-muted);margin-top:2px;">
|
||||
Total semana: <strong style="color:var(--color-primary);font-family:var(--font-mono);">$267,840</strong>
|
||||
<div style="font-size:var(--text-caption);color:var(--color-text-muted);margin-top:2px;" id="chart-week-total">
|
||||
Total semana: <strong style="color:var(--color-primary);font-family:var(--font-mono);">--</strong>
|
||||
</div>
|
||||
</div>
|
||||
<div class="period-selector">
|
||||
@@ -1574,68 +1557,15 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bar chart: heights are % of max (Jue = max $52k) -->
|
||||
<!-- Bar chart: populated by JS -->
|
||||
<div class="bar-chart" id="bar-chart">
|
||||
|
||||
<div class="bar-chart__col">
|
||||
<div class="bar-chart__bar-wrap">
|
||||
<div class="bar-chart__bar" style="height:62%" data-value="$32,100"></div>
|
||||
</div>
|
||||
<span class="bar-chart__label">Lun</span>
|
||||
<!-- Filled dynamically -->
|
||||
</div>
|
||||
|
||||
<div class="bar-chart__col">
|
||||
<div class="bar-chart__bar-wrap">
|
||||
<div class="bar-chart__bar" style="height:79%" data-value="$41,200"></div>
|
||||
</div>
|
||||
<span class="bar-chart__label">Mar</span>
|
||||
</div>
|
||||
|
||||
<div class="bar-chart__col">
|
||||
<div class="bar-chart__bar-wrap">
|
||||
<div class="bar-chart__bar" style="height:71%" data-value="$36,800"></div>
|
||||
</div>
|
||||
<span class="bar-chart__label">Mié</span>
|
||||
</div>
|
||||
|
||||
<div class="bar-chart__col">
|
||||
<div class="bar-chart__bar-wrap">
|
||||
<div class="bar-chart__bar" style="height:100%" data-value="$52,000"></div>
|
||||
</div>
|
||||
<span class="bar-chart__label">Jue</span>
|
||||
</div>
|
||||
|
||||
<div class="bar-chart__col">
|
||||
<div class="bar-chart__bar-wrap">
|
||||
<div class="bar-chart__bar" style="height:88%" data-value="$45,800"></div>
|
||||
</div>
|
||||
<span class="bar-chart__label">Vie</span>
|
||||
</div>
|
||||
|
||||
<div class="bar-chart__col">
|
||||
<div class="bar-chart__bar-wrap">
|
||||
<div class="bar-chart__bar" style="height:55%" data-value="$28,600"></div>
|
||||
</div>
|
||||
<span class="bar-chart__label">Sáb</span>
|
||||
</div>
|
||||
|
||||
<div class="bar-chart__col">
|
||||
<div class="bar-chart__bar-wrap">
|
||||
<div class="bar-chart__bar today" style="height:87%" data-value="$45,230 ← Hoy"></div>
|
||||
</div>
|
||||
<span class="bar-chart__label" style="color:var(--color-primary);font-weight:700;">Dom</span>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="chart-legend">
|
||||
<div class="chart-legend" id="chart-legend">
|
||||
<div class="legend-item">
|
||||
<div class="legend-dot"></div>
|
||||
Ventas brutas
|
||||
</div>
|
||||
<div class="legend-item">
|
||||
<div class="legend-dot legend-dot--success"></div>
|
||||
Semana anterior promedio: $38,400
|
||||
Ventas brutas (7 días)
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1656,147 +1586,27 @@
|
||||
<div class="rank-card">
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:var(--space-4);">
|
||||
<div style="font-family:var(--font-heading);font-weight:var(--heading-weight-secondary);font-size:var(--text-body-sm);letter-spacing:var(--tracking-wider);text-transform:uppercase;color:var(--color-text-secondary);">
|
||||
Top Productos
|
||||
Top Productos Hoy
|
||||
</div>
|
||||
<a href="/pos/inventory" style="font-size:var(--text-caption);color:var(--color-primary);font-weight:var(--font-weight-semibold);cursor:pointer;text-decoration:none;">Ver todos</a>
|
||||
</div>
|
||||
<div class="rank-list">
|
||||
|
||||
<div class="rank-item">
|
||||
<div class="rank-num rank-num--1">1</div>
|
||||
<div class="rank-item__info">
|
||||
<div class="rank-item__name">Balatas Brembo Delanteras</div>
|
||||
<div class="rank-item__sub">SKU-0041 · 84 pzas vendidas</div>
|
||||
<div class="rank-item__bar-bg">
|
||||
<div class="rank-item__bar-fill" style="width:100%"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="rank-item__value">$67,200</div>
|
||||
</div>
|
||||
|
||||
<div class="rank-item">
|
||||
<div class="rank-num rank-num--2">2</div>
|
||||
<div class="rank-item__info">
|
||||
<div class="rank-item__name">Filtro de Aceite Mann</div>
|
||||
<div class="rank-item__sub">SKU-0087 · 121 pzas vendidas</div>
|
||||
<div class="rank-item__bar-bg">
|
||||
<div class="rank-item__bar-fill" style="width:78%"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="rank-item__value">$52,360</div>
|
||||
</div>
|
||||
|
||||
<div class="rank-item">
|
||||
<div class="rank-num">3</div>
|
||||
<div class="rank-item__info">
|
||||
<div class="rank-item__name">Amortiguador KYB Trasero</div>
|
||||
<div class="rank-item__sub">SKU-0023 · 38 pzas vendidas</div>
|
||||
<div class="rank-item__bar-bg">
|
||||
<div class="rank-item__bar-fill" style="width:61%"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="rank-item__value">$41,040</div>
|
||||
</div>
|
||||
|
||||
<div class="rank-item">
|
||||
<div class="rank-num">4</div>
|
||||
<div class="rank-item__info">
|
||||
<div class="rank-item__name">Bujía NGK Iridium</div>
|
||||
<div class="rank-item__sub">SKU-0156 · 204 pzas vendidas</div>
|
||||
<div class="rank-item__bar-bg">
|
||||
<div class="rank-item__bar-fill" style="width:49%"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="rank-item__value">$32,844</div>
|
||||
</div>
|
||||
|
||||
<div class="rank-item">
|
||||
<div class="rank-num">5</div>
|
||||
<div class="rank-item__info">
|
||||
<div class="rank-item__name">Banda de Distribución Gates</div>
|
||||
<div class="rank-item__sub">SKU-0072 · 27 pzas vendidas</div>
|
||||
<div class="rank-item__bar-bg">
|
||||
<div class="rank-item__bar-fill" style="width:38%"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="rank-item__value">$25,650</div>
|
||||
</div>
|
||||
|
||||
<div class="rank-list" id="top-products-list">
|
||||
<div class="rank-item" style="justify-content:center;color:var(--color-text-muted);font-size:var(--text-caption);">Cargando...</div>
|
||||
</div>
|
||||
</div><!-- end Top Productos -->
|
||||
|
||||
<!-- Top Clientes -->
|
||||
<!-- Desglose por Caja -->
|
||||
<div class="rank-card">
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:var(--space-4);">
|
||||
<div style="font-family:var(--font-heading);font-weight:var(--heading-weight-secondary);font-size:var(--text-body-sm);letter-spacing:var(--tracking-wider);text-transform:uppercase;color:var(--color-text-secondary);">
|
||||
Top Clientes
|
||||
Cajas del Día
|
||||
</div>
|
||||
<a href="/pos/customers" style="font-size:var(--text-caption);color:var(--color-primary);font-weight:var(--font-weight-semibold);cursor:pointer;text-decoration:none;">Ver todos</a>
|
||||
<a href="/pos/config" style="font-size:var(--text-caption);color:var(--color-primary);font-weight:var(--font-weight-semibold);cursor:pointer;text-decoration:none;">Gestionar</a>
|
||||
</div>
|
||||
<div class="rank-list">
|
||||
|
||||
<div class="rank-item">
|
||||
<div class="rank-num rank-num--1">1</div>
|
||||
<div class="rank-item__info">
|
||||
<div class="rank-item__name">Taller Mecánico Ramírez</div>
|
||||
<div class="rank-item__sub">RFC TMR-210415 · 18 compras/mes</div>
|
||||
<div class="rank-item__bar-bg">
|
||||
<div class="rank-item__bar-fill" style="width:100%"></div>
|
||||
<div class="rank-list" id="registers-list">
|
||||
<div class="rank-item" style="justify-content:center;color:var(--color-text-muted);font-size:var(--text-caption);">Cargando...</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="rank-item__value">$89,400</div>
|
||||
</div>
|
||||
|
||||
<div class="rank-item">
|
||||
<div class="rank-num rank-num--2">2</div>
|
||||
<div class="rank-item__info">
|
||||
<div class="rank-item__name">Distribuidora Automotriz López</div>
|
||||
<div class="rank-item__sub">RFC DAL-190302 · 11 compras/mes</div>
|
||||
<div class="rank-item__bar-bg">
|
||||
<div class="rank-item__bar-fill" style="width:74%"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="rank-item__value">$66,100</div>
|
||||
</div>
|
||||
|
||||
<div class="rank-item">
|
||||
<div class="rank-num">3</div>
|
||||
<div class="rank-item__info">
|
||||
<div class="rank-item__name">Servicio Automotriz Torres</div>
|
||||
<div class="rank-item__sub">RFC SAT-180720 · 9 compras/mes</div>
|
||||
<div class="rank-item__bar-bg">
|
||||
<div class="rank-item__bar-fill" style="width:57%"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="rank-item__value">$50,820</div>
|
||||
</div>
|
||||
|
||||
<div class="rank-item">
|
||||
<div class="rank-num">4</div>
|
||||
<div class="rank-item__info">
|
||||
<div class="rank-item__name">Flotilla Empresarial Hernández</div>
|
||||
<div class="rank-item__sub">RFC FEH-200115 · 7 compras/mes</div>
|
||||
<div class="rank-item__bar-bg">
|
||||
<div class="rank-item__bar-fill" style="width:43%"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="rank-item__value">$38,250</div>
|
||||
</div>
|
||||
|
||||
<div class="rank-item">
|
||||
<div class="rank-num">5</div>
|
||||
<div class="rank-item__info">
|
||||
<div class="rank-item__name">Refaccionaria El Pistón</div>
|
||||
<div class="rank-item__sub">RFC REP-221108 · 5 compras/mes</div>
|
||||
<div class="rank-item__bar-bg">
|
||||
<div class="rank-item__bar-fill" style="width:30%"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="rank-item__value">$26,700</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div><!-- end Top Clientes -->
|
||||
</div><!-- end Cajas del Día -->
|
||||
|
||||
</div><!-- end two-col-grid -->
|
||||
</section>
|
||||
@@ -1810,62 +1620,8 @@
|
||||
<span class="section-title">Alertas & Pendientes</span>
|
||||
<span class="section-action">Gestionar todo →</span>
|
||||
</div>
|
||||
<div class="alerts-grid">
|
||||
|
||||
<!-- Alert 1: Low stock (red) -->
|
||||
<div class="alert-item alert-item--error">
|
||||
<div class="alert-icon-wrap alert-icon-wrap--error">
|
||||
<svg width="18" height="18" viewBox="0 0 18 18" fill="none">
|
||||
<path d="M9 3L16.5 15H1.5L9 3z" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/>
|
||||
<path d="M9 8v3M9 12.5v.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="alert-content">
|
||||
<div style="display:flex;align-items:center;gap:var(--space-2);margin-bottom:3px;">
|
||||
<span class="alert-title">Stock Crítico</span>
|
||||
<span class="alert-badge alert-badge--error">3 items</span>
|
||||
</div>
|
||||
<div class="alert-desc">Balatas Brembo: <strong>3 unidades</strong> · Filtro Mann: <strong>2 unidades</strong> · Bujía NGK: <strong>5 unidades</strong></div>
|
||||
</div>
|
||||
<div class="alert-action">Reabastecer</div>
|
||||
</div>
|
||||
|
||||
<!-- Alert 2: Pending invoices (yellow) -->
|
||||
<div class="alert-item alert-item--warning">
|
||||
<div class="alert-icon-wrap alert-icon-wrap--warning">
|
||||
<svg width="18" height="18" viewBox="0 0 18 18" fill="none">
|
||||
<path d="M4 2h10a1 1 0 011 1v12a1 1 0 01-1 1H4a1 1 0 01-1-1V3a1 1 0 011-1z" stroke="currentColor" stroke-width="1.5"/>
|
||||
<path d="M6 7h6M6 9.5h4M6 12h2" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="alert-content">
|
||||
<div style="display:flex;align-items:center;gap:var(--space-2);margin-bottom:3px;">
|
||||
<span class="alert-title">Ventas sin Facturar</span>
|
||||
<span class="alert-badge alert-badge--warning">8 ventas</span>
|
||||
</div>
|
||||
<div class="alert-desc">8 ventas del día sin timbrar ante el SAT · Total: <strong>$34,820</strong> · Plazo límite hoy</div>
|
||||
</div>
|
||||
<div class="alert-action">Facturar</div>
|
||||
</div>
|
||||
|
||||
<!-- Alert 3: Credits due (orange) -->
|
||||
<div class="alert-item alert-item--orange">
|
||||
<div class="alert-icon-wrap alert-icon-wrap--orange">
|
||||
<svg width="18" height="18" viewBox="0 0 18 18" fill="none">
|
||||
<circle cx="9" cy="9" r="7" stroke="currentColor" stroke-width="1.5"/>
|
||||
<path d="M9 5v4l2.5 2.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="alert-content">
|
||||
<div style="display:flex;align-items:center;gap:var(--space-2);margin-bottom:3px;">
|
||||
<span class="alert-title">Crédito Vencido</span>
|
||||
<span class="alert-badge alert-badge--orange">Urgente</span>
|
||||
</div>
|
||||
<div class="alert-desc">Taller Ramírez: <strong>$12,500</strong> vencido hace 15 días · Distribuidora López: <strong>$8,200</strong> vence hoy</div>
|
||||
</div>
|
||||
<div class="alert-action">Cobrar</div>
|
||||
</div>
|
||||
|
||||
<div class="alerts-grid" id="alerts-grid">
|
||||
<div class="alert-item" style="justify-content:center;color:var(--color-text-muted);font-size:var(--text-caption);border-left:none;">Cargando alertas...</div>
|
||||
</div><!-- end alerts-grid -->
|
||||
</section>
|
||||
|
||||
@@ -1902,42 +1658,8 @@
|
||||
<th>Método Pago</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><span class="td-time">14:32</span></td>
|
||||
<td><span class="td-client">Taller Ramírez</span></td>
|
||||
<td class="td-products">Balatas Brembo (x2), Filtro Mann (x1)</td>
|
||||
<td><span class="td-mono">$3,840</span></td>
|
||||
<td><span class="pago-badge pago-badge--credito">Crédito 30d</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class="td-time">13:18</span></td>
|
||||
<td><span class="td-client">Público General</span></td>
|
||||
<td class="td-products">Amortiguador KYB (x1), Bujía NGK (x4)</td>
|
||||
<td><span class="td-mono">$2,156</span></td>
|
||||
<td><span class="pago-badge pago-badge--tarjeta">Tarjeta</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class="td-time">12:45</span></td>
|
||||
<td><span class="td-client">Flotilla Hernández</span></td>
|
||||
<td class="td-products">Banda Gates (x3), Filtro aceite (x6)</td>
|
||||
<td><span class="td-mono">$7,380</span></td>
|
||||
<td><span class="pago-badge pago-badge--transferencia">Transferencia</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class="td-time">11:07</span></td>
|
||||
<td><span class="td-client">Servicio Torres</span></td>
|
||||
<td class="td-products">Amortiguador KYB (x2)</td>
|
||||
<td><span class="td-mono">$2,160</span></td>
|
||||
<td><span class="pago-badge pago-badge--efectivo">Efectivo</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class="td-time">10:23</span></td>
|
||||
<td><span class="td-client">Distribuidora López</span></td>
|
||||
<td class="td-products">Bujía NGK (x24), Filtro Mann (x12), Balatas (x4)</td>
|
||||
<td><span class="td-mono">$18,440</span></td>
|
||||
<td><span class="pago-badge pago-badge--transferencia">Transferencia</span></td>
|
||||
</tr>
|
||||
<tbody id="recent-sales-tbody">
|
||||
<tr><td colspan="5" style="text-align:center;color:var(--color-text-muted);font-size:var(--text-caption);">Cargando ventas recientes...</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user