Download web/patients.html from CYV28/RetinAI: direct link, hf CLI and curl.
- Browser
- Download file 62.5 kB
-
https://huggingface.co/spaces/CYV28/RetinAI/resolve/main/web/patients.html
- Command line
-
hf download hf://spaces/CYV28/RetinAI/web/patients.html
-
curl -L -o patients.html https://huggingface.co/spaces/CYV28/RetinAI/resolve/main/web/patients.html
62.5 kB
| <html lang="en"> | |
| <head> | |
| <meta charset="utf-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> | |
| <meta name="description" content=""> | |
| <meta name="author" content=""> | |
| <link rel="icon" href="favicon.ico"> | |
| <title>Gestión de Pacientes</title> | |
| <link rel="stylesheet" href="css/simplebar.css"> | |
| <link href="https://fonts.googleapis.com/css2?family=Overpass:ital,wght@0,100;0,200;0,300;0,400;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,600;1,700;1,800;1,900&display=swap" rel="stylesheet"> | |
| <script src="https://cdn.jsdelivr.net/npm/feather-icons/dist/feather.min.js"></script> | |
| <link rel="stylesheet" href="css/select2.css"> | |
| <link rel="stylesheet" href="css/dropzone.css"> | |
| <link rel="stylesheet" href="css/uppy.min.css"> | |
| <link rel="stylesheet" href="css/jquery.steps.css"> | |
| <link rel="stylesheet" href="css/jquery.timepicker.css"> | |
| <link rel="stylesheet" href="css/quill.snow.css"> | |
| <link rel="stylesheet" href="css/daterangepicker.css"> | |
| <script> | |
| (function() { | |
| var dark = localStorage.getItem('theme') === 'dark'; | |
| document.write( | |
| '<link rel="stylesheet" href="css/app-' + (dark ? 'dark' : 'light') + '.css" id="' + (dark ? 'dark' : 'light') + 'Theme">' + | |
| '<link rel="stylesheet" href="css/app-' + (dark ? 'light' : 'dark') + '.css" id="' + (dark ? 'light' : 'dark') + 'Theme" disabled>' | |
| ); | |
| })(); | |
| </script> | |
| <style> | |
| .patient-card { | |
| transition: all 0.3s ease; | |
| border-radius: 12px; | |
| } | |
| .patient-card:hover { | |
| transform: translateY(-2px); | |
| box-shadow: 0 8px 25px rgba(0,0,0,0.1); | |
| } | |
| .search-section { | |
| color: white; | |
| border-radius: 12px; | |
| padding: 2rem; | |
| margin-bottom: 2rem; | |
| background-color: #f5f5f5; | |
| } | |
| .search-input { | |
| border: none; | |
| border-radius: 25px; | |
| padding: 12px 20px; | |
| font-size: 16px; | |
| box-shadow: 0 4px 15px rgba(0,0,0,0.1); | |
| } | |
| .patient-avatar { | |
| width: 60px; | |
| height: 60px; | |
| border-radius: 50%; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| font-size: 24px; | |
| font-weight: bold; | |
| color: white; | |
| margin-right: 15px; | |
| } | |
| .diabetes-badge { | |
| font-size: 0.75rem; | |
| padding: 4px 8px; | |
| border-radius: 12px; | |
| } | |
| .form-wizard { | |
| background: white; | |
| border-radius: 12px; | |
| overflow: hidden; | |
| box-shadow: 0 4px 20px rgba(0,0,0,0.1); | |
| } | |
| .wizard-step { | |
| display: none; | |
| } | |
| .wizard-step.active { | |
| display: block; | |
| animation: fadeIn 0.5s ease; | |
| } | |
| @keyframes fadeIn { | |
| from { opacity: 0; transform: translateX(20px); } | |
| to { opacity: 1; transform: translateX(0); } | |
| } | |
| .step-indicator { | |
| background: #f8f9fa; | |
| padding: 1rem; | |
| border-bottom: 1px solid #dee2e6; | |
| } | |
| .step-item { | |
| display: inline-block; | |
| padding: 8px 16px; | |
| margin-right: 10px; | |
| border-radius: 20px; | |
| background: #e9ecef; | |
| color: #6c757d; | |
| font-size: 14px; | |
| transition: all 0.3s ease; | |
| } | |
| .step-item.active { | |
| background: #007bff; | |
| color: white; | |
| } | |
| .step-item.completed { | |
| background: #28a745; | |
| color: white; | |
| } | |
| .patient-stats { | |
| background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%); | |
| color: white; | |
| border-radius: 12px; | |
| padding: 1.5rem; | |
| } | |
| .stats-item { | |
| text-align: center; | |
| } | |
| .stats-number { | |
| font-size: 2rem; | |
| font-weight: bold; | |
| margin-bottom: 5px; | |
| } | |
| .filter-chip { | |
| display: inline-block; | |
| background: #e9ecef; | |
| color: #495057; | |
| padding: 6px 12px; | |
| border-radius: 15px; | |
| font-size: 14px; | |
| margin-right: 8px; | |
| margin-bottom: 8px; | |
| cursor: pointer; | |
| transition: all 0.3s ease; | |
| } | |
| .filter-chip.active { | |
| background: #007bff; | |
| color: white; | |
| } | |
| .filter-chip:hover { | |
| background: #6c757d; | |
| color: white; | |
| } | |
| .risk-factor-tag { | |
| display: inline-block; | |
| background: #ffc107; | |
| color: #212529; | |
| padding: 2px 8px; | |
| border-radius: 10px; | |
| font-size: 12px; | |
| margin: 2px; | |
| } | |
| .empty-state { | |
| text-align: center; | |
| padding: 3rem; | |
| color: #6c757d; | |
| } | |
| .empty-state i { | |
| font-size: 4rem; | |
| margin-bottom: 1rem; | |
| opacity: 0.5; | |
| } | |
| /* ── TOAST NOTIFICATIONS ─────────────────────────── */ | |
| #toast-container { | |
| position: fixed; | |
| bottom: 1.5rem; | |
| right: 1.5rem; | |
| z-index: 99999; | |
| display: flex; | |
| flex-direction: column; | |
| gap: 0.6rem; | |
| pointer-events: none; | |
| } | |
| .toast-notify { | |
| display: flex; | |
| align-items: flex-start; | |
| gap: 12px; | |
| min-width: 300px; | |
| max-width: 380px; | |
| padding: 14px 16px; | |
| border-radius: 10px; | |
| box-shadow: 0 8px 24px rgba(0,0,0,0.15); | |
| background: #fff; | |
| border-left: 5px solid #ccc; | |
| pointer-events: all; | |
| opacity: 0; | |
| transform: translateX(60px); | |
| transition: opacity 0.3s ease, transform 0.3s ease; | |
| position: relative; | |
| overflow: hidden; | |
| } | |
| .toast-notify.show { opacity: 1; transform: translateX(0); } | |
| .toast-notify.hide { opacity: 0; transform: translateX(60px); } | |
| .toast-notify.toast-success { border-left-color: #28a745; } | |
| .toast-notify.toast-error { border-left-color: #dc3545; } | |
| .toast-notify.toast-warning { border-left-color: #ffc107; } | |
| .toast-notify.toast-info { border-left-color: #17a2b8; } | |
| .toast-icon { flex-shrink: 0; margin-top: 1px; } | |
| .toast-success .toast-icon svg { stroke: #28a745; } | |
| .toast-error .toast-icon svg { stroke: #dc3545; } | |
| .toast-warning .toast-icon svg { stroke: #ffc107; } | |
| .toast-info .toast-icon svg { stroke: #17a2b8; } | |
| .toast-body { flex: 1; } | |
| .toast-title { font-weight: 600; font-size: 0.875rem; margin-bottom: 2px; } | |
| .toast-success .toast-title { color: #155724; } | |
| .toast-error .toast-title { color: #721c24; } | |
| .toast-warning .toast-title { color: #856404; } | |
| .toast-info .toast-title { color: #0c5460; } | |
| .toast-msg { font-size: 0.825rem; color: #555; line-height: 1.4; } | |
| .toast-close { | |
| background: none; border: none; font-size: 1rem; | |
| line-height: 1; color: #aaa; cursor: pointer; padding: 0; flex-shrink: 0; | |
| } | |
| .toast-close:hover { color: #333; } | |
| .toast-progress { | |
| position: absolute; | |
| bottom: 0; left: 0; | |
| height: 3px; | |
| border-radius: 0 0 0 10px; | |
| } | |
| .toast-success .toast-progress { background: #28a745; } | |
| .toast-error .toast-progress { background: #dc3545; } | |
| .toast-warning .toast-progress { background: #ffc107; } | |
| .toast-info .toast-progress { background: #17a2b8; } | |
| .search-section h5, | |
| .search-section h6, | |
| .search-section label, | |
| .search-section .filter-chip { | |
| color: #212529 ; | |
| } | |
| </style> | |
| </head> | |
| <body class="vertical light"> | |
| <div class="wrapper"> | |
| <nav class="topnav navbar navbar-light"> | |
| <button type="button" class="navbar-toggler text-muted mt-2 p-0 mr-3 collapseSidebar"> | |
| <i data-feather="menu" class="navbar-toggler-icon"></i> | |
| </button> | |
| <ul class="nav"> | |
| <li class="nav-item"> | |
| <a class="nav-link text-muted my-2" href="#" id="modeSwitcher" data-mode="light"> | |
| <i data-feather="sun"></i> | |
| </a> | |
| </li> | |
| <li class="nav-item"> | |
| <a href="#" onclick="confirmLogout()" class="nav-link text-muted" title="Cerrar sesión" style="display: flex; align-items: center; gap: 6px;"> | |
| <span class="avatar avatar-sm mt-2"> | |
| <img src="./assets/images/persona.png" alt="..." class="avatar-img rounded-circle"> | |
| </span> | |
| <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" title="Cerrar sesión"> | |
| <path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/> | |
| <polyline points="16 17 21 12 16 7"/> | |
| <line x1="21" y1="12" x2="9" y2="12"/> | |
| </svg> | |
| </a> | |
| </li> | |
| </ul> | |
| </nav> | |
| <!-- Sidebar --> | |
| <aside class="sidebar-left border-right bg-white shadow" id="leftSidebar" data-simplebar> | |
| <a href="#" class="btn collapseSidebar toggle-btn d-lg-none text-muted ml-2 mt-3" data-toggle="toggle"> | |
| <i data-feather="x"><span class="sr-only"></span></i> | |
| </a> | |
| <nav class="vertnav navbar navbar-light"> | |
| <!-- nav bar --> | |
| <a class="navbar-brand mx-auto mt-2 flex-fill text-center"> | |
| <img src="assets/images/RetinAILogo.png" alt="Logo" id="sidebarLogo" style="max-width:140px; transition: opacity 0.3s ease;"> | |
| <img src="assets/images/LogoMini.png" alt="Logo Mini" id="sidebarLogoMini" style="max-width:44px; display:none; transition: opacity 0.3s ease;"> | |
| </a> | |
| <ul class="navbar-nav flex-fill w-100 mb-2"> | |
| <li class="nav-item"> | |
| <a class="nav-link" href="./index.html"> | |
| <i data-feather="home" class="fe-16"></i> | |
| <span class="ml-3 item-text">Dashboard</span> | |
| </a> | |
| </li> | |
| <li class="nav-item"> | |
| <a class="nav-link" href="./diagnosis.html"> | |
| <i data-feather="eye" class="fe-16"></i> | |
| <span class="ml-3 item-text">Diagnóstico</span> | |
| </a> | |
| </li> | |
| <li class="nav-item active"> | |
| <a class="nav-link" href="./patients.html"> | |
| <i data-feather="users" class="fe-16"></i> | |
| <span class="ml-3 item-text">Pacientes</span> | |
| </a> | |
| </li> | |
| <li class="nav-item"> | |
| <a class="nav-link" href="./historial-consultas.html"> | |
| <i data-feather="file-text" class="fe-16"></i> | |
| <span class="ml-3 item-text">Consultas</span> | |
| </a> | |
| </li> | |
| <li class="nav-item"> | |
| <a class="nav-link" href="./users.html"> | |
| <i data-feather="user" class="fe-16"></i> | |
| <span class="ml-3 item-text">Usuarios</span> | |
| </a> | |
| </li> | |
| </ul> | |
| </nav> | |
| </aside> | |
| <!-- Contenido principal --> | |
| <main role="main" class="main-content"> | |
| <div class="container-fluid"> | |
| <div class="row justify-content-center"> | |
| <div class="col-12"> | |
| <!-- Header --> | |
| <div class="row align-items-center mb-4"> | |
| <div class="col"> | |
| <h2 class="h3 page-title"> | |
| <i data-feather="users" class="fe-24 mr-2 text-primary"></i> | |
| Gestión de Pacientes | |
| </h2> | |
| <p class="text-muted">Administra la información de tus pacientes de manera eficiente</p> | |
| </div> | |
| <div class="col-auto"> | |
| <button class="btn btn-primary" onclick="showNewPatientWizard()"> | |
| <i data-feather="plus" class="fe-16 mr-2"></i>Nuevo Paciente | |
| </button> | |
| </div> | |
| </div> | |
| <!-- Estadísticas rápidas --> | |
| <div class="row mb-4"> | |
| <div class="col-lg-3 col-md-6"> | |
| <div class="card shadow mb-4"> | |
| <div class="card-body"> | |
| <div class="stats-item"> | |
| <div class="stats-number text-primary" id="totalPatients">0</div> | |
| <div class="text-muted">Total Pacientes</div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="col-lg-3 col-md-6"> | |
| <div class="card shadow mb-4"> | |
| <div class="card-body"> | |
| <div class="stats-item"> | |
| <div class="stats-number text-warning" id="diabetesType1">0</div> | |
| <div class="text-muted">Diabetes Tipo 1</div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="col-lg-3 col-md-6"> | |
| <div class="card shadow mb-4"> | |
| <div class="card-body"> | |
| <div class="stats-item"> | |
| <div class="stats-number text-info" id="diabetesType2">0</div> | |
| <div class="text-muted">Diabetes Tipo 2</div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="col-lg-3 col-md-6"> | |
| <div class="card shadow mb-4"> | |
| <div class="card-body"> | |
| <div class="stats-item"> | |
| <div class="stats-number text-success" id="recentConsultations">0</div> | |
| <div class="text-muted">Consultas Recientes</div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Sección de búsqueda y filtros --> | |
| <div class="search-section"> | |
| <div class="row align-items-center" style="background-color: #f5f5f5; padding: 15px; border-radius: 8px;"> | |
| <div class="col-lg-6"> | |
| <h5 class="mb-3"> | |
| <i data-feather="search" class="mr-2"></i>Buscar Pacientes | |
| </h5> | |
| <input type="text" class="form-control search-input" id="patientSearchInput" placeholder="Buscar por nombre, ID o tipo de diabetes..."> | |
| </div> | |
| <div class="col-lg-6"> | |
| <h6 class="mb-2">Filtros rápidos:</h6> | |
| <div class="filter-chips"> | |
| <span class="filter-chip active" data-filter="all">Todos</span> | |
| <span class="filter-chip" data-filter="tipo1">Tipo 1</span> | |
| <span class="filter-chip" data-filter="tipo2">Tipo 2</span> | |
| <span class="filter-chip" data-filter="recientes">Recientes</span> | |
| <span class="filter-chip" data-filter="masculino">Masculino</span> | |
| <span class="filter-chip" data-filter="femenino">Femenino</span> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Lista de pacientes --> | |
| <div class="row" id="patientsContainer"> | |
| <!-- Los pacientes se cargarán aquí dinámicamente --> | |
| </div> | |
| <div id="emptyState" class="empty-state" style="display: none;"> | |
| <i data-feather="users"></i> | |
| <h4>No hay pacientes registrados</h4> | |
| <p class="text-muted">Comienza agregando tu primer paciente al sistema</p> | |
| <button class="btn btn-primary" onclick="showNewPatientWizard()"> | |
| <i data-feather="plus" class="mr-2"></i>Agregar Primer Paciente | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </main> | |
| </div> | |
| <!-- Modal para nuevo paciente --> | |
| <div class="modal fade" id="newPatientModal" tabindex="-1" role="dialog"> | |
| <div class="modal-dialog modal-lg" role="document"> | |
| <div class="modal-content form-wizard"> | |
| <div class="modal-header step-indicator"> | |
| <h5 class="modal-title"> | |
| <i data-feather="user-plus" class="mr-2"></i>Nuevo Paciente | |
| </h5> | |
| <div class="ml-auto"> | |
| <span class="step-item active" data-step="1">1. Información Personal</span> | |
| </div> | |
| <button type="button" class="close ml-3" data-dismiss="modal"> | |
| <span aria-hidden="true">×</span> | |
| </button> | |
| </div> | |
| <div class="modal-body"> | |
| <form id="newPatientForm"> | |
| <!-- Paso 1: Información Personal --> | |
| <div class="wizard-step active" data-step="1"> | |
| <h6 class="mb-4">Información Personal del Paciente</h6> | |
| <div class="form-group"> | |
| <label for="patientName">Nombre Completo *</label> | |
| <input type="text" class="form-control" id="patientName" required> | |
| <div class="invalid-feedback">El nombre es requerido</div> | |
| </div> | |
| <div class="row"> | |
| <div class="col-md-6"> | |
| <div class="form-group"> | |
| <label for="patientBirthDate">Fecha de Nacimiento</label> | |
| <input type="date" class="form-control" id="patientBirthDate"> | |
| </div> | |
| </div> | |
| <div class="col-md-6"> | |
| <div class="form-group"> | |
| <label for="patientGender">Género</label> | |
| <select class="form-control" id="patientGender"> | |
| <option value="">Seleccionar...</option> | |
| <option value="M">Masculino</option> | |
| <option value="F">Femenino</option> | |
| </select> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="row"> | |
| <div class="col-md-6"> | |
| <div class="form-group"> | |
| <label for="diabetesType">Tipo de Diabetes</label> | |
| <select class="form-control" id="diabetesType"> | |
| <option value="">Seleccionar...</option> | |
| <option value="Tipo 1">Diabetes Tipo 1</option> | |
| <option value="Tipo 2">Diabetes Tipo 2</option> | |
| <option value="Gestacional">Diabetes Gestacional</option> | |
| <option value="MODY">MODY</option> | |
| <option value="Otro">Otro</option> | |
| </select> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </form> | |
| </div> | |
| <div class="modal-footer"> | |
| <button type="button" class="btn btn-success" id="savePatientBtn" style="display: none;"> | |
| <i data-feather="save" class="mr-2"></i>Guardar Paciente | |
| </button> | |
| <button type="button" class="btn btn-secondary" data-dismiss="modal">Cancelar</button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Modal para editar paciente --> | |
| <div class="modal fade" id="editPatientModal" tabindex="-1" role="dialog"> | |
| <div class="modal-dialog modal-lg" role="document"> | |
| <div class="modal-content"> | |
| <div class="modal-header"> | |
| <h5 class="modal-title"> | |
| <i data-feather="edit" class="mr-2"></i>Editar Paciente | |
| </h5> | |
| <button type="button" class="close" data-dismiss="modal"> | |
| <span aria-hidden="true">×</span> | |
| </button> | |
| </div> | |
| <div class="modal-body"> | |
| <form id="editPatientForm"> | |
| <input type="hidden" id="editPatientId"> | |
| <div class="row"> | |
| <div class="col-md-6"> | |
| <div class="form-group"> | |
| <label for="editPatientName">Nombre Completo *</label> | |
| <input type="text" class="form-control" id="editPatientName" required> | |
| </div> | |
| </div> | |
| <div class="col-md-6"> | |
| <div class="form-group"> | |
| <label for="editPatientBirthDate">Fecha de Nacimiento</label> | |
| <input type="date" class="form-control" id="editPatientBirthDate"> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="row"> | |
| <div class="col-md-6"> | |
| <div class="form-group"> | |
| <label for="editPatientGender">Género</label> | |
| <select class="form-control" id="editPatientGender"> | |
| <option value="">Seleccionar...</option> | |
| <option value="M">Masculino</option> | |
| <option value="F">Femenino</option> | |
| </select> | |
| </div> | |
| </div> | |
| <div class="col-md-6"> | |
| <div class="form-group"> | |
| <label for="editDiabetesType">Tipo de Diabetes</label> | |
| <select class="form-control" id="editDiabetesType"> | |
| <option value="">Seleccionar...</option> | |
| <option value="Tipo 1">Diabetes Tipo 1</option> | |
| <option value="Tipo 2">Diabetes Tipo 2</option> | |
| <option value="Gestacional">Diabetes Gestacional</option> | |
| <option value="MODY">MODY</option> | |
| <option value="Otro">Otro</option> | |
| </select> | |
| </div> | |
| </div> | |
| </div> | |
| </form> | |
| </div> | |
| <div class="modal-footer"> | |
| <button type="button" class="btn btn-success" id="updatePatientBtn"> | |
| <i data-feather="save" class="mr-2"></i>Actualizar | |
| </button> | |
| <button type="button" class="btn btn-secondary" data-dismiss="modal">Cancelar</button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Modal para ver detalles del paciente --> | |
| <div class="modal fade" id="patientDetailsModal" tabindex="-1" role="dialog"> | |
| <div class="modal-dialog modal-xl" role="document"> | |
| <div class="modal-content"> | |
| <div class="modal-header"> | |
| <h5 class="modal-title"> | |
| <i data-feather="user" class="mr-2"></i>Detalles del Paciente | |
| </h5> | |
| <button type="button" class="close" data-dismiss="modal"> | |
| <span aria-hidden="true">×</span> | |
| </button> | |
| </div> | |
| <div class="modal-body" id="patientDetailsContent"> | |
| <!-- Contenido cargado dinámicamente --> | |
| </div> | |
| <div class="modal-footer"> | |
| <button type="button" class="btn btn-primary" onclick="startDiagnosisForPatient()"> | |
| <i data-feather="eye" class="mr-2"></i>Nuevo Diagnóstico | |
| </button> | |
| <button type="button" class="btn btn-secondary" data-dismiss="modal">Cerrar</button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Scripts --> | |
| <script src="js/jquery.min.js"></script> | |
| <script src="js/popper.min.js"></script> | |
| <script src="js/moment.min.js"></script> | |
| <script src="js/bootstrap.min.js"></script> | |
| <script src="js/simplebar.min.js"></script> | |
| <script src="js/daterangepicker.js"></script> | |
| <script src="js/jquery.stickOnScroll.js"></script> | |
| <script src="js/tinycolor-min.js"></script> | |
| <script src="js/config.js"></script> | |
| <script src="api.js"></script> | |
| <script> | |
| $(document).ready(function() { | |
| console.log('👥 Sistema de gestión de pacientes iniciado'); | |
| let currentUser = null; | |
| let allPatients = []; | |
| let filteredPatients = []; | |
| let currentFilter = 'all'; | |
| let riskFactors = []; | |
| let currentWizardStep = 1; | |
| let selectedPatientForDetails = null; | |
| initializePatientsApp(); | |
| function initializePatientsApp() { | |
| console.log('Inicializando gestión de pacientes...'); | |
| checkAuthentication(); | |
| loadPatientsList(); | |
| loadRiskFactors(); | |
| setupEventListeners(); | |
| } | |
| function checkAuthentication() { | |
| api.getSession().then(function(response) { | |
| if (response && response.success) { | |
| currentUser = response.user; | |
| } else { | |
| window.location.href = 'auth-login.html'; | |
| } | |
| }).catch(() => { window.location.href = 'auth-login.html'; }); | |
| } | |
| function setupEventListeners() { | |
| $('#patientSearchInput, #globalSearch').on('input', handlePatientSearch); | |
| $('.filter-chip').on('click', handleFilterClick); | |
| $('#savePatientBtn').on('click', saveNewPatient); | |
| $('#updatePatientBtn').on('click', updatePatient); | |
| $('#logoutBtn').on('click', handleLogout); | |
| $('#newPatientModal').on('hidden.bs.modal', resetWizard); | |
| } | |
| function loadPatientsList() { | |
| api.getPatients().then(function(response) { | |
| if (response && response.success) { | |
| allPatients = response.patients; | |
| filteredPatients = [...allPatients]; | |
| displayPatients(filteredPatients); | |
| updatePatientStats(); | |
| loadStatistics(); // llamar aquí para tener allPatients ya disponible | |
| console.log('Pacientes cargados:', allPatients.length); | |
| } else { | |
| console.log('Error cargando pacientes'); | |
| showEmptyState(); | |
| } | |
| }); | |
| } | |
| function loadRiskFactors() { | |
| api.getRiskFactors().then(function(response) { | |
| if (response && response.success) { | |
| riskFactors = response.risk_factors; | |
| populateRiskFactorsCheckboxes(); | |
| } | |
| }); | |
| } | |
| function loadStatistics() { | |
| // El total real de pacientes se toma directamente de la lista completa | |
| // ya cargada, no del dashboard (que filtra solo por diagnosticados) | |
| $('#totalPatients').text(allPatients.length); | |
| } | |
| function displayPatients(patients) { | |
| const container = $('#patientsContainer'); | |
| container.empty(); | |
| if (patients.length === 0) { | |
| showEmptyState(); | |
| return; | |
| } | |
| $('#emptyState').hide(); | |
| patients.forEach(patient => { | |
| const patientCard = createPatientCard(patient); | |
| container.append(patientCard); | |
| }); | |
| } | |
| function createPatientCard(patient) { | |
| const age = patient.birthDate | |
| ? moment().diff(moment(patient.birthDate), 'years') | |
| : 'N/A'; | |
| const avatarColor = getAvatarColor(patient.name); | |
| const initials = getInitials(patient.name); | |
| const genderIcon = patient.gender === 'M' ? 'fe-user' | |
| : patient.gender === 'F' ? 'fe-user' | |
| : 'fe-users'; | |
| const diabetesBadge = patient.diabetesType | |
| ? `<span class="diabetes-badge bg-warning text-dark">${patient.diabetesType}</span>` | |
| : ''; | |
| const consultCount = patient.consultationCount ?? 0; | |
| const lastVisit = patient.lastConsultationDate | |
| ? moment(patient.lastConsultationDate).format('DD/MM/YY') | |
| : 'Sin visitas'; | |
| const lastVisitClass = patient.lastConsultationDate ? '' : 'text-muted'; | |
| return ` | |
| <div class="col-xl-4 col-lg-6 col-md-6 mb-4"> | |
| <div class="card patient-card shadow-sm h-100"> | |
| <div class="card-body"> | |
| <div class="d-flex align-items-start"> | |
| <div class="patient-avatar" style="background-color: ${avatarColor}"> | |
| ${initials} | |
| </div> | |
| <div class="flex-grow-1"> | |
| <h6 class="card-title mb-1">${patient.name}</h6> | |
| <p class="text-muted mb-2"> | |
| <i class="fe ${genderIcon} fe-12 mr-1"></i> | |
| ${age} años • ID: ${patient.patientID} | |
| </p> | |
| ${diabetesBadge} | |
| <div class="mt-3"> | |
| <small class="text-muted"> | |
| <i data-feather="calendar" class="fe-12 mr-1"></i> | |
| Registro: ${moment(patient.creationDate).format('DD/MM/YYYY')} | |
| </small> | |
| </div> | |
| </div> | |
| <div class="dropdown"> | |
| <button class="btn btn-sm btn-outline-secondary" | |
| type="button" | |
| data-toggle="dropdown" | |
| aria-haspopup="true" | |
| aria-expanded="false" | |
| style="padding: 4px 8px; line-height: 1;"> | |
| <svg xmlns="http://www.w3.org/2000/svg" | |
| width="16" height="16" | |
| viewBox="0 0 24 24" | |
| fill="currentColor" | |
| style="display:block; pointer-events:none;"> | |
| <circle cx="5" cy="12" r="2"/> | |
| <circle cx="12" cy="12" r="2"/> | |
| <circle cx="19" cy="12" r="2"/> | |
| </svg> | |
| </button> | |
| <div class="dropdown-menu dropdown-menu-right"> | |
| <a class="dropdown-item" href="#" | |
| onclick="viewPatientDetails(${patient.patientID})"> | |
| <i data-feather="eye" class="mr-2"></i>Ver Detalles | |
| </a> | |
| <a class="dropdown-item" href="#" | |
| onclick="editPatient(${patient.patientID})"> | |
| <i data-feather="edit" class="mr-2"></i>Editar | |
| </a> | |
| <a class="dropdown-item" | |
| href="diagnosis.html?patient=${patient.patientID}"> | |
| <i data-feather="activity" class="mr-2"></i>Nuevo Diagnóstico | |
| </a> | |
| <div class="dropdown-divider"></div> | |
| <a class="dropdown-item text-danger" href="#" | |
| onclick="deletePatient(${patient.patientID}, '${patient.name}')"> | |
| <i data-feather="trash-2" class="mr-2"></i>Eliminar | |
| </a> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="row mt-3 text-center border-top pt-3"> | |
| <div class="col-6"> | |
| <small class="text-muted d-block">Consultas</small> | |
| <span class="font-weight-bold text-primary">${consultCount}</span> | |
| </div> | |
| <div class="col-6"> | |
| <small class="text-muted d-block">Última Visita</small> | |
| <span class="font-weight-bold ${lastVisitClass}" | |
| style="font-size:0.85rem">${lastVisit}</span> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| `; | |
| } | |
| function updatePatientStats() { | |
| const stats = { | |
| total: allPatients.length, | |
| tipo1: allPatients.filter(p => p.diabetesType === 'Tipo 1').length, | |
| tipo2: allPatients.filter(p => p.diabetesType === 'Tipo 2').length, | |
| recent: allPatients.filter(p => moment().diff(moment(p.creationDate), 'days') <= 30).length | |
| }; | |
| $('#totalPatients').text(stats.total); | |
| $('#diabetesType1').text(stats.tipo1); | |
| $('#diabetesType2').text(stats.tipo2); | |
| $('#recentConsultations').text(stats.recent); | |
| } | |
| function handlePatientSearch() { | |
| const searchTerm = $(this).val().toLowerCase(); | |
| filteredPatients = allPatients.filter(patient => | |
| patient.name.toLowerCase().includes(searchTerm) || | |
| patient.patientID.toString().includes(searchTerm) || | |
| (patient.diabetesType && patient.diabetesType.toLowerCase().includes(searchTerm)) | |
| ); | |
| applyCurrentFilter(); | |
| } | |
| function handleFilterClick() { | |
| $('.filter-chip').removeClass('active'); | |
| $(this).addClass('active'); | |
| currentFilter = $(this).data('filter'); | |
| applyCurrentFilter(); | |
| } | |
| function applyCurrentFilter() { | |
| let filtered = [...filteredPatients]; | |
| switch(currentFilter) { | |
| case 'tipo1': | |
| filtered = filtered.filter(p => p.diabetesType === 'Tipo 1'); | |
| break; | |
| case 'tipo2': | |
| filtered = filtered.filter(p => p.diabetesType === 'Tipo 2'); | |
| break; | |
| case 'recientes': | |
| filtered = filtered.filter(p => moment().diff(moment(p.creationDate), 'days') <= 30); | |
| break; | |
| case 'masculino': | |
| filtered = filtered.filter(p => p.gender === 'M'); | |
| break; | |
| case 'femenino': | |
| filtered = filtered.filter(p => p.gender === 'F'); | |
| break; | |
| } | |
| displayPatients(filtered); | |
| } | |
| window.showNewPatientWizard = function() { | |
| resetWizard(); | |
| $('#newPatientModal').modal('show'); | |
| }; | |
| function resetWizard() { | |
| currentWizardStep = 1; | |
| $('.wizard-step').removeClass('active'); | |
| $('.wizard-step[data-step="1"]').addClass('active'); | |
| $('.step-item').removeClass('active completed'); | |
| $('.step-item[data-step="1"]').addClass('active'); | |
| $('#newPatientForm')[0].reset(); | |
| $('#savePatientBtn').show(); | |
| } | |
| function validateCurrentStep() { | |
| let isValid = true; | |
| if (currentWizardStep === 1) { | |
| const name = $('#patientName').val().trim(); | |
| if (!name) { | |
| $('#patientName').addClass('is-invalid'); | |
| isValid = false; | |
| } else { | |
| $('#patientName').removeClass('is-invalid'); | |
| } | |
| } | |
| if (!isValid) { | |
| showAlert('error', 'Por favor completa los campos requeridos'); | |
| } | |
| return isValid; | |
| } | |
| function generatePatientSummary() { | |
| const formData = getFormData(); | |
| const age = formData.birthDate ? moment().diff(moment(formData.birthDate), 'years') : 'No especificada'; | |
| const summaryHtml = ` | |
| <div class="row"> | |
| <div class="col-md-6"> | |
| <h6>Información Personal</h6> | |
| <p><strong>Nombre:</strong> ${formData.name}</p> | |
| <p><strong>Edad:</strong> ${age} años</p> | |
| <p><strong>Género:</strong> ${getGenderText(formData.gender)}</p> | |
| ${formData.diabetesType ? `<p><strong>Diabetes:</strong> ${formData.diabetesType}</p>` : ''} | |
| ${formData.diagnosisDate ? `<p><strong>Diagnóstico:</strong> ${moment(formData.diagnosisDate).format('DD/MM/YYYY')}</p>` : ''} | |
| </div> | |
| </div> | |
| `; | |
| $('#patientSummary').html(summaryHtml); | |
| } | |
| function getFormData() { | |
| return { | |
| name: $('#patientName').val().trim(), | |
| birthDate: $('#patientBirthDate').val(), | |
| gender: $('#patientGender').val(), | |
| diabetesType: $('#diabetesType').val(), | |
| diagnosisDate: $('#diagnosisDate').val(), | |
| }; | |
| } | |
| function saveNewPatient() { | |
| const formData = getFormData(); | |
| if (!formData.name) { | |
| showAlert('error', 'El nombre del paciente es requerido'); | |
| return; | |
| } | |
| $('#savePatientBtn').prop('disabled', true) | |
| .html('<i class="spinner-border spinner-border-sm mr-2"></i>Guardando...'); | |
| api.createPatient({ | |
| name: formData.name, | |
| birthDate: formData.birthDate || null, | |
| gender: formData.gender || null, | |
| diabetesType: formData.diabetesType || null | |
| }).then(function(response) { | |
| $('#savePatientBtn').prop('disabled', false) | |
| .html('<i data-feather="save" class="mr-2"></i>Guardar Paciente'); | |
| if (response && response.success) { | |
| showAlert('success', `Paciente ${formData.name} creado exitosamente`); | |
| $('#newPatientModal').modal('hide'); | |
| loadPatientsList(); | |
| console.log('Paciente creado:', response.patient); | |
| } else { | |
| showAlert('error', response?.message || 'Error al crear el paciente'); | |
| } | |
| }); | |
| } | |
| window.editPatient = function(patientId) { | |
| const patient = allPatients.find(p => p.patientID === patientId); | |
| if (!patient) return; | |
| $('#editPatientId').val(patient.patientID); | |
| $('#editPatientName').val(patient.name); | |
| $('#editPatientBirthDate').val(patient.birthDate || ''); | |
| $('#editPatientGender').val(patient.gender || ''); | |
| $('#editDiabetesType').val(patient.diabetesType || ''); | |
| $('#editPatientModal').modal('show'); | |
| }; | |
| function updatePatient() { | |
| const patientId = $('#editPatientId').val(); | |
| const formData = { | |
| name: $('#editPatientName').val().trim(), | |
| birth_date: $('#editPatientBirthDate').val() || null, | |
| gender: $('#editPatientGender').val() || null, | |
| diabetes_type: $('#editDiabetesType').val() || null | |
| }; | |
| if (!formData.name) { | |
| showAlert('error', 'El nombre del paciente es requerido'); | |
| return; | |
| } | |
| $('#updatePatientBtn').prop('disabled', true) | |
| .html('<i class="spinner-border spinner-border-sm mr-2"></i>Actualizando...'); | |
| api.updatePatient(parseInt(patientId), { | |
| name: formData.name, | |
| birthDate: formData.birth_date, | |
| gender: formData.gender, | |
| diabetesType: formData.diabetes_type | |
| }).then(function(response) { | |
| $('#updatePatientBtn').prop('disabled', false) | |
| .html('<i data-feather="save" class="mr-2"></i>Actualizar'); | |
| if (response && response.success) { | |
| showAlert('success', 'Paciente actualizado exitosamente'); | |
| $('#editPatientModal').modal('hide'); | |
| loadPatientsList(); | |
| } else { | |
| showAlert('error', response?.message || 'Error al actualizar el paciente'); | |
| } | |
| }); | |
| } | |
| window.viewPatientDetails = function(patientId) { | |
| api.getPatient(patientId).then(function(response) { | |
| if (response && response.success) { | |
| selectedPatientForDetails = response.patient; | |
| displayPatientDetails(response); | |
| $('#patientDetailsModal').modal('show'); | |
| } else { | |
| showAlert('error', 'Error al cargar detalles del paciente'); | |
| } | |
| }); | |
| }; | |
| function displayPatientDetails(data) { | |
| const patient = data.patient; | |
| const consultations = data.consultations || []; | |
| const riskFactors = data.risk_factors || []; | |
| const age = patient.birthDate ? moment().diff(moment(patient.birthDate), 'years') : 'N/A'; | |
| const consultationsHtml = consultations.map(consultation => ` | |
| <tr> | |
| <td>${moment(consultation.consultationDate).format('DD/MM/YYYY HH:mm')}</td> | |
| <td> | |
| <span class="badge ${consultation.diabeticRetinopathy ? 'badge-warning' : 'badge-success'}"> | |
| ${consultation.diabeticRetinopathy ? 'Positivo' : 'Negativo'} | |
| </span> | |
| </td> | |
| <td>${consultation.confidence ? consultation.confidence.toFixed(1) + '%' : 'N/A'}</td> | |
| <td>${consultation.notes || 'Sin notas'}</td> | |
| </tr> | |
| `).join(''); | |
| const riskFactorsHtml = riskFactors.map(rf => | |
| `<span class="risk-factor-tag">${rf.name}</span>` | |
| ).join(''); | |
| const detailsHtml = ` | |
| <div class="row"> | |
| <div class="col-md-4"> | |
| <div class="card"> | |
| <div class="card-body text-center"> | |
| <div class="patient-avatar mx-auto mb-3" style="background-color: ${getAvatarColor(patient.name)}; width: 80px; height: 80px; font-size: 32px;"> | |
| ${getInitials(patient.name)} | |
| </div> | |
| <h5>${patient.name}</h5> | |
| <p class="text-muted">ID: ${patient.patientID}</p> | |
| </div> | |
| </div> | |
| <div class="card mt-3"> | |
| <div class="card-header"> | |
| <h6 class="mb-0">Información Personal</h6> | |
| </div> | |
| <div class="card-body"> | |
| <p><strong>Edad:</strong> ${age} años</p> | |
| <p><strong>Género:</strong> ${getGenderText(patient.gender)}</p> | |
| ${patient.diabetesType ? `<p><strong>Diabetes:</strong> ${patient.diabetesType}</p>` : ''} | |
| <p><strong>Registro:</strong> ${moment(patient.creationDate).format('DD/MM/YYYY')}</p> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="col-md-8"> | |
| <div class="card"> | |
| <div class="card-header"> | |
| <h6 class="mb-0">Historial de Consultas</h6> | |
| </div> | |
| <div class="card-body"> | |
| ${consultations.length > 0 ? ` | |
| <div class="table-responsive"> | |
| <table class="table table-sm"> | |
| <thead> | |
| <tr> | |
| <th>Fecha</th> | |
| <th>Resultado</th> | |
| <th>Confianza</th> | |
| <th>Notas</th> | |
| </tr> | |
| </thead> | |
| <tbody>${consultationsHtml}</tbody> | |
| </table> | |
| </div> | |
| ` : ` | |
| <div class="text-center text-muted py-4"> | |
| <i data-feather="file-text" class="fe-48 mb-3"></i> | |
| <p>No hay consultas registradas</p> | |
| </div> | |
| `} | |
| </div> | |
| </div> | |
| ${riskFactors.length > 0 ? ` | |
| <div class="card mt-3"> | |
| <div class="card-header"> | |
| <h6 class="mb-0">Factores de Riesgo</h6> | |
| </div> | |
| <div class="card-body"> | |
| ${riskFactorsHtml} | |
| </div> | |
| </div> | |
| ` : ''} | |
| </div> | |
| </div> | |
| `; | |
| $('#patientDetailsContent').html(detailsHtml); | |
| } | |
| window.deletePatient = function(patientId, patientName) { | |
| 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.`)) { | |
| showAlert('info', 'Función de eliminación no implementada por seguridad'); | |
| } | |
| }; | |
| function populateRiskFactorsCheckboxes() { | |
| const container = $('#riskFactorsContainer'); | |
| container.empty(); | |
| riskFactors.forEach(factor => { | |
| container.append(` | |
| <div class="form-check"> | |
| <input class="form-check-input" type="checkbox" value="${factor.riskFactorID}" id="risk_${factor.riskFactorID}"> | |
| <label class="form-check-label" for="risk_${factor.riskFactorID}"> | |
| ${factor.name} | |
| ${factor.description ? `<small class="text-muted d-block">${factor.description}</small>` : ''} | |
| </label> | |
| </div> | |
| `); | |
| }); | |
| } | |
| function getAvatarColor(name) { | |
| const colors = [ | |
| '#6f42c1', '#007bff', '#28a745', '#dc3545', '#ffc107', | |
| '#17a2b8', '#fd7e14', '#e83e8c', '#6610f2', '#20c997' | |
| ]; | |
| const index = name.length % colors.length; | |
| return colors[index]; | |
| } | |
| function getInitials(name) { | |
| return name.split(' ') | |
| .map(word => word.charAt(0)) | |
| .join('') | |
| .toUpperCase() | |
| .substring(0, 2); | |
| } | |
| function getGenderText(gender) { | |
| switch(gender) { | |
| case 'M': return 'Masculino'; | |
| case 'F': return 'Femenino'; | |
| default: return 'No especificado'; | |
| } | |
| } | |
| function showEmptyState() { | |
| $('#patientsContainer').empty(); | |
| $('#emptyState').show(); | |
| } | |
| function showAlert(type, message) { | |
| if (!document.getElementById('toast-container')) { | |
| const container = document.createElement('div'); | |
| container.id = 'toast-container'; | |
| document.body.appendChild(container); | |
| } | |
| const container = document.getElementById('toast-container'); | |
| const icons = { | |
| success: '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/></svg>', | |
| error: '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="15" y1="9" x2="9" y2="15"/><line x1="9" y1="9" x2="15" y2="15"/></svg>', | |
| warning: '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>', | |
| info: '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>' | |
| }; | |
| const titles = { success: 'Éxito', error: 'Error', warning: 'Advertencia', info: 'Información' }; | |
| const duration = type === 'error' ? 6000 : 4000; | |
| const toast = document.createElement('div'); | |
| toast.className = `toast-notify toast-${type}`; | |
| toast.innerHTML = ` | |
| <div class="toast-icon">${icons[type] || icons.info}</div> | |
| <div class="toast-body"> | |
| <div class="toast-title">${titles[type] || 'Aviso'}</div> | |
| <div class="toast-msg">${message}</div> | |
| </div> | |
| <button class="toast-close" onclick="this.closest('.toast-notify').remove()">×</button> | |
| <div class="toast-progress" style="width:100%;transition:width ${duration}ms linear;"></div> | |
| `; | |
| container.appendChild(toast); | |
| requestAnimationFrame(() => requestAnimationFrame(() => { | |
| toast.classList.add('show'); | |
| const bar = toast.querySelector('.toast-progress'); | |
| if (bar) requestAnimationFrame(() => { bar.style.width = '0%'; }); | |
| })); | |
| const timer = setTimeout(() => { | |
| toast.classList.add('hide'); | |
| toast.addEventListener('transitionend', () => toast.remove(), { once: true }); | |
| }, duration); | |
| toast.querySelector('.toast-close').addEventListener('click', () => clearTimeout(timer)); | |
| } | |
| function handleLogout() { | |
| if (confirm('¿Estás seguro que deseas cerrar sesión?')) { | |
| api.logout().then(() => { window.location.href = 'auth-login.html'; }); | |
| } | |
| } | |
| window.exportPatientsList = function() { | |
| showAlert('info', 'Exportación disponible desde el panel de administración'); | |
| }; | |
| window.startDiagnosisForPatient = function() { | |
| if (selectedPatientForDetails) { | |
| $('#patientDetailsModal').modal('hide'); | |
| window.location.href = `diagnosis.html?patient=${selectedPatientForDetails.patientID}`; | |
| } | |
| }; | |
| window.showConsultationsView = function() { | |
| showAlert('info', 'Vista de consultas en desarrollo'); | |
| }; | |
| }); | |
| </script> | |
| <script> | |
| function confirmLogout() { | |
| executeLogout(); | |
| } | |
| function executeLogout() { | |
| api.logout().then(() => { window.location.href = 'auth-login.html'; }); | |
| } | |
| </script> | |
| <script> | |
| (function() { | |
| function centerWindow() { | |
| try { | |
| const screenWidth = window.screen.availWidth; | |
| const screenHeight = window.screen.availHeight; | |
| const x = Math.floor((screenWidth - 1200) / 2); | |
| const y = Math.floor((screenHeight - 800) / 2); | |
| if (window.moveTo) { | |
| window.moveTo(Math.max(0, x), Math.max(0, y)); | |
| } | |
| } catch (e) { | |
| console.warn('No se pudo centrar ventana:', e); | |
| } | |
| } | |
| if (document.readyState === 'loading') { | |
| document.addEventListener('DOMContentLoaded', centerWindow); | |
| } else { | |
| centerWindow(); | |
| } | |
| window.addEventListener('load', centerWindow); | |
| window.addEventListener('focus', centerWindow); | |
| })(); | |
| </script> | |
| <script> | |
| function checkUserRoleAndHideNavigation() { | |
| api.getSession().then(function(response) { | |
| if (response.success && response.user) { | |
| const userRole = response.user.role; | |
| const usersNavItem = document.querySelector('a[href="./users.html"]'); | |
| if (usersNavItem) { | |
| const navItemContainer = usersNavItem.closest('li.nav-item'); | |
| if (userRole === 'Admin') { | |
| if (navItemContainer) { | |
| navItemContainer.style.display = 'block'; | |
| } | |
| console.log('Navegacion de usuarios visible para administrador:', response.user.username); | |
| } else { | |
| if (navItemContainer) { | |
| navItemContainer.style.display = 'none'; | |
| } | |
| console.log('Navegacion de usuarios oculta para usuario:', response.user.username, '(Rol:', userRole + ')'); | |
| } | |
| } else { | |
| console.warn('No se encontró el elemento de navegación de usuarios'); | |
| } | |
| } else { | |
| console.warn('No se pudo obtener información del usuario actual'); | |
| } | |
| }); | |
| } | |
| function checkUsersPageAccess() { | |
| if (window.location.pathname.includes('users.html') || window.location.href.includes('users.html')) { | |
| api.getSession().then(function(response) { | |
| if (!response.success) { window.location.href = 'auth-login.html'; return; } | |
| if (response.user.role !== 'Admin') { window.location.href = 'index.html'; } | |
| }); | |
| } | |
| } | |
| $(document).ready(function() { | |
| setTimeout(() => { | |
| checkUserRoleAndHideNavigation(); | |
| checkUsersPageAccess(); | |
| }, 100); | |
| }); | |
| function updateNavigationForUser() { | |
| checkUserRoleAndHideNavigation(); | |
| } | |
| function updateUserInfo() { | |
| api.getSession().then(function(response) { | |
| if (response.success && response.user) { | |
| const userDropdown = document.querySelector('#navbarDropdownMenuLink'); | |
| if (userDropdown) { | |
| const roleBadge = response.user.role === 'Admin' ? | |
| '<span class="badge badge-primary badge-sm ml-1">Admin</span>' : | |
| '<span class="badge badge-secondary badge-sm ml-1">Doctor</span>'; | |
| if (!userDropdown.querySelector('.badge')) userDropdown.innerHTML += roleBadge; | |
| } | |
| } | |
| }); | |
| } | |
| $(document).ready(function() { | |
| setTimeout(() => { | |
| updateUserInfo(); | |
| }, 200); | |
| }); | |
| </script> | |
| <script src="js/gauge.min.js"></script> | |
| <script src="js/jquery.sparkline.min.js"></script> | |
| <script src="js/apps.js"></script> | |
| <script> | |
| function _initFeather() { | |
| if (typeof feather !== 'undefined') feather.replace({ 'stroke-width': 1.8 }); | |
| } | |
| document.addEventListener('DOMContentLoaded', _initFeather); | |
| var _fObserver = new MutationObserver(function(mutations) { | |
| if (mutations.some(function(m){ return m.addedNodes.length > 0; })) _initFeather(); | |
| }); | |
| document.addEventListener('DOMContentLoaded', function() { | |
| _fObserver.observe(document.body, { childList: true, subtree: true }); | |
| }); | |
| </script> | |
| <script> | |
| (function () { | |
| const overlay = document.createElement('div'); | |
| overlay.id = 'theme-overlay'; | |
| overlay.style.cssText = [ | |
| 'position:fixed','inset:0','z-index:99999', | |
| 'pointer-events:none','opacity:0', | |
| 'transition:opacity 0.18s ease','background:#fff' | |
| ].join(';'); | |
| document.body.appendChild(overlay); | |
| document.addEventListener('DOMContentLoaded', function () { | |
| const btn = document.getElementById('modeSwitcher'); | |
| if (!btn) return; | |
| const newBtn = btn.cloneNode(true); | |
| btn.parentNode.replaceChild(newBtn, btn); | |
| newBtn.addEventListener('click', function (e) { | |
| e.preventDefault(); | |
| e.stopPropagation(); | |
| switchTheme(); | |
| }); | |
| }); | |
| })(); | |
| </script> | |
| <script> | |
| (function () { | |
| // 1. Aplicar tema guardado al cargar | |
| function applyTheme(theme) { | |
| const lightTheme = document.getElementById('lightTheme'); | |
| const darkTheme = document.getElementById('darkTheme'); | |
| if (theme === 'dark') { | |
| lightTheme.disabled = true; | |
| darkTheme.disabled = false; | |
| } else { | |
| lightTheme.disabled = false; | |
| darkTheme.disabled = true; | |
| } | |
| } | |
| applyTheme(localStorage.getItem('theme') || 'light'); | |
| // 2. Escuchar cambios desde OTRAS pestañas | |
| window.addEventListener('storage', function(e) { | |
| if (e.key === 'theme') { | |
| applyTheme(e.newValue); | |
| } | |
| }); | |
| // 3. Overlay de transición | |
| const overlay = document.createElement('div'); | |
| overlay.id = 'theme-overlay'; | |
| overlay.style.cssText = [ | |
| 'position:fixed','inset:0','z-index:99999', | |
| 'pointer-events:none','opacity:0', | |
| 'transition:opacity 0.18s ease','background:#fff' | |
| ].join(';'); | |
| document.body.appendChild(overlay); | |
| function switchTheme() { | |
| const darkTheme = document.getElementById('darkTheme'); | |
| const goingDark = darkTheme.disabled; // si está disabled, vamos a dark | |
| overlay.style.background = goingDark ? '#fff' : '#1a1a2e'; | |
| overlay.style.opacity = '1'; | |
| setTimeout(function () { | |
| const newTheme = goingDark ? 'dark' : 'light'; | |
| applyTheme(newTheme); | |
| localStorage.setItem('theme', newTheme); // 👈 esto dispara el evento en otras pestañas | |
| setTimeout(function () { overlay.style.opacity = '0'; }, 50); | |
| }, 180); | |
| } | |
| document.addEventListener('DOMContentLoaded', function () { | |
| const btn = document.getElementById('modeSwitcher'); | |
| if (!btn) return; | |
| const newBtn = btn.cloneNode(true); | |
| btn.parentNode.replaceChild(newBtn, btn); | |
| newBtn.addEventListener('click', function (e) { | |
| e.preventDefault(); | |
| e.stopPropagation(); | |
| switchTheme(); | |
| }); | |
| }); | |
| })(); | |
| (function() { | |
| var sidebar = document.getElementById('leftSidebar'); | |
| var logo = document.getElementById('sidebarLogo'); | |
| var logoMini = document.getElementById('sidebarLogoMini'); | |
| if (!sidebar || !logo || !logoMini) return; | |
| function updateLogo() { | |
| var isCollapsed = sidebar.classList.contains('collapsed') | |
| || document.body.classList.contains('collapsed') | |
| || sidebar.offsetWidth < 100; | |
| logo.style.display = isCollapsed ? 'none' : ''; | |
| logoMini.style.display = isCollapsed ? 'block' : 'none'; | |
| } | |
| var observer = new MutationObserver(updateLogo); | |
| observer.observe(sidebar, { attributes: true, attributeFilter: ['class', 'style'] }); | |
| observer.observe(document.body, { attributes: true, attributeFilter: ['class'] }); | |
| updateLogo(); | |
| })(); | |
| </script> | |
| </body> | |
| </html> |