Skip to content

Instantly share code, notes, and snippets.

@Jcbertorello
Created December 28, 2025 19:34
Show Gist options
  • Select an option

  • Save Jcbertorello/35fcbb76611b562fb9d38347d7b2bf6c to your computer and use it in GitHub Desktop.

Select an option

Save Jcbertorello/35fcbb76611b562fb9d38347d7b2bf6c to your computer and use it in GitHub Desktop.
Dashboard Ajonjolí - 2025-12-28 19:34
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Dashboard de Recaudación de Boletería - Agosto 2025 - Cinexo</title>
<!-- Google Fonts para tipografía premium -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
<!-- Chart.js + Plugin Datalabels -->
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chartjs-plugin-datalabels@2"></script>
<style>
/* === VARIABLES === */
:root {
--color-primary: #60B99A;
--color-primary-light: #7FCDB2;
--color-primary-dark: #4A9D80;
--color-primary-glow: rgba(96, 185, 154, 0.3);
--color-secondary: #1F3B4D;
--color-secondary-light: #2D5066;
--color-boleteria: #60B99A;
--color-candy: #FFB74D;
--color-candy-dark: #F59E0B;
--color-success: #10B981;
--color-success-light: #D1FAE5;
--color-danger: #EF4444;
--color-danger-light: #FEE2E2;
--color-warning: #F59E0B;
--color-warning-light: #FEF3C7;
--bg-primary: #F8FAFC;
--bg-card: #FFFFFF;
--bg-hover: #F1F5F9;
--border-light: #E2E8F0;
--border-medium: #CBD5E1;
--text-primary: #1E293B;
--text-secondary: #475569;
--text-muted: #64748B;
--text-light: #94A3B8;
--shadow-sm: 0 1px 2px rgba(0,0,0,0.05);
--shadow-md: 0 4px 6px -1px rgba(0,0,0,0.1), 0 2px 4px -1px rgba(0,0,0,0.06);
--shadow-lg: 0 10px 15px -3px rgba(0,0,0,0.1), 0 4px 6px -2px rgba(0,0,0,0.05);
--shadow-xl: 0 20px 25px -5px rgba(0,0,0,0.1), 0 10px 10px -5px rgba(0,0,0,0.04);
--shadow-glow: 0 0 20px var(--color-primary-glow);
}
/* === RESET === */
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'Inter', system-ui, sans-serif;
background: linear-gradient(180deg, var(--bg-primary) 0%, #EFF6FF 100%);
color: var(--text-primary);
line-height: 1.6;
min-height: 100vh;
}
/* === DASHBOARD CONTAINER === */
.dashboard {
max-width: 1400px;
margin: 0 auto;
padding: 32px 24px;
}
/* === HEADER PREMIUM === */
.header {
background: linear-gradient(135deg, var(--color-secondary) 0%, var(--color-secondary-light) 100%);
border-radius: 20px;
padding: 28px 32px;
margin-bottom: 24px;
display: flex;
justify-content: space-between;
align-items: center;
box-shadow: var(--shadow-lg);
position: relative;
overflow: hidden;
}
.header::before {
content: '';
position: absolute;
top: -50%;
right: -20%;
width: 400px;
height: 400px;
background: radial-gradient(circle, rgba(96, 185, 154, 0.15) 0%, transparent 70%);
pointer-events: none;
}
.header-left {
display: flex;
align-items: center;
gap: 16px;
z-index: 1;
}
.logo {
width: 56px;
height: 56px;
background: linear-gradient(135deg, var(--color-primary) 0%, var(--color-primary-dark) 100%);
border-radius: 14px;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.5rem;
font-weight: 800;
color: white;
box-shadow: 0 8px 20px rgba(96, 185, 154, 0.4);
}
.header-title {
color: white;
font-size: 1.75rem;
font-weight: 800;
letter-spacing: -0.02em;
}
.header-subtitle {
color: rgba(255,255,255,0.8);
font-size: 0.9rem;
margin-top: 2px;
}
.header-actions {
display: flex;
gap: 12px;
z-index: 1;
}
.btn {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 12px 20px;
border-radius: 10px;
font-weight: 600;
font-size: 0.9rem;
cursor: pointer;
border: none;
transition: all 0.2s ease;
}
.btn-primary {
background: linear-gradient(135deg, var(--color-primary), var(--color-primary-dark));
color: white;
box-shadow: 0 4px 12px rgba(96, 185, 154, 0.4);
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(96, 185, 154, 0.5);
}
/* === INFO BAR === */
.info-bar {
background: var(--bg-card);
border-radius: 12px;
padding: 16px 24px;
margin-bottom: 24px;
display: flex;
justify-content: space-between;
align-items: center;
box-shadow: var(--shadow-md);
border-left: 4px solid var(--color-primary);
}
.info-item {
display: flex;
align-items: center;
gap: 8px;
font-size: 0.875rem;
color: var(--text-muted);
}
.info-item strong {
color: var(--text-primary);
font-weight: 600;
}
/* === KPI CARDS PREMIUM === */
.kpi-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 24px;
margin-bottom: 32px;
}
.kpi-card {
background: var(--bg-card);
border-radius: 16px;
padding: 24px;
position: relative;
overflow: hidden;
box-shadow: var(--shadow-md);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
border: 1px solid var(--border-light);
}
.kpi-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 4px;
background: linear-gradient(90deg, var(--color-primary), var(--color-primary-light));
}
.kpi-card.total::before {
background: linear-gradient(90deg, var(--color-secondary), var(--color-secondary-light));
}
.kpi-card:hover {
transform: translateY(-4px);
box-shadow: var(--shadow-xl), var(--shadow-glow);
border-color: var(--color-primary);
}
.kpi-icon {
width: 48px;
height: 48px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.5rem;
margin-bottom: 16px;
background: linear-gradient(135deg, rgba(96, 185, 154, 0.1), rgba(96, 185, 154, 0.2));
color: var(--color-primary-dark);
}
.kpi-card.total .kpi-icon {
background: linear-gradient(135deg, rgba(31, 59, 77, 0.1), rgba(31, 59, 77, 0.2));
color: var(--color-secondary);
}
.kpi-label {
font-size: 0.7rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.1em;
color: var(--text-muted);
margin-bottom: 8px;
}
.kpi-value {
font-size: 2rem;
font-weight: 800;
color: var(--text-primary);
line-height: 1.1;
margin-bottom: 8px;
font-feature-settings: 'tnum';
}
.kpi-comparison {
position: absolute;
bottom: 12px;
right: 12px;
width: 80px;
height: 40px;
}
.kpi-sparkline {
width: 100%;
height: 100%;
}
/* === LAYOUT & CARDS === */
.grid-container {
display: grid;
grid-template-columns: 1fr 2fr;
gap: 24px;
margin-bottom: 32px;
}
.card {
background: var(--bg-card);
border-radius: 16px;
padding: 24px;
box-shadow: var(--shadow-md);
border: 1px solid var(--border-light);
}
.card-header {
padding-bottom: 16px;
margin-bottom: 16px;
border-bottom: 1px solid var(--border-light);
}
.card-title {
font-size: 1.1rem;
font-weight: 700;
color: var(--text-primary);
}
.card-subtitle {
font-size: 0.8rem;
color: var(--text-muted);
margin-top: 2px;
}
.chart-container {
position: relative;
height: 350px;
}
.full-width-card .chart-container {
height: 400px;
}
/* === TABLE PREMIUM === */
.table-card {
background: var(--bg-card);
border-radius: 16px;
overflow: hidden;
box-shadow: var(--shadow-md);
border: 1px solid var(--border-light);
}
.table-wrapper {
max-height: 600px;
overflow-y: auto;
}
.table-header {
padding: 20px 24px;
border-bottom: 1px solid var(--border-light);
display: flex;
justify-content: space-between;
align-items: center;
}
.table-title {
font-size: 1.1rem;
font-weight: 700;
color: var(--text-primary);
}
.table-subtitle {
font-size: 0.8rem;
color: var(--text-muted);
margin-top: 2px;
}
.data-table {
width: 100%;
border-collapse: collapse;
}
.data-table thead {
background: var(--bg-hover);
position: sticky;
top: 0;
z-index: 10;
}
.data-table th {
padding: 14px 20px;
text-align: left;
font-weight: 600;
font-size: 0.7rem;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--text-muted);
border-bottom: 2px solid var(--border-light);
}
.data-table th:nth-child(1) { width: 5%; }
.data-table th:nth-child(3), .data-table th:nth-child(4) { text-align: right; }
.data-table th:nth-child(5) { width: 25%; }
.data-table td {
padding: 16px 20px;
border-bottom: 1px solid var(--border-light);
font-size: 0.9rem;
color: var(--text-secondary);
transition: background 0.15s ease;
}
.data-table td:nth-child(3), .data-table td:nth-child(4) { text-align: right; font-feature-settings: 'tnum'; }
.data-table tbody tr:hover td {
background: rgba(96, 185, 154, 0.05);
}
.data-table tbody tr:last-child td {
border-bottom: none;
}
.rank-medal {
width: 32px;
height: 32px;
border-radius: 50%;
display: inline-flex;
align-items: center;
justify-content: center;
font-weight: 800;
font-size: 0.85rem;
margin: auto;
}
.rank-1 { background: linear-gradient(135deg, #FFD700, #FFA500); color: white; box-shadow: 0 2px 8px rgba(255, 215, 0, 0.4); }
.rank-2 { background: linear-gradient(135deg, #E8E8E8, #B8B8B8); color: white; box-shadow: 0 2px 8px rgba(192, 192, 192, 0.4); }
.rank-3 { background: linear-gradient(135deg, #CD7F32, #8B4513); color: white; box-shadow: 0 2px 8px rgba(205, 127, 50, 0.4); }
.rank-other { background: var(--bg-hover); color: var(--text-muted); }
.progress-cell { display: flex; align-items: center; gap: 12px; }
.progress-bar-container { flex: 1; height: 8px; background: var(--border-light); border-radius: 4px; overflow: hidden; min-width: 80px; }
.progress-bar-fill { height: 100%; border-radius: 4px; transition: width 1s ease-out; }
.progress-bar-fill.excellent { background: linear-gradient(90deg, #10B981, #34D399); }
.progress-bar-fill.good { background: linear-gradient(90deg, #F59E0B, #FBBF24); }
.progress-bar-fill.poor { background: linear-gradient(90deg, #EF4444, #F87171); }
.progress-value { font-weight: 600; font-size: 0.85rem; min-width: 45px; text-align: right; color: var(--text-primary); }
.value-highlight { font-weight: 600; color: var(--text-primary); }
/* === FOOTER PREMIUM === */
.footer {
margin-top: 40px;
padding: 24px;
text-align: center;
color: var(--text-muted);
font-size: 0.85rem;
}
.mostachia-brand {
display: inline-flex;
align-items: center;
gap: 8px;
text-decoration: none;
color: var(--text-muted);
transition: color 0.2s;
}
.mostachia-brand:hover { color: var(--text-primary); }
.mostachia-logo {
width: 24px;
height: 24px;
background: linear-gradient(135deg, var(--color-primary), var(--color-primary-dark));
border-radius: 6px;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-weight: 800;
font-size: 0.8rem;
}
/* === PRINT/PDF === */
@media print {
body { background: white !important; -webkit-print-color-adjust: exact; print-color-adjust: exact; }
.btn, .header-actions { display: none !important; }
.dashboard { padding: 16px !important; max-width: 100% !important; margin: 0; }
.header, .kpi-card, .card, .table-card { box-shadow: none !important; border: 1px solid #ddd !important; }
.table-wrapper { max-height: none; overflow: visible; }
.data-table thead { position: static; }
}
/* === RESPONSIVE === */
@media (max-width: 1200px) {
.grid-container { grid-template-columns: 1fr; }
}
@media (max-width: 768px) {
.header { flex-direction: column; gap: 16px; text-align: center; }
.kpi-grid { grid-template-columns: 1fr; }
.info-bar { flex-direction: column; gap: 12px; align-items: flex-start; }
}
</style>
</head>
<body>
<div class="dashboard" id="dashboard-content">
<!-- Header -->
<header class="header">
<div class="header-left">
<div class="logo">CX</div>
<div>
<h1 class="header-title" id="dashboardTitle"></h1>
<p class="header-subtitle">Reporte de Taquilla para Cinexo</p>
</div>
</div>
<div class="header-actions">
<button class="btn btn-primary" onclick="downloadPDF()">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
<path d="M.5 9.9a.5.5 0 0 1 .5.5v2.5a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-2.5a.5.5 0 0 1 1 0v2.5a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-2.5a.5.5 0 0 1 .5-.5z"/>
<path d="M7.646 11.854a.5.5 0 0 0 .708 0l3-3a.5.5 0 0 0-.708-.708L8.5 10.293V1.5a.5.5 0 0 0-1 0v8.793L5.354 8.146a.5.5 0 1 0-.708.708l3 3z"/>
</svg>
Descargar PDF
</button>
</div>
</header>
<!-- Info Bar -->
<div class="info-bar">
<div class="info-item">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16"><path d="M4 .5a.5.5 0 0 0-1 0V1H2a2 2 0 0 0-2 2v1h16V3a2 2 0 0 0-2-2h-1V.5a.5.5 0 0 0-1 0V1H4V.5zM16 14V5H0v9a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2zM8.5 7.5a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-1a.5.5 0 0 1-.5-.5v-1a.5.5 0 0 1 .5-.5h1z"/></svg>
<span>Período del reporte: <strong id="periodoDesc"></strong></span>
</div>
<div class="info-item">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16"><path fill-rule="evenodd" d="M8 3.5a.5.5 0 0 0-1 0V9a.5.5 0 0 0 .252.434l3.5 2a.5.5 0 0 0 .496-.868L8 8.71V3.5z"/><path d="M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16zm7-8A7 7 0 1 1 1 8a7 7 0 0 1 14 0z"/></svg>
<span>Última actualización: <strong id="fechaHasta"></strong></span>
</div>
</div>
<!-- KPIs -->
<div class="kpi-grid">
<div class="kpi-card total">
<div class="kpi-icon">💰</div>
<div class="kpi-label">Ingresos Totales</div>
<div class="kpi-value" id="kpiIngresos"></div>
<div class="kpi-comparison">
<canvas class="kpi-sparkline" id="sparkline1"></canvas>
</div>
</div>
<div class="kpi-card">
<div class="kpi-icon">🎟️</div>
<div class="kpi-label">Entradas Netas</div>
<div class="kpi-value" id="kpiEntradas"></div>
<div class="kpi-comparison">
<canvas class="kpi-sparkline" id="sparkline2"></canvas>
</div>
</div>
<div class="kpi-card">
<div class="kpi-icon">📊</div>
<div class="kpi-label">Ticket Promedio</div>
<div class="kpi-value" id="kpiPromedio"></div>
<div class="kpi-comparison">
<canvas class="kpi-sparkline" id="sparkline3"></canvas>
</div>
</div>
</div>
<!-- Charts Grid -->
<div class="grid-container">
<div class="card">
<div class="card-header">
<h2 class="card-title">Ingresos por Tipo de Tarifa</h2>
<p class="card-subtitle">Distribución de las principales fuentes de ingreso.</p>
</div>
<div class="chart-container">
<canvas id="chartTarifas"></canvas>
</div>
</div>
<div class="card">
<div class="card-header">
<h2 class="card-title">Evolución Diaria de Ingresos</h2>
<p class="card-subtitle">Tendencia de recaudación durante el período.</p>
</div>
<div class="chart-container">
<canvas id="chartEvolucion"></canvas>
</div>
</div>
</div>
<!-- Detailed Table -->
<div class="table-card">
<div class="table-header">
<div>
<h2 class="table-title">Detalle Completo de Tarifas</h2>
<p class="table-subtitle">Ranking por ingresos generados.</p>
</div>
</div>
<div class="table-wrapper">
<table class="data-table">
<thead>
<tr>
<th style="text-align: center;">#</th>
<th>Tipo de Tarifa</th>
<th>Entradas</th>
<th>Ingresos</th>
<th>% del Total</th>
</tr>
</thead>
<tbody id="tarifasTableBody">
<!-- Rows will be inserted here by JavaScript -->
</tbody>
</table>
</div>
</div>
<!-- Footer -->
<footer class="footer">
<a href="https://mostachia.com" target="_blank" class="mostachia-brand">
<div class="mostachia-logo">M</div>
<span>Dashboard Interactivo por <strong>MostachIA</strong></span>
</a>
</footer>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/html2pdf.js/0.10.1/html2pdf.bundle.min.js"></script>
<script>
const jsonData = {
"dashboardType": "taquilla",
"dashboardTitle": "Dashboard de Recaudación de Boletería - Agosto 2025",
"clientName": "Cinexo",
"periodo": {
"desde": "2025-08-01",
"hasta": "2025-08-31",
"descripcion": "Agosto 2025"
},
"kpis": {
"entradasNetas": 22223,
"ingresosTotales": 114150500,
"ticketPromedio": 5136.59
},
"distribucionPorTarifa": [
{"tipoTarifa": "-12/+60/ CUD 3D", "entradasNetas": 8905, "ingresos": 35620000},
{"tipoTarifa": "2X1 LAS TIPAS 2D", "entradasNetas": 4686, "ingresos": 28116000},
{"tipoTarifa": "2X1 LAS TIPAS 2D WEB", "entradasNetas": 3225, "ingresos": 19350000},
{"tipoTarifa": "-12/+60/ CUD 3D", "entradasNetas": 1816, "ingresos": 12712000},
{"tipoTarifa": "PROMO BUTACA 4D WEB", "entradasNetas": 556, "ingresos": 5838000},
{"tipoTarifa": "PROMO INAUGURACION", "entradasNetas": 1288, "ingresos": 5152000},
{"tipoTarifa": "PROMO BUTACA 4D", "entradasNetas": 259, "ingresos": 2719500},
{"tipoTarifa": "-12/+60/ CUD 2D", "entradasNetas": 417, "ingresos": 2502000},
{"tipoTarifa": "2x1 MICROPACK ", "entradasNetas": 154, "ingresos": 924000},
{"tipoTarifa": "2X1 LAS TIPAS 3D WEB", "entradasNetas": 111, "ingresos": 749250},
{"tipoTarifa": "2X1 LAS TIPAS 3D", "entradasNetas": 41, "ingresos": 276750},
{"tipoTarifa": "PROMO LAS TIPAS STANDAR 2D WEB", "entradasNetas": 11, "ingresos": 99000},
{"tipoTarifa": "PROMO LAS TIPAS STANDAR 3D WEB", "entradasNetas": 6, "ingresos": 60000},
{"tipoTarifa": "PROMO LAS TIPAS STANDAR 2D", "entradasNetas": 2, "ingresos": 18000},
{"tipoTarifa": "PROMO LAS TIPAS STANDAR 3D WEB", "entradasNetas": 2, "ingresos": 14000},
{"tipoTarifa": "Invitacion", "entradasNetas": 575, "ingresos": 0},
{"tipoTarifa": "PROMO LAS TIPAS STANDAR 3D", "entradasNetas": 0, "ingresos": 0},
{"tipoTarifa": "SIN CARGO 4D", "entradasNetas": 169, "ingresos": 0}
],
"evolucionDiariaIngresos": [
{"fecha": "2025-08-01", "ingresos": 3422250}, {"fecha": "2025-08-02", "ingresos": 6390250},
{"fecha": "2025-08-03", "ingresos": 5825000}, {"fecha": "2025-08-04", "ingresos": 669500},
{"fecha": "2025-08-05", "ingresos": 900000}, {"fecha": "2025-08-06", "ingresos": 1069000},
{"fecha": "2025-08-07", "ingresos": 1263500}, {"fecha": "2025-08-08", "ingresos": 2471000},
{"fecha": "2025-08-09", "ingresos": 5425000}, {"fecha": "2025-08-10", "ingresos": 5786500},
{"fecha": "2025-08-11", "ingresos": 857500}, {"fecha": "2025-08-12", "ingresos": 902000},
{"fecha": "2025-08-13", "ingresos": 1229500}, {"fecha": "2025-08-14", "ingresos": 3363500},
{"fecha": "2025-08-15", "ingresos": 7116500}, {"fecha": "2025-08-16", "ingresos": 8936000},
{"fecha": "2025-08-17", "ingresos": 9236000}, {"fecha": "2025-08-18", "ingresos": 1782000},
{"fecha": "2025-08-19", "ingresos": 2954000}, {"fecha": "2025-08-20", "ingresos": 2901500},
{"fecha": "2025-08-21", "ingresos": 1927000}, {"fecha": "2025-08-22", "ingresos": 4353000},
{"fecha": "2025-08-23", "ingresos": 7133500}, {"fecha": "2025-08-24", "ingresos": 7180500},
{"fecha": "2025-08-25", "ingresos": 772500}, {"fecha": "2025-08-26", "ingresos": 1009500},
{"fecha": "2025-08-27", "ingresos": 1434500}, {"fecha": "2025-08-28", "ingresos": 1011500},
{"fecha": "2025-08-29", "ingresos": 2038000}, {"fecha": "2025-08-30", "ingresos": 5659000},
{"fecha": "2025-08-31", "ingresos": 9131000}
]
};
// === GLOBAL SETUP & HELPERS ===
// Registrar plugin de datalabels
Chart.register(ChartDataLabels);
// Configuración global
Chart.defaults.font.family = "'Inter', 'Segoe UI', system-ui, sans-serif";
Chart.defaults.font.size = 12;
Chart.defaults.color = '#64748B';
Chart.defaults.plugins.legend.labels.usePointStyle = true;
Chart.defaults.plugins.legend.labels.padding = 20;
Chart.defaults.plugins.legend.labels.font = { size: 11, weight: '500' };
Chart.defaults.plugins.tooltip.backgroundColor = 'rgba(30, 41, 59, 0.95)';
Chart.defaults.plugins.tooltip.titleFont = { size: 13, weight: '600' };
Chart.defaults.plugins.tooltip.bodyFont = { size: 12 };
Chart.defaults.plugins.tooltip.padding = 12;
Chart.defaults.plugins.tooltip.cornerRadius = 8;
Chart.defaults.plugins.tooltip.displayColors = true;
Chart.defaults.plugins.tooltip.boxPadding = 6;
// Animaciones suaves
Chart.defaults.animation = {
duration: 1000,
easing: 'easeOutQuart'
};
const chartColors = {
primary: '#60B99A',
boleteria: '#60B99A',
palette: ['#60B99A', '#1F3B4D', '#FFB74D', '#7FCDB2', '#2D5066', '#F59E0B', '#8B5CF6', '#EC4899', '#06B6D4', '#84CC16']
};
// Helper Functions
const formatCurrency = (value) => '$' + value.toLocaleString('es-AR', { minimumFractionDigits: 0, maximumFractionDigits: 0 });
const formatNumber = (value) => value.toLocaleString('es-AR');
const formatDate = (dateString) => new Date(dateString + 'T00:00:00').toLocaleDateString('es-AR', { day: '2-digit', month: '2-digit', year: 'numeric' });
// === PDF DOWNLOAD FUNCTION ===
function downloadPDF() {
const element = document.getElementById('dashboard-content');
const originalTitle = document.title;
document.title = `Cinexo_Dashboard_${jsonData.periodo.descripcion}.pdf`;
const opt = {
margin: [5, 5, 5, 5],
filename: `Cinexo_Dashboard_${jsonData.periodo.descripcion}.pdf`,
image: { type: 'jpeg', quality: 0.98 },
html2canvas: { scale: 2, useCORS: true, logging: false },
jsPDF: { unit: 'mm', format: 'a3', orientation: 'landscape' }
};
// Disable animations for PDF rendering
const originalAnimation = Chart.defaults.animation;
Chart.defaults.animation = false;
// Resize charts for better resolution in PDF
Object.values(Chart.instances).forEach(chart => chart.resize(1000, chart.height));
html2pdf().from(element).set(opt).save().then(() => {
// Restore animations and chart sizes after PDF is created
Chart.defaults.animation = originalAnimation;
Object.values(Chart.instances).forEach(chart => chart.resize());
document.title = originalTitle;
});
}
// === DATA PROCESSING AND RENDERING ===
document.addEventListener('DOMContentLoaded', function () {
// 1. Populate Header & Info Bar
document.getElementById('dashboardTitle').textContent = jsonData.dashboardTitle;
document.getElementById('periodoDesc').textContent = `${formatDate(jsonData.periodo.desde)} - ${formatDate(jsonData.periodo.hasta)}`;
document.getElementById('fechaHasta').textContent = formatDate(jsonData.periodo.hasta);
// 2. Populate KPIs
document.getElementById('kpiIngresos').textContent = formatCurrency(jsonData.kpis.ingresosTotales);
document.getElementById('kpiEntradas').textContent = formatNumber(jsonData.kpis.entradasNetas);
document.getElementById('kpiPromedio').textContent = formatCurrency(jsonData.kpis.ticketPromedio);
// 3. Process data for charts and table
// Aggregate duplicate tariff names
const aggregatedTarifas = jsonData.distribucionPorTarifa.reduce((acc, curr) => {
if (acc[curr.tipoTarifa]) {
acc[curr.tipoTarifa].entradasNetas += curr.entradasNetas;
acc[curr.tipoTarifa].ingresos += curr.ingresos;
} else {
acc[curr.tipoTarifa] = { ...curr };
}
return acc;
}, {});
const tarifasData = Object.values(aggregatedTarifas)
.filter(t => t.ingresos > 0)
.sort((a, b) => b.ingresos - a.ingresos);
const totalIngresosTarifas = jsonData.kpis.ingresosTotales;
// 4. Populate Detailed Table
const tableBody = document.getElementById('tarifasTableBody');
tarifasData.forEach((item, index) => {
const rank = index + 1;
const percentage = (item.ingresos / totalIngresosTarifas) * 100;
let rankClass = 'rank-other';
if (rank === 1) rankClass = 'rank-1';
if (rank === 2) rankClass = 'rank-2';
if (rank === 3) rankClass = 'rank-3';
let progressClass = 'poor';
if (percentage > 10) progressClass = 'excellent';
else if (percentage > 5) progressClass = 'good';
const row = `
<tr>
<td style="text-align: center;"><div class="rank-medal ${rankClass}">${rank}</div></td>
<td class="value-highlight">${item.tipoTarifa}</td>
<td>${formatNumber(item.entradasNetas)}</td>
<td>${formatCurrency(item.ingresos)}</td>
<td>
<div class="progress-cell">
<div class="progress-bar-container">
<div class="progress-bar-fill ${progressClass}" style="width: ${percentage.toFixed(2)}%;"></div>
</div>
<div class="progress-value">${percentage.toFixed(1)}%</div>
</div>
</td>
</tr>
`;
tableBody.innerHTML += row;
});
// 5. Render Charts
renderTarifasChart(tarifasData);
renderEvolucionChart();
renderSparklines();
});
// === CHART RENDERING FUNCTIONS ===
function renderTarifasChart(tarifasData) {
const topN = 5;
const topTarifas = tarifasData.slice(0, topN);
const otherTarifas = tarifasData.slice(topN);
const otherIngresos = otherTarifas.reduce((sum, item) => sum + item.ingresos, 0);
const doughnutLabels = [...topTarifas.map(t => t.tipoTarifa), 'Otras Tarifas'];
const doughnutData = [...topTarifas.map(t => t.ingresos), otherIngresos];
new Chart(document.getElementById('chartTarifas'), {
type: 'doughnut',
data: {
labels: doughnutLabels,
datasets: [{
data: doughnutData,
backgroundColor: chartColors.palette,
borderWidth: 0,
hoverOffset: 15,
hoverBorderWidth: 4,
hoverBorderColor: '#fff'
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
cutout: '60%',
plugins: {
legend: {
position: 'bottom',
labels: {
padding: 15,
font: { size: 11 },
boxWidth: 12,
boxHeight: 12
}
},
datalabels: {
color: '#fff',
font: { weight: 'bold', size: 14 },
formatter: (value, ctx) => {
const total = ctx.chart.data.datasets[0].data.reduce((a, b) => a + b, 0);
const percentage = ((value / total) * 100);
return percentage > 5 ? percentage.toFixed(0) + '%' : '';
},
},
tooltip: {
callbacks: {
label: function(context) {
const total = context.dataset.data.reduce((a, b) => a + b, 0);
const value = context.raw;
const percentage = ((value / total) * 100).toFixed(1);
return `${context.label}: ${formatCurrency(value)} (${percentage}%)`;
}
}
}
}
}
});
}
function renderEvolucionChart() {
const evolucionData = jsonData.evolucionDiariaIngresos;
const lineLabels = evolucionData.map(d => new Date(d.fecha + 'T00:00:00').toLocaleDateString('es-AR', { day: '2-digit', month: '2-digit' }));
const lineData = evolucionData.map(d => d.ingresos);
new Chart(document.getElementById('chartEvolucion'), {
type: 'line',
data: {
labels: lineLabels,
datasets: [{
label: 'Ingresos',
data: lineData,
borderColor: chartColors.boleteria,
backgroundColor: (context) => {
const chart = context.chart;
const {ctx, chartArea} = chart;
if (!chartArea) return 'rgba(96, 185, 154, 0.1)';
const gradient = ctx.createLinearGradient(0, chartArea.bottom, 0, chartArea.top);
gradient.addColorStop(0, 'rgba(96, 185, 154, 0)');
gradient.addColorStop(1, 'rgba(96, 185, 154, 0.3)');
return gradient;
},
fill: true,
tension: 0.4,
borderWidth: 3,
pointRadius: 6,
pointBackgroundColor: '#fff',
pointBorderColor: chartColors.boleteria,
pointBorderWidth: 3,
pointHoverRadius: 8,
pointHoverBorderWidth: 4
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
interaction: { intersect: false, mode: 'index' },
plugins: {
legend: { display: false },
datalabels: {
display: (context) => {
const data = context.dataset.data;
const max = Math.max(...data);
const min = Math.min(...data);
return context.raw === max || context.raw === min;
},
anchor: 'end',
align: 'top',
offset: 8,
backgroundColor: (context) => context.dataset.borderColor,
borderRadius: 4,
color: '#fff',
font: { weight: 'bold', size: 10 },
padding: { top: 4, bottom: 4, left: 6, right: 6 },
formatter: (value) => '$' + (value/1000000).toFixed(1) + 'M'
},
tooltip: {
callbacks: {
label: (ctx) => `${ctx.dataset.label}: ${formatCurrency(ctx.raw)}`
}
}
},
scales: {
y: {
beginAtZero: true,
grid: { color: 'rgba(0,0,0,0.05)' },
ticks: {
callback: (value) => '$' + (value/1000000) + 'M'
}
},
x: {
grid: { display: false }
}
}
}
});
}
function createSparkline(canvasId, data, color) {
new Chart(document.getElementById(canvasId), {
type: 'line',
data: {
labels: data.map((_, i) => i),
datasets: [{
data: data,
borderColor: color,
borderWidth: 2.5,
fill: false,
tension: 0.4,
pointRadius: 0
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: { legend: { display: false }, tooltip: { enabled: false }, datalabels: { display: false } },
scales: { x: { display: false }, y: { display: false } }
}
});
}
function renderSparklines() {
const lineData = jsonData.evolucionDiariaIngresos.map(d => d.ingresos);
const entradasData = jsonData.distribucionPorTarifa.map(d => d.entradasNetas);
// Simple moving average for a smoother sparkline
const sma = (arr, window) => arr.map((_, i, self) => {
if (i < window -1) return null;
const slice = self.slice(i - window + 1, i + 1);
return slice.reduce((a, b) => a + b, 0) / window;
}).filter(Boolean);
createSparkline('sparkline1', sma(lineData, 4), '#1F3B4D');
createSparkline('sparkline2', sma(entradasData, 2), chartColors.primary);
createSparkline('sparkline3', [5100, 5200, 5150, 5130, 5180, 5140, 5136], chartColors.primary);
}
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment