Files
whatsapp/modules/medicos/views/index.php
T
Lizandro GuarnizoandClaude Sonnet 4.6 0cef678848 Add server-side pagination to medicos list (25 per page)
list.php now accepts page/limit params and returns total count.
View shows X-Y de N label, page buttons with ellipsis for large ranges,
and prev/next arrows. Search resets to page 1; save/delete stay on
current page.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-03 14:24:55 -05:00

350 lines
16 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
require_once APP_ROOT . '/config/config.php';
if (!isUserLoggedIn()) { header('Location: ' . BASE_URL . 'login.php'); exit; }
Layout::open('Médicos', 'fas fa-user-md');
$API = BASE_URL . 'modules/medicos/api/';
?>
<style>
body { background: #f1f5f9; }
.page-header {
background: #fff; border-bottom: 1px solid #e2e8f0;
padding: 14px 24px; display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
}
.page-header h1 { font-size: 1.18rem; font-weight: 700; color: #1e293b; margin: 0; flex: 1; }
.content-wrap { max-width: 1100px; margin: 24px auto; padding: 0 16px 48px; }
.card-box { background: #fff; border: 1px solid #e2e8f0; border-radius: 12px; overflow: hidden; }
.card-box .toolbar {
padding: 14px 16px; display: flex; gap: 10px; align-items: center;
border-bottom: 1px solid #e2e8f0; flex-wrap: wrap;
}
.card-box .toolbar input { max-width: 260px; }
table { width: 100%; border-collapse: collapse; font-size: .9rem; }
thead th { background: #f8fafc; padding: 10px 14px; font-size: .72rem; text-transform: uppercase;
letter-spacing: .07em; color: #64748b; border-bottom: 1px solid #e2e8f0; text-align: left; }
tbody td { padding: 10px 14px; border-bottom: 1px solid #f1f5f9; color: #1e293b; vertical-align: middle; }
tbody tr:last-child td { border-bottom: none; }
tbody tr:hover td { background: #f8fafc; }
.badge-esp { background: #eff6ff; color: #1d4ed8; border-radius: 20px; padding: 2px 10px;
font-size: .75rem; font-weight: 600; }
.acciones { display: flex; gap: 6px; }
#tbl-empty { text-align: center; padding: 3rem; color: #94a3b8; }
.pag-bar {
display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: .5rem;
padding: 10px 16px; border-top: 1px solid #e2e8f0; background: #f8fafc;
}
.pag-bar .pag-info { font-size: .8rem; color: #64748b; }
.pag-controls { display: flex; gap: 4px; align-items: center; }
.pag-btn {
min-width: 32px; height: 32px; padding: 0 8px;
border: 1px solid #e2e8f0; border-radius: 7px; background: #fff;
font-size: .82rem; color: #374151; cursor: pointer; font-weight: 500;
display: flex; align-items: center; justify-content: center;
transition: background .12s, border-color .12s;
}
.pag-btn:hover:not(:disabled) { background: #eff6ff; border-color: #bfdbfe; color: #1d4ed8; }
.pag-btn.active { background: #2563eb; color: #fff; border-color: #2563eb; }
.pag-btn:disabled { opacity: .4; cursor: not-allowed; }
.pag-ellipsis { font-size: .82rem; color: #94a3b8; padding: 0 4px; line-height: 32px; }
</style>
<div class="page-header">
<i class="fas fa-user-md" style="font-size:1.3rem;color:#6366f1"></i>
<h1>Médicos</h1>
<button class="btn btn-primary btn-sm ms-auto" onclick="abrirModal()">
<i class="fas fa-plus me-1"></i>Nuevo médico
</button>
</div>
<div class="content-wrap">
<div class="card-box">
<div class="toolbar">
<input type="search" id="inp-buscar" class="form-control form-control-sm"
placeholder="Buscar por nombre, código o documento…" oninput="buscar()">
<span class="text-muted small ms-auto" id="lbl-total"></span>
</div>
<div style="overflow-x:auto">
<table id="tbl-medicos">
<thead>
<tr>
<th>Código</th>
<th>Nombres</th>
<th>Apellidos</th>
<th>Especialidad</th>
<th>Teléfonos</th>
<th>Doc. ID</th>
<th></th>
</tr>
</thead>
<tbody id="tbl-body">
<tr id="tbl-empty"><td colspan="7">Cargando…</td></tr>
</tbody>
</table>
</div>
<div class="pag-bar" id="pag-bar" style="display:none">
<span class="pag-info" id="pag-info"></span>
<div class="pag-controls" id="pag-controls"></div>
</div>
</div>
</div>
<!-- ══ Modal agregar / editar ══════════════════════════════════════ -->
<div class="modal fade" id="modalMedico" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="modal-titulo">Nuevo médico</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div id="modal-alert" class="alert alert-danger d-none py-2"></div>
<input type="hidden" id="med-id">
<div class="row g-3">
<div class="col-4">
<label class="form-label small fw-semibold">Código <span class="text-danger">*</span></label>
<input type="text" id="med-codigo" class="form-control form-control-sm"
placeholder="Ej: MED001" maxlength="20">
</div>
<div class="col-8">
<label class="form-label small fw-semibold">Doc. ID Médico</label>
<input type="text" id="med-docid" class="form-control form-control-sm"
placeholder="Número de documento" maxlength="30">
</div>
<div class="col-6">
<label class="form-label small fw-semibold">Nombres <span class="text-danger">*</span></label>
<input type="text" id="med-nombres" class="form-control form-control-sm"
placeholder="Nombres" maxlength="100">
</div>
<div class="col-6">
<label class="form-label small fw-semibold">Apellidos <span class="text-danger">*</span></label>
<input type="text" id="med-apellidos" class="form-control form-control-sm"
placeholder="Apellidos" maxlength="100">
</div>
<div class="col-6">
<label class="form-label small fw-semibold">Teléfonos</label>
<input type="text" id="med-telefonos" class="form-control form-control-sm"
placeholder="Teléfonos" maxlength="50">
</div>
<div class="col-6">
<label class="form-label small fw-semibold">Email</label>
<input type="email" id="med-email" class="form-control form-control-sm"
placeholder="correo@ejemplo.com" maxlength="100">
</div>
<div class="col-12">
<label class="form-label small fw-semibold">Código especialidad</label>
<input type="text" id="med-esp" class="form-control form-control-sm"
placeholder="Ej: GEN, PED, GIN…" maxlength="10">
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary btn-sm" data-bs-dismiss="modal">Cancelar</button>
<button type="button" class="btn btn-primary btn-sm" id="btn-guardar" onclick="guardar()">
<i class="fas fa-save me-1"></i>Guardar
</button>
</div>
</div>
</div>
</div>
<!-- ══ Modal confirmar eliminar ══════════════════════════════════════ -->
<div class="modal fade" id="modalEliminar" tabindex="-1">
<div class="modal-dialog modal-sm">
<div class="modal-content">
<div class="modal-header border-0 pb-0">
<h5 class="modal-title text-danger"><i class="fas fa-trash me-1"></i>Eliminar médico</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
¿Eliminar a <strong id="del-nombre"></strong>? Esta acción no se puede deshacer.
</div>
<div class="modal-footer border-0 pt-0">
<button type="button" class="btn btn-secondary btn-sm" data-bs-dismiss="modal">Cancelar</button>
<button type="button" class="btn btn-danger btn-sm" id="btn-confirmar-del" onclick="confirmarEliminar()">
Eliminar
</button>
</div>
</div>
</div>
</div>
<script>
const API = '<?= $API ?>';
let _modal, _modalDel, _pendingDelId, _buscarTimer;
let _page = 1, _limit = 25, _total = 0, _q = '';
document.addEventListener('DOMContentLoaded', () => {
_modal = new bootstrap.Modal(document.getElementById('modalMedico'));
_modalDel = new bootstrap.Modal(document.getElementById('modalEliminar'));
cargar();
});
async function cargar(q = _q, page = _page) {
_q = q;
_page = page;
const params = new URLSearchParams({ page, limit: _limit });
if (q) params.set('q', q);
const res = await fetch(API + 'list.php?' + params);
const json = await res.json();
if (!json.ok) return;
_total = json.total;
_page = json.page;
renderTabla(json.data);
renderPaginacion();
}
function buscar() {
clearTimeout(_buscarTimer);
_buscarTimer = setTimeout(() => cargar(document.getElementById('inp-buscar').value.trim(), 1), 280);
}
function renderTabla(rows) {
const body = document.getElementById('tbl-body');
const desde = (_page - 1) * _limit + 1;
const hasta = Math.min(_page * _limit, _total);
document.getElementById('lbl-total').textContent =
_total ? `${desde}${hasta} de ${_total} médico(s)` : '0 médicos';
if (!rows.length) {
body.innerHTML = '<tr id="tbl-empty"><td colspan="7">Sin resultados.</td></tr>';
return;
}
body.innerHTML = rows.map(m => `
<tr>
<td><code>${esc(m.codigo)}</code></td>
<td>${esc(m.nombres)}</td>
<td>${esc(m.apellidos)}</td>
<td>${m.cod_especialidad ? `<span class="badge-esp">${esc(m.cod_especialidad)}</span>` : '<span class="text-muted">—</span>'}</td>
<td style="font-size:.82rem;color:#475569">${esc(m.telefonos || '—')}</td>
<td>${esc(m.docidmedico || '—')}</td>
<td>
<div class="acciones">
<button class="btn btn-outline-primary btn-sm py-0 px-2" onclick='abrirEditar(${JSON.stringify(m)})'>
<i class="fas fa-pencil-alt"></i>
</button>
<button class="btn btn-outline-danger btn-sm py-0 px-2" onclick="pedirEliminar(${m.id}, '${esc(m.nombres)} ${esc(m.apellidos)}')">
<i class="fas fa-trash"></i>
</button>
</div>
</td>
</tr>`).join('');
}
function renderPaginacion() {
const totalPages = Math.ceil(_total / _limit);
const bar = document.getElementById('pag-bar');
const info = document.getElementById('pag-info');
const ctrl = document.getElementById('pag-controls');
if (totalPages <= 1) { bar.style.display = 'none'; return; }
bar.style.display = 'flex';
info.textContent = `Página ${_page} de ${totalPages}`;
// Compute page window: always show first, last, current ±2
const pages = new Set([1, totalPages, _page]);
for (let i = _page - 2; i <= _page + 2; i++) if (i > 0 && i <= totalPages) pages.add(i);
const sorted = [...pages].sort((a, b) => a - b);
let html = '';
html += `<button class="pag-btn" onclick="cargar(_q,${_page-1})" ${_page===1?'disabled':''}>
<i class="fas fa-chevron-left"></i>
</button>`;
let prev = 0;
for (const p of sorted) {
if (prev && p - prev > 1) html += `<span class="pag-ellipsis">…</span>`;
html += `<button class="pag-btn ${p===_page?'active':''}" onclick="cargar(_q,${p})">${p}</button>`;
prev = p;
}
html += `<button class="pag-btn" onclick="cargar(_q,${_page+1})" ${_page===totalPages?'disabled':''}>
<i class="fas fa-chevron-right"></i>
</button>`;
ctrl.innerHTML = html;
}
function abrirModal(m = null) {
document.getElementById('modal-titulo').textContent = m ? 'Editar médico' : 'Nuevo médico';
document.getElementById('modal-alert').classList.add('d-none');
document.getElementById('med-id').value = m?.id ?? '';
document.getElementById('med-codigo').value = m?.codigo ?? '';
document.getElementById('med-nombres').value = m?.nombres ?? '';
document.getElementById('med-apellidos').value = m?.apellidos ?? '';
document.getElementById('med-telefonos').value = m?.telefonos ?? '';
document.getElementById('med-email').value = m?.email ?? '';
document.getElementById('med-esp').value = m?.cod_especialidad ?? '';
document.getElementById('med-docid').value = m?.docidmedico ?? '';
_modal.show();
setTimeout(() => document.getElementById('med-codigo').focus(), 300);
}
function abrirEditar(m) { abrirModal(m); }
async function guardar() {
const alerta = document.getElementById('modal-alert');
alerta.classList.add('d-none');
const payload = {
id: document.getElementById('med-id').value || null,
codigo: document.getElementById('med-codigo').value.trim(),
nombres: document.getElementById('med-nombres').value.trim(),
apellidos: document.getElementById('med-apellidos').value.trim(),
telefonos: document.getElementById('med-telefonos').value.trim(),
email: document.getElementById('med-email').value.trim(),
cod_especialidad: document.getElementById('med-esp').value.trim(),
docidmedico: document.getElementById('med-docid').value.trim(),
};
const btn = document.getElementById('btn-guardar');
btn.disabled = true;
btn.innerHTML = '<i class="fas fa-spinner fa-spin me-1"></i>Guardando…';
try {
const res = await fetch(API + 'save.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
const json = await res.json();
if (!json.ok) {
alerta.textContent = json.error;
alerta.classList.remove('d-none');
return;
}
_modal.hide();
cargar(_q, _page);
} finally {
btn.disabled = false;
btn.innerHTML = '<i class="fas fa-save me-1"></i>Guardar';
}
}
function pedirEliminar(id, nombre) {
_pendingDelId = id;
document.getElementById('del-nombre').textContent = nombre;
_modalDel.show();
}
async function confirmarEliminar() {
const btn = document.getElementById('btn-confirmar-del');
btn.disabled = true;
try {
const res = await fetch(API + 'delete.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: _pendingDelId }),
});
const json = await res.json();
if (json.ok) {
_modalDel.hide();
cargar(_q, _page);
}
} finally {
btn.disabled = false;
}
}
function esc(str) {
if (!str) return '';
return String(str).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}
</script>
<?php Layout::close(); ?>