feat: CRUD de ciudades + select en modal de paciente
- Tabla lab_ciudades creada y poblada con 86 ciudades existentes - Módulo lab_ciudades: listado, crear, editar, activar/desactivar, eliminar - API /api/lab/ciudades.php: GET list, POST save/toggle/delete - Modal paciente: campo ciudad cambiado a select, cargado junto con EPS en un solo Promise.all al iniciar; Cúcuta preseleccionada por defecto Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
da2bfde38f
commit
4cb1a135b3
@@ -0,0 +1,12 @@
|
||||
<?php return [
|
||||
'slug' => 'lab_ciudades',
|
||||
'name' => 'Ciudades',
|
||||
'icon' => 'fas fa-map-marker-alt',
|
||||
'category' => 'lab',
|
||||
'route' => '/lab_ciudades.php',
|
||||
'is_active' => true,
|
||||
'sort_order' => 23,
|
||||
'oleada' => 0,
|
||||
'description' => 'Gestión de ciudades de pacientes',
|
||||
'links' => [['name' => 'Ciudades', 'icon' => 'fas fa-map-marker-alt', 'route' => '/lab_ciudades.php']],
|
||||
];
|
||||
@@ -0,0 +1,149 @@
|
||||
<?php
|
||||
if (!isUserLoggedIn()) { header('Location: ' . BASE_URL . 'login.php'); exit; }
|
||||
requireRole('admin');
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="es">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Ciudades — <?= htmlspecialchars($_cfg['empresa_nombre'] ?? 'ERP') ?></title>
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<link href="<?= BASE_URL ?>assets/css/styles.css?v=15" rel="stylesheet">
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
|
||||
<style>
|
||||
.ciu-table { width:100%; border-collapse:collapse; font-size:.9rem; }
|
||||
.ciu-table th { background:#f8fafc; font-weight:700; font-size:.75rem; text-transform:uppercase;
|
||||
letter-spacing:.06em; color:#64748b; padding:.6rem 1rem; border-bottom:2px solid #e2e8f0; }
|
||||
.ciu-table td { padding:.65rem 1rem; border-bottom:1px solid #f1f5f9; vertical-align:middle; }
|
||||
.ciu-table tr:hover td { background:#f8fafc; }
|
||||
.badge-activa { background:#dcfce7; color:#166534; font-size:.7rem; font-weight:700; padding:2px 8px; border-radius:99px; }
|
||||
.badge-inactiva { background:#f1f5f9; color:#94a3b8; font-size:.7rem; font-weight:700; padding:2px 8px; border-radius:99px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<?php include APP_ROOT . '/partials/navbar.php'; ?>
|
||||
|
||||
<div class="container-fluid py-4" style="max-width:720px">
|
||||
<div class="d-flex align-items-center justify-content-between mb-3">
|
||||
<h5 class="mb-0"><i class="fas fa-map-marker-alt me-2 text-primary"></i>Ciudades</h5>
|
||||
<button class="btn btn-primary btn-sm" onclick="abrirModal()">
|
||||
<i class="fas fa-plus me-1"></i>Nueva ciudad
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body p-0">
|
||||
<table class="ciu-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Nombre</th>
|
||||
<th style="width:90px">Estado</th>
|
||||
<th style="width:110px">Acciones</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="tbody-ciu">
|
||||
<tr><td colspan="3" class="text-muted text-center py-3">Cargando…</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal -->
|
||||
<div class="modal fade" id="modal-ciu" tabindex="-1">
|
||||
<div class="modal-dialog modal-sm">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h6 class="modal-title" id="modal-ciu-titulo">Nueva ciudad</h6>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<input type="hidden" id="ciu-id">
|
||||
<div class="mb-3">
|
||||
<label class="form-label small fw-semibold">Nombre <span class="text-danger">*</span></label>
|
||||
<input type="text" id="ciu-nombre" class="form-control" maxlength="100" placeholder="Ej: Cúcuta, Bogotá…">
|
||||
</div>
|
||||
<div id="ciu-error" class="text-danger small d-none"></div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-secondary btn-sm" data-bs-dismiss="modal">Cancelar</button>
|
||||
<button class="btn btn-primary btn-sm" onclick="guardar()">
|
||||
<i class="fas fa-save me-1"></i>Guardar
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const API = '<?= BASE_URL ?>api/lab/ciudades.php';
|
||||
let _modal;
|
||||
|
||||
async function cargar() {
|
||||
const r = await fetch(API + '?action=list');
|
||||
const j = await r.json();
|
||||
const tbody = document.getElementById('tbody-ciu');
|
||||
if (!j.ok || !j.ciudades.length) {
|
||||
tbody.innerHTML = '<tr><td colspan="3" class="text-muted text-center py-3">Sin registros</td></tr>';
|
||||
return;
|
||||
}
|
||||
tbody.innerHTML = j.ciudades.map(c => `
|
||||
<tr>
|
||||
<td>${escHtml(c.nombre)}</td>
|
||||
<td><span class="badge-${c.activa == 1 ? 'activa' : 'inactiva'}">${c.activa == 1 ? 'Activa' : 'Inactiva'}</span></td>
|
||||
<td class="d-flex gap-1">
|
||||
<button class="btn btn-outline-secondary btn-sm" onclick="abrirModal(${c.id},'${escHtml(c.nombre).replace(/'/g,"\\'")}')"><i class="fas fa-pen"></i></button>
|
||||
<button class="btn btn-outline-${c.activa == 1 ? 'warning' : 'success'} btn-sm" onclick="toggle(${c.id})"><i class="fas fa-${c.activa == 1 ? 'ban' : 'check'}"></i></button>
|
||||
<button class="btn btn-outline-danger btn-sm" onclick="eliminar(${c.id},'${escHtml(c.nombre).replace(/'/g,"\\'")}')"><i class="fas fa-trash"></i></button>
|
||||
</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
function abrirModal(id = 0, nombre = '') {
|
||||
document.getElementById('ciu-id').value = id;
|
||||
document.getElementById('ciu-nombre').value = nombre;
|
||||
document.getElementById('ciu-error').classList.add('d-none');
|
||||
document.getElementById('modal-ciu-titulo').textContent = id ? 'Editar ciudad' : 'Nueva ciudad';
|
||||
_modal = _modal || new bootstrap.Modal(document.getElementById('modal-ciu'));
|
||||
_modal.show();
|
||||
setTimeout(() => document.getElementById('ciu-nombre').focus(), 300);
|
||||
}
|
||||
|
||||
async function guardar() {
|
||||
const nombre = document.getElementById('ciu-nombre').value.trim();
|
||||
const id = parseInt(document.getElementById('ciu-id').value) || 0;
|
||||
const errEl = document.getElementById('ciu-error');
|
||||
errEl.classList.add('d-none');
|
||||
if (!nombre) { errEl.textContent = 'El nombre es requerido.'; errEl.classList.remove('d-none'); return; }
|
||||
const r = await fetch(API, { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({action:'save', id, nombre}) });
|
||||
const j = await r.json();
|
||||
if (!j.ok) { errEl.textContent = j.error || 'Error'; errEl.classList.remove('d-none'); return; }
|
||||
_modal.hide();
|
||||
cargar();
|
||||
}
|
||||
|
||||
async function toggle(id) {
|
||||
await fetch(API, { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({action:'toggle', id}) });
|
||||
cargar();
|
||||
}
|
||||
|
||||
async function eliminar(id, nombre) {
|
||||
if (!confirm(`¿Eliminar "${nombre}"?`)) return;
|
||||
const r = await fetch(API, { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({action:'delete', id}) });
|
||||
const j = await r.json();
|
||||
if (!j.ok) { alert(j.error); return; }
|
||||
cargar();
|
||||
}
|
||||
|
||||
function escHtml(str) {
|
||||
const d = document.createElement('div');
|
||||
d.appendChild(document.createTextNode(String(str)));
|
||||
return d.innerHTML;
|
||||
}
|
||||
|
||||
document.getElementById('ciu-nombre').addEventListener('keydown', e => { if (e.key === 'Enter') guardar(); });
|
||||
cargar();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1008,7 +1008,9 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
</div>
|
||||
<div class="col-sm-4">
|
||||
<div class="form-floating">
|
||||
<input type="text" class="form-control form-control-sm" id="mpac-ciudad" placeholder="Ciudad" value="Cúcuta">
|
||||
<select class="form-select form-select-sm" id="mpac-ciudad">
|
||||
<option value="Cúcuta">Cúcuta</option>
|
||||
</select>
|
||||
<label>Ciudad</label>
|
||||
</div>
|
||||
</div>
|
||||
@@ -2326,17 +2328,29 @@ function fmt(n) {
|
||||
|
||||
const BASE_URL_API_LAB = '<?= BASE_URL ?>/api/lab/';
|
||||
|
||||
// ── Cargar EPS una vez al inicio ─────────────────────────────
|
||||
// ── Cargar EPS y ciudades una vez al inicio ───────────────────
|
||||
(async function() {
|
||||
try {
|
||||
const r = await fetch(BASE_URL_API_LAB + 'eps.php?action=list&solo_activas=1');
|
||||
const j = await r.json();
|
||||
if (!j.ok) return;
|
||||
const sel = document.getElementById('mpac-eps');
|
||||
(j.eps || []).forEach(e => {
|
||||
const [rEps, rCiu] = await Promise.all([
|
||||
fetch(BASE_URL_API_LAB + 'eps.php?action=list&solo_activas=1'),
|
||||
fetch(BASE_URL_API_LAB + 'ciudades.php?action=list&solo_activas=1'),
|
||||
]);
|
||||
const [jEps, jCiu] = await Promise.all([rEps.json(), rCiu.json()]);
|
||||
|
||||
const selEps = document.getElementById('mpac-eps');
|
||||
(jEps.eps || []).forEach(e => {
|
||||
const o = document.createElement('option');
|
||||
o.value = e.nombre; o.textContent = e.nombre;
|
||||
sel.appendChild(o);
|
||||
selEps.appendChild(o);
|
||||
});
|
||||
|
||||
const selCiu = document.getElementById('mpac-ciudad');
|
||||
selCiu.innerHTML = '<option value="">— Seleccionar —</option>';
|
||||
(jCiu.ciudades || []).forEach(c => {
|
||||
const o = document.createElement('option');
|
||||
o.value = c.nombre; o.textContent = c.nombre;
|
||||
if (c.nombre === 'Cúcuta') o.selected = true;
|
||||
selCiu.appendChild(o);
|
||||
});
|
||||
} catch(_) {}
|
||||
})();
|
||||
@@ -2572,7 +2586,8 @@ async function abrirModalPaciente(id, nombrePrefill) {
|
||||
document.getElementById('mpac-avatar').textContent = '?';
|
||||
document.getElementById('mpac-wa-badge').innerHTML = '';
|
||||
document.getElementById('mpac-user-id').value = '';
|
||||
document.getElementById('mpac-ciudad').value = 'Cúcuta';
|
||||
document.getElementById('mpac-ciudad').value = 'Cúcuta';
|
||||
document.getElementById('mpac-eps').value = '';
|
||||
|
||||
if (id) {
|
||||
document.getElementById('mpac-titulo').textContent = 'Cargando…';
|
||||
|
||||
Reference in New Issue
Block a user