RetinAI / web /index.html
ViannyCruz's picture
Update web/index.html
cbea885 verified
Raw
History Blame
94.2 kB
<!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 dropdown">
<a class="nav-link dropdown-toggle text-muted pr-0" href="#" id="navbarDropdownMenuLink" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span class="avatar avatar-sm mt-2">
<img src="./assets/images/persona.png" alt="..." class="avatar-img rounded-circle">
</span>
</a>
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="navbarDropdownMenuLink">
<a class="dropdown-item" href="#" onclick="confirmLogout()">Cerrar Sesión</a>
</div>
</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">Total 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">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">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">&times;</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">&times;</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') {
updateChartData(7);
}
});
}
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>&times;</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">&times;</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>&times;</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);
});
function switchTheme() {
var darkTheme = document.getElementById('darkTheme');
if (!darkTheme) return;
var newTheme = darkTheme.disabled ? 'dark' : 'light';
applyTheme(newTheme);
localStorage.setItem('theme', newTheme);
if (typeof updateChartData === 'function') {
updateChartData(7);
}
}
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>