{% extends "@UVDeskCoreFramework//Templates//layout.html.twig" %}
{% block title %}Dashboard{% endblock %}
{% block pageContent %}
<style>
.btn{
border: 1px solid #1a1a1a;
display: inline-block;
padding: 10px;
position: relative;
text-align: center;
transition: background 600ms ease, color 600ms ease;
margin-right: 0px !important;
}
input[type="radio"].toggle {
display: none;
}
input[type=radio]:checked + label {
background: blue;
color:white;
}
.uv-action-bar .uv-field-block.date {
display: inline-block;
margin-right: 8px;
}
.uv-action-bar label {
font-size: 16px;
vertical-align: middle;
margin-right: 10px;
}
.uv-inner-section .uv-action-bar label{
font-size: 15px;
}
@media screen and (min-width: 1100px) and (max-width: 1260px) {
.uv-inner-section .uv-action-bar .uv-action-bar-col-lt, .uv-inner-section .uv-action-bar .uv-action-bar-col-rt {
width: 55% !important;
}
}
.graficosTortaBarra{
font-size:11px;width: 520px;border:1px solid #d3d3d3;height: 300px;padding:10px;
}
.graficosTortaBarra label{
line-height: 2;
padding: 0 8px;
display: inline-block;
}
.boch {
display:inline-block;
height: 10px;
width: 10px;
margin-right: 3px;
border-radius: 50%;
}
.box-graficos {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.uv-activity-wrapper {
margin-top: 60px;
}
.uv-activity-wrapper .uv-activity-chart-col-lt {
width: 80%;
float: left;
}
ul.uv-activity-brick-wrapper {
list-style: none;
margin: 0;
padding: 0;
width: 100%;
display: inline-block;
}
ul.uv-activity-brick-wrapper li {
width: 25%;
display: inline-block;
float: left;
padding-left: 10px;
padding-right: 10px;
color: #fff;
}
ul.uv-activity-brick-wrapper .uv-activity-brick {
border-radius: 3px;
padding: 10px;
text-align: center;
}
ul.uv-activity-brick-wrapper li a {
color: #fff;
font-size: 45px;
width: 100%;
display: inline-block;
}
ul.uv-activity-brick-wrapper li label {
font-size: 18px;
width: 100%;
display: inline-block;
}
.uv-activity-chart-bottom-row .uv-pannel-body {
height: 450px;
}
.kudos-overview {
width: 40%;
float: left;
padding-right: 10px;
}
.recent-notification {
width: 30%;
float: left;
padding-left: 10px;
}
.completion-chart {
width: 300px;
margin: 0 auto;
}
.progress-meter .background {
fill: #EFEFEF;
}
.progress-meter text {
font-size: 30px;
}
.kudos-overview .uv-pannel-body {
text-align: center;
padding-top: 50px;
}
.kudos-overview .uv-pannel-body label {
margin-top: 10px;
display: inline-block;
}
.recent-notification ul {
list-style: none;
padding: 0;
margin: 0;
overflow-y: auto;
max-height: 400px !important;
}
.recent-notification .uv-pannel-body {
padding: 0;
}
.recent-notification ul li {
color: #333333;
border-bottom: solid 1px #D3D3D3;
padding: 15px 20px;
}
.recent-notification ul li:first-child {
border-top: none;
}
.recent-notification ul li:last-child {
border-bottom: none;
}
.recent-notification ul li * {
display: inline-block !important;
}
.recent-notification ul li .timeago {
color: #9E9E9E;
margin-top: 5px;
font-size: 13px;
}
.recent-notification label {
text-align: center;
display: inline-block;
width: 100%;
padding-top: 15px;
border-top: 1px solid #d3d3d3;
}
.recent-notification span.uv-notification-message {
float: left;
width: 100%;
}
.kudos-count {
width: 30%;
float: left;
padding-right: 10px;
padding-left: 10px;
}
.kudos-count .uv-pannel-body {
padding-top: 50px;
overflow-y: auto;
}
.kudos-count ul {
list-style: none;
padding: 0;
margin: 0;
}
.kudos-count ul li {
width: 100%;
display: inline-block;
padding: 15px 0;
}
.kudos-count ul li .uv-icon-kudos {
vertical-align: middle;
margin-right: 10px;
}
.uv-activity-wrapper .uv-activity-chart-col-rt {
width: 20%;
float: left;
}
.uv-activity-chart-col-rt ul {
padding: 0;
margin: 0;
list-style: none;
}
.uv-activity-chart-col-rt ul li {
margin-bottom: 10px
}
.uv-activity-chart-col-rt ul li span {
width: 100%;
display: inline-block;
color: #6f6f6f;
}
.uv-middle {
margin: 0 auto;
display: inline-block;
margin-top: 200px;
text-align: center;
width: 100%;
}
@media screen and (max-width: 1024px) {
.uv-activity-wrapper .uv-activity-chart-col-lt {
width: 100%;
padding: 0;
}
.uv-activity-wrapper .uv-activity-chart-col-rt {
width: 100%;
}
.kudos-overview {
width: 100%;
padding: 0;
}
.kudos-count {
width: 100%;
padding: 0;
}
.recent-notification {
width: 100%;
padding: 0;
}
ul.uv-activity-brick-wrapper li {
width: 50%;
margin: 10px 0;
}
}
@media screen and (max-width: 768px) {
ul.uv-activity-brick-wrapper li {
width: 100%;
}
}
@media screen and (max-width: 467px) {
.completion-chart {
width: 100%;
}
}
span.uv-notification-message a:link, span.uv-notification-message a:visited, label a:link, label a:visited {
color: #2750C4;
font-size: 15px;
}
.uv-mob-aside {
display: none;
}
{# .uv-copyright {
text-align: center;
} #}
/* Estilos para KPI Cards */
.kpi-cards {
display: flex;
flex-wrap: wrap;
gap: 20px;
margin: 20px 0;
padding: 0;
}
.kpi-card {
flex: 1;
min-width: 200px;
background: #fff;
border: 1px solid #d3d3d3;
border-radius: 8px;
padding: 20px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.kpi-card h3 {
margin: 0 0 10px 0;
font-size: 14px;
color: #666;
text-transform: uppercase;
}
.kpi-card .value {
font-size: 32px;
font-weight: bold;
color: #333;
margin: 10px 0;
}
.kpi-card .label {
font-size: 12px;
color: #999;
}
/* Estilos para gráficos mejorados */
.chart-container {
background: #fff;
border: 1px solid #d3d3d3;
border-radius: 8px;
padding: 20px;
margin: 20px 0;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.chart-container h3 {
margin: 0 0 20px 0;
font-size: 18px;
color: #333;
}
.table-container {
background: #fff;
border: 1px solid #d3d3d3;
border-radius: 8px;
padding: 20px;
margin: 20px 0;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.table-container h3 {
margin: 0 0 20px 0;
font-size: 18px;
color: #333;
}
.table-container table {
width: 100%;
border-collapse: collapse;
}
.table-container table th,
.table-container table td {
padding: 12px;
text-align: left;
border-bottom: 1px solid #ddd;
}
.table-container table th {
background-color: #f5f5f5;
font-weight: bold;
}
</style>
<div class="uv-area">
<div>
<div class="uv-action-bar">
<div class="uv-action-bar-col-lt">
{# Botón para cargar Excel #}
<div style="display: inline-block; margin-right: 20px; vertical-align: top;">
<label style="display: block; margin-bottom: 5px;">{{ 'Cargar Excel'|trans }}</label>
<input type="file" id="excelFileInput" accept=".xlsx,.xls,.csv" style="display: none;">
<button type="button" id="btnUploadExcel" class="btn" style="background: #4caf50; color: white; border: none; padding: 8px 15px; cursor: pointer;">
{{ 'Subir Excel'|trans }}
</button>
<button type="button" id="btnClearExcel" class="btn" style="background: #f44336; color: white; border: none; padding: 8px 15px; cursor: pointer; margin-left: 10px; display: none;">
{{ 'Usar BD'|trans }}
</button>
<span id="excelStatus" style="margin-left: 10px; color: #4caf50; font-weight: bold;"></span>
</div>
<label>{{ 'From'|trans }}</label>
<div class="uv-field-block date">
<input type="text" class="uv-field uv-date-picker date date-from" id="from" value="">
</div>
<label>{{ 'To'|trans }}</label>
<div class="uv-field-block date">
<input type="text" class="uv-field uv-date-picker date date-to" id="to" value="">
</div>
<input value="year" type="radio" class="toggle " name="options" id="year" autocomplete="off" >
<label class="btn" for="year">Last Year</label>
<input value="month" type="radio" class="toggle " name="options" id="month" autocomplete="off">
<label class="btn" for="month">Last Month</label>
<input value="week" type="radio" class="toggle " name="options" id="week" autocomplete="off" >
<label class="btn" for="week">Last Week</label>
<input value="day" type="radio" class="toggle " name="options" id="day" autocomplete="off" >
<label class="btn" for="day">Last Day</label>
</div>
{# <input value="year" type="radio" class="toggle " name="options" id="year" autocomplete="off" >
<label class="btn" for="year">Last Year</label>
<input value="month" type="radio" class="toggle " name="options" id="month" autocomplete="off">
<label class="btn" for="month">Last Month</label>
<input value="week" type="radio" class="toggle " name="options" id="week" autocomplete="off" >
<label class="btn" for="week">Last Week</label>
<input value="day" type="radio" class="toggle " name="options" id="day" autocomplete="off" >
<label class="btn" for="day">Last Day</label>#}
</div>
{# Sección de KPIs - Estilo Looker Studio #}
<div class="kpi-cards" id="kpiCards" style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 20px; margin: 20px 0;">
<div class="kpi-card" style="background: #4caf50; color: white;">
<h3 style="color: white;">{{ 'Fecha Último Ticket'|trans }}</h3>
<div class="value" id="kpiLastTicketDate" style="color: white;">-</div>
</div>
<div class="kpi-card" style="background: white;">
<h3>{{ 'Menos de 5 días'|trans }}</h3>
<div class="value" id="kpiLessThan5Days">-</div>
</div>
<div class="kpi-card">
<h3>{{ 'Total Tickets'|trans }}</h3>
<div class="value" id="kpiTotalTickets">-</div>
<div class="label">{{ 'In selected period'|trans }}</div>
</div>
<div class="kpi-card">
<h3>{{ 'Resolved Tickets'|trans }}</h3>
<div class="value" id="kpiResolvedTickets">-</div>
<div class="label">{{ 'Successfully closed'|trans }}</div>
</div>
<div class="kpi-card">
<h3>{{ 'Open Tickets'|trans }}</h3>
<div class="value" id="kpiOpenTickets">-</div>
<div class="label">{{ 'Currently active'|trans }}</div>
</div>
</div>
{# Layout tipo Looker Studio - Grid de 3 columnas #}
<div style="display: grid; grid-template-columns: repeat(3, 1fr); gap: 20px; margin: 20px 0;">
{# Columna 1: Tabla de Agentes #}
<div class="table-container">
<h3>{{ 'AGENT NAME'|trans }}</h3>
<table id="agentsTable">
<thead>
<tr>
<th>{{ 'Agent'|trans }}</th>
<th>{{ 'Records'|trans }}</th>
</tr>
</thead>
<tbody id="agentsTableBody">
<tr><td colspan="2">{{ 'Loading...'|trans }}</td></tr>
</tbody>
</table>
</div>
{# Columna 2: Gráfico de Tipos y Pie Chart #}
<div>
<div class="chart-container">
<h3>{{ 'Tipo de Tickets'|trans }}</h3>
<canvas id="ticketsByTypeStackedChart" height="200"></canvas>
</div>
<div class="chart-container" style="margin-top: 20px;">
<h3>{{ 'Tipos de Incidentes'|trans }}</h3>
<canvas id="incidentTypesChart" height="200"></canvas>
</div>
</div>
{# Columna 3: Tabla de Clientes #}
<div class="table-container">
<h3>{{ 'CUSTOMER NAME'|trans }}</h3>
<table id="customersTable">
<thead>
<tr>
<th>{{ 'Customer'|trans }}</th>
<th>{{ 'Records'|trans }}</th>
</tr>
</thead>
<tbody id="customersTableBody">
<tr><td colspan="2">{{ 'Loading...'|trans }}</td></tr>
</tbody>
</table>
</div>
</div>
{# Gráfico de Tickets por Mes #}
<div class="chart-container">
<h3>{{ 'Tickets por Mes'|trans }}</h3>
<canvas id="ticketsByMonthChart" height="80"></canvas>
</div>
{# Gráfico de Tiempo de Cierre #}
<div class="chart-container">
<h3>{{ 'Tiempo Cierre Tickets'|trans }}</h3>
<canvas id="closureTimeChart" height="80"></canvas>
</div>
<div class="box-graficos">
<div>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-bar-chart" viewBox="0 0 16 16">
<path d="M4 11H2v3h2v-3zm5-4H7v7h2V7zm5-5v12h-2V2h2zm-2-1a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h2a1 1 0 0 0 1-1V2a1 1 0 0 0-1-1h-2zM6 7a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1v7a1 1 0 0 1-1 1H7a1 1 0 0 1-1-1V7zm-5 4a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1v3a1 1 0 0 1-1 1H2a1 1 0 0 1-1-1v-3z"/>
</svg>
<b>{{ 'Tickets by Status' |trans }}</b><br><br>
<div class="graficosTortaBarra" >
<span id="contenidoticketsByStatus">
{% for tStatus in report_service.ticketsByStatus %}
<label>
<span class='boch' style="background: {{ tStatus.Color }}"> </span>
({{ tStatus.Cantidad }}) - {{ tStatus.Nombre }}
</label>
{% endfor %}
</span>
<div style="padding:25px;" >
<canvas id="ticketsByStatus" width="700" height="350"></canvas>
</div>
</div>
</div>
<div>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-bar-chart" viewBox="0 0 16 16">
<path d="M4 11H2v3h2v-3zm5-4H7v7h2V7zm5-5v12h-2V2h2zm-2-1a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h2a1 1 0 0 0 1-1V2a1 1 0 0 0-1-1h-2zM6 7a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1v7a1 1 0 0 1-1 1H7a1 1 0 0 1-1-1V7zm-5 4a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1v3a1 1 0 0 1-1 1H2a1 1 0 0 1-1-1v-3z"/>
</svg>
<b>{{ 'Tickets by Type' |trans }}</b><br><br>
<div class="graficosTortaBarra" >
<span id="contenidoticketsByType">
{% for tStatus in report_service.ticketsByType %}
<label>
<span class='boch' style="background: {{ tStatus.Color }}"> </span>
({{ tStatus.Cantidad }}) - {{ tStatus.Nombre }}
</label>
{% endfor %}
</span>
<div style="width: 220px; margin: 0 auto;padding:25px;">
<canvas id="ticketsByType" ></canvas>
</div>
</div>
</div>
<div>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-bar-chart" viewBox="0 0 16 16">
<path d="M4 11H2v3h2v-3zm5-4H7v7h2V7zm5-5v12h-2V2h2zm-2-1a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h2a1 1 0 0 0 1-1V2a1 1 0 0 0-1-1h-2zM6 7a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1v7a1 1 0 0 1-1 1H7a1 1 0 0 1-1-1V7zm-5 4a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1v3a1 1 0 0 1-1 1H2a1 1 0 0 1-1-1v-3z"/>
</svg>
<b>{{ 'Tickets by Priority' |trans }}</b><br><br>
<div class="graficosTortaBarra" >
<span id="contenidoticketsByPriority">
{% for tPriority in report_service.ticketsByPriority %}
<label>
<span class='boch' style="background: {{ tPriority.Color }}"> </span>
({{ tPriority.Cantidad }}) - {{ tPriority.Nombre }}
</label>
{% endfor %}
</span>
<div style="padding:25px;">
<canvas id="ticketsByPriority" width="700" height="350" ></canvas>
</div>
</div>
</div>
<div>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-bar-chart" viewBox="0 0 16 16">
<path d="M4 11H2v3h2v-3zm5-4H7v7h2V7zm5-5v12h-2V2h2zm-2-1a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h2a1 1 0 0 0 1-1V2a1 1 0 0 0-1-1h-2zM6 7a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1v7a1 1 0 0 1-1 1H7a1 1 0 0 1-1-1V7zm-5 4a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1v3a1 1 0 0 1-1 1H2a1 1 0 0 1-1-1v-3z"/>
</svg>
<b>{{ 'Tickets per Agent' |trans }}</b><br><br>
<div class="graficosTortaBarra" >
<span id="contenidoticketsPerAgent">
{% for tAgent in report_service.ticketsPerAgent %}
<label>
<span class='boch' style="background: {{ tAgent.Color }}"> </span>
({{ tAgent.Cantidad }}) - {{ tAgent.Nombre }}
</label>
{% endfor %}
</span>
<div style="padding:25px;width: 220px; margin: 0 auto;">
<canvas id="ticketsPerAgent" ></canvas>
</div>
</div>
</div>
<div>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-bar-chart" viewBox="0 0 16 16">
<path d="M4 11H2v3h2v-3zm5-4H7v7h2V7zm5-5v12h-2V2h2zm-2-1a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h2a1 1 0 0 0 1-1V2a1 1 0 0 0-1-1h-2zM6 7a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1v7a1 1 0 0 1-1 1H7a1 1 0 0 1-1-1V7zm-5 4a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1v3a1 1 0 0 1-1 1H2a1 1 0 0 1-1-1v-3z"/>
</svg>
<b>{{ 'Tickets per Team' |trans }}</b><br><br>
<div class="graficosTortaBarra" >
<span id="contenidoticketsPerTeam">
{% for tTeam in report_service.ticketsPerTeam %}
<label>
<span class='boch' style="background: {{ tTeam.Color }}"> </span>
({{ tTeam.Cantidad }}) - {{ tTeam.Nombre }}
</label>
{% endfor %}
</span>
<div style="padding:25px;width: 220px; margin: 0 auto;">
<canvas id="ticketsPerTeam" ></canvas>
</div>
</div>
</div>
<div>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-bar-chart" viewBox="0 0 16 16">
<path d="M4 11H2v3h2v-3zm5-4H7v7h2V7zm5-5v12h-2V2h2zm-2-1a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h2a1 1 0 0 0 1-1V2a1 1 0 0 0-1-1h-2zM6 7a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1v7a1 1 0 0 1-1 1H7a1 1 0 0 1-1-1V7zm-5 4a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1v3a1 1 0 0 1-1 1H2a1 1 0 0 1-1-1v-3z"/>
</svg>
<b>{{ 'Kudos' |trans }}</b><br><br>
<div class="graficosTortaBarra" >
<span id="contenidokudos">
{% for tKudos in report_service.Kudos %}
<label>
<span class='boch' style="background: {{ tKudos.Color }}"> </span>
({{ tKudos.Cantidad }}) - {{ tKudos.Nombre }}
</label>
{% endfor %}
</span>
<div style="padding:25px;">
<canvas id="kudos" width="700" height="350" ></canvas>
</div>
</div>
</div>
</div>
</div>
<div class="uv-copyright">
<span class="uv-credit-text">Powered by <a href="https://www.uvdesk.com" target="_blank">UVdesk</a></span>
</div>
</div>
{% endblock %}
{% block footer %}
{{ parent() }}
<script src="https://cdn.jsdelivr.net/npm/chart.js@3.9.1/dist/chart.min.js"></script>
<script>
let startGlobalDate = "";
let endGlobalDate = "";
let date = new Date();
let currentMonth = date.getMonth();
let currentDate = date.getDate();
let currentYear = date.getFullYear();
//Filtros año, mes, semana, dia
$("input[type='radio']").click(function(){
currentDate = new Date();
start = new Date();
end = new Date();
$('.uv-action-bar .date').find("#to").val(currentDate.getFullYear() + "-" + (currentDate.getMonth()+1) + "-" + currentDate.getDate());
var hasta = new Date($('.uv-action-bar .date').find("#to").val());
switch ($('input[name="options"]:checked').val()) {
case "year":
start.setFullYear(currentDate.getFullYear() - 1);
start.setMonth(0);
start.setDate(1);
start.setHours(0, 0, 0, 0);
end.setFullYear(currentDate.getFullYear() - 1);
end.setMonth(11);
end.setDate(new Date(currentDate.getFullYear() - 1, 12, 0).getDate())
end.setHours(0, 0, 0, 0);
datestringStart = start.getFullYear() + "-" + (start.getMonth()+1) + "-" + start.getDate();
datestringEnd = end.getFullYear() + "-" + (end.getMonth()+1) + "-" + end.getDate();
$('.uv-action-bar .date').find("#from").val(datestringStart)
$('.uv-action-bar .date').find("#to").val(datestringEnd).trigger('change');
break;
case "month":
start.setMonth(currentDate.getMonth() - 1);
start.setDate(1);
start.setHours(0, 0, 0, 0);
end.setMonth(currentDate.getMonth() - 1);
end.setDate(new Date(end.getFullYear(), end.getMonth(), 0).getDate())
end.setHours(0, 0, 0, 0);
datestringStart = start.getFullYear() + "-" + (start.getMonth()+1) + "-" + start.getDate();
datestringEnd = end.getFullYear() + "-" + (end.getMonth()+1) + "-" + end.getDate();
$('.uv-action-bar .date').find("#from").val(datestringStart);
$('.uv-action-bar .date').find("#to").val(datestringEnd).trigger('change');
break;
case "week":
var beforeOneWeek = new Date(new Date().getTime() - 60 * 60 * 24 * 7 * 1000)
var beforeOneWeek2 = new Date(beforeOneWeek);
day = beforeOneWeek.getDay()
diffToMonday = beforeOneWeek.getDate() - day + (day === 0 ? -6 : 1)
lastMonday = new Date(beforeOneWeek.setDate(diffToMonday))
lastSunday = new Date(beforeOneWeek2.setDate(diffToMonday + 6));
datestringStart = lastMonday.getFullYear() + "-" + (lastMonday.getMonth()+1) + "-" + lastMonday.getDate();
datestringEnd = lastSunday.getFullYear() + "-" + (lastSunday.getMonth()+1) + "-" + lastSunday.getDate();
$('.uv-action-bar .date').find("#from").val(datestringStart);
$('.uv-action-bar .date').find("#to").val(datestringEnd).trigger('change');
break;
case "day":
start.setFullYear(hasta.getFullYear());
start.setMonth(hasta.getMonth());
start.setDate(hasta.getDate());
start.setHours(0, 0, 0, 0);
datestring = start.getFullYear() + "-" + (start.getMonth()+1) + "-" + start.getDate();
$('.uv-action-bar .date').find("#from").val(datestring);
$('.uv-action-bar .date').find("#to").val(datestring).trigger('change');
break;
}
})
let ticketsByStatus = {{report_service.ticketsByStatus|json_encode|raw}};
let ticketsByType = {{report_service.ticketsByType|json_encode|raw}};
let ticketsByPriority = {{report_service.ticketsByPriority|json_encode|raw}};
let ticketsPerAgent = {{report_service.ticketsPerAgent|json_encode|raw}};
let ticketsPerTeam = {{report_service.ticketsPerTeam|json_encode|raw}};
let kudos = {{report_service.kudos|json_encode|raw}};
$('.uv-action-bar .date #from').datetimepicker({
maxDate: new Date(currentYear, currentMonth, currentDate),
format: 'YYYY-MM-DD',
}).on('dp.change', function(e) {
filtroFechas();
});
$('.uv-action-bar .date #to').datetimepicker({
maxDate: new Date(currentYear, currentMonth, currentDate),
format: 'YYYY-MM-DD',
}).on('dp.change', function(e) {
filtroFechas();
});
const filtroFechas = () => {
const fromDate = $('.uv-action-bar .date').find("#from").val();
const toDate = $('.uv-action-bar .date').find("#to").val();
// Validar que las fechas estén presentes
if (!fromDate || !toDate) {
console.warn('Fechas no definidas:', fromDate, toDate);
return;
}
console.log('Enviando fechas al servidor:', fromDate, toDate);
$.ajax({
type: "POST",
url: "{{ path('helpdesk_member_report_charts_xhr') }}",
data: {
'from': fromDate,
'to': toDate
},
dataType: "json",
success: function(msg){
console.log('Respuesta del servidor:', msg);
// Actualizar datos
ticketsByStatus = msg['ticketsByStatus'] || {};
ticketsByType = msg['ticketsByType'] || {};
ticketsByPriority = msg['ticketsByPriority'] || {};
ticketsPerAgent = msg['ticketsPerAgent'] || {};
ticketsPerTeam = msg['ticketsPerTeam'] || {};
kudos = msg['kudos'] || {};
// Reinicializar gráficos si no existen o si los datos cambiaron
if (!graficoTicketByStatus && Object.keys(ticketsByStatus).length > 0) {
graficoTicketByStatus = crearGrafico(ticketsByStatus, 'ticketsByStatus');
}
if (!graficoTicketByType && Object.keys(ticketsByType).length > 0) {
graficoTicketByType = crearGrafico(ticketsByType, 'ticketsByType');
}
if (!graficoTicketByPriority && Object.keys(ticketsByPriority).length > 0) {
graficoTicketByPriority = crearGrafico(ticketsByPriority, 'ticketsByPriority');
}
if (!graficoPerAgent && Object.keys(ticketsPerAgent).length > 0) {
graficoPerAgent = crearGrafico(ticketsPerAgent, 'ticketsPerAgent');
}
if (!graficoPerTeam && Object.keys(ticketsPerTeam).length > 0) {
graficoPerTeam = crearGrafico(ticketsPerTeam, 'ticketsPerTeam');
}
if (!graficoKudos && Object.keys(kudos).length > 0) {
graficoKudos = crearGrafico(kudos, 'kudos');
}
// Actualizar gráficos existentes
actualizarGraficos();
// Actualizar nuevas métricas
if (msg['kpiSummary']) {
actualizarKPIs(msg['kpiSummary'], msg['lastTicketDate'], msg['ticketsLessThan5Days']);
} else {
actualizarKPIs(null, msg['lastTicketDate'], msg['ticketsLessThan5Days']);
}
// Tablas
if (msg['agentsWithCount']) {
console.log('Actualizando agentes:', msg['agentsWithCount']);
actualizarAgentes(msg['agentsWithCount']);
} else {
console.log('No hay datos de agentes');
}
if (msg['customersWithCount']) {
console.log('Actualizando clientes:', msg['customersWithCount']);
actualizarTopClientes(msg['customersWithCount']);
} else {
console.log('No hay datos de clientes');
const tbody = document.getElementById('customersTableBody');
if (tbody) {
tbody.innerHTML = '<tr><td colspan="2">{{ 'No data available'|trans }}</td></tr>';
}
}
// Gráficos
console.log('Datos recibidos para gráficos:', {
ticketsByTypeStacked: msg['ticketsByTypeStacked'],
incidentTypes: msg['incidentTypes'],
ticketsByMonth: msg['ticketsByMonth'],
closureTimeCategories: msg['closureTimeCategories']
});
if (msg['ticketsByTypeStacked'] && msg['ticketsByTypeStacked'].length > 0) {
console.log('Creando gráfico de tipos apilados');
crearGraficoTiposStacked(msg['ticketsByTypeStacked']);
} else {
console.log('No hay datos para ticketsByTypeStacked');
}
if (msg['incidentTypes'] && msg['incidentTypes'].length > 0) {
console.log('Creando gráfico de tipos de incidentes');
crearGraficoIncidentTypes(msg['incidentTypes']);
} else {
console.log('No hay datos para incidentTypes');
}
if (msg['ticketsByMonth'] && msg['ticketsByMonth'].length > 0) {
console.log('Creando gráfico de tickets por mes');
crearGraficoTicketsByMonth(msg['ticketsByMonth']);
} else {
console.log('No hay datos para ticketsByMonth');
}
if (msg['closureTimeCategories'] && msg['closureTimeCategories'].length > 0) {
console.log('Creando gráfico de tiempo de cierre');
crearGraficoClosureTime(msg['closureTimeCategories']);
} else {
console.log('No hay datos para closureTimeCategories');
}
// Gráficos adicionales (opcionales)
if (msg['ticketsByDay']) {
crearGraficoTicketsByDay(msg['ticketsByDay']);
}
if (msg['ticketsByHour']) {
crearGraficoTicketsByHour(msg['ticketsByHour']);
}
}
});
}
// Funcionalidad para cargar Excel
$('#btnUploadExcel').on('click', function() {
$('#excelFileInput').click();
});
$('#excelFileInput').on('change', function(e) {
const file = e.target.files[0];
if (!file) return;
const formData = new FormData();
formData.append('excel_file', file);
$('#excelStatus').text('{{ 'Cargando...'|trans }}');
$.ajax({
url: '/dashboard/excel/upload',
type: 'POST',
data: formData,
processData: false,
contentType: false,
success: function(response) {
if (response.success) {
$('#excelStatus').text('{{ 'Excel cargado: '|trans }}' + response.rows + ' {{ 'filas'|trans }}');
$('#btnClearExcel').show();
// Recargar datos
filtroFechas();
} else {
$('#excelStatus').text('{{ 'Error: '|trans }}' + response.message);
alert(response.message);
}
},
error: function(xhr) {
const response = xhr.responseJSON || {};
$('#excelStatus').text('{{ 'Error al cargar'|trans }}');
alert(response.message || '{{ 'Error al cargar el archivo'|trans }}');
}
});
});
$('#btnClearExcel').on('click', function() {
$.ajax({
url: '/dashboard/excel/clear',
type: 'POST',
success: function(response) {
if (response.success) {
$('#excelStatus').text('');
$('#btnClearExcel').hide();
$('#excelFileInput').val('');
// Recargar datos
filtroFechas();
}
}
});
});
// Verificar estado al cargar
function checkExcelStatus() {
$.ajax({
url: '/dashboard/excel/status',
type: 'GET',
success: function(response) {
if (response.hasExcelData) {
$('#excelStatus').text('{{ 'Usando Excel: '|trans }}' + response.rows + ' {{ 'filas'|trans }}');
$('#btnClearExcel').show();
}
}
});
}
// Cargar datos iniciales al cargar la página
$(document).ready(function() {
// Verificar si hay Excel cargado
checkExcelStatus();
// Establecer fechas por defecto (últimos 30 días)
let endDate = new Date();
let startDate = new Date();
startDate.setDate(startDate.getDate() - 30);
const formatDate = (date) => {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
};
$('.uv-action-bar .date').find("#from").val(formatDate(startDate));
$('.uv-action-bar .date').find("#to").val(formatDate(endDate));
// Inicializar gráficos con datos iniciales si existen
inicializarGraficos();
// Cargar datos iniciales
filtroFechas();
});
const capitalizeFirstLetter = (string) => {
return string.charAt(0).toUpperCase() + string.slice(1);
}
const crearGrafico = (tickets, nombre) => {
let tipo = 'pie'
let labels = Object.values(tickets).map(function (el) {return el.Nombre })
let label = ''
let options = {
legend: { display: false }
}
switch(nombre) {
case 'kudos':
tipo = 'bar';
options.indexAxis = 'y';
break;
case 'ticketsByStatus':
tipo = 'bar';
options.indexAxis = 'y';
break;
case 'ticketsByPriority':
tipo = 'bar';
options.indexAxis = 'y';
break;
}
return new Chart(
document.getElementById(nombre),
{
type: tipo,
data: {
labels: labels,
datasets: [{
label: label,
data: Object.values(tickets).map(function (el) { return el.Cantidad }),
backgroundColor: Object.values(tickets).map(function (el) { return el.Color }),
hoverOffset: 4
}]
},
options: options
}
)
}
const actualizaGrafico = (grafico, tickets, nombre) => {
if (!grafico || !tickets || Object.keys(tickets).length === 0) {
return;
}
let labels = Object.values(tickets).map(function (el) {return el.Nombre })
const data = {
labels: labels,
datasets: [
{
label: '',
data: Object.values(tickets).map(function (el) { return el.Cantidad }),
backgroundColor: Object.values(tickets).map(function (el) { return el.Color }),
hoverOffset: 4
}
]
};
grafico.data.labels = data.labels;
grafico.data.datasets = data.datasets;
grafico.update();
let html = '';
$.each(tickets, function (key, value) {
let Color = value.Color;
let Cantidad = value.Cantidad;
let Nombre = value.Nombre;
html += '<label><span class="boch" style="background: '+ Color +'"></span> '+ Cantidad +' - '+ Nombre +' </label>';
});
$('#'+nombre).html(html);
}
// Declarar variables de gráficos globalmente
let graficoTicketByStatus = null;
let graficoTicketByType = null;
let graficoTicketByPriority = null;
let graficoPerAgent = null;
let graficoPerTeam = null;
let graficoKudos = null;
// Función para actualizar gráficos - debe estar antes de filtroFechas
const actualizarGraficos = () => {
if (graficoTicketByStatus && ticketsByStatus) {
actualizaGrafico(graficoTicketByStatus, ticketsByStatus, 'contenidoticketsByStatus');
}
if (graficoTicketByType && ticketsByType) {
actualizaGrafico(graficoTicketByType, ticketsByType, 'contenidoticketsByType');
}
if (graficoTicketByPriority && ticketsByPriority) {
actualizaGrafico(graficoTicketByPriority, ticketsByPriority, 'contenidoticketsByPriority');
}
if (graficoPerAgent && ticketsPerAgent) {
actualizaGrafico(graficoPerAgent, ticketsPerAgent, 'contenidoticketsPerAgent');
}
if (graficoPerTeam && ticketsPerTeam) {
actualizaGrafico(graficoPerTeam, ticketsPerTeam, 'contenidoticketsPerTeam');
}
if (graficoKudos && kudos) {
actualizaGrafico(graficoKudos, kudos, 'contenidokudos');
}
}
// Inicializar gráficos solo si hay datos
const inicializarGraficos = () => {
if (Object.keys(ticketsByStatus).length > 0) {
graficoTicketByStatus = crearGrafico(ticketsByStatus, 'ticketsByStatus');
}
if (Object.keys(ticketsByType).length > 0) {
graficoTicketByType = crearGrafico(ticketsByType, 'ticketsByType');
}
if (Object.keys(ticketsByPriority).length > 0) {
graficoTicketByPriority = crearGrafico(ticketsByPriority, 'ticketsByPriority');
}
if (Object.keys(ticketsPerAgent).length > 0) {
graficoPerAgent = crearGrafico(ticketsPerAgent, 'ticketsPerAgent');
}
if (Object.keys(ticketsPerTeam).length > 0) {
graficoPerTeam = crearGrafico(ticketsPerTeam, 'ticketsPerTeam');
}
if (Object.keys(kudos).length > 0) {
graficoKudos = crearGrafico(kudos, 'kudos');
}
}
// Variables globales para nuevos gráficos
let ticketsByDayChart = null;
let ticketsOpenVsClosedChart = null;
let ticketsByHourChart = null;
let ticketsByMonthChart = null;
let ticketsByTypeStackedChart = null;
let incidentTypesChart = null;
let closureTimeChart = null;
// Función para actualizar KPIs
const actualizarKPIs = (kpiData, lastTicketDate, lessThan5Days) => {
if (kpiData) {
document.getElementById('kpiTotalTickets').textContent = kpiData.total_tickets || 0;
document.getElementById('kpiResolvedTickets').textContent = kpiData.tickets_resueltos || 0;
document.getElementById('kpiOpenTickets').textContent = kpiData.tickets_abiertos || 0;
}
if (lastTicketDate) {
document.getElementById('kpiLastTicketDate').textContent = lastTicketDate;
}
if (lessThan5Days !== undefined) {
document.getElementById('kpiLessThan5Days').textContent = lessThan5Days.toLocaleString();
}
}
// Función para crear gráfico de tendencia temporal
const crearGraficoTicketsByDay = (data) => {
const ctx = document.getElementById('ticketsByDayChart');
if (!ctx) return;
if (ticketsByDayChart) {
ticketsByDayChart.destroy();
}
const labels = data.map(item => item.fecha);
const values = data.map(item => item.cantidad);
ticketsByDayChart = new Chart(ctx, {
type: 'line',
data: {
labels: labels,
datasets: [{
label: '{{ 'Tickets Created'|trans }}',
data: values,
borderColor: 'rgb(75, 192, 192)',
backgroundColor: 'rgba(75, 192, 192, 0.2)',
tension: 0.1
}]
},
options: {
responsive: true,
maintainAspectRatio: true,
scales: {
y: {
beginAtZero: true,
ticks: {
stepSize: 1,
precision: 0
}
}
}
}
});
}
// Función para crear gráfico de tickets abiertos vs cerrados
const crearGraficoTicketsOpenVsClosed = (data) => {
const ctx = document.getElementById('ticketsOpenVsClosedChart');
if (!ctx) return;
if (ticketsOpenVsClosedChart) {
ticketsOpenVsClosedChart.destroy();
}
const labels = data.map(item => item.fecha);
const abiertos = data.map(item => item.abiertos);
const cerrados = data.map(item => item.cerrados);
ticketsOpenVsClosedChart = new Chart(ctx, {
type: 'bar',
data: {
labels: labels,
datasets: [
{
label: '{{ 'Opened'|trans }}',
data: abiertos,
backgroundColor: 'rgba(255, 99, 132, 0.5)',
borderColor: 'rgb(255, 99, 132)',
borderWidth: 1
},
{
label: '{{ 'Closed'|trans }}',
data: cerrados,
backgroundColor: 'rgba(54, 162, 235, 0.5)',
borderColor: 'rgb(54, 162, 235)',
borderWidth: 1
}
]
},
options: {
responsive: true,
maintainAspectRatio: true,
scales: {
y: {
beginAtZero: true,
ticks: {
stepSize: 1,
precision: 0
}
}
}
}
});
}
// Función para crear gráfico de tickets por hora
const crearGraficoTicketsByHour = (data) => {
const ctx = document.getElementById('ticketsByHourChart');
if (!ctx) return;
if (ticketsByHourChart) {
ticketsByHourChart.destroy();
}
// Crear array completo de 24 horas
const hoursData = Array.from({length: 24}, (_, i) => {
const found = data.find(item => item.hora === i);
return found ? found.cantidad : 0;
});
const labels = Array.from({length: 24}, (_, i) => i + ':00');
ticketsByHourChart = new Chart(ctx, {
type: 'bar',
data: {
labels: labels,
datasets: [{
label: '{{ 'Tickets'|trans }}',
data: hoursData,
backgroundColor: 'rgba(153, 102, 255, 0.5)',
borderColor: 'rgb(153, 102, 255)',
borderWidth: 1
}]
},
options: {
responsive: true,
maintainAspectRatio: true,
scales: {
y: {
beginAtZero: true,
ticks: {
stepSize: 1,
precision: 0
}
}
}
}
});
}
// Función para actualizar tabla de top agentes
const actualizarTopAgentes = (data) => {
const tbody = document.getElementById('topAgentsTableBody');
if (!tbody) return;
if (!data || data.length === 0) {
tbody.innerHTML = '<tr><td colspan="2">{{ 'No data available'|trans }}</td></tr>';
return;
}
tbody.innerHTML = data.map(agent => `
<tr>
<td>${agent.agente || '-'}</td>
<td>${agent.tickets_resueltos || 0}</td>
</tr>
`).join('');
}
// Función para actualizar tabla de top clientes
const actualizarTopClientes = (data) => {
const tbody = document.getElementById('customersTableBody');
if (!tbody) {
console.error('No se encontró el elemento customersTableBody');
return;
}
console.log('Actualizando tabla de clientes con datos:', data);
if (!data || data.length === 0) {
tbody.innerHTML = '<tr><td colspan="2">{{ 'No data available'|trans }}</td></tr>';
return;
}
tbody.innerHTML = data.map(customer => `
<tr>
<td>${customer.cliente || '-'}</td>
<td>${customer.total_tickets || 0}</td>
</tr>
`).join('');
}
// Función para actualizar tabla de agentes
const actualizarAgentes = (data) => {
const tbody = document.getElementById('agentsTableBody');
if (!tbody) return;
if (!data || data.length === 0) {
tbody.innerHTML = '<tr><td colspan="2">{{ 'No data available'|trans }}</td></tr>';
return;
}
tbody.innerHTML = data.map(agent => `
<tr>
<td>${agent.agente || '-'}</td>
<td>${agent.total_tickets || 0}</td>
</tr>
`).join('');
}
// Función para crear gráfico apilado de tipos
const crearGraficoTiposStacked = (data) => {
const ctx = document.getElementById('ticketsByTypeStackedChart');
if (!ctx) {
console.error('No se encontró el elemento ticketsByTypeStackedChart');
return;
}
if (ticketsByTypeStackedChart) {
ticketsByTypeStackedChart.destroy();
}
console.log('Datos para gráfico apilado:', data);
const labels = ['Todo'];
const colors = ['rgba(54, 162, 235, 0.8)', 'rgba(75, 192, 192, 0.8)', 'rgba(153, 102, 255, 0.8)', 'rgba(255, 206, 86, 0.8)', 'rgba(255, 99, 132, 0.8)', 'rgba(255, 159, 64, 0.8)'];
const datasets = data.map((item, index) => ({
label: item.tipo || 'Sin tipo',
data: [item.cantidad || 0],
backgroundColor: colors[index % colors.length] || 'rgba(201, 203, 207, 0.8)'
}));
ticketsByTypeStackedChart = new Chart(ctx, {
type: 'bar',
data: {
labels: labels,
datasets: datasets
},
options: {
indexAxis: 'y',
responsive: true,
maintainAspectRatio: true,
plugins: {
legend: {
display: true,
position: 'top'
}
},
scales: {
x: {
stacked: true,
beginAtZero: true,
ticks: {
stepSize: 1,
precision: 0
}
},
y: {
stacked: true
}
}
}
});
}
// Función para crear pie chart de tipos de incidentes
const crearGraficoIncidentTypes = (data) => {
const ctx = document.getElementById('incidentTypesChart');
if (!ctx) {
console.error('No se encontró el elemento incidentTypesChart');
return;
}
if (incidentTypesChart) {
incidentTypesChart.destroy();
}
console.log('Datos para gráfico de incidentes:', data);
const total = data.reduce((sum, item) => sum + (item.cantidad || 0), 0);
const labels = data.map(item => {
const cantidad = item.cantidad || 0;
const percentage = total > 0 ? ((cantidad / total) * 100).toFixed(1) : 0;
return `${item.tipo || 'Sin tipo'} (${percentage}%)`;
});
const values = data.map(item => item.cantidad || 0);
// Colores para el pie chart
const colors = [
'rgba(255, 99, 132, 0.8)', 'rgba(54, 162, 235, 0.8)', 'rgba(255, 206, 86, 0.8)',
'rgba(75, 192, 192, 0.8)', 'rgba(153, 102, 255, 0.8)', 'rgba(255, 159, 64, 0.8)',
'rgba(199, 199, 199, 0.8)', 'rgba(83, 102, 255, 0.8)', 'rgba(255, 99, 255, 0.8)',
'rgba(99, 255, 132, 0.8)', 'rgba(255, 159, 132, 0.8)', 'rgba(132, 99, 255, 0.8)',
'rgba(99, 132, 255, 0.8)', 'rgba(255, 99, 99, 0.8)', 'rgba(99, 255, 255, 0.8)'
];
incidentTypesChart = new Chart(ctx, {
type: 'pie',
data: {
labels: labels,
datasets: [{
data: values,
backgroundColor: colors.slice(0, data.length)
}]
},
options: {
responsive: true,
maintainAspectRatio: true,
plugins: {
legend: {
position: 'right',
labels: {
boxWidth: 12,
font: {
size: 10
}
}
}
}
}
});
}
// Función para crear gráfico de tickets por mes
const crearGraficoTicketsByMonth = (data) => {
const ctx = document.getElementById('ticketsByMonthChart');
if (!ctx) {
console.error('No se encontró el elemento ticketsByMonthChart');
return;
}
if (ticketsByMonthChart) {
ticketsByMonthChart.destroy();
}
console.log('Datos para gráfico por mes:', data);
const labels = data.map(item => item.mes_formateado || item.mes || '');
const values = data.map(item => item.cantidad || 0);
ticketsByMonthChart = new Chart(ctx, {
type: 'bar',
data: {
labels: labels,
datasets: [{
label: '{{ 'Tickets'|trans }}',
data: values,
backgroundColor: 'rgba(54, 162, 235, 0.8)',
borderColor: 'rgb(54, 162, 235)',
borderWidth: 1
}]
},
options: {
responsive: true,
maintainAspectRatio: true,
scales: {
y: {
beginAtZero: true,
ticks: {
stepSize: 1,
precision: 0
}
}
}
}
});
}
// Función para crear gráfico de tiempo de cierre
const crearGraficoClosureTime = (data) => {
const ctx = document.getElementById('closureTimeChart');
if (!ctx) {
console.error('No se encontró el elemento closureTimeChart');
return;
}
if (closureTimeChart) {
closureTimeChart.destroy();
}
console.log('Datos para gráfico de tiempo de cierre:', data);
const labels = data.map(item => item.categoria || '');
const values = data.map(item => item.cantidad || 0);
// Colores específicos para cada categoría
const colors = labels.map((label, index) => {
const categoryColors = {
'Menos 30 min': 'rgba(75, 192, 192, 0.8)',
'Menos 2 horas': 'rgba(153, 102, 255, 0.8)',
'Menos 1 día': 'rgba(255, 159, 64, 0.8)',
'Menos 5 días': 'rgba(255, 99, 132, 0.8)',
'Menos 15 días': 'rgba(255, 206, 86, 0.8)',
'Más de 15 días': 'rgba(54, 162, 235, 0.8)',
'No Cerrado': 'rgba(201, 203, 207, 0.8)'
};
return categoryColors[label] || 'rgba(199, 199, 199, 0.8)';
});
closureTimeChart = new Chart(ctx, {
type: 'bar',
data: {
labels: labels,
datasets: [{
label: '{{ 'Tickets'|trans }}',
data: values,
backgroundColor: colors,
borderColor: colors.map(c => (c && typeof c === 'string') ? c.replace('0.8', '1') : 'rgba(199, 199, 199, 1)'),
borderWidth: 1
}]
},
options: {
indexAxis: 'y',
responsive: true,
maintainAspectRatio: true,
scales: {
x: {
beginAtZero: true,
ticks: {
stepSize: 1,
precision: 0
}
}
}
}
});
}
</script>
{% endblock %}