| <!doctype html> |
| <html lang="en"> |
| <head> |
| <meta charset="utf-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> |
| <meta name="description" content=""> |
| <meta name="author" content=""> |
| <link rel="icon" href="favicon.ico"> |
| <title>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-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 !important; |
| } |
| </style> |
| </head> |
| <body class="vertical light"> |
| <div class="wrapper"> |
| <nav class="topnav navbar navbar-light"> |
| <button type="button" class="navbar-toggler text-muted mt-2 p-0 mr-3 collapseSidebar"> |
| <i data-feather="menu" class="navbar-toggler-icon"></i> |
| </button> |
|
|
| <ul class="nav"> |
| <li class="nav-item"> |
| <a class="nav-link text-muted my-2" href="#" id="modeSwitcher" data-mode="light"> |
| <i data-feather="sun"></i> |
| </a> |
| </li> |
| <li class="nav-item"> |
| <a href="#" onclick="confirmLogout()" class="nav-link text-muted" title="Cerrar sesión" style="display: flex; align-items: center; gap: 6px;"> |
| <span class="avatar avatar-sm mt-2"> |
| <img src="./assets/images/persona.png" alt="..." class="avatar-img rounded-circle"> |
| </span> |
| <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" title="Cerrar sesión"> |
| <path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/> |
| <polyline points="16 17 21 12 16 7"/> |
| <line x1="21" y1="12" x2="9" y2="12"/> |
| </svg> |
| </a> |
| </li> |
| </ul> |
| </nav> |
|
|
| |
| <aside class="sidebar-left border-right bg-white shadow" id="leftSidebar" data-simplebar> |
| <a href="#" class="btn collapseSidebar toggle-btn d-lg-none text-muted ml-2 mt-3" data-toggle="toggle"> |
| <i data-feather="x"><span class="sr-only"></span></i> |
| </a> |
| <nav class="vertnav navbar navbar-light"> |
| |
| <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> |
|
|
|
|
| |
| <main role="main" class="main-content"> |
| <div class="container-fluid"> |
| <div class="row justify-content-center"> |
| <div class="col-12"> |
| |
| <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> |
|
|
| |
| <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> |
|
|
| |
| <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> |
|
|
| |
| <div class="row" id="patientsContainer"> |
| |
| </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> |
|
|
| |
| <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"> |
| |
| <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> |
|
|
| |
| <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> |
|
|
| |
| <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"> |
| |
| </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> |
|
|
| |
| <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(); |
| 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() { |
| |
| |
| $('#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 () { |
| |
| 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'); |
| |
| |
| window.addEventListener('storage', function(e) { |
| if (e.key === 'theme') { |
| applyTheme(e.newValue); |
| } |
| }); |
| |
| |
| 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; |
| overlay.style.background = goingDark ? '#fff' : '#1a1a2e'; |
| overlay.style.opacity = '1'; |
| setTimeout(function () { |
| const newTheme = goingDark ? 'dark' : 'light'; |
| applyTheme(newTheme); |
| localStorage.setItem('theme', newTheme); |
| 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> |