ViannyCruz commited on
Commit
3ce36cf
·
verified ·
1 Parent(s): 50c0cce

Upload 3 files

Browse files
Files changed (3) hide show
  1. web/api.js +91 -0
  2. web/auth-login.html +131 -0
  3. web/index.html +1930 -0
web/api.js ADDED
@@ -0,0 +1,91 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * api.js — Capa de comunicación con el backend Flask
3
+ * Reemplaza todas las llamadas eel.función() del frontend original.
4
+ */
5
+
6
+ const api = (() => {
7
+ const BASE = '';
8
+
9
+ // Páginas que NO deben redirigir al login (para evitar loops)
10
+ const ON_LOGIN_PAGE = window.location.pathname.includes('auth-login');
11
+
12
+ async function request(method, path, body = null) {
13
+ const opts = {
14
+ method,
15
+ headers: { 'Content-Type': 'application/json' },
16
+ credentials: 'same-origin',
17
+ };
18
+ if (body !== null) opts.body = JSON.stringify(body);
19
+
20
+ let data;
21
+ try {
22
+ const res = await fetch(BASE + path, opts);
23
+ data = await res.json();
24
+ } catch (e) {
25
+ if (!ON_LOGIN_PAGE) window.location.href = '/auth-login.html';
26
+ return { success: false, error: 'Error de conexión' };
27
+ }
28
+
29
+ // Solo redirigir si NO estamos ya en la página de login
30
+ if (data.redirect_to_login && !ON_LOGIN_PAGE) {
31
+ window.location.href = '/auth-login.html';
32
+ }
33
+
34
+ return data;
35
+ }
36
+
37
+ const get = (path) => request('GET', path);
38
+ const post = (path, body) => request('POST', path, body);
39
+ const put = (path, body) => request('PUT', path, body);
40
+ const del = (path) => request('DELETE', path);
41
+
42
+ return {
43
+ /* ── SESIÓN ─────────────────────────────────────────── */
44
+ login: (username, password) => post('/api/login', { username, password }),
45
+ logout: () => post('/api/logout', {}),
46
+ getSession: () => get('/api/session'),
47
+
48
+ /* ── USUARIOS (solo Admin) ───────────────────────────── */
49
+ getUsers: () => get('/api/users'),
50
+ createUser: (username, password, role) => post('/api/users', { username, password, role }),
51
+ updateUser: (userId, data) => put(`/api/users/${userId}`, data),
52
+ deleteUser: (userId) => del(`/api/users/${userId}`),
53
+
54
+ /* ── PACIENTES ──────────────────────────────────────── */
55
+ getPatients: (search = '') => get(`/api/patients${search ? '?search=' + encodeURIComponent(search) : ''}`),
56
+ createPatient: (data) => post('/api/patients', data),
57
+ getPatient: (id) => get(`/api/patients/${id}`),
58
+ updatePatient: (id, data) => put(`/api/patients/${id}`, data),
59
+ deletePatient: (id) => del(`/api/patients/${id}`),
60
+
61
+ /* ── FACTORES DE RIESGO ─────────────────────────────── */
62
+ getRiskFactors: () => get('/api/risk-factors'),
63
+ addPatientRiskFactor: (pid, rfid) => post(`/api/patients/${pid}/risk-factors`, { riskFactorID: rfid }),
64
+ removePatientRiskFactor: (pid, rfid) => del(`/api/patients/${pid}/risk-factors/${rfid}`),
65
+
66
+ /* ── PREDICCIÓN / IA ────────────────────────────────── */
67
+ predict: (imageData, filename) =>
68
+ post('/api/predict', { imageData, filename }),
69
+
70
+ gradcam: (imageData, filename, predictionResult) =>
71
+ post('/api/gradcam', { imageData, filename, predictionResult }),
72
+
73
+ /* ── CONSULTAS ──────────────────────────────────────── */
74
+ getConsultations: (page = 1, perPage = 10, search = '', filter = 'all') =>
75
+ get(`/api/consultations?page=${page}&per_page=${perPage}&search=${encodeURIComponent(search)}&filter=${filter}`),
76
+
77
+ saveConsultation: (data) => post('/api/consultations', data),
78
+
79
+ /* ── DASHBOARD ──────────────────────────────────────── */
80
+ getDashboardStats: () => get('/api/dashboard/stats'),
81
+ getModelInfo: () => get('/api/model/info'),
82
+
83
+ /* ── TAREAS ─────────────────────────────────────────── */
84
+ getTasks: () => get('/api/tasks'),
85
+ addTask: (text) => post('/api/tasks', { text }),
86
+ toggleTask: (id) => post(`/api/tasks/${id}/toggle`, {}),
87
+ deleteTask: (id) => del(`/api/tasks/${id}`),
88
+ };
89
+ })();
90
+
91
+ if (typeof window !== 'undefined') window.api = api;
web/auth-login.html ADDED
@@ -0,0 +1,131 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
6
+ <meta name="description" content="">
7
+ <meta name="author" content="">
8
+ <link rel="icon" href="favicon.ico">
9
+ <title>Login</title>
10
+ <link rel="stylesheet" href="css/simplebar.css">
11
+ <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">
12
+ <link rel="stylesheet" href="css/feather.css">
13
+ <link rel="stylesheet" href="css/daterangepicker.css">
14
+ <link rel="stylesheet" href="css/app-light.css" id="lightTheme">
15
+ <link rel="stylesheet" href="css/app-dark.css" id="darkTheme" disabled>
16
+ <style>
17
+ .error-message {
18
+ color: #dc3545;
19
+ font-size: 0.875rem;
20
+ margin-top: 0.25rem;
21
+ display: none;
22
+ }
23
+ .is-invalid {
24
+ border-color: #dc3545 !important;
25
+ }
26
+ #loadingIndicator {
27
+ display: none;
28
+ }
29
+ </style>
30
+ </head>
31
+ <body class="light">
32
+ <div class="wrapper vh-100">
33
+ <div class="row align-items-center h-100">
34
+ <form class="col-lg-3 col-md-4 col-10 mx-auto text-center" id="loginForm">
35
+ <a class="navbar-brand mx-auto mt-2 flex-fill text-center">
36
+ <img src="assets/images/LOGO.png" alt="Logo">
37
+ </a>
38
+ <h1 class="h6 mb-3">Iniciar Sesión</h1>
39
+
40
+ <div id="generalError" class="alert alert-danger" role="alert" style="display: none;"></div>
41
+
42
+ <div class="form-group">
43
+ <label for="inputUsername" class="sr-only">Usuario</label>
44
+ <input type="text" id="inputUsername" class="form-control form-control-lg" placeholder="Usuario" required autofocus>
45
+ <div id="usernameError" class="error-message">Usuario requerido</div>
46
+ </div>
47
+ <div class="form-group">
48
+ <label for="inputPassword" class="sr-only">Contraseña</label>
49
+ <input type="password" id="inputPassword" class="form-control form-control-lg" placeholder="Contraseña" required>
50
+ <div id="passwordError" class="error-message">Contraseña requerida</div>
51
+ </div>
52
+
53
+ <button class="btn btn-lg btn-primary btn-block" type="submit" id="loginButton">
54
+ <span id="loginText">Ingresar</span>
55
+ <span id="loadingIndicator" class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
56
+ </button>
57
+ <p class="mt-5 mb-3 text-muted">Sistema de Diagnóstico asistido por Inteligencia Artificial para la detección de Retinopatía Diabética</p>
58
+ </form>
59
+ </div>
60
+ </div>
61
+
62
+ <script src="js/jquery.min.js"></script>
63
+ <script src="js/popper.min.js"></script>
64
+ <script src="js/bootstrap.min.js"></script>
65
+ <script src="api.js"></script>
66
+
67
+ <script>
68
+ $(document).ready(function() {
69
+ checkCurrentUser();
70
+ loadRememberedUser();
71
+ $('#loginForm').on('submit', handleLogin);
72
+
73
+ async function checkCurrentUser() {
74
+ try {
75
+ const r = await api.getSession();
76
+ if (r && r.success) window.location.href = 'index.html';
77
+ } catch(e) {}
78
+ }
79
+
80
+ function loadRememberedUser() {
81
+ if (localStorage.getItem('rememberMe') === 'true') {
82
+ const u = localStorage.getItem('username');
83
+ if (u) $('#inputUsername').val(u);
84
+ }
85
+ }
86
+
87
+ async function handleLogin(e) {
88
+ e.preventDefault();
89
+ $('.error-message').hide();
90
+ $('#inputUsername, #inputPassword').removeClass('is-invalid');
91
+ $('#generalError').hide().removeClass('alert-success').addClass('alert-danger');
92
+
93
+ const username = $('#inputUsername').val().trim();
94
+ const password = $('#inputPassword').val().trim();
95
+ if (!username) { $('#inputUsername').addClass('is-invalid'); $('#usernameError').show(); return; }
96
+ if (!password) { $('#inputPassword').addClass('is-invalid'); $('#passwordError').show(); return; }
97
+
98
+ $('#loginText').hide(); $('#loadingIndicator').show(); $('#loginButton').prop('disabled', true);
99
+ const response = await api.login(username, password).catch(() => null);
100
+ $('#loginText').show(); $('#loadingIndicator').hide(); $('#loginButton').prop('disabled', false);
101
+
102
+ if (response && response.success) {
103
+ if ($('#rememberMe').is(':checked')) {
104
+ localStorage.setItem('rememberMe', 'true');
105
+ localStorage.setItem('username', username);
106
+ } else {
107
+ localStorage.removeItem('rememberMe');
108
+ localStorage.removeItem('username');
109
+ }
110
+ $('#generalError').removeClass('alert-danger').addClass('alert-success')
111
+ .text('¡Bienvenido! Redirigiendo...').show();
112
+ setTimeout(() => { window.location.href = 'index.html'; }, 800);
113
+ } else {
114
+ const msg = response?.message || 'Usuario o contraseña incorrectos';
115
+ $('#generalError').text(msg).show();
116
+ $('#inputUsername, #inputPassword').addClass('is-invalid');
117
+ $('#inputPassword').val('');
118
+ }
119
+ }
120
+ });
121
+ </script>
122
+
123
+
124
+
125
+
126
+
127
+
128
+
129
+ </body>
130
+ </html>
131
+ </html>
web/index.html ADDED
@@ -0,0 +1,1930 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
6
+ <meta name="description" content="">
7
+ <meta name="author" content="">
8
+ <link rel="icon" href="favicon.ico">
9
+ <title>Dashboard</title>
10
+ <link rel="stylesheet" href="css/simplebar.css">
11
+ <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">
12
+ <link rel="stylesheet" href="css/feather.css">
13
+ <link rel="stylesheet" href="css/select2.css">
14
+ <link rel="stylesheet" href="css/dropzone.css">
15
+ <link rel="stylesheet" href="css/uppy.min.css">
16
+ <link rel="stylesheet" href="css/jquery.steps.css">
17
+ <link rel="stylesheet" href="css/jquery.timepicker.css">
18
+ <link rel="stylesheet" href="css/quill.snow.css">
19
+ <link rel="stylesheet" href="css/daterangepicker.css">
20
+ <link rel="stylesheet" href="css/app-light.css" id="lightTheme">
21
+ <link rel="stylesheet" href="css/app-dark.css" id="darkTheme" disabled>
22
+ </head>
23
+ <body class="vertical light ">
24
+ <div class="wrapper">
25
+ <nav class="topnav navbar navbar-light">
26
+ <button type="button" class="navbar-toggler text-muted mt-2 p-0 mr-3 collapseSidebar">
27
+ <i class="fe fe-menu navbar-toggler-icon"></i>
28
+ </button>
29
+
30
+ <ul class="nav">
31
+
32
+
33
+ <li class="nav-item dropdown">
34
+ <a class="nav-link dropdown-toggle text-muted pr-0" href="#" id="navbarDropdownMenuLink" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
35
+ <span class="avatar avatar-sm mt-2">
36
+ <img src="./assets/images/persona.png" alt="..." class="avatar-img rounded-circle">
37
+ </span>
38
+ </a>
39
+ <div class="dropdown-menu dropdown-menu-right" aria-labelledby="navbarDropdownMenuLink">
40
+ <a class="dropdown-item" href="#" onclick="confirmLogout()">Cerrar Sesión</a>
41
+ </div>
42
+ </li>
43
+ </ul>
44
+ </nav>
45
+ <aside class="sidebar-left border-right bg-white shadow" id="leftSidebar" data-simplebar>
46
+ <a href="#" class="btn collapseSidebar toggle-btn d-lg-none text-muted ml-2 mt-3" data-toggle="toggle">
47
+ <i class="fe fe-x"><span class="sr-only"></span></i>
48
+ </a>
49
+ <nav class="vertnav navbar navbar-light">
50
+ <!-- nav bar -->
51
+ <div class="w-100 mb-4 d-flex">
52
+ <a class="navbar-brand mx-auto mt-2 flex-fill text-center">
53
+ <img src="assets/images/LOGO.png" alt="Logo">
54
+ </a>
55
+ </div>
56
+
57
+ <ul class="navbar-nav flex-fill w-100 mb-2">
58
+ <li class="nav-item active">
59
+ <a class="nav-link" href="./index.html">
60
+ <i class="fe fe-home fe-16"></i>
61
+ <span class="ml-3 item-text">Dashboard</span>
62
+ </a>
63
+ </li>
64
+ <li class="nav-item">
65
+ <a class="nav-link" href="./diagnosis.html">
66
+ <i class="fe fe-eye fe-16"></i>
67
+ <span class="ml-3 item-text">Diagnóstico</span>
68
+ </a>
69
+ </li>
70
+ <li class="nav-item">
71
+ <a class="nav-link" href="./patients.html">
72
+ <i class="fe fe-users fe-16"></i>
73
+ <span class="ml-3 item-text">Pacientes</span>
74
+ </a>
75
+ </li>
76
+ <li class="nav-item">
77
+ <a class="nav-link" href="./historial-consultas.html">
78
+ <i class="fe fe-file-text fe-16"></i>
79
+ <span class="ml-3 item-text">Consultas</span>
80
+ </a>
81
+ </li>
82
+ <li class="nav-item active">
83
+ <a class="nav-link" href="./users.html">
84
+ <i class="fe fe-user fe-16"></i>
85
+ <span class="ml-3 item-text">Usuarios</span>
86
+ </a>
87
+ </li>
88
+ </ul>
89
+ </nav>
90
+ </aside>
91
+ <main role="main" class="main-content">
92
+ <div class="container-fluid">
93
+ <div class="row justify-content-center">
94
+ <div class="col-12">
95
+ <div class="row mb-4">
96
+ <div class="col-md-6 col-lg-3">
97
+ <div class="card shadow border-0 mb-4" id="totalPatientsCard">
98
+ <div class="card-body">
99
+ <div class="row align-items-center">
100
+ <div class="col-3 text-center">
101
+ <span class="circle circle-sm bg-primary">
102
+ <i class="fe fe-users fe-16 text-white"></i>
103
+ </span>
104
+ </div>
105
+ <div class="col pr-0">
106
+ <p class="small text-muted mb-0">Total Pacientes</p>
107
+ <span class="h3 mb-0">0</span>
108
+ </div>
109
+ </div>
110
+ </div>
111
+ </div>
112
+ </div>
113
+ <div class="col-md-6 col-lg-3">
114
+ <div class="card shadow border-0 mb-4" id="positiveCasesCard">
115
+ <div class="card-body">
116
+ <div class="row align-items-center">
117
+ <div class="col-3 text-center">
118
+ <span class="circle circle-sm bg-warning">
119
+ <i class="fe fe-alert-triangle fe-16 text-white"></i>
120
+ </span>
121
+ </div>
122
+ <div class="col pr-0">
123
+ <p class="small text-muted mb-0">Con Retinopatía</p>
124
+ <span class="h3 mb-0">0</span>
125
+ </div>
126
+ </div>
127
+ </div>
128
+ </div>
129
+ </div>
130
+ <div class="col-md-6 col-lg-3">
131
+ <div class="card shadow border-0 mb-4" id="negativeCasesCard">
132
+ <div class="card-body">
133
+ <div class="row align-items-center">
134
+ <div class="col-3 text-center">
135
+ <span class="circle circle-sm bg-success">
136
+ <i class="fe fe-check fe-16 text-white"></i>
137
+ </span>
138
+ </div>
139
+ <div class="col pr-0">
140
+ <p class="small text-muted mb-0">Sin Retinopatía</p>
141
+ <span class="h3 mb-0">0</span>
142
+ </div>
143
+ </div>
144
+ </div>
145
+ </div>
146
+ </div>
147
+ <div class="col-md-6 col-lg-3">
148
+ <div class="card shadow border-0 mb-4" id="positivityRateCard">
149
+ <div class="card-body">
150
+ <div class="row align-items-center">
151
+ <div class="col-3 text-center">
152
+ <span class="circle circle-sm bg-info">
153
+ <i class="fe fe-percent fe-16 text-white"></i>
154
+ </span>
155
+ </div>
156
+ <div class="col pr-0">
157
+ <p class="small text-muted mb-0">Tasa Positividad</p>
158
+ <span class="h3 mb-0">0%</span>
159
+ </div>
160
+ </div>
161
+ </div>
162
+ </div>
163
+ </div>
164
+ </div>
165
+
166
+ <div class="mb-2 align-items-center">
167
+ <div class="card shadow mb-4">
168
+ <div class="card-body">
169
+ <div class="row mt-1 align-items-center"></div>
170
+ <div class="chartbox mr-4">
171
+ <div class="d-flex justify-content-between align-items-center mb-3">
172
+ <h6 class="card-title mb-0">Estadísticas de Consultas</h6>
173
+ </div>
174
+ <div id="areaChart"></div>
175
+ </div>
176
+ </div> <!-- .card-body -->
177
+ </div> <!-- .card -->
178
+ </div>
179
+
180
+ <div class="row">
181
+ <div class="col-md-12 col-lg-4">
182
+ <div class="card shadow h-100 mb-4">
183
+ <div class="card-header">
184
+ <h6 class="card-title mb-0">
185
+ <i class="fe fe-pie-chart mr-2"></i>
186
+ Rangos de Edad con Retinopatía
187
+ </h6>
188
+ </div>
189
+ <div class="card-body d-flex flex-column">
190
+ <div class="chart-widget mb-2 flex-grow-1 d-flex align-items-center justify-content-center">
191
+ <div id="ageRangesChart" style="width: 100%; height: 300px;"></div>
192
+ </div>
193
+ <div class="row items-align-center mt-auto" id="ageStatsRow">
194
+ <div class="col-4 text-center">
195
+ <p class="text-muted mb-1">Promedio</p>
196
+ <h6 class="mb-1" id="avgAge">-</h6>
197
+ <p class="text-muted mb-0">años</p>
198
+ </div>
199
+ <div class="col-4 text-center">
200
+ <p class="text-muted mb-1">Rango</p>
201
+ <h6 class="mb-1" id="ageRange">-</h6>
202
+ <p class="text-muted mb-0">min-max</p>
203
+ </div>
204
+ <div class="col-4 text-center">
205
+ <p class="text-muted mb-1">Total</p>
206
+ <h6 class="mb-1" id="totalPatientsAge">-</h6>
207
+ <p class="text-muted mb-0">consultas</p>
208
+ </div>
209
+ </div>
210
+ </div>
211
+ </div>
212
+ </div>
213
+
214
+ <div class="col-md-12 col-lg-8">
215
+ <div class="card shadow h-100">
216
+ <div class="card-header">
217
+ <strong class="card-title">Consultas Recientes</strong>
218
+ </div>
219
+ <div class="card-body my-n2">
220
+ <table class="table table-striped table-hover table-borderless">
221
+ <thead>
222
+ <tr>
223
+ <th>Fecha</th>
224
+ <th>Paciente</th>
225
+ <th>Resultado</th>
226
+ <th>Confianza</th>
227
+ </tr>
228
+ </thead>
229
+ <tbody id="recentConsultationsBody">
230
+ <tr>
231
+ <td colspan="5" class="text-center text-muted">
232
+ <i class="fe fe-loader fe-16 spinner"></i> Cargando consultas...
233
+ </td>
234
+ </tr>
235
+ </tbody>
236
+ </table>
237
+ </div>
238
+ </div>
239
+ </div>
240
+ </div> <!-- .row -->
241
+ </div> <!-- .col-12 -->
242
+ </div> <!-- .row -->
243
+ </div> <!-- .container-fluid -->
244
+ <div class="modal fade modal-notif modal-slide" tabindex="-1" role="dialog" aria-labelledby="defaultModalLabel" aria-hidden="true">
245
+ <div class="modal-dialog modal-sm" role="document">
246
+ <div class="modal-content">
247
+ <div class="modal-header">
248
+ <h5 class="modal-title" id="defaultModalLabel">Notifications</h5>
249
+ <button type="button" class="close" data-dismiss="modal" aria-label="Close">
250
+ <span aria-hidden="true">&times;</span>
251
+ </button>
252
+ </div>
253
+ <div class="modal-body">
254
+ <div class="list-group list-group-flush my-n3">
255
+ <div class="list-group-item bg-transparent">
256
+ <div class="row align-items-center">
257
+ <div class="col-auto">
258
+ <span class="fe fe-box fe-24"></span>
259
+ </div>
260
+ <div class="col">
261
+ <small><strong>Package has uploaded successfull</strong></small>
262
+ <div class="my-0 text-muted small">Package is zipped and uploaded</div>
263
+ <small class="badge badge-pill badge-light text-muted">1m ago</small>
264
+ </div>
265
+ </div>
266
+ </div>
267
+ <div class="list-group-item bg-transparent">
268
+ <div class="row align-items-center">
269
+ <div class="col-auto">
270
+ <span class="fe fe-download fe-24"></span>
271
+ </div>
272
+ <div class="col">
273
+ <small><strong>Widgets are updated successfull</strong></small>
274
+ <div class="my-0 text-muted small">Just create new layout Index, form, table</div>
275
+ <small class="badge badge-pill badge-light text-muted">2m ago</small>
276
+ </div>
277
+ </div>
278
+ </div>
279
+ <div class="list-group-item bg-transparent">
280
+ <div class="row align-items-center">
281
+ <div class="col-auto">
282
+ <span class="fe fe-inbox fe-24"></span>
283
+ </div>
284
+ <div class="col">
285
+ <small><strong>Notifications have been sent</strong></small>
286
+ <div class="my-0 text-muted small">Fusce dapibus, tellus ac cursus commodo</div>
287
+ <small class="badge badge-pill badge-light text-muted">30m ago</small>
288
+ </div>
289
+ </div> <!-- / .row -->
290
+ </div>
291
+ <div class="list-group-item bg-transparent">
292
+ <div class="row align-items-center">
293
+ <div class="col-auto">
294
+ <span class="fe fe-link fe-24"></span>
295
+ </div>
296
+ <div class="col">
297
+ <small><strong>Link was attached to menu</strong></small>
298
+ <div class="my-0 text-muted small">New layout has been attached to the menu</div>
299
+ <small class="badge badge-pill badge-light text-muted">1h ago</small>
300
+ </div>
301
+ </div>
302
+ </div> <!-- / .row -->
303
+ </div> <!-- / .list-group -->
304
+ </div>
305
+ <div class="modal-footer">
306
+ <button type="button" class="btn btn-secondary btn-block" data-dismiss="modal">Clear All</button>
307
+ </div>
308
+ </div>
309
+ </div>
310
+ </div>
311
+ <div class="modal fade modal-shortcut modal-slide" tabindex="-1" role="dialog" aria-labelledby="defaultModalLabel" aria-hidden="true">
312
+ <div class="modal-dialog" role="document">
313
+ <div class="modal-content">
314
+ <div class="modal-header">
315
+ <h5 class="modal-title" id="defaultModalLabel">Shortcuts</h5>
316
+ <button type="button" class="close" data-dismiss="modal" aria-label="Close">
317
+ <span aria-hidden="true">&times;</span>
318
+ </button>
319
+ </div>
320
+ <div class="modal-body px-5">
321
+ <div class="row align-items-center">
322
+ <div class="col-6 text-center">
323
+ <div class="squircle bg-success justify-content-center">
324
+ <i class="fe fe-cpu fe-32 align-self-center text-white"></i>
325
+ </div>
326
+ <p>Control area</p>
327
+ </div>
328
+ <div class="col-6 text-center">
329
+ <div class="squircle bg-primary justify-content-center">
330
+ <i class="fe fe-activity fe-32 align-self-center text-white"></i>
331
+ </div>
332
+ <p>Activity</p>
333
+ </div>
334
+ </div>
335
+ <div class="row align-items-center">
336
+ <div class="col-6 text-center">
337
+ <div class="squircle bg-primary justify-content-center">
338
+ <i class="fe fe-droplet fe-32 align-self-center text-white"></i>
339
+ </div>
340
+ <p>Droplet</p>
341
+ </div>
342
+ <div class="col-6 text-center">
343
+ <div class="squircle bg-primary justify-content-center">
344
+ <i class="fe fe-upload-cloud fe-32 align-self-center text-white"></i>
345
+ </div>
346
+ <p>Upload</p>
347
+ </div>
348
+ </div>
349
+ <div class="row align-items-center">
350
+ <div class="col-6 text-center">
351
+ <div class="squircle bg-primary justify-content-center">
352
+ <i class="fe fe-users fe-32 align-self-center text-white"></i>
353
+ </div>
354
+ <p>Users</p>
355
+ </div>
356
+ <div class="col-6 text-center">
357
+ <div class="squircle bg-primary justify-content-center">
358
+ <i class="fe fe-settings fe-32 align-self-center text-white"></i>
359
+ </div>
360
+ <p>Settings</p>
361
+ </div>
362
+ </div>
363
+ </div>
364
+ </div>
365
+ </div>
366
+ </div>
367
+ </main> <!-- main -->
368
+ <script src="js/jquery.min.js"></script>
369
+ <script src="js/popper.min.js"></script>
370
+ <script src="js/moment.min.js"></script>
371
+ <script src="js/bootstrap.min.js"></script>
372
+ <script src="js/simplebar.min.js"></script>
373
+ <script src='js/daterangepicker.js'></script>
374
+ <script src='js/jquery.stickOnScroll.js'></script>
375
+ <script src="js/tinycolor-min.js"></script>
376
+ <script src="js/config.js"></script>
377
+ <script src="js/d3.min.js"></script>
378
+ <script src="js/topojson.min.js"></script>
379
+ <script src="js/datamaps.all.min.js"></script>
380
+ <script src="js/datamaps-zoomto.js"></script>
381
+ <script src="js/datamaps.custom.js"></script>
382
+ <script src="js/Chart.min.js"></script>
383
+ <script src="api.js"></script>
384
+ <script>
385
+ api.getSession().catch(() => { window.location.href = 'auth-login.html'; })
386
+ .then(r => { if (r && !r.success) window.location.href = 'auth-login.html'; });
387
+ </script>
388
+ <script src="js/gauge.min.js"></script>
389
+ <script src="js/jquery.sparkline.min.js"></script>
390
+ <script src="js/apexcharts.min.js"></script>
391
+ <script src="js/apexcharts.custom.js"></script>
392
+
393
+ <script src='js/jquery.mask.min.js'></script>
394
+ <script src='js/select2.min.js'></script>
395
+ <script src='js/jquery.steps.min.js'></script>
396
+ <script src='js/jquery.validate.min.js'></script>
397
+ <script src='js/jquery.timepicker.js'></script>
398
+ <script src='js/dropzone.min.js'></script>
399
+ <script src='js/uppy.min.js'></script>
400
+ <script src='js/quill.min.js'></script>
401
+ <script>
402
+ $('.select2').select2(
403
+ {
404
+ theme: 'bootstrap4',
405
+ });
406
+ $('.select2-multi').select2(
407
+ {
408
+ multiple: true,
409
+ theme: 'bootstrap4',
410
+ });
411
+ $('.drgpicker').daterangepicker(
412
+ {
413
+ singleDatePicker: true,
414
+ timePicker: false,
415
+ showDropdowns: true,
416
+ locale:
417
+ {
418
+ format: 'MM/DD/YYYY'
419
+ }
420
+ });
421
+ $('.time-input').timepicker(
422
+ {
423
+ 'scrollDefault': 'now',
424
+ 'zindex': '9999' /* fix modal open */
425
+ });
426
+ /** date range picker */
427
+ if ($('.datetimes').length)
428
+ {
429
+ $('.datetimes').daterangepicker(
430
+ {
431
+ timePicker: true,
432
+ startDate: moment().startOf('hour'),
433
+ endDate: moment().startOf('hour').add(32, 'hour'),
434
+ locale:
435
+ {
436
+ format: 'M/DD hh:mm A'
437
+ }
438
+ });
439
+ }
440
+ var start = moment().subtract(29, 'days');
441
+ var end = moment();
442
+
443
+ function cb(start, end)
444
+ {
445
+ $('#reportrange span').html(start.format('MMMM D, YYYY') + ' - ' + end.format('MMMM D, YYYY'));
446
+ }
447
+ $('#reportrange').daterangepicker(
448
+ {
449
+ startDate: start,
450
+ endDate: end,
451
+ ranges:
452
+ {
453
+ 'Today': [moment(), moment()],
454
+ 'Yesterday': [moment().subtract(1, 'days'), moment().subtract(1, 'days')],
455
+ 'Last 7 Days': [moment().subtract(6, 'days'), moment()],
456
+ 'Last 30 Days': [moment().subtract(29, 'days'), moment()],
457
+ 'This Month': [moment().startOf('month'), moment().endOf('month')],
458
+ 'Last Month': [moment().subtract(1, 'month').startOf('month'), moment().subtract(1, 'month').endOf('month')]
459
+ }
460
+ }, cb);
461
+ cb(start, end);
462
+ $('.input-placeholder').mask("00/00/0000",
463
+ {
464
+ placeholder: "__/__/____"
465
+ });
466
+ $('.input-zip').mask('00000-000',
467
+ {
468
+ placeholder: "____-___"
469
+ });
470
+ $('.input-money').mask("#.##0,00",
471
+ {
472
+ reverse: true
473
+ });
474
+ $('.input-phoneus').mask('(000) 000-0000');
475
+ $('.input-mixed').mask('AAA 000-S0S');
476
+ $('.input-ip').mask('0ZZ.0ZZ.0ZZ.0ZZ',
477
+ {
478
+ translation:
479
+ {
480
+ 'Z':
481
+ {
482
+ pattern: /[0-9]/,
483
+ optional: true
484
+ }
485
+ },
486
+ placeholder: "___.___.___.___"
487
+ });
488
+ // editor
489
+ var editor = document.getElementById('editor');
490
+ if (editor)
491
+ {
492
+ var toolbarOptions = [
493
+ [
494
+ {
495
+ 'font': []
496
+ }],
497
+ [
498
+ {
499
+ 'header': [1, 2, 3, 4, 5, 6, false]
500
+ }],
501
+ ['bold', 'italic', 'underline', 'strike'],
502
+ ['blockquote', 'code-block'],
503
+ [
504
+ {
505
+ 'header': 1
506
+ },
507
+ {
508
+ 'header': 2
509
+ }],
510
+ [
511
+ {
512
+ 'list': 'ordered'
513
+ },
514
+ {
515
+ 'list': 'bullet'
516
+ }],
517
+ [
518
+ {
519
+ 'script': 'sub'
520
+ },
521
+ {
522
+ 'script': 'super'
523
+ }],
524
+ [
525
+ {
526
+ 'indent': '-1'
527
+ },
528
+ {
529
+ 'indent': '+1'
530
+ }],
531
+ [
532
+ {
533
+ 'direction': 'rtl'
534
+ }],
535
+ [
536
+ {
537
+ 'color': []
538
+ },
539
+ {
540
+ 'background': []
541
+ }],
542
+ [
543
+ {
544
+ 'align': []
545
+ }],
546
+ ['clean']
547
+ ];
548
+ var quill = new Quill(editor,
549
+ {
550
+ modules:
551
+ {
552
+ toolbar: toolbarOptions
553
+ },
554
+ theme: 'snow'
555
+ });
556
+ }
557
+ (function()
558
+ {
559
+ 'use strict';
560
+ window.addEventListener('load', function()
561
+ {
562
+ var forms = document.getElementsByClassName('needs-validation');
563
+ var validation = Array.prototype.filter.call(forms, function(form)
564
+ {
565
+ form.addEventListener('submit', function(event)
566
+ {
567
+ if (form.checkValidity() === false)
568
+ {
569
+ event.preventDefault();
570
+ event.stopPropagation();
571
+ }
572
+ form.classList.add('was-validated');
573
+ }, false);
574
+ });
575
+ }, false);
576
+ })();
577
+ </script>
578
+ <script>
579
+ var uptarg = document.getElementById('drag-drop-area');
580
+ if (uptarg)
581
+ {
582
+ var uppy = Uppy.Core().use(Uppy.Dashboard,
583
+ {
584
+ inline: true,
585
+ target: uptarg,
586
+ proudlyDisplayPoweredByUppy: false,
587
+ theme: 'dark',
588
+ width: 770,
589
+ height: 210,
590
+ plugins: ['Webcam']
591
+ }).use(Uppy.Tus,
592
+ {
593
+ endpoint: 'https://master.tus.io/files/'
594
+ });
595
+ uppy.on('complete', (result) =>
596
+ {
597
+ console.log('Upload complete! We\'ve uploaded these files:', result.successful) });
598
+ }
599
+ </script>
600
+ <script src="js/apps.js"></script>
601
+ <script async src="https://www.googletagmanager.com/gtag/js?id=UA-56159088-1"></script>
602
+ <script>
603
+ window.dataLayer = window.dataLayer || [];
604
+
605
+ function gtag()
606
+ {
607
+ dataLayer.push(arguments);
608
+ }
609
+ gtag('js', new Date());
610
+ gtag('config', 'UA-56159088-1');
611
+ </script>
612
+ <script>
613
+ $(document).ready(function() {
614
+ console.log('Dashboard iniciado');
615
+
616
+ let currentUser = null;
617
+ let systemStatus = {};
618
+ let recentAnalysis = [];
619
+ let areaChartInstance = null;
620
+ let ageRangesChartInstance = null;
621
+
622
+ initializeDashboard();
623
+
624
+ setupEventListeners();
625
+
626
+ function initializeDashboard() {
627
+ console.log('Inicializando dashboard...');
628
+
629
+ checkAuthentication();
630
+
631
+ loadSystemStatus();
632
+
633
+ loadDashboardStats();
634
+
635
+ // loadRecentAnalysis() - handled by loadRecentConsultations
636
+
637
+ updateChartData(7);
638
+
639
+ loadAgeRangesData();
640
+
641
+ setupDateRangePicker();
642
+
643
+ updateDateTime();
644
+ setInterval(updateDateTime, 60000); // Actualizar cada minuto
645
+ }
646
+
647
+ async function checkAuthentication() {
648
+ const response = await api.getSession().catch(() => null);
649
+ if (response && response.success) {
650
+ currentUser = response.user;
651
+ updateUserInterface();
652
+ } else {
653
+ window.location.href = 'auth-login.html';
654
+ }
655
+ }
656
+
657
+ function updateUserInterface() {
658
+ if (currentUser) {
659
+ $('#currentUserName').text(currentUser.username);
660
+ $('#welcomeMessage').text(`¡Bienvenido, ${currentUser.username}!`);
661
+
662
+ const roleClass = currentUser.role === 'Admin' ? 'badge-primary' : 'badge-success';
663
+ $('#currentUserName').after(`<span class="badge ${roleClass} ml-2">${currentUser.role}</span>`);
664
+ }
665
+ }
666
+
667
+ async function loadSystemStatus() {
668
+ const response = await api.getModelInfo().catch(() => null);
669
+ if (response && response.loaded) {
670
+ systemStatus = { model_loaded: response.loaded, database_connected: true, app_version: 'v2.1' };
671
+ updateSystemStatusUI();
672
+ } else {
673
+ systemStatus = { model_loaded: false, database_connected: true, app_version: 'v2.1' };
674
+ updateSystemStatusUI();
675
+ }
676
+ }
677
+
678
+ function updateSystemStatusUI() {
679
+ if (systemStatus.model_loaded) {
680
+ $('#modelStatus').removeClass('bg-danger').addClass('bg-success');
681
+ $('#modelStatusText').text('Cargado').removeClass('text-danger').addClass('text-success');
682
+ } else {
683
+ $('#modelStatus').removeClass('bg-success').addClass('bg-danger');
684
+ $('#modelStatusText').text('Error').removeClass('text-success').addClass('text-danger');
685
+ }
686
+
687
+ if (systemStatus.database_connected) {
688
+ $('#dbStatus').removeClass('bg-danger').addClass('bg-success');
689
+ $('#dbStatusText').text('Conectada').removeClass('text-danger').addClass('text-success');
690
+ } else {
691
+ $('#dbStatus').removeClass('bg-success').addClass('bg-danger');
692
+ $('#dbStatusText').text('Desconectada').removeClass('text-success').addClass('text-danger');
693
+ }
694
+
695
+ $('#appVersion').text(systemStatus.app_version || 'v2.1');
696
+
697
+ const isSystemHealthy = systemStatus.model_loaded && systemStatus.database_connected;
698
+ if (isSystemHealthy) {
699
+ $('#systemStatusIcon').removeClass('bg-danger').addClass('bg-success');
700
+ $('#systemStatusIcon i').removeClass('fe-x').addClass('fe-check');
701
+ $('#systemStatusText').text('Operativo');
702
+ } else {
703
+ $('#systemStatusIcon').removeClass('bg-success').addClass('bg-danger');
704
+ $('#systemStatusIcon i').removeClass('fe-check').addClass('fe-x');
705
+ $('#systemStatusText').text('Con errores');
706
+ }
707
+ }
708
+
709
+ async function loadDashboardStats() {
710
+ const response = await api.getDashboardStats().catch(() => null);
711
+ if (response && response.success) {
712
+ const stats = response.stats;
713
+ updateStatCard('#totalPatientsCard', stats.total_patients);
714
+ updateStatCard('#positiveCasesCard', stats.positive_cases);
715
+ updateStatCard('#negativeCasesCard', stats.negative_cases);
716
+ updateStatCard('#positivityRateCard', (stats.positivity_rate || 0) + '%');
717
+ updateDonutChart(stats);
718
+ } else {
719
+ showErrorMessage('Error cargando estadísticas del dashboard');
720
+ }
721
+ }
722
+
723
+ function updateStatCard(selector, value) {
724
+ const card = $(selector);
725
+ const valueElement = card.find('span.h3');
726
+ const currentValue = parseInt(valueElement.text()) || 0;
727
+
728
+ if (typeof value === 'number' || (typeof value === 'string' && !value.includes('%'))) {
729
+ const targetValue = parseInt(value) || 0;
730
+ animateValue(valueElement[0], currentValue, targetValue, 1000);
731
+ } else {
732
+ valueElement.text(value);
733
+ }
734
+ }
735
+
736
+ // Función para animar valores numéricos
737
+ function animateValue(element, start, end, duration) {
738
+ if (!element) return;
739
+
740
+ const startTimestamp = Date.now();
741
+ const step = (timestamp) => {
742
+ const elapsed = timestamp - startTimestamp;
743
+ const progress = Math.min(elapsed / duration, 1);
744
+ const current = Math.floor(progress * (end - start) + start);
745
+ element.textContent = current;
746
+
747
+ if (progress < 1) {
748
+ window.requestAnimationFrame(step);
749
+ }
750
+ };
751
+ window.requestAnimationFrame(step);
752
+ }
753
+
754
+ function updateDonutChart(stats) {
755
+ const donutChartElement = document.querySelector("#donutChart");
756
+ if (!donutChartElement) return;
757
+
758
+ donutChartElement.innerHTML = '';
759
+
760
+ const donutOptions = {
761
+ chart: {
762
+ type: 'donut',
763
+ height: 300
764
+ },
765
+ colors: ['#66DA26', '#FF9800'],
766
+ series: [stats.patients_without_rd || 0, stats.patients_with_rd || 0],
767
+ labels: ['Sin Retinopatía', 'Con Retinopatía'],
768
+ legend: {
769
+ position: 'bottom',
770
+ labels: {
771
+ colors: '#9aa0ac',
772
+ }
773
+ },
774
+ dataLabels: {
775
+ enabled: true,
776
+ formatter: function(val, opts) {
777
+ return Math.round(val) + '%';
778
+ }
779
+ },
780
+ plotOptions: {
781
+ pie: {
782
+ donut: {
783
+ labels: {
784
+ show: true,
785
+ total: {
786
+ show: true,
787
+ label: 'Total Pacientes',
788
+ color: '#9aa0ac',
789
+ formatter: function(w) {
790
+ return stats.total_unique_patients || 0;
791
+ }
792
+ }
793
+ }
794
+ }
795
+ }
796
+ },
797
+ responsive: [{
798
+ breakpoint: 480,
799
+ options: {
800
+ chart: {
801
+ height: 250
802
+ },
803
+ legend: {
804
+ position: 'bottom'
805
+ }
806
+ }
807
+ }]
808
+ };
809
+
810
+ try {
811
+ const donutChart = new ApexCharts(donutChartElement, donutOptions);
812
+ donutChart.render();
813
+ } catch (error) {
814
+ console.error('Error creando gráfico donut:', error);
815
+ }
816
+ }
817
+
818
+ function updateChartData(days) {
819
+ console.log(`Actualizando grafico para los ultimos ${days} dias`);
820
+
821
+ api.getConsultations(1, 100).then(function(response) {
822
+ console.log('Respuesta del backend:', response);
823
+
824
+ if (response && response.success && response.consultations.length > 0) {
825
+ // Agrupar consultas por fecha
826
+ const grouped = {};
827
+ response.consultations.forEach(c => {
828
+ const d = (c.consultationDate || '').substring(0,10);
829
+ if (!grouped[d]) grouped[d] = {total:0, pos:0, neg:0};
830
+ grouped[d].total++;
831
+ if (c.diabeticRetinopathy) grouped[d].pos++; else grouped[d].neg++;
832
+ });
833
+ const dates = Object.keys(grouped).sort();
834
+ const totalC = dates.map(d => grouped[d].total);
835
+ const posC = dates.map(d => grouped[d].pos);
836
+ const negC = dates.map(d => grouped[d].neg);
837
+ createOrUpdateChart(dates, totalC, posC, negC);
838
+ } else {
839
+ createEmptyChart();
840
+ }
841
+ });
842
+ }
843
+
844
+ function setupDateRangePicker() {
845
+ var start = moment().subtract(29, 'days');
846
+ var end = moment();
847
+
848
+ function cb(start, end) {
849
+ $('#reportrange span').html(start.format('MMMM D, YYYY') + ' - ' + end.format('MMMM D, YYYY'));
850
+ }
851
+
852
+ $('#reportrange').daterangepicker({
853
+ startDate: start,
854
+ endDate: end,
855
+ ranges: {
856
+ 'Hoy': [moment(), moment()],
857
+ 'Ayer': [moment().subtract(1, 'days'), moment().subtract(1, 'days')],
858
+ 'Últimos 7 Días': [moment().subtract(6, 'days'), moment()],
859
+ 'Últimos 30 Días': [moment().subtract(29, 'days'), moment()],
860
+ 'Este Mes': [moment().startOf('month'), moment().endOf('month')],
861
+ 'Mes Anterior': [moment().subtract(1, 'month').startOf('month'), moment().subtract(1, 'month').endOf('month')]
862
+ }
863
+ }, cb);
864
+
865
+ cb(start, end);
866
+ }
867
+
868
+ function loadRecentConsultations() {
869
+ $('#recentConsultationsBody').html(`
870
+ <tr>
871
+ <td colspan="5" class="text-center text-muted">
872
+ <i class="fe fe-loader fe-16 spinner"></i> Cargando consultas...
873
+ </td>
874
+ </tr>
875
+ `);
876
+
877
+ api.getConsultations(1, 10).then(function(response) {
878
+ if (response.success) {
879
+ updateRecentConsultationsTable(response.consultations);
880
+ } else {
881
+ showErrorInTable(response.message);
882
+ }
883
+ });
884
+ }
885
+
886
+ function updateRecentConsultationsTable(consultations) {
887
+ const tbody = $('#recentConsultationsBody');
888
+ tbody.empty();
889
+
890
+ if (consultations.length === 0) {
891
+ tbody.append(`
892
+ <tr>
893
+ <td colspan="5" class="text-center text-muted">
894
+ <i class="fe fe-inbox fe-16"></i><br>
895
+ No hay consultas recientes
896
+ </td>
897
+ </tr>
898
+ `);
899
+ return;
900
+ }
901
+
902
+ consultations.forEach(consultation => {
903
+ const date = consultation.consultationDate ?
904
+ new Date(consultation.consultationDate).toLocaleString('es-ES') : 'N/A';
905
+
906
+ const isPositive = consultation.diabeticRetinopathy;
907
+ const resultClass = isPositive ? 'badge-warning' : 'badge-success';
908
+ const resultText = isPositive ? 'Positivo' : 'Negativo';
909
+ const confidence = consultation.confidence ?
910
+ `${consultation.confidence.toFixed(1)}%` : 'N/A';
911
+
912
+ tbody.append(`
913
+ <tr>
914
+ <td>${date}</td>
915
+ <td>${consultation.patient_name || 'Desconocido'}</td>
916
+ <td><span class="badge ${resultClass}">${resultText}</span></td>
917
+ <td>${confidence}</td>
918
+ </tr>
919
+ `);
920
+ });
921
+ }
922
+
923
+ window.showConsultationDetails = function(consultationId) {
924
+ alert('Ver detalles: Consulta #' + consultationId);
925
+ };
926
+
927
+ function showConsultationModal(consultation) {
928
+ const modalHtml = `
929
+ <div class="modal fade" id="consultationModal" tabindex="-1" role="dialog">
930
+ <div class="modal-dialog modal-lg" role="document">
931
+ <div class="modal-content">
932
+ <div class="modal-header">
933
+ <h5 class="modal-title">Detalles de Consulta</h5>
934
+ <button type="button" class="close" data-dismiss="modal">
935
+ <span>&times;</span>
936
+ </button>
937
+ </div>
938
+ <div class="modal-body">
939
+ <div class="row mb-3">
940
+ <div class="col-md-6">
941
+ <h6>Información Básica</h6>
942
+ <p><strong>Paciente:</strong> ${consultation.patient_name || 'Desconocido'}</p>
943
+ <p><strong>Fecha:</strong> ${consultation.consultationDate || 'N/A'}</p>
944
+ </div>
945
+ <div class="col-md-6">
946
+ <h6>Resultado</h6>
947
+ <p><strong>Diagnóstico:</strong>
948
+ <span class="badge ${consultation.diabeticRetinopathy ? 'badge-warning' : 'badge-success'}">
949
+ ${consultation.diabeticRetinopathy ? 'Positivo' : 'Negativo'}
950
+ </span>
951
+ </p>
952
+ <p><strong>Confianza:</strong> ${consultation.confidence || 'N/A'}%</p>
953
+ </div>
954
+ </div>
955
+ ${consultation.notes ? `
956
+ <div class="card mb-3">
957
+ <div class="card-header">
958
+ <h6 class="card-title mb-0">Notas</h6>
959
+ </div>
960
+ <div class="card-body">
961
+ <p>${consultation.notes}</p>
962
+ </div>
963
+ </div>` : ''}
964
+ ${consultation.imagePath ? `
965
+ <div class="card">
966
+ <div class="card-header">
967
+ <h6 class="card-title mb-0">Imagen</h6>
968
+ </div>
969
+ <div class="card-body text-center">
970
+ <img src="${consultation.imagePath}" class="img-fluid" style="max-height: 300px;" alt="Imagen de retina">
971
+ </div>
972
+ </div>` : ''}
973
+ </div>
974
+ <div class="modal-footer">
975
+ <button type="button" class="btn btn-secondary" data-dismiss="modal">Cerrar</button>
976
+ </div>
977
+ </div>
978
+ </div>
979
+ </div>
980
+ `;
981
+
982
+ $('body').append(modalHtml);
983
+ $('#consultationModal').modal('show');
984
+
985
+ $('#consultationModal').on('hidden.bs.modal', function() {
986
+ $(this).remove();
987
+ });
988
+ }
989
+
990
+
991
+ function showErrorInTable(message) {
992
+ $('#recentConsultationsBody').html(`
993
+ <tr>
994
+ <td colspan="5" class="text-center text-danger">
995
+ <i class="fe fe-alert-triangle fe-16"></i><br>
996
+ ${message || 'Error al cargar consultas'}
997
+ </td>
998
+ </tr>
999
+ `);
1000
+ }
1001
+
1002
+ $(document).ready(function() {
1003
+ loadRecentConsultations();
1004
+ });
1005
+
1006
+ function createOrUpdateChart(dates, totalConsultations, positiveCases, negativeCases) {
1007
+ if (areaChartInstance && typeof areaChartInstance.destroy === 'function') {
1008
+ try {
1009
+ areaChartInstance.destroy();
1010
+ areaChartInstance = null;
1011
+ } catch (error) {
1012
+ console.log('Error destruyendo grafico anterior:', error);
1013
+ }
1014
+ }
1015
+
1016
+ const chartElement = document.querySelector("#areaChart");
1017
+ if (!chartElement) {
1018
+ console.error('Elemento #areaChart no encontrado');
1019
+ return;
1020
+ }
1021
+
1022
+ chartElement.innerHTML = '';
1023
+
1024
+ setTimeout(() => {
1025
+ try {
1026
+ const options = {
1027
+ chart: {
1028
+ type: 'area',
1029
+ height: 350,
1030
+ width: '100%',
1031
+ toolbar: {
1032
+ show: true,
1033
+ tools: {
1034
+ download: false,
1035
+ selection: false,
1036
+ zoom: false,
1037
+ zoomin: false,
1038
+ zoomout: false,
1039
+ pan: false,
1040
+ reset: false
1041
+ }
1042
+ },
1043
+ animations: {
1044
+ enabled: true,
1045
+ easing: 'easeinout',
1046
+ speed: 800
1047
+ },
1048
+ background: 'transparent'
1049
+ },
1050
+ colors: ['#2E93fA', '#FF9800', '#66DA26'],
1051
+ series: [
1052
+ {
1053
+ name: 'Total Consultas',
1054
+ data: totalConsultations
1055
+ },
1056
+ {
1057
+ name: 'Con Retinopatía',
1058
+ data: positiveCases
1059
+ },
1060
+ {
1061
+ name: 'Sin Retinopatía',
1062
+ data: negativeCases
1063
+ }
1064
+ ],
1065
+ xaxis: {
1066
+ categories: dates,
1067
+ type: 'category',
1068
+ labels: {
1069
+ formatter: function(value) {
1070
+ if (value) {
1071
+ try {
1072
+ const date = new Date(value);
1073
+ return date.toLocaleDateString('es-ES', {
1074
+ day: '2-digit',
1075
+ month: '2-digit'
1076
+ });
1077
+ } catch (e) {
1078
+ return value;
1079
+ }
1080
+ }
1081
+ return value;
1082
+ },
1083
+ style: {
1084
+ fontSize: '12px',
1085
+ fontFamily: 'inherit'
1086
+ }
1087
+ },
1088
+ title: {
1089
+ text: 'Fecha',
1090
+ style: {
1091
+ fontSize: '14px',
1092
+ fontFamily: 'inherit'
1093
+ }
1094
+ }
1095
+ },
1096
+ yaxis: {
1097
+ title: {
1098
+ text: 'Número de Consultas',
1099
+ style: {
1100
+ fontSize: '14px',
1101
+ fontFamily: 'inherit'
1102
+ }
1103
+ },
1104
+ min: 0,
1105
+ labels: {
1106
+ style: {
1107
+ fontSize: '12px',
1108
+ fontFamily: 'inherit'
1109
+ }
1110
+ }
1111
+ },
1112
+ tooltip: {
1113
+ shared: true,
1114
+ intersect: false,
1115
+ x: {
1116
+ formatter: function(value) {
1117
+ if (value) {
1118
+ try {
1119
+ const date = new Date(value);
1120
+ return date.toLocaleDateString('es-ES', {
1121
+ weekday: 'long',
1122
+ year: 'numeric',
1123
+ month: 'long',
1124
+ day: 'numeric'
1125
+ });
1126
+ } catch (e) {
1127
+ return value;
1128
+ }
1129
+ }
1130
+ return value;
1131
+ }
1132
+ },
1133
+ y: {
1134
+ formatter: function(value) {
1135
+ return value + ' consulta' + (value !== 1 ? 's' : '');
1136
+ }
1137
+ }
1138
+ },
1139
+ stroke: {
1140
+ curve: 'smooth',
1141
+ width: 2
1142
+ },
1143
+ fill: {
1144
+ type: 'gradient',
1145
+ gradient: {
1146
+ shadeIntensity: 1,
1147
+ opacityFrom: 0.7,
1148
+ opacityTo: 0.3,
1149
+ stops: [0, 90, 100]
1150
+ }
1151
+ },
1152
+ legend: {
1153
+ position: 'top',
1154
+ horizontalAlign: 'left',
1155
+ labels: {
1156
+ colors: '#9aa0ac'
1157
+ }
1158
+ },
1159
+ grid: {
1160
+ borderColor: '#f1f1f1',
1161
+ strokeDashArray: 4,
1162
+ xaxis: {
1163
+ lines: {
1164
+ show: true
1165
+ }
1166
+ },
1167
+ yaxis: {
1168
+ lines: {
1169
+ show: true
1170
+ }
1171
+ }
1172
+ },
1173
+ dataLabels: {
1174
+ enabled: false
1175
+ },
1176
+ responsive: [{
1177
+ breakpoint: 768,
1178
+ options: {
1179
+ chart: {
1180
+ height: 300
1181
+ },
1182
+ legend: {
1183
+ position: 'bottom'
1184
+ }
1185
+ }
1186
+ }]
1187
+ };
1188
+
1189
+ areaChartInstance = new ApexCharts(chartElement, options);
1190
+
1191
+ areaChartInstance.render()
1192
+ .then(() => {
1193
+ console.log('Gráfico creado exitosamente');
1194
+ })
1195
+ .catch((error) => {
1196
+ console.error('Error renderizando gráfico:', error);
1197
+ createFallbackChart(chartElement, dates, totalConsultations, positiveCases, negativeCases);
1198
+ });
1199
+
1200
+ } catch (error) {
1201
+ console.error('Error creando gráfico:', error);
1202
+ createFallbackChart(chartElement, dates, totalConsultations, positiveCases, negativeCases);
1203
+ }
1204
+ }, 100);
1205
+ }
1206
+
1207
+ function createFallbackChart(chartElement, dates, totalConsultations, positiveCases, negativeCases) {
1208
+ console.log('Creando grafico de respaldo...');
1209
+
1210
+ const maxValue = Math.max(...totalConsultations, 1);
1211
+
1212
+ let tableHtml = `
1213
+ <div class="fallback-chart p-3">
1214
+ <h6 class="text-muted mb-3">Datos de Consultas (Vista de Tabla)</h6>
1215
+ <div class="table-responsive">
1216
+ <table class="table table-sm table-hover">
1217
+ <thead class="thead-light">
1218
+ <tr>
1219
+ <th>Fecha</th>
1220
+ <th>Total</th>
1221
+ <th>Con RD</th>
1222
+ <th>Sin RD</th>
1223
+ <th>Gráfico</th>
1224
+ </tr>
1225
+ </thead>
1226
+ <tbody>
1227
+ `;
1228
+
1229
+ dates.forEach((date, index) => {
1230
+ const total = totalConsultations[index] || 0;
1231
+ const positive = positiveCases[index] || 0;
1232
+ const negative = negativeCases[index] || 0;
1233
+ const percentage = maxValue > 0 ? (total / maxValue) * 100 : 0;
1234
+
1235
+ const formattedDate = new Date(date).toLocaleDateString('es-ES', {
1236
+ day: '2-digit',
1237
+ month: '2-digit'
1238
+ });
1239
+
1240
+ tableHtml += `
1241
+ <tr>
1242
+ <td>${formattedDate}</td>
1243
+ <td><span class="badge badge-primary">${total}</span></td>
1244
+ <td><span class="badge badge-warning">${positive}</span></td>
1245
+ <td><span class="badge badge-success">${negative}</span></td>
1246
+ <td>
1247
+ <div class="progress" style="height: 20px;">
1248
+ <div class="progress-bar" role="progressbar" style="width: ${percentage}%"
1249
+ aria-valuenow="${total}" aria-valuemin="0" aria-valuemax="${maxValue}">
1250
+ </div>
1251
+ </div>
1252
+ </td>
1253
+ </tr>
1254
+ `;
1255
+ });
1256
+
1257
+ tableHtml += `
1258
+ </tbody>
1259
+ </table>
1260
+ </div>
1261
+ <small class="text-muted">
1262
+ <i class="fe fe-info mr-1"></i>
1263
+ Vista alternativa - el gráfico principal no se pudo cargar
1264
+ </small>
1265
+ </div>
1266
+ `;
1267
+
1268
+ chartElement.innerHTML = tableHtml;
1269
+ }
1270
+
1271
+ function createEmptyChart() {
1272
+ if (areaChartInstance && typeof areaChartInstance.destroy === 'function') {
1273
+ try {
1274
+ areaChartInstance.destroy();
1275
+ areaChartInstance = null;
1276
+ } catch (error) {
1277
+ console.log('Error destruyendo grafico:', error);
1278
+ }
1279
+ }
1280
+
1281
+ const chartElement = document.querySelector("#areaChart");
1282
+ if (chartElement) {
1283
+ chartElement.innerHTML = `
1284
+ <div class="d-flex justify-content-center align-items-center" style="height: 350px;">
1285
+ <div class="text-center text-muted">
1286
+ <i class="fe fe-bar-chart-2 fe-48 mb-3"></i>
1287
+ <h6>No hay datos disponibles</h6>
1288
+ <p class="small">Las consultas aparecerán aquí cuando se registren</p>
1289
+ <button class="btn btn-sm btn-outline-primary" onclick="insertSampleData()">
1290
+ <i class="fe fe-plus mr-1"></i>Generar datos de prueba
1291
+ </button>
1292
+ </div>
1293
+ </div>
1294
+ `;
1295
+ }
1296
+ }
1297
+
1298
+ function loadRecentAnalysis() { return; /* legacy - no endpoint */ }
1299
+
1300
+ function updateRecentAnalysisTable() {
1301
+ const tbody = $('#recentAnalysisTable');
1302
+ tbody.empty();
1303
+
1304
+ if (recentAnalysis.length === 0) {
1305
+ tbody.append(`
1306
+ <tr>
1307
+ <td colspan="5" class="text-center text-muted">
1308
+ <i class="fe fe-inbox fe-24 mb-2"></i><br>
1309
+ No hay análisis recientes
1310
+ </td>
1311
+ </tr>
1312
+ `);
1313
+ return;
1314
+ }
1315
+
1316
+ recentAnalysis.reverse().forEach(function(analysis) {
1317
+ const resultClass = analysis.prediction.class_index === 1 ? 'badge-warning' : 'badge-success';
1318
+ const resultText = analysis.prediction.class_index === 1 ? 'Positivo' : 'Negativo';
1319
+
1320
+ tbody.append(`
1321
+ <tr>
1322
+ <td>${moment(analysis.timestamp).format('DD/MM/YYYY HH:mm')}</td>
1323
+ <td>${analysis.filename}</td>
1324
+ <td><span class="badge ${resultClass}">${resultText}</span></td>
1325
+ <td>${analysis.prediction.confidence}%</td>
1326
+ <td>
1327
+ <button class="btn btn-sm btn-outline-primary" onclick="viewAnalysis('${analysis.filename}')">
1328
+ <i class="fe fe-eye fe-12"></i>
1329
+ </button>
1330
+ </td>
1331
+ </tr>
1332
+ `);
1333
+ });
1334
+ }
1335
+
1336
+ function setupEventListeners() {
1337
+ $('#logoutBtn').on('click', function(e) {
1338
+ e.preventDefault();
1339
+ handleLogout();
1340
+ });
1341
+
1342
+ $('#refreshAgeDataBtn').on('click', function() {
1343
+ refreshAgeData();
1344
+ });
1345
+
1346
+ $(document).on('click', '[onclick^="updateChartData"]', function(e) {
1347
+ e.preventDefault();
1348
+ const days = parseInt(this.getAttribute('onclick').match(/\d+/)[0]);
1349
+ updateChartData(days);
1350
+ });
1351
+
1352
+ $('#imageInput').on('change', handleImageSelect);
1353
+
1354
+ $('#analyzeBtn').on('click', handleImageAnalysis);
1355
+
1356
+ $('#clearBtn').on('click', clearImagePreview);
1357
+
1358
+ $('#systemTestBtn').on('click', runSystemTest);
1359
+
1360
+ setupDragAndDrop();
1361
+ }
1362
+
1363
+ function setupDragAndDrop() {
1364
+ }
1365
+
1366
+ function handleImageSelect(e) {
1367
+ const file = e.target.files[0];
1368
+ if (file) {
1369
+ handleImageFile(file);
1370
+ }
1371
+ }
1372
+
1373
+ function handleImageFile(file) {
1374
+ if (!file.type.startsWith('image/')) {
1375
+ showAlert('error', 'Por favor selecciona un archivo de imagen válido');
1376
+ return;
1377
+ }
1378
+
1379
+ if (file.size > 10 * 1024 * 1024) {
1380
+ showAlert('error', 'La imagen es demasiado grande. Máximo 10MB');
1381
+ return;
1382
+ }
1383
+
1384
+ const reader = new FileReader();
1385
+ reader.onload = function(e) {
1386
+ $('#previewImg').attr('src', e.target.result);
1387
+ $('#imagePreview').show();
1388
+ $('#uploadZone').hide();
1389
+ };
1390
+ reader.readAsDataURL(file);
1391
+
1392
+ console.log('Imagen cargada:', file.name);
1393
+ }
1394
+
1395
+ function handleImageAnalysis() {
1396
+ const imgSrc = $('#previewImg').attr('src');
1397
+ if (!imgSrc) return;
1398
+
1399
+ $('#analyzeBtn').prop('disabled', true).html('<i class="spinner-border spinner-border-sm mr-2"></i>Analizando...');
1400
+
1401
+ const fileName = $('#imageInput')[0].files[0]?.name || 'imagen_' + Date.now() + '.jpg';
1402
+
1403
+ api.predict(imgSrc, fileName).then(function(result) {
1404
+ $('#analyzeBtn').prop('disabled', false).html('<i class="fe fe-eye mr-2"></i>Analizar con IA');
1405
+
1406
+ if (result && result.success) {
1407
+ showAnalysisResults(result);
1408
+ // loadRecentAnalysis() - handled by loadRecentConsultations
1409
+ } else {
1410
+ showAlert('error', result?.error || 'Error en el análisis');
1411
+ }
1412
+ });
1413
+ }
1414
+
1415
+ function showAnalysisResults(result) {
1416
+ const prediction = result.prediction;
1417
+ const isPositive = prediction.class_index === 1;
1418
+ const alertClass = isPositive ? 'alert-warning' : 'alert-success';
1419
+ const iconClass = isPositive ? 'fe-alert-triangle' : 'fe-check-circle';
1420
+ const resultText = isPositive ? 'Retinopatía Diabética Detectada' : 'No se Detectó Retinopatía Diabética';
1421
+
1422
+ const resultsHtml = `
1423
+ <div class="alert ${alertClass} d-flex align-items-center">
1424
+ <i class="fe ${iconClass} fe-24 mr-3"></i>
1425
+ <div>
1426
+ <h6 class="mb-1">${resultText}</h6>
1427
+ <p class="mb-0">Confianza: <strong>${prediction.confidence}%</strong></p>
1428
+ <small class="text-muted">Análisis realizado: ${result.timestamp}</small>
1429
+ </div>
1430
+ </div>
1431
+ ${isPositive ? `
1432
+ ` : ''}
1433
+ `;
1434
+
1435
+ $('#resultContent').html(resultsHtml);
1436
+ $('#analysisResults').show();
1437
+
1438
+ console.log('Analisis completado:', prediction.class);
1439
+ }
1440
+
1441
+ function clearImagePreview() {
1442
+ $('#imagePreview').hide();
1443
+ $('#analysisResults').hide();
1444
+ $('#uploadZone').show();
1445
+ $('#imageInput').val('');
1446
+ }
1447
+
1448
+ function runSystemTest() {
1449
+ $('#systemTestBtn').prop('disabled', true).html('<i class="spinner-border spinner-border-sm mr-2"></i>Probando...');
1450
+
1451
+ api.getModelInfo().then(function(result) {
1452
+ $('#systemTestBtn').prop('disabled', false).html('<i class="fe fe-refresh-cw fe-16 mr-2"></i>Probar Sistema');
1453
+ if (result && result.loaded) {
1454
+ showAlert('info', '• Modelo IA: SÍ\n• Base de datos: SÍ');
1455
+ } else {
1456
+ showAlert('error', 'Modelo no cargado');
1457
+ }
1458
+ });
1459
+ }
1460
+
1461
+ function handleLogout() {
1462
+ if (confirm('¿Estás seguro que deseas cerrar sesión?')) {
1463
+ api.logout().then(() => { window.location.href = 'auth-login.html'; });
1464
+ }
1465
+ }
1466
+
1467
+ function redirectToLogin() {
1468
+ window.location.href = 'auth-login.html';
1469
+ }
1470
+
1471
+ function updateDateTime() {
1472
+ const now = moment();
1473
+ $('#currentDateTime').text(now.format('dddd, DD [de] MMMM [de] YYYY - HH:mm'));
1474
+ }
1475
+
1476
+ function showAlert(type, message) {
1477
+ const alertClass = {
1478
+ 'success': 'alert-success',
1479
+ 'error': 'alert-danger',
1480
+ 'warning': 'alert-warning',
1481
+ 'info': 'alert-info'
1482
+ }[type] || 'alert-info';
1483
+
1484
+ const alertHtml = `
1485
+ <div class="alert ${alertClass} alert-dismissible fade show" role="alert">
1486
+ ${message.replace(/\n/g, '<br>')}
1487
+ <button type="button" class="close" data-dismiss="alert">
1488
+ <span aria-hidden="true">&times;</span>
1489
+ </button>
1490
+ </div>
1491
+ `;
1492
+
1493
+ $('.container-fluid').prepend(alertHtml);
1494
+
1495
+ setTimeout(() => {
1496
+ $('.alert').first().alert('close');
1497
+ }, 5000);
1498
+ }
1499
+
1500
+ function showErrorMessage(message) {
1501
+ showAlert('error', message);
1502
+ }
1503
+
1504
+ function showSystemError() {
1505
+ $('#systemStatusIcon').removeClass('bg-success').addClass('bg-danger');
1506
+ $('#systemStatusIcon i').removeClass('fe-check').addClass('fe-x');
1507
+ $('#systemStatusText').text('Error de conexión');
1508
+
1509
+ showAlert('error', 'Error al conectar con el sistema. Verifica que el backend esté funcionando.');
1510
+ }
1511
+
1512
+ function loadAgeRangesData() {
1513
+ console.log('Cargando datos de rangos de edad...');
1514
+
1515
+ api.getDashboardStats().then(function(response) {
1516
+ console.log('Datos de edad recibidos:', response);
1517
+
1518
+ if (response && response.success) {
1519
+ currentAgeData = response;
1520
+ updateAgeRangesChart(response);
1521
+ updateAgeStatistics(response);
1522
+ } else {
1523
+ console.error('Error cargando datos de edad:', response?.error);
1524
+ showEmptyAgeChart();
1525
+ }
1526
+ });
1527
+ }
1528
+
1529
+ function updateAgeRangesChart(data) {
1530
+ if (ageRangesChartInstance) {
1531
+ try {
1532
+ ageRangesChartInstance.destroy();
1533
+ ageRangesChartInstance = null;
1534
+ } catch (error) {
1535
+ console.log('Error destruyendo grafico anterior:', error);
1536
+ }
1537
+ }
1538
+
1539
+ const chartElement = document.querySelector("#ageRangesChart");
1540
+ if (!chartElement) {
1541
+ console.error('Elemento #ageRangesChart no encontrado');
1542
+ return;
1543
+ }
1544
+
1545
+ chartElement.innerHTML = '';
1546
+
1547
+ const chartData = data.chart_data;
1548
+
1549
+ if (!chartData || !chartData.labels || chartData.labels.length === 0) {
1550
+ showEmptyAgeChart();
1551
+ return;
1552
+ }
1553
+
1554
+ const colors = generateAgeRangeColors(chartData.labels.length);
1555
+
1556
+ const options = {
1557
+ chart: {
1558
+ type: 'donut',
1559
+ height: 300,
1560
+ toolbar: {
1561
+ show: false
1562
+ },
1563
+ animations: {
1564
+ enabled: true,
1565
+ easing: 'easeinout',
1566
+ speed: 800
1567
+ }
1568
+ },
1569
+ colors: colors,
1570
+ series: chartData.data,
1571
+ labels: chartData.labels.map(label => label + ' años'),
1572
+ legend: {
1573
+ position: 'bottom',
1574
+ fontSize: '12px',
1575
+ labels: {
1576
+ colors: '#9aa0ac'
1577
+ },
1578
+ markers: {
1579
+ width: 8,
1580
+ height: 8
1581
+ }
1582
+ },
1583
+ dataLabels: {
1584
+ enabled: true,
1585
+ formatter: function(val, opts) {
1586
+ const count = chartData.data[opts.seriesIndex];
1587
+ return count + '\n(' + Math.round(val) + '%)';
1588
+ },
1589
+ style: {
1590
+ fontSize: '10px',
1591
+ fontWeight: 'bold'
1592
+ }
1593
+ },
1594
+ plotOptions: {
1595
+ pie: {
1596
+ donut: {
1597
+ size: '65%',
1598
+ labels: {
1599
+ show: true,
1600
+ name: {
1601
+ show: true,
1602
+ fontSize: '14px',
1603
+ fontWeight: 'bold',
1604
+ color: '#373d3f'
1605
+ },
1606
+ value: {
1607
+ show: true,
1608
+ fontSize: '16px',
1609
+ fontWeight: 'bold',
1610
+ color: '#373d3f',
1611
+ formatter: function(val) {
1612
+ return val + ' pacientes';
1613
+ }
1614
+ },
1615
+ total: {
1616
+ show: true,
1617
+ label: 'Total Pacientes',
1618
+ fontSize: '12px',
1619
+ color: '#9aa0ac',
1620
+ formatter: function(w) {
1621
+ return chartData.total;
1622
+ }
1623
+ }
1624
+ }
1625
+ }
1626
+ }
1627
+ },
1628
+ tooltip: {
1629
+ enabled: true,
1630
+ y: {
1631
+ formatter: function(value, { seriesIndex }) {
1632
+ const percentage = ((value / chartData.total) * 100).toFixed(1);
1633
+ return value + ' pacientes (' + percentage + '%)';
1634
+ }
1635
+ }
1636
+ },
1637
+ responsive: [{
1638
+ breakpoint: 480,
1639
+ options: {
1640
+ chart: {
1641
+ height: 250
1642
+ },
1643
+ legend: {
1644
+ position: 'bottom'
1645
+ }
1646
+ }
1647
+ }]
1648
+ };
1649
+
1650
+ try {
1651
+ ageRangesChartInstance = new ApexCharts(chartElement, options);
1652
+ ageRangesChartInstance.render().then(() => {
1653
+ console.log('Grafico de rangos de edad creado exitosamente');
1654
+ }).catch((error) => {
1655
+ console.error('Error renderizando grafico de edad:', error);
1656
+ showEmptyAgeChart();
1657
+ });
1658
+ } catch (error) {
1659
+ console.error('Error creando grafico de edad:', error);
1660
+ showEmptyAgeChart();
1661
+ }
1662
+ }
1663
+
1664
+ function populateAgeDetailsModal(data) {
1665
+ if (!data.success) return;
1666
+
1667
+ api.getDashboardStats().then(function(response) {
1668
+ if (response && response.success) {
1669
+ const stats = response.detailed_analysis.statistics;
1670
+ const summary = data.summary;
1671
+
1672
+ $('#modalAvgAge').text(stats.avg_age + ' años');
1673
+ $('#modalMedianAge').text(stats.median_age + ' años');
1674
+
1675
+ if (summary.most_affected_range) {
1676
+ $('#mostAffectedRange').text(summary.most_affected_range[0] + ' años');
1677
+ $('#mostAffectedCount').text(summary.most_affected_range[1] + ' pacientes');
1678
+ }
1679
+
1680
+ const tbody = $('#ageDistributionTable tbody');
1681
+ tbody.empty();
1682
+
1683
+ const total = data.chart_data.total;
1684
+ Object.entries(data.filtered_ranges).forEach(([range, count]) => {
1685
+ const percentage = ((count / total) * 100).toFixed(1);
1686
+ const barWidth = Math.max((count / total) * 100, 5);
1687
+
1688
+ tbody.append(`
1689
+ <tr>
1690
+ <td>${range} años</td>
1691
+ <td><strong>${count}</strong></td>
1692
+ <td>${percentage}%</td>
1693
+ <td>
1694
+ <div class="progress" style="height: 20px;">
1695
+ <div class="progress-bar bg-primary" style="width: ${barWidth}%"></div>
1696
+ </div>
1697
+ </td>
1698
+ </tr>
1699
+ `);
1700
+ });
1701
+ }
1702
+ });
1703
+ }
1704
+
1705
+ function updateAgeStatistics(data) {
1706
+ if (!data || !data.success) return;
1707
+
1708
+ api.getDashboardStats().then(function(response) {
1709
+ if (response && response.success && response.detailed_analysis) {
1710
+ const stats = response.detailed_analysis.statistics;
1711
+
1712
+ $('#avgAge').text(stats.avg_age || '-');
1713
+ $('#ageRange').text(stats.min_age && stats.max_age ?
1714
+ stats.min_age + '-' + stats.max_age : '-');
1715
+ $('#totalPatientsAge').text(stats.total_patients || '-');
1716
+ }
1717
+ });
1718
+ }
1719
+
1720
+ function generateAgeRangeColors(count) {
1721
+ const baseColors = [
1722
+ '#E3F2FD', //(0-10)
1723
+ '#BBDEFB', //(11-20)
1724
+ '#90CAF9', //(21-30)
1725
+ '#64B5F6', //(31-40)
1726
+ '#42A5F5', //(41-50)
1727
+ '#2196F3', //(51-60)
1728
+ '#1E88E5', //(61-70)
1729
+ '#1976D2', //(71-80)
1730
+ '#1565C0', //(81-90)
1731
+ '#0D47A1' //(91+)
1732
+ ];
1733
+
1734
+ return baseColors.slice(0, count);
1735
+ }
1736
+
1737
+ function showEmptyAgeChart() {
1738
+ const chartElement = document.querySelector("#ageRangesChart");
1739
+ if (chartElement) {
1740
+ chartElement.innerHTML = `
1741
+ <div class="d-flex justify-content-center align-items-center" style="height: 300px;">
1742
+ <div class="text-center text-muted">
1743
+ <i class="fe fe-users fe-48 mb-3"></i>
1744
+ <h6>No hay datos de edad</h6>
1745
+ <p class="small">Los rangos de edad aparecerán cuando haya<br>pacientes con retinopatía diabética</p>
1746
+ </div>
1747
+ </div>
1748
+ `;
1749
+ }
1750
+
1751
+ $('#avgAge').text('-');
1752
+ $('#ageRange').text('-');
1753
+ $('#totalPatientsAge').text('-');
1754
+ }
1755
+
1756
+ window.viewAnalysis = function(filename) {
1757
+ const analysis = recentAnalysis.find(a => a.filename === filename);
1758
+ if (analysis) {
1759
+ showAnalysisDetails(analysis);
1760
+ }
1761
+ };
1762
+
1763
+ function showAnalysisDetails(analysis) {
1764
+ const modal = `
1765
+ <div class="modal fade" id="analysisModal" tabindex="-1">
1766
+ <div class="modal-dialog modal-lg">
1767
+ <div class="modal-content">
1768
+ <div class="modal-header">
1769
+ <h5 class="modal-title">Detalles del Análisis</h5>
1770
+ <button type="button" class="close" data-dismiss="modal">
1771
+ <span>&times;</span>
1772
+ </button>
1773
+ </div>
1774
+ <div class="modal-body">
1775
+ <div class="row">
1776
+ <div class="col-md-6">
1777
+ <h6>Información General</h6>
1778
+ <p><strong>Archivo:</strong> ${analysis.filename}</p>
1779
+ <p><strong>Fecha:</strong> ${moment(analysis.timestamp).format('DD/MM/YYYY HH:mm')}</p>
1780
+ <p><strong>Resultado:</strong> ${analysis.prediction.class}</p>
1781
+ <p><strong>Confianza:</strong> ${analysis.prediction.confidence}%</p>
1782
+ </div>
1783
+ <div class="col-md-6">
1784
+ <h6>Datos Técnicos</h6>
1785
+ <p><strong>Raw Output:</strong> ${analysis.prediction.raw_output}</p>
1786
+ <p><strong>Clase Predicha:</strong> ${analysis.prediction.class_index}</p>
1787
+ </div>
1788
+ </div>
1789
+ </div>
1790
+ <div class="modal-footer">
1791
+ <button type="button" class="btn btn-secondary" data-dismiss="modal">Cerrar</button>
1792
+ </div>
1793
+ </div>
1794
+ </div>
1795
+ </div>
1796
+ `;
1797
+
1798
+ $('body').append(modal);
1799
+ $('#analysisModal').modal('show');
1800
+ $('#analysisModal').on('hidden.bs.modal', function() {
1801
+ $(this).remove();
1802
+ });
1803
+ }
1804
+
1805
+ window.updateChartData = updateChartData;
1806
+ });
1807
+ </script>
1808
+ <script>
1809
+ function confirmLogout() {
1810
+ executeLogout();
1811
+ }
1812
+
1813
+ function executeLogout() {
1814
+ api.logout().then(() => { window.location.href = 'auth-login.html'; });
1815
+ }
1816
+ </script>
1817
+
1818
+ <script>
1819
+ (function() {
1820
+ function centerWindow() {
1821
+ try {
1822
+ const screenWidth = window.screen.availWidth;
1823
+ const screenHeight = window.screen.availHeight;
1824
+ const x = Math.floor((screenWidth - 1200) / 2);
1825
+ const y = Math.floor((screenHeight - 800) / 2);
1826
+
1827
+ if (window.moveTo) {
1828
+ window.moveTo(Math.max(0, x), Math.max(0, y));
1829
+ }
1830
+ } catch (e) {
1831
+ console.warn('No se pudo centrar ventana:', e);
1832
+ }
1833
+ }
1834
+
1835
+ if (document.readyState === 'loading') {
1836
+ document.addEventListener('DOMContentLoaded', centerWindow);
1837
+ } else {
1838
+ centerWindow();
1839
+ }
1840
+
1841
+ window.addEventListener('load', centerWindow);
1842
+ window.addEventListener('focus', centerWindow);
1843
+ })();
1844
+ </script>
1845
+
1846
+ <script>
1847
+ function checkUserRoleAndHideNavigation() {
1848
+ api.getSession().then(function(response) {
1849
+ if (response.success && response.user) {
1850
+ const userRole = response.user.role;
1851
+ const usersNavItem = document.querySelector('a[href="./users.html"]');
1852
+
1853
+ if (usersNavItem) {
1854
+ const navItemContainer = usersNavItem.closest('li.nav-item');
1855
+
1856
+ if (userRole === 'Admin') {
1857
+ if (navItemContainer) {
1858
+ navItemContainer.style.display = 'block';
1859
+ }
1860
+ console.log('Navegacion de usuarios visible para administrador:', response.user.username);
1861
+ } else {
1862
+ if (navItemContainer) {
1863
+ navItemContainer.style.display = 'none';
1864
+ }
1865
+ console.log('Navegacion de usuarios oculta para usuario:', response.user.username, '(Rol:', userRole + ')');
1866
+ }
1867
+ } else {
1868
+ console.warn('No se encontró el elemento de navegación de usuarios');
1869
+ }
1870
+ } else {
1871
+ console.warn('No se pudo obtener información del usuario actual');
1872
+ }
1873
+ });
1874
+ }
1875
+
1876
+ function checkUsersPageAccess() {
1877
+ if (window.location.pathname.includes('users.html') || window.location.href.includes('users.html')) {
1878
+ api.getSession().then(function(response) {
1879
+ if (!response.success) {
1880
+ window.location.href = 'auth-login.html';
1881
+ return;
1882
+ }
1883
+
1884
+ if (response.user.role !== 'Admin') {
1885
+ alert('Acceso Denegado\n\nSolo los administradores pueden gestionar usuarios.');
1886
+ window.location.href = 'index.html';
1887
+ return;
1888
+ }
1889
+ });
1890
+ }
1891
+ }
1892
+
1893
+ $(document).ready(function() {
1894
+ setTimeout(() => {
1895
+ checkUserRoleAndHideNavigation();
1896
+ checkUsersPageAccess();
1897
+ }, 100);
1898
+ });
1899
+
1900
+ function updateNavigationForUser() {
1901
+ checkUserRoleAndHideNavigation();
1902
+ }
1903
+
1904
+ function updateUserInfo() {
1905
+ api.getSession().then(function(response) {
1906
+ if (response.success && response.user) {
1907
+ const userDropdown = document.querySelector('#navbarDropdownMenuLink');
1908
+ if (userDropdown) {
1909
+ const userRole = response.user.role;
1910
+ const roleBadge = userRole === 'Admin' ?
1911
+ '<span class="badge badge-primary badge-sm ml-1">Admin</span>' :
1912
+ '<span class="badge badge-secondary badge-sm ml-1">Doctor</span>';
1913
+
1914
+ const existingBadge = userDropdown.querySelector('.badge');
1915
+ if (!existingBadge) {
1916
+ userDropdown.innerHTML += roleBadge;
1917
+ }
1918
+ }
1919
+ }
1920
+ });
1921
+ }
1922
+
1923
+ $(document).ready(function() {
1924
+ setTimeout(() => {
1925
+ updateUserInfo();
1926
+ }, 200);
1927
+ });
1928
+ </script>
1929
+ </body>
1930
+ </html>