623 lines
17 KiB
JavaScript
623 lines
17 KiB
JavaScript
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);
|