You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1490 lines
69 KiB

<!DOCTYPE html>
<html lang="es">
<head>
<link rel="icon" type="image/png" href="images/favicon.png">
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Dashboard CDR - FreePBX</title>
<script src="https://cdn.tailwindcss.com/3.4.17"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
body { font-family: 'Inter', sans-serif; }
.tab-active { border-bottom: 3px solid #3b82f6; color: #3b82f6; }
.kpi-card { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); }
.chart-container { position: relative; height: 300px; }
.grid[class*="lg:grid-cols-1"] .chart-container { overflow: hidden; max-width: 100%; }
.grid[class*="lg:grid-cols-1"] > .bg-white.border.rounded-lg { overflow: hidden; min-width: 0; }
.grid[class*="lg:grid-cols-3"] .chart-container { overflow: hidden; max-width: 100%; }
.grid[class*="lg:grid-cols-3"] > .bg-white.border.rounded-lg { overflow: hidden; min-width: 0; }
.content-section .overflow-x-auto table { table-layout: fixed; width: 100%; }
#content-salientes .overflow-x-auto table th:nth-child(1),
#content-salientes .overflow-x-auto table td:nth-child(1) { width: 2rem; max-width: 2rem; padding-left: 0.5rem; padding-right: 0.5rem; }
#content-salientes .overflow-x-auto table th:nth-child(2),
#content-salientes .overflow-x-auto table td:nth-child(2) { width: 48%; }
#content-salientes .overflow-x-auto table th:nth-child(3),
#content-salientes .overflow-x-auto table td:nth-child(3) { width: 12%; }
#content-salientes .overflow-x-auto table th:nth-child(4),
#content-salientes .overflow-x-auto table td:nth-child(4) { width: 12%; }
#content-salientes .overflow-x-auto table th:nth-child(5),
#content-salientes .overflow-x-auto table td:nth-child(5) { width: 12%; }
#content-entrantes .overflow-x-auto table th:nth-child(1),
#content-entrantes .overflow-x-auto table td:nth-child(1),
#content-internas .overflow-x-auto table th:nth-child(1),
#content-internas .overflow-x-auto table td:nth-child(1) { width: 2rem; max-width: 2rem; padding-left: 0.5rem; padding-right: 0.5rem; }
#content-entrantes .overflow-x-auto table th:nth-child(2),
#content-entrantes .overflow-x-auto table td:nth-child(2),
#content-internas .overflow-x-auto table th:nth-child(2),
#content-internas .overflow-x-auto table td:nth-child(2) { width: 62%; }
#content-entrantes .overflow-x-auto table th:nth-child(3),
#content-entrantes .overflow-x-auto table td:nth-child(3),
#content-internas .overflow-x-auto table th:nth-child(3),
#content-internas .overflow-x-auto table td:nth-child(3) { width: 16%; }
#content-entrantes .overflow-x-auto table th:nth-child(4),
#content-entrantes .overflow-x-auto table td:nth-child(4),
#content-internas .overflow-x-auto table th:nth-child(4),
#content-internas .overflow-x-auto table td:nth-child(4) { width: 16%; }
.content-section .overflow-x-auto table td { word-wrap: break-word; overflow-wrap: break-word; }
.print-only { display: none !important; }
@media print {
@page {
size: A4 landscape;
margin: 15mm;
}
/* Tailwind .hidden va después en la cascada: forzar secciones visibles al imprimir */
body .content-section.hidden {
display: block !important;
visibility: visible !important;
opacity: 1 !important;
}
html, body,
.content-section, .content-section *,
.header-print, .header-print * {
-webkit-print-color-adjust: exact !important;
print-color-adjust: exact !important;
}
.no-print { display: none !important; }
.print-only { display: block !important; }
.content-section,
.content-section *,
.header-print,
.header-print * { box-shadow: none !important; }
.content-section {
display: block !important;
page-break-after: always;
margin-bottom: 20px;
}
.content-section:last-child {
page-break-after: auto;
}
body { background: white; }
.bg-gray-50 { background: white; }
html, body {
width: 100% !important;
max-width: 100% !important;
margin: 0 !important;
padding: 0 !important;
overflow: visible !important;
}
.container {
max-width: 100% !important;
width: 100% !important;
}
.overflow-x-auto {
overflow: visible !important;
width: 100% !important;
}
.chart-container,
.bg-white.border.rounded-lg {
page-break-inside: avoid !important;
}
.chart-container {
overflow: visible !important;
width: 100% !important;
position: relative !important;
}
/* Imagen sustituta del canvas (inyectada por JS); el canvas real suele salir en blanco en PDF */
.chart-container img.cdr-chart-print-img {
max-width: 100% !important;
height: auto !important;
display: block !important;
}
.chart-container canvas.cdr-canvas-oculto-impresion {
display: none !important;
}
.header-print {
page-break-after: always !important;
min-height: 150mm !important;
display: flex !important;
align-items: center !important;
justify-content: center !important;
}
.header-print img {
width: auto !important;
height: auto !important;
max-height: 52mm !important;
max-width: 72mm !important;
object-fit: contain !important;
object-position: center !important;
flex-shrink: 0 !important;
}
.content-section {
page-break-before: always !important;
}
.content-section:first-of-type {
page-break-before: auto !important;
}
.content-section > div > .grid.gap-4.mb-6:first-of-type,
.content-section .grid[class*="md:grid-cols-4"] {
grid-template-columns: repeat(4, 1fr) !important;
gap: 0.5rem !important;
margin-bottom: 1rem !important;
}
.content-section .grid[class*="md:grid-cols-4"] .bg-gradient-to-br {
padding: 0.5rem 0.75rem !important;
display: flex !important;
flex-direction: row !important;
align-items: center !important;
justify-content: space-between !important;
}
.content-section .grid[class*="md:grid-cols-4"] .bg-gradient-to-br .text-sm {
font-size: 0.85rem !important;
margin: 0 !important;
}
.content-section .grid[class*="md:grid-cols-4"] .bg-gradient-to-br .text-4xl {
font-size: 1.25rem !important;
margin: 0 !important;
}
.content-section .grid[class*="lg:grid-cols-1"] {
grid-template-columns: 1fr !important;
}
.content-section .grid[class*="lg:grid-cols-3"] {
grid-template-columns: repeat(3, 1fr) !important;
}
.content-section .grid[class*="lg:grid-cols-2"] {
grid-template-columns: 1fr !important;
page-break-before: always !important;
page-break-inside: avoid !important;
}
.content-section::before {
display: block;
font-size: 18px;
font-weight: bold;
color: #0f766e;
margin-bottom: 12px;
padding-bottom: 8px;
border-bottom: 2px solid #0f766e;
}
#content-salientes::before { content: "Llamadas Salientes"; }
#content-entrantes::before { content: "Llamadas Entrantes"; }
#content-internas::before { content: "Llamadas Internas"; }
.content-section > div { padding: 1rem !important; }
.content-section .grid[class*="lg:grid-cols-1"],
.content-section .grid[class*="lg:grid-cols-3"],
.content-section .grid[class*="lg:grid-cols-2"] {
gap: 0.75rem !important;
margin-bottom: 0.75rem !important;
}
.content-section .bg-white.border.rounded-lg { padding: 0.75rem !important; }
.content-section .bg-white.border.rounded-lg h3 {
font-size: 0.95rem !important;
margin-bottom: 0.5rem !important;
}
/* Gráfico de barras “por día”: más alto para ejes X, dos escalas Y y leyenda (antes 180px lo cortaba) */
.content-section .grid[class*="lg:grid-cols-1"] .chart-container {
height: auto !important;
min-height: 300px !important;
width: 100% !important;
max-width: 100% !important;
overflow: visible !important;
}
.content-section .grid[class*="lg:grid-cols-1"] > .bg-white.border.rounded-lg {
overflow: visible !important;
min-width: 0 !important;
}
.content-section .grid[class*="lg:grid-cols-3"] .chart-container {
height: auto !important;
min-height: 150px !important;
width: 100% !important;
max-width: 100% !important;
overflow: visible !important;
}
.content-section .grid[class*="lg:grid-cols-3"] > .bg-white.border.rounded-lg {
overflow: visible !important;
}
table {
font-size: 0.8rem !important;
table-layout: fixed !important;
}
table th, table td {
padding: 0.35rem 0.5rem !important;
word-wrap: break-word !important;
}
.content-section [id^="comentarios-"] {
font-size: 0.8rem !important;
line-height: 1.4 !important;
}
.text-gray-700 { color: #000 !important; }
.text-gray-600 { color: #333 !important; }
}
/*
* Modo captura: mismo aspecto que @media print en pantalla, para html2canvas → PDF.
* El informe vive en #cdr-captura-informe (fuera de vista mientras se captura).
*/
html.cdr-pdf-captura .no-print { display: none !important; }
html.cdr-pdf-captura .print-only { display: block !important; }
html.cdr-pdf-captura body { background: #fff !important; }
html.cdr-pdf-captura #cdr-captura-informe {
position: fixed;
left: -12000px;
top: 0;
width: 1123px;
max-width: 1123px;
background: #fff;
overflow: visible;
box-sizing: border-box;
}
html.cdr-pdf-captura #cdr-captura-informe,
html.cdr-pdf-captura #cdr-captura-informe * {
-webkit-print-color-adjust: exact !important;
print-color-adjust: exact !important;
}
html.cdr-pdf-captura #cdr-captura-informe .content-section.hidden {
display: block !important;
visibility: visible !important;
opacity: 1 !important;
}
html.cdr-pdf-captura #cdr-captura-informe .content-section,
html.cdr-pdf-captura #cdr-captura-informe .content-section *,
html.cdr-pdf-captura #cdr-captura-informe .header-print,
html.cdr-pdf-captura #cdr-captura-informe .header-print * { box-shadow: none !important; }
html.cdr-pdf-captura #cdr-captura-informe .content-section {
display: block !important;
margin-bottom: 20px;
}
html.cdr-pdf-captura #cdr-captura-informe .container {
max-width: 100% !important;
width: 100% !important;
}
html.cdr-pdf-captura #cdr-captura-informe .overflow-x-auto {
overflow: visible !important;
width: 100% !important;
}
html.cdr-pdf-captura #cdr-captura-informe .chart-container {
overflow: visible !important;
width: 100% !important;
position: relative !important;
}
html.cdr-pdf-captura #cdr-captura-informe .chart-container img.cdr-chart-print-img {
max-width: 100% !important;
height: auto !important;
display: block !important;
}
html.cdr-pdf-captura #cdr-captura-informe .chart-container canvas.cdr-canvas-oculto-impresion {
display: none !important;
}
html.cdr-pdf-captura #cdr-captura-informe .header-print {
min-height: 400px !important;
display: flex !important;
align-items: center !important;
justify-content: center !important;
margin-bottom: 0.5rem;
}
html.cdr-pdf-captura #cdr-captura-informe .header-print img {
width: auto !important;
height: auto !important;
max-height: 200px !important;
max-width: 280px !important;
object-fit: contain !important;
object-position: center !important;
flex-shrink: 0 !important;
}
html.cdr-pdf-captura #cdr-captura-informe .content-section > div > .grid.gap-4.mb-6:first-of-type,
html.cdr-pdf-captura #cdr-captura-informe .content-section .grid[class*="md:grid-cols-4"] {
grid-template-columns: repeat(4, 1fr) !important;
gap: 0.5rem !important;
margin-bottom: 1rem !important;
}
html.cdr-pdf-captura #cdr-captura-informe .content-section .grid[class*="md:grid-cols-4"] .bg-gradient-to-br {
padding: 0.5rem 0.75rem !important;
display: flex !important;
flex-direction: row !important;
align-items: center !important;
justify-content: space-between !important;
}
html.cdr-pdf-captura #cdr-captura-informe .content-section .grid[class*="md:grid-cols-4"] .bg-gradient-to-br .text-sm {
font-size: 0.85rem !important;
margin: 0 !important;
}
html.cdr-pdf-captura #cdr-captura-informe .content-section .grid[class*="md:grid-cols-4"] .bg-gradient-to-br .text-4xl {
font-size: 1.25rem !important;
margin: 0 !important;
}
html.cdr-pdf-captura #cdr-captura-informe .content-section .grid[class*="lg:grid-cols-1"] {
grid-template-columns: 1fr !important;
}
html.cdr-pdf-captura #cdr-captura-informe .content-section .grid[class*="lg:grid-cols-3"] {
grid-template-columns: repeat(3, 1fr) !important;
}
html.cdr-pdf-captura #cdr-captura-informe .content-section .grid[class*="lg:grid-cols-2"] {
grid-template-columns: 1fr !important;
}
html.cdr-pdf-captura #cdr-captura-informe .content-section::before {
display: block;
font-size: 18px;
font-weight: bold;
color: #0f766e;
margin-bottom: 12px;
padding-bottom: 8px;
border-bottom: 2px solid #0f766e;
}
html.cdr-pdf-captura #cdr-captura-informe #content-salientes::before { content: "Llamadas Salientes"; }
html.cdr-pdf-captura #cdr-captura-informe #content-entrantes::before { content: "Llamadas Entrantes"; }
html.cdr-pdf-captura #cdr-captura-informe #content-internas::before { content: "Llamadas Internas"; }
html.cdr-pdf-captura #cdr-captura-informe .content-section > div { padding: 1rem !important; }
html.cdr-pdf-captura #cdr-captura-informe .content-section .grid[class*="lg:grid-cols-1"],
html.cdr-pdf-captura #cdr-captura-informe .content-section .grid[class*="lg:grid-cols-3"],
html.cdr-pdf-captura #cdr-captura-informe .content-section .grid[class*="lg:grid-cols-2"] {
gap: 0.75rem !important;
margin-bottom: 0.75rem !important;
}
html.cdr-pdf-captura #cdr-captura-informe .content-section .bg-white.border.rounded-lg { padding: 0.75rem !important; }
html.cdr-pdf-captura #cdr-captura-informe .content-section .bg-white.border.rounded-lg h3 {
font-size: 0.95rem !important;
margin-bottom: 0.5rem !important;
}
html.cdr-pdf-captura #cdr-captura-informe .content-section .grid[class*="lg:grid-cols-1"] .chart-container {
height: auto !important;
min-height: 300px !important;
width: 100% !important;
max-width: 100% !important;
overflow: visible !important;
}
html.cdr-pdf-captura #cdr-captura-informe .content-section .grid[class*="lg:grid-cols-1"] > .bg-white.border.rounded-lg {
overflow: visible !important;
}
html.cdr-pdf-captura #cdr-captura-informe .content-section .grid[class*="lg:grid-cols-3"] .chart-container {
height: auto !important;
min-height: 150px !important;
width: 100% !important;
max-width: 100% !important;
overflow: visible !important;
}
html.cdr-pdf-captura #cdr-captura-informe .content-section .grid[class*="lg:grid-cols-3"] > .bg-white.border.rounded-lg {
overflow: visible !important;
}
html.cdr-pdf-captura #cdr-captura-informe table {
font-size: 0.8rem !important;
table-layout: fixed !important;
}
html.cdr-pdf-captura #cdr-captura-informe table th,
html.cdr-pdf-captura #cdr-captura-informe table td {
padding: 0.35rem 0.5rem !important;
word-wrap: break-word !important;
}
html.cdr-pdf-captura #cdr-captura-informe .content-section [id^="comentarios-"] {
font-size: 0.8rem !important;
line-height: 1.4 !important;
}
html.cdr-pdf-captura #cdr-captura-informe .text-gray-700 { color: #000 !important; }
html.cdr-pdf-captura #cdr-captura-informe .text-gray-600 { color: #333 !important; }
</style>
</head>
<body class="bg-gray-50">
<!-- Header pantalla -->
<header class="no-print bg-gradient-to-r from-teal-700 to-teal-600 text-white shadow-lg">
<div class="container mx-auto px-4">
<div class="flex justify-between items-center">
<!-- Logo Izquierdo -->
<div class="flex-shrink-0">
<img id="logo-left" src="images/logo-left.png" alt="Logo" class="w-auto"
onerror="this.style.display='none'" style="height: 120px; max-height: 120px; object-fit: contain;">
</div>
<!-- Título Central -->
<div class="flex-1 text-center px-4">
<h1 class="text-3xl font-bold">INFORME DE LLAMADAS</h1>
<p class="text-teal-100 mt-1">Dashboard de Reportes CDR</p>
</div>
<!-- Logo Derecho y Período -->
<div class="flex items-center space-x-6 flex-shrink-0">
<div class="text-right">
<div class="text-sm text-teal-100">Período</div>
<div id="periodo-actual" class="text-lg font-semibold">Cargando...</div>
</div>
<img id="logo-right" src="images/logo-right.png" alt="Logo" class="w-auto"
onerror="this.style.display='none'" style="height: 120px; max-height: 120px; object-fit: contain;">
</div>
</div>
</div>
</header>
<div id="cdr-captura-informe">
<header class="print-only header-print" style="background: #ffffff; color: #1e293b; padding: 0.75rem 0; margin-bottom: 0.5rem;">
<div class="container mx-auto px-4" style="max-width: 100%; width: 100%;">
<div class="flex justify-between items-center" style="display: flex; justify-content: space-between; align-items: center;">
<div class="flex-shrink-0">
<img class="header-print-logo" src="images/logo-left.png" alt="Logo" style="display: block;"
onerror="this.style.display='none'">
</div>
<div style="flex: 1; text-align: center; padding: 0 1rem;">
<h1 style="font-size: 1.875rem; font-weight: bold; margin: 0; color: #0f766e;">INFORME DE LLAMADAS</h1>
<p id="periodo-actual-print" style="margin: 0.35rem 0 0; font-size: 1.05rem; font-weight: 600; color: #334155;">-</p>
</div>
<div class="flex-shrink-0">
<img class="header-print-logo" src="images/logo-right.png" alt="Logo" style="display: block;"
onerror="this.style.display='none'">
</div>
</div>
</div>
</header>
<!-- Date Selector -->
<div class="container mx-auto px-4 py-4 no-print">
<div class="bg-white rounded-lg shadow p-4 flex items-center space-x-4">
<label class="font-medium text-gray-700">Seleccionar Período:</label>
<select id="mes-selector" class="border rounded px-3 py-2 text-gray-700" onchange="cargarDatos()">
<option value="01">Enero</option>
<option value="02">Febrero</option>
<option value="03">Marzo</option>
<option value="04">Abril</option>
<option value="05">Mayo</option>
<option value="06">Junio</option>
<option value="07">Julio</option>
<option value="08">Agosto</option>
<option value="09">Septiembre</option>
<option value="10">Octubre</option>
<option value="11">Noviembre</option>
<option value="12">Diciembre</option>
</select>
<select id="anio-selector" class="border rounded px-3 py-2 text-gray-700" onchange="cargarDatos()">
<!-- Se llenará dinámicamente con JavaScript -->
</select>
<button onclick="cargarDatos()" class="bg-blue-600 hover:bg-blue-700 text-white px-6 py-2 rounded-lg font-medium">
Actualizar
</button>
<button type="button" id="btn-descargar-pdf" onclick="generarPdfPorCaptura(this)" class="bg-gray-600 hover:bg-gray-700 text-white px-6 py-2 rounded-lg font-medium" title="Genera un PDF con capturas del informe (mismo aspecto que la vista de impresión)">
Descargar PDF
</button>
</div>
</div>
<!-- Tabs -->
<div class="container mx-auto px-4 mt-4 no-print">
<div class="bg-white rounded-t-lg shadow">
<div class="flex border-b">
<button onclick="cambiarTab('salientes')" id="tab-salientes" class="px-6 py-3 font-medium tab-active">
Llamadas Salientes
</button>
<button onclick="cambiarTab('entrantes')" id="tab-entrantes" class="px-6 py-3 font-medium text-gray-600 hover:text-blue-600">
Llamadas Entrantes
</button>
<button onclick="cambiarTab('internas')" id="tab-internas" class="px-6 py-3 font-medium text-gray-600 hover:text-blue-600">
Llamadas Internas
</button>
</div>
</div>
</div>
<!-- Content Sections -->
<div class="container mx-auto px-4 pb-8">
<!-- LLAMADAS SALIENTES -->
<div id="content-salientes" class="content-section">
<div class="bg-white rounded-b-lg shadow p-6">
<!-- KPIs -->
<div class="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6">
<div class="bg-gradient-to-br from-blue-500 to-blue-600 rounded-lg p-6 text-white">
<div class="text-sm opacity-90"># Extensiones</div>
<div id="sal-extensiones" class="text-4xl font-bold mt-2">0</div>
</div>
<div class="bg-gradient-to-br from-green-500 to-green-600 rounded-lg p-6 text-white">
<div class="text-sm opacity-90">Cant. Llamadas</div>
<div id="sal-llamadas" class="text-4xl font-bold mt-2">0</div>
</div>
<div class="bg-gradient-to-br from-purple-500 to-purple-600 rounded-lg p-6 text-white">
<div class="text-sm opacity-90">Minutos</div>
<div id="sal-minutos" class="text-4xl font-bold mt-2">0</div>
</div>
<div class="bg-gradient-to-br from-red-500 to-red-600 rounded-lg p-6 text-white">
<div class="text-sm opacity-90">Llamadas Perdidas</div>
<div id="sal-records" class="text-4xl font-bold mt-2">0</div>
</div>
</div>
<!-- Charts Row 1 -->
<div class="grid grid-cols-1 lg:grid-cols-1 gap-6 mb-6">
<div class="bg-white border rounded-lg p-6">
<h3 class="text-lg font-semibold text-gray-800 mb-4">LLAMADAS Y MINUTOS POR DÍA</h3>
<div class="chart-container" style="height: 400px;">
<canvas id="chart-sal-dias"></canvas>
</div>
</div>
</div>
<!-- Charts Row 2 -->
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-6">
<div class="bg-white border rounded-lg p-6">
<h3 class="text-lg font-semibold text-gray-800 mb-4">RESULTADO DE LAS LLAMADAS</h3>
<div class="chart-container" style="height: 250px;">
<canvas id="chart-sal-resultado"></canvas>
</div>
</div>
<div class="bg-white border rounded-lg p-6">
<h3 class="text-lg font-semibold text-gray-800 mb-4">DESTINOS</h3>
<div class="chart-container" style="height: 250px;">
<canvas id="chart-sal-destinos"></canvas>
</div>
</div>
<div class="bg-white border rounded-lg p-6">
<h3 class="text-lg font-semibold text-gray-800 mb-4">MINUTOS POR MES</h3>
<div class="chart-container" style="height: 250px;">
<canvas id="chart-sal-minutos"></canvas>
</div>
</div>
</div>
<!-- Tables Row -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<div class="bg-white border rounded-lg p-6">
<h3 class="text-lg font-semibold text-gray-800 mb-4">TOP DE LLAMADAS</h3>
<div class="overflow-x-auto">
<table class="w-full">
<thead class="bg-gray-50">
<tr>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">#</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Nombre</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Extensión</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Llamadas</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Minutos</th>
</tr>
</thead>
<tbody id="tabla-sal-top" class="divide-y divide-gray-200">
</tbody>
</table>
</div>
</div>
<div class="bg-white border rounded-lg p-6">
<h3 class="text-lg font-semibold text-gray-800 mb-4">COMENTARIOS</h3>
<div id="comentarios-salientes" class="text-gray-700 leading-relaxed text-sm">
Cargando análisis...
</div>
</div>
</div>
</div>
</div>
<!-- LLAMADAS ENTRANTES -->
<div id="content-entrantes" class="content-section hidden">
<div class="bg-white rounded-b-lg shadow p-6">
<!-- KPIs -->
<div class="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6">
<div class="bg-gradient-to-br from-blue-500 to-blue-600 rounded-lg p-6 text-white">
<div class="text-sm opacity-90"># Extensiones</div>
<div id="ent-extensiones" class="text-4xl font-bold mt-2">0</div>
</div>
<div class="bg-gradient-to-br from-green-500 to-green-600 rounded-lg p-6 text-white">
<div class="text-sm opacity-90">Cant. Llamadas</div>
<div id="ent-llamadas" class="text-4xl font-bold mt-2">0</div>
</div>
<div class="bg-gradient-to-br from-purple-500 to-purple-600 rounded-lg p-6 text-white">
<div class="text-sm opacity-90">Total Minutos</div>
<div id="ent-minutos" class="text-4xl font-bold mt-2">0</div>
</div>
<div class="bg-gradient-to-br from-red-500 to-red-600 rounded-lg p-6 text-white">
<div class="text-sm opacity-90">Llamadas Perdidas</div>
<div id="ent-records" class="text-4xl font-bold mt-2">0</div>
</div>
</div>
<!-- Charts -->
<div class="grid grid-cols-1 lg:grid-cols-1 gap-6 mb-6">
<div class="bg-white border rounded-lg p-6">
<h3 class="text-lg font-semibold text-gray-800 mb-4">LLAMADAS Y MINUTOS POR DÍA</h3>
<div class="chart-container" style="height: 400px;">
<canvas id="chart-ent-dias"></canvas>
</div>
</div>
</div>
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-6">
<div class="bg-white border rounded-lg p-6">
<h3 class="text-lg font-semibold text-gray-800 mb-4">RESULTADO DE LAS LLAMADAS</h3>
<div class="chart-container" style="height: 250px;">
<canvas id="chart-ent-resultado"></canvas>
</div>
</div>
<div class="bg-white border rounded-lg p-6">
<h3 class="text-lg font-semibold text-gray-800 mb-4">DESTINOS</h3>
<div class="chart-container" style="height: 250px;">
<canvas id="chart-ent-destinos"></canvas>
</div>
</div>
<div class="bg-white border rounded-lg p-6">
<h3 class="text-lg font-semibold text-gray-800 mb-4">MINUTOS POR MES</h3>
<div class="chart-container" style="height: 250px;">
<canvas id="chart-ent-minutos"></canvas>
</div>
</div>
</div>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<div class="bg-white border rounded-lg p-6">
<h3 class="text-lg font-semibold text-gray-800 mb-4">TOP DE LLAMADAS</h3>
<div class="overflow-x-auto">
<table class="w-full">
<thead class="bg-gray-50">
<tr>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">#</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Destino</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Llamadas</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Minutos</th>
</tr>
</thead>
<tbody id="tabla-ent-top" class="divide-y divide-gray-200">
</tbody>
</table>
</div>
</div>
<div class="bg-white border rounded-lg p-6">
<h3 class="text-lg font-semibold text-gray-800 mb-4">COMENTARIOS</h3>
<div id="comentarios-entrantes" class="text-gray-700 leading-relaxed text-sm">
Cargando análisis...
</div>
</div>
</div>
</div>
</div>
<!-- LLAMADAS INTERNAS -->
<div id="content-internas" class="content-section hidden">
<div class="bg-white rounded-b-lg shadow p-6">
<!-- KPIs -->
<div class="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6">
<div class="bg-gradient-to-br from-blue-500 to-blue-600 rounded-lg p-6 text-white">
<div class="text-sm opacity-90"># Extensiones</div>
<div id="int-extensiones" class="text-4xl font-bold mt-2">0</div>
</div>
<div class="bg-gradient-to-br from-green-500 to-green-600 rounded-lg p-6 text-white">
<div class="text-sm opacity-90">Cant. Llamadas</div>
<div id="int-llamadas" class="text-4xl font-bold mt-2">0</div>
</div>
<div class="bg-gradient-to-br from-purple-500 to-purple-600 rounded-lg p-6 text-white">
<div class="text-sm opacity-90">Total Minutos</div>
<div id="int-minutos" class="text-4xl font-bold mt-2">0</div>
</div>
<div class="bg-gradient-to-br from-red-500 to-red-600 rounded-lg p-6 text-white">
<div class="text-sm opacity-90">Llamadas Perdidas</div>
<div id="int-records" class="text-4xl font-bold mt-2">0</div>
</div>
</div>
<!-- Charts -->
<div class="grid grid-cols-1 lg:grid-cols-1 gap-6 mb-6">
<div class="bg-white border rounded-lg p-6">
<h3 class="text-lg font-semibold text-gray-800 mb-4">LLAMADAS Y MINUTOS POR DÍA</h3>
<div class="chart-container" style="height: 400px;">
<canvas id="chart-int-dias"></canvas>
</div>
</div>
</div>
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-6">
<div class="bg-white border rounded-lg p-6">
<h3 class="text-lg font-semibold text-gray-800 mb-4">RESULTADO DE LAS LLAMADAS</h3>
<div class="chart-container" style="height: 250px;">
<canvas id="chart-int-resultado"></canvas>
</div>
</div>
<div class="bg-white border rounded-lg p-6">
<h3 class="text-lg font-semibold text-gray-800 mb-4">DESTINOS</h3>
<div class="chart-container" style="height: 250px;">
<canvas id="chart-int-destinos"></canvas>
</div>
</div>
<div class="bg-white border rounded-lg p-6">
<h3 class="text-lg font-semibold text-gray-800 mb-4">MINUTOS POR MES</h3>
<div class="chart-container" style="height: 250px;">
<canvas id="chart-int-minutos"></canvas>
</div>
</div>
</div>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<div class="bg-white border rounded-lg p-6">
<h3 class="text-lg font-semibold text-gray-800 mb-4">TOP DE LLAMADAS</h3>
<div class="overflow-x-auto">
<table class="w-full">
<thead class="bg-gray-50">
<tr>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">#</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Origen</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Llamadas</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Minutos</th>
</tr>
</thead>
<tbody id="tabla-int-top" class="divide-y divide-gray-200">
</tbody>
</table>
</div>
</div>
<div class="bg-white border rounded-lg p-6">
<h3 class="text-lg font-semibold text-gray-800 mb-4">COMENTARIOS</h3>
<div id="comentarios-internas" class="text-gray-700 leading-relaxed text-sm">
Cargando análisis...
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script src="https://cdn.jsdelivr.net/npm/jspdf@2.5.2/dist/jspdf.umd.min.js" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script>
// Variables globales para los charts
let charts = {};
let datosGlobales = null;
/** Pares { canvas, img } para imprimir gráficos como PNG (evita canvas en blanco en PDF) */
let cdrChartPrintSnapshots = [];
function limpiarSnapshotsGraficosImpresion() {
cdrChartPrintSnapshots.forEach(({ canvas, img }) => {
if (img && img.parentNode) img.remove();
if (canvas) {
canvas.classList.remove('cdr-canvas-oculto-impresion');
canvas.style.display = '';
}
});
cdrChartPrintSnapshots = [];
}
/**
* Sustituye cada canvas de Chart por una imagen PNG antes de rasterizar impresión/PDF.
* Debe llamarse de forma síncrona tras resize/update, sin depender de setTimeout.
*/
function aplicarSnapshotsGraficosParaImpresion() {
limpiarSnapshotsGraficosImpresion();
Object.values(charts).forEach(chart => {
if (!chart || !chart.canvas) return;
const canvas = chart.canvas;
let dataUrl;
try {
dataUrl = canvas.toDataURL('image/png', 1.0);
} catch (e) {
console.warn('No se pudo exportar un gráfico para impresión', e);
return;
}
if (!dataUrl || dataUrl.length < 32) return;
const img = document.createElement('img');
img.className = 'cdr-chart-print-img';
img.alt = '';
img.src = dataUrl;
const rect = canvas.getBoundingClientRect();
const w = Math.max(1, Math.round(rect.width || canvas.offsetWidth || canvas.width));
img.style.width = w + 'px';
img.style.maxWidth = '100%';
img.style.height = 'auto';
const parent = canvas.parentNode;
if (!parent) return;
canvas.classList.add('cdr-canvas-oculto-impresion');
canvas.style.display = 'none';
parent.insertBefore(img, canvas);
cdrChartPrintSnapshots.push({ canvas, img });
});
}
function prepararGraficosParaImpresionSincrono() {
Object.values(charts).forEach(chart => {
if (!chart) return;
chart.options.devicePixelRatio = Math.min(2, window.devicePixelRatio || 2);
chart.resize();
chart.update('none');
});
}
// Configuración inicial de fechas
function inicializarFechas() {
const hoy = new Date();
const añoActual = hoy.getFullYear();
const mesActual = String(hoy.getMonth() + 1).padStart(2, '0');
// Llenar selector de años (desde 2020 hasta año actual + 1)
const selectorAnio = document.getElementById('anio-selector');
for (let año = 2020; año <= añoActual + 1; año++) {
const option = document.createElement('option');
option.value = año;
option.textContent = año;
if (año === añoActual) {
option.selected = true;
}
selectorAnio.appendChild(option);
}
// Establecer mes actual
document.getElementById('mes-selector').value = mesActual;
}
// Cambiar entre tabs
function cambiarTab(tipo) {
// Ocultar todos los contenidos
document.querySelectorAll('.content-section').forEach(el => el.classList.add('hidden'));
// Remover active de todos los tabs
document.querySelectorAll('[id^="tab-"]').forEach(el => {
el.classList.remove('tab-active');
el.classList.add('text-gray-600');
});
// Mostrar contenido seleccionado
document.getElementById('content-' + tipo).classList.remove('hidden');
// Activar tab seleccionado
const tab = document.getElementById('tab-' + tipo);
tab.classList.add('tab-active');
tab.classList.remove('text-gray-600');
}
// Cargar datos desde la API
async function cargarDatos() {
const mes = document.getElementById('mes-selector').value;
const año = document.getElementById('anio-selector').value;
if (!mes || !año) {
alert('Por favor selecciona mes y año');
return;
}
// Calcular primer y último día del mes seleccionado
const ultimoDia = new Date(parseInt(año), parseInt(mes), 0).getDate();
const fechaInicio = `${año}-${mes}-01`;
const fechaFin = `${año}-${mes}-${String(ultimoDia).padStart(2, '0')}`;
try {
const response = await fetch(`api.php?fecha_inicio=${fechaInicio}&fecha_fin=${fechaFin}`);
const datos = await response.json();
console.log('JSON recibido de la API:', datos);
if (datos.error) {
alert('Error: ' + datos.error);
return;
}
datosGlobales = datos;
actualizarPeriodo(datos.periodo);
renderizarSalientes(datos.salientes);
renderizarEntrantes(datos.entrantes);
renderizarInternas(datos.internas);
} catch (error) {
console.error('Error cargando datos:', error);
alert('Error al cargar los datos. Verifica la conexión con el servidor.');
}
}
function actualizarPeriodo(periodo) {
// Parsear fechas sin conversión de zona horaria
const formatearFecha = (fechaStr) => {
const [año, mes, dia] = fechaStr.split('-');
const meses = ['ene', 'feb', 'mar', 'abr', 'may', 'jun', 'jul', 'ago', 'sep', 'oct', 'nov', 'dic'];
return `${parseInt(dia)} ${meses[parseInt(mes) - 1]} ${año}`;
};
document.getElementById('periodo-actual').textContent =
`${formatearFecha(periodo.inicio)} - ${formatearFecha(periodo.fin)}`;
}
// Renderizar SALIENTES
function renderizarSalientes(datos) {
const stats = datos.stats;
const minutosReales = Math.round((stats.minutos_totales || 0) / 60);
document.getElementById('sal-extensiones').textContent = stats.extensiones || 0;
document.getElementById('sal-llamadas').textContent = stats.total_llamadas || 0;
document.getElementById('sal-minutos').textContent = minutosReales;
document.getElementById('sal-records').textContent = stats.no_contestadas || 0;
// Gráfico por días
renderizarGraficoDias('chart-sal-dias', datos.por_dia);
// Gráfico de resultado (donut)
renderizarGraficoResultado('chart-sal-resultado', stats);
// Gráfico de destinos
renderizarGraficoDestinos('chart-sal-destinos', datos.destinos);
// Gráfico de minutos por mes (bar)
renderizarGraficoMinutosMes('chart-sal-minutos', minutosReales);
// Tabla top
renderizarTablaTop('tabla-sal-top', datos.top_extensiones, true);
// Comentarios
generarComentariosSalientes(datos);
}
// Renderizar ENTRANTES
function renderizarEntrantes(datos) {
const stats = datos.stats;
const minutosReales = Math.round((stats.minutos_totales || 0) / 60);
document.getElementById('ent-extensiones').textContent = stats.extensiones || 0;
document.getElementById('ent-llamadas').textContent = stats.total_llamadas || 0;
document.getElementById('ent-minutos').textContent = minutosReales;
document.getElementById('ent-records').textContent = stats.no_contestadas || 0;
renderizarGraficoDias('chart-ent-dias', datos.por_dia);
renderizarGraficoResultado('chart-ent-resultado', stats);
renderizarGraficoDestinosEntrantes('chart-ent-destinos', datos.top_destinos);
renderizarGraficoMinutosMes('chart-ent-minutos', minutosReales);
renderizarTablaTopEntrantes('tabla-ent-top', datos.top_destinos);
generarComentariosEntrantes(datos);
}
// Renderizar INTERNAS
function renderizarInternas(datos) {
const stats = datos.stats;
const minutosReales = Math.round((stats.minutos_totales || 0) / 60);
document.getElementById('int-extensiones').textContent = stats.extensiones_origen || 0;
document.getElementById('int-llamadas').textContent = stats.total_llamadas || 0;
document.getElementById('int-minutos').textContent = minutosReales;
document.getElementById('int-records').textContent = stats.no_contestadas || 0;
renderizarGraficoDias('chart-int-dias', datos.por_dia);
renderizarGraficoResultado('chart-int-resultado', stats);
renderizarGraficoDestinosInternas('chart-int-destinos', datos.top_destinos);
renderizarGraficoMinutosMes('chart-int-minutos', minutosReales);
renderizarTablaTopInternas('tabla-int-top', datos.top_origen);
generarComentariosInternas(datos);
}
// Funciones de renderizado de gráficos
function renderizarGraficoDias(canvasId, datos) {
if (charts[canvasId]) charts[canvasId].destroy();
const labels = datos.map(d => {
// Parsear fecha como string sin conversión de zona horaria
const [año, mes, dia] = d.fecha.split('-');
const meses = ['ene', 'feb', 'mar', 'abr', 'may', 'jun', 'jul', 'ago', 'sep', 'oct', 'nov', 'dic'];
return parseInt(dia) + ' ' + meses[parseInt(mes) - 1];
});
const llamadas = datos.map(d => parseInt(d.llamadas));
const minutos = datos.map(d => Math.round(parseInt(d.minutos) / 60));
const ctx = document.getElementById(canvasId).getContext('2d');
charts[canvasId] = new Chart(ctx, {
type: 'bar',
data: {
labels: labels,
datasets: [
{
label: 'Llamadas',
data: llamadas,
backgroundColor: 'rgba(16, 185, 129, 0.7)',
borderColor: 'rgba(16, 185, 129, 1)',
borderWidth: 1,
yAxisID: 'y'
},
{
label: 'Minutos',
data: minutos,
type: 'line',
borderColor: 'rgba(59, 130, 246, 1)',
backgroundColor: 'rgba(59, 130, 246, 0.1)',
borderWidth: 2,
tension: 0.4,
yAxisID: 'y1'
}
]
},
options: {
responsive: true,
maintainAspectRatio: false,
interaction: {
mode: 'index',
intersect: false
},
scales: {
y: {
type: 'linear',
display: true,
position: 'left',
title: { display: true, text: 'Llamadas' }
},
y1: {
type: 'linear',
display: true,
position: 'right',
title: { display: true, text: 'Minutos' },
grid: { drawOnChartArea: false }
}
}
}
});
}
function renderizarGraficoResultado(canvasId, stats) {
if (charts[canvasId]) charts[canvasId].destroy();
const ctx = document.getElementById(canvasId).getContext('2d');
charts[canvasId] = new Chart(ctx, {
type: 'doughnut',
data: {
labels: ['ANSWERED', 'NO ANSWER', 'FAILED', 'BUSY'],
datasets: [{
data: [
stats.contestadas || 0,
stats.no_contestadas || 0,
stats.fallidas || 0,
stats.ocupadas || 0
],
backgroundColor: [
'rgba(59, 130, 246, 0.8)',
'rgba(16, 185, 129, 0.8)',
'rgba(239, 68, 68, 0.8)',
'rgba(245, 158, 11, 0.8)'
],
borderWidth: 2,
borderColor: '#fff'
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: { position: 'bottom' }
}
}
});
}
function renderizarGraficoDestinos(canvasId, destinos) {
if (charts[canvasId]) charts[canvasId].destroy();
const ctx = document.getElementById(canvasId).getContext('2d');
charts[canvasId] = new Chart(ctx, {
type: 'doughnut',
data: {
labels: destinos.map(d => d.destino),
datasets: [{
data: destinos.map(d => parseInt(d.llamadas)),
backgroundColor: [
'rgba(59, 130, 246, 0.8)',
'rgba(16, 185, 129, 0.8)',
'rgba(139, 92, 246, 0.8)',
'rgba(245, 158, 11, 0.8)',
'rgba(236, 72, 153, 0.8)'
],
borderWidth: 2,
borderColor: '#fff'
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: { position: 'bottom' }
}
}
});
}
function renderizarGraficoDestinosEntrantes(canvasId, destinos) {
if (charts[canvasId]) charts[canvasId].destroy();
const ctx = document.getElementById(canvasId).getContext('2d');
charts[canvasId] = new Chart(ctx, {
type: 'doughnut',
data: {
labels: destinos.map(d => 'Ext ' + d.extension),
datasets: [{
data: destinos.map(d => parseInt(d.llamadas)),
backgroundColor: [
'rgba(59, 130, 246, 0.8)',
'rgba(16, 185, 129, 0.8)',
'rgba(139, 92, 246, 0.8)',
'rgba(245, 158, 11, 0.8)',
'rgba(236, 72, 153, 0.8)'
],
borderWidth: 2,
borderColor: '#fff'
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: { position: 'bottom' }
}
}
});
}
function renderizarGraficoDestinosInternas(canvasId, destinos) {
renderizarGraficoDestinosEntrantes(canvasId, destinos);
}
function renderizarGraficoMinutosMes(canvasId, minutos) {
if (charts[canvasId]) charts[canvasId].destroy();
const meses = ['Enero', 'Feb', 'Marzo', 'Abril', 'Mayo', 'Junio', 'Julio', 'Agosto', 'Sept', 'Oct', 'Nov', 'Dic'];
const mesActual = meses[new Date().getMonth()];
const ctx = document.getElementById(canvasId).getContext('2d');
charts[canvasId] = new Chart(ctx, {
type: 'bar',
data: {
labels: [mesActual],
datasets: [{
label: 'Minutos',
data: [minutos],
backgroundColor: 'rgba(139, 92, 246, 0.8)',
borderColor: 'rgba(139, 92, 246, 1)',
borderWidth: 1
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: { display: false }
},
scales: {
y: {
beginAtZero: true,
title: { display: true, text: 'Minutos' }
}
}
}
});
}
function renderizarTablaTop(tablaId, datos, incluirNombre) {
const tbody = document.getElementById(tablaId);
tbody.innerHTML = '';
datos.forEach((item, index) => {
const row = document.createElement('tr');
const minutosReales = Math.round(parseInt(item.minutos || 0) / 60);
if (incluirNombre) {
row.innerHTML = `
<td class="px-4 py-3 text-sm">${index + 1}</td>
<td class="px-4 py-3 text-sm">${item.nombre || 'Sin nombre'}</td>
<td class="px-4 py-3 text-sm">${item.extension}</td>
<td class="px-4 py-3 text-sm font-medium text-blue-600">${item.llamadas}</td>
<td class="px-4 py-3 text-sm">${minutosReales}</td>
`;
} else {
row.innerHTML = `
<td class="px-4 py-3 text-sm">${index + 1}</td>
<td class="px-4 py-3 text-sm">${item.extension}</td>
<td class="px-4 py-3 text-sm font-medium text-blue-600">${item.llamadas}</td>
<td class="px-4 py-3 text-sm">${minutosReales}</td>
`;
}
tbody.appendChild(row);
});
}
function renderizarTablaTopEntrantes(tablaId, datos) {
const tbody = document.getElementById(tablaId);
tbody.innerHTML = '';
datos.forEach((item, index) => {
const row = document.createElement('tr');
const minutosReales = Math.round(parseInt(item.minutos || 0) / 60);
row.innerHTML = `
<td class="px-4 py-3 text-sm">${index + 1}</td>
<td class="px-4 py-3 text-sm">${item.extension}</td>
<td class="px-4 py-3 text-sm font-medium text-blue-600">${item.llamadas}</td>
<td class="px-4 py-3 text-sm">${minutosReales}</td>
`;
tbody.appendChild(row);
});
}
function renderizarTablaTopInternas(tablaId, datos) {
renderizarTablaTopEntrantes(tablaId, datos);
}
// Generar comentarios automáticos
function generarComentariosSalientes(datos) {
const stats = datos.stats;
const total = stats.total_llamadas || 0;
const contestadas = stats.contestadas || 0;
const noContestadas = stats.no_contestadas || 0;
const topExt = datos.top_extensiones[0];
let comentarios = '';
if (total === 0) {
comentarios = 'No se registraron llamadas salientes en este período.';
} else {
const tasaExito = ((contestadas / total) * 100).toFixed(1);
const tasaNoContestadas = ((noContestadas / total) * 100).toFixed(1);
if (topExt) {
comentarios = `La extensión que más llamadas salientes ha realizado es la ${topExt.extension}, con un total de ${topExt.llamadas} llamadas y un consumo de ${Math.round(topExt.minutos/60)} minutos. `;
}
comentarios += `De las llamadas salientes, el ${tasaExito}% fueron realizadas con éxito, mientras que el ${tasaNoContestadas}% no fueron contestadas por los usuarios. `;
if (tasaExito > 70) {
comentarios += 'Se identifica que hay un flujo normal de llamadas.';
} else if (tasaExito > 50) {
comentarios += 'Se recomienda revisar los horarios de llamadas para mejorar la tasa de respuesta.';
} else {
comentarios += 'Se detecta una tasa de respuesta baja, se recomienda revisar la estrategia de contacto.';
}
}
document.getElementById('comentarios-salientes').textContent = comentarios;
}
function generarComentariosEntrantes(datos) {
const stats = datos.stats;
const total = stats.total_llamadas || 0;
const topDest = datos.top_destinos[0];
let comentarios = '';
if (total === 0) {
comentarios = 'No se registraron llamadas entrantes en este período.';
} else {
if (topDest) {
const minutosReales = Math.round(parseInt(topDest.minutos) / 60);
comentarios = `Se evidenció que la extensión que más llamadas recibió fue la ${topDest.extension}, con un total de ${topDest.llamadas} llamadas y un consumo de ${minutosReales} minutos hablados. `;
}
const contestadas = stats.contestadas || 0;
const tasaRespuesta = ((contestadas / total) * 100).toFixed(1);
comentarios += `La tasa de respuesta general fue del ${tasaRespuesta}%.`;
if (tasaRespuesta < 70) {
comentarios += ' Se recomienda revisar la disponibilidad de agentes para mejorar la atención.';
}
}
document.getElementById('comentarios-entrantes').textContent = comentarios;
}
function generarComentariosInternas(datos) {
const stats = datos.stats;
const total = stats.total_llamadas || 0;
const topOrigen = datos.top_origen[0];
const topOrigen2 = datos.top_origen[1];
let comentarios = '';
if (total === 0) {
comentarios = 'No se registraron llamadas internas en este período.';
} else {
if (topOrigen) {
comentarios = `Se evidenció que la extensión ${topOrigen.extension} tiene el mayor número de llamadas internas con una totalidad de ${topOrigen.llamadas}. `;
}
if (topOrigen2) {
comentarios += `También se evidencia que la extensión ${topOrigen2.extension} presentó un alto flujo de llamadas internas con una totalidad de ${topOrigen2.llamadas}.`;
}
}
document.getElementById('comentarios-internas').textContent = comentarios;
}
function sincronizarPeriodoPrint() {
const periodo = document.getElementById('periodo-actual');
const periodoPrint = document.getElementById('periodo-actual-print');
if (periodo && periodoPrint) periodoPrint.textContent = periodo.textContent;
}
let seccionActivaAntesImprimir = null;
/**
* PDF por imágenes: replica estilos de impresión en pantalla (html.cdr-pdf-captura),
* captura cada bloque con html2canvas y monta un A4 apaisado con jsPDF.
* No se puede leer la vista previa del navegador; esto aproxima el mismo maquetado.
*/
async function generarPdfPorCaptura(btn) {
if (typeof html2canvas === 'undefined') {
alert('No se cargó html2canvas. Comprueba la conexión a internet (CDN).');
return;
}
const j = window.jspdf;
const JsPDF = (j && j.jsPDF) ? j.jsPDF : (j && j.default && j.default.jsPDF) ? j.default.jsPDF : (typeof window.jsPDF === 'function' ? window.jsPDF : null);
if (!JsPDF) {
alert('No se cargó jsPDF. Comprueba la conexión a internet (CDN) o bloqueos del navegador.');
return;
}
const mes = document.getElementById('mes-selector').value;
const año = document.getElementById('anio-selector').value;
if (!mes || !año) {
alert('Por favor selecciona mes y año');
return;
}
const elBtn = btn && btn.nodeType === 1 ? btn : document.getElementById('btn-descargar-pdf');
const labelOriginal = elBtn ? elBtn.textContent : 'Descargar PDF';
if (elBtn) {
elBtn.disabled = true;
elBtn.textContent = 'Generando PDF…';
}
const root = document.getElementById('cdr-captura-informe');
if (!root) {
alert('No se encontró el bloque del informe.');
if (elBtn) { elBtn.disabled = false; elBtn.textContent = labelOriginal; }
return;
}
seccionActivaAntesImprimir = document.querySelector('.content-section:not(.hidden)');
sincronizarPeriodoPrint();
document.querySelectorAll('.content-section').forEach(s => s.classList.remove('hidden'));
await new Promise(r => setTimeout(r, 500));
prepararGraficosParaImpresionSincrono();
await new Promise(r => requestAnimationFrame(() => requestAnimationFrame(r)));
prepararGraficosParaImpresionSincrono();
aplicarSnapshotsGraficosParaImpresion();
document.documentElement.classList.add('cdr-pdf-captura');
try {
await document.fonts.ready;
} catch (_) {}
await new Promise(r => setTimeout(r, 150));
await new Promise(r => requestAnimationFrame(() => requestAnimationFrame(r)));
const targets = [
root.querySelector('.header-print'),
document.getElementById('content-salientes'),
document.getElementById('content-entrantes'),
document.getElementById('content-internas')
].filter(Boolean);
const pdf = new JsPDF({ orientation: 'landscape', unit: 'mm', format: 'a4' });
const margin = 10;
const pageW = pdf.internal.pageSize.getWidth();
const pageH = pdf.internal.pageSize.getHeight();
const maxW = pageW - 2 * margin;
const maxH = pageH - 2 * margin;
try {
for (let i = 0; i < targets.length; i++) {
const el = targets[i];
const c = await html2canvas(el, {
scale: 2,
useCORS: true,
allowTaint: true,
backgroundColor: '#ffffff',
logging: false
});
const imgData = c.toDataURL('image/jpeg', 0.92);
const cw = c.width;
const ch = c.height;
const ar = ch / cw;
let wMm = maxW;
let hMm = wMm * ar;
if (hMm > maxH) {
hMm = maxH;
wMm = hMm / ar;
}
const x = margin + (maxW - wMm) / 2;
const y = margin + (maxH - hMm) / 2;
if (i > 0) pdf.addPage();
pdf.addImage(imgData, 'JPEG', x, y, wMm, hMm);
}
pdf.save(`informe-cdr-${año}-${mes}.pdf`);
} catch (err) {
console.error(err);
alert('Error al generar el PDF: ' + (err.message || err));
} finally {
document.documentElement.classList.remove('cdr-pdf-captura');
limpiarSnapshotsGraficosImpresion();
if (seccionActivaAntesImprimir) {
document.querySelectorAll('.content-section').forEach(section => {
if (section !== seccionActivaAntesImprimir) {
section.classList.add('hidden');
}
});
seccionActivaAntesImprimir = null;
}
Object.values(charts).forEach(chart => {
if (chart) {
chart.options.devicePixelRatio = undefined;
chart.resize();
chart.update('none');
}
});
if (elBtn) {
elBtn.disabled = false;
elBtn.textContent = labelOriginal;
}
}
}
window.addEventListener('keydown', function (e) {
if ((e.ctrlKey || e.metaKey) && String(e.key).toLowerCase() === 'p') {
e.preventDefault();
generarPdfPorCaptura(document.getElementById('btn-descargar-pdf'));
}
}, true);
window.addEventListener('beforeprint', () => {
limpiarSnapshotsGraficosImpresion();
sincronizarPeriodoPrint();
document.querySelectorAll('.content-section').forEach(s => s.classList.remove('hidden'));
prepararGraficosParaImpresionSincrono();
aplicarSnapshotsGraficosParaImpresion();
});
window.addEventListener('afterprint', () => {
limpiarSnapshotsGraficosImpresion();
if (seccionActivaAntesImprimir) {
document.querySelectorAll('.content-section').forEach(section => {
if (section !== seccionActivaAntesImprimir) {
section.classList.add('hidden');
}
});
seccionActivaAntesImprimir = null;
}
Object.values(charts).forEach(chart => {
if (chart) {
chart.options.devicePixelRatio = undefined;
chart.resize();
chart.update('none');
}
});
});
// Inicialización
window.addEventListener('load', () => {
inicializarFechas();
cargarDatos();
});
</script>
</body>
</html>