dashboard

This commit is contained in:
Lucas Tettamanti
2026-01-27 02:41:39 -03:00
parent 493f26af17
commit df9420b954
19 changed files with 2105 additions and 111 deletions

View File

@@ -0,0 +1,622 @@
import { api } from "../lib/api.js";
function formatCurrency(value) {
if (value == null) return "$0";
return new Intl.NumberFormat("es-AR", { style: "currency", currency: "ARS", maximumFractionDigits: 0 }).format(value);
}
function formatNumber(value) {
if (value == null) return "0";
return new Intl.NumberFormat("es-AR", { maximumFractionDigits: 1 }).format(value);
}
class HomeDashboard extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: "open" });
this.stats = null;
this.loading = false;
this.charts = {};
this.shadowRoot.innerHTML = `
<style>
:host {
--bg: #0b0f14;
--panel: #121823;
--muted: #8aa0b5;
--text: #e7eef7;
--line: #1e2a3a;
--blue: #3b82f6;
--green: #25D366;
--purple: #8B5CF6;
--orange: #F59E0B;
--emerald: #10B981;
--pink: #EC4899;
--gray: #9CA3AF;
}
* { box-sizing: border-box; font-family: system-ui, Segoe UI, Roboto, Arial; }
.container {
min-height: 100%;
background: var(--bg);
color: var(--text);
padding: 16px;
overflow-y: auto;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.header h1 {
font-size: 24px;
font-weight: 600;
margin: 0;
}
.sync-info {
font-size: 12px;
color: var(--muted);
}
.grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
gap: 16px;
}
.chart-card {
background: var(--panel);
border-radius: 8px;
padding: 16px;
}
.chart-card.full-width {
grid-column: 1 / -1;
}
.chart-title {
font-size: 14px;
font-weight: 600;
color: var(--muted);
text-transform: uppercase;
letter-spacing: 0.5px;
margin-bottom: 12px;
}
.chart-container {
position: relative;
height: 250px;
}
.chart-container.tall {
height: 300px;
}
.chart-container.short {
height: 200px;
}
.loading {
display: flex;
align-items: center;
justify-content: center;
height: 200px;
color: var(--muted);
}
.kpi-row {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 16px;
margin-bottom: 20px;
}
.kpi-card {
background: var(--panel);
border-radius: 8px;
padding: 16px;
text-align: center;
}
.kpi-value {
font-size: 24px;
font-weight: 700;
margin-bottom: 4px;
}
.kpi-label {
font-size: 12px;
color: var(--muted);
text-transform: uppercase;
}
.donut-row {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 16px;
}
canvas {
max-width: 100%;
}
</style>
<div class="container">
<div class="header">
<h1>Dashboard de Ventas</h1>
<div class="sync-info"></div>
</div>
<div class="kpi-row"></div>
<div class="grid">
<div class="chart-card full-width">
<div class="chart-title">Ventas Totales por Mes</div>
<div class="chart-container tall">
<canvas id="monthly-chart"></canvas>
</div>
</div>
<div class="chart-card">
<div class="chart-title">Web vs WhatsApp</div>
<div class="chart-container">
<canvas id="source-chart"></canvas>
</div>
</div>
<div class="chart-card">
<div class="chart-title">Comparativa Año a Año</div>
<div class="chart-container">
<canvas id="yoy-chart"></canvas>
</div>
</div>
<div class="donut-row">
<div class="chart-card">
<div class="chart-title">Por Canal</div>
<div class="chart-container short">
<canvas id="source-donut"></canvas>
</div>
</div>
<div class="chart-card">
<div class="chart-title">Delivery vs Retiro</div>
<div class="chart-container short">
<canvas id="shipping-donut"></canvas>
</div>
</div>
<div class="chart-card">
<div class="chart-title">Efectivo vs Tarjeta</div>
<div class="chart-container short">
<canvas id="payment-donut"></canvas>
</div>
</div>
</div>
<div class="chart-card full-width">
<div class="chart-title">Top Productos por Facturación</div>
<div class="chart-container tall">
<canvas id="products-chart"></canvas>
</div>
</div>
<div class="chart-card">
<div class="chart-title">Top por Kg Vendidos</div>
<div class="chart-container">
<canvas id="kg-chart"></canvas>
</div>
</div>
<div class="chart-card">
<div class="chart-title">Top por Unidades</div>
<div class="chart-container">
<canvas id="units-chart"></canvas>
</div>
</div>
</div>
</div>
`;
}
connectedCallback() {
this.loadStats();
}
disconnectedCallback() {
// Destruir charts para liberar memoria
Object.values(this.charts).forEach(chart => chart?.destroy?.());
this.charts = {};
}
async loadStats() {
this.loading = true;
try {
this.stats = await api.getOrderStats();
this.render();
} catch (err) {
console.error("[home-dashboard] loadStats error:", err);
} finally {
this.loading = false;
}
}
render() {
if (!this.stats) return;
// Actualizar sync info
const syncInfo = this.shadowRoot.querySelector(".sync-info");
syncInfo.textContent = `${this.stats.total_in_cache || 0} pedidos en cache`;
if (this.stats.synced > 0) {
syncInfo.textContent += ` (${this.stats.synced} nuevos sincronizados)`;
}
// Renderizar KPIs
this.renderKPIs();
// Renderizar charts
this.renderMonthlyChart();
this.renderSourceChart();
this.renderYoyChart();
this.renderDonuts();
this.renderProductsChart();
this.renderKgChart();
this.renderUnitsChart();
}
renderKPIs() {
const totals = this.stats.totals_aggregated || {};
const kpiRow = this.shadowRoot.querySelector(".kpi-row");
kpiRow.innerHTML = `
<div class="kpi-card">
<div class="kpi-value" style="color: var(--blue)">${formatCurrency(totals.total_revenue)}</div>
<div class="kpi-label">Total Facturado</div>
</div>
<div class="kpi-card">
<div class="kpi-value">${formatNumber(totals.total_orders)}</div>
<div class="kpi-label">Pedidos</div>
</div>
<div class="kpi-card">
<div class="kpi-value" style="color: var(--green)">${formatCurrency(totals.by_source?.whatsapp)}</div>
<div class="kpi-label">WhatsApp</div>
</div>
<div class="kpi-card">
<div class="kpi-value" style="color: var(--blue)">${formatCurrency(totals.by_source?.web)}</div>
<div class="kpi-label">Web</div>
</div>
`;
}
renderMonthlyChart() {
const ctx = this.shadowRoot.getElementById("monthly-chart");
if (!ctx) return;
if (this.charts.monthly) this.charts.monthly.destroy();
const months = this.stats.months || [];
const totals = this.stats.totals || [];
this.charts.monthly = new Chart(ctx, {
type: "bar",
data: {
labels: months.map(m => {
const [y, mo] = m.split("-");
return `${mo}/${y.slice(2)}`;
}),
datasets: [{
label: "Ventas",
data: totals,
backgroundColor: "rgba(59, 130, 246, 0.8)",
borderColor: "#3b82f6",
borderWidth: 1,
}],
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: { display: false },
},
scales: {
y: {
beginAtZero: true,
ticks: { color: "#8aa0b5" },
grid: { color: "#1e2a3a" },
},
x: {
ticks: { color: "#8aa0b5" },
grid: { display: false },
},
},
},
});
}
renderSourceChart() {
const ctx = this.shadowRoot.getElementById("source-chart");
if (!ctx) return;
if (this.charts.source) this.charts.source.destroy();
const months = this.stats.months || [];
const waData = this.stats.by_source?.whatsapp || [];
const webData = this.stats.by_source?.web || [];
this.charts.source = new Chart(ctx, {
type: "bar",
data: {
labels: months.map(m => {
const [y, mo] = m.split("-");
return `${mo}/${y.slice(2)}`;
}),
datasets: [
{
label: "WhatsApp",
data: waData,
backgroundColor: "#25D366",
},
{
label: "Web",
data: webData,
backgroundColor: "#3b82f6",
},
],
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: "top",
labels: { color: "#8aa0b5" },
},
},
scales: {
y: {
stacked: true,
beginAtZero: true,
ticks: { color: "#8aa0b5" },
grid: { color: "#1e2a3a" },
},
x: {
stacked: true,
ticks: { color: "#8aa0b5" },
grid: { display: false },
},
},
},
});
}
renderYoyChart() {
const ctx = this.shadowRoot.getElementById("yoy-chart");
if (!ctx) return;
if (this.charts.yoy) this.charts.yoy.destroy();
const yoy = this.stats.yoy || {};
this.charts.yoy = new Chart(ctx, {
type: "line",
data: {
labels: yoy.months || [],
datasets: [
{
label: String(yoy.current_year || "Actual"),
data: yoy.current_year_data || [],
borderColor: "#3b82f6",
backgroundColor: "rgba(59, 130, 246, 0.1)",
fill: true,
tension: 0.3,
},
{
label: String(yoy.last_year || "Anterior"),
data: yoy.last_year_data || [],
borderColor: "#9CA3AF",
backgroundColor: "rgba(156, 163, 175, 0.1)",
fill: true,
tension: 0.3,
},
],
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: "top",
labels: { color: "#8aa0b5" },
},
},
scales: {
y: {
beginAtZero: true,
ticks: { color: "#8aa0b5" },
grid: { color: "#1e2a3a" },
},
x: {
ticks: { color: "#8aa0b5" },
grid: { display: false },
},
},
},
});
}
renderDonuts() {
const totals = this.stats.totals_aggregated || {};
// Source donut
const sourceCtx = this.shadowRoot.getElementById("source-donut");
if (sourceCtx) {
if (this.charts.sourceDonut) this.charts.sourceDonut.destroy();
this.charts.sourceDonut = new Chart(sourceCtx, {
type: "doughnut",
data: {
labels: ["WhatsApp", "Web"],
datasets: [{
data: [totals.by_source?.whatsapp || 0, totals.by_source?.web || 0],
backgroundColor: ["#25D366", "#3b82f6"],
}],
},
options: this.getDonutOptions(),
});
}
// Shipping donut
const shippingCtx = this.shadowRoot.getElementById("shipping-donut");
if (shippingCtx) {
if (this.charts.shippingDonut) this.charts.shippingDonut.destroy();
this.charts.shippingDonut = new Chart(shippingCtx, {
type: "doughnut",
data: {
labels: ["Delivery", "Retiro"],
datasets: [{
data: [totals.by_shipping?.delivery || 0, totals.by_shipping?.pickup || 0],
backgroundColor: ["#8B5CF6", "#F59E0B"],
}],
},
options: this.getDonutOptions(),
});
}
// Payment donut
const paymentCtx = this.shadowRoot.getElementById("payment-donut");
if (paymentCtx) {
if (this.charts.paymentDonut) this.charts.paymentDonut.destroy();
this.charts.paymentDonut = new Chart(paymentCtx, {
type: "doughnut",
data: {
labels: ["Efectivo", "Tarjeta"],
datasets: [{
data: [totals.by_payment?.cash || 0, totals.by_payment?.card || 0],
backgroundColor: ["#10B981", "#EC4899"],
}],
},
options: this.getDonutOptions(),
});
}
}
getDonutOptions() {
return {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: "bottom",
labels: { color: "#8aa0b5" },
},
},
};
}
renderProductsChart() {
const ctx = this.shadowRoot.getElementById("products-chart");
if (!ctx) return;
if (this.charts.products) this.charts.products.destroy();
const products = this.stats.top_products_revenue || [];
const labels = products.map(p => p.name?.slice(0, 30) || "Sin nombre");
const data = products.map(p => p.revenue || 0);
this.charts.products = new Chart(ctx, {
type: "bar",
data: {
labels,
datasets: [{
label: "Facturación",
data,
backgroundColor: "rgba(59, 130, 246, 0.8)",
borderColor: "#3b82f6",
borderWidth: 1,
}],
},
options: {
indexAxis: "y",
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: { display: false },
},
scales: {
x: {
beginAtZero: true,
ticks: { color: "#8aa0b5" },
grid: { color: "#1e2a3a" },
},
y: {
ticks: { color: "#8aa0b5" },
grid: { display: false },
},
},
},
});
}
renderKgChart() {
const ctx = this.shadowRoot.getElementById("kg-chart");
if (!ctx) return;
if (this.charts.kg) this.charts.kg.destroy();
const products = this.stats.top_products_kg || [];
const labels = products.map(p => p.name?.slice(0, 25) || "Sin nombre");
const data = products.map(p => p.kg || 0);
this.charts.kg = new Chart(ctx, {
type: "bar",
data: {
labels,
datasets: [{
label: "Kg",
data,
backgroundColor: "rgba(139, 92, 246, 0.8)",
borderColor: "#8B5CF6",
borderWidth: 1,
}],
},
options: {
indexAxis: "y",
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: { display: false },
},
scales: {
x: {
beginAtZero: true,
ticks: { color: "#8aa0b5" },
grid: { color: "#1e2a3a" },
},
y: {
ticks: { color: "#8aa0b5", font: { size: 10 } },
grid: { display: false },
},
},
},
});
}
renderUnitsChart() {
const ctx = this.shadowRoot.getElementById("units-chart");
if (!ctx) return;
if (this.charts.units) this.charts.units.destroy();
const products = this.stats.top_products_units || [];
const labels = products.map(p => p.name?.slice(0, 25) || "Sin nombre");
const data = products.map(p => p.units || 0);
this.charts.units = new Chart(ctx, {
type: "bar",
data: {
labels,
datasets: [{
label: "Unidades",
data,
backgroundColor: "rgba(245, 158, 11, 0.8)",
borderColor: "#F59E0B",
borderWidth: 1,
}],
},
options: {
indexAxis: "y",
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: { display: false },
},
scales: {
x: {
beginAtZero: true,
ticks: { color: "#8aa0b5" },
grid: { color: "#1e2a3a" },
},
y: {
ticks: { color: "#8aa0b5", font: { size: 10 } },
grid: { display: false },
},
},
},
});
}
}
customElements.define("home-dashboard", HomeDashboard);