ViannyCruz commited on
Commit
40256dd
·
verified ·
1 Parent(s): 3ce36cf

Upload 5 files

Browse files
web/consultas.html ADDED
@@ -0,0 +1,184 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="es">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Historial de Consultas</title>
7
+ <link rel="stylesheet" href="css/styles.css">
8
+ <script src="api.js"></script>
9
+ <script src="js/consultas.js"></script>
10
+ <style>
11
+ .consultations-list {
12
+ margin-top: 20px;
13
+ overflow-x: auto;
14
+ }
15
+
16
+ table {
17
+ width: 100%;
18
+ border-collapse: collapse;
19
+ margin-top: 10px;
20
+ }
21
+
22
+ th, td {
23
+ padding: 12px 15px;
24
+ text-align: left;
25
+ border-bottom: 1px solid #ddd;
26
+ }
27
+
28
+ th {
29
+ background-color: #f8f9fa;
30
+ font-weight: bold;
31
+ }
32
+
33
+ tr:hover {
34
+ background-color: #f5f5f5;
35
+ }
36
+
37
+ .diagnosis-positive {
38
+ color: #dc3545;
39
+ font-weight: bold;
40
+ }
41
+
42
+ .diagnosis-negative {
43
+ color: #28a745;
44
+ font-weight: bold;
45
+ }
46
+
47
+ .filters {
48
+ margin-bottom: 20px;
49
+ display: flex;
50
+ gap: 10px;
51
+ }
52
+
53
+ .filters input {
54
+ padding: 8px;
55
+ border: 1px solid #ddd;
56
+ border-radius: 4px;
57
+ flex-grow: 1;
58
+ }
59
+
60
+ .filters button {
61
+ padding: 8px 15px;
62
+ background-color: #007bff;
63
+ color: white;
64
+ border: none;
65
+ border-radius: 4px;
66
+ cursor: pointer;
67
+ }
68
+
69
+ .filters button:hover {
70
+ background-color: #0056b3;
71
+ }
72
+ </style>
73
+ </head>
74
+ <body>
75
+ <div class="container">
76
+ <header>
77
+ <h1>Historial de Consultas</h1>
78
+ <nav>
79
+ <a href="index.html">Inicio</a>
80
+ <a href="pacientes.html">Pacientes</a>
81
+ <a href="consultas.html" class="active">Consultas</a>
82
+ </nav>
83
+ </header>
84
+
85
+ <div class="content">
86
+ <div class="filters">
87
+ <input type="text" id="searchInput" placeholder="Buscar por nombre de paciente...">
88
+ <button onclick="loadConsultations()">Buscar</button>
89
+ </div>
90
+
91
+ <div class="consultations-list">
92
+ <table id="consultationsTable">
93
+ <thead>
94
+ <tr>
95
+ <th>ID</th>
96
+ <th>Paciente</th>
97
+ <th>Fecha</th>
98
+ <th>Diagnóstico</th>
99
+ <th>Confianza</th>
100
+ <th>Notas</th>
101
+ </tr>
102
+ </thead>
103
+ <tbody id="consultationsBody">
104
+ <!-- Las consultas se cargargan aqui -->
105
+ </tbody>
106
+ </table>
107
+ </div>
108
+ </div>
109
+ </div>
110
+
111
+ <script>
112
+ // Funcion para cargar todas las consultas
113
+ async function loadConsultations() {
114
+ try {
115
+ const searchTerm = document.getElementById('searchInput').value;
116
+ let result;
117
+
118
+ if (searchTerm) {
119
+ const searchResp = await api.getPatients(searchTerm);
120
+ if (searchResp.success && searchResp.patients.length > 0) {
121
+ const allResp = await api.getConsultations(1, 200, searchTerm);
122
+ displayConsultations(allResp.consultations || []);
123
+ return;
124
+ }
125
+ }
126
+
127
+ // Si no hay termino de busqueda o no se encontraron pacientes, cargar todas las consultas
128
+ result = await api.getConsultations(1, 100);
129
+ result.consultations = result.consultations || [];
130
+ if (result.success) {
131
+ displayConsultations(result.consultations || []);
132
+ } else {
133
+ alert('Error al cargar consultas: ' + (result.message || result.error));
134
+ }
135
+ } catch (error) {
136
+ console.error('Error:', error);
137
+ alert('Ocurrio un error al cargar las consultas');
138
+ }
139
+ }
140
+
141
+ // Funcion para mostrar las consultas en la tabla
142
+ function displayConsultations(consultations) {
143
+ const tbody = document.getElementById('consultationsBody');
144
+ tbody.innerHTML = '';
145
+
146
+ if (consultations.length === 0) {
147
+ tbody.innerHTML = '<tr><td colspan="6">No se encontraron consultas</td></tr>';
148
+ return;
149
+ }
150
+
151
+ consultations.sort((a, b) => new Date(b.consultationDate) - new Date(a.consultationDate));
152
+
153
+ consultations.forEach(consultation => {
154
+ const row = document.createElement('tr');
155
+
156
+ row.innerHTML = `
157
+ <td>${consultation.consultationID}</td>
158
+ <td>${consultation.patientName || consultation.patient_name || 'N/A'}</td>
159
+ <td>${formatDate(consultation.consultationDate)}</td>
160
+ <td class="${consultation.diabeticRetinopathy ? 'diagnosis-positive' : 'diagnosis-negative'}">
161
+ ${consultation.diabeticRetinopathy ? 'Positivo' : 'Negativo'}
162
+ </td>
163
+ <td>${consultation.confidence ? consultation.confidence + '%' : 'N/A'}</td>
164
+ <td>${consultation.notes || ''}</td>
165
+ `;
166
+
167
+ tbody.appendChild(row);
168
+ });
169
+ }
170
+
171
+ // Funcion para formatear la fecha
172
+ function formatDate(dateString) {
173
+ if (!dateString) return 'N/A';
174
+ const date = new Date(dateString);
175
+ return date.toLocaleDateString() + ' ' + date.toLocaleTimeString();
176
+ }
177
+
178
+ // Cargar consultas al abrir la página
179
+ document.addEventListener('DOMContentLoaded', () => {
180
+ loadConsultations();
181
+ });
182
+ </script>
183
+ </body>
184
+ </html>
web/diagnosis.html ADDED
@@ -0,0 +1,1417 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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>Diagnóstico IA</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
+
23
+ <style>
24
+ .upload-zone {
25
+ border: 3px dashed #dee2e6;
26
+ border-radius: 12px;
27
+ padding: 3rem 2rem;
28
+ text-align: center;
29
+ background: #f8f9fa;
30
+ transition: all 0.3s ease;
31
+ cursor: pointer;
32
+ min-height: 300px;
33
+ display: flex;
34
+ flex-direction: column;
35
+ justify-content: center;
36
+ align-items: center;
37
+ position: relative;
38
+ user-select: none;
39
+ }
40
+
41
+ .upload-zone:hover {
42
+ background: #f3f0ff;
43
+ border-color: #6c757d;
44
+ transform: translateY(-2px);
45
+ }
46
+
47
+ .upload-zone.dragover {
48
+ border-color: #28a745;
49
+ background: #f0fff4;
50
+ transform: scale(1.02);
51
+ }
52
+
53
+ .upload-zone * {
54
+ pointer-events: none;
55
+ }
56
+
57
+ .image-preview {
58
+ max-width: 100%;
59
+ max-height: 400px;
60
+ border-radius: 8px;
61
+ box-shadow: 0 4px 12px rgba(0,0,0,0.1);
62
+ }
63
+
64
+ .analysis-card {
65
+ color: white;
66
+ border-radius: 12px;
67
+ overflow: hidden;
68
+ }
69
+
70
+ .result-positive {
71
+ background: linear-gradient(135deg, #ff9a9e 0%, #fecfef 100%);
72
+ border-left: 4px solid #dc3545;
73
+ }
74
+
75
+ .result-negative {
76
+ background: linear-gradient(135deg, #a8edea 0%, #fed6e3 100%);
77
+ border-left: 4px solid #28a745;
78
+ }
79
+
80
+ .confidence-meter {
81
+ height: 8px;
82
+ background: #e9ecef;
83
+ border-radius: 4px;
84
+ overflow: hidden;
85
+ }
86
+
87
+ .confidence-fill {
88
+ height: 100%;
89
+ background: linear-gradient(90deg, #28a745, #ffc107, #dc3545);
90
+ transition: width 0.8s ease;
91
+ }
92
+
93
+ .patient-search {
94
+ position: relative;
95
+ }
96
+
97
+ .patient-results {
98
+ position: absolute;
99
+ top: 100%;
100
+ left: 0;
101
+ right: 0;
102
+ background: white;
103
+ border: 1px solid #dee2e6;
104
+ border-radius: 4px;
105
+ max-height: 200px;
106
+ overflow-y: auto;
107
+ z-index: 1000;
108
+ display: none;
109
+ }
110
+
111
+ .patient-item {
112
+ padding: 8px 12px;
113
+ cursor: pointer;
114
+ border-bottom: 1px solid #f8f9fa;
115
+ }
116
+
117
+ .patient-item:hover {
118
+ background: #f8f9fa;
119
+ }
120
+
121
+ .model-status {
122
+ position: fixed;
123
+ top: 20px;
124
+ right: 20px;
125
+ z-index: 1050;
126
+ }
127
+
128
+ .pulse-animation {
129
+ animation: pulse 2s infinite;
130
+ }
131
+
132
+ @keyframes pulse {
133
+ 0% { transform: scale(1); }
134
+ 50% { transform: scale(1.05); }
135
+ 100% { transform: scale(1); }
136
+ }
137
+
138
+ .fade-in {
139
+ animation: fadeIn 0.5s ease;
140
+ }
141
+
142
+ @keyframes fadeIn {
143
+ from { opacity: 0; transform: translateY(20px); }
144
+ to { opacity: 1; transform: translateY(0); }
145
+ }
146
+
147
+ .gradcam-btn {
148
+ background-color: #dc3545;
149
+ border: 1px solid #dc3545;
150
+ color: white;
151
+ }
152
+
153
+ .gradcam-btn:hover {
154
+ background-color: #c82333;
155
+ border-color: #bd2130;
156
+ color: white;
157
+ }
158
+
159
+ .gradcam-modal .modal-dialog {
160
+ max-width: 95vw;
161
+ }
162
+
163
+ .gradcam-modal .modal-content {
164
+ border-radius: 12px;
165
+ overflow: hidden;
166
+ }
167
+
168
+ .dual-analysis-container {
169
+ display: flex;
170
+ flex-wrap: wrap;
171
+ gap: 20px;
172
+ }
173
+
174
+ .analysis-column {
175
+ flex: 1;
176
+ min-width: 300px;
177
+ }
178
+
179
+ @media (max-width: 992px) {
180
+ .dual-analysis-container {
181
+ flex-direction: column;
182
+ }
183
+ }
184
+
185
+ .hidden-file-input {
186
+ position: absolute;
187
+ left: -9999px;
188
+ opacity: 0;
189
+ pointer-events: none;
190
+ }
191
+ </style>
192
+ </head>
193
+ <body class="vertical light">
194
+ <div class="wrapper">
195
+ <!-- Navegación superior -->
196
+ <nav class="topnav navbar navbar-light">
197
+ <button type="button" class="navbar-toggler text-muted mt-2 p-0 mr-3 collapseSidebar">
198
+ <i class="fe fe-menu navbar-toggler-icon"></i>
199
+ </button>
200
+
201
+ <ul class="nav">
202
+ <li class="nav-item dropdown">
203
+ <a class="nav-link dropdown-toggle text-muted pr-0" href="#" id="navbarDropdownMenuLink" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
204
+ <span class="avatar avatar-sm mt-2">
205
+ <img src="./assets/images/persona.png" alt="..." class="avatar-img rounded-circle">
206
+ </span>
207
+ </a>
208
+ <div class="dropdown-menu dropdown-menu-right" aria-labelledby="navbarDropdownMenuLink">
209
+ <a class="dropdown-item" href="#" onclick="confirmLogout()">Cerrar Sesión</a>
210
+ </div>
211
+ </li>
212
+ </ul>
213
+ </nav>
214
+ <aside class="sidebar-left border-right bg-white shadow" id="leftSidebar" data-simplebar>
215
+ <a href="#" class="btn collapseSidebar toggle-btn d-lg-none text-muted ml-2 mt-3" data-toggle="toggle">
216
+ <i class="fe fe-x"><span class="sr-only"></span></i>
217
+ </a>
218
+ <nav class="vertnav navbar navbar-light">
219
+ <!-- nav bar -->
220
+ <div class="w-100 mb-4 d-flex">
221
+ <a class="navbar-brand mx-auto mt-2 flex-fill text-center">
222
+ <img src="assets/images/LOGO.png" alt="Logo">
223
+ </a>
224
+ </div>
225
+
226
+ <ul class="navbar-nav flex-fill w-100 mb-2">
227
+ <li class="nav-item">
228
+ <a class="nav-link" href="./index.html">
229
+ <i class="fe fe-home fe-16"></i>
230
+ <span class="ml-3 item-text">Dashboard</span>
231
+ </a>
232
+ </li>
233
+ <li class="nav-item active">
234
+ <a class="nav-link" href="./diagnosis.html">
235
+ <i class="fe fe-eye fe-16"></i>
236
+ <span class="ml-3 item-text">Diagnóstico</span>
237
+ </a>
238
+ </li>
239
+ <li class="nav-item">
240
+ <a class="nav-link" href="./patients.html">
241
+ <i class="fe fe-users fe-16"></i>
242
+ <span class="ml-3 item-text">Pacientes</span>
243
+ </a>
244
+ </li>
245
+ <li class="nav-item">
246
+ <a class="nav-link" href="./historial-consultas.html">
247
+ <i class="fe fe-file-text fe-16"></i>
248
+ <span class="ml-3 item-text">Consultas</span>
249
+ </a>
250
+ </li>
251
+ <li class="nav-item active">
252
+ <a class="nav-link" href="./users.html">
253
+ <i class="fe fe-user fe-16"></i>
254
+ <span class="ml-3 item-text">Usuarios</span>
255
+ </a>
256
+ </li>
257
+ </ul>
258
+ </nav>
259
+ </aside>
260
+
261
+ <!-- Contenido principal -->
262
+ <main role="main" class="main-content">
263
+ <div class="container-fluid">
264
+ <div class="row justify-content-center">
265
+ <div class="col-12">
266
+ <!-- Header -->
267
+ <div class="row align-items-center mb-4">
268
+ <div class="col">
269
+ <h2 class="h3 page-title">
270
+ <i class="fe fe-eye fe-24 mr-2 text-primary"></i>
271
+ Diagnóstico con Inteligencia Artificial
272
+ </h2>
273
+ <p class="text-muted">Diagnostico de retinopatía diabética mediante análisis de imágenes</p>
274
+ </div>
275
+ </div>
276
+
277
+ <!-- Formulario principal -->
278
+ <div class="row">
279
+ <div class="col-lg-8">
280
+ <div class="card shadow mb-4">
281
+ <div class="card-header">
282
+ <h5 class="card-title mb-0">
283
+ <i class="fe fe-upload fe-16 mr-2"></i>Cargar Imágenes para Análisis
284
+ </h5>
285
+ </div>
286
+ <div class="card-body">
287
+ <div class="dual-analysis-container">
288
+ <!-- Columna 1 - Ojo Derecho -->
289
+ <div class="analysis-column">
290
+ <h6 class="text-center mb-3">Ojo Derecho</h6>
291
+
292
+ <!-- Input oculto para zona 2 -->
293
+ <input type="file" id="imageInput2" class="hidden-file-input" accept="image/*">
294
+
295
+ <!-- Zona de carga 2 -->
296
+ <div id="uploadZone2" class="upload-zone" data-zone="2">
297
+ <i class="fe fe-camera fe-48 text-muted mb-3"></i>
298
+ <h5 class="text-muted">Arrastra una imagen aquí o haz clic para seleccionar</h5>
299
+ <p class="text-muted mb-0">Formatos soportados: JPG, PNG, JPEG (Máx. 10MB)</p>
300
+ </div>
301
+
302
+ <!-- Preview de imagen 2 -->
303
+ <div id="imagePreview2" style="display: none;" class="text-center mt-4">
304
+ <img id="previewImg2" class="image-preview" alt="Vista previa">
305
+ <div class="mt-3">
306
+ <button class="btn btn-success btn-lg mr-2" id="analyzeBtn2">
307
+ <i class="fe fe-zap fe-16 mr-2"></i>Analizar con IA
308
+ </button>
309
+ <button class="btn btn-outline-secondary mr-2" id="clearBtn2">
310
+ <i class="fe fe-x fe-16 mr-2"></i>Limpiar
311
+ </button>
312
+ <button class="btn btn-warning gradcam-btn" id="gradcamBtn2" style="display: none;">
313
+ <i class="fe fe-eye fe-16 mr-2"></i>Ver Grad-CAM
314
+ </button>
315
+ </div>
316
+ </div>
317
+
318
+ <!-- Resultados del analisis 2 -->
319
+ <div id="analysisResults2" style="display: none;" class="mt-4">
320
+ <div class="card shadow mb-4 analysis-card">
321
+ <div class="card-body">
322
+ <h5 class="mb-4">
323
+ <i class="fe fe-activity fe-16 mr-2"></i>Resultados - Ojo Derecho
324
+ </h5>
325
+ <div id="resultContent2"></div>
326
+ </div>
327
+ </div>
328
+ </div>
329
+ </div>
330
+
331
+ <!-- Columna 2 - Ojo Izquierdo -->
332
+ <div class="analysis-column">
333
+ <h6 class="text-center mb-3">Ojo Izquierdo</h6>
334
+
335
+ <!-- Input oculto para zona 1 -->
336
+ <input type="file" id="imageInput1" class="hidden-file-input" accept="image/*">
337
+
338
+ <!-- Zona de carga 1 -->
339
+ <div id="uploadZone1" class="upload-zone" data-zone="1">
340
+ <i class="fe fe-camera fe-48 text-muted mb-3"></i>
341
+ <h5 class="text-muted">Arrastra una imagen aquí o haz clic para seleccionar</h5>
342
+ <p class="text-muted mb-0">Formatos soportados: JPG, PNG, JPEG (Máx. 10MB)</p>
343
+ </div>
344
+
345
+ <!-- Preview de imagen 1 -->
346
+ <div id="imagePreview1" style="display: none;" class="text-center mt-4">
347
+ <img id="previewImg1" class="image-preview" alt="Vista previa">
348
+ <div class="mt-3">
349
+ <button class="btn btn-success btn-lg mr-2" id="analyzeBtn1">
350
+ <i class="fe fe-zap fe-16 mr-2"></i>Analizar con IA
351
+ </button>
352
+ <button class="btn btn-outline-secondary mr-2" id="clearBtn1">
353
+ <i class="fe fe-x fe-16 mr-2"></i>Limpiar
354
+ </button>
355
+ <button class="btn btn-warning gradcam-btn" id="gradcamBtn1" style="display: none;">
356
+ <i class="fe fe-eye fe-16 mr-2"></i>Ver Grad-CAM
357
+ </button>
358
+ </div>
359
+ </div>
360
+
361
+ <!-- Resultados del analisis 1 -->
362
+ <div id="analysisResults1" style="display: none;" class="mt-4">
363
+ <div class="card shadow mb-4 analysis-card">
364
+ <div class="card-body">
365
+ <h5 class="mb-4">
366
+ <i class="fe fe-activity fe-16 mr-2"></i>Resultados - Ojo Izquierdo
367
+ </h5>
368
+ <div id="resultContent1"></div>
369
+ </div>
370
+ </div>
371
+ </div>
372
+ </div>
373
+ </div>
374
+ </div>
375
+ </div>
376
+ </div>
377
+
378
+ <!-- Panel lateral -->
379
+ <div class="col-lg-4">
380
+ <div class="card shadow mb-4">
381
+ <div class="card-header">
382
+ <h6 class="card-title mb-0">
383
+ <i class="fe fe-user fe-16 mr-2"></i>Información del Paciente y Consulta
384
+ </h6>
385
+ </div>
386
+ <div class="card-body">
387
+ <div class="form-group patient-search">
388
+ <label for="patientSearch">Buscar Paciente</label>
389
+ <input type="text" class="form-control" id="patientSearch" placeholder="Nombre del paciente...">
390
+ <div id="patientResults" class="patient-results"></div>
391
+ </div>
392
+
393
+ <div id="selectedPatientInfo" style="display: none;">
394
+ <div class="alert alert-info">
395
+ <h6 id="patientName"></h6>
396
+ <p class="mb-0" id="patientDetails"></p>
397
+ <input type="hidden" id="selectedPatientId">
398
+ </div>
399
+ </div>
400
+
401
+ <hr>
402
+
403
+ <div class="card">
404
+ <div class="card-header bg-light">
405
+ <h5 class="mr-2">Ojo Derecho</h5>
406
+ </div>
407
+ <div class="form-group" style="margin: 20px">
408
+ <label for="diabeticRetinopathy_right">Retinopatía Diabética</label>
409
+ <select class="form-control" id="diabeticRetinopathy_right">
410
+ <option value="false">No</option>
411
+ <option value="true">Sí</option>
412
+ </select>
413
+ </div>
414
+ </div>
415
+
416
+ <hr>
417
+
418
+ <div class="card">
419
+ <div class="card-header bg-light">
420
+ <h5 class="mr-2">Ojo Izquierdo</h5>
421
+ </div>
422
+ <div class="form-group" style="margin: 20px">
423
+ <label for="diabeticRetinopathy_left">Retinopatía Diabética</label>
424
+ <select class="form-control" id="diabeticRetinopathy_left">
425
+ <option value="false">No</option>
426
+ <option value="true">Sí</option>
427
+ </select>
428
+ </div>
429
+ </div>
430
+
431
+ <hr>
432
+
433
+ <div class="form-group">
434
+ <label for="consultationNotes">Notas de la Consulta</label>
435
+ <textarea class="form-control" id="consultationNotes" rows="3" placeholder="Observaciones adicionales..."></textarea>
436
+ </div>
437
+
438
+ <button class="btn btn-primary btn-block" id="saveConsultationBtn" type="button">
439
+ <i class="fe fe-save fe-16 mr-2"></i>Guardar Consulta
440
+ </button>
441
+ </div>
442
+ </div>
443
+
444
+ <!-- Importante -->
445
+ <div class="card shadow mb-4">
446
+ <div class="card-header">
447
+ <h6 class="card-title mb-0">
448
+ <i class="fe fe-info fe-16 mr-2"></i>Importante
449
+ </h6>
450
+ </div>
451
+ <div class="card-body">
452
+ <div class="alert alert-light">
453
+ <small>
454
+ <strong>Recuerda:</strong> La IA es una herramienta de apoyo.
455
+ Siempre confirma el diagnóstico con examen clínico completo.
456
+ </small>
457
+ </div>
458
+ </div>
459
+ </div>
460
+ </div>
461
+ </div>
462
+ </div>
463
+ </div>
464
+ </div>
465
+ </main>
466
+ </div>
467
+
468
+ <script src="js/jquery.min.js"></script>
469
+ <script src="js/popper.min.js"></script>
470
+ <script src="js/moment.min.js"></script>
471
+ <script src="js/bootstrap.min.js"></script>
472
+ <script src="js/simplebar.min.js"></script>
473
+ <script src="js/daterangepicker.js"></script>
474
+ <script src="js/jquery.stickOnScroll.js"></script>
475
+ <script src="js/tinycolor-min.js"></script>
476
+ <script src="js/config.js"></script>
477
+ <script src="api.js"></script>
478
+
479
+ <script>
480
+ $(document).ready(function() {
481
+ console.log('Sistema de diagnostico iniciado');
482
+
483
+ // Variables globales
484
+ let currentUser = null;
485
+ let selectedPatient = null;
486
+ let lastAnalysisResult1 = null; // Ojo IZQUIERDO (zona 1)
487
+ let lastAnalysisResult2 = null; // Ojo DERECHO (zona 2)
488
+ let todayStats = { analysis: 0, positive: 0 };
489
+
490
+ // Inicializar aplicacion
491
+ initializeDiagnosisApp();
492
+
493
+ // FUNCION DE INICIALIZACION
494
+ function initializeDiagnosisApp() {
495
+ console.log('Inicializando aplicacion de diagnostico...');
496
+
497
+ checkAuthentication();
498
+
499
+ checkModelStatus();
500
+
501
+ setupEventListeners();
502
+
503
+ loadTodayStats();
504
+
505
+ updateLastUpdate();
506
+ setInterval(updateLastUpdate, 60000);
507
+ }
508
+
509
+ // AUTENTICACION
510
+ function checkAuthentication() {
511
+ api.getSession().then(function(response) {
512
+ if (response && response.success) {
513
+ currentUser = response.user;
514
+ } else {
515
+ window.location.href = 'auth-login.html';
516
+ }
517
+ }).catch(() => { window.location.href = 'auth-login.html'; });
518
+ }
519
+
520
+ // VERIFICAR ESTADO DEL MODELO
521
+ function checkModelStatus() {
522
+ api.getModelInfo().then(function(response) {
523
+ if (response && response.loaded) {
524
+ $('#modelStatusBadge').removeClass('badge-danger').addClass('badge-success');
525
+ $('#modelStatusText').text('Modelo Cargado');
526
+ } else {
527
+ $('#modelStatusBadge').removeClass('badge-success').addClass('badge-danger');
528
+ $('#modelStatusText').text('Modelo No Disponible');
529
+ showAlert('warning', 'El modelo de IA no está disponible.');
530
+ }
531
+ });
532
+ }
533
+
534
+ // EVENT LISTENERS
535
+ function setupEventListeners() {
536
+ console.log('Configurando event listeners...');
537
+
538
+ $(document).on('click', '#uploadZone1', function(e) {
539
+ e.preventDefault();
540
+ e.stopPropagation();
541
+ console.log('Click detectado en zona 1');
542
+ $('#imageInput1')[0].click();
543
+ });
544
+
545
+ $(document).on('click', '#uploadZone2', function(e) {
546
+ e.preventDefault();
547
+ e.stopPropagation();
548
+ console.log('Click detectado en zona 2');
549
+ $('#imageInput2')[0].click();
550
+ });
551
+
552
+ // Carga de archivos para ambas zonas
553
+ $(document).on('change', '#imageInput1', function(e) {
554
+ console.log('Archivo seleccionado en zona 1');
555
+ handleImageSelect(e, 1);
556
+ });
557
+
558
+ $(document).on('change', '#imageInput2', function(e) {
559
+ console.log('Archivo seleccionado en zona 2');
560
+ handleImageSelect(e, 2);
561
+ });
562
+
563
+ // Drag and drop para ambas zonas
564
+ setupDragAndDrop(1);
565
+ setupDragAndDrop(2);
566
+
567
+ // Analisis y limpieza para ambas zonas
568
+ $(document).on('click', '#analyzeBtn1', function() { handleImageAnalysis(1); });
569
+ $(document).on('click', '#analyzeBtn2', function() { handleImageAnalysis(2); });
570
+
571
+ $(document).on('click', '#clearBtn1', function() { clearImagePreview(1); });
572
+ $(document).on('click', '#clearBtn2', function() { clearImagePreview(2); });
573
+
574
+ // Grad-CAM para ambas zonas
575
+ $(document).on('click', '#gradcamBtn1', function() { showGradCAMAnalysis(1); });
576
+ $(document).on('click', '#gradcamBtn2', function() { showGradCAMAnalysis(2); });
577
+
578
+ // Busqueda de pacientes
579
+ $(document).on('input', '#patientSearch', handlePatientSearch);
580
+ $(document).on('click', '.patient-item', handlePatientSelection);
581
+
582
+ // Guardar consulta
583
+ $(document).on('click', '#saveConsultationBtn', function(e) {
584
+ e.preventDefault();
585
+ console.log('Boton guardar consulta clickeado');
586
+ handleSaveConsultation();
587
+ });
588
+
589
+ // Eventos para selectores manuales
590
+ $(document).on('change', '#diabeticRetinopathy_right, #diabeticRetinopathy_left', function() {
591
+ console.log('Diagnostico manual cambiado');
592
+ enableSaveButton();
593
+ });
594
+
595
+ // Cerrar dropdowns al hacer click fuera
596
+ $(document).on('click', function(e) {
597
+ if (!$(e.target).closest('.patient-search').length) {
598
+ $('#patientResults').hide();
599
+ }
600
+ });
601
+
602
+ console.log('Event listeners configurados correctamente');
603
+ }
604
+
605
+ // DRAG AND DROP
606
+ function setupDragAndDrop(zoneNumber) {
607
+ const uploadZone = document.getElementById(`uploadZone${zoneNumber}`);
608
+ if (!uploadZone) return;
609
+
610
+ ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
611
+ uploadZone.addEventListener(eventName, preventDefaults, false);
612
+ });
613
+
614
+ ['dragenter', 'dragover'].forEach(eventName => {
615
+ uploadZone.addEventListener(eventName, function() { highlight(zoneNumber); }, false);
616
+ });
617
+
618
+ ['dragleave', 'drop'].forEach(eventName => {
619
+ uploadZone.addEventListener(eventName, function() { unhighlight(zoneNumber); }, false);
620
+ });
621
+
622
+ uploadZone.addEventListener('drop', function(e) { handleDrop(e, zoneNumber); }, false);
623
+
624
+ function preventDefaults(e) {
625
+ e.preventDefault();
626
+ e.stopPropagation();
627
+ }
628
+
629
+ function highlight(zoneNumber) {
630
+ $(`#uploadZone${zoneNumber}`).addClass('dragover');
631
+ }
632
+
633
+ function unhighlight(zoneNumber) {
634
+ $(`#uploadZone${zoneNumber}`).removeClass('dragover');
635
+ }
636
+
637
+ function handleDrop(e, zoneNumber) {
638
+ const files = e.dataTransfer.files;
639
+ if (files.length > 0) {
640
+ console.log(`Archivo arrastrado a zona ${zoneNumber}`);
641
+ handleImageFile(files[0], zoneNumber);
642
+ }
643
+ }
644
+ }
645
+
646
+ // MANEJO DE ARCHIVOS
647
+ function handleImageSelect(e, zoneNumber) {
648
+ const file = e.target.files[0];
649
+ if (file) {
650
+ console.log(`Imagen seleccionada para zona ${zoneNumber}:`, file.name);
651
+ handleImageFile(file, zoneNumber);
652
+ }
653
+ }
654
+
655
+ function handleImageFile(file, zoneNumber) {
656
+ // Validaciones
657
+ if (!file.type.startsWith('image/')) {
658
+ showAlert('error', 'Por favor selecciona un archivo de imagen valido');
659
+ return;
660
+ }
661
+
662
+ if (file.size > 20 * 1024 * 1024) {
663
+ showAlert('error', 'La imagen es demasiado grande. Máximo 20MB');
664
+ return;
665
+ }
666
+
667
+ // Mostrar preview
668
+ const reader = new FileReader();
669
+ reader.onload = function(e) {
670
+ $(`#previewImg${zoneNumber}`).attr('src', e.target.result);
671
+ $(`#imagePreview${zoneNumber}`).show().addClass('fade-in');
672
+ $(`#uploadZone${zoneNumber}`).hide();
673
+ $(`#analysisResults${zoneNumber}`).hide();
674
+ $(`#analyzeBtn${zoneNumber}`).prop('disabled', false);
675
+ $(`#gradcamBtn${zoneNumber}`).hide();
676
+ };
677
+ reader.readAsDataURL(file);
678
+
679
+ console.log(`Imagen cargada en zona ${zoneNumber}:`, file.name);
680
+ showAlert('success', `Imagen cargada correctamente en ojo ${zoneNumber === 1 ? 'izquierdo' : 'derecho'}. Procede al analisis.`);
681
+ }
682
+
683
+ // ANALISIS DE IMAGEN
684
+ function handleImageAnalysis(zoneNumber) {
685
+ if (!$(`#previewImg${zoneNumber}`).attr('src')) {
686
+ showAlert('error', 'No hay imagen para analizar');
687
+ return;
688
+ }
689
+
690
+ // Mostrar loading
691
+ $(`#analyzeBtn${zoneNumber}`).prop('disabled', true)
692
+ .html('<i class="spinner-border spinner-border-sm mr-2"></i>Analizando...')
693
+ .addClass('pulse-animation');
694
+
695
+ // Obtener datos
696
+ const imageData = $(`#previewImg${zoneNumber}`).attr('src');
697
+ const fileName = $(`#imageInput${zoneNumber}`)[0].files[0]?.name || `analisis_${Date.now()}.jpg`;
698
+ const patientId = selectedPatient ? selectedPatient.patientID : null;
699
+ const notes = $('#consultationNotes').val().trim();
700
+
701
+ console.log(`Iniciando analisis zona ${zoneNumber}:`, {
702
+ fileName,
703
+ patientId,
704
+ hasNotes: !!notes
705
+ });
706
+
707
+ if (false) { // simulation removed
708
+ console.log('Eel no disponible - simulando analisis');
709
+ setTimeout(() => {
710
+ const mockResult = {
711
+ success: true,
712
+ prediction: {
713
+ class: Math.random() > 0.5 ? 'Retinopatía Diabética' : 'Normal',
714
+ class_index: Math.random() > 0.5 ? 1 : 0,
715
+ confidence: Math.floor(Math.random() * 30 + 70) // 70-100%
716
+ },
717
+ timestamp: new Date().toISOString(),
718
+ filename: fileName
719
+ };
720
+
721
+ if (zoneNumber === 1) {
722
+ lastAnalysisResult1 = mockResult;
723
+ } else {
724
+ lastAnalysisResult2 = mockResult;
725
+ }
726
+
727
+ $(`#analyzeBtn${zoneNumber}`).prop('disabled', false)
728
+ .html('<i class="fe fe-zap fe-16 mr-2"></i>Analizar con IA')
729
+ .removeClass('pulse-animation');
730
+
731
+ showAnalysisResults(mockResult, zoneNumber);
732
+ enableSaveButton();
733
+ }, 2000);
734
+ return;
735
+ }
736
+
737
+ // Llamar al modelo
738
+ api.predict(imageData, fileName).then(function(result) {
739
+ // Restaurar boton
740
+ $(`#analyzeBtn${zoneNumber}`).prop('disabled', false)
741
+ .html('<i class="fe fe-zap fe-16 mr-2"></i>Analizar con IA')
742
+ .removeClass('pulse-animation');
743
+
744
+ if (result && result.success) {
745
+ if (zoneNumber === 1) {
746
+ lastAnalysisResult1 = result;
747
+ } else {
748
+ lastAnalysisResult2 = result;
749
+ }
750
+
751
+ showAnalysisResults(result, zoneNumber);
752
+ updateTodayStats();
753
+
754
+ // Habilitar boton guardar siempre que haya analisis
755
+ enableSaveButton();
756
+
757
+ console.log(`Analisis completado zona ${zoneNumber}:`, result.prediction.class);
758
+ } else {
759
+ showAlert('error', result?.error || 'Error en el analisis de la imagen');
760
+ console.log(`Error en analisis zona ${zoneNumber}:`, result);
761
+ }
762
+ });
763
+ }
764
+
765
+ // === FUNCION PARA HABILITAR BOTON GUARDAR ===
766
+ function enableSaveButton() {
767
+ // Habilitar si hay paciente seleccionado Y (analisis o diagoóstico manual)
768
+ const hasPatient = selectedPatient !== null;
769
+ const hasAnalysis = lastAnalysisResult1 !== null || lastAnalysisResult2 !== null;
770
+ const hasManualDiagnosis = $('#diabeticRetinopathy_right').val() !== '' || $('#diabeticRetinopathy_left').val() !== '';
771
+
772
+ const shouldEnable = hasPatient && (hasAnalysis || hasManualDiagnosis);
773
+
774
+ console.log('Verificando boton guardar:', {
775
+ hasPatient,
776
+ hasAnalysis,
777
+ hasManualDiagnosis,
778
+ shouldEnable
779
+ });
780
+
781
+ $('#saveConsultationBtn').prop('disabled', !shouldEnable);
782
+
783
+ if (shouldEnable) {
784
+ $('#saveConsultationBtn').removeClass('btn-secondary').addClass('btn-primary');
785
+ } else {
786
+ $('#saveConsultationBtn').removeClass('btn-primary').addClass('btn-secondary');
787
+ }
788
+ }
789
+
790
+ // === MOSTRAR RESULTADOS ===
791
+ function showAnalysisResults(result, zoneNumber) {
792
+ const prediction = result.prediction;
793
+ const isPositive = prediction.class_index === 1;
794
+ const confidence = prediction.confidence;
795
+
796
+ // Determinar clase de resultado y colores
797
+ const resultClass = isPositive ? 'border-warning' : 'border-success';
798
+ const iconClass = isPositive ? 'fe-alert-triangle' : 'fe-check-circle';
799
+ const iconColor = isPositive ? 'text-warning' : 'text-success';
800
+ const resultText = isPositive ? 'Retinopatía Diabética Detectada' : 'No se Detectó Retinopatía Diabética';
801
+ const eyeText = zoneNumber === 1 ? 'Ojo Izquierdo' : 'Ojo Derecho';
802
+
803
+ const resultHtml = `
804
+ <!-- Resultado Principal -->
805
+ <div class="card shadow-sm ${resultClass} border-2">
806
+ <div class="card-body">
807
+ <div class="row align-items-center">
808
+ <div class="col-2 text-center">
809
+ <i class="fe ${iconClass} fe-48 ${iconColor}"></i>
810
+ </div>
811
+ <div class="col-10">
812
+ <h5 class="card-title mb-1">${resultText}</h5>
813
+ <p class="text-muted mb-0">${eyeText}</p>
814
+ </div>
815
+ </div>
816
+ </div>
817
+ </div>
818
+
819
+ <!-- Detalles del Analisis -->
820
+ <div class="card shadow-sm mt-3">
821
+ <div class="card-header d-flex justify-content-between align-items-center">
822
+ <h6 class="mb-0">
823
+ <i class="fe fe-bar-chart-2 fe-16 mr-2"></i>Detalles del Análisis
824
+ </h6>
825
+ <span class="badge badge-pill ${isPositive ? 'badge-warning' : 'badge-success'}">${confidence}%</span>
826
+ </div>
827
+ <div class="card-body">
828
+ <!-- Medidor de Confianza -->
829
+ <div class="mb-3">
830
+ <div class="d-flex justify-content-between align-items-center mb-2">
831
+ <small class="text-muted">Nivel de Confianza</small>
832
+ <small class="font-weight-bold">${confidence}%</small>
833
+ </div>
834
+ <div class="progress" style="height: 8px;">
835
+ <div class="progress-bar ${isPositive ? 'bg-warning' : 'bg-success'}"
836
+ role="progressbar"
837
+ style="width: ${confidence}%"
838
+ aria-valuenow="${confidence}"
839
+ aria-valuemin="0"
840
+ aria-valuemax="100">
841
+ </div>
842
+ </div>
843
+ </div>
844
+
845
+ <!-- Informacion Temporal -->
846
+ <div class="row text-center">
847
+ <div class="col-6">
848
+ <div class="border-right">
849
+ <h6 class="text-muted mb-0">Análisis realizado</h6>
850
+ <small class="text-muted">
851
+ <i class="fe fe-clock fe-12 mr-1"></i>
852
+ ${moment ? moment(result.timestamp).format('DD/MM/YYYY HH:mm') : new Date(result.timestamp).toLocaleString()}
853
+ </small>
854
+ </div>
855
+ </div>
856
+ <div class="col-6">
857
+ <h6 class="text-muted mb-0">Modelo utilizado</h6>
858
+ <small class="text-muted">
859
+ <i class="fe fe-cpu fe-12 mr-1"></i>
860
+ EfficientNetB0
861
+ </small>
862
+ </div>
863
+ </div>
864
+ </div>
865
+ </div>
866
+ `;
867
+
868
+ $(`#resultContent${zoneNumber}`).html(resultHtml);
869
+ $(`#analysisResults${zoneNumber}`).show().addClass('fade-in');
870
+
871
+ // Cambiar el fondo del contenedor de resultados
872
+ $(`#analysisResults${zoneNumber} .analysis-card`).css({
873
+ 'background': '#f8f9fa',
874
+ 'color': '#495057'
875
+ });
876
+
877
+ // Scroll suave hacia los resultados
878
+ $('html, body').animate({
879
+ scrollTop: $(`#analysisResults${zoneNumber}`).offset().top - 100
880
+ }, 800);
881
+
882
+ // Actualizar selectores correctamente
883
+ if(zoneNumber === 1) {
884
+ // Zona 1 = Ojo IZQUIERDO
885
+ document.getElementById('diabeticRetinopathy_left').value = isPositive.toString();
886
+ } else {
887
+ // Zona 2 = Ojo DERECHO
888
+ document.getElementById('diabeticRetinopathy_right').value = isPositive.toString();
889
+ }
890
+
891
+ // Mostrar boton Grad-CAM solo si se detecto retinopatia diabetica
892
+ if (isPositive) {
893
+ $(`#gradcamBtn${zoneNumber}`).show().addClass('pulse-animation');
894
+ console.log(`Retinopatia detectada en zona ${zoneNumber} - Grad-CAM disponible`);
895
+ } else {
896
+ $(`#gradcamBtn${zoneNumber}`).hide();
897
+ }
898
+
899
+ // Habilitar boton guardar
900
+ enableSaveButton();
901
+ }
902
+
903
+ // === BUSQUEDA DE PACIENTES ===
904
+ function handlePatientSearch() {
905
+ const searchTerm = $('#patientSearch').val().trim();
906
+
907
+ if (searchTerm.length < 2) {
908
+ $('#patientResults').hide();
909
+ return;
910
+ }
911
+
912
+ if (typeof eel === 'undefined') {
913
+ // Simular busqueda de pacientes
914
+ const mockPatients = [
915
+ { patientID: 1, name: 'Juan Pérez', birthDate: '1980-05-15', diabetesType: 'Tipo 2' },
916
+ { patientID: 2, name: 'María García', birthDate: '2005-08-22', diabetesType: 'Tipo 1' },
917
+ { patientID: 3, name: 'Carlos López', birthDate: '1990-12-10', diabetesType: 'Tipo 2' }
918
+ ].filter(p => p.name.toLowerCase().includes(searchTerm.toLowerCase()));
919
+
920
+ displayPatientResults(mockPatients);
921
+ return;
922
+ }
923
+
924
+ api.getPatients(searchTerm).then(function(response) {
925
+ if (response && response.success) {
926
+ displayPatientResults(response.patients);
927
+ } else {
928
+ $('#patientResults').hide();
929
+ }
930
+ });
931
+ }
932
+
933
+ function displayPatientResults(patients) {
934
+ const resultsHtml = patients.map(patient => `
935
+ <div class="patient-item" data-patient='${JSON.stringify(patient)}'>
936
+ <strong>${patient.name}</strong>
937
+ ${patient.birthDate ? `<br><small class="text-muted">Nacimiento: ${moment ? moment(patient.birthDate).format('DD/MM/YYYY') : patient.birthDate}</small>` : ''}
938
+ ${patient.diabetesType ? `<br><small class="text-muted">Diabetes: ${patient.diabetesType}</small>` : ''}
939
+ </div>
940
+ `).join('');
941
+
942
+ $('#patientResults').html(resultsHtml).show();
943
+ }
944
+
945
+ function handlePatientSelection() {
946
+ const patientData = $(this).data('patient');
947
+ selectedPatient = patientData;
948
+
949
+ $('#patientSearch').val(patientData.name);
950
+ $('#patientResults').hide();
951
+
952
+ // Mostrar informacion del paciente
953
+ const age = patientData.birthDate ? (moment ? moment().diff(moment(patientData.birthDate), 'years') : 'N/A') : 'N/A';
954
+ $('#selectedPatientInfo').show();
955
+ $('#patientName').text(patientData.name);
956
+ $('#patientDetails').html(`
957
+ <strong>ID:</strong> ${patientData.patientID} |
958
+ <strong>Edad:</strong> ${age} años
959
+ ${patientData.diabetesType ? `| <strong>Diabetes:</strong> ${patientData.diabetesType}` : ''}
960
+ `);
961
+ $('#selectedPatientId').val(patientData.patientID);
962
+
963
+ console.log('Paciente seleccionado:', patientData.name);
964
+ showAlert('info', `Paciente ${patientData.name} seleccionado correctamente`);
965
+
966
+ // Habilitar boton de guardar
967
+ enableSaveButton();
968
+ }
969
+
970
+ // === FUNCION CORREGIDA GUARDAR CONSULTA ===
971
+ function handleSaveConsultation() {
972
+ console.log('Iniciando guardado de consulta...');
973
+
974
+ if (!selectedPatient) {
975
+ showAlert('error', 'Selecciona un paciente antes de guardar');
976
+ return;
977
+ }
978
+
979
+ const notes = $('#consultationNotes').val().trim();
980
+
981
+ // Datos correctamente estructurados
982
+ const consultationData = {
983
+ patientId: selectedPatient.patientID,
984
+ notes: notes,
985
+ rightEye: {
986
+ hasAnalysis: lastAnalysisResult2 !== null, // Zona 2 = Ojo DERECHO
987
+ diagnosis: lastAnalysisResult2 ?
988
+ (lastAnalysisResult2.prediction.class_index === 1) :
989
+ ($('#diabeticRetinopathy_right').val() === 'true'),
990
+ confidence: lastAnalysisResult2 ? lastAnalysisResult2.prediction.confidence : null,
991
+ imagePath: lastAnalysisResult2 ? lastAnalysisResult2.filename : null
992
+ },
993
+ leftEye: {
994
+ hasAnalysis: lastAnalysisResult1 !== null, // Zona 1 = Ojo IZQUIERDO
995
+ diagnosis: lastAnalysisResult1 ?
996
+ (lastAnalysisResult1.prediction.class_index === 1) :
997
+ ($('#diabeticRetinopathy_left').val() === 'true'),
998
+ confidence: lastAnalysisResult1 ? lastAnalysisResult1.prediction.confidence : null,
999
+ imagePath: lastAnalysisResult1 ? lastAnalysisResult1.filename : null
1000
+ }
1001
+ };
1002
+
1003
+ // Log detallado para debug
1004
+ console.log('Datos de consulta a guardar:', {
1005
+ paciente: selectedPatient.name,
1006
+ rightEye: consultationData.rightEye,
1007
+ leftEye: consultationData.leftEye,
1008
+ notes: notes
1009
+ });
1010
+
1011
+ // Mostrar loading
1012
+ $('#saveConsultationBtn').prop('disabled', true)
1013
+ .html('<i class="spinner-border spinner-border-sm mr-2"></i>Guardando...');
1014
+
1015
+ if (false) { // simulation removed
1016
+ console.log('Eel no disponible - simulando guardado');
1017
+ setTimeout(() => {
1018
+ $('#saveConsultationBtn').prop('disabled', false)
1019
+ .html('<i class="fe fe-save fe-16 mr-2"></i>Guardar Consulta');
1020
+ showAlert('success', 'Consulta guardada correctamente (simulación)');
1021
+ }, 1500);
1022
+ return;
1023
+ }
1024
+
1025
+ // Llamada correcta a EEL
1026
+ api.saveConsultation(consultationData).then(function(response) {
1027
+ console.log('Respuesta del servidor:', response);
1028
+
1029
+ // Restaurar boton
1030
+ $('#saveConsultationBtn').prop('disabled', false)
1031
+ .html('<i class="fe fe-save fe-16 mr-2"></i>Guardar Consulta');
1032
+
1033
+ if (response && response.success) {
1034
+ showAlert('success', 'Consulta guardada correctamente');
1035
+ console.log('Consulta guardada exitosamente:', response);
1036
+ } else {
1037
+ const errorMsg = response?.message || 'Error al guardar la consulta';
1038
+ showAlert('error', errorMsg);
1039
+ console.error('Error guardando consulta:', response);
1040
+ }
1041
+ });
1042
+ }
1043
+
1044
+ // === FUNCION GRAD-CAM ===
1045
+ function showGradCAMAnalysis(zoneNumber) {
1046
+ const lastResult = zoneNumber === 1 ? lastAnalysisResult1 : lastAnalysisResult2;
1047
+
1048
+ if (!lastResult) {
1049
+ showAlert('error', 'No hay analisis disponible para generar Grad-CAM');
1050
+ return;
1051
+ }
1052
+
1053
+ // Solo generar si se detecto retinopatia
1054
+ if (lastResult.prediction.class_index !== 1) {
1055
+ showAlert('info', 'Grad-CAM solo se genera para casos positivos de retinopatia diabetica');
1056
+ return;
1057
+ }
1058
+
1059
+ const imageData = $(`#previewImg${zoneNumber}`).attr('src');
1060
+ const fileName = lastResult.filename;
1061
+ const eyeName = zoneNumber === 1 ? 'Ojo Izquierdo' : 'Ojo Derecho';
1062
+
1063
+ // Mostrar loading en el boton
1064
+ $(`#gradcamBtn${zoneNumber}`).prop('disabled', true)
1065
+ .html('<i class="spinner-border spinner-border-sm mr-2"></i>Generando...');
1066
+
1067
+ console.log(`Generando Grad-CAM para zona ${zoneNumber}:`, fileName);
1068
+
1069
+ if (false) { /* removed */ }
1070
+
1071
+ // Llamar a la funcion de backend
1072
+ api.gradcam(imageData, fileName, lastResult).then(function(response) {
1073
+ // Restaurar boton
1074
+ $(`#gradcamBtn${zoneNumber}`).prop('disabled', false)
1075
+ .html('<i class="fe fe-eye fe-16 mr-2"></i>Ver Grad-CAM');
1076
+
1077
+ if (response && response.success) {
1078
+ console.log('Grad-CAM generado exitosamente');
1079
+ showGradCAMModal(response, eyeName, zoneNumber);
1080
+ } else {
1081
+ const errorMsg = response?.error || 'Error generando analisis Grad-CAM';
1082
+ showAlert('error', errorMsg);
1083
+ console.error('Error Grad-CAM:', response);
1084
+ }
1085
+ });
1086
+ }
1087
+
1088
+ function showGradCAMModal(gradcamData, eyeName, zoneNumber) {
1089
+ const analysis = gradcamData.analysis || {};
1090
+
1091
+ // Campos con valores por defecto para compatibilidad
1092
+ const maxAct = (analysis.max_activation || 0).toFixed(3);
1093
+ const avgAct = (analysis.avg_activation || 0).toFixed(3);
1094
+ const highPct = (analysis.high_activation_pct || analysis.zoom_high_activation_pct || 0).toFixed(1);
1095
+ const clinical = analysis.clinical_info || 'Análisis completado';
1096
+ const region = analysis.zoom_region_hd || [0,0,0,0];
1097
+ const alertType = parseFloat(highPct) > 20 ? 'danger' : parseFloat(highPct) > 10 ? 'warning' : 'info';
1098
+
1099
+ const modalHtml = `
1100
+ <div class="modal fade gradcam-modal" id="gradcamModal" tabindex="-1" role="dialog">
1101
+ <div class="modal-dialog modal-lg" role="document">
1102
+ <div class="modal-content">
1103
+ <div class="modal-header bg-danger text-white">
1104
+ <h5 class="modal-title">
1105
+ <i class="fe fe-eye mr-2"></i>Zoom HD - ${eyeName}
1106
+ <span class="badge badge-light text-danger ml-2">Retinopatía Detectada</span>
1107
+ </h5>
1108
+ <button type="button" class="close text-white" data-dismiss="modal">
1109
+ <span>&times;</span>
1110
+ </button>
1111
+ </div>
1112
+ <div class="modal-body p-0">
1113
+ <div class="text-center bg-white p-4">
1114
+ <img src="${gradcamData.gradcam_image}"
1115
+ class="img-fluid"
1116
+ style="max-width: 100%; border-radius: 8px; border: 2px solid #dc3545;"
1117
+ alt="Zoom HD - Región Crítica">
1118
+ <div class="mt-3">
1119
+ <h6 class="text-danger mb-2">
1120
+ <i class="fe fe-zoom-in mr-2"></i>Zona Crítica en Alta Resolución
1121
+ </h6>
1122
+ <p class="text-muted mb-0">
1123
+ <strong>Activación máxima:</strong> ${maxAct} |
1124
+ <strong>Activación promedio:</strong> ${avgAct}
1125
+ </p>
1126
+ </div>
1127
+ </div>
1128
+
1129
+ <div class="p-4">
1130
+ <div class="alert alert-info">
1131
+ <h6 class="text-info mb-2">Coordenadas en imagen original:</h6>
1132
+ <p class="mb-1">Región HD: [x: ${region[0]}–${region[2]}, y: ${region[1]}–${region[3]}]</p>
1133
+ <p class="mb-0"><strong>Área de alta activación:</strong> ${highPct}% del total</p>
1134
+ </div>
1135
+ <div class="alert alert-${alertType}">
1136
+ <small>
1137
+ <i class="fe fe-info mr-1"></i>
1138
+ ${clinical}
1139
+ </small>
1140
+ </div>
1141
+ </div>
1142
+ </div>
1143
+ <div class="modal-footer">
1144
+ <button type="button" class="btn btn-outline-primary" onclick="downloadGradCAMImage(${zoneNumber})">
1145
+ <i class="fe fe-download mr-2"></i>Descargar Imagen
1146
+ </button>
1147
+ <button type="button" class="btn btn-secondary" data-dismiss="modal">
1148
+ <i class="fe fe-x mr-2"></i>Cerrar
1149
+ </button>
1150
+ </div>
1151
+ </div>
1152
+ </div>
1153
+ </div>
1154
+ `;
1155
+
1156
+ // Remover modal existente si lo hay
1157
+ $('#gradcamModal').remove();
1158
+
1159
+ // Agregar y mostrar nuevo modal
1160
+ $('body').append(modalHtml);
1161
+ $('#gradcamModal').modal('show').on('hidden.bs.modal', function() {
1162
+ $(this).remove();
1163
+ });
1164
+
1165
+ // Guardar datos para descarga
1166
+ window.currentGradCAMData = gradcamData;
1167
+ }
1168
+
1169
+ // === FUNCIONES DE UTILIDAD ===
1170
+ function clearImagePreview(zoneNumber) {
1171
+ $(`#imagePreview${zoneNumber}`).hide();
1172
+ $(`#analysisResults${zoneNumber}`).hide();
1173
+ $(`#uploadZone${zoneNumber}`).show();
1174
+ $(`#imageInput${zoneNumber}`).val('');
1175
+ $(`#analyzeBtn${zoneNumber}`).prop('disabled', false);
1176
+ $(`#gradcamBtn${zoneNumber}`).hide().removeClass('pulse-animation');
1177
+
1178
+ if (zoneNumber === 1) {
1179
+ lastAnalysisResult1 = null;
1180
+ } else {
1181
+ lastAnalysisResult2 = null;
1182
+ }
1183
+
1184
+ // Verificar si debe deshabilitar guardar
1185
+ enableSaveButton();
1186
+ }
1187
+
1188
+ function loadTodayStats() {
1189
+ api.getConsultations(1, 50).then(function(r) {
1190
+ const history = (r.consultations || []).map(c => ({
1191
+ timestamp: c.consultationDate,
1192
+ prediction: { class_index: c.diabeticRetinopathy ? 1 : 0 }
1193
+ }));
1194
+ if (history.length > 0) {
1195
+ const today = moment ? moment().format('YYYY-MM-DD') : new Date().toISOString().split('T')[0];
1196
+ const todayAnalyses = history.filter(item =>
1197
+ (item.timestamp || '').substring(0, 10) === today
1198
+ );
1199
+ todayStats.analysis = todayAnalyses.length;
1200
+ todayStats.positive = todayAnalyses.filter(i => i.prediction.class_index === 1).length;
1201
+ updateStatsDisplay();
1202
+ }
1203
+ });
1204
+ }
1205
+
1206
+ function updateTodayStats() {
1207
+ if (lastAnalysisResult1 || lastAnalysisResult2) {
1208
+ todayStats.analysis++;
1209
+
1210
+ const hasPositive = (lastAnalysisResult1 && lastAnalysisResult1.prediction.class_index === 1) ||
1211
+ (lastAnalysisResult2 && lastAnalysisResult2.prediction.class_index === 1);
1212
+
1213
+ if (hasPositive) {
1214
+ todayStats.positive++;
1215
+ }
1216
+ updateStatsDisplay();
1217
+ }
1218
+ }
1219
+
1220
+ function updateStatsDisplay() {
1221
+ $('#todayAnalysis').text(todayStats.analysis);
1222
+ $('#todayPositive').text(todayStats.positive);
1223
+ }
1224
+
1225
+ function updateLastUpdate() {
1226
+ const now = moment ? moment().format('HH:mm') : new Date().toLocaleTimeString().slice(0,5);
1227
+ $('#lastUpdate').text(now);
1228
+ }
1229
+
1230
+ function showAlert(type, message) {
1231
+ const alertClass = {
1232
+ 'success': 'alert-success',
1233
+ 'error': 'alert-danger',
1234
+ 'warning': 'alert-warning',
1235
+ 'info': 'alert-info'
1236
+ }[type] || 'alert-info';
1237
+
1238
+ const alertHtml = `
1239
+ <div class="alert ${alertClass} alert-dismissible fade show" role="alert">
1240
+ <i class="fe fe-${type === 'error' ? 'x-circle' : type === 'success' ? 'check-circle' : type === 'warning' ? 'alert-triangle' : 'info'} mr-2"></i>
1241
+ ${message}
1242
+ <button type="button" class="close" data-dismiss="alert">
1243
+ <span aria-hidden="true">&times;</span>
1244
+ </button>
1245
+ </div>
1246
+ `;
1247
+
1248
+ $('.container-fluid').prepend(alertHtml);
1249
+
1250
+ setTimeout(() => {
1251
+ $('.alert').first().alert('close');
1252
+ }, 5000);
1253
+ }
1254
+
1255
+ // === FUNCIONES GLOBALES ===
1256
+ window.downloadGradCAMImage = function(zoneNumber) {
1257
+ if (!window.currentGradCAMData) {
1258
+ showAlert('error', 'No hay imagen Grad-CAM para descargar');
1259
+ return;
1260
+ }
1261
+
1262
+ try {
1263
+ const eyeName = zoneNumber === 1 ? 'izquierdo' : 'derecho';
1264
+ const filename = `gradcam_${eyeName}_${window.currentGradCAMData.filename}_${new Date().getTime()}.png`;
1265
+
1266
+ // Crear enlace de descarga
1267
+ const link = document.createElement('a');
1268
+ link.href = window.currentGradCAMData.gradcam_image;
1269
+ link.download = filename;
1270
+ document.body.appendChild(link);
1271
+ link.click();
1272
+ document.body.removeChild(link);
1273
+
1274
+ showAlert('success', `Imagen Grad-CAM descargada: ${filename}`);
1275
+ } catch (error) {
1276
+ showAlert('error', 'Error al descargar la imagen');
1277
+ console.error('Error descarga:', error);
1278
+ }
1279
+ };
1280
+
1281
+ console.log('Sistema de diagnostico completamente inicializado');
1282
+ });
1283
+ </script>
1284
+
1285
+ <script>
1286
+ function confirmLogout() {
1287
+ executeLogout();
1288
+ }
1289
+
1290
+ function executeLogout() {
1291
+ api.logout().then(() => { window.location.href = 'auth-login.html'; });
1292
+ }
1293
+ </script>
1294
+
1295
+
1296
+
1297
+
1298
+
1299
+
1300
+ <script>
1301
+ // Centrar ventana al cargar pagina
1302
+ (function() {
1303
+ function centerWindow() {
1304
+ try {
1305
+ const screenWidth = window.screen.availWidth;
1306
+ const screenHeight = window.screen.availHeight;
1307
+ const x = Math.floor((screenWidth - 1200) / 2);
1308
+ const y = Math.floor((screenHeight - 800) / 2);
1309
+
1310
+ if (window.moveTo) {
1311
+ window.moveTo(Math.max(0, x), Math.max(0, y));
1312
+ }
1313
+ } catch (e) {
1314
+ console.warn('No se pudo centrar ventana:', e);
1315
+ }
1316
+ }
1317
+
1318
+ // Centrar al cargar
1319
+ if (document.readyState === 'loading') {
1320
+ document.addEventListener('DOMContentLoaded', centerWindow);
1321
+ } else {
1322
+ centerWindow();
1323
+ }
1324
+
1325
+ window.addEventListener('load', centerWindow);
1326
+ window.addEventListener('focus', centerWindow);
1327
+ })();
1328
+ </script>
1329
+
1330
+
1331
+
1332
+
1333
+ <script>
1334
+ function checkUserRoleAndHideNavigation() {
1335
+ api.getSession().then(function(response) {
1336
+ if (response.success && response.user) {
1337
+ const userRole = response.user.role;
1338
+ const usersNavItem = document.querySelector('a[href="./users.html"]');
1339
+
1340
+ if (usersNavItem) {
1341
+ const navItemContainer = usersNavItem.closest('li.nav-item');
1342
+
1343
+ if (userRole === 'Admin') {
1344
+ // Usuario es Admin - mostrar elemento
1345
+ if (navItemContainer) {
1346
+ navItemContainer.style.display = 'block';
1347
+ }
1348
+ console.log('Navegacion de usuarios visible para administrador:', response.user.username);
1349
+ } else {
1350
+ // Usuario NO es Admin - ocultar elemento
1351
+ if (navItemContainer) {
1352
+ navItemContainer.style.display = 'none';
1353
+ }
1354
+ console.log('Navegacion de usuarios oculta para usuario:', response.user.username, '(Rol:', userRole + ')');
1355
+ }
1356
+ } else {
1357
+ console.warn('No se encontró el elemento de navegación de usuarios');
1358
+ }
1359
+ } else {
1360
+ console.warn('No se pudo obtener información del usuario actual');
1361
+ }
1362
+ });
1363
+ }
1364
+
1365
+ // Funcion para verificar si el usuario actual esta intentando acceder a usuarios.html
1366
+ function checkUsersPageAccess() {
1367
+ // Solo ejecutar en la pagina de usuarios
1368
+ if (window.location.pathname.includes('users.html') || window.location.href.includes('users.html')) {
1369
+ api.getSession().then(function(response) {
1370
+ if (!response.success) { window.location.href = 'auth-login.html'; return; }
1371
+ if (response.user.role !== 'Admin') { window.location.href = 'index.html'; return;
1372
+ }
1373
+ });
1374
+ }
1375
+ }
1376
+
1377
+ // Ejecutar al cargar la pagina
1378
+ $(document).ready(function() {
1379
+ // Pequeno delay para asegurar que Eel este listo
1380
+ setTimeout(() => {
1381
+ checkUserRoleAndHideNavigation();
1382
+ checkUsersPageAccess();
1383
+ }, 100);
1384
+ });
1385
+
1386
+ // Tambien ejecutar cuando cambie el estado de autenticacion
1387
+ function updateNavigationForUser() {
1388
+ checkUserRoleAndHideNavigation();
1389
+ }
1390
+
1391
+ // Funcion para mostrar informacion del usuario en el dropdown (opcional)
1392
+ function updateUserInfo() {
1393
+ api.getSession().then(function(response) {
1394
+ if (response.success && response.user) {
1395
+ const userDropdown = document.querySelector('#navbarDropdownMenuLink');
1396
+ if (userDropdown) {
1397
+ const roleBadge = response.user.role === 'Admin' ?
1398
+ '<span class="badge badge-primary badge-sm ml-1">Admin</span>' :
1399
+ '<span class="badge badge-secondary badge-sm ml-1">Doctor</span>';
1400
+ if (!userDropdown.querySelector('.badge')) userDropdown.innerHTML += roleBadge;
1401
+ }
1402
+ }
1403
+ });
1404
+ }
1405
+
1406
+ // Llamar tambien a updateUserInfo al cargar
1407
+ $(document).ready(function() {
1408
+ setTimeout(() => {
1409
+ updateUserInfo();
1410
+ }, 200);
1411
+ });
1412
+ </script>
1413
+ <script src="js/gauge.min.js"></script>
1414
+ <script src="js/jquery.sparkline.min.js"></script>
1415
+ <script src="js/apps.js"></script>
1416
+ </body>
1417
+ </html>
web/historial-consultas.html ADDED
@@ -0,0 +1,955 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <html lang="es">
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="Sistema de Gestión de Consultas">
7
+ <meta name="author" content="Sistema Médico">
8
+ <link rel="icon" href="favicon.ico">
9
+ <title>Consultas</title>
10
+
11
+ <!-- Simple bar CSS -->
12
+ <link rel="stylesheet" href="css/simplebar.css">
13
+ <!-- Fonts CSS -->
14
+ <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">
15
+ <!-- Icons CSS -->
16
+ <link rel="stylesheet" href="css/feather.css">
17
+ <link rel="stylesheet" href="css/select2.css">
18
+ <link rel="stylesheet" href="css/daterangepicker.css">
19
+ <!-- App 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
+
24
+ <body class="vertical light">
25
+ <div class="wrapper">
26
+ <!-- Top Navigation -->
27
+ <nav class="topnav navbar navbar-light">
28
+ <button type="button" class="navbar-toggler text-muted mt-2 p-0 mr-3 collapseSidebar">
29
+ <i class="fe fe-menu navbar-toggler-icon"></i>
30
+ </button>
31
+
32
+ <ul class="nav">
33
+ <!--
34
+ <li class="nav-item">
35
+ <a class="nav-link text-muted my-2" href="#" id="modeSwitcher" data-mode="light">
36
+ <i class="fe fe-sun fe-16"></i>
37
+ </a>
38
+ </li>
39
+ -->
40
+ <li class="nav-item dropdown">
41
+ <a class="nav-link dropdown-toggle text-muted pr-0" href="#" id="navbarDropdownMenuLink" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
42
+ <span class="avatar avatar-sm mt-2">
43
+ <img src="./assets/images/persona.png" alt="..." class="avatar-img rounded-circle">
44
+ </span>
45
+ </a>
46
+ <div class="dropdown-menu dropdown-menu-right" aria-labelledby="navbarDropdownMenuLink">
47
+
48
+ <a class="dropdown-item" href="#" id="logoutBtn">Cerrar Sesión</a>
49
+ </div>
50
+ </li>
51
+ </ul>
52
+ </nav>
53
+
54
+ <!-- Sidebar -->
55
+ <aside class="sidebar-left border-right bg-white shadow" id="leftSidebar" data-simplebar>
56
+ <a href="#" class="btn collapseSidebar toggle-btn d-lg-none text-muted ml-2 mt-3" data-toggle="toggle">
57
+ <i class="fe fe-x"><span class="sr-only"></span></i>
58
+ </a>
59
+ <nav class="vertnav navbar navbar-light">
60
+ <!-- nav bar -->
61
+ <div class="w-100 mb-4 d-flex">
62
+ <a class="navbar-brand mx-auto mt-2 flex-fill text-center">
63
+ <img src="assets/images/LOGO.png" alt="Logo">
64
+ </a>
65
+ </div>
66
+
67
+
68
+ <ul class="navbar-nav flex-fill w-100 mb-2">
69
+ <li class="nav-item">
70
+ <a class="nav-link" href="./index.html">
71
+ <i class="fe fe-home fe-16"></i>
72
+ <span class="ml-3 item-text">Dashboard</span>
73
+ </a>
74
+ </li>
75
+ <li class="nav-item">
76
+ <a class="nav-link" href="./diagnosis.html">
77
+ <i class="fe fe-eye fe-16"></i>
78
+ <span class="ml-3 item-text">Diagnóstico</span>
79
+ </a>
80
+ </li>
81
+ <li class="nav-item">
82
+ <a class="nav-link" href="./patients.html">
83
+ <i class="fe fe-users fe-16"></i>
84
+ <span class="ml-3 item-text">Pacientes</span>
85
+ </a>
86
+ </li>
87
+ <li class="nav-item active">
88
+ <a class="nav-link" href="./historial-consultas.html">
89
+ <i class="fe fe-file-text fe-16"></i>
90
+ <span class="ml-3 item-text">Consultas</span>
91
+ </a>
92
+ </li>
93
+ <li class="nav-item active">
94
+ <a class="nav-link" href="./users.html">
95
+ <i class="fe fe-user fe-16"></i>
96
+ <span class="ml-3 item-text">Usuarios</span>
97
+ </a>
98
+ </li>
99
+ </ul>
100
+
101
+ </nav>
102
+ </aside>
103
+
104
+ <!-- Main Content -->
105
+ <main role="main" class="main-content">
106
+ <div class="container-fluid">
107
+ <div class="row justify-content-center">
108
+ <div class="col-12">
109
+ <!-- Header -->
110
+ <div class="row align-items-center mb-2">
111
+ <div class="col">
112
+ <h2 class="h5 page-title">Gestión de Consultas</h2>
113
+ </div>
114
+ <div class="col-auto">
115
+ <button class="btn btn-primary" onclick="refreshConsultations()">
116
+ <i class="fe fe-refresh-cw fe-16 mr-2"></i>Actualizar
117
+ </button>
118
+ </div>
119
+ </div>
120
+
121
+ <!-- Statistics Cards -->
122
+ <div class="row mb-4">
123
+ <div class="col-md-3">
124
+ <div class="card border-0 shadow">
125
+ <div class="card-body">
126
+ <div class="row align-items-center">
127
+ <div class="col-3 text-center">
128
+ <span class="circle circle-sm bg-primary">
129
+ <i class="fe fe-clipboard fe-16 text-white"></i>
130
+ </span>
131
+ </div>
132
+ <div class="col pr-0">
133
+ <p class="small text-muted mb-0">Total Consultas</p>
134
+ <span class="h3 mb-0" id="totalConsultations">-</span>
135
+ </div>
136
+ </div>
137
+ </div>
138
+ </div>
139
+ </div>
140
+ <div class="col-md-3">
141
+ <div class="card border-0 shadow">
142
+ <div class="card-body">
143
+ <div class="row align-items-center">
144
+ <div class="col-3 text-center">
145
+ <span class="circle circle-sm bg-warning">
146
+ <i class="fe fe-alert-triangle fe-16 text-white"></i>
147
+ </span>
148
+ </div>
149
+ <div class="col pr-0">
150
+ <p class="small text-muted mb-0">Casos Positivos</p>
151
+ <span class="h3 mb-0" id="positiveCases">-</span>
152
+ </div>
153
+ </div>
154
+ </div>
155
+ </div>
156
+ </div>
157
+ <div class="col-md-3">
158
+ <div class="card border-0 shadow">
159
+ <div class="card-body">
160
+ <div class="row align-items-center">
161
+ <div class="col-3 text-center">
162
+ <span class="circle circle-sm bg-success">
163
+ <i class="fe fe-check fe-16 text-white"></i>
164
+ </span>
165
+ </div>
166
+ <div class="col pr-0">
167
+ <p class="small text-muted mb-0">Casos Negativos</p>
168
+ <span class="h3 mb-0" id="negativeCases">-</span>
169
+ </div>
170
+ </div>
171
+ </div>
172
+ </div>
173
+ </div>
174
+ <div class="col-md-3">
175
+ <div class="card border-0 shadow">
176
+ <div class="card-body">
177
+ <div class="row align-items-center">
178
+ <div class="col-3 text-center">
179
+ <span class="circle circle-sm bg-info">
180
+ <i class="fe fe-users fe-16 text-white"></i>
181
+ </span>
182
+ </div>
183
+ <div class="col pr-0">
184
+ <p class="small text-muted mb-0">Pacientes Únicos</p>
185
+ <span class="h3 mb-0" id="uniquePatients">-</span>
186
+ </div>
187
+ </div>
188
+ </div>
189
+ </div>
190
+ </div>
191
+ </div>
192
+
193
+ <!-- Filters and Search -->
194
+ <div class="card shadow mb-4">
195
+ <div class="card-header">
196
+ <h6 class="card-title mb-0">
197
+ <i class="fe fe-filter mr-2"></i>Filtros y Búsqueda
198
+ </h6>
199
+ </div>
200
+ <div class="card-body">
201
+ <div class="row">
202
+ <div class="col-md-4">
203
+ <div class="form-group">
204
+ <label for="searchInput">Buscar</label>
205
+ <input type="text" class="form-control" id="searchInput" placeholder="Buscar por paciente o notas...">
206
+ </div>
207
+ </div>
208
+ <div class="col-md-3">
209
+ <div class="form-group">
210
+ <label for="filterType">Tipo de Resultado</label>
211
+ <select class="form-control" id="filterType">
212
+ <option value="all">Todos</option>
213
+ <option value="positive">Positivos</option>
214
+ <option value="negative">Negativos</option>
215
+ </select>
216
+ </div>
217
+ </div>
218
+ <div class="col-md-3">
219
+ <div class="form-group">
220
+ <label for="perPageSelect">Resultados por página</label>
221
+ <select class="form-control" id="perPageSelect">
222
+ <option value="10">10</option>
223
+ <option value="25">25</option>
224
+ <option value="50">50</option>
225
+ <option value="100">100</option>
226
+ </select>
227
+ </div>
228
+ </div>
229
+ <div class="col-md-2">
230
+ <div class="form-group">
231
+ <label>&nbsp;</label>
232
+ <button type="button" class="btn btn-primary btn-block" onclick="applyFilters()">
233
+ <i class="fe fe-search mr-1"></i>Buscar
234
+ </button>
235
+ </div>
236
+ </div>
237
+ </div>
238
+ </div>
239
+ </div>
240
+
241
+ <!-- Consultations Table -->
242
+ <div class="card shadow">
243
+ <div class="card-header">
244
+ <h6 class="card-title mb-0">
245
+ <i class="fe fe-clipboard mr-2"></i>Lista de Consultas
246
+ <span class="badge badge-secondary ml-2" id="recordCount">0 registros</span>
247
+ </h6>
248
+ </div>
249
+ <div class="card-body">
250
+ <!-- Loading Spinner -->
251
+ <div id="loadingSpinner" class="text-center py-5">
252
+ <div class="spinner-border text-primary" role="status">
253
+ <span class="sr-only">Cargando...</span>
254
+ </div>
255
+ <p class="text-muted mt-2">Cargando consultas...</p>
256
+ </div>
257
+
258
+ <!-- Consultations Table -->
259
+ <div id="consultationsTableContainer" style="display: none;">
260
+ <div class="table-responsive">
261
+ <table class="table table-striped table-hover">
262
+ <thead class="thead-light">
263
+ <tr>
264
+ <th>Fecha</th>
265
+ <th>Paciente</th>
266
+ <th>Resultado</th>
267
+ <th>Confianza</th>
268
+ <th>Notas</th>
269
+ <th class="text-center">Acciones</th>
270
+ </tr>
271
+ </thead>
272
+ <tbody id="consultationsTableBody">
273
+ <!-- Datos se cargarán aquí dinámicamente -->
274
+ </tbody>
275
+ </table>
276
+ </div>
277
+
278
+ <!-- Pagination -->
279
+ <div class="row align-items-center mt-4">
280
+ <div class="col-md-6">
281
+ <p class="text-muted" id="paginationInfo">
282
+ Mostrando 0 de 0 resultados
283
+ </p>
284
+ </div>
285
+ <div class="col-md-6">
286
+ <nav aria-label="Paginación de consultas">
287
+ <ul class="pagination justify-content-end mb-0" id="paginationContainer">
288
+ <!-- Paginación se generará dinámicamente -->
289
+ </ul>
290
+ </nav>
291
+ </div>
292
+ </div>
293
+ </div>
294
+
295
+ <!-- Empty State -->
296
+ <div id="emptyState" class="text-center py-5" style="display: none;">
297
+ <i class="fe fe-inbox fe-48 text-muted mb-3"></i>
298
+ <h6 class="text-muted">No se encontraron consultas</h6>
299
+ <p class="text-muted">No hay consultas que coincidan con los filtros seleccionados.</p>
300
+ <button class="btn btn-outline-primary" onclick="clearFilters()">
301
+ <i class="fe fe-refresh-cw mr-1"></i>Limpiar Filtros
302
+ </button>
303
+ </div>
304
+ </div>
305
+ </div>
306
+ </div>
307
+ </div>
308
+ </div>
309
+ </main>
310
+ </div>
311
+
312
+ <!-- Modal para detalles de consulta -->
313
+ <div class="modal fade" id="consultationModal" tabindex="-1" role="dialog" aria-labelledby="consultationModalLabel" aria-hidden="true">
314
+ <div class="modal-dialog modal-lg" role="document">
315
+ <div class="modal-content">
316
+ <div class="modal-header">
317
+ <h5 class="modal-title" id="consultationModalLabel">
318
+ <i class="fe fe-eye mr-2"></i>Detalles de Consulta
319
+ </h5>
320
+ <button type="button" class="close" data-dismiss="modal" aria-label="Close">
321
+ <span aria-hidden="true">&times;</span>
322
+ </button>
323
+ </div>
324
+ <div class="modal-body" id="consultationModalBody">
325
+ <!-- Contenido se cargara dinamicamente -->
326
+ </div>
327
+ <div class="modal-footer">
328
+ <button type="button" class="btn btn-secondary" data-dismiss="modal">Cerrar</button>
329
+ <button type="button" class="btn btn-danger" id="deleteConsultationBtn" onclick="deleteConsultation()">
330
+ <i class="fe fe-trash-2 mr-1"></i>Eliminar
331
+ </button>
332
+ </div>
333
+ </div>
334
+ </div>
335
+ </div>
336
+
337
+ <!-- Scripts -->
338
+ <script src="js/jquery.min.js"></script>
339
+ <script src="js/popper.min.js"></script>
340
+ <script src="js/moment.min.js"></script>
341
+ <script src="js/bootstrap.min.js"></script>
342
+ <script src="js/simplebar.min.js"></script>
343
+ <script src="js/daterangepicker.js"></script>
344
+ <script src="js/select2.min.js"></script>
345
+ <script src="js/config.js"></script>
346
+ <script src="api.js"></script>
347
+
348
+ <script>
349
+ $(document).ready(function() {
350
+ // Variables globales
351
+ let currentPage = 1;
352
+ let currentPerPage = 10;
353
+ let currentSearch = '';
354
+ let currentFilter = 'all';
355
+ let currentConsultationId = null;
356
+
357
+ // Inicializar pagina
358
+ initializePage();
359
+
360
+ // Event listeners
361
+ setupEventListeners();
362
+
363
+ function initializePage() {
364
+ console.log('Inicializando página de consultas...');
365
+
366
+ // Verificar autenticación
367
+ checkAuthentication();
368
+
369
+ // Cargar estadísticas
370
+ loadSummaryStats();
371
+
372
+ // Cargar consultas inicial
373
+ loadConsultations();
374
+ }
375
+
376
+ function checkAuthentication() {
377
+ api.getSession().then(function(response) {
378
+ if (!response.success) window.location.href = 'auth-login.html';
379
+ }).catch(() => { window.location.href = 'auth-login.html'; });
380
+ }
381
+
382
+ function setupEventListeners() {
383
+ // Búsqueda en tiempo real (con debounce)
384
+ let searchTimeout;
385
+ $('#searchInput').on('input', function() {
386
+ clearTimeout(searchTimeout);
387
+ searchTimeout = setTimeout(() => {
388
+ currentSearch = $(this).val();
389
+ currentPage = 1;
390
+ loadConsultations();
391
+ }, 500);
392
+ });
393
+
394
+ // Filtro por tipo
395
+ $('#filterType').on('change', function() {
396
+ currentFilter = $(this).val();
397
+ currentPage = 1;
398
+ loadConsultations();
399
+ });
400
+
401
+ // Resultados por página
402
+ $('#perPageSelect').on('change', function() {
403
+ currentPerPage = parseInt($(this).val());
404
+ currentPage = 1;
405
+ loadConsultations();
406
+ });
407
+
408
+ // Logout
409
+ $('#logoutBtn').on('click', function(e) {
410
+ e.preventDefault();
411
+ executeLogout();
412
+ });
413
+
414
+ // Tecla Enter en búsqueda
415
+ $('#searchInput').on('keypress', function(e) {
416
+ if (e.which === 13) {
417
+ applyFilters();
418
+ }
419
+ });
420
+ }
421
+
422
+ function loadSummaryStats() {
423
+ api.getDashboardStats().then(function(response) {
424
+ if (response.success) {
425
+ const s = response.stats;
426
+ $('#totalConsultations').text(s.total_consultations || 0);
427
+ $('#positiveCases').text(s.positive_cases || 0);
428
+ $('#negativeCases').text(s.negative_cases || 0);
429
+ $('#uniquePatients').text(s.total_patients || 0);
430
+ }
431
+ });
432
+ }
433
+
434
+ function loadConsultations() {
435
+ console.log(`Cargando consultas - Página: ${currentPage}, Por página: ${currentPerPage}`);
436
+
437
+ // Mostrar loading
438
+ $('#loadingSpinner').show();
439
+ $('#consultationsTableContainer').hide();
440
+ $('#emptyState').hide();
441
+
442
+ api.getConsultations(currentPage, currentPerPage, currentSearch, currentFilter).then(function(response) {
443
+ $('#loadingSpinner').hide();
444
+
445
+ if (response.success) {
446
+ if (response.consultations.length > 0) {
447
+ renderConsultationsTable(response.consultations);
448
+ renderPagination(response.pagination);
449
+ updateRecordCount(response.pagination);
450
+ $('#consultationsTableContainer').show();
451
+ } else {
452
+ $('#emptyState').show();
453
+ }
454
+ } else {
455
+ console.error('Error cargando consultas:', response.error);
456
+ showAlert('error', 'Error cargando consultas: ' + response.error);
457
+ $('#emptyState').show();
458
+ }
459
+ });
460
+ }
461
+
462
+ function renderConsultationsTable(consultations) {
463
+ const tbody = $('#consultationsTableBody');
464
+ tbody.empty();
465
+
466
+ consultations.forEach(consultation => {
467
+ const date = consultation.consultationDate ?
468
+ new Date(consultation.consultationDate).toLocaleString('es-ES') : 'N/A';
469
+
470
+ const isPositive = consultation.diabeticRetinopathy;
471
+ const resultClass = isPositive ? 'badge-warning' : 'badge-success';
472
+ const resultText = isPositive ? 'Positivo' : 'Negativo';
473
+ const resultIcon = isPositive ? 'fe-alert-triangle' : 'fe-check';
474
+
475
+ const confidence = consultation.confidence ?
476
+ `${consultation.confidence.toFixed(1)}%` : 'N/A';
477
+
478
+ const notes = consultation.notes ?
479
+ (consultation.notes.length > 50 ?
480
+ consultation.notes.substring(0, 50) + '...' :
481
+ consultation.notes) :
482
+ '<em class="text-muted">Sin notas</em>';
483
+
484
+ const row = `
485
+ <tr>
486
+ <td>
487
+ <small class="text-muted">${date}</small>
488
+ </td>
489
+ <td>
490
+ <strong>${consultation.patient_name || 'Desconocido'}</strong>
491
+ </td>
492
+ <td>
493
+ <span class="badge ${resultClass}">
494
+ <i class="fe ${resultIcon} mr-1"></i>${resultText}
495
+ </span>
496
+ </td>
497
+ <td>
498
+ <span class="text-${isPositive ? 'warning' : 'success'}">${confidence}</span>
499
+ </td>
500
+ <td>
501
+ <small>${notes}</small>
502
+ </td>
503
+ <td class="text-center">
504
+ <div class="btn-group" role="group">
505
+ <button class="btn btn-sm btn-outline-primary"
506
+ onclick="viewConsultationDetails(${consultation.consultationID})"
507
+ title="Ver detalles">
508
+ <i class="fe fe-eye fe-12"></i>
509
+ </button>
510
+ <button class="btn btn-sm btn-outline-danger"
511
+ onclick="confirmDeleteConsultation(${consultation.consultationID})"
512
+ title="Eliminar">
513
+ <i class="fe fe-trash-2 fe-12"></i>
514
+ </button>
515
+ </div>
516
+ </td>
517
+ </tr>
518
+ `;
519
+
520
+ tbody.append(row);
521
+ });
522
+ }
523
+
524
+ function renderPagination(pagination) {
525
+ const container = $('#paginationContainer');
526
+ container.empty();
527
+
528
+ if (pagination.total_pages <= 1) {
529
+ return;
530
+ }
531
+
532
+ // Boton anterior
533
+ const prevDisabled = !pagination.has_previous ? 'disabled' : '';
534
+ container.append(`
535
+ <li class="page-item ${prevDisabled}">
536
+ <a class="page-link" href="#" onclick="changePage(${pagination.current_page - 1})" ${prevDisabled ? 'tabindex="-1"' : ''}>
537
+ <i class="fe fe-chevron-left"></i>
538
+ </a>
539
+ </li>
540
+ `);
541
+
542
+ // Paginas
543
+ const startPage = Math.max(1, pagination.current_page - 2);
544
+ const endPage = Math.min(pagination.total_pages, pagination.current_page + 2);
545
+
546
+ // Primera pagina
547
+ if (startPage > 1) {
548
+ container.append(`
549
+ <li class="page-item">
550
+ <a class="page-link" href="#" onclick="changePage(1)">1</a>
551
+ </li>
552
+ `);
553
+ if (startPage > 2) {
554
+ container.append('<li class="page-item disabled"><span class="page-link">...</span></li>');
555
+ }
556
+ }
557
+
558
+ // Paginas del rango
559
+ for (let i = startPage; i <= endPage; i++) {
560
+ const activeClass = i === pagination.current_page ? 'active' : '';
561
+ container.append(`
562
+ <li class="page-item ${activeClass}">
563
+ <a class="page-link" href="#" onclick="changePage(${i})">${i}</a>
564
+ </li>
565
+ `);
566
+ }
567
+
568
+ // Ultima pagina
569
+ if (endPage < pagination.total_pages) {
570
+ if (endPage < pagination.total_pages - 1) {
571
+ container.append('<li class="page-item disabled"><span class="page-link">...</span></li>');
572
+ }
573
+ container.append(`
574
+ <li class="page-item">
575
+ <a class="page-link" href="#" onclick="changePage(${pagination.total_pages})">${pagination.total_pages}</a>
576
+ </li>
577
+ `);
578
+ }
579
+
580
+ // Boton siguiente
581
+ const nextDisabled = !pagination.has_next ? 'disabled' : '';
582
+ container.append(`
583
+ <li class="page-item ${nextDisabled}">
584
+ <a class="page-link" href="#" onclick="changePage(${pagination.current_page + 1})" ${nextDisabled ? 'tabindex="-1"' : ''}>
585
+ <i class="fe fe-chevron-right"></i>
586
+ </a>
587
+ </li>
588
+ `);
589
+ }
590
+
591
+ function updateRecordCount(pagination) {
592
+ const start = (pagination.current_page - 1) * pagination.per_page + 1;
593
+ const end = Math.min(pagination.current_page * pagination.per_page, pagination.total_records);
594
+
595
+ $('#recordCount').text(`${pagination.total_records} registros`);
596
+ $('#paginationInfo').text(`Mostrando ${start} - ${end} de ${pagination.total_records} resultados`);
597
+ }
598
+
599
+ function viewConsultationDetails(consultationId) {
600
+ console.log(`Viendo detalles de consulta: ${consultationId}`);
601
+ currentConsultationId = consultationId;
602
+
603
+ api.getConsultations(1, 100).then(function(response) {
604
+ const consultation = (response.consultations || []).find(c => c.consultationID === consultationId);
605
+ if (consultation) {
606
+ renderConsultationModal(consultation, []);
607
+ $('#consultationModal').modal('show');
608
+ } else {
609
+ showAlert('error', 'Consulta no encontrada');
610
+ }
611
+ });
612
+ }
613
+
614
+ function renderConsultationModal(consultation, riskFactors) {
615
+ const isPositive = consultation.diabeticRetinopathy;
616
+ const resultClass = isPositive ? 'alert-warning' : 'alert-success';
617
+ const resultIcon = isPositive ? 'fe-alert-triangle' : 'fe-check-circle';
618
+ const resultText = isPositive ? 'Retinopatía Diabética Detectada' : 'No se Detectó Retinopatía Diabética';
619
+
620
+ const date = consultation.consultationDate ?
621
+ new Date(consultation.consultationDate).toLocaleString('es-ES') : 'N/A';
622
+
623
+ const confidence = consultation.confidence ?
624
+ `${consultation.confidence.toFixed(1)}%` : 'N/A';
625
+
626
+ // Calcular edad si hay fecha de nacimiento
627
+ let age = 'N/A';
628
+ if (consultation.birthDate) {
629
+ try {
630
+ const birthDate = new Date(consultation.birthDate);
631
+ const today = new Date();
632
+ age = today.getFullYear() - birthDate.getFullYear();
633
+ if (today.getMonth() < birthDate.getMonth() ||
634
+ (today.getMonth() === birthDate.getMonth() && today.getDate() < birthDate.getDate())) {
635
+ age--;
636
+ }
637
+ age += ' años';
638
+ } catch (e) {
639
+ age = 'N/A';
640
+ }
641
+ }
642
+
643
+ // Factores de riesgo
644
+ let riskFactorsHtml = '';
645
+ if (riskFactors && riskFactors.length > 0) {
646
+ riskFactorsHtml = riskFactors.map(rf =>
647
+ `<span class="badge badge-secondary mr-1 mb-1">${rf.name}</span>`
648
+ ).join('');
649
+ } else {
650
+ riskFactorsHtml = '<span class="text-muted">No se han registrado factores de riesgo</span>';
651
+ }
652
+
653
+ const modalContent = `
654
+ <div class="row">
655
+ <div class="col-md-6">
656
+ <h6 class="mb-3">
657
+ <i class="fe fe-user mr-2"></i>Información del Paciente
658
+ </h6>
659
+ <table class="table table-sm">
660
+ <tr>
661
+ <td><strong>Nombre:</strong></td>
662
+ <td>${consultation.patient_name || 'N/A'}</td>
663
+ </tr>
664
+ <tr>
665
+ <td><strong>Edad:</strong></td>
666
+ <td>${age}</td>
667
+ </tr>
668
+ <tr>
669
+ <td><strong>Género:</strong></td>
670
+ <td>${consultation.gender || 'N/A'}</td>
671
+ </tr>
672
+ <tr>
673
+ <td><strong>Tipo Diabetes:</strong></td>
674
+ <td>${consultation.diabetesType || 'N/A'}</td>
675
+ </tr>
676
+ </table>
677
+ </div>
678
+ <div class="col-md-6">
679
+ <h6 class="mb-3">
680
+ <i class="fe fe-clipboard mr-2"></i>Información de la Consulta
681
+ </h6>
682
+ <table class="table table-sm">
683
+ <tr>
684
+ <td><strong>Fecha:</strong></td>
685
+ <td>${date}</td>
686
+ </tr>
687
+ <tr>
688
+ <td><strong>ID Consulta:</strong></td>
689
+ <td>#${consultation.consultationID}</td>
690
+ </tr>
691
+ <tr>
692
+ <td><strong>Confianza:</strong></td>
693
+ <td>${confidence}</td>
694
+ </tr>
695
+ <tr>
696
+ <td><strong>Imagen:</strong></td>
697
+ <td>${consultation.imagePath || 'N/A'}</td>
698
+ </tr>
699
+ </table>
700
+ </div>
701
+ </div>
702
+
703
+ <div class="row mt-4">
704
+ <div class="col-12">
705
+ <div class="alert ${resultClass} d-flex align-items-center">
706
+ <i class="fe ${resultIcon} fe-24 mr-3"></i>
707
+ <div>
708
+ <h6 class="mb-1">${resultText}</h6>
709
+ <p class="mb-0">Confianza del diagnóstico: <strong>${confidence}</strong></p>
710
+ </div>
711
+ </div>
712
+ </div>
713
+ </div>
714
+
715
+ ${consultation.notes ? `
716
+ <div class="row">
717
+ <div class="col-12">
718
+ <h6 class="mb-2">
719
+ <i class="fe fe-edit-2 mr-2"></i>Notas de la Consulta
720
+ </h6>
721
+ <div class="card bg-light">
722
+ <div class="card-body">
723
+ <p class="mb-0">${consultation.notes}</p>
724
+ </div>
725
+ </div>
726
+ </div>
727
+ </div>
728
+ ` : ''}
729
+ `;
730
+
731
+ $('#consultationModalBody').html(modalContent);
732
+ }
733
+
734
+ function confirmDeleteConsultation(consultationId) {
735
+ if (confirm('¿Estás seguro que deseas eliminar esta consulta? Esta acción no se puede deshacer.')) {
736
+ currentConsultationId = consultationId;
737
+ deleteConsultation();
738
+ }
739
+ }
740
+
741
+ function deleteConsultation() {
742
+ if (!currentConsultationId) {
743
+ showAlert('error', 'No se ha seleccionado ninguna consulta');
744
+ return;
745
+ }
746
+
747
+ fetch('/api/consultations/' + currentConsultationId, {method:'DELETE',credentials:'same-origin'})
748
+ .then(r => r.json()).then(function(response) {
749
+ if (response.success) {
750
+ showAlert('success', 'Consulta eliminada exitosamente');
751
+ $('#consultationModal').modal('hide');
752
+ loadConsultations();
753
+ loadSummaryStats();
754
+ } else {
755
+ showAlert('error', 'Error eliminando consulta: ' + (response.error||response.message));
756
+ }
757
+ });
758
+ }
759
+
760
+ function changePage(page) {
761
+ if (page >= 1) {
762
+ currentPage = page;
763
+ loadConsultations();
764
+ }
765
+ }
766
+
767
+ function applyFilters() {
768
+ currentSearch = $('#searchInput').val();
769
+ currentFilter = $('#filterType').val();
770
+ currentPerPage = parseInt($('#perPageSelect').val());
771
+ currentPage = 1;
772
+ loadConsultations();
773
+ }
774
+
775
+ function clearFilters() {
776
+ $('#searchInput').val('');
777
+ $('#filterType').val('all');
778
+ $('#perPageSelect').val('10');
779
+ currentSearch = '';
780
+ currentFilter = 'all';
781
+ currentPerPage = 10;
782
+ currentPage = 1;
783
+ loadConsultations();
784
+ }
785
+
786
+ function refreshConsultations() {
787
+ loadConsultations();
788
+ loadSummaryStats();
789
+ showAlert('success', 'Datos actualizados correctamente');
790
+ }
791
+
792
+ function showAlert(type, message) {
793
+ const alertClass = {
794
+ 'success': 'alert-success',
795
+ 'error': 'alert-danger',
796
+ 'warning': 'alert-warning',
797
+ 'info': 'alert-info'
798
+ }[type] || 'alert-info';
799
+
800
+ const alertHtml = `
801
+ <div class="alert ${alertClass} alert-dismissible fade show" role="alert">
802
+ ${message}
803
+ <button type="button" class="close" data-dismiss="alert">
804
+ <span aria-hidden="true">&times;</span>
805
+ </button>
806
+ </div>
807
+ `;
808
+
809
+ $('.container-fluid').prepend(alertHtml);
810
+
811
+ setTimeout(() => {
812
+ $('.alert').first().alert('close');
813
+ }, 5000);
814
+ }
815
+
816
+ // Funciones globales para uso en HTML
817
+ window.viewConsultationDetails = viewConsultationDetails;
818
+ window.confirmDeleteConsultation = confirmDeleteConsultation;
819
+ window.deleteConsultation = deleteConsultation;
820
+ window.changePage = changePage;
821
+ window.applyFilters = applyFilters;
822
+ window.clearFilters = clearFilters;
823
+ window.refreshConsultations = refreshConsultations;
824
+ });
825
+ </script>
826
+ <script>
827
+ function confirmLogout() {
828
+ // Sin confirm() para evitar el mensaje del navegador
829
+ executeLogout();
830
+ }
831
+
832
+ function executeLogout() {
833
+ api.logout().then(() => { window.location.href = 'auth-login.html'; });
834
+ }
835
+ </script>
836
+
837
+
838
+ <script>
839
+ // Centrar ventana al cargar página
840
+ (function() {
841
+ function centerWindow() {
842
+ try {
843
+ const screenWidth = window.screen.availWidth;
844
+ const screenHeight = window.screen.availHeight;
845
+ const x = Math.floor((screenWidth - 1200) / 2);
846
+ const y = Math.floor((screenHeight - 800) / 2);
847
+
848
+ if (window.moveTo) {
849
+ window.moveTo(Math.max(0, x), Math.max(0, y));
850
+ }
851
+ } catch (e) {
852
+ console.warn('No se pudo centrar ventana:', e);
853
+ }
854
+ }
855
+
856
+ // Centrar al cargar
857
+ if (document.readyState === 'loading') {
858
+ document.addEventListener('DOMContentLoaded', centerWindow);
859
+ } else {
860
+ centerWindow();
861
+ }
862
+
863
+ window.addEventListener('load', centerWindow);
864
+ window.addEventListener('focus', centerWindow);
865
+ })();
866
+ </script>
867
+
868
+
869
+
870
+
871
+ <script>
872
+ function checkUserRoleAndHideNavigation() {
873
+ api.getSession().then(function(response) {
874
+ if (response.success && response.user) {
875
+ const userRole = response.user.role;
876
+ const usersNavItem = document.querySelector('a[href="./users.html"]');
877
+
878
+ if (usersNavItem) {
879
+ const navItemContainer = usersNavItem.closest('li.nav-item');
880
+
881
+ if (userRole === 'Admin') {
882
+ // Usuario es Admin - mostrar elemento
883
+ if (navItemContainer) {
884
+ navItemContainer.style.display = 'block';
885
+ }
886
+ console.log('Navegacion de usuarios visible para administrador:', response.user.username);
887
+ } else {
888
+ // Usuario NO es Admin - ocultar elemento
889
+ if (navItemContainer) {
890
+ navItemContainer.style.display = 'none';
891
+ }
892
+ console.log('Navegacion de usuarios oculta para usuario:', response.user.username, '(Rol:', userRole + ')');
893
+ }
894
+ } else {
895
+ console.warn('No se encontró el elemento de navegación de usuarios');
896
+ }
897
+ } else {
898
+ console.warn('No se pudo obtener información del usuario actual');
899
+ }
900
+ });
901
+ }
902
+
903
+ // Funcion para verificar si el usuario actual está intentando acceder a usuarios.html
904
+ function checkUsersPageAccess() {
905
+ // Solo ejecutar en la pagina de usuarios
906
+ if (window.location.pathname.includes('users.html') || window.location.href.includes('users.html')) {
907
+ api.getSession().then(function(response) {
908
+ if (!response.success) { window.location.href = 'auth-login.html'; return; }
909
+ if (response.user.role !== 'Admin') { window.location.href = 'index.html'; }
910
+ });
911
+ }
912
+ }
913
+
914
+ // Ejecutar al cargar la página
915
+ $(document).ready(function() {
916
+ // Pequeno delay para asegurar que Eel esté listo
917
+ setTimeout(() => {
918
+ checkUserRoleAndHideNavigation();
919
+ checkUsersPageAccess();
920
+ }, 100);
921
+ });
922
+
923
+ // Tambien ejecutar cuando cambie el estado de autenticacion
924
+ function updateNavigationForUser() {
925
+ checkUserRoleAndHideNavigation();
926
+ }
927
+
928
+ // Funcion para mostrar informacion del usuario en el dropdown (opcional)
929
+ function updateUserInfo() {
930
+ api.getSession().then(function(response) {
931
+ if (response.success && response.user) {
932
+ const userDropdown = document.querySelector('#navbarDropdownMenuLink');
933
+ if (userDropdown) {
934
+ const roleBadge = response.user.role === 'Admin' ?
935
+ '<span class="badge badge-primary badge-sm ml-1">Admin</span>' :
936
+ '<span class="badge badge-secondary badge-sm ml-1">Doctor</span>';
937
+ if (!userDropdown.querySelector('.badge')) userDropdown.innerHTML += roleBadge;
938
+ }
939
+ }
940
+ });
941
+ }
942
+
943
+ // Llamar tambien a updateUserInfo al cargar
944
+ $(document).ready(function() {
945
+ setTimeout(() => {
946
+ updateUserInfo();
947
+ }, 200);
948
+ });
949
+ </script>
950
+
951
+
952
+ <!-- App scripts -->
953
+ <script src="js/apps.js"></script>
954
+ </body>
955
+ </html>
web/patients.html ADDED
@@ -0,0 +1,1237 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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>Gestión de Pacientes</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
+
23
+ <style>
24
+ .patient-card {
25
+ transition: all 0.3s ease;
26
+ border-radius: 12px;
27
+ }
28
+
29
+ .patient-card:hover {
30
+ transform: translateY(-2px);
31
+ box-shadow: 0 8px 25px rgba(0,0,0,0.1);
32
+ }
33
+
34
+ .search-section {
35
+ color: white;
36
+ border-radius: 12px;
37
+ padding: 2rem;
38
+ margin-bottom: 2rem;
39
+
40
+
41
+ background-color: #f5f5f5;
42
+
43
+
44
+ }
45
+
46
+ .search-input {
47
+ border: none;
48
+ border-radius: 25px;
49
+ padding: 12px 20px;
50
+ font-size: 16px;
51
+ box-shadow: 0 4px 15px rgba(0,0,0,0.1);
52
+ }
53
+
54
+ .patient-avatar {
55
+ width: 60px;
56
+ height: 60px;
57
+ border-radius: 50%;
58
+ display: flex;
59
+ align-items: center;
60
+ justify-content: center;
61
+ font-size: 24px;
62
+ font-weight: bold;
63
+ color: white;
64
+ margin-right: 15px;
65
+ }
66
+
67
+ .diabetes-badge {
68
+ font-size: 0.75rem;
69
+ padding: 4px 8px;
70
+ border-radius: 12px;
71
+ }
72
+
73
+ .form-wizard {
74
+ background: white;
75
+ border-radius: 12px;
76
+ overflow: hidden;
77
+ box-shadow: 0 4px 20px rgba(0,0,0,0.1);
78
+ }
79
+
80
+ .wizard-step {
81
+ display: none;
82
+ }
83
+
84
+ .wizard-step.active {
85
+ display: block;
86
+ animation: fadeIn 0.5s ease;
87
+ }
88
+
89
+ @keyframes fadeIn {
90
+ from { opacity: 0; transform: translateX(20px); }
91
+ to { opacity: 1; transform: translateX(0); }
92
+ }
93
+
94
+ .step-indicator {
95
+ background: #f8f9fa;
96
+ padding: 1rem;
97
+ border-bottom: 1px solid #dee2e6;
98
+ }
99
+
100
+ .step-item {
101
+ display: inline-block;
102
+ padding: 8px 16px;
103
+ margin-right: 10px;
104
+ border-radius: 20px;
105
+ background: #e9ecef;
106
+ color: #6c757d;
107
+ font-size: 14px;
108
+ transition: all 0.3s ease;
109
+ }
110
+
111
+ .step-item.active {
112
+ background: #007bff;
113
+ color: white;
114
+ }
115
+
116
+ .step-item.completed {
117
+ background: #28a745;
118
+ color: white;
119
+ }
120
+
121
+ .patient-stats {
122
+ background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%);
123
+ color: white;
124
+ border-radius: 12px;
125
+ padding: 1.5rem;
126
+ }
127
+
128
+ .stats-item {
129
+ text-align: center;
130
+ }
131
+
132
+ .stats-number {
133
+ font-size: 2rem;
134
+ font-weight: bold;
135
+ margin-bottom: 5px;
136
+ }
137
+
138
+ .filter-chip {
139
+ display: inline-block;
140
+ background: #e9ecef;
141
+ color: #495057;
142
+ padding: 6px 12px;
143
+ border-radius: 15px;
144
+ font-size: 14px;
145
+ margin-right: 8px;
146
+ margin-bottom: 8px;
147
+ cursor: pointer;
148
+ transition: all 0.3s ease;
149
+ }
150
+
151
+ .filter-chip.active {
152
+ background: #007bff;
153
+ color: white;
154
+ }
155
+
156
+ .filter-chip:hover {
157
+ background: #6c757d;
158
+ color: white;
159
+ }
160
+
161
+ .risk-factor-tag {
162
+ display: inline-block;
163
+ background: #ffc107;
164
+ color: #212529;
165
+ padding: 2px 8px;
166
+ border-radius: 10px;
167
+ font-size: 12px;
168
+ margin: 2px;
169
+ }
170
+
171
+ .empty-state {
172
+ text-align: center;
173
+ padding: 3rem;
174
+ color: #6c757d;
175
+ }
176
+
177
+ .empty-state i {
178
+ font-size: 4rem;
179
+ margin-bottom: 1rem;
180
+ opacity: 0.5;
181
+ }
182
+ </style>
183
+ </head>
184
+ <body class="vertical light">
185
+ <div class="wrapper">
186
+ <nav class="topnav navbar navbar-light">
187
+ <button type="button" class="navbar-toggler text-muted mt-2 p-0 mr-3 collapseSidebar">
188
+ <i class="fe fe-menu navbar-toggler-icon"></i>
189
+ </button>
190
+
191
+ <ul class="nav">
192
+ <li class="nav-item dropdown">
193
+ <a class="nav-link dropdown-toggle text-muted pr-0" href="#" id="navbarDropdownMenuLink" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
194
+ <span class="avatar avatar-sm mt-2">
195
+ <img src="./assets/images/persona.png" alt="..." class="avatar-img rounded-circle">
196
+ </span>
197
+ </a>
198
+ <div class="dropdown-menu dropdown-menu-right" aria-labelledby="navbarDropdownMenuLink">
199
+ <a class="dropdown-item" href="#" onclick="confirmLogout()">Cerrar Sesión</a>
200
+ </div>
201
+ </li>
202
+ </ul>
203
+ </nav>
204
+
205
+ <!-- Sidebar -->
206
+ <aside class="sidebar-left border-right bg-white shadow" id="leftSidebar" data-simplebar>
207
+ <a href="#" class="btn collapseSidebar toggle-btn d-lg-none text-muted ml-2 mt-3" data-toggle="toggle">
208
+ <i class="fe fe-x"><span class="sr-only"></span></i>
209
+ </a>
210
+ <nav class="vertnav navbar navbar-light">
211
+ <!-- nav bar -->
212
+ <div class="w-100 mb-4 d-flex">
213
+ <a class="navbar-brand mx-auto mt-2 flex-fill text-center">
214
+ <img src="assets/images/LOGO.png" alt="Logo">
215
+ </a>
216
+ </div>
217
+
218
+
219
+ <ul class="navbar-nav flex-fill w-100 mb-2">
220
+ <li class="nav-item">
221
+ <a class="nav-link" href="./index.html">
222
+ <i class="fe fe-home fe-16"></i>
223
+ <span class="ml-3 item-text">Dashboard</span>
224
+ </a>
225
+ </li>
226
+ <li class="nav-item">
227
+ <a class="nav-link" href="./diagnosis.html">
228
+ <i class="fe fe-eye fe-16"></i>
229
+ <span class="ml-3 item-text">Diagnóstico</span>
230
+ </a>
231
+ </li>
232
+ <li class="nav-item active">
233
+ <a class="nav-link" href="./patients.html">
234
+ <i class="fe fe-users fe-16"></i>
235
+ <span class="ml-3 item-text">Pacientes</span>
236
+ </a>
237
+ </li>
238
+ <li class="nav-item">
239
+ <a class="nav-link" href="./historial-consultas.html">
240
+ <i class="fe fe-file-text fe-16"></i>
241
+ <span class="ml-3 item-text">Consultas</span>
242
+ </a>
243
+ </li>
244
+ <li class="nav-item active">
245
+ <a class="nav-link" href="./users.html">
246
+ <i class="fe fe-user fe-16"></i>
247
+ <span class="ml-3 item-text">Usuarios</span>
248
+ </a>
249
+ </li>
250
+ </ul>
251
+
252
+ </nav>
253
+ </aside>
254
+
255
+
256
+ <!-- Contenido principal -->
257
+ <main role="main" class="main-content">
258
+ <div class="container-fluid">
259
+ <div class="row justify-content-center">
260
+ <div class="col-12">
261
+ <!-- Header -->
262
+ <div class="row align-items-center mb-4">
263
+ <div class="col">
264
+ <h2 class="h3 page-title">
265
+ <i class="fe fe-users fe-24 mr-2 text-primary"></i>
266
+ Gestión de Pacientes
267
+ </h2>
268
+ <p class="text-muted">Administra la información de tus pacientes de manera eficiente</p>
269
+ </div>
270
+ <div class="col-auto">
271
+ <button class="btn btn-primary" onclick="showNewPatientWizard()">
272
+ <i class="fe fe-plus fe-16 mr-2"></i>Nuevo Paciente
273
+ </button>
274
+ </div>
275
+ </div>
276
+
277
+ <!-- Estadísticas rápidas -->
278
+ <div class="row mb-4">
279
+ <div class="col-lg-3 col-md-6">
280
+ <div class="card shadow mb-4">
281
+ <div class="card-body">
282
+ <div class="stats-item">
283
+ <div class="stats-number text-primary" id="totalPatients">0</div>
284
+ <div class="text-muted">Total Pacientes</div>
285
+ </div>
286
+ </div>
287
+ </div>
288
+ </div>
289
+ <div class="col-lg-3 col-md-6">
290
+ <div class="card shadow mb-4">
291
+ <div class="card-body">
292
+ <div class="stats-item">
293
+ <div class="stats-number text-warning" id="diabetesType1">0</div>
294
+ <div class="text-muted">Diabetes Tipo 1</div>
295
+ </div>
296
+ </div>
297
+ </div>
298
+ </div>
299
+ <div class="col-lg-3 col-md-6">
300
+ <div class="card shadow mb-4">
301
+ <div class="card-body">
302
+ <div class="stats-item">
303
+ <div class="stats-number text-info" id="diabetesType2">0</div>
304
+ <div class="text-muted">Diabetes Tipo 2</div>
305
+ </div>
306
+ </div>
307
+ </div>
308
+ </div>
309
+ <div class="col-lg-3 col-md-6">
310
+ <div class="card shadow mb-4">
311
+ <div class="card-body">
312
+ <div class="stats-item">
313
+ <div class="stats-number text-success" id="recentConsultations">0</div>
314
+ <div class="text-muted">Consultas Recientes</div>
315
+ </div>
316
+ </div>
317
+ </div>
318
+ </div>
319
+ </div>
320
+
321
+ <!-- Sección de búsqueda y filtros -->
322
+ <div class="search-section">
323
+ <div class="row align-items-center" style="background-color: #f5f5f5; padding: 15px; border-radius: 8px;">
324
+ <div class="col-lg-6">
325
+ <h5 class="mb-3">
326
+ <i class="fe fe-search mr-2"></i>Buscar Pacientes
327
+ </h5>
328
+ <input type="text" class="form-control search-input" id="patientSearchInput" placeholder="Buscar por nombre, ID o tipo de diabetes...">
329
+ </div>
330
+ <div class="col-lg-6">
331
+ <h6 class="mb-2">Filtros rápidos:</h6>
332
+ <div class="filter-chips">
333
+ <span class="filter-chip active" data-filter="all">Todos</span>
334
+ <span class="filter-chip" data-filter="tipo1">Tipo 1</span>
335
+ <span class="filter-chip" data-filter="tipo2">Tipo 2</span>
336
+ <span class="filter-chip" data-filter="recientes">Recientes</span>
337
+ <span class="filter-chip" data-filter="masculino">Masculino</span>
338
+ <span class="filter-chip" data-filter="femenino">Femenino</span>
339
+ </div>
340
+ </div>
341
+ </div>
342
+ </div>
343
+
344
+ <!-- Lista de pacientes -->
345
+ <div class="row" id="patientsContainer">
346
+ <!-- Los pacientes se cargarán aquí dinámicamente -->
347
+ </div>
348
+
349
+ <div id="emptyState" class="empty-state" style="display: none;">
350
+ <i class="fe fe-users"></i>
351
+ <h4>No hay pacientes registrados</h4>
352
+ <p class="text-muted">Comienza agregando tu primer paciente al sistema</p>
353
+ <button class="btn btn-primary" onclick="showNewPatientWizard()">
354
+ <i class="fe fe-plus mr-2"></i>Agregar Primer Paciente
355
+ </button>
356
+ </div>
357
+ </div>
358
+ </div>
359
+ </div>
360
+ </main>
361
+ </div>
362
+
363
+ <!-- Modal para nuevo paciente -->
364
+ <div class="modal fade" id="newPatientModal" tabindex="-1" role="dialog">
365
+ <div class="modal-dialog modal-lg" role="document">
366
+ <div class="modal-content form-wizard">
367
+ <div class="modal-header step-indicator">
368
+ <h5 class="modal-title">
369
+ <i class="fe fe-user-plus mr-2"></i>Nuevo Paciente
370
+ </h5>
371
+ <div class="ml-auto">
372
+ <span class="step-item active" data-step="1">1. Información Personal</span>
373
+ </div>
374
+ <button type="button" class="close ml-3" data-dismiss="modal">
375
+ <span aria-hidden="true">&times;</span>
376
+ </button>
377
+ </div>
378
+
379
+ <div class="modal-body">
380
+ <form id="newPatientForm">
381
+ <!-- Paso 1: Información Personal -->
382
+ <div class="wizard-step active" data-step="1">
383
+ <h6 class="mb-4">Información Personal del Paciente</h6>
384
+
385
+ <div class="form-group">
386
+ <label for="patientName">Nombre Completo *</label>
387
+ <input type="text" class="form-control" id="patientName" required>
388
+ <div class="invalid-feedback">El nombre es requerido</div>
389
+ </div>
390
+
391
+ <div class="row">
392
+ <div class="col-md-6">
393
+ <div class="form-group">
394
+ <label for="patientBirthDate">Fecha de Nacimiento</label>
395
+ <input type="date" class="form-control" id="patientBirthDate">
396
+ </div>
397
+ </div>
398
+ <div class="col-md-6">
399
+ <div class="form-group">
400
+ <label for="patientGender">Género</label>
401
+ <select class="form-control" id="patientGender">
402
+ <option value="">Seleccionar...</option>
403
+ <option value="M">Masculino</option>
404
+ <option value="F">Femenino</option>
405
+ </select>
406
+ </div>
407
+ </div>
408
+ </div>
409
+
410
+ <div class="row">
411
+ <div class="col-md-6">
412
+ <div class="form-group">
413
+ <label for="diabetesType">Tipo de Diabetes</label>
414
+ <select class="form-control" id="diabetesType">
415
+ <option value="">Seleccionar...</option>
416
+ <option value="Tipo 1">Diabetes Tipo 1</option>
417
+ <option value="Tipo 2">Diabetes Tipo 2</option>
418
+ <option value="Gestacional">Diabetes Gestacional</option>
419
+ <option value="MODY">MODY</option>
420
+ <option value="Otro">Otro</option>
421
+ </select>
422
+ </div>
423
+ </div>
424
+ </div>
425
+ </div>
426
+ </form>
427
+ </div>
428
+
429
+ <div class="modal-footer">
430
+ <button type="button" class="btn btn-success" id="savePatientBtn" style="display: none;">
431
+ <i class="fe fe-save mr-2"></i>Guardar Paciente
432
+ </button>
433
+ <button type="button" class="btn btn-secondary" data-dismiss="modal">Cancelar</button>
434
+ </div>
435
+ </div>
436
+ </div>
437
+ </div>
438
+
439
+ <!-- Modal para editar paciente -->
440
+ <div class="modal fade" id="editPatientModal" tabindex="-1" role="dialog">
441
+ <div class="modal-dialog modal-lg" role="document">
442
+ <div class="modal-content">
443
+ <div class="modal-header">
444
+ <h5 class="modal-title">
445
+ <i class="fe fe-edit mr-2"></i>Editar Paciente
446
+ </h5>
447
+ <button type="button" class="close" data-dismiss="modal">
448
+ <span aria-hidden="true">&times;</span>
449
+ </button>
450
+ </div>
451
+ <div class="modal-body">
452
+ <form id="editPatientForm">
453
+ <input type="hidden" id="editPatientId">
454
+
455
+ <div class="row">
456
+ <div class="col-md-6">
457
+ <div class="form-group">
458
+ <label for="editPatientName">Nombre Completo *</label>
459
+ <input type="text" class="form-control" id="editPatientName" required>
460
+ </div>
461
+ </div>
462
+ <div class="col-md-6">
463
+ <div class="form-group">
464
+ <label for="editPatientBirthDate">Fecha de Nacimiento</label>
465
+ <input type="date" class="form-control" id="editPatientBirthDate">
466
+ </div>
467
+ </div>
468
+ </div>
469
+
470
+ <div class="row">
471
+ <div class="col-md-6">
472
+ <div class="form-group">
473
+ <label for="editPatientGender">Género</label>
474
+ <select class="form-control" id="editPatientGender">
475
+ <option value="">Seleccionar...</option>
476
+ <option value="M">Masculino</option>
477
+ <option value="F">Femenino</option>
478
+ </select>
479
+ </div>
480
+ </div>
481
+ <div class="col-md-6">
482
+ <div class="form-group">
483
+ <label for="editDiabetesType">Tipo de Diabetes</label>
484
+ <select class="form-control" id="editDiabetesType">
485
+ <option value="">Seleccionar...</option>
486
+ <option value="Tipo 1">Diabetes Tipo 1</option>
487
+ <option value="Tipo 2">Diabetes Tipo 2</option>
488
+ <option value="Gestacional">Diabetes Gestacional</option>
489
+ <option value="MODY">MODY</option>
490
+ <option value="Otro">Otro</option>
491
+ </select>
492
+ </div>
493
+ </div>
494
+ </div>
495
+ </form>
496
+ </div>
497
+ <div class="modal-footer">
498
+ <button type="button" class="btn btn-success" id="updatePatientBtn">
499
+ <i class="fe fe-save mr-2"></i>Actualizar
500
+ </button>
501
+ <button type="button" class="btn btn-secondary" data-dismiss="modal">Cancelar</button>
502
+ </div>
503
+ </div>
504
+ </div>
505
+ </div>
506
+
507
+ <!-- Modal para ver detalles del paciente -->
508
+ <div class="modal fade" id="patientDetailsModal" tabindex="-1" role="dialog">
509
+ <div class="modal-dialog modal-xl" role="document">
510
+ <div class="modal-content">
511
+ <div class="modal-header">
512
+ <h5 class="modal-title">
513
+ <i class="fe fe-user mr-2"></i>Detalles del Paciente
514
+ </h5>
515
+ <button type="button" class="close" data-dismiss="modal">
516
+ <span aria-hidden="true">&times;</span>
517
+ </button>
518
+ </div>
519
+ <div class="modal-body" id="patientDetailsContent">
520
+ <!-- Contenido cargado dinámicamente -->
521
+ </div>
522
+ <div class="modal-footer">
523
+ <button type="button" class="btn btn-primary" onclick="startDiagnosisForPatient()">
524
+ <i class="fe fe-eye mr-2"></i>Nuevo Diagnóstico
525
+ </button>
526
+ <button type="button" class="btn btn-secondary" data-dismiss="modal">Cerrar</button>
527
+ </div>
528
+ </div>
529
+ </div>
530
+ </div>
531
+
532
+ <!-- Scripts -->
533
+ <script src="js/jquery.min.js"></script>
534
+ <script src="js/popper.min.js"></script>
535
+ <script src="js/moment.min.js"></script>
536
+ <script src="js/bootstrap.min.js"></script>
537
+ <script src="js/simplebar.min.js"></script>
538
+ <script src="js/daterangepicker.js"></script>
539
+ <script src="js/jquery.stickOnScroll.js"></script>
540
+ <script src="js/tinycolor-min.js"></script>
541
+ <script src="js/config.js"></script>
542
+ <script src="api.js"></script>
543
+
544
+ <script>
545
+ $(document).ready(function() {
546
+ console.log('👥 Sistema de gestión de pacientes iniciado');
547
+
548
+ let currentUser = null;
549
+ let allPatients = [];
550
+ let filteredPatients = [];
551
+ let currentFilter = 'all';
552
+ let riskFactors = [];
553
+ let currentWizardStep = 1;
554
+ let selectedPatientForDetails = null;
555
+
556
+ initializePatientsApp();
557
+
558
+ function initializePatientsApp() {
559
+ console.log('Inicializando gestión de pacientes...');
560
+
561
+ checkAuthentication();
562
+
563
+ loadPatientsList();
564
+ loadRiskFactors();
565
+ loadStatistics();
566
+
567
+ setupEventListeners();
568
+ }
569
+
570
+ function checkAuthentication() {
571
+ api.getSession().then(function(response) {
572
+ if (response && response.success) {
573
+ currentUser = response.user;
574
+ } else {
575
+ window.location.href = 'auth-login.html';
576
+ }
577
+ }).catch(() => { window.location.href = 'auth-login.html'; });
578
+ }
579
+
580
+ function setupEventListeners() {
581
+ $('#patientSearchInput, #globalSearch').on('input', handlePatientSearch);
582
+
583
+ $('.filter-chip').on('click', handleFilterClick);
584
+
585
+ $('#savePatientBtn').on('click', saveNewPatient);
586
+
587
+ $('#updatePatientBtn').on('click', updatePatient);
588
+
589
+ $('#logoutBtn').on('click', handleLogout);
590
+
591
+ $('#newPatientModal').on('hidden.bs.modal', resetWizard);
592
+ }
593
+
594
+ function loadPatientsList() {
595
+ api.getPatients().then(function(response) {
596
+ if (response && response.success) {
597
+ allPatients = response.patients;
598
+ filteredPatients = [...allPatients];
599
+ displayPatients(filteredPatients);
600
+ updatePatientStats();
601
+ console.log('Pacientes cargados:', allPatients.length);
602
+ } else {
603
+ console.log('Error cargando pacientes');
604
+ showEmptyState();
605
+ }
606
+ });
607
+ }
608
+
609
+ function loadRiskFactors() {
610
+ api.getRiskFactors().then(function(response) {
611
+ if (response && response.success) {
612
+ riskFactors = response.risk_factors;
613
+ populateRiskFactorsCheckboxes();
614
+ }
615
+ });
616
+ }
617
+
618
+ function loadStatistics() {
619
+ api.getDashboardStats().then(function(response) {
620
+ if (response && response.success) {
621
+ $('#totalPatients').text(response.stats.total_patients || 0);
622
+ }
623
+ });
624
+ }
625
+
626
+ function displayPatients(patients) {
627
+ const container = $('#patientsContainer');
628
+ container.empty();
629
+
630
+ if (patients.length === 0) {
631
+ showEmptyState();
632
+ return;
633
+ }
634
+
635
+ $('#emptyState').hide();
636
+
637
+ patients.forEach(patient => {
638
+ const patientCard = createPatientCard(patient);
639
+ container.append(patientCard);
640
+ });
641
+ }
642
+
643
+ function createPatientCard(patient) {
644
+ const age = patient.birthDate ? moment().diff(moment(patient.birthDate), 'years') : 'N/A';
645
+ const avatarColor = getAvatarColor(patient.name);
646
+ const initials = getInitials(patient.name);
647
+ const genderIcon = patient.gender === 'M' ? 'fe-user' : patient.gender === 'F' ? 'fe-user' : 'fe-users';
648
+ const diabetesBadge = patient.diabetesType ? `<span class="diabetes-badge bg-warning text-dark">${patient.diabetesType}</span>` : '';
649
+
650
+ return `
651
+ <div class="col-xl-4 col-lg-6 col-md-6 mb-4">
652
+ <div class="card patient-card shadow-sm h-100">
653
+ <div class="card-body">
654
+ <div class="d-flex align-items-start">
655
+ <div class="patient-avatar" style="background-color: ${avatarColor}">
656
+ ${initials}
657
+ </div>
658
+ <div class="flex-grow-1">
659
+ <h6 class="card-title mb-1">${patient.name}</h6>
660
+ <p class="text-muted mb-2">
661
+ <i class="fe ${genderIcon} fe-12 mr-1"></i>
662
+ ${age} años • ID: ${patient.patientID}
663
+ </p>
664
+ ${diabetesBadge}
665
+
666
+ <div class="mt-3">
667
+ <small class="text-muted">
668
+ <i class="fe fe-calendar fe-12 mr-1"></i>
669
+ Registro: ${moment(patient.creationDate).format('DD/MM/YYYY')}
670
+ </small>
671
+ </div>
672
+ </div>
673
+
674
+ <div class="dropdown">
675
+ <button class="btn btn-sm btn-outline-secondary dropdown-toggle" type="button" data-toggle="dropdown">
676
+ <i class="fe fe-more-horizontal"></i>
677
+ </button>
678
+ <div class="dropdown-menu dropdown-menu-right">
679
+ <a class="dropdown-item" href="#" onclick="viewPatientDetails(${patient.patientID})">
680
+ <i class="fe fe-eye mr-2"></i>Ver Detalles
681
+ </a>
682
+ <a class="dropdown-item" href="#" onclick="editPatient(${patient.patientID})">
683
+ <i class="fe fe-edit mr-2"></i>Editar
684
+ </a>
685
+ <a class="dropdown-item" href="diagnosis.html?patient=${patient.patientID}">
686
+ <i class="fe fe-activity mr-2"></i>Nuevo Diagnóstico
687
+ </a>
688
+ <div class="dropdown-divider"></div>
689
+ <a class="dropdown-item text-danger" href="#" onclick="deletePatient(${patient.patientID}, '${patient.name}')">
690
+ <i class="fe fe-trash-2 mr-2"></i>Eliminar
691
+ </a>
692
+ </div>
693
+ </div>
694
+ </div>
695
+
696
+ <div class="row mt-3 text-center">
697
+ <div class="col-6">
698
+ <small class="text-muted">Consultas</small>
699
+ <div class="font-weight-bold" id="consultations-${patient.patientID}">-</div>
700
+ </div>
701
+ <div class="col-6">
702
+ <small class="text-muted">Última Visita</small>
703
+ <div class="font-weight-bold" id="lastvisit-${patient.patientID}">-</div>
704
+ </div>
705
+ </div>
706
+ </div>
707
+ </div>
708
+ </div>
709
+ `;
710
+ }
711
+
712
+ function updatePatientStats() {
713
+ const stats = {
714
+ total: allPatients.length,
715
+ tipo1: allPatients.filter(p => p.diabetesType === 'Tipo 1').length,
716
+ tipo2: allPatients.filter(p => p.diabetesType === 'Tipo 2').length,
717
+ recent: allPatients.filter(p => moment().diff(moment(p.creationDate), 'days') <= 30).length
718
+ };
719
+
720
+ $('#totalPatients').text(stats.total);
721
+ $('#diabetesType1').text(stats.tipo1);
722
+ $('#diabetesType2').text(stats.tipo2);
723
+ $('#recentConsultations').text(stats.recent);
724
+ }
725
+
726
+ function handlePatientSearch() {
727
+ const searchTerm = $(this).val().toLowerCase();
728
+
729
+ filteredPatients = allPatients.filter(patient =>
730
+ patient.name.toLowerCase().includes(searchTerm) ||
731
+ patient.patientID.toString().includes(searchTerm) ||
732
+ (patient.diabetesType && patient.diabetesType.toLowerCase().includes(searchTerm))
733
+ );
734
+
735
+ applyCurrentFilter();
736
+ }
737
+
738
+ function handleFilterClick() {
739
+ $('.filter-chip').removeClass('active');
740
+ $(this).addClass('active');
741
+ currentFilter = $(this).data('filter');
742
+ applyCurrentFilter();
743
+ }
744
+
745
+ function applyCurrentFilter() {
746
+ let filtered = [...filteredPatients];
747
+
748
+ switch(currentFilter) {
749
+ case 'tipo1':
750
+ filtered = filtered.filter(p => p.diabetesType === 'Tipo 1');
751
+ break;
752
+ case 'tipo2':
753
+ filtered = filtered.filter(p => p.diabetesType === 'Tipo 2');
754
+ break;
755
+ case 'recientes':
756
+ filtered = filtered.filter(p => moment().diff(moment(p.creationDate), 'days') <= 30);
757
+ break;
758
+ case 'masculino':
759
+ filtered = filtered.filter(p => p.gender === 'M');
760
+ break;
761
+ case 'femenino':
762
+ filtered = filtered.filter(p => p.gender === 'F');
763
+ break;
764
+ }
765
+
766
+ displayPatients(filtered);
767
+ }
768
+
769
+ window.showNewPatientWizard = function() {
770
+ resetWizard();
771
+ $('#newPatientModal').modal('show');
772
+ };
773
+
774
+ function resetWizard() {
775
+ currentWizardStep = 1;
776
+ $('.wizard-step').removeClass('active');
777
+ $('.wizard-step[data-step="1"]').addClass('active');
778
+ $('.step-item').removeClass('active completed');
779
+ $('.step-item[data-step="1"]').addClass('active');
780
+ $('#newPatientForm')[0].reset();
781
+ $('#savePatientBtn').show();
782
+ }
783
+
784
+ function validateCurrentStep() {
785
+ let isValid = true;
786
+
787
+ if (currentWizardStep === 1) {
788
+ const name = $('#patientName').val().trim();
789
+ if (!name) {
790
+ $('#patientName').addClass('is-invalid');
791
+ isValid = false;
792
+ } else {
793
+ $('#patientName').removeClass('is-invalid');
794
+ }
795
+ }
796
+
797
+ if (!isValid) {
798
+ showAlert('error', 'Por favor completa los campos requeridos');
799
+ }
800
+
801
+ return isValid;
802
+ }
803
+
804
+ function generatePatientSummary() {
805
+ const formData = getFormData();
806
+ const age = formData.birthDate ? moment().diff(moment(formData.birthDate), 'years') : 'No especificada';
807
+
808
+ const summaryHtml = `
809
+ <div class="row">
810
+ <div class="col-md-6">
811
+ <h6>Información Personal</h6>
812
+ <p><strong>Nombre:</strong> ${formData.name}</p>
813
+ <p><strong>Edad:</strong> ${age} años</p>
814
+ <p><strong>Género:</strong> ${getGenderText(formData.gender)}</p>
815
+ ${formData.diabetesType ? `<p><strong>Diabetes:</strong> ${formData.diabetesType}</p>` : ''}
816
+ ${formData.diagnosisDate ? `<p><strong>Diagnóstico:</strong> ${moment(formData.diagnosisDate).format('DD/MM/YYYY')}</p>` : ''}
817
+ </div>
818
+ </div>
819
+ `;
820
+
821
+ $('#patientSummary').html(summaryHtml);
822
+ }
823
+
824
+ function getFormData() {
825
+ return {
826
+ name: $('#patientName').val().trim(),
827
+ birthDate: $('#patientBirthDate').val(),
828
+ gender: $('#patientGender').val(),
829
+ diabetesType: $('#diabetesType').val(),
830
+ diagnosisDate: $('#diagnosisDate').val(),
831
+ };
832
+ }
833
+
834
+ function saveNewPatient() {
835
+ const formData = getFormData();
836
+
837
+ if (!formData.name) {
838
+ showAlert('error', 'El nombre del paciente es requerido');
839
+ return;
840
+ }
841
+
842
+ $('#savePatientBtn').prop('disabled', true)
843
+ .html('<i class="spinner-border spinner-border-sm mr-2"></i>Guardando...');
844
+
845
+ api.createPatient({
846
+ name: formData.name,
847
+ birthDate: formData.birthDate || null,
848
+ gender: formData.gender || null,
849
+ diabetesType: formData.diabetesType || null
850
+ }).then(function(response) {
851
+ $('#savePatientBtn').prop('disabled', false)
852
+ .html('<i class="fe fe-save mr-2"></i>Guardar Paciente');
853
+
854
+ if (response && response.success) {
855
+ showAlert('success', `Paciente ${formData.name} creado exitosamente`);
856
+ $('#newPatientModal').modal('hide');
857
+ loadPatientsList();
858
+ console.log('Paciente creado:', response.patient);
859
+ } else {
860
+ showAlert('error', response?.message || 'Error al crear el paciente');
861
+ }
862
+ });
863
+ }
864
+
865
+ window.editPatient = function(patientId) {
866
+ const patient = allPatients.find(p => p.patientID === patientId);
867
+ if (!patient) return;
868
+
869
+ $('#editPatientId').val(patient.patientID);
870
+ $('#editPatientName').val(patient.name);
871
+ $('#editPatientBirthDate').val(patient.birthDate || '');
872
+ $('#editPatientGender').val(patient.gender || '');
873
+ $('#editDiabetesType').val(patient.diabetesType || '');
874
+
875
+ $('#editPatientModal').modal('show');
876
+ };
877
+
878
+ function updatePatient() {
879
+ const patientId = $('#editPatientId').val();
880
+ const formData = {
881
+ name: $('#editPatientName').val().trim(),
882
+ birth_date: $('#editPatientBirthDate').val() || null,
883
+ gender: $('#editPatientGender').val() || null,
884
+ diabetes_type: $('#editDiabetesType').val() || null
885
+ };
886
+
887
+ if (!formData.name) {
888
+ showAlert('error', 'El nombre del paciente es requerido');
889
+ return;
890
+ }
891
+
892
+ $('#updatePatientBtn').prop('disabled', true)
893
+ .html('<i class="spinner-border spinner-border-sm mr-2"></i>Actualizando...');
894
+
895
+ api.updatePatient(parseInt(patientId), {
896
+ name: formData.name,
897
+ birthDate: formData.birth_date,
898
+ gender: formData.gender,
899
+ diabetesType: formData.diabetes_type
900
+ }).then(function(response) {
901
+ $('#updatePatientBtn').prop('disabled', false)
902
+ .html('<i class="fe fe-save mr-2"></i>Actualizar');
903
+
904
+ if (response && response.success) {
905
+ showAlert('success', 'Paciente actualizado exitosamente');
906
+ $('#editPatientModal').modal('hide');
907
+ loadPatientsList();
908
+ } else {
909
+ showAlert('error', response?.message || 'Error al actualizar el paciente');
910
+ }
911
+ });
912
+ }
913
+
914
+ window.viewPatientDetails = function(patientId) {
915
+ api.getPatient(patientId).then(function(response) {
916
+ if (response && response.success) {
917
+ selectedPatientForDetails = response.patient;
918
+ displayPatientDetails(response);
919
+ $('#patientDetailsModal').modal('show');
920
+ } else {
921
+ showAlert('error', 'Error al cargar detalles del paciente');
922
+ }
923
+ });
924
+ };
925
+
926
+ function displayPatientDetails(data) {
927
+ const patient = data.patient;
928
+ const consultations = data.consultations || [];
929
+ const riskFactors = data.risk_factors || [];
930
+
931
+ const age = patient.birthDate ? moment().diff(moment(patient.birthDate), 'years') : 'N/A';
932
+
933
+ const consultationsHtml = consultations.map(consultation => `
934
+ <tr>
935
+ <td>${moment(consultation.consultationDate).format('DD/MM/YYYY HH:mm')}</td>
936
+ <td>
937
+ <span class="badge ${consultation.diabeticRetinopathy ? 'badge-warning' : 'badge-success'}">
938
+ ${consultation.diabeticRetinopathy ? 'Positivo' : 'Negativo'}
939
+ </span>
940
+ </td>
941
+ <td>${consultation.confidence ? consultation.confidence.toFixed(1) + '%' : 'N/A'}</td>
942
+ <td>${consultation.notes || 'Sin notas'}</td>
943
+ </tr>
944
+ `).join('');
945
+
946
+ const riskFactorsHtml = riskFactors.map(rf =>
947
+ `<span class="risk-factor-tag">${rf.name}</span>`
948
+ ).join('');
949
+
950
+ const detailsHtml = `
951
+ <div class="row">
952
+ <div class="col-md-4">
953
+ <div class="card">
954
+ <div class="card-body text-center">
955
+ <div class="patient-avatar mx-auto mb-3" style="background-color: ${getAvatarColor(patient.name)}; width: 80px; height: 80px; font-size: 32px;">
956
+ ${getInitials(patient.name)}
957
+ </div>
958
+ <h5>${patient.name}</h5>
959
+ <p class="text-muted">ID: ${patient.patientID}</p>
960
+ </div>
961
+ </div>
962
+
963
+ <div class="card mt-3">
964
+ <div class="card-header">
965
+ <h6 class="mb-0">Información Personal</h6>
966
+ </div>
967
+ <div class="card-body">
968
+ <p><strong>Edad:</strong> ${age} años</p>
969
+ <p><strong>Género:</strong> ${getGenderText(patient.gender)}</p>
970
+ ${patient.diabetesType ? `<p><strong>Diabetes:</strong> ${patient.diabetesType}</p>` : ''}
971
+ <p><strong>Registro:</strong> ${moment(patient.creationDate).format('DD/MM/YYYY')}</p>
972
+ </div>
973
+ </div>
974
+ </div>
975
+
976
+ <div class="col-md-8">
977
+ <div class="card">
978
+ <div class="card-header">
979
+ <h6 class="mb-0">Historial de Consultas</h6>
980
+ </div>
981
+ <div class="card-body">
982
+ ${consultations.length > 0 ? `
983
+ <div class="table-responsive">
984
+ <table class="table table-sm">
985
+ <thead>
986
+ <tr>
987
+ <th>Fecha</th>
988
+ <th>Resultado</th>
989
+ <th>Confianza</th>
990
+ <th>Notas</th>
991
+ </tr>
992
+ </thead>
993
+ <tbody>${consultationsHtml}</tbody>
994
+ </table>
995
+ </div>
996
+ ` : `
997
+ <div class="text-center text-muted py-4">
998
+ <i class="fe fe-file-text fe-48 mb-3"></i>
999
+ <p>No hay consultas registradas</p>
1000
+ </div>
1001
+ `}
1002
+ </div>
1003
+ </div>
1004
+
1005
+ ${riskFactors.length > 0 ? `
1006
+ <div class="card mt-3">
1007
+ <div class="card-header">
1008
+ <h6 class="mb-0">Factores de Riesgo</h6>
1009
+ </div>
1010
+ <div class="card-body">
1011
+ ${riskFactorsHtml}
1012
+ </div>
1013
+ </div>
1014
+ ` : ''}
1015
+ </div>
1016
+ </div>
1017
+ `;
1018
+
1019
+ $('#patientDetailsContent').html(detailsHtml);
1020
+ }
1021
+
1022
+ window.deletePatient = function(patientId, patientName) {
1023
+ if (confirm(`¿Estás seguro que deseas eliminar al paciente ${patientName}?\n\nEsta acción no se puede deshacer y eliminará toda la información asociada.`)) {
1024
+ showAlert('info', 'Función de eliminación no implementada por seguridad');
1025
+ }
1026
+ };
1027
+
1028
+ function populateRiskFactorsCheckboxes() {
1029
+ const container = $('#riskFactorsContainer');
1030
+ container.empty();
1031
+
1032
+ riskFactors.forEach(factor => {
1033
+ container.append(`
1034
+ <div class="form-check">
1035
+ <input class="form-check-input" type="checkbox" value="${factor.riskFactorID}" id="risk_${factor.riskFactorID}">
1036
+ <label class="form-check-label" for="risk_${factor.riskFactorID}">
1037
+ ${factor.name}
1038
+ ${factor.description ? `<small class="text-muted d-block">${factor.description}</small>` : ''}
1039
+ </label>
1040
+ </div>
1041
+ `);
1042
+ });
1043
+ }
1044
+
1045
+ function getAvatarColor(name) {
1046
+ const colors = [
1047
+ '#6f42c1', '#007bff', '#28a745', '#dc3545', '#ffc107',
1048
+ '#17a2b8', '#fd7e14', '#e83e8c', '#6610f2', '#20c997'
1049
+ ];
1050
+ const index = name.length % colors.length;
1051
+ return colors[index];
1052
+ }
1053
+
1054
+ function getInitials(name) {
1055
+ return name.split(' ')
1056
+ .map(word => word.charAt(0))
1057
+ .join('')
1058
+ .toUpperCase()
1059
+ .substring(0, 2);
1060
+ }
1061
+
1062
+ function getGenderText(gender) {
1063
+ switch(gender) {
1064
+ case 'M': return 'Masculino';
1065
+ case 'F': return 'Femenino';
1066
+ default: return 'No especificado';
1067
+ }
1068
+ }
1069
+
1070
+ function showEmptyState() {
1071
+ $('#patientsContainer').empty();
1072
+ $('#emptyState').show();
1073
+ }
1074
+
1075
+ function showAlert(type, message) {
1076
+ const alertClass = {
1077
+ 'success': 'alert-success',
1078
+ 'error': 'alert-danger',
1079
+ 'warning': 'alert-warning',
1080
+ 'info': 'alert-info'
1081
+ }[type] || 'alert-info';
1082
+
1083
+ const alertHtml = `
1084
+ <div class="alert ${alertClass} alert-dismissible fade show" role="alert">
1085
+ <i class="fe fe-${type === 'error' ? 'x-circle' : type === 'success' ? 'check-circle' : type === 'warning' ? 'alert-triangle' : 'info'} mr-2"></i>
1086
+ ${message}
1087
+ <button type="button" class="close" data-dismiss="alert">
1088
+ <span aria-hidden="true">&times;</span>
1089
+ </button>
1090
+ </div>
1091
+ `;
1092
+
1093
+ $('.container-fluid').prepend(alertHtml);
1094
+
1095
+ setTimeout(() => {
1096
+ $('.alert').first().alert('close');
1097
+ }, 5000);
1098
+ }
1099
+
1100
+ function handleLogout() {
1101
+ if (confirm('¿Estás seguro que deseas cerrar sesión?')) {
1102
+ api.logout().then(() => { window.location.href = 'auth-login.html'; });
1103
+ }
1104
+ }
1105
+
1106
+ window.exportPatientsList = function() {
1107
+ showAlert('info', 'Exportación disponible desde el panel de administración');
1108
+ };
1109
+
1110
+ window.startDiagnosisForPatient = function() {
1111
+ if (selectedPatientForDetails) {
1112
+ setTimeout(() => {
1113
+ window.location.href = 'auth-login.html';
1114
+
1115
+ }, 200);
1116
+ }
1117
+ };
1118
+
1119
+ window.showConsultationsView = function() {
1120
+ showAlert('info', 'Vista de consultas en desarrollo');
1121
+ };
1122
+ });
1123
+ </script>
1124
+ <script>
1125
+ function confirmLogout() {
1126
+ executeLogout();
1127
+ }
1128
+
1129
+ function executeLogout() {
1130
+ api.logout().then(() => { window.location.href = 'auth-login.html'; });
1131
+ }
1132
+ </script>
1133
+
1134
+ <script>
1135
+ (function() {
1136
+ function centerWindow() {
1137
+ try {
1138
+ const screenWidth = window.screen.availWidth;
1139
+ const screenHeight = window.screen.availHeight;
1140
+ const x = Math.floor((screenWidth - 1200) / 2);
1141
+ const y = Math.floor((screenHeight - 800) / 2);
1142
+
1143
+ if (window.moveTo) {
1144
+ window.moveTo(Math.max(0, x), Math.max(0, y));
1145
+ }
1146
+ } catch (e) {
1147
+ console.warn('No se pudo centrar ventana:', e);
1148
+ }
1149
+ }
1150
+
1151
+ if (document.readyState === 'loading') {
1152
+ document.addEventListener('DOMContentLoaded', centerWindow);
1153
+ } else {
1154
+ centerWindow();
1155
+ }
1156
+
1157
+ window.addEventListener('load', centerWindow);
1158
+ window.addEventListener('focus', centerWindow);
1159
+ })();
1160
+ </script>
1161
+
1162
+ <script>
1163
+ function checkUserRoleAndHideNavigation() {
1164
+ api.getSession().then(function(response) {
1165
+ if (response.success && response.user) {
1166
+ const userRole = response.user.role;
1167
+ const usersNavItem = document.querySelector('a[href="./users.html"]');
1168
+
1169
+ if (usersNavItem) {
1170
+ const navItemContainer = usersNavItem.closest('li.nav-item');
1171
+
1172
+ if (userRole === 'Admin') {
1173
+ if (navItemContainer) {
1174
+ navItemContainer.style.display = 'block';
1175
+ }
1176
+ console.log('Navegacion de usuarios visible para administrador:', response.user.username);
1177
+ } else {
1178
+ if (navItemContainer) {
1179
+ navItemContainer.style.display = 'none';
1180
+ }
1181
+ console.log('Navegacion de usuarios oculta para usuario:', response.user.username, '(Rol:', userRole + ')');
1182
+ }
1183
+ } else {
1184
+ console.warn('No se encontró el elemento de navegación de usuarios');
1185
+ }
1186
+ } else {
1187
+ console.warn('No se pudo obtener información del usuario actual');
1188
+ }
1189
+ });
1190
+ }
1191
+
1192
+ function checkUsersPageAccess() {
1193
+ if (window.location.pathname.includes('users.html') || window.location.href.includes('users.html')) {
1194
+ api.getSession().then(function(response) {
1195
+ if (!response.success) { window.location.href = 'auth-login.html'; return; }
1196
+ if (response.user.role !== 'Admin') { window.location.href = 'index.html'; }
1197
+ });
1198
+ }
1199
+ }
1200
+
1201
+ $(document).ready(function() {
1202
+ setTimeout(() => {
1203
+ checkUserRoleAndHideNavigation();
1204
+ checkUsersPageAccess();
1205
+ }, 100);
1206
+ });
1207
+
1208
+ function updateNavigationForUser() {
1209
+ checkUserRoleAndHideNavigation();
1210
+ }
1211
+
1212
+ function updateUserInfo() {
1213
+ api.getSession().then(function(response) {
1214
+ if (response.success && response.user) {
1215
+ const userDropdown = document.querySelector('#navbarDropdownMenuLink');
1216
+ if (userDropdown) {
1217
+ const roleBadge = response.user.role === 'Admin' ?
1218
+ '<span class="badge badge-primary badge-sm ml-1">Admin</span>' :
1219
+ '<span class="badge badge-secondary badge-sm ml-1">Doctor</span>';
1220
+ if (!userDropdown.querySelector('.badge')) userDropdown.innerHTML += roleBadge;
1221
+ }
1222
+ }
1223
+ });
1224
+ }
1225
+
1226
+ $(document).ready(function() {
1227
+ setTimeout(() => {
1228
+ updateUserInfo();
1229
+ }, 200);
1230
+ });
1231
+ </script>
1232
+
1233
+ <script src="js/gauge.min.js"></script>
1234
+ <script src="js/jquery.sparkline.min.js"></script>
1235
+ <script src="js/apps.js"></script>
1236
+ </body>
1237
+ </html>
web/users.html ADDED
@@ -0,0 +1,552 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <html lang="es">
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="Gestión de Usuarios">
7
+ <meta name="author" content="Sistema Médico">
8
+ <link rel="icon" href="favicon.ico">
9
+ <title>Usuarios</title>
10
+
11
+ <link rel="stylesheet" href="css/simplebar.css">
12
+ <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">
13
+ <link rel="stylesheet" href="css/feather.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
+ </head>
17
+
18
+ <body class="vertical light">
19
+ <div class="wrapper">
20
+ <!-- Top Navigation -->
21
+ <nav class="topnav navbar navbar-light">
22
+ <button type="button" class="navbar-toggler text-muted mt-2 p-0 mr-3 collapseSidebar">
23
+ <i class="fe fe-menu navbar-toggler-icon"></i>
24
+ </button>
25
+
26
+ <ul class="nav">
27
+ <li class="nav-item dropdown">
28
+ <a class="nav-link dropdown-toggle text-muted pr-0" href="#" id="navbarDropdownMenuLink" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
29
+ <span class="avatar avatar-sm mt-2">
30
+ <img src="./assets/images/persona.png" alt="..." class="avatar-img rounded-circle">
31
+ </span>
32
+ </a>
33
+ <div class="dropdown-menu dropdown-menu-right" aria-labelledby="navbarDropdownMenuLink">
34
+ <a class="dropdown-item" href="#" onclick="confirmLogout()">Cerrar Sesión</a>
35
+ </div>
36
+ </li>
37
+ </ul>
38
+ </nav>
39
+
40
+ <!-- Sidebar -->
41
+ <aside class="sidebar-left border-right bg-white shadow" id="leftSidebar" data-simplebar>
42
+ <a href="#" class="btn collapseSidebar toggle-btn d-lg-none text-muted ml-2 mt-3" data-toggle="toggle">
43
+ <i class="fe fe-x"><span class="sr-only"></span></i>
44
+ </a>
45
+ <nav class="vertnav navbar navbar-light">
46
+ <!-- nav bar -->
47
+ <div class="w-100 mb-4 d-flex">
48
+ <a class="navbar-brand mx-auto mt-2 flex-fill text-center">
49
+ <img src="assets/images/LOGO.png" alt="Logo">
50
+ </a>
51
+ </div>
52
+
53
+ <ul class="navbar-nav flex-fill w-100 mb-2">
54
+ <li class="nav-item">
55
+ <a class="nav-link" href="./index.html">
56
+ <i class="fe fe-home fe-16"></i>
57
+ <span class="ml-3 item-text">Dashboard</span>
58
+ </a>
59
+ </li>
60
+ <li class="nav-item">
61
+ <a class="nav-link" href="./diagnosis.html">
62
+ <i class="fe fe-eye fe-16"></i>
63
+ <span class="ml-3 item-text">Diagnóstico</span>
64
+ </a>
65
+ </li>
66
+ <li class="nav-item">
67
+ <a class="nav-link" href="./patients.html">
68
+ <i class="fe fe-users fe-16"></i>
69
+ <span class="ml-3 item-text">Pacientes</span>
70
+ </a>
71
+ </li>
72
+ <li class="nav-item">
73
+ <a class="nav-link" href="./historial-consultas.html">
74
+ <i class="fe fe-file-text fe-16"></i>
75
+ <span class="ml-3 item-text">Consultas</span>
76
+ </a>
77
+ </li>
78
+ <li class="nav-item active">
79
+ <a class="nav-link" href="./users.html">
80
+ <i class="fe fe-user fe-16"></i>
81
+ <span class="ml-3 item-text">Usuarios</span>
82
+ </a>
83
+ </li>
84
+ </ul>
85
+ </nav>
86
+ </aside>
87
+
88
+ <!-- Main Content -->
89
+ <main role="main" class="main-content">
90
+ <div class="container-fluid">
91
+ <div class="row justify-content-center">
92
+ <div class="col-12">
93
+ <!-- Header -->
94
+ <div class="row align-items-center mb-2">
95
+ <div class="col">
96
+ <h2 class="h5 page-title">Gestión de Usuarios</h2>
97
+ </div>
98
+ <div class="col-auto">
99
+ <button class="btn btn-primary" onclick="loadUsers()">
100
+ <i class="fe fe-refresh-cw fe-16 mr-2"></i>Actualizar
101
+ </button>
102
+ <button class="btn btn-success ml-2" onclick="showAddUserModal()">
103
+ <i class="fe fe-plus fe-16 mr-2"></i>Nuevo Usuario
104
+ </button>
105
+ </div>
106
+ </div>
107
+
108
+ <!-- Users Table -->
109
+ <div class="card shadow">
110
+ <div class="card-header">
111
+ <h6 class="card-title mb-0">
112
+ <i class="fe fe-users mr-2"></i>Lista de Usuarios
113
+ </h6>
114
+ </div>
115
+ <div class="card-body">
116
+ <div id="loadingSpinner" class="text-center py-5">
117
+ <div class="spinner-border text-primary" role="status">
118
+ <span class="sr-only">Cargando...</span>
119
+ </div>
120
+ <p class="text-muted mt-2">Cargando usuarios...</p>
121
+ </div>
122
+
123
+ <div id="usersTableContainer" style="display: none;">
124
+ <div class="table-responsive">
125
+ <table class="table table-striped table-hover">
126
+ <thead class="thead-light">
127
+ <tr>
128
+ <th>ID</th>
129
+ <th>Nombre de Usuario</th>
130
+ <th>Rol</th>
131
+ <th>Fecha de Creación</th>
132
+ <th class="text-center">Acciones</th>
133
+ </tr>
134
+ </thead>
135
+ <tbody id="usersTableBody">
136
+ <!-- Datos se cargarán aquí dinámicamente -->
137
+ </tbody>
138
+ </table>
139
+ </div>
140
+ </div>
141
+
142
+ <div id="emptyState" class="text-center py-5" style="display: none;">
143
+ <i class="fe fe-users fe-48 text-muted mb-3"></i>
144
+ <h6 class="text-muted">No se encontraron usuarios</h6>
145
+ <p class="text-muted">No hay usuarios registrados en el sistema.</p>
146
+ <button class="btn btn-primary" onclick="showAddUserModal()">
147
+ <i class="fe fe-plus mr-1"></i>Crear Primer Usuario
148
+ </button>
149
+ </div>
150
+ </div>
151
+ </div>
152
+ </div>
153
+ </div>
154
+ </div>
155
+ </main>
156
+ </div>
157
+
158
+ <!-- Modal para agregar/editar usuario -->
159
+ <div class="modal fade" id="userModal" tabindex="-1" role="dialog" aria-labelledby="userModalLabel" aria-hidden="true">
160
+ <div class="modal-dialog" role="document">
161
+ <div class="modal-content">
162
+ <div class="modal-header">
163
+ <h5 class="modal-title" id="userModalLabel">
164
+ <i class="fe fe-user mr-2"></i>Nuevo Usuario
165
+ </h5>
166
+ <button type="button" class="close" data-dismiss="modal" aria-label="Close">
167
+ <span aria-hidden="true">&times;</span>
168
+ </button>
169
+ </div>
170
+ <div class="modal-body">
171
+ <form id="userForm">
172
+ <input type="hidden" id="userId">
173
+ <div class="form-group">
174
+ <label for="username">Nombre de Usuario</label>
175
+ <input type="text" class="form-control" id="username" required>
176
+ </div>
177
+ <div class="form-group">
178
+ <label for="password">Contraseña</label>
179
+ <input type="password" class="form-control" id="password" required>
180
+ </div>
181
+ <div class="form-group">
182
+ <label for="confirmPassword">Confirmar Contraseña</label>
183
+ <input type="password" class="form-control" id="confirmPassword" required>
184
+ </div>
185
+ <div class="form-group">
186
+ <label for="role">Rol</label>
187
+ <select class="form-control" id="role" required>
188
+ <option value="Doctor">Doctor</option>
189
+ <option value="Admin">Administrador</option>
190
+ </select>
191
+ </div>
192
+ </form>
193
+ </div>
194
+ <div class="modal-footer">
195
+ <button type="button" class="btn btn-secondary" data-dismiss="modal">Cancelar</button>
196
+ <button type="button" class="btn btn-primary" id="saveUserBtn" onclick="saveUser()">Guardar</button>
197
+ </div>
198
+ </div>
199
+ </div>
200
+ </div>
201
+
202
+ <script src="js/jquery.min.js"></script>
203
+ <script src="js/popper.min.js"></script>
204
+ <script src="js/bootstrap.min.js"></script>
205
+ <script src="js/simplebar.min.js"></script>
206
+ <script src="js/config.js"></script>
207
+ <script src="api.js"></script>
208
+
209
+ <script>
210
+ $(document).ready(function() {
211
+ checkAuthentication();
212
+
213
+ loadUsers();
214
+
215
+ $('#logoutBtn').on('click', function(e) {
216
+ e.preventDefault();
217
+ if (confirm('¿Estás seguro que deseas cerrar sesión?')) {
218
+ api.logout().then(() => { window.location.href = 'auth-login.html'; });
219
+ }
220
+ });
221
+ });
222
+
223
+ function checkAuthentication() {
224
+ api.getSession().then(r => { if (!r.success) window.location.href = 'auth-login.html'; }).catch(() => { window.location.href = 'auth-login.html'; });
225
+ }
226
+
227
+ function loadUsers() {
228
+ $('#loadingSpinner').show();
229
+ $('#usersTableContainer').hide();
230
+ $('#emptyState').hide();
231
+
232
+ api.getUsers().then(function(response) {
233
+ $('#loadingSpinner').hide();
234
+
235
+ if (response.success) {
236
+ if (response.users.length > 0) {
237
+ renderUsersTable(response.users);
238
+ $('#usersTableContainer').show();
239
+ } else {
240
+ $('#emptyState').show();
241
+ }
242
+ } else {
243
+ console.error('Error cargando usuarios:', response.message);
244
+ showAlert('error', 'Error cargando usuarios: ' + response.message);
245
+ $('#emptyState').show();
246
+ }
247
+ });
248
+ }
249
+
250
+ function renderUsersTable(users) {
251
+ const tbody = $('#usersTableBody');
252
+ tbody.empty();
253
+
254
+ users.forEach(user => {
255
+ const row = `
256
+ <tr>
257
+ <td>${user.userID}</td>
258
+ <td>${user.username}</td>
259
+ <td>
260
+ <span class="badge ${user.role === 'Admin' ? 'badge-primary' : 'badge-secondary'}">
261
+ ${user.role}
262
+ </span>
263
+ </td>
264
+ <td>${user.creationDate}</td>
265
+ <td class="text-center">
266
+ <div class="btn-group" role="group">
267
+ <button class="btn btn-sm btn-outline-primary"
268
+ onclick="editUser(${user.userID})"
269
+ title="Editar">
270
+ <i class="fe fe-edit fe-12"></i>
271
+ </button>
272
+ <button class="btn btn-sm btn-outline-danger"
273
+ onclick="confirmDeleteUser(${user.userID})"
274
+ title="Eliminar">
275
+ <i class="fe fe-trash-2 fe-12"></i>
276
+ </button>
277
+ </div>
278
+ </td>
279
+ </tr>
280
+ `;
281
+ tbody.append(row);
282
+ });
283
+ }
284
+
285
+ function showAddUserModal() {
286
+ $('#userForm')[0].reset();
287
+ $('#userId').val('');
288
+ $('#userModalLabel').html('<i class="fe fe-user-plus mr-2"></i>Nuevo Usuario');
289
+ $('#userModal').modal('show');
290
+ }
291
+
292
+ function editUser(userId) {
293
+ api.getUsers().then(function(response) {
294
+ if (response.success) {
295
+ const user = response.users.find(u => u.userID == userId);
296
+ if (user) {
297
+ $('#userId').val(user.userID);
298
+ $('#username').val(user.username);
299
+ $('#role').val(user.role);
300
+ $('#password').val('');
301
+ $('#confirmPassword').val('');
302
+
303
+ $('#userModalLabel').html('<i class="fe fe-edit mr-2"></i>Editar Usuario');
304
+ $('#userModal').modal('show');
305
+ }
306
+ }
307
+ });
308
+ }
309
+
310
+ function saveUser() {
311
+ const userId = $('#userId').val();
312
+ const username = $('#username').val().trim();
313
+ const password = $('#password').val();
314
+ const confirmPassword = $('#confirmPassword').val();
315
+ const role = $('#role').val();
316
+
317
+ if (!username) {
318
+ showAlert('error', 'El nombre de usuario es requerido');
319
+ return;
320
+ }
321
+
322
+ if (!userId && (!password || !confirmPassword)) {
323
+ showAlert('error', 'La contraseña es requerida para nuevos usuarios');
324
+ return;
325
+ }
326
+
327
+ if (password !== confirmPassword) {
328
+ showAlert('error', 'Las contraseñas no coinciden');
329
+ return;
330
+ }
331
+
332
+ if (userId) {
333
+ const userData = {
334
+ userId: userId,
335
+ username: username,
336
+ role: role
337
+ };
338
+
339
+ if (password) {
340
+ userData.password = password;
341
+ }
342
+
343
+ api.updateUser(userId, userData).then(function(response) {
344
+ if (response.success) {
345
+ showAlert('success', 'Usuario actualizado exitosamente');
346
+ $('#userModal').modal('hide');
347
+ loadUsers();
348
+ } else {
349
+ showAlert('error', response.message || 'Error actualizando usuario');
350
+ }
351
+ });
352
+ } else {
353
+ api.createUser(username, password, role).then(function(response) {
354
+ if (response.success) {
355
+ showAlert('success', 'Usuario creado exitosamente');
356
+ $('#userModal').modal('hide');
357
+ loadUsers();
358
+ } else {
359
+ showAlert('error', response.message || 'Error creando usuario');
360
+ }
361
+ });
362
+ }
363
+ }
364
+
365
+ function confirmDeleteUser(userId) {
366
+ if (confirm('¿Estás seguro que deseas eliminar este usuario? Esta acción no se puede deshacer.')) {
367
+ deleteUser(userId);
368
+ }
369
+ }
370
+
371
+ function deleteUser(userId) {
372
+ api.deleteUser(userId).then(function(response) {
373
+ if (response.success) {
374
+ showAlert('success', 'Usuario eliminado exitosamente');
375
+ loadUsers();
376
+ } else {
377
+ showAlert('error', response.message || 'Error eliminando usuario');
378
+ }
379
+ });
380
+ }
381
+
382
+ function showAlert(type, message) {
383
+ const alertClass = {
384
+ 'success': 'alert-success',
385
+ 'error': 'alert-danger',
386
+ 'warning': 'alert-warning',
387
+ 'info': 'alert-info'
388
+ }[type] || 'alert-info';
389
+
390
+ const alertHtml = `
391
+ <div class="alert ${alertClass} alert-dismissible fade show" role="alert">
392
+ ${message}
393
+ <button type="button" class="close" data-dismiss="alert">
394
+ <span aria-hidden="true">&times;</span>
395
+ </button>
396
+ </div>
397
+ `;
398
+
399
+ $('.container-fluid').prepend(alertHtml);
400
+
401
+ setTimeout(() => {
402
+ $('.alert').first().alert('close');
403
+ }, 5000);
404
+ }
405
+ </script>
406
+
407
+ <script>
408
+ function confirmLogout() {
409
+ executeLogout();
410
+ }
411
+
412
+ function executeLogout() {
413
+ api.logout().then(() => { window.location.href = 'auth-login.html'; });
414
+ }
415
+ </script>
416
+
417
+ <script>
418
+ function checkAdminAccess() {
419
+ api.getSession().then(r => { if (!r.success) { window.location.href = 'auth-login.html'; } else if (r.user.role !== 'Admin') { alert('Acceso denegado: Solo administradores'); window.location.href = 'index.html'; } }).catch(() => { window.location.href = 'auth-login.html'; });
420
+ }
421
+
422
+ function renderUsersTableSecure(users) {
423
+ const tbody = $('#usersTableBody');
424
+ tbody.empty();
425
+
426
+ api.getSession().then(function(currentUserResponse) { if (!currentUserResponse.success) return; const currentUserId = currentUserResponse.user.userID;
427
+
428
+ users.forEach(user => {
429
+ const canDelete = user.userID !== currentUserId;
430
+
431
+ const deleteButton = canDelete ?
432
+ `<button class="btn btn-sm btn-outline-danger"
433
+ onclick="confirmDeleteUser(${user.userID})"
434
+ title="Eliminar">
435
+ <i class="fe fe-trash-2 fe-12"></i>
436
+ </button>` :
437
+ `<button class="btn btn-sm btn-outline-secondary"
438
+ disabled
439
+ title="No puedes eliminar tu propia cuenta">
440
+ <i class="fe fe-user-x fe-12"></i>
441
+ </button>`;
442
+
443
+ const row = `
444
+ <tr>
445
+ <td>${user.userID}</td>
446
+ <td>
447
+ ${user.username}
448
+ ${user.userID === currentUserId ? '<span class="badge badge-info ml-2">Tú</span>' : ''}
449
+ </td>
450
+ <td>
451
+ <span class="badge ${user.role === 'Admin' ? 'badge-primary' : 'badge-secondary'}">
452
+ ${user.role}
453
+ </span>
454
+ </td>
455
+ <td>${user.creationDate}</td>
456
+ <td class="text-center">
457
+ <div class="btn-group" role="group">
458
+ <button class="btn btn-sm btn-outline-primary"
459
+ onclick="editUser(${user.userID})"
460
+ title="Editar">
461
+ <i class="fe fe-edit fe-12"></i>
462
+ </button>
463
+ ${deleteButton}
464
+ </div>
465
+ </td>
466
+ </tr>
467
+ `;
468
+ tbody.append(row);
469
+ });
470
+ });
471
+ }
472
+
473
+ function confirmDeleteUserSecure(userId) {
474
+ api.getSession().then(function(response) { if (!response.success) { showAlert('error', 'Error de autenticación'); return; } if (response.user.userID === userId) {
475
+ showAlert('error', 'No puedes eliminar tu propia cuenta');
476
+ return;
477
+ }
478
+
479
+ const confirmation = confirm(
480
+ 'ELIMINAR USUARIO\n\n' +
481
+ '¿Estás seguro que deseas eliminar este usuario?\n\n' +
482
+ '• Esta acción NO se puede deshacer\n' +
483
+ '• Se perderán todos los datos asociados\n' +
484
+ '• El usuario no podrá acceder al sistema\n\n' +
485
+ 'Escribe "CONFIRMAR" para continuar:'
486
+ );
487
+
488
+ if (confirmation) {
489
+ const secondConfirmation = prompt(
490
+ 'Para confirmar la eliminación, escribe: CONFIRMAR'
491
+ );
492
+
493
+ if (secondConfirmation === 'CONFIRMAR') {
494
+ deleteUser(userId);
495
+ } else {
496
+ showAlert('info', 'Eliminación cancelada - Confirmación incorrecta');
497
+ }
498
+ }
499
+ });
500
+ }
501
+
502
+ $(document).ready(function() {
503
+ checkAdminAccess();
504
+
505
+ setTimeout(() => {
506
+ checkAuthentication();
507
+
508
+ loadUsers();
509
+
510
+ $('#logoutBtn').on('click', function(e) {
511
+ e.preventDefault();
512
+ if (confirm('¿Estás seguro que deseas cerrar sesión?')) {
513
+ api.logout().then(() => { window.location.href = 'auth-login.html'; });
514
+ }
515
+ });
516
+ }, 500); // Pequeño delay para asegurar que la verificación de admin se complete
517
+ });
518
+
519
+ window.renderUsersTable = renderUsersTableSecure;
520
+
521
+ window.confirmDeleteUser = confirmDeleteUserSecure;
522
+ </script>
523
+
524
+ <script>
525
+ (function() {
526
+ function centerWindow() {
527
+ try {
528
+ const screenWidth = window.screen.availWidth;
529
+ const screenHeight = window.screen.availHeight;
530
+ const x = Math.floor((screenWidth - 1200) / 2);
531
+ const y = Math.floor((screenHeight - 800) / 2);
532
+
533
+ if (window.moveTo) {
534
+ window.moveTo(Math.max(0, x), Math.max(0, y));
535
+ }
536
+ } catch (e) {
537
+ console.warn('No se pudo centrar ventana:', e);
538
+ }
539
+ }
540
+
541
+ if (document.readyState === 'loading') {
542
+ document.addEventListener('DOMContentLoaded', centerWindow);
543
+ } else {
544
+ centerWindow();
545
+ }
546
+
547
+ window.addEventListener('load', centerWindow);
548
+ window.addEventListener('focus', centerWindow);
549
+ })();
550
+ </script>
551
+ </body>
552
+ </html>