vendor/uvdesk/core-framework/Resources/views/dashboard.html.twig line 1

Open in your IDE?
  1. {% extends "@UVDeskCoreFramework//Templates//layout.html.twig" %}
  2. {% block title %}Dashboard{% endblock %}
  3. {% block pageContent %}
  4.     <style>
  5.         .btn{
  6.             border: 1px solid #1a1a1a;
  7.             display: inline-block;
  8.             padding: 10px;
  9.             position: relative;
  10.             text-align: center;
  11.             transition: background 600ms ease, color 600ms ease;
  12.             margin-right: 0px !important;
  13.         }
  14.         input[type="radio"].toggle {
  15.             display: none;
  16.         }
  17.         input[type=radio]:checked + label {
  18.             background: blue;
  19.             color:white;
  20.         }
  21.         .uv-action-bar .uv-field-block.date {
  22.             display: inline-block;
  23.             margin-right: 8px;
  24.         }
  25.         .uv-action-bar label {
  26.             font-size: 16px;
  27.             vertical-align: middle;
  28.             margin-right: 10px;
  29.         }
  30.         .uv-inner-section .uv-action-bar label{
  31.             font-size: 15px;
  32.         }
  33.         @media screen and (min-width: 1100px) and (max-width: 1260px) {
  34.             .uv-inner-section .uv-action-bar .uv-action-bar-col-lt, .uv-inner-section .uv-action-bar .uv-action-bar-col-rt {
  35.                 width: 55% !important;
  36.             }
  37.         }
  38.         .graficosTortaBarra{
  39.             font-size:11px;width: 520px;border:1px solid #d3d3d3;height: 300px;padding:10px;
  40.         }
  41.         .graficosTortaBarra label{
  42.             line-height: 2;
  43.             padding: 0 8px;
  44.             display: inline-block;
  45.         }
  46.         .boch {
  47.             display:inline-block;
  48.             height: 10px;
  49.             width: 10px;
  50.             margin-right: 3px;
  51.             border-radius: 50%;
  52.         }
  53.         .box-graficos {
  54.             display: flex;
  55.             flex-wrap: wrap;
  56.             gap: 10px;
  57.         }
  58.         .uv-activity-wrapper {
  59.             margin-top: 60px;
  60.         }
  61.         .uv-activity-wrapper .uv-activity-chart-col-lt {
  62.             width: 80%;
  63.             float: left;
  64.         }
  65.         ul.uv-activity-brick-wrapper {
  66.             list-style: none;
  67.             margin: 0;
  68.             padding: 0;
  69.             width: 100%;
  70.             display: inline-block;
  71.         }
  72.         ul.uv-activity-brick-wrapper li {
  73.             width: 25%;
  74.             display: inline-block;
  75.             float: left;
  76.             padding-left: 10px;
  77.             padding-right: 10px;
  78.             color: #fff;
  79.         }
  80.         ul.uv-activity-brick-wrapper .uv-activity-brick {
  81.             border-radius: 3px;
  82.             padding: 10px;
  83.             text-align: center;
  84.         }
  85.         ul.uv-activity-brick-wrapper li a {
  86.             color: #fff;
  87.             font-size: 45px;
  88.             width: 100%;
  89.             display: inline-block;
  90.         }
  91.         ul.uv-activity-brick-wrapper li label {
  92.             font-size: 18px;
  93.             width: 100%;
  94.             display: inline-block;
  95.         }
  96.         .uv-activity-chart-bottom-row .uv-pannel-body {
  97.             height: 450px;
  98.         }
  99.         .kudos-overview {
  100.             width: 40%;
  101.             float: left;
  102.             padding-right: 10px;
  103.         }
  104.         .recent-notification {
  105.             width: 30%;
  106.             float: left;
  107.             padding-left: 10px;
  108.         }
  109.         .completion-chart {
  110.             width: 300px;
  111.             margin: 0 auto;
  112.         }
  113.         .progress-meter .background {
  114.             fill: #EFEFEF;
  115.         }
  116.         .progress-meter text {
  117.             font-size: 30px;
  118.         }
  119.         .kudos-overview .uv-pannel-body {
  120.             text-align: center;
  121.             padding-top: 50px;
  122.         }
  123.         .kudos-overview .uv-pannel-body label {
  124.             margin-top: 10px;
  125.             display: inline-block;
  126.         }
  127.         .recent-notification ul {
  128.             list-style: none;
  129.             padding: 0;
  130.             margin: 0;
  131.             overflow-y: auto;
  132.             max-height: 400px !important;
  133.         }
  134.         .recent-notification .uv-pannel-body {
  135.             padding: 0;
  136.         }
  137.         .recent-notification ul li {
  138.             color: #333333;
  139.             border-bottom: solid 1px #D3D3D3;
  140.             padding: 15px 20px;
  141.         }
  142.         .recent-notification ul li:first-child {
  143.             border-top: none;
  144.         }
  145.         .recent-notification ul li:last-child {
  146.             border-bottom: none;
  147.         }
  148.         .recent-notification ul li * {
  149.             display: inline-block !important;
  150.         }
  151.         .recent-notification ul li .timeago {
  152.             color: #9E9E9E;
  153.             margin-top: 5px;
  154.             font-size: 13px;
  155.         }
  156.         .recent-notification label {
  157.             text-align: center;
  158.             display: inline-block;
  159.             width: 100%;
  160.             padding-top: 15px;
  161.             border-top: 1px solid #d3d3d3;
  162.         }
  163.         .recent-notification span.uv-notification-message {
  164.             float: left;
  165.             width: 100%;
  166.         }
  167.         .kudos-count {
  168.             width: 30%;
  169.             float: left;
  170.             padding-right: 10px;
  171.             padding-left: 10px;
  172.         }
  173.         .kudos-count .uv-pannel-body {
  174.             padding-top: 50px;
  175.             overflow-y: auto;
  176.         }
  177.         .kudos-count ul {
  178.             list-style: none;
  179.             padding: 0;
  180.             margin: 0;
  181.         }
  182.         .kudos-count ul li {
  183.             width: 100%;
  184.             display: inline-block;
  185.             padding: 15px 0;
  186.         }
  187.         .kudos-count ul li .uv-icon-kudos  {
  188.             vertical-align: middle;
  189.             margin-right: 10px;
  190.         }
  191.         .uv-activity-wrapper .uv-activity-chart-col-rt {
  192.             width: 20%;
  193.             float: left;
  194.         }
  195.         .uv-activity-chart-col-rt ul {
  196.             padding: 0;
  197.             margin: 0;
  198.             list-style: none;
  199.         }
  200.         .uv-activity-chart-col-rt ul li {
  201.             margin-bottom: 10px
  202.         }
  203.         .uv-activity-chart-col-rt ul li span {
  204.             width: 100%;
  205.             display: inline-block;
  206.             color: #6f6f6f;
  207.         }
  208.         .uv-middle {
  209.             margin: 0 auto;
  210.             display: inline-block;
  211.             margin-top: 200px;
  212.             text-align: center;
  213.             width: 100%;
  214.         }
  215.         @media screen and (max-width: 1024px) {
  216.             .uv-activity-wrapper .uv-activity-chart-col-lt {
  217.                 width: 100%;
  218.                 padding: 0;
  219.             }
  220.             .uv-activity-wrapper .uv-activity-chart-col-rt {
  221.                 width: 100%;
  222.             }
  223.             .kudos-overview {
  224.                 width: 100%;
  225.                 padding: 0;
  226.             }
  227.             .kudos-count {
  228.                 width: 100%;
  229.                 padding: 0;
  230.             }
  231.             .recent-notification {
  232.                 width: 100%;
  233.                 padding: 0;
  234.             }
  235.             ul.uv-activity-brick-wrapper li {
  236.                 width: 50%;
  237.                 margin: 10px 0;
  238.             }
  239.         }
  240.         @media screen and (max-width: 768px) {
  241.             ul.uv-activity-brick-wrapper li {
  242.                 width: 100%;
  243.             }
  244.         }
  245.         @media screen and (max-width: 467px) {
  246.             .completion-chart {
  247.                 width: 100%;
  248.             }
  249.         }
  250.         span.uv-notification-message a:link, span.uv-notification-message a:visited, label a:link, label a:visited {
  251.             color: #2750C4;
  252.             font-size: 15px;
  253.         }
  254.         .uv-mob-aside {
  255.             display: none;
  256.         }
  257.         {# .uv-copyright {
  258.             text-align: center;
  259.         } #}
  260.         
  261.         /* Estilos para KPI Cards */
  262.         .kpi-cards {
  263.             display: flex;
  264.             flex-wrap: wrap;
  265.             gap: 20px;
  266.             margin: 20px 0;
  267.             padding: 0;
  268.         }
  269.         .kpi-card {
  270.             flex: 1;
  271.             min-width: 200px;
  272.             background: #fff;
  273.             border: 1px solid #d3d3d3;
  274.             border-radius: 8px;
  275.             padding: 20px;
  276.             box-shadow: 0 2px 4px rgba(0,0,0,0.1);
  277.         }
  278.         .kpi-card h3 {
  279.             margin: 0 0 10px 0;
  280.             font-size: 14px;
  281.             color: #666;
  282.             text-transform: uppercase;
  283.         }
  284.         .kpi-card .value {
  285.             font-size: 32px;
  286.             font-weight: bold;
  287.             color: #333;
  288.             margin: 10px 0;
  289.         }
  290.         .kpi-card .label {
  291.             font-size: 12px;
  292.             color: #999;
  293.         }
  294.         
  295.         /* Estilos para gráficos mejorados */
  296.         .chart-container {
  297.             background: #fff;
  298.             border: 1px solid #d3d3d3;
  299.             border-radius: 8px;
  300.             padding: 20px;
  301.             margin: 20px 0;
  302.             box-shadow: 0 2px 4px rgba(0,0,0,0.1);
  303.         }
  304.         .chart-container h3 {
  305.             margin: 0 0 20px 0;
  306.             font-size: 18px;
  307.             color: #333;
  308.         }
  309.         .table-container {
  310.             background: #fff;
  311.             border: 1px solid #d3d3d3;
  312.             border-radius: 8px;
  313.             padding: 20px;
  314.             margin: 20px 0;
  315.             box-shadow: 0 2px 4px rgba(0,0,0,0.1);
  316.         }
  317.         .table-container h3 {
  318.             margin: 0 0 20px 0;
  319.             font-size: 18px;
  320.             color: #333;
  321.         }
  322.         .table-container table {
  323.             width: 100%;
  324.             border-collapse: collapse;
  325.         }
  326.         .table-container table th,
  327.         .table-container table td {
  328.             padding: 12px;
  329.             text-align: left;
  330.             border-bottom: 1px solid #ddd;
  331.         }
  332.         .table-container table th {
  333.             background-color: #f5f5f5;
  334.             font-weight: bold;
  335.         }
  336.     </style>
  337.     <div class="uv-area">
  338.         <div>
  339.             <div class="uv-action-bar">
  340.                 <div class="uv-action-bar-col-lt">
  341.                     {# Botón para cargar Excel #}
  342.                     <div style="display: inline-block; margin-right: 20px; vertical-align: top;">
  343.                         <label style="display: block; margin-bottom: 5px;">{{ 'Cargar Excel'|trans }}</label>
  344.                         <input type="file" id="excelFileInput" accept=".xlsx,.xls,.csv" style="display: none;">
  345.                         <button type="button" id="btnUploadExcel" class="btn" style="background: #4caf50; color: white; border: none; padding: 8px 15px; cursor: pointer;">
  346.                             {{ 'Subir Excel'|trans }}
  347.                         </button>
  348.                         <button type="button" id="btnClearExcel" class="btn" style="background: #f44336; color: white; border: none; padding: 8px 15px; cursor: pointer; margin-left: 10px; display: none;">
  349.                             {{ 'Usar BD'|trans }}
  350.                         </button>
  351.                         <span id="excelStatus" style="margin-left: 10px; color: #4caf50; font-weight: bold;"></span>
  352.                     </div>
  353.                     <label>{{ 'From'|trans }}</label>
  354.                     <div class="uv-field-block date">
  355.                         <input type="text" class="uv-field uv-date-picker date date-from" id="from" value="">
  356.                     </div>
  357.                     <label>{{ 'To'|trans }}</label>
  358.                     <div class="uv-field-block date">
  359.                         <input type="text" class="uv-field uv-date-picker date date-to"  id="to" value="">
  360.                     </div>
  361.                     <input value="year" type="radio" class="toggle " name="options" id="year" autocomplete="off" >
  362.                     <label class="btn" for="year">Last Year</label>
  363.                     <input value="month" type="radio" class="toggle " name="options" id="month" autocomplete="off">
  364.                     <label class="btn" for="month">Last Month</label>
  365.                     <input value="week" type="radio" class="toggle " name="options" id="week" autocomplete="off" >
  366.                     <label class="btn" for="week">Last Week</label>
  367.                     <input value="day" type="radio" class="toggle " name="options" id="day" autocomplete="off" >
  368.                     <label class="btn" for="day">Last Day</label>
  369.                 </div>
  370.                 {# <input value="year" type="radio" class="toggle " name="options" id="year" autocomplete="off" >
  371.                  <label class="btn" for="year">Last Year</label>
  372.                  <input value="month" type="radio" class="toggle " name="options" id="month" autocomplete="off">
  373.                  <label class="btn" for="month">Last Month</label>
  374.                  <input value="week" type="radio" class="toggle " name="options" id="week" autocomplete="off" >
  375.                  <label class="btn" for="week">Last Week</label>
  376.                  <input value="day" type="radio" class="toggle " name="options" id="day" autocomplete="off" >
  377.                  <label class="btn" for="day">Last Day</label>#}
  378.             </div>
  379.             {# Sección de KPIs - Estilo Looker Studio #}
  380.             <div class="kpi-cards" id="kpiCards" style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 20px; margin: 20px 0;">
  381.                 <div class="kpi-card" style="background: #4caf50; color: white;">
  382.                     <h3 style="color: white;">{{ 'Fecha Último Ticket'|trans }}</h3>
  383.                     <div class="value" id="kpiLastTicketDate" style="color: white;">-</div>
  384.                 </div>
  385.                 <div class="kpi-card" style="background: white;">
  386.                     <h3>{{ 'Menos de 5 días'|trans }}</h3>
  387.                     <div class="value" id="kpiLessThan5Days">-</div>
  388.                 </div>
  389.                 <div class="kpi-card">
  390.                     <h3>{{ 'Total Tickets'|trans }}</h3>
  391.                     <div class="value" id="kpiTotalTickets">-</div>
  392.                     <div class="label">{{ 'In selected period'|trans }}</div>
  393.                 </div>
  394.                 <div class="kpi-card">
  395.                     <h3>{{ 'Resolved Tickets'|trans }}</h3>
  396.                     <div class="value" id="kpiResolvedTickets">-</div>
  397.                     <div class="label">{{ 'Successfully closed'|trans }}</div>
  398.                 </div>
  399.                 <div class="kpi-card">
  400.                     <h3>{{ 'Open Tickets'|trans }}</h3>
  401.                     <div class="value" id="kpiOpenTickets">-</div>
  402.                     <div class="label">{{ 'Currently active'|trans }}</div>
  403.                 </div>
  404.             </div>
  405.             {# Layout tipo Looker Studio - Grid de 3 columnas #}
  406.             <div style="display: grid; grid-template-columns: repeat(3, 1fr); gap: 20px; margin: 20px 0;">
  407.                 {# Columna 1: Tabla de Agentes #}
  408.                 <div class="table-container">
  409.                     <h3>{{ 'AGENT NAME'|trans }}</h3>
  410.                     <table id="agentsTable">
  411.                         <thead>
  412.                             <tr>
  413.                                 <th>{{ 'Agent'|trans }}</th>
  414.                                 <th>{{ 'Records'|trans }}</th>
  415.                             </tr>
  416.                         </thead>
  417.                         <tbody id="agentsTableBody">
  418.                             <tr><td colspan="2">{{ 'Loading...'|trans }}</td></tr>
  419.                         </tbody>
  420.                     </table>
  421.                 </div>
  422.                 {# Columna 2: Gráfico de Tipos y Pie Chart #}
  423.                 <div>
  424.                     <div class="chart-container">
  425.                         <h3>{{ 'Tipo de Tickets'|trans }}</h3>
  426.                         <canvas id="ticketsByTypeStackedChart" height="200"></canvas>
  427.                     </div>
  428.                     <div class="chart-container" style="margin-top: 20px;">
  429.                         <h3>{{ 'Tipos de Incidentes'|trans }}</h3>
  430.                         <canvas id="incidentTypesChart" height="200"></canvas>
  431.                     </div>
  432.                 </div>
  433.                 {# Columna 3: Tabla de Clientes #}
  434.                 <div class="table-container">
  435.                     <h3>{{ 'CUSTOMER NAME'|trans }}</h3>
  436.                     <table id="customersTable">
  437.                         <thead>
  438.                             <tr>
  439.                                 <th>{{ 'Customer'|trans }}</th>
  440.                                 <th>{{ 'Records'|trans }}</th>
  441.                             </tr>
  442.                         </thead>
  443.                         <tbody id="customersTableBody">
  444.                             <tr><td colspan="2">{{ 'Loading...'|trans }}</td></tr>
  445.                         </tbody>
  446.                     </table>
  447.                 </div>
  448.             </div>
  449.             {# Gráfico de Tickets por Mes #}
  450.             <div class="chart-container">
  451.                 <h3>{{ 'Tickets por Mes'|trans }}</h3>
  452.                 <canvas id="ticketsByMonthChart" height="80"></canvas>
  453.             </div>
  454.             {# Gráfico de Tiempo de Cierre #}
  455.             <div class="chart-container">
  456.                 <h3>{{ 'Tiempo Cierre Tickets'|trans }}</h3>
  457.                 <canvas id="closureTimeChart" height="80"></canvas>
  458.             </div>
  459.             <div class="box-graficos">
  460.                 <div>
  461.                     <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-bar-chart" viewBox="0 0 16 16">
  462.                         <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"/>
  463.                     </svg>
  464.                     <b>{{  'Tickets by Status' |trans }}</b><br><br>
  465.                     <div class="graficosTortaBarra" >
  466.                         <span id="contenidoticketsByStatus">
  467.                             {% for tStatus in report_service.ticketsByStatus %}
  468.                                 <label>
  469.                                    <span class='boch'  style="background: {{ tStatus.Color }}"> </span>
  470.                                     ({{ tStatus.Cantidad }}) - {{  tStatus.Nombre }}
  471.                                </label>
  472.                             {% endfor %}
  473.                         </span>
  474.                         <div style="padding:25px;" >
  475.                             <canvas id="ticketsByStatus"   width="700" height="350"></canvas>
  476.                         </div>
  477.                     </div>
  478.                 </div>
  479.                 <div>
  480.                     <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-bar-chart" viewBox="0 0 16 16">
  481.                         <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"/>
  482.                     </svg>
  483.                     <b>{{ 'Tickets by Type' |trans }}</b><br><br>
  484.                     <div class="graficosTortaBarra" >
  485.                         <span id="contenidoticketsByType">
  486.                             {% for tStatus in report_service.ticketsByType %}
  487.                                 <label>
  488.                                     <span class='boch'  style="background: {{ tStatus.Color }}"> </span>
  489.                                     ({{ tStatus.Cantidad }}) - {{  tStatus.Nombre }}
  490.                                 </label>
  491.                             {% endfor %}
  492.                         </span>
  493.                         <div style="width: 220px;  margin: 0 auto;padding:25px;">
  494.                             <canvas id="ticketsByType" ></canvas>
  495.                         </div>
  496.                     </div>
  497.                 </div>
  498.                 <div>
  499.                     <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-bar-chart" viewBox="0 0 16 16">
  500.                         <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"/>
  501.                     </svg>
  502.                     <b>{{ 'Tickets by Priority' |trans }}</b><br><br>
  503.                     <div class="graficosTortaBarra" >
  504.                         <span id="contenidoticketsByPriority">
  505.                             {% for tPriority in report_service.ticketsByPriority %}
  506.                                 <label>
  507.                                     <span class='boch'  style="background: {{ tPriority.Color }}"> </span>
  508.                                     ({{ tPriority.Cantidad }}) - {{ tPriority.Nombre }}
  509.                                 </label>
  510.                             {% endfor %}
  511.                         </span>
  512.                         <div style="padding:25px;">
  513.                             <canvas id="ticketsByPriority" width="700" height="350"  ></canvas>
  514.                         </div>
  515.                     </div>
  516.                 </div>
  517.                 <div>
  518.                     <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-bar-chart" viewBox="0 0 16 16">
  519.                         <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"/>
  520.                     </svg>
  521.                     <b>{{ 'Tickets per Agent' |trans }}</b><br><br>
  522.                     <div class="graficosTortaBarra" >
  523.                         <span id="contenidoticketsPerAgent">
  524.                             {% for tAgent in report_service.ticketsPerAgent %}
  525.                                 <label>
  526.                                     <span class='boch'  style="background: {{ tAgent.Color }}"> </span>
  527.                                     ({{ tAgent.Cantidad }}) - {{  tAgent.Nombre }}
  528.                                 </label>
  529.                             {% endfor %}
  530.                         </span>
  531.                         <div style="padding:25px;width: 220px;  margin: 0 auto;">
  532.                             <canvas id="ticketsPerAgent" ></canvas>
  533.                         </div>
  534.                     </div>
  535.                 </div>
  536.                 <div>
  537.                     <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-bar-chart" viewBox="0 0 16 16">
  538.                         <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"/>
  539.                     </svg>
  540.                     <b>{{ 'Tickets per Team' |trans }}</b><br><br>
  541.                     <div class="graficosTortaBarra" >
  542.                         <span id="contenidoticketsPerTeam">
  543.                             {% for tTeam in report_service.ticketsPerTeam %}
  544.                                 <label>
  545.                                     <span class='boch'  style="background: {{ tTeam.Color }}"> </span>
  546.                                     ({{ tTeam.Cantidad }}) - {{  tTeam.Nombre }}
  547.                                 </label>
  548.                             {% endfor %}
  549.                         </span>
  550.                         <div  style="padding:25px;width: 220px;  margin: 0 auto;">
  551.                             <canvas id="ticketsPerTeam" ></canvas>
  552.                         </div>
  553.                     </div>
  554.                 </div>
  555.                 <div>
  556.                     <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-bar-chart" viewBox="0 0 16 16">
  557.                         <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"/>
  558.                     </svg>
  559.                     <b>{{ 'Kudos' |trans }}</b><br><br>
  560.                     <div class="graficosTortaBarra" >
  561.                         <span id="contenidokudos">
  562.                             {% for tKudos in report_service.Kudos %}
  563.                                 <label>
  564.                                     <span class='boch'  style="background: {{ tKudos.Color }}"> </span>
  565.                                     ({{ tKudos.Cantidad }}) - {{ tKudos.Nombre }}
  566.                                 </label>
  567.                             {% endfor %}
  568.                         </span>
  569.                         <div style="padding:25px;">
  570.                             <canvas id="kudos" width="700" height="350" ></canvas>
  571.                         </div>
  572.                     </div>
  573.                 </div>
  574.             </div>
  575.         </div>
  576.         <div class="uv-copyright">
  577.             <span class="uv-credit-text">Powered by <a href="https://www.uvdesk.com" target="_blank">UVdesk</a></span>
  578.         </div>
  579.     </div>
  580. {% endblock %}
  581. {% block footer %}
  582.     {{ parent() }}
  583.     <script src="https://cdn.jsdelivr.net/npm/chart.js@3.9.1/dist/chart.min.js"></script>
  584.     <script>
  585.         let startGlobalDate = "";
  586.         let endGlobalDate = "";
  587.         let date = new Date();
  588.         let currentMonth = date.getMonth();
  589.         let currentDate = date.getDate();
  590.         let currentYear = date.getFullYear();
  591.         //Filtros año, mes, semana, dia
  592.         $("input[type='radio']").click(function(){
  593.             currentDate = new Date();
  594.             start = new Date();
  595.             end = new Date();
  596.             $('.uv-action-bar .date').find("#to").val(currentDate.getFullYear() + "-" + (currentDate.getMonth()+1) + "-" + currentDate.getDate());
  597.             var hasta = new Date($('.uv-action-bar .date').find("#to").val());
  598.             switch ($('input[name="options"]:checked').val()) {
  599.                 case "year":
  600.                     start.setFullYear(currentDate.getFullYear() - 1);
  601.                     start.setMonth(0);
  602.                     start.setDate(1);
  603.                     start.setHours(0, 0, 0, 0);
  604.                     end.setFullYear(currentDate.getFullYear() - 1);
  605.                     end.setMonth(11);
  606.                     end.setDate(new Date(currentDate.getFullYear() - 1, 12, 0).getDate())
  607.                     end.setHours(0, 0, 0, 0);
  608.                     datestringStart = start.getFullYear() + "-" + (start.getMonth()+1) + "-" + start.getDate();
  609.                     datestringEnd = end.getFullYear() + "-" + (end.getMonth()+1) + "-" + end.getDate();
  610.                     $('.uv-action-bar .date').find("#from").val(datestringStart)
  611.                     $('.uv-action-bar .date').find("#to").val(datestringEnd).trigger('change');
  612.                     break;
  613.                 case "month":
  614.                     start.setMonth(currentDate.getMonth() - 1);
  615.                     start.setDate(1);
  616.                     start.setHours(0, 0, 0, 0);
  617.                     end.setMonth(currentDate.getMonth() - 1);
  618.                     end.setDate(new Date(end.getFullYear(), end.getMonth(), 0).getDate())
  619.                     end.setHours(0, 0, 0, 0);
  620.                     datestringStart = start.getFullYear() + "-" + (start.getMonth()+1) + "-" + start.getDate();
  621.                     datestringEnd = end.getFullYear() + "-" + (end.getMonth()+1) + "-" + end.getDate();
  622.                     $('.uv-action-bar .date').find("#from").val(datestringStart);
  623.                     $('.uv-action-bar .date').find("#to").val(datestringEnd).trigger('change');
  624.                     break;
  625.                 case "week":
  626.                     var beforeOneWeek = new Date(new Date().getTime() - 60 * 60 * 24 * 7 * 1000)
  627.                     var beforeOneWeek2 = new Date(beforeOneWeek);
  628.                     day = beforeOneWeek.getDay()
  629.                     diffToMonday = beforeOneWeek.getDate() - day + (day === 0 ? -6 : 1)
  630.                     lastMonday = new Date(beforeOneWeek.setDate(diffToMonday))
  631.                     lastSunday = new Date(beforeOneWeek2.setDate(diffToMonday + 6));
  632.                     datestringStart = lastMonday.getFullYear() + "-" + (lastMonday.getMonth()+1) + "-" + lastMonday.getDate();
  633.                     datestringEnd = lastSunday.getFullYear() + "-" + (lastSunday.getMonth()+1) + "-" + lastSunday.getDate();
  634.                     $('.uv-action-bar .date').find("#from").val(datestringStart);
  635.                     $('.uv-action-bar .date').find("#to").val(datestringEnd).trigger('change');
  636.                     break;
  637.                 case "day":
  638.                     start.setFullYear(hasta.getFullYear());
  639.                     start.setMonth(hasta.getMonth());
  640.                     start.setDate(hasta.getDate());
  641.                     start.setHours(0, 0, 0, 0);
  642.                     datestring =  start.getFullYear() + "-" + (start.getMonth()+1) + "-" + start.getDate();
  643.                     $('.uv-action-bar .date').find("#from").val(datestring);
  644.                     $('.uv-action-bar .date').find("#to").val(datestring).trigger('change');
  645.                     break;
  646.             }
  647.         })
  648.         let ticketsByStatus = {{report_service.ticketsByStatus|json_encode|raw}};
  649.         let ticketsByType = {{report_service.ticketsByType|json_encode|raw}};
  650.         let ticketsByPriority = {{report_service.ticketsByPriority|json_encode|raw}};
  651.         let ticketsPerAgent = {{report_service.ticketsPerAgent|json_encode|raw}};
  652.         let ticketsPerTeam = {{report_service.ticketsPerTeam|json_encode|raw}};
  653.         let kudos = {{report_service.kudos|json_encode|raw}};
  654.         $('.uv-action-bar .date #from').datetimepicker({
  655.             maxDate: new Date(currentYear, currentMonth, currentDate),
  656.             format: 'YYYY-MM-DD',
  657.         }).on('dp.change', function(e) {
  658.             filtroFechas();
  659.         });
  660.         $('.uv-action-bar .date #to').datetimepicker({
  661.             maxDate: new Date(currentYear, currentMonth, currentDate),
  662.             format: 'YYYY-MM-DD',
  663.         }).on('dp.change', function(e) {
  664.             filtroFechas();
  665.         });
  666.         const filtroFechas = () => {
  667.             const fromDate = $('.uv-action-bar .date').find("#from").val();
  668.             const toDate = $('.uv-action-bar .date').find("#to").val();
  669.             // Validar que las fechas estén presentes
  670.             if (!fromDate || !toDate) {
  671.                 console.warn('Fechas no definidas:', fromDate, toDate);
  672.                 return;
  673.             }
  674.             console.log('Enviando fechas al servidor:', fromDate, toDate);
  675.             $.ajax({
  676.                 type: "POST",
  677.                 url: "{{ path('helpdesk_member_report_charts_xhr') }}",
  678.                 data: {
  679.                     'from': fromDate,
  680.                     'to': toDate
  681.                 },
  682.                 dataType: "json",
  683.                 success: function(msg){
  684.                     console.log('Respuesta del servidor:', msg);
  685.                     // Actualizar datos
  686.                     ticketsByStatus = msg['ticketsByStatus'] || {};
  687.                     ticketsByType = msg['ticketsByType'] || {};
  688.                     ticketsByPriority = msg['ticketsByPriority'] || {};
  689.                     ticketsPerAgent = msg['ticketsPerAgent'] || {};
  690.                     ticketsPerTeam = msg['ticketsPerTeam'] || {};
  691.                     kudos = msg['kudos'] || {};
  692.                     // Reinicializar gráficos si no existen o si los datos cambiaron
  693.                     if (!graficoTicketByStatus && Object.keys(ticketsByStatus).length > 0) {
  694.                         graficoTicketByStatus = crearGrafico(ticketsByStatus, 'ticketsByStatus');
  695.                     }
  696.                     if (!graficoTicketByType && Object.keys(ticketsByType).length > 0) {
  697.                         graficoTicketByType = crearGrafico(ticketsByType, 'ticketsByType');
  698.                     }
  699.                     if (!graficoTicketByPriority && Object.keys(ticketsByPriority).length > 0) {
  700.                         graficoTicketByPriority = crearGrafico(ticketsByPriority, 'ticketsByPriority');
  701.                     }
  702.                     if (!graficoPerAgent && Object.keys(ticketsPerAgent).length > 0) {
  703.                         graficoPerAgent = crearGrafico(ticketsPerAgent, 'ticketsPerAgent');
  704.                     }
  705.                     if (!graficoPerTeam && Object.keys(ticketsPerTeam).length > 0) {
  706.                         graficoPerTeam = crearGrafico(ticketsPerTeam, 'ticketsPerTeam');
  707.                     }
  708.                     if (!graficoKudos && Object.keys(kudos).length > 0) {
  709.                         graficoKudos = crearGrafico(kudos, 'kudos');
  710.                     }
  711.                     // Actualizar gráficos existentes
  712.                     actualizarGraficos();
  713.                     
  714.                     // Actualizar nuevas métricas
  715.                     if (msg['kpiSummary']) {
  716.                         actualizarKPIs(msg['kpiSummary'], msg['lastTicketDate'], msg['ticketsLessThan5Days']);
  717.                     } else {
  718.                         actualizarKPIs(null, msg['lastTicketDate'], msg['ticketsLessThan5Days']);
  719.                     }
  720.                     
  721.                     // Tablas
  722.                     if (msg['agentsWithCount']) {
  723.                         console.log('Actualizando agentes:', msg['agentsWithCount']);
  724.                         actualizarAgentes(msg['agentsWithCount']);
  725.                     } else {
  726.                         console.log('No hay datos de agentes');
  727.                     }
  728.                     if (msg['customersWithCount']) {
  729.                         console.log('Actualizando clientes:', msg['customersWithCount']);
  730.                         actualizarTopClientes(msg['customersWithCount']);
  731.                     } else {
  732.                         console.log('No hay datos de clientes');
  733.                         const tbody = document.getElementById('customersTableBody');
  734.                         if (tbody) {
  735.                             tbody.innerHTML = '<tr><td colspan="2">{{ 'No data available'|trans }}</td></tr>';
  736.                         }
  737.                     }
  738.                     
  739.                     // Gráficos
  740.                     console.log('Datos recibidos para gráficos:', {
  741.                         ticketsByTypeStacked: msg['ticketsByTypeStacked'],
  742.                         incidentTypes: msg['incidentTypes'],
  743.                         ticketsByMonth: msg['ticketsByMonth'],
  744.                         closureTimeCategories: msg['closureTimeCategories']
  745.                     });
  746.                     
  747.                     if (msg['ticketsByTypeStacked'] && msg['ticketsByTypeStacked'].length > 0) {
  748.                         console.log('Creando gráfico de tipos apilados');
  749.                         crearGraficoTiposStacked(msg['ticketsByTypeStacked']);
  750.                     } else {
  751.                         console.log('No hay datos para ticketsByTypeStacked');
  752.                     }
  753.                     if (msg['incidentTypes'] && msg['incidentTypes'].length > 0) {
  754.                         console.log('Creando gráfico de tipos de incidentes');
  755.                         crearGraficoIncidentTypes(msg['incidentTypes']);
  756.                     } else {
  757.                         console.log('No hay datos para incidentTypes');
  758.                     }
  759.                     if (msg['ticketsByMonth'] && msg['ticketsByMonth'].length > 0) {
  760.                         console.log('Creando gráfico de tickets por mes');
  761.                         crearGraficoTicketsByMonth(msg['ticketsByMonth']);
  762.                     } else {
  763.                         console.log('No hay datos para ticketsByMonth');
  764.                     }
  765.                     if (msg['closureTimeCategories'] && msg['closureTimeCategories'].length > 0) {
  766.                         console.log('Creando gráfico de tiempo de cierre');
  767.                         crearGraficoClosureTime(msg['closureTimeCategories']);
  768.                     } else {
  769.                         console.log('No hay datos para closureTimeCategories');
  770.                     }
  771.                     
  772.                     // Gráficos adicionales (opcionales)
  773.                     if (msg['ticketsByDay']) {
  774.                         crearGraficoTicketsByDay(msg['ticketsByDay']);
  775.                     }
  776.                     if (msg['ticketsByHour']) {
  777.                         crearGraficoTicketsByHour(msg['ticketsByHour']);
  778.                     }
  779.                 }
  780.             });
  781.         }
  782.         // Funcionalidad para cargar Excel
  783.         $('#btnUploadExcel').on('click', function() {
  784.             $('#excelFileInput').click();
  785.         });
  786.         $('#excelFileInput').on('change', function(e) {
  787.             const file = e.target.files[0];
  788.             if (!file) return;
  789.             const formData = new FormData();
  790.             formData.append('excel_file', file);
  791.             $('#excelStatus').text('{{ 'Cargando...'|trans }}');
  792.             $.ajax({
  793.                 url: '/dashboard/excel/upload',
  794.                 type: 'POST',
  795.                 data: formData,
  796.                 processData: false,
  797.                 contentType: false,
  798.                 success: function(response) {
  799.                     if (response.success) {
  800.                         $('#excelStatus').text('{{ 'Excel cargado: '|trans }}' + response.rows + ' {{ 'filas'|trans }}');
  801.                         $('#btnClearExcel').show();
  802.                         // Recargar datos
  803.                         filtroFechas();
  804.                     } else {
  805.                         $('#excelStatus').text('{{ 'Error: '|trans }}' + response.message);
  806.                         alert(response.message);
  807.                     }
  808.                 },
  809.                 error: function(xhr) {
  810.                     const response = xhr.responseJSON || {};
  811.                     $('#excelStatus').text('{{ 'Error al cargar'|trans }}');
  812.                     alert(response.message || '{{ 'Error al cargar el archivo'|trans }}');
  813.                 }
  814.             });
  815.         });
  816.         $('#btnClearExcel').on('click', function() {
  817.             $.ajax({
  818.                 url: '/dashboard/excel/clear',
  819.                 type: 'POST',
  820.                 success: function(response) {
  821.                     if (response.success) {
  822.                         $('#excelStatus').text('');
  823.                         $('#btnClearExcel').hide();
  824.                         $('#excelFileInput').val('');
  825.                         // Recargar datos
  826.                         filtroFechas();
  827.                     }
  828.                 }
  829.             });
  830.         });
  831.         // Verificar estado al cargar
  832.         function checkExcelStatus() {
  833.             $.ajax({
  834.                 url: '/dashboard/excel/status',
  835.                 type: 'GET',
  836.                 success: function(response) {
  837.                     if (response.hasExcelData) {
  838.                         $('#excelStatus').text('{{ 'Usando Excel: '|trans }}' + response.rows + ' {{ 'filas'|trans }}');
  839.                         $('#btnClearExcel').show();
  840.                     }
  841.                 }
  842.             });
  843.         }
  844.         // Cargar datos iniciales al cargar la página
  845.         $(document).ready(function() {
  846.             // Verificar si hay Excel cargado
  847.             checkExcelStatus();
  848.             // Establecer fechas por defecto (últimos 30 días)
  849.             let endDate = new Date();
  850.             let startDate = new Date();
  851.             startDate.setDate(startDate.getDate() - 30);
  852.             
  853.             const formatDate = (date) => {
  854.                 const year = date.getFullYear();
  855.                 const month = String(date.getMonth() + 1).padStart(2, '0');
  856.                 const day = String(date.getDate()).padStart(2, '0');
  857.                 return `${year}-${month}-${day}`;
  858.             };
  859.             
  860.             $('.uv-action-bar .date').find("#from").val(formatDate(startDate));
  861.             $('.uv-action-bar .date').find("#to").val(formatDate(endDate));
  862.             
  863.             // Inicializar gráficos con datos iniciales si existen
  864.             inicializarGraficos();
  865.             
  866.             // Cargar datos iniciales
  867.             filtroFechas();
  868.         });
  869.         const capitalizeFirstLetter = (string) => {
  870.             return string.charAt(0).toUpperCase() + string.slice(1);
  871.         }
  872.         const crearGrafico = (tickets, nombre) => {
  873.             let tipo = 'pie'
  874.             let labels = Object.values(tickets).map(function (el) {return el.Nombre })
  875.             let label = ''
  876.             let options = {
  877.                 legend: { display: false }
  878.             }
  879.             switch(nombre) {
  880.                 case 'kudos':
  881.                     tipo = 'bar';
  882.                     options.indexAxis = 'y';
  883.                     break;
  884.                 case 'ticketsByStatus':
  885.                     tipo = 'bar';
  886.                     options.indexAxis = 'y';
  887.                     break;
  888.                 case 'ticketsByPriority':
  889.                     tipo = 'bar';
  890.                     options.indexAxis = 'y';
  891.                     break;
  892.             }
  893.             return new Chart(
  894.                 document.getElementById(nombre),
  895.                 {
  896.                     type: tipo,
  897.                     data: {
  898.                         labels: labels,
  899.                         datasets: [{
  900.                             label: label,
  901.                             data: Object.values(tickets).map(function (el) { return el.Cantidad }),
  902.                             backgroundColor: Object.values(tickets).map(function (el) { return el.Color }),
  903.                             hoverOffset: 4
  904.                         }]
  905.                     },
  906.                     options: options
  907.                 }
  908.             )
  909.         }
  910.         const actualizaGrafico = (grafico, tickets, nombre) => {
  911.             if (!grafico || !tickets || Object.keys(tickets).length === 0) {
  912.                 return;
  913.             }
  914.             let labels = Object.values(tickets).map(function (el) {return el.Nombre })
  915.             const data = {
  916.                 labels: labels,
  917.                 datasets: [
  918.                     {
  919.                         label: '',
  920.                         data: Object.values(tickets).map(function (el) { return el.Cantidad }),
  921.                         backgroundColor: Object.values(tickets).map(function (el) { return el.Color }),
  922.                         hoverOffset: 4
  923.                     }
  924.                 ]
  925.             };
  926.             grafico.data.labels = data.labels;
  927.             grafico.data.datasets = data.datasets;
  928.             grafico.update();
  929.             let html = '';
  930.             $.each(tickets, function (key, value) {
  931.                 let Color = value.Color;
  932.                 let Cantidad = value.Cantidad;
  933.                 let Nombre = value.Nombre;
  934.                 html += '<label><span class="boch" style="background: '+ Color +'"></span>  '+ Cantidad +' - '+ Nombre +' </label>';
  935.             });
  936.             $('#'+nombre).html(html);
  937.         }
  938.         // Declarar variables de gráficos globalmente
  939.         let graficoTicketByStatus = null;
  940.         let graficoTicketByType = null;
  941.         let graficoTicketByPriority = null;
  942.         let graficoPerAgent = null;
  943.         let graficoPerTeam = null;
  944.         let graficoKudos = null;
  945.         // Función para actualizar gráficos - debe estar antes de filtroFechas
  946.         const actualizarGraficos = () => {
  947.             if (graficoTicketByStatus && ticketsByStatus) {
  948.             actualizaGrafico(graficoTicketByStatus, ticketsByStatus, 'contenidoticketsByStatus');
  949.             }
  950.             if (graficoTicketByType && ticketsByType) {
  951.             actualizaGrafico(graficoTicketByType, ticketsByType, 'contenidoticketsByType');
  952.             }
  953.             if (graficoTicketByPriority && ticketsByPriority) {
  954.             actualizaGrafico(graficoTicketByPriority, ticketsByPriority, 'contenidoticketsByPriority');
  955.             }
  956.             if (graficoPerAgent && ticketsPerAgent) {
  957.             actualizaGrafico(graficoPerAgent, ticketsPerAgent, 'contenidoticketsPerAgent');
  958.             }
  959.             if (graficoPerTeam && ticketsPerTeam) {
  960.             actualizaGrafico(graficoPerTeam, ticketsPerTeam, 'contenidoticketsPerTeam');
  961.             }
  962.             if (graficoKudos && kudos) {
  963.             actualizaGrafico(graficoKudos, kudos, 'contenidokudos');
  964.             }
  965.         }
  966.         // Inicializar gráficos solo si hay datos
  967.         const inicializarGraficos = () => {
  968.             if (Object.keys(ticketsByStatus).length > 0) {
  969.                 graficoTicketByStatus = crearGrafico(ticketsByStatus, 'ticketsByStatus');
  970.             }
  971.             if (Object.keys(ticketsByType).length > 0) {
  972.                 graficoTicketByType = crearGrafico(ticketsByType, 'ticketsByType');
  973.             }
  974.             if (Object.keys(ticketsByPriority).length > 0) {
  975.                 graficoTicketByPriority = crearGrafico(ticketsByPriority, 'ticketsByPriority');
  976.             }
  977.             if (Object.keys(ticketsPerAgent).length > 0) {
  978.                 graficoPerAgent = crearGrafico(ticketsPerAgent, 'ticketsPerAgent');
  979.             }
  980.             if (Object.keys(ticketsPerTeam).length > 0) {
  981.                 graficoPerTeam = crearGrafico(ticketsPerTeam, 'ticketsPerTeam');
  982.             }
  983.             if (Object.keys(kudos).length > 0) {
  984.                 graficoKudos = crearGrafico(kudos, 'kudos');
  985.             }
  986.         }
  987.         // Variables globales para nuevos gráficos
  988.         let ticketsByDayChart = null;
  989.         let ticketsOpenVsClosedChart = null;
  990.         let ticketsByHourChart = null;
  991.         let ticketsByMonthChart = null;
  992.         let ticketsByTypeStackedChart = null;
  993.         let incidentTypesChart = null;
  994.         let closureTimeChart = null;
  995.         // Función para actualizar KPIs
  996.         const actualizarKPIs = (kpiData, lastTicketDate, lessThan5Days) => {
  997.             if (kpiData) {
  998.                 document.getElementById('kpiTotalTickets').textContent = kpiData.total_tickets || 0;
  999.                 document.getElementById('kpiResolvedTickets').textContent = kpiData.tickets_resueltos || 0;
  1000.                 document.getElementById('kpiOpenTickets').textContent = kpiData.tickets_abiertos || 0;
  1001.             }
  1002.             if (lastTicketDate) {
  1003.                 document.getElementById('kpiLastTicketDate').textContent = lastTicketDate;
  1004.             }
  1005.             if (lessThan5Days !== undefined) {
  1006.                 document.getElementById('kpiLessThan5Days').textContent = lessThan5Days.toLocaleString();
  1007.             }
  1008.         }
  1009.         // Función para crear gráfico de tendencia temporal
  1010.         const crearGraficoTicketsByDay = (data) => {
  1011.             const ctx = document.getElementById('ticketsByDayChart');
  1012.             if (!ctx) return;
  1013.             if (ticketsByDayChart) {
  1014.                 ticketsByDayChart.destroy();
  1015.             }
  1016.             const labels = data.map(item => item.fecha);
  1017.             const values = data.map(item => item.cantidad);
  1018.             ticketsByDayChart = new Chart(ctx, {
  1019.                 type: 'line',
  1020.                 data: {
  1021.                     labels: labels,
  1022.                     datasets: [{
  1023.                         label: '{{ 'Tickets Created'|trans }}',
  1024.                         data: values,
  1025.                         borderColor: 'rgb(75, 192, 192)',
  1026.                         backgroundColor: 'rgba(75, 192, 192, 0.2)',
  1027.                         tension: 0.1
  1028.                     }]
  1029.                 },
  1030.                 options: {
  1031.                     responsive: true,
  1032.                     maintainAspectRatio: true,
  1033.                     scales: {
  1034.                         y: {
  1035.                             beginAtZero: true,
  1036.                             ticks: {
  1037.                                 stepSize: 1,
  1038.                                 precision: 0
  1039.                             }
  1040.                         }
  1041.                     }
  1042.                 }
  1043.             });
  1044.         }
  1045.         // Función para crear gráfico de tickets abiertos vs cerrados
  1046.         const crearGraficoTicketsOpenVsClosed = (data) => {
  1047.             const ctx = document.getElementById('ticketsOpenVsClosedChart');
  1048.             if (!ctx) return;
  1049.             if (ticketsOpenVsClosedChart) {
  1050.                 ticketsOpenVsClosedChart.destroy();
  1051.             }
  1052.             const labels = data.map(item => item.fecha);
  1053.             const abiertos = data.map(item => item.abiertos);
  1054.             const cerrados = data.map(item => item.cerrados);
  1055.             ticketsOpenVsClosedChart = new Chart(ctx, {
  1056.                 type: 'bar',
  1057.                 data: {
  1058.                     labels: labels,
  1059.                     datasets: [
  1060.                         {
  1061.                             label: '{{ 'Opened'|trans }}',
  1062.                             data: abiertos,
  1063.                             backgroundColor: 'rgba(255, 99, 132, 0.5)',
  1064.                             borderColor: 'rgb(255, 99, 132)',
  1065.                             borderWidth: 1
  1066.                         },
  1067.                         {
  1068.                             label: '{{ 'Closed'|trans }}',
  1069.                             data: cerrados,
  1070.                             backgroundColor: 'rgba(54, 162, 235, 0.5)',
  1071.                             borderColor: 'rgb(54, 162, 235)',
  1072.                             borderWidth: 1
  1073.                         }
  1074.                     ]
  1075.                 },
  1076.                 options: {
  1077.                     responsive: true,
  1078.                     maintainAspectRatio: true,
  1079.                     scales: {
  1080.                         y: {
  1081.                             beginAtZero: true,
  1082.                             ticks: {
  1083.                                 stepSize: 1,
  1084.                                 precision: 0
  1085.                             }
  1086.                         }
  1087.                     }
  1088.                 }
  1089.             });
  1090.         }
  1091.         // Función para crear gráfico de tickets por hora
  1092.         const crearGraficoTicketsByHour = (data) => {
  1093.             const ctx = document.getElementById('ticketsByHourChart');
  1094.             if (!ctx) return;
  1095.             if (ticketsByHourChart) {
  1096.                 ticketsByHourChart.destroy();
  1097.             }
  1098.             // Crear array completo de 24 horas
  1099.             const hoursData = Array.from({length: 24}, (_, i) => {
  1100.                 const found = data.find(item => item.hora === i);
  1101.                 return found ? found.cantidad : 0;
  1102.             });
  1103.             const labels = Array.from({length: 24}, (_, i) => i + ':00');
  1104.             ticketsByHourChart = new Chart(ctx, {
  1105.                 type: 'bar',
  1106.                 data: {
  1107.                     labels: labels,
  1108.                     datasets: [{
  1109.                         label: '{{ 'Tickets'|trans }}',
  1110.                         data: hoursData,
  1111.                         backgroundColor: 'rgba(153, 102, 255, 0.5)',
  1112.                         borderColor: 'rgb(153, 102, 255)',
  1113.                         borderWidth: 1
  1114.                     }]
  1115.                 },
  1116.                 options: {
  1117.                     responsive: true,
  1118.                     maintainAspectRatio: true,
  1119.                     scales: {
  1120.                         y: {
  1121.                             beginAtZero: true,
  1122.                             ticks: {
  1123.                                 stepSize: 1,
  1124.                                 precision: 0
  1125.                             }
  1126.                         }
  1127.                     }
  1128.                 }
  1129.             });
  1130.         }
  1131.         // Función para actualizar tabla de top agentes
  1132.         const actualizarTopAgentes = (data) => {
  1133.             const tbody = document.getElementById('topAgentsTableBody');
  1134.             if (!tbody) return;
  1135.             if (!data || data.length === 0) {
  1136.                 tbody.innerHTML = '<tr><td colspan="2">{{ 'No data available'|trans }}</td></tr>';
  1137.                 return;
  1138.             }
  1139.             tbody.innerHTML = data.map(agent => `
  1140.                 <tr>
  1141.                     <td>${agent.agente || '-'}</td>
  1142.                     <td>${agent.tickets_resueltos || 0}</td>
  1143.                 </tr>
  1144.             `).join('');
  1145.         }
  1146.         // Función para actualizar tabla de top clientes
  1147.         const actualizarTopClientes = (data) => {
  1148.             const tbody = document.getElementById('customersTableBody');
  1149.             if (!tbody) {
  1150.                 console.error('No se encontró el elemento customersTableBody');
  1151.                 return;
  1152.             }
  1153.             console.log('Actualizando tabla de clientes con datos:', data);
  1154.             if (!data || data.length === 0) {
  1155.                 tbody.innerHTML = '<tr><td colspan="2">{{ 'No data available'|trans }}</td></tr>';
  1156.                 return;
  1157.             }
  1158.             tbody.innerHTML = data.map(customer => `
  1159.                 <tr>
  1160.                     <td>${customer.cliente || '-'}</td>
  1161.                     <td>${customer.total_tickets || 0}</td>
  1162.                 </tr>
  1163.             `).join('');
  1164.         }
  1165.         // Función para actualizar tabla de agentes
  1166.         const actualizarAgentes = (data) => {
  1167.             const tbody = document.getElementById('agentsTableBody');
  1168.             if (!tbody) return;
  1169.             if (!data || data.length === 0) {
  1170.                 tbody.innerHTML = '<tr><td colspan="2">{{ 'No data available'|trans }}</td></tr>';
  1171.                 return;
  1172.             }
  1173.             tbody.innerHTML = data.map(agent => `
  1174.                 <tr>
  1175.                     <td>${agent.agente || '-'}</td>
  1176.                     <td>${agent.total_tickets || 0}</td>
  1177.                 </tr>
  1178.             `).join('');
  1179.         }
  1180.         // Función para crear gráfico apilado de tipos
  1181.         const crearGraficoTiposStacked = (data) => {
  1182.             const ctx = document.getElementById('ticketsByTypeStackedChart');
  1183.             if (!ctx) {
  1184.                 console.error('No se encontró el elemento ticketsByTypeStackedChart');
  1185.                 return;
  1186.             }
  1187.             if (ticketsByTypeStackedChart) {
  1188.                 ticketsByTypeStackedChart.destroy();
  1189.             }
  1190.             console.log('Datos para gráfico apilado:', data);
  1191.             const labels = ['Todo'];
  1192.             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)'];
  1193.             const datasets = data.map((item, index) => ({
  1194.                 label: item.tipo || 'Sin tipo',
  1195.                 data: [item.cantidad || 0],
  1196.                 backgroundColor: colors[index % colors.length] || 'rgba(201, 203, 207, 0.8)'
  1197.             }));
  1198.             ticketsByTypeStackedChart = new Chart(ctx, {
  1199.                 type: 'bar',
  1200.                 data: {
  1201.                     labels: labels,
  1202.                     datasets: datasets
  1203.                 },
  1204.                 options: {
  1205.                     indexAxis: 'y',
  1206.                     responsive: true,
  1207.                     maintainAspectRatio: true,
  1208.                     plugins: {
  1209.                         legend: {
  1210.                             display: true,
  1211.                             position: 'top'
  1212.                         }
  1213.                     },
  1214.                     scales: {
  1215.                         x: {
  1216.                             stacked: true,
  1217.                             beginAtZero: true,
  1218.                             ticks: {
  1219.                                 stepSize: 1,
  1220.                                 precision: 0
  1221.                             }
  1222.                         },
  1223.                         y: {
  1224.                             stacked: true
  1225.                         }
  1226.                     }
  1227.                 }
  1228.             });
  1229.         }
  1230.         // Función para crear pie chart de tipos de incidentes
  1231.         const crearGraficoIncidentTypes = (data) => {
  1232.             const ctx = document.getElementById('incidentTypesChart');
  1233.             if (!ctx) {
  1234.                 console.error('No se encontró el elemento incidentTypesChart');
  1235.                 return;
  1236.             }
  1237.             if (incidentTypesChart) {
  1238.                 incidentTypesChart.destroy();
  1239.             }
  1240.             console.log('Datos para gráfico de incidentes:', data);
  1241.             const total = data.reduce((sum, item) => sum + (item.cantidad || 0), 0);
  1242.             const labels = data.map(item => {
  1243.                 const cantidad = item.cantidad || 0;
  1244.                 const percentage = total > 0 ? ((cantidad / total) * 100).toFixed(1) : 0;
  1245.                 return `${item.tipo || 'Sin tipo'} (${percentage}%)`;
  1246.             });
  1247.             const values = data.map(item => item.cantidad || 0);
  1248.             // Colores para el pie chart
  1249.             const colors = [
  1250.                 'rgba(255, 99, 132, 0.8)', 'rgba(54, 162, 235, 0.8)', 'rgba(255, 206, 86, 0.8)',
  1251.                 'rgba(75, 192, 192, 0.8)', 'rgba(153, 102, 255, 0.8)', 'rgba(255, 159, 64, 0.8)',
  1252.                 'rgba(199, 199, 199, 0.8)', 'rgba(83, 102, 255, 0.8)', 'rgba(255, 99, 255, 0.8)',
  1253.                 'rgba(99, 255, 132, 0.8)', 'rgba(255, 159, 132, 0.8)', 'rgba(132, 99, 255, 0.8)',
  1254.                 'rgba(99, 132, 255, 0.8)', 'rgba(255, 99, 99, 0.8)', 'rgba(99, 255, 255, 0.8)'
  1255.             ];
  1256.             incidentTypesChart = new Chart(ctx, {
  1257.                 type: 'pie',
  1258.                 data: {
  1259.                     labels: labels,
  1260.                     datasets: [{
  1261.                         data: values,
  1262.                         backgroundColor: colors.slice(0, data.length)
  1263.                     }]
  1264.                 },
  1265.                 options: {
  1266.                     responsive: true,
  1267.                     maintainAspectRatio: true,
  1268.                     plugins: {
  1269.                         legend: {
  1270.                             position: 'right',
  1271.                             labels: {
  1272.                                 boxWidth: 12,
  1273.                                 font: {
  1274.                                     size: 10
  1275.                                 }
  1276.                             }
  1277.                         }
  1278.                     }
  1279.                 }
  1280.             });
  1281.         }
  1282.         // Función para crear gráfico de tickets por mes
  1283.         const crearGraficoTicketsByMonth = (data) => {
  1284.             const ctx = document.getElementById('ticketsByMonthChart');
  1285.             if (!ctx) {
  1286.                 console.error('No se encontró el elemento ticketsByMonthChart');
  1287.                 return;
  1288.             }
  1289.             if (ticketsByMonthChart) {
  1290.                 ticketsByMonthChart.destroy();
  1291.             }
  1292.             console.log('Datos para gráfico por mes:', data);
  1293.             const labels = data.map(item => item.mes_formateado || item.mes || '');
  1294.             const values = data.map(item => item.cantidad || 0);
  1295.             ticketsByMonthChart = new Chart(ctx, {
  1296.                 type: 'bar',
  1297.                 data: {
  1298.                     labels: labels,
  1299.                     datasets: [{
  1300.                         label: '{{ 'Tickets'|trans }}',
  1301.                         data: values,
  1302.                         backgroundColor: 'rgba(54, 162, 235, 0.8)',
  1303.                         borderColor: 'rgb(54, 162, 235)',
  1304.                         borderWidth: 1
  1305.                     }]
  1306.                 },
  1307.                 options: {
  1308.                     responsive: true,
  1309.                     maintainAspectRatio: true,
  1310.                     scales: {
  1311.                         y: {
  1312.                             beginAtZero: true,
  1313.                             ticks: {
  1314.                                 stepSize: 1,
  1315.                                 precision: 0
  1316.                             }
  1317.                         }
  1318.                     }
  1319.                 }
  1320.             });
  1321.         }
  1322.         // Función para crear gráfico de tiempo de cierre
  1323.         const crearGraficoClosureTime = (data) => {
  1324.             const ctx = document.getElementById('closureTimeChart');
  1325.             if (!ctx) {
  1326.                 console.error('No se encontró el elemento closureTimeChart');
  1327.                 return;
  1328.             }
  1329.             if (closureTimeChart) {
  1330.                 closureTimeChart.destroy();
  1331.             }
  1332.             console.log('Datos para gráfico de tiempo de cierre:', data);
  1333.             const labels = data.map(item => item.categoria || '');
  1334.             const values = data.map(item => item.cantidad || 0);
  1335.             // Colores específicos para cada categoría
  1336.             const colors = labels.map((label, index) => {
  1337.                 const categoryColors = {
  1338.                     'Menos 30 min': 'rgba(75, 192, 192, 0.8)',
  1339.                     'Menos 2 horas': 'rgba(153, 102, 255, 0.8)',
  1340.                     'Menos 1 día': 'rgba(255, 159, 64, 0.8)',
  1341.                     'Menos 5 días': 'rgba(255, 99, 132, 0.8)',
  1342.                     'Menos 15 días': 'rgba(255, 206, 86, 0.8)',
  1343.                     'Más de 15 días': 'rgba(54, 162, 235, 0.8)',
  1344.                     'No Cerrado': 'rgba(201, 203, 207, 0.8)'
  1345.                 };
  1346.                 return categoryColors[label] || 'rgba(199, 199, 199, 0.8)';
  1347.             });
  1348.             closureTimeChart = new Chart(ctx, {
  1349.                 type: 'bar',
  1350.                 data: {
  1351.                     labels: labels,
  1352.                     datasets: [{
  1353.                         label: '{{ 'Tickets'|trans }}',
  1354.                         data: values,
  1355.                         backgroundColor: colors,
  1356.                         borderColor: colors.map(c => (c && typeof c === 'string') ? c.replace('0.8', '1') : 'rgba(199, 199, 199, 1)'),
  1357.                         borderWidth: 1
  1358.                     }]
  1359.                 },
  1360.                 options: {
  1361.                     indexAxis: 'y',
  1362.                     responsive: true,
  1363.                     maintainAspectRatio: true,
  1364.                     scales: {
  1365.                         x: {
  1366.                             beginAtZero: true,
  1367.                             ticks: {
  1368.                                 stepSize: 1,
  1369.                                 precision: 0
  1370.                             }
  1371.                         }
  1372.                     }
  1373.                 }
  1374.             });
  1375.         }
  1376.     </script>
  1377. {% endblock %}