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
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>
|