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>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
00725ccd11
commit
0cef678848
@@ -6,21 +6,35 @@ header('Content-Type: application/json');
|
||||
|
||||
$pdo = Database::getInstance()->getConnection();
|
||||
$buscar = trim($_GET['q'] ?? '');
|
||||
$page = max(1, (int)($_GET['page'] ?? 1));
|
||||
$limit = max(1, min(200, (int)($_GET['limit'] ?? 25)));
|
||||
$offset = ($page - 1) * $limit;
|
||||
|
||||
$where = '';
|
||||
$params = [];
|
||||
|
||||
if ($buscar !== '') {
|
||||
$like = '%' . $buscar . '%';
|
||||
$stmt = $pdo->prepare(
|
||||
"SELECT id, codigo, nombres, apellidos, telefonos, email, cod_especialidad, docidmedico, activo
|
||||
FROM medicos
|
||||
WHERE nombres LIKE ? OR apellidos LIKE ? OR codigo LIKE ? OR docidmedico LIKE ?
|
||||
ORDER BY apellidos, nombres"
|
||||
);
|
||||
$stmt->execute([$like, $like, $like, $like]);
|
||||
} else {
|
||||
$stmt = $pdo->query(
|
||||
"SELECT id, codigo, nombres, apellidos, telefonos, email, cod_especialidad, docidmedico, activo
|
||||
FROM medicos ORDER BY apellidos, nombres"
|
||||
);
|
||||
$like = '%' . $buscar . '%';
|
||||
$where = "WHERE nombres LIKE ? OR apellidos LIKE ? OR codigo LIKE ? OR docidmedico LIKE ?";
|
||||
$params = [$like, $like, $like, $like];
|
||||
}
|
||||
|
||||
echo json_encode(['ok' => true, 'data' => $stmt->fetchAll(PDO::FETCH_ASSOC)]);
|
||||
$countStmt = $pdo->prepare("SELECT COUNT(*) FROM medicos $where");
|
||||
$countStmt->execute($params);
|
||||
$total = (int)$countStmt->fetchColumn();
|
||||
|
||||
$dataStmt = $pdo->prepare(
|
||||
"SELECT id, codigo, nombres, apellidos, telefonos, email, cod_especialidad, docidmedico, activo
|
||||
FROM medicos $where
|
||||
ORDER BY apellidos, nombres
|
||||
LIMIT $limit OFFSET $offset"
|
||||
);
|
||||
$dataStmt->execute($params);
|
||||
|
||||
echo json_encode([
|
||||
'ok' => true,
|
||||
'data' => $dataStmt->fetchAll(PDO::FETCH_ASSOC),
|
||||
'total' => $total,
|
||||
'page' => $page,
|
||||
'limit' => $limit,
|
||||
]);
|
||||
|
||||
@@ -29,6 +29,23 @@ $API = BASE_URL . 'modules/medicos/api/';
|
||||
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">
|
||||
@@ -60,10 +77,14 @@ $API = BASE_URL . 'modules/medicos/api/';
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="tbl-body">
|
||||
<tr id="tbl-empty"><td colspan="6">Cargando…</td></tr>
|
||||
<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>
|
||||
|
||||
@@ -150,6 +171,7 @@ $API = BASE_URL . 'modules/medicos/api/';
|
||||
<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'));
|
||||
@@ -157,24 +179,33 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
cargar();
|
||||
});
|
||||
|
||||
async function cargar(q = '') {
|
||||
const url = API + 'list.php' + (q ? '?q=' + encodeURIComponent(q) : '');
|
||||
const res = await fetch(url);
|
||||
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()), 280);
|
||||
_buscarTimer = setTimeout(() => cargar(document.getElementById('inp-buscar').value.trim(), 1), 280);
|
||||
}
|
||||
|
||||
function renderTabla(rows) {
|
||||
const body = document.getElementById('tbl-body');
|
||||
document.getElementById('lbl-total').textContent = rows.length + ' médico(s)';
|
||||
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="6">Sin resultados.</td></tr>';
|
||||
body.innerHTML = '<tr id="tbl-empty"><td colspan="7">Sin resultados.</td></tr>';
|
||||
return;
|
||||
}
|
||||
body.innerHTML = rows.map(m => `
|
||||
@@ -198,6 +229,37 @@ function renderTabla(rows) {
|
||||
</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');
|
||||
@@ -247,7 +309,7 @@ async function guardar() {
|
||||
return;
|
||||
}
|
||||
_modal.hide();
|
||||
cargar(document.getElementById('inp-buscar').value.trim());
|
||||
cargar(_q, _page);
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = '<i class="fas fa-save me-1"></i>Guardar';
|
||||
@@ -272,7 +334,7 @@ async function confirmarEliminar() {
|
||||
const json = await res.json();
|
||||
if (json.ok) {
|
||||
_modalDel.hide();
|
||||
cargar(document.getElementById('inp-buscar').value.trim());
|
||||
cargar(_q, _page);
|
||||
}
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
|
||||
Reference in New Issue
Block a user