RetinAI / web /patients.html
ViannyCruz's picture
Update web/patients.html
19ab831 verified
Raw
History Blame Contribute Delete
62.5 kB
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="description" content="">
<meta name="author" content="">
<link rel="icon" href="favicon.ico">
<title>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 !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>
<!-- 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">&times;</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">&times;</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">&times;</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()">&times;</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>