Created
December 28, 2025 19:34
-
-
Save Jcbertorello/35fcbb76611b562fb9d38347d7b2bf6c to your computer and use it in GitHub Desktop.
Dashboard Ajonjolí - 2025-12-28 19:34
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!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