| <!doctype html> |
| <html lang="en"> |
| <head> |
| <meta charset="utf-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> |
| |
| <meta name="description" content=""> |
| <meta name="author" content=""> |
|
|
| <link rel="icon" href="favicon.ico"> |
| <title>Dashboard</title> |
| <link rel="stylesheet" href="css/simplebar.css"> |
| <link href="https://fonts.googleapis.com/css2?family=Overpass:ital,wght@0,100;0,200;0,300;0,400;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,600;1,700;1,800;1,900&display=swap" rel="stylesheet"> |
| <script src="https://cdn.jsdelivr.net/npm/feather-icons/dist/feather.min.js"></script> |
| <link rel="stylesheet" href="css/select2.css"> |
| <link rel="stylesheet" href="css/dropzone.css"> |
| <link rel="stylesheet" href="css/uppy.min.css"> |
| <link rel="stylesheet" href="css/jquery.steps.css"> |
| <link rel="stylesheet" href="css/jquery.timepicker.css"> |
| <link rel="stylesheet" href="css/quill.snow.css"> |
| <link rel="stylesheet" href="css/daterangepicker.css"> |
| <style id="themeBlock">html{visibility:hidden}</style> |
| <style> |
| .circle { |
| display: flex !important; |
| align-items: center !important; |
| justify-content: center !important; |
| } |
| |
| #leftSidebar.collapsed #sidebarLogo, |
| body.collapsed #leftSidebar #sidebarLogo, |
| #leftSidebar[style*="width: 0"] #sidebarLogo { |
| max-width: 36px !important; |
| width: 36px !important; |
| } |
| </style> |
| <script> |
| (function(){ |
| var dark = localStorage.getItem('theme') === 'dark'; |
| document.write( |
| '<link rel="stylesheet" href="css/app-' + (dark?'dark':'light') + '.css" id="' + (dark?'dark':'light') + 'Theme">' + |
| '<link rel="stylesheet" href="css/app-' + (dark?'light':'dark') + '.css" id="' + (dark?'light':'dark') + 'Theme" disabled>' |
| ); |
| if(dark) document.documentElement.style.backgroundColor = '#1a1a2e'; |
| })(); |
| </script> |
| <style>html{visibility:visible}</style> |
| |
| </head> |
| <body class="vertical light "> |
| <div class="wrapper"> |
| <nav class="topnav navbar navbar-light"> |
| <button type="button" class="navbar-toggler text-muted mt-2 p-0 mr-3 collapseSidebar"> |
| <i data-feather="menu" class="navbar-toggler-icon"></i> |
| </button> |
|
|
| <ul class="nav"> |
| <li class="nav-item"> |
| <a class="nav-link text-muted my-2" href="#" id="modeSwitcher" data-mode="light"> |
| <i data-feather="sun"></i> |
| </a> |
| </li> |
|
|
|
|
|
|
| <li class="nav-item"> |
| <a href="#" onclick="confirmLogout()" class="nav-link text-muted" title="Cerrar sesión" style="display: flex; align-items: center; gap: 6px;"> |
| <span class="avatar avatar-sm mt-2"> |
| <img src="./assets/images/persona.png" alt="..." class="avatar-img rounded-circle"> |
| </span> |
| <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" title="Cerrar sesión"> |
| <path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/> |
| <polyline points="16 17 21 12 16 7"/> |
| <line x1="21" y1="12" x2="9" y2="12"/> |
| </svg> |
| </a> |
| </li> |
| </ul> |
| </nav> |
| <aside class="sidebar-left border-right bg-white shadow" id="leftSidebar" data-simplebar> |
| <a href="#" class="btn collapseSidebar toggle-btn d-lg-none text-muted ml-2 mt-3" data-toggle="toggle"> |
| <i data-feather="x"><span class="sr-only"></span></i> |
| </a> |
| <nav class="vertnav navbar navbar-light"> |
| |
| <div class="w-100 mb-4 d-flex"> |
| <a class="navbar-brand mx-auto mt-2 flex-fill text-center"> |
| <img src="assets/images/RetinAILogo.png" alt="Logo" id="sidebarLogo" style="max-width:140px; transition: opacity 0.3s ease;"> |
| <img src="assets/images/LogoMini.png" alt="Logo Mini" id="sidebarLogoMini" style="max-width:44px; display:none; transition: opacity 0.3s ease;"> |
| </a> |
| </div> |
|
|
| <ul class="navbar-nav flex-fill w-100 mb-2"> |
| <li class="nav-item active"> |
| <a class="nav-link" href="./index.html"> |
| <i data-feather="home" class="fe-16"></i> |
| <span class="ml-3 item-text">Dashboard</span> |
| </a> |
| </li> |
| <li class="nav-item"> |
| <a class="nav-link" href="./diagnosis.html"> |
| <i data-feather="eye" class="fe-16"></i> |
| <span class="ml-3 item-text">Diagnóstico</span> |
| </a> |
| </li> |
| <li class="nav-item"> |
| <a class="nav-link" href="./patients.html"> |
| <i data-feather="users" class="fe-16"></i> |
| <span class="ml-3 item-text">Pacientes</span> |
| </a> |
| </li> |
| <li class="nav-item"> |
| <a class="nav-link" href="./historial-consultas.html"> |
| <i data-feather="file-text" class="fe-16"></i> |
| <span class="ml-3 item-text">Consultas</span> |
| </a> |
| </li> |
| <li class="nav-item"> |
| <a class="nav-link" href="./users.html"> |
| <i data-feather="user" class="fe-16"></i> |
| <span class="ml-3 item-text">Usuarios</span> |
| </a> |
| </li> |
| </ul> |
| </nav> |
| </aside> |
| <main role="main" class="main-content"> |
| <div class="container-fluid"> |
| <div class="row justify-content-center"> |
| <div class="col-12"> |
| <div class="row mb-4"> |
| <div class="col-md-6 col-lg-3"> |
| <div class="card shadow border-0 mb-4" id="totalPatientsCard"> |
| <div class="card-body"> |
| <div class="row align-items-center"> |
| <div class="col-3 text-center"> |
| <span class="circle circle-sm bg-primary"> |
| <i data-feather="users" class="fe-16 text-white"></i> |
| </span> |
| </div> |
| <div class="col pr-0"> |
| <p class="small text-muted mb-0">Pacientes Diagnosticados</p> |
| <span class="h3 mb-0">0</span> |
| </div> |
| </div> |
| </div> |
| </div> |
| </div> |
| <div class="col-md-6 col-lg-3"> |
| <div class="card shadow border-0 mb-4" id="positiveCasesCard"> |
| <div class="card-body"> |
| <div class="row align-items-center"> |
| <div class="col-3 text-center"> |
| <span class="circle circle-sm bg-warning"> |
| <i data-feather="alert-triangle" class="fe-16 text-white"></i> |
| </span> |
| </div> |
| <div class="col pr-0"> |
| <p class="small text-muted mb-0">Diagnósticos con Retinopatía</p> |
| <span class="h3 mb-0">0</span> |
| </div> |
| </div> |
| </div> |
| </div> |
| </div> |
| <div class="col-md-6 col-lg-3"> |
| <div class="card shadow border-0 mb-4" id="negativeCasesCard"> |
| <div class="card-body"> |
| <div class="row align-items-center"> |
| <div class="col-3 text-center"> |
| <span class="circle circle-sm bg-success"> |
| <i data-feather="check" class="fe-16 text-white"></i> |
| </span> |
| </div> |
| <div class="col pr-0"> |
| <p class="small text-muted mb-0">Diagnósticos sin Retinopatía</p> |
| <span class="h3 mb-0">0</span> |
| </div> |
| </div> |
| </div> |
| </div> |
| </div> |
| <div class="col-md-6 col-lg-3"> |
| <div class="card shadow border-0 mb-4" id="positivityRateCard"> |
| <div class="card-body"> |
| <div class="row align-items-center"> |
| <div class="col-3 text-center"> |
| <span class="circle circle-sm bg-info"> |
| <i data-feather="percent" class="fe-16 text-white"></i> |
| </span> |
| </div> |
| <div class="col pr-0"> |
| <p class="small text-muted mb-0">Tasa Positividad</p> |
| <span class="h3 mb-0">0%</span> |
| </div> |
| </div> |
| </div> |
| </div> |
| </div> |
| </div> |
|
|
| <div class="mb-2 align-items-center"> |
| <div class="card shadow mb-4"> |
| <div class="card-body"> |
| <div class="row mt-1 align-items-center"></div> |
| <div class="chartbox mr-4"> |
| <div class="d-flex justify-content-between align-items-center mb-3"> |
| <h6 class="card-title mb-0">Estadísticas de Consultas</h6> |
| </div> |
| <div id="areaChart"></div> |
| </div> |
| </div> |
| </div> |
| </div> |
|
|
| <div class="row"> |
| <div class="col-md-12 col-lg-4"> |
| <div class="card shadow h-100 mb-4"> |
| <div class="card-header"> |
| <h6 class="card-title mb-0"> |
| <i data-feather="pie-chart" class="mr-2"></i> |
| Rangos de Edad con Retinopatía |
| </h6> |
| </div> |
| <div class="card-body d-flex flex-column"> |
| <div class="chart-widget mb-2 flex-grow-1 d-flex align-items-center justify-content-center"> |
| <div id="ageRangesChart" style="width: 100%; height: 300px;"></div> |
| </div> |
| <div class="row items-align-center mt-auto" id="ageStatsRow"> |
| <div class="col-4 text-center"> |
| <p class="text-muted mb-1">Promedio</p> |
| <h6 class="mb-1" id="avgAge">-</h6> |
| <p class="text-muted mb-0">años</p> |
| </div> |
| <div class="col-4 text-center"> |
| <p class="text-muted mb-1">Rango</p> |
| <h6 class="mb-1" id="ageRange">-</h6> |
| <p class="text-muted mb-0">min-max</p> |
| </div> |
| <div class="col-4 text-center"> |
| <p class="text-muted mb-1">Total</p> |
| <h6 class="mb-1" id="totalPatientsAge">-</h6> |
| <p class="text-muted mb-0">con RD</p> |
| </div> |
| </div> |
| </div> |
| </div> |
| </div> |
|
|
| <div class="col-md-12 col-lg-8"> |
| <div class="card shadow h-100"> |
| <div class="card-header"> |
| <strong class="card-title">Consultas Recientes</strong> |
| </div> |
| <div class="card-body my-n2"> |
| <table class="table table-striped table-hover table-borderless"> |
| <thead> |
| <tr> |
| <th>Fecha</th> |
| <th>Paciente</th> |
| <th>Resultado</th> |
| <th>Confianza</th> |
| </tr> |
| </thead> |
| <tbody id="recentConsultationsBody"> |
| <tr> |
| <td colspan="5" class="text-center text-muted"> |
| <i data-feather="loader" class="fe-16 spinner"></i> Cargando consultas... |
| </td> |
| </tr> |
| </tbody> |
| </table> |
| </div> |
| </div> |
| </div> |
| </div> |
| </div> |
| </div> |
| </div> |
| <div class="modal fade modal-notif modal-slide" tabindex="-1" role="dialog" aria-labelledby="defaultModalLabel" aria-hidden="true"> |
| <div class="modal-dialog modal-sm" role="document"> |
| <div class="modal-content"> |
| <div class="modal-header"> |
| <h5 class="modal-title" id="defaultModalLabel">Notifications</h5> |
| <button type="button" class="close" data-dismiss="modal" aria-label="Close"> |
| <span aria-hidden="true">×</span> |
| </button> |
| </div> |
| <div class="modal-body"> |
| <div class="list-group list-group-flush my-n3"> |
| <div class="list-group-item bg-transparent"> |
| <div class="row align-items-center"> |
| <div class="col-auto"> |
| <span data-feather="box" class="fe-24"></span> |
| </div> |
| <div class="col"> |
| <small><strong>Package has uploaded successfull</strong></small> |
| <div class="my-0 text-muted small">Package is zipped and uploaded</div> |
| <small class="badge badge-pill badge-light text-muted">1m ago</small> |
| </div> |
| </div> |
| </div> |
| <div class="list-group-item bg-transparent"> |
| <div class="row align-items-center"> |
| <div class="col-auto"> |
| <span data-feather="download" class="fe-24"></span> |
| </div> |
| <div class="col"> |
| <small><strong>Widgets are updated successfull</strong></small> |
| <div class="my-0 text-muted small">Just create new layout Index, form, table</div> |
| <small class="badge badge-pill badge-light text-muted">2m ago</small> |
| </div> |
| </div> |
| </div> |
| <div class="list-group-item bg-transparent"> |
| <div class="row align-items-center"> |
| <div class="col-auto"> |
| <span data-feather="inbox" class="fe-24"></span> |
| </div> |
| <div class="col"> |
| <small><strong>Notifications have been sent</strong></small> |
| <div class="my-0 text-muted small">Fusce dapibus, tellus ac cursus commodo</div> |
| <small class="badge badge-pill badge-light text-muted">30m ago</small> |
| </div> |
| </div> |
| </div> |
| <div class="list-group-item bg-transparent"> |
| <div class="row align-items-center"> |
| <div class="col-auto"> |
| <span data-feather="link" class="fe-24"></span> |
| </div> |
| <div class="col"> |
| <small><strong>Link was attached to menu</strong></small> |
| <div class="my-0 text-muted small">New layout has been attached to the menu</div> |
| <small class="badge badge-pill badge-light text-muted">1h ago</small> |
| </div> |
| </div> |
| </div> |
| </div> |
| </div> |
| <div class="modal-footer"> |
| <button type="button" class="btn btn-secondary btn-block" data-dismiss="modal">Clear All</button> |
| </div> |
| </div> |
| </div> |
| </div> |
| <div class="modal fade modal-shortcut modal-slide" tabindex="-1" role="dialog" aria-labelledby="defaultModalLabel" aria-hidden="true"> |
| <div class="modal-dialog" role="document"> |
| <div class="modal-content"> |
| <div class="modal-header"> |
| <h5 class="modal-title" id="defaultModalLabel">Shortcuts</h5> |
| <button type="button" class="close" data-dismiss="modal" aria-label="Close"> |
| <span aria-hidden="true">×</span> |
| </button> |
| </div> |
| <div class="modal-body px-5"> |
| <div class="row align-items-center"> |
| <div class="col-6 text-center"> |
| <div class="squircle bg-success justify-content-center"> |
| <i data-feather="cpu" class="fe-32 align-self-center text-white"></i> |
| </div> |
| <p>Control area</p> |
| </div> |
| <div class="col-6 text-center"> |
| <div class="squircle bg-primary justify-content-center"> |
| <i data-feather="activity" class="fe-32 align-self-center text-white"></i> |
| </div> |
| <p>Activity</p> |
| </div> |
| </div> |
| <div class="row align-items-center"> |
| <div class="col-6 text-center"> |
| <div class="squircle bg-primary justify-content-center"> |
| <i data-feather="droplet" class="fe-32 align-self-center text-white"></i> |
| </div> |
| <p>Droplet</p> |
| </div> |
| <div class="col-6 text-center"> |
| <div class="squircle bg-primary justify-content-center"> |
| <i data-feather="upload-cloud" class="fe-32 align-self-center text-white"></i> |
| </div> |
| <p>Upload</p> |
| </div> |
| </div> |
| <div class="row align-items-center"> |
| <div class="col-6 text-center"> |
| <div class="squircle bg-primary justify-content-center"> |
| <i data-feather="users" class="fe-32 align-self-center text-white"></i> |
| </div> |
| <p>Users</p> |
| </div> |
| <div class="col-6 text-center"> |
| <div class="squircle bg-primary justify-content-center"> |
| <i data-feather="settings" class="fe-32 align-self-center text-white"></i> |
| </div> |
| <p>Settings</p> |
| </div> |
| </div> |
| </div> |
| </div> |
| </div> |
| </div> |
| </main> |
| <script src="js/jquery.min.js"></script> |
| <script src="js/popper.min.js"></script> |
| <script src="js/moment.min.js"></script> |
| <script src="js/bootstrap.min.js"></script> |
| <script src="js/simplebar.min.js"></script> |
| <script src='js/daterangepicker.js'></script> |
| <script src='js/jquery.stickOnScroll.js'></script> |
| <script src="js/tinycolor-min.js"></script> |
| <script src="js/config.js"></script> |
| <script src="js/d3.min.js"></script> |
| <script src="js/topojson.min.js"></script> |
| <script src="js/datamaps.all.min.js"></script> |
| <script src="js/datamaps-zoomto.js"></script> |
| <script src="js/datamaps.custom.js"></script> |
| <script src="js/Chart.min.js"></script> |
| <script src="api.js"></script> |
| <script> |
| api.getSession().catch(() => { window.location.href = 'auth-login.html'; }) |
| .then(r => { if (r && !r.success) window.location.href = 'auth-login.html'; }); |
| </script> |
| <script src="js/gauge.min.js"></script> |
| <script src="js/jquery.sparkline.min.js"></script> |
| <script src="js/apexcharts.min.js"></script> |
| <script src="js/apexcharts.custom.js"></script> |
|
|
| <script src='js/jquery.mask.min.js'></script> |
| <script src='js/select2.min.js'></script> |
| <script src='js/jquery.steps.min.js'></script> |
| <script src='js/jquery.validate.min.js'></script> |
| <script src='js/jquery.timepicker.js'></script> |
| <script src='js/dropzone.min.js'></script> |
| <script src='js/uppy.min.js'></script> |
| <script src='js/quill.min.js'></script> |
| <script> |
| $('.select2').select2( |
| { |
| theme: 'bootstrap4', |
| }); |
| $('.select2-multi').select2( |
| { |
| multiple: true, |
| theme: 'bootstrap4', |
| }); |
| $('.drgpicker').daterangepicker( |
| { |
| singleDatePicker: true, |
| timePicker: false, |
| showDropdowns: true, |
| locale: |
| { |
| format: 'MM/DD/YYYY' |
| } |
| }); |
| $('.time-input').timepicker( |
| { |
| 'scrollDefault': 'now', |
| 'zindex': '9999' |
| }); |
| |
| if ($('.datetimes').length) |
| { |
| $('.datetimes').daterangepicker( |
| { |
| timePicker: true, |
| startDate: moment().startOf('hour'), |
| endDate: moment().startOf('hour').add(32, 'hour'), |
| locale: |
| { |
| format: 'M/DD hh:mm A' |
| } |
| }); |
| } |
| var start = moment().subtract(29, 'days'); |
| var end = moment(); |
| |
| function cb(start, end) |
| { |
| $('#reportrange span').html(start.format('MMMM D, YYYY') + ' - ' + end.format('MMMM D, YYYY')); |
| } |
| $('#reportrange').daterangepicker( |
| { |
| startDate: start, |
| endDate: end, |
| ranges: |
| { |
| 'Today': [moment(), moment()], |
| 'Yesterday': [moment().subtract(1, 'days'), moment().subtract(1, 'days')], |
| 'Last 7 Days': [moment().subtract(6, 'days'), moment()], |
| 'Last 30 Days': [moment().subtract(29, 'days'), moment()], |
| 'This Month': [moment().startOf('month'), moment().endOf('month')], |
| 'Last Month': [moment().subtract(1, 'month').startOf('month'), moment().subtract(1, 'month').endOf('month')] |
| } |
| }, cb); |
| cb(start, end); |
| $('.input-placeholder').mask("00/00/0000", |
| { |
| placeholder: "__/__/____" |
| }); |
| $('.input-zip').mask('00000-000', |
| { |
| placeholder: "____-___" |
| }); |
| $('.input-money').mask("#.##0,00", |
| { |
| reverse: true |
| }); |
| $('.input-phoneus').mask('(000) 000-0000'); |
| $('.input-mixed').mask('AAA 000-S0S'); |
| $('.input-ip').mask('0ZZ.0ZZ.0ZZ.0ZZ', |
| { |
| translation: |
| { |
| 'Z': |
| { |
| pattern: /[0-9]/, |
| optional: true |
| } |
| }, |
| placeholder: "___.___.___.___" |
| }); |
| |
| var editor = document.getElementById('editor'); |
| if (editor) |
| { |
| var toolbarOptions = [ |
| [ |
| { |
| 'font': [] |
| }], |
| [ |
| { |
| 'header': [1, 2, 3, 4, 5, 6, false] |
| }], |
| ['bold', 'italic', 'underline', 'strike'], |
| ['blockquote', 'code-block'], |
| [ |
| { |
| 'header': 1 |
| }, |
| { |
| 'header': 2 |
| }], |
| [ |
| { |
| 'list': 'ordered' |
| }, |
| { |
| 'list': 'bullet' |
| }], |
| [ |
| { |
| 'script': 'sub' |
| }, |
| { |
| 'script': 'super' |
| }], |
| [ |
| { |
| 'indent': '-1' |
| }, |
| { |
| 'indent': '+1' |
| }], |
| [ |
| { |
| 'direction': 'rtl' |
| }], |
| [ |
| { |
| 'color': [] |
| }, |
| { |
| 'background': [] |
| }], |
| [ |
| { |
| 'align': [] |
| }], |
| ['clean'] |
| ]; |
| var quill = new Quill(editor, |
| { |
| modules: |
| { |
| toolbar: toolbarOptions |
| }, |
| theme: 'snow' |
| }); |
| } |
| (function() |
| { |
| 'use strict'; |
| window.addEventListener('load', function() |
| { |
| var forms = document.getElementsByClassName('needs-validation'); |
| var validation = Array.prototype.filter.call(forms, function(form) |
| { |
| form.addEventListener('submit', function(event) |
| { |
| if (form.checkValidity() === false) |
| { |
| event.preventDefault(); |
| event.stopPropagation(); |
| } |
| form.classList.add('was-validated'); |
| }, false); |
| }); |
| }, false); |
| })(); |
| </script> |
| <script> |
| var uptarg = document.getElementById('drag-drop-area'); |
| if (uptarg) |
| { |
| var uppy = Uppy.Core().use(Uppy.Dashboard, |
| { |
| inline: true, |
| target: uptarg, |
| proudlyDisplayPoweredByUppy: false, |
| theme: 'dark', |
| width: 770, |
| height: 210, |
| plugins: ['Webcam'] |
| }).use(Uppy.Tus, |
| { |
| endpoint: 'https://master.tus.io/files/' |
| }); |
| uppy.on('complete', (result) => |
| { |
| console.log('Upload complete! We\'ve uploaded these files:', result.successful) }); |
| } |
| </script> |
| <script src="js/apps.js"></script> |
| <script async src="https://www.googletagmanager.com/gtag/js?id=UA-56159088-1"></script> |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/feather-icons/4.29.1/feather.min.js"></script> |
|
|
| <script> |
| feather.replace(); |
| </script> |
| <script> |
| window.dataLayer = window.dataLayer || []; |
| |
| function gtag() |
| { |
| dataLayer.push(arguments); |
| } |
| gtag('js', new Date()); |
| gtag('config', 'UA-56159088-1'); |
| </script> |
| <script> |
| $(document).ready(function() { |
| console.log('Dashboard iniciado'); |
| |
| let currentUser = null; |
| let systemStatus = {}; |
| let recentAnalysis = []; |
| let areaChartInstance = null; |
| let ageRangesChartInstance = null; |
| |
| initializeDashboard(); |
| |
| setupEventListeners(); |
| |
| function initializeDashboard() { |
| console.log('Inicializando dashboard...'); |
| |
| checkAuthentication(); |
| |
| loadSystemStatus(); |
| |
| loadDashboardStats(); |
| |
| updateChartData(7); |
| |
| loadAgeRangesData(); |
| |
| setupDateRangePicker(); |
| |
| updateDateTime(); |
| setInterval(updateDateTime, 60000); |
| |
| window.addEventListener('storage', function(e) { |
| if (e.key === 'theme') { |
| setTimeout(function() { |
| updateChartData(7); |
| loadAgeRangesData(); |
| }, 450); |
| } |
| }); |
| } |
| |
| async function checkAuthentication() { |
| const response = await api.getSession().catch(() => null); |
| if (response && response.success) { |
| currentUser = response.user; |
| updateUserInterface(); |
| } else { |
| window.location.href = 'auth-login.html'; |
| } |
| } |
| |
| function updateUserInterface() { |
| if (currentUser) { |
| $('#currentUserName').text(currentUser.username); |
| $('#welcomeMessage').text(`¡Bienvenido, ${currentUser.username}!`); |
| |
| const roleClass = currentUser.role === 'Admin' ? 'badge-primary' : 'badge-success'; |
| $('#currentUserName').after(`<span class="badge ${roleClass} ml-2">${currentUser.role}</span>`); |
| } |
| } |
| |
| async function loadSystemStatus() { |
| const response = await api.getModelInfo().catch(() => null); |
| if (response && response.loaded) { |
| systemStatus = { model_loaded: response.loaded, database_connected: true, app_version: 'v2.1' }; |
| updateSystemStatusUI(); |
| } else { |
| systemStatus = { model_loaded: false, database_connected: true, app_version: 'v2.1' }; |
| updateSystemStatusUI(); |
| } |
| } |
| |
| function updateSystemStatusUI() { |
| if (systemStatus.model_loaded) { |
| $('#modelStatus').removeClass('bg-danger').addClass('bg-success'); |
| $('#modelStatusText').text('Cargado').removeClass('text-danger').addClass('text-success'); |
| } else { |
| $('#modelStatus').removeClass('bg-success').addClass('bg-danger'); |
| $('#modelStatusText').text('Error').removeClass('text-success').addClass('text-danger'); |
| } |
| |
| if (systemStatus.database_connected) { |
| $('#dbStatus').removeClass('bg-danger').addClass('bg-success'); |
| $('#dbStatusText').text('Conectada').removeClass('text-danger').addClass('text-success'); |
| } else { |
| $('#dbStatus').removeClass('bg-success').addClass('bg-danger'); |
| $('#dbStatusText').text('Desconectada').removeClass('text-success').addClass('text-danger'); |
| } |
| |
| $('#appVersion').text(systemStatus.app_version || 'v2.1'); |
| |
| const isSystemHealthy = systemStatus.model_loaded && systemStatus.database_connected; |
| if (isSystemHealthy) { |
| $('#systemStatusIcon').removeClass('bg-danger').addClass('bg-success'); |
| $('#systemStatusIcon i').removeClass('fe-x').addClass('fe-check'); |
| $('#systemStatusText').text('Operativo'); |
| } else { |
| $('#systemStatusIcon').removeClass('bg-success').addClass('bg-danger'); |
| $('#systemStatusIcon i').removeClass('fe-check').addClass('fe-x'); |
| $('#systemStatusText').text('Con errores'); |
| } |
| } |
| |
| async function loadDashboardStats() { |
| const response = await api.getDashboardStats().catch(() => null); |
| if (response && response.success) { |
| const stats = response.stats; |
| |
| const totalPatients = parseInt(stats.total_patients || stats.total_unique_patients || 0); |
| const positiveCases = parseInt(stats.positive_cases || stats.patients_with_rd || 0); |
| const negativeCases = parseInt(stats.negative_cases || stats.patients_without_rd || 0); |
| const positivityRate = parseFloat(stats.positivity_rate || 0).toFixed(1); |
| |
| updateStatCard('#totalPatientsCard', totalPatients); |
| updateStatCard('#positiveCasesCard', positiveCases); |
| updateStatCard('#negativeCasesCard', negativeCases); |
| updateStatCard('#positivityRateCard', positivityRate + '%'); |
| |
| |
| updateDonutChart({ |
| total_unique_patients: totalPatients, |
| patients_with_rd: positiveCases, |
| patients_without_rd: negativeCases |
| }); |
| } else { |
| showErrorMessage('Error cargando estadísticas del dashboard'); |
| } |
| } |
| |
| function updateStatCard(selector, value) { |
| const card = $(selector); |
| const valueElement = card.find('span.h3'); |
| |
| if (typeof value === 'string' && value.includes('%')) { |
| valueElement.text(value); |
| } else { |
| const targetValue = parseInt(value) || 0; |
| animateValue(valueElement[0], 0, targetValue, 800); |
| } |
| } |
| |
| function animateValue(element, start, end, duration) { |
| if (!element) return; |
| const safeEnd = Math.max(0, parseInt(end) || 0); |
| const safeStart = Math.max(0, parseInt(start) || 0); |
| const startTime = performance.now(); |
| const step = (currentTime) => { |
| const elapsed = currentTime - startTime; |
| const progress = Math.min(elapsed / duration, 1); |
| element.textContent = Math.floor(progress * (safeEnd - safeStart) + safeStart); |
| if (progress < 1) window.requestAnimationFrame(step); |
| }; |
| window.requestAnimationFrame(step); |
| } |
| |
| function updateDonutChart(stats) { |
| const donutChartElement = document.querySelector("#donutChart"); |
| if (!donutChartElement) return; |
| |
| donutChartElement.innerHTML = ''; |
| |
| const donutOptions = { |
| chart: { |
| type: 'donut', |
| height: 300 |
| }, |
| colors: ['#66DA26', '#FF9800'], |
| series: [stats.patients_without_rd || 0, stats.patients_with_rd || 0], |
| labels: ['Sin Retinopatía', 'Con Retinopatía'], |
| legend: { |
| position: 'bottom', |
| labels: { |
| colors: '#9aa0ac', |
| } |
| }, |
| dataLabels: { |
| enabled: true, |
| formatter: function(val, opts) { |
| return Math.round(val) + '%'; |
| } |
| }, |
| plotOptions: { |
| pie: { |
| donut: { |
| labels: { |
| show: true, |
| total: { |
| show: true, |
| label: 'Total Pacientes', |
| color: '#9aa0ac', |
| formatter: function(w) { |
| return stats.total_unique_patients || 0; |
| } |
| } |
| } |
| } |
| } |
| }, |
| responsive: [{ |
| breakpoint: 480, |
| options: { |
| chart: { |
| height: 250 |
| }, |
| legend: { |
| position: 'bottom' |
| } |
| } |
| }] |
| }; |
| |
| try { |
| const donutChart = new ApexCharts(donutChartElement, donutOptions); |
| donutChart.render(); |
| } catch (error) { |
| console.error('Error creando gráfico donut:', error); |
| } |
| } |
| |
| function updateChartData(days) { |
| console.log(`Actualizando grafico para los ultimos ${days} dias`); |
| |
| api.getConsultations(1, 100).then(function(response) { |
| console.log('Respuesta del backend:', response); |
| |
| if (response && response.success && response.consultations.length > 0) { |
| |
| const grouped = {}; |
| response.consultations.forEach(c => { |
| const raw = c.consultationDate || ''; |
| |
| const d = raw.replace('T', ' ').substring(0, 10); |
| if (!d || d === '0000-00-00') return; |
| if (!grouped[d]) grouped[d] = {total:0, pos:0, neg:0}; |
| grouped[d].total++; |
| if (c.diabeticRetinopathy) grouped[d].pos++; else grouped[d].neg++; |
| }); |
| const dates = Object.keys(grouped).sort(); |
| if (dates.length === 0) { createEmptyChart(); return; } |
| createOrUpdateChart( |
| dates, |
| dates.map(d => grouped[d].total), |
| dates.map(d => grouped[d].pos), |
| dates.map(d => grouped[d].neg) |
| ); |
| } else { |
| createEmptyChart(); |
| } |
| }); |
| } |
| |
| function setupDateRangePicker() { |
| var start = moment().subtract(29, 'days'); |
| var end = moment(); |
| |
| function cb(start, end) { |
| $('#reportrange span').html(start.format('MMMM D, YYYY') + ' - ' + end.format('MMMM D, YYYY')); |
| } |
| |
| $('#reportrange').daterangepicker({ |
| startDate: start, |
| endDate: end, |
| ranges: { |
| 'Hoy': [moment(), moment()], |
| 'Ayer': [moment().subtract(1, 'days'), moment().subtract(1, 'days')], |
| 'Últimos 7 Días': [moment().subtract(6, 'days'), moment()], |
| 'Últimos 30 Días': [moment().subtract(29, 'days'), moment()], |
| 'Este Mes': [moment().startOf('month'), moment().endOf('month')], |
| 'Mes Anterior': [moment().subtract(1, 'month').startOf('month'), moment().subtract(1, 'month').endOf('month')] |
| } |
| }, cb); |
| |
| cb(start, end); |
| } |
| |
| function loadRecentConsultations() { |
| $('#recentConsultationsBody').html(` |
| <tr> |
| <td colspan="5" class="text-center text-muted"> |
| <i data-feather="loader" class="fe-16 spinner"></i> Cargando consultas... |
| </td> |
| </tr> |
| `); |
| |
| api.getConsultations(1, 10).then(function(response) { |
| if (response.success) { |
| updateRecentConsultationsTable(response.consultations); |
| } else { |
| showErrorInTable(response.message); |
| } |
| }); |
| } |
| |
| function updateRecentConsultationsTable(consultations) { |
| const tbody = $('#recentConsultationsBody'); |
| tbody.empty(); |
| |
| if (consultations.length === 0) { |
| tbody.append(` |
| <tr> |
| <td colspan="5" class="text-center text-muted"> |
| <i data-feather="inbox" class="fe-16"></i><br> |
| No hay consultas recientes |
| </td> |
| </tr> |
| `); |
| return; |
| } |
| |
| consultations.forEach(consultation => { |
| const date = consultation.consultationDate ? |
| new Date(consultation.consultationDate).toLocaleString('es-ES') : 'N/A'; |
| |
| const isPositive = consultation.diabeticRetinopathy; |
| const resultClass = isPositive ? 'badge-warning' : 'badge-success'; |
| const resultText = isPositive ? 'Positivo' : 'Negativo'; |
| const confidence = consultation.confidence ? |
| `${consultation.confidence.toFixed(1)}%` : 'N/A'; |
| |
| tbody.append(` |
| <tr> |
| <td>${date}</td> |
| <td>${consultation.patientName || consultation.patient_name || 'Desconocido'}</td> |
| <td><span class="badge ${resultClass}">${resultText}</span></td> |
| <td>${confidence}</td> |
| </tr> |
| `); |
| }); |
| } |
| |
| window.showConsultationDetails = function(consultationId) { |
| alert('Ver detalles: Consulta #' + consultationId); |
| }; |
| |
| function showConsultationModal(consultation) { |
| const modalHtml = ` |
| <div class="modal fade" id="consultationModal" tabindex="-1" role="dialog"> |
| <div class="modal-dialog modal-lg" role="document"> |
| <div class="modal-content"> |
| <div class="modal-header"> |
| <h5 class="modal-title">Detalles de Consulta</h5> |
| <button type="button" class="close" data-dismiss="modal"> |
| <span>×</span> |
| </button> |
| </div> |
| <div class="modal-body"> |
| <div class="row mb-3"> |
| <div class="col-md-6"> |
| <h6>Información Básica</h6> |
| <p><strong>Paciente:</strong> ${consultation.patientName || consultation.patient_name || 'Desconocido'}</p> |
| <p><strong>Fecha:</strong> ${consultation.consultationDate || 'N/A'}</p> |
| </div> |
| <div class="col-md-6"> |
| <h6>Resultado</h6> |
| <p><strong>Diagnóstico:</strong> |
| <span class="badge ${consultation.diabeticRetinopathy ? 'badge-warning' : 'badge-success'}"> |
| ${consultation.diabeticRetinopathy ? 'Positivo' : 'Negativo'} |
| </span> |
| </p> |
| <p><strong>Confianza:</strong> ${consultation.confidence || 'N/A'}%</p> |
| </div> |
| </div> |
| ${consultation.notes ? ` |
| <div class="card mb-3"> |
| <div class="card-header"> |
| <h6 class="card-title mb-0">Notas</h6> |
| </div> |
| <div class="card-body"> |
| <p>${consultation.notes}</p> |
| </div> |
| </div>` : ''} |
| ${consultation.imagePath ? ` |
| <div class="card"> |
| <div class="card-header"> |
| <h6 class="card-title mb-0">Imagen</h6> |
| </div> |
| <div class="card-body text-center"> |
| <img src="${consultation.imagePath}" class="img-fluid" style="max-height: 300px;" alt="Imagen de retina"> |
| </div> |
| </div>` : ''} |
| </div> |
| <div class="modal-footer"> |
| <button type="button" class="btn btn-secondary" data-dismiss="modal">Cerrar</button> |
| </div> |
| </div> |
| </div> |
| </div> |
| `; |
| |
| $('body').append(modalHtml); |
| $('#consultationModal').modal('show'); |
| |
| $('#consultationModal').on('hidden.bs.modal', function() { |
| $(this).remove(); |
| }); |
| } |
| |
| |
| function showErrorInTable(message) { |
| $('#recentConsultationsBody').html(` |
| <tr> |
| <td colspan="5" class="text-center text-danger"> |
| <i data-feather="alert-triangle" class="fe-16"></i><br> |
| ${message || 'Error al cargar consultas'} |
| </td> |
| </tr> |
| `); |
| } |
| |
| $(document).ready(function() { |
| loadRecentConsultations(); |
| }); |
| |
| function createOrUpdateChart(dates, totalConsultations, positiveCases, negativeCases) { |
| if (areaChartInstance && typeof areaChartInstance.destroy === 'function') { |
| try { |
| areaChartInstance.destroy(); |
| areaChartInstance = null; |
| } catch (error) { |
| console.log('Error destruyendo grafico anterior:', error); |
| } |
| } |
| |
| const chartElement = document.querySelector("#areaChart"); |
| if (!chartElement) { |
| console.error('Elemento #areaChart no encontrado'); |
| return; |
| } |
| |
| chartElement.innerHTML = ''; |
| |
| setTimeout(() => { |
| try { |
| const isDark = localStorage.getItem('theme') === 'dark'; |
| const axisColor = isDark ? '#ffffff' : '#212529'; |
| |
| const options = { |
| chart: { |
| type: 'area', |
| height: 350, |
| width: '100%', |
| toolbar: { |
| show: true, |
| tools: { |
| download: false, |
| selection: false, |
| zoom: false, |
| zoomin: false, |
| zoomout: false, |
| pan: false, |
| reset: false |
| } |
| }, |
| animations: { |
| enabled: true, |
| easing: 'easeinout', |
| speed: 800 |
| }, |
| background: 'transparent' |
| }, |
| colors: ['#2E93fA', '#FF9800', '#66DA26'], |
| series: (function() { |
| function toTs(d) { |
| var p = d.split('-').map(Number); |
| return Date.UTC(p[0], p[1] - 1, p[2], 12, 0, 0); |
| } |
| function toSeries(vals) { |
| return dates.map(function(d, i) { return { x: toTs(d), y: vals[i] }; }); |
| } |
| return [ |
| { name: 'Total Consultas', data: toSeries(totalConsultations) }, |
| { name: 'Con Retinopatía', data: toSeries(positiveCases) }, |
| { name: 'Sin Retinopatía', data: toSeries(negativeCases) } |
| ]; |
| })(), |
| xaxis: { |
| type: 'datetime', |
| labels: { |
| datetimeUTC: false, |
| datetimeFormatter: { |
| year: 'yyyy', |
| month: "MMM 'yy", |
| day: 'dd/MM', |
| hour: 'HH:mm' |
| }, |
| style: { |
| fontSize: '12px', |
| fontFamily: 'inherit', |
| colors: axisColor |
| } |
| }, |
| title: { |
| text: 'Fecha', |
| style: { |
| fontSize: '14px', |
| fontFamily: 'inherit', |
| color: axisColor |
| } |
| } |
| }, |
| yaxis: { |
| title: { |
| text: 'Número de Consultas', |
| style: { |
| fontSize: '14px', |
| fontFamily: 'inherit', |
| color: axisColor |
| } |
| }, |
| min: 0, |
| tickAmount: Math.max.apply(null, totalConsultations) || 1, |
| labels: { |
| formatter: function(val) { return Math.floor(val); }, |
| style: { |
| fontSize: '12px', |
| fontFamily: 'inherit', |
| colors: axisColor |
| } |
| } |
| }, |
| tooltip: { |
| shared: true, |
| intersect: false, |
| x: { |
| formatter: function(value) { |
| return new Date(value).toLocaleDateString('es-ES', { |
| weekday: 'long', |
| year: 'numeric', |
| month: 'long', |
| day: 'numeric' |
| }); |
| } |
| }, |
| y: { |
| formatter: function(value) { |
| return value + ' consulta' + (value !== 1 ? 's' : ''); |
| } |
| } |
| }, |
| stroke: { |
| curve: 'smooth', |
| width: 2 |
| }, |
| fill: { |
| type: 'gradient', |
| gradient: { |
| shadeIntensity: 1, |
| opacityFrom: 0.7, |
| opacityTo: 0.3, |
| stops: [0, 90, 100] |
| } |
| }, |
| legend: { |
| position: 'top', |
| horizontalAlign: 'left', |
| labels: { |
| colors: '#9aa0ac' |
| } |
| }, |
| grid: { |
| borderColor: '#f1f1f1', |
| strokeDashArray: 4, |
| xaxis: { |
| lines: { |
| show: true |
| } |
| }, |
| yaxis: { |
| lines: { |
| show: true |
| } |
| } |
| }, |
| dataLabels: { |
| enabled: false |
| }, |
| responsive: [{ |
| breakpoint: 768, |
| options: { |
| chart: { |
| height: 300 |
| }, |
| legend: { |
| position: 'bottom' |
| } |
| } |
| }] |
| }; |
| |
| areaChartInstance = new ApexCharts(chartElement, options); |
| |
| areaChartInstance.render() |
| .then(() => { |
| console.log('Gráfico creado exitosamente'); |
| }) |
| .catch((error) => { |
| console.error('Error renderizando gráfico:', error); |
| createFallbackChart(chartElement, dates, totalConsultations, positiveCases, negativeCases); |
| }); |
| |
| } catch (error) { |
| console.error('Error creando gráfico:', error); |
| createFallbackChart(chartElement, dates, totalConsultations, positiveCases, negativeCases); |
| } |
| }, 100); |
| } |
| |
| function createFallbackChart(chartElement, dates, totalConsultations, positiveCases, negativeCases) { |
| console.log('Creando grafico de respaldo...'); |
| |
| const maxValue = Math.max(...totalConsultations, 1); |
| |
| let tableHtml = ` |
| <div class="fallback-chart p-3"> |
| <h6 class="text-muted mb-3">Datos de Consultas (Vista de Tabla)</h6> |
| <div class="table-responsive"> |
| <table class="table table-sm table-hover"> |
| <thead class="thead-light"> |
| <tr> |
| <th>Fecha</th> |
| <th>Total</th> |
| <th>Con RD</th> |
| <th>Sin RD</th> |
| <th>Gráfico</th> |
| </tr> |
| </thead> |
| <tbody> |
| `; |
| |
| dates.forEach((date, index) => { |
| const total = totalConsultations[index] || 0; |
| const positive = positiveCases[index] || 0; |
| const negative = negativeCases[index] || 0; |
| const percentage = maxValue > 0 ? (total / maxValue) * 100 : 0; |
| |
| const formattedDate = new Date(date).toLocaleDateString('es-ES', { |
| day: '2-digit', |
| month: '2-digit' |
| }); |
| |
| tableHtml += ` |
| <tr> |
| <td>${formattedDate}</td> |
| <td><span class="badge badge-primary">${total}</span></td> |
| <td><span class="badge badge-warning">${positive}</span></td> |
| <td><span class="badge badge-success">${negative}</span></td> |
| <td> |
| <div class="progress" style="height: 20px;"> |
| <div class="progress-bar" role="progressbar" style="width: ${percentage}%" |
| aria-valuenow="${total}" aria-valuemin="0" aria-valuemax="${maxValue}"> |
| </div> |
| </div> |
| </td> |
| </tr> |
| `; |
| }); |
| |
| tableHtml += ` |
| </tbody> |
| </table> |
| </div> |
| <small class="text-muted"> |
| <i data-feather="info" class="mr-1"></i> |
| Vista alternativa - el gráfico principal no se pudo cargar |
| </small> |
| </div> |
| `; |
| |
| chartElement.innerHTML = tableHtml; |
| } |
| |
| function createEmptyChart() { |
| if (areaChartInstance && typeof areaChartInstance.destroy === 'function') { |
| try { |
| areaChartInstance.destroy(); |
| areaChartInstance = null; |
| } catch (error) { |
| console.log('Error destruyendo grafico:', error); |
| } |
| } |
| |
| const chartElement = document.querySelector("#areaChart"); |
| if (chartElement) { |
| chartElement.innerHTML = ` |
| <div class="d-flex justify-content-center align-items-center" style="height: 350px;"> |
| <div class="text-center text-muted"> |
| <i data-feather="bar-chart-2" class="fe-48 mb-3"></i> |
| <h6>No hay datos disponibles</h6> |
| <p class="small">Las consultas aparecerán aquí cuando se registren</p> |
| <button class="btn btn-sm btn-outline-primary" onclick="insertSampleData()"> |
| <i data-feather="plus" class="mr-1"></i>Generar datos de prueba |
| </button> |
| </div> |
| </div> |
| `; |
| } |
| } |
| |
| function loadRecentAnalysis() { return; } |
| |
| function updateRecentAnalysisTable() { |
| const tbody = $('#recentAnalysisTable'); |
| tbody.empty(); |
| |
| if (recentAnalysis.length === 0) { |
| tbody.append(` |
| <tr> |
| <td colspan="5" class="text-center text-muted"> |
| <i data-feather="inbox" class="fe-24 mb-2"></i><br> |
| No hay análisis recientes |
| </td> |
| </tr> |
| `); |
| return; |
| } |
| |
| recentAnalysis.reverse().forEach(function(analysis) { |
| const resultClass = analysis.prediction.class_index === 1 ? 'badge-warning' : 'badge-success'; |
| const resultText = analysis.prediction.class_index === 1 ? 'Positivo' : 'Negativo'; |
| |
| tbody.append(` |
| <tr> |
| <td>${moment(analysis.timestamp).format('DD/MM/YYYY HH:mm')}</td> |
| <td>${analysis.filename}</td> |
| <td><span class="badge ${resultClass}">${resultText}</span></td> |
| <td>${analysis.prediction.confidence}%</td> |
| <td> |
| <button class="btn btn-sm btn-outline-primary" onclick="viewAnalysis('${analysis.filename}')"> |
| <i data-feather="eye" class="fe-12"></i> |
| </button> |
| </td> |
| </tr> |
| `); |
| }); |
| } |
| |
| function setupEventListeners() { |
| $('#logoutBtn').on('click', function(e) { |
| e.preventDefault(); |
| handleLogout(); |
| }); |
| |
| $('#refreshAgeDataBtn').on('click', function() { |
| refreshAgeData(); |
| }); |
| |
| $(document).on('click', '[onclick^="updateChartData"]', function(e) { |
| e.preventDefault(); |
| const days = parseInt(this.getAttribute('onclick').match(/\d+/)[0]); |
| updateChartData(days); |
| }); |
| |
| $('#imageInput').on('change', handleImageSelect); |
| |
| $('#analyzeBtn').on('click', handleImageAnalysis); |
| |
| $('#clearBtn').on('click', clearImagePreview); |
| |
| $('#systemTestBtn').on('click', runSystemTest); |
| |
| setupDragAndDrop(); |
| } |
| |
| function setupDragAndDrop() { |
| } |
| |
| function handleImageSelect(e) { |
| const file = e.target.files[0]; |
| if (file) { |
| handleImageFile(file); |
| } |
| } |
| |
| function handleImageFile(file) { |
| if (!file.type.startsWith('image/')) { |
| showAlert('error', 'Por favor selecciona un archivo de imagen válido'); |
| return; |
| } |
| |
| if (file.size > 10 * 1024 * 1024) { |
| showAlert('error', 'La imagen es demasiado grande. Máximo 10MB'); |
| return; |
| } |
| |
| const reader = new FileReader(); |
| reader.onload = function(e) { |
| $('#previewImg').attr('src', e.target.result); |
| $('#imagePreview').show(); |
| $('#uploadZone').hide(); |
| }; |
| reader.readAsDataURL(file); |
| |
| console.log('Imagen cargada:', file.name); |
| } |
| |
| function handleImageAnalysis() { |
| const imgSrc = $('#previewImg').attr('src'); |
| if (!imgSrc) return; |
| |
| $('#analyzeBtn').prop('disabled', true).html('<i class="spinner-border spinner-border-sm mr-2"></i>Analizando...'); |
| |
| const fileName = $('#imageInput')[0].files[0]?.name || 'imagen_' + Date.now() + '.jpg'; |
| |
| api.predict(imgSrc, fileName).then(function(result) { |
| $('#analyzeBtn').prop('disabled', false).html('<i data-feather="eye" class="mr-2"></i>Analizar con IA'); |
| |
| if (result && result.success) { |
| showAnalysisResults(result); |
| |
| } else { |
| showAlert('error', result?.error || 'Error en el análisis'); |
| } |
| }); |
| } |
| |
| function showAnalysisResults(result) { |
| const prediction = result.prediction; |
| const isPositive = prediction.class_index === 1; |
| const alertClass = isPositive ? 'alert-warning' : 'alert-success'; |
| const iconClass = isPositive ? 'fe-alert-triangle' : 'fe-check-circle'; |
| const resultText = isPositive ? 'Retinopatía Diabética Detectada' : 'No se Detectó Retinopatía Diabética'; |
| |
| const resultsHtml = ` |
| <div class="alert ${alertClass} d-flex align-items-center"> |
| <i class="fe ${iconClass} fe-24 mr-3"></i> |
| <div> |
| <h6 class="mb-1">${resultText}</h6> |
| <p class="mb-0">Confianza: <strong>${prediction.confidence}%</strong></p> |
| <small class="text-muted">Análisis realizado: ${result.timestamp}</small> |
| </div> |
| </div> |
| ${isPositive ? ` |
| ` : ''} |
| `; |
| |
| $('#resultContent').html(resultsHtml); |
| $('#analysisResults').show(); |
| |
| console.log('Analisis completado:', prediction.class); |
| } |
| |
| function clearImagePreview() { |
| $('#imagePreview').hide(); |
| $('#analysisResults').hide(); |
| $('#uploadZone').show(); |
| $('#imageInput').val(''); |
| } |
| |
| function runSystemTest() { |
| $('#systemTestBtn').prop('disabled', true).html('<i class="spinner-border spinner-border-sm mr-2"></i>Probando...'); |
| |
| api.getModelInfo().then(function(result) { |
| $('#systemTestBtn').prop('disabled', false).html('<i data-feather="refresh-cw" class="fe-16 mr-2"></i>Probar Sistema'); |
| if (result && result.loaded) { |
| showAlert('info', '• Modelo IA: SÍ\n• Base de datos: SÍ'); |
| } else { |
| showAlert('error', 'Modelo no cargado'); |
| } |
| }); |
| } |
| |
| function handleLogout() { |
| if (confirm('¿Estás seguro que deseas cerrar sesión?')) { |
| api.logout().then(() => { window.location.href = 'auth-login.html'; }); |
| } |
| } |
| |
| function redirectToLogin() { |
| window.location.href = 'auth-login.html'; |
| } |
| |
| function updateDateTime() { |
| const now = moment(); |
| $('#currentDateTime').text(now.format('dddd, DD [de] MMMM [de] YYYY - HH:mm')); |
| } |
| |
| function showAlert(type, message) { |
| const alertClass = { |
| 'success': 'alert-success', |
| 'error': 'alert-danger', |
| 'warning': 'alert-warning', |
| 'info': 'alert-info' |
| }[type] || 'alert-info'; |
| |
| const alertHtml = ` |
| <div class="alert ${alertClass} alert-dismissible fade show" role="alert"> |
| ${message.replace(/\n/g, '<br>')} |
| <button type="button" class="close" data-dismiss="alert"> |
| <span aria-hidden="true">×</span> |
| </button> |
| </div> |
| `; |
| |
| $('.container-fluid').prepend(alertHtml); |
| |
| setTimeout(() => { |
| $('.alert').first().alert('close'); |
| }, 5000); |
| } |
| |
| function showErrorMessage(message) { |
| showAlert('error', message); |
| } |
| |
| function showSystemError() { |
| $('#systemStatusIcon').removeClass('bg-success').addClass('bg-danger'); |
| $('#systemStatusIcon i').removeClass('fe-check').addClass('fe-x'); |
| $('#systemStatusText').text('Error de conexión'); |
| |
| showAlert('error', 'Error al conectar con el sistema. Verifica que el backend esté funcionando.'); |
| } |
| |
| function loadAgeRangesData() { |
| console.log('Cargando datos de rangos de edad con Retinopatía Diabética...'); |
| |
| |
| Promise.all([ |
| api.getPatients(''), |
| api.getConsultations(1, 1000) |
| ]).then(function([patientsResp, consultationsResp]) { |
| if (!patientsResp || !patientsResp.success || !patientsResp.patients.length) { |
| showEmptyAgeChart(); |
| return; |
| } |
| |
| |
| const positivePatientIds = new Set(); |
| if (consultationsResp && consultationsResp.success && consultationsResp.consultations) { |
| consultationsResp.consultations.forEach(c => { |
| if (c.diabeticRetinopathy && c.patientID) { |
| positivePatientIds.add(c.patientID); |
| } |
| }); |
| } |
| |
| |
| const patients = patientsResp.patients.filter(p => |
| p.birthDate && positivePatientIds.has(p.patientID) |
| ); |
| |
| if (!patients.length) { showEmptyAgeChart(); return; } |
| |
| |
| const ranges = {'0-20': 0, '21-40': 0, '41-60': 0, '61-80': 0, '80+': 0}; |
| let totalAge = 0, minAge = 999, maxAge = 0; |
| |
| patients.forEach(p => { |
| const age = moment().diff(moment(p.birthDate), 'years'); |
| totalAge += age; |
| if (age < minAge) minAge = age; |
| if (age > maxAge) maxAge = age; |
| if (age <= 20) ranges['0-20']++; |
| else if (age <= 40) ranges['21-40']++; |
| else if (age <= 60) ranges['41-60']++; |
| else if (age <= 80) ranges['61-80']++; |
| else ranges['80+']++; |
| }); |
| |
| const labels = Object.keys(ranges).filter(k => ranges[k] > 0); |
| const data = labels.map(k => ranges[k]); |
| |
| const chartData = { labels, data, total: patients.length }; |
| const avgAge = Math.round(totalAge / patients.length); |
| |
| updateAgeRangesChart({ chart_data: chartData }); |
| $('#avgAge').text(avgAge); |
| $('#ageRange').text(minAge + '-' + maxAge); |
| $('#totalPatientsAge').text(patients.length); |
| }); |
| } |
| |
| function updateAgeRangesChart(data) { |
| if (ageRangesChartInstance) { |
| try { |
| ageRangesChartInstance.destroy(); |
| ageRangesChartInstance = null; |
| } catch (error) { |
| console.log('Error destruyendo grafico anterior:', error); |
| } |
| } |
| |
| const chartElement = document.querySelector("#ageRangesChart"); |
| if (!chartElement) { |
| console.error('Elemento #ageRangesChart no encontrado'); |
| return; |
| } |
| |
| chartElement.innerHTML = ''; |
| |
| const chartData = data.chart_data; |
| |
| if (!chartData || !chartData.labels || chartData.labels.length === 0) { |
| showEmptyAgeChart(); |
| return; |
| } |
| |
| const colors = generateAgeRangeColors(chartData.labels.length); |
| |
| const options = { |
| chart: { |
| type: 'donut', |
| height: 300, |
| toolbar: { |
| show: false |
| }, |
| animations: { |
| enabled: true, |
| easing: 'easeinout', |
| speed: 800 |
| } |
| }, |
| colors: colors, |
| series: chartData.data, |
| labels: chartData.labels.map(label => label + ' años'), |
| legend: { |
| position: 'bottom', |
| fontSize: '12px', |
| labels: { |
| colors: '#9aa0ac' |
| }, |
| markers: { |
| width: 8, |
| height: 8 |
| } |
| }, |
| dataLabels: { |
| enabled: true, |
| formatter: function(val, opts) { |
| const count = chartData.data[opts.seriesIndex]; |
| return count + '\n(' + Math.round(val) + '%)'; |
| }, |
| style: { |
| fontSize: '10px', |
| fontWeight: 'bold' |
| } |
| }, |
| plotOptions: { |
| pie: { |
| donut: { |
| size: '65%', |
| labels: { |
| show: true, |
| name: { |
| show: true, |
| fontSize: '14px', |
| fontWeight: 'bold', |
| color: '#373d3f' |
| }, |
| value: { |
| show: true, |
| fontSize: '16px', |
| fontWeight: 'bold', |
| color: '#373d3f', |
| formatter: function(val) { |
| return val + ' pacientes'; |
| } |
| }, |
| total: { |
| show: true, |
| label: 'Con Retinopatía', |
| fontSize: '12px', |
| color: '#9aa0ac', |
| formatter: function(w) { |
| return chartData.total; |
| } |
| } |
| } |
| } |
| } |
| }, |
| tooltip: { |
| enabled: true, |
| y: { |
| formatter: function(value, { seriesIndex }) { |
| const percentage = ((value / chartData.total) * 100).toFixed(1); |
| return value + ' pacientes (' + percentage + '%)'; |
| } |
| } |
| }, |
| responsive: [{ |
| breakpoint: 480, |
| options: { |
| chart: { |
| height: 250 |
| }, |
| legend: { |
| position: 'bottom' |
| } |
| } |
| }] |
| }; |
| |
| try { |
| ageRangesChartInstance = new ApexCharts(chartElement, options); |
| ageRangesChartInstance.render().then(() => { |
| console.log('Grafico de rangos de edad creado exitosamente'); |
| }).catch((error) => { |
| console.error('Error renderizando grafico de edad:', error); |
| showEmptyAgeChart(); |
| }); |
| } catch (error) { |
| console.error('Error creando grafico de edad:', error); |
| showEmptyAgeChart(); |
| } |
| } |
| |
| function populateAgeDetailsModal(data) { |
| if (!data.success) return; |
| |
| api.getDashboardStats().then(function(response) { |
| if (response && response.success) { |
| const stats = response.detailed_analysis.statistics; |
| const summary = data.summary; |
| |
| $('#modalAvgAge').text(stats.avg_age + ' años'); |
| $('#modalMedianAge').text(stats.median_age + ' años'); |
| |
| if (summary.most_affected_range) { |
| $('#mostAffectedRange').text(summary.most_affected_range[0] + ' años'); |
| $('#mostAffectedCount').text(summary.most_affected_range[1] + ' pacientes'); |
| } |
| |
| const tbody = $('#ageDistributionTable tbody'); |
| tbody.empty(); |
| |
| const total = data.chart_data.total; |
| Object.entries(data.filtered_ranges).forEach(([range, count]) => { |
| const percentage = ((count / total) * 100).toFixed(1); |
| const barWidth = Math.max((count / total) * 100, 5); |
| |
| tbody.append(` |
| <tr> |
| <td>${range} años</td> |
| <td><strong>${count}</strong></td> |
| <td>${percentage}%</td> |
| <td> |
| <div class="progress" style="height: 20px;"> |
| <div class="progress-bar bg-primary" style="width: ${barWidth}%"></div> |
| </div> |
| </td> |
| </tr> |
| `); |
| }); |
| } |
| }); |
| } |
| |
| function updateAgeStatistics(data) { |
| if (!data || !data.success) return; |
| |
| api.getDashboardStats().then(function(response) { |
| if (response && response.success && response.detailed_analysis) { |
| const stats = response.detailed_analysis.statistics; |
| |
| $('#avgAge').text(stats.avg_age || '-'); |
| $('#ageRange').text(stats.min_age && stats.max_age ? |
| stats.min_age + '-' + stats.max_age : '-'); |
| $('#totalPatientsAge').text(stats.total_patients || '-'); |
| } |
| }); |
| } |
| |
| function generateAgeRangeColors(count) { |
| const baseColors = [ |
| '#E3F2FD', |
| '#BBDEFB', |
| '#90CAF9', |
| '#64B5F6', |
| '#42A5F5', |
| '#2196F3', |
| '#1E88E5', |
| '#1976D2', |
| '#1565C0', |
| '#0D47A1' |
| ]; |
| |
| return baseColors.slice(0, count); |
| } |
| |
| function showEmptyAgeChart() { |
| const chartElement = document.querySelector("#ageRangesChart"); |
| if (chartElement) { |
| chartElement.innerHTML = ` |
| <div class="d-flex justify-content-center align-items-center" style="height: 300px;"> |
| <div class="text-center text-muted"> |
| <i data-feather="users" class="fe-48 mb-3"></i> |
| <h6>No hay datos de edad</h6> |
| <p class="small">Los rangos de edad aparecerán cuando haya<br>pacientes con retinopatía diabética</p> |
| </div> |
| </div> |
| `; |
| } |
| |
| $('#avgAge').text('-'); |
| $('#ageRange').text('-'); |
| $('#totalPatientsAge').text('-'); |
| } |
| |
| window.viewAnalysis = function(filename) { |
| const analysis = recentAnalysis.find(a => a.filename === filename); |
| if (analysis) { |
| showAnalysisDetails(analysis); |
| } |
| }; |
| |
| function showAnalysisDetails(analysis) { |
| const modal = ` |
| <div class="modal fade" id="analysisModal" tabindex="-1"> |
| <div class="modal-dialog modal-lg"> |
| <div class="modal-content"> |
| <div class="modal-header"> |
| <h5 class="modal-title">Detalles del Análisis</h5> |
| <button type="button" class="close" data-dismiss="modal"> |
| <span>×</span> |
| </button> |
| </div> |
| <div class="modal-body"> |
| <div class="row"> |
| <div class="col-md-6"> |
| <h6>Información General</h6> |
| <p><strong>Archivo:</strong> ${analysis.filename}</p> |
| <p><strong>Fecha:</strong> ${moment(analysis.timestamp).format('DD/MM/YYYY HH:mm')}</p> |
| <p><strong>Resultado:</strong> ${analysis.prediction.class}</p> |
| <p><strong>Confianza:</strong> ${analysis.prediction.confidence}%</p> |
| </div> |
| <div class="col-md-6"> |
| <h6>Datos Técnicos</h6> |
| <p><strong>Raw Output:</strong> ${analysis.prediction.raw_output}</p> |
| <p><strong>Clase Predicha:</strong> ${analysis.prediction.class_index}</p> |
| </div> |
| </div> |
| </div> |
| <div class="modal-footer"> |
| <button type="button" class="btn btn-secondary" data-dismiss="modal">Cerrar</button> |
| </div> |
| </div> |
| </div> |
| </div> |
| `; |
| |
| $('body').append(modal); |
| $('#analysisModal').modal('show'); |
| $('#analysisModal').on('hidden.bs.modal', function() { |
| $(this).remove(); |
| }); |
| } |
| |
| window.updateChartData = updateChartData; |
| }); |
| </script> |
| <script> |
| function confirmLogout() { |
| executeLogout(); |
| } |
| |
| function executeLogout() { |
| api.logout().then(() => { window.location.href = 'auth-login.html'; }); |
| } |
| </script> |
|
|
| <script> |
| (function() { |
| function centerWindow() { |
| try { |
| const screenWidth = window.screen.availWidth; |
| const screenHeight = window.screen.availHeight; |
| const x = Math.floor((screenWidth - 1200) / 2); |
| const y = Math.floor((screenHeight - 800) / 2); |
| |
| if (window.moveTo) { |
| window.moveTo(Math.max(0, x), Math.max(0, y)); |
| } |
| } catch (e) { |
| console.warn('No se pudo centrar ventana:', e); |
| } |
| } |
| |
| if (document.readyState === 'loading') { |
| document.addEventListener('DOMContentLoaded', centerWindow); |
| } else { |
| centerWindow(); |
| } |
| |
| window.addEventListener('load', centerWindow); |
| })(); |
| </script> |
|
|
| <script> |
| function checkUserRoleAndHideNavigation() { |
| api.getSession().then(function(response) { |
| if (response.success && response.user) { |
| const userRole = response.user.role; |
| const usersNavItem = document.querySelector('a[href="./users.html"]'); |
| |
| if (usersNavItem) { |
| const navItemContainer = usersNavItem.closest('li.nav-item'); |
| |
| if (userRole === 'Admin') { |
| if (navItemContainer) { |
| navItemContainer.style.display = 'block'; |
| } |
| console.log('Navegacion de usuarios visible para administrador:', response.user.username); |
| } else { |
| if (navItemContainer) { |
| navItemContainer.style.display = 'none'; |
| } |
| console.log('Navegacion de usuarios oculta para usuario:', response.user.username, '(Rol:', userRole + ')'); |
| } |
| } else { |
| console.warn('No se encontró el elemento de navegación de usuarios'); |
| } |
| } else { |
| console.warn('No se pudo obtener información del usuario actual'); |
| } |
| }); |
| } |
| |
| function checkUsersPageAccess() { |
| if (window.location.pathname.includes('users.html') || window.location.href.includes('users.html')) { |
| api.getSession().then(function(response) { |
| if (!response.success) { |
| window.location.href = 'auth-login.html'; |
| return; |
| } |
| |
| if (response.user.role !== 'Admin') { |
| alert('Acceso Denegado\n\nSolo los administradores pueden gestionar usuarios.'); |
| window.location.href = 'index.html'; |
| return; |
| } |
| }); |
| } |
| } |
| |
| $(document).ready(function() { |
| setTimeout(() => { |
| checkUserRoleAndHideNavigation(); |
| checkUsersPageAccess(); |
| }, 100); |
| }); |
| |
| function updateNavigationForUser() { |
| checkUserRoleAndHideNavigation(); |
| } |
| |
| function updateUserInfo() { |
| api.getSession().then(function(response) { |
| if (response.success && response.user) { |
| const userDropdown = document.querySelector('#navbarDropdownMenuLink'); |
| if (userDropdown) { |
| const userRole = response.user.role; |
| const roleBadge = userRole === 'Admin' ? |
| '<span class="badge badge-primary badge-sm ml-1">Admin</span>' : |
| '<span class="badge badge-secondary badge-sm ml-1">Doctor</span>'; |
| |
| const existingBadge = userDropdown.querySelector('.badge'); |
| if (!existingBadge) { |
| userDropdown.innerHTML += roleBadge; |
| } |
| } |
| } |
| }); |
| } |
| |
| $(document).ready(function() { |
| setTimeout(() => { |
| updateUserInfo(); |
| }, 200); |
| }); |
| </script> |
| <script> |
| (function () { |
| |
| function _initFeather() { |
| if (typeof feather === 'undefined') return; |
| |
| var pending = document.querySelectorAll('i[data-feather]'); |
| if (pending.length > 0) { |
| feather.replace({ 'stroke-width': 1.8 }); |
| } |
| } |
| |
| document.addEventListener('DOMContentLoaded', _initFeather); |
| |
| var _fObserverActive = false; |
| document.addEventListener('DOMContentLoaded', function () { |
| var observer = new MutationObserver(function (mutations) { |
| if (_fObserverActive) return; |
| var hasPendingFeather = mutations.some(function (m) { |
| return Array.from(m.addedNodes).some(function (n) { |
| if (n.nodeType !== 1) return false; |
| |
| return n.matches('i[data-feather]') || n.querySelector('i[data-feather]'); |
| }); |
| }); |
| if (hasPendingFeather) { |
| _fObserverActive = true; |
| _initFeather(); |
| setTimeout(function () { _fObserverActive = false; }, 50); |
| } |
| }); |
| observer.observe(document.body, { childList: true, subtree: true }); |
| }); |
| })(); |
| </script> |
|
|
| <script> |
| (function () { |
| function applyTheme(theme) { |
| var lightTheme = document.getElementById('lightTheme'); |
| var darkTheme = document.getElementById('darkTheme'); |
| if (!lightTheme || !darkTheme) return; |
| if (theme === 'dark') { |
| lightTheme.disabled = true; |
| darkTheme.disabled = false; |
| document.documentElement.style.backgroundColor = '#1a1a2e'; |
| } else { |
| lightTheme.disabled = false; |
| darkTheme.disabled = true; |
| document.documentElement.style.backgroundColor = ''; |
| } |
| |
| } |
| |
| |
| applyTheme(localStorage.getItem('theme') || 'light'); |
| |
| |
| window.addEventListener('storage', function (e) { |
| if (e.key === 'theme') applyTheme(e.newValue); |
| }); |
| |
| var overlay = document.getElementById('theme-overlay'); |
| if (!overlay) { |
| overlay = document.createElement('div'); |
| overlay.id = 'theme-overlay'; |
| overlay.style.cssText = [ |
| 'position:fixed','inset:0','z-index:99999', |
| 'pointer-events:none','opacity:0', |
| 'transition:opacity 0.18s ease','background:#fff' |
| ].join(';'); |
| document.body.appendChild(overlay); |
| } |
| |
| function switchTheme() { |
| var darkTheme = document.getElementById('darkTheme'); |
| if (!darkTheme) return; |
| var goingDark = darkTheme.disabled; |
| overlay.style.background = goingDark ? '#fff' : '#1a1a2e'; |
| overlay.style.opacity = '1'; |
| setTimeout(function () { |
| var newTheme = goingDark ? 'dark' : 'light'; |
| applyTheme(newTheme); |
| localStorage.setItem('theme', newTheme); |
| setTimeout(function () { |
| overlay.style.opacity = '0'; |
| }, 50); |
| }, 180); |
| } |
| |
| document.addEventListener('DOMContentLoaded', function () { |
| var btn = document.getElementById('modeSwitcher'); |
| if (!btn) return; |
| var newBtn = btn.cloneNode(true); |
| btn.parentNode.replaceChild(newBtn, btn); |
| newBtn.addEventListener('click', function (e) { |
| e.preventDefault(); |
| e.stopPropagation(); |
| switchTheme(); |
| }); |
| }); |
| })(); |
| |
| |
| |
| |
| |
| |
| |
| (function() { |
| var sidebar = document.getElementById('leftSidebar'); |
| var logo = document.getElementById('sidebarLogo'); |
| var logoMini = document.getElementById('sidebarLogoMini'); |
| if (!sidebar || !logo || !logoMini) return; |
| |
| function updateLogo() { |
| var isCollapsed = sidebar.classList.contains('collapsed') |
| || document.body.classList.contains('collapsed') |
| || sidebar.offsetWidth < 100; |
| |
| logo.style.display = isCollapsed ? 'none' : ''; |
| logoMini.style.display = isCollapsed ? 'block' : 'none'; |
| } |
| |
| var observer = new MutationObserver(updateLogo); |
| observer.observe(sidebar, { attributes: true, attributeFilter: ['class', 'style'] }); |
| observer.observe(document.body, { attributes: true, attributeFilter: ['class'] }); |
| |
| updateLogo(); |
| })(); |
| </script> |
|
|
| |
| </body> |
| </html> |