Download web/index.html from CYV28/RetinAI: direct link, hf CLI and curl.
- Browser
- Download file 95.1 kB
-
https://huggingface.co/spaces/CYV28/RetinAI/resolve/main/web/index.html
- Command line
-
hf download hf://spaces/CYV28/RetinAI/web/index.html
-
curl -L -o index.html https://huggingface.co/spaces/CYV28/RetinAI/resolve/main/web/index.html
95.1 kB
| <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 ; | |
| align-items: center ; | |
| justify-content: center ; | |
| } | |
| #leftSidebar.collapsed #sidebarLogo, | |
| body.collapsed #leftSidebar #sidebarLogo, | |
| #leftSidebar[style*="width: 0"] #sidebarLogo { | |
| max-width: 36px ; | |
| width: 36px ; | |
| } | |
| </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"> | |
| <!-- nav bar --> | |
| <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> <!-- .card-body --> | |
| </div> <!-- .card --> | |
| </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> <!-- .row --> | |
| </div> <!-- .col-12 --> | |
| </div> <!-- .row --> | |
| </div> <!-- .container-fluid --> | |
| <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> <!-- / .row --> | |
| </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> <!-- / .row --> | |
| </div> <!-- / .list-group --> | |
| </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> <!-- 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' /* fix modal open */ | |
| }); | |
| /** date range picker */ | |
| 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: "___.___.___.___" | |
| }); | |
| // editor | |
| 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; | |
| // Usar || 0 para evitar undefined/null que causan contadores negativos | |
| 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 + '%'); | |
| // Pasar stats normalizados al donut | |
| 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) { | |
| // Agrupar consultas por fecha — parsear correctamente | |
| const grouped = {}; | |
| response.consultations.forEach(c => { | |
| const raw = c.consultationDate || ''; | |
| // Soporta "2026-03-17 23:05:12" y "2026-03-17T23:05:12" | |
| 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; /* legacy - no endpoint */ } | |
| 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); | |
| // loadRecentAnalysis() - handled by loadRecentConsultations | |
| } 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...'); | |
| // Obtener pacientes y consultas en paralelo | |
| Promise.all([ | |
| api.getPatients(''), | |
| api.getConsultations(1, 1000) | |
| ]).then(function([patientsResp, consultationsResp]) { | |
| if (!patientsResp || !patientsResp.success || !patientsResp.patients.length) { | |
| showEmptyAgeChart(); | |
| return; | |
| } | |
| // Obtener IDs de pacientes con al menos un diagnóstico positivo de RD | |
| const positivePatientIds = new Set(); | |
| if (consultationsResp && consultationsResp.success && consultationsResp.consultations) { | |
| consultationsResp.consultations.forEach(c => { | |
| if (c.diabeticRetinopathy && c.patientID) { | |
| positivePatientIds.add(c.patientID); | |
| } | |
| }); | |
| } | |
| // Filtrar solo pacientes con RD y con fecha de nacimiento | |
| const patients = patientsResp.patients.filter(p => | |
| p.birthDate && positivePatientIds.has(p.patientID) | |
| ); | |
| if (!patients.length) { showEmptyAgeChart(); return; } | |
| // Calcular rangos de edad | |
| 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', //(0-10) | |
| '#BBDEFB', //(11-20) | |
| '#90CAF9', //(21-30) | |
| '#64B5F6', //(31-40) | |
| '#42A5F5', //(41-50) | |
| '#2196F3', //(51-60) | |
| '#1E88E5', //(61-70) | |
| '#1976D2', //(71-80) | |
| '#1565C0', //(81-90) | |
| '#0D47A1' //(91+) | |
| ]; | |
| 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 () { | |
| // Feather: reemplazar SOLO íconos que aún son <i>, nunca tocar SVGs ya renderizados | |
| function _initFeather() { | |
| if (typeof feather === 'undefined') return; | |
| // Solo actuar si hay elementos <i data-feather> sin reemplazar todavía | |
| 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; // evitar loops | |
| var hasPendingFeather = mutations.some(function (m) { | |
| return Array.from(m.addedNodes).some(function (n) { | |
| if (n.nodeType !== 1) return false; | |
| // Solo disparar si el nodo agregado tiene <i data-feather> dentro | |
| 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 = ''; | |
| } | |
| // ✅ NO tocar visibility aquí — solo el script del <head> debe hacerlo al cargar | |
| } | |
| // Aplicar tema guardado (sin tocar visibility) | |
| applyTheme(localStorage.getItem('theme') || 'light'); | |
| // Sincronizar entre pestañas | |
| 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> |