feat: admin processes module with CRUD and filtering

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Gestoría LP
2026-03-02 00:30:03 +00:00
parent e41e07bf52
commit b78b18b65a
2 changed files with 809 additions and 0 deletions

585
admin/tramite-detalle.php Normal file
View File

@@ -0,0 +1,585 @@
<?php
$pageTitle = 'Detalle Tr&aacute;mite';
require_once __DIR__ . '/../includes/db.php';
require_once __DIR__ . '/../includes/config.php';
require_once __DIR__ . '/includes/admin-header.php';
$db = getDB();
// Label maps
$tipoLabels = [
'visa' => 'Visa',
'sentri' => 'Sentri/Global',
'pasaporte' => 'Pasaporte',
'adelanto_cita' => 'Adelanto Cita',
'doble_nacionalidad' => 'Doble Nacionalidad',
];
$estadoLabels = [
'nuevo' => 'Nuevo',
'en_proceso' => 'En Proceso',
'en_revision' => 'En Revisi&oacute;n',
'completado' => 'Completado',
'cancelado' => 'Cancelado',
];
// Determine mode: edit (id exists) or create (no id)
$tramiteId = isset($_GET['id']) ? (int)$_GET['id'] : 0;
$isEdit = $tramiteId > 0;
$tramite = null;
$errors = [];
$success = '';
// ── Handle POST actions ─────────────────────────────────────────
if ($_SERVER['REQUEST_METHOD'] === 'POST' && csrfValidate()) {
$action = $_POST['action'] ?? 'save_tramite';
switch ($action) {
// ── Save tramite (create / update) ──────────────────────
case 'save_tramite':
$clienteId = (int)($_POST['cliente_id'] ?? 0);
$tipo = trim($_POST['tipo'] ?? '');
$estado = trim($_POST['estado'] ?? 'nuevo');
$fechaSolicitud = trim($_POST['fecha_solicitud'] ?? '');
$fechaCita = trim($_POST['fecha_cita'] ?? '');
$fechaResolucion = trim($_POST['fecha_resolucion'] ?? '');
$precio = $_POST['precio'] ?? '';
$notas = trim($_POST['notas'] ?? '');
// Validate required fields
if ($clienteId <= 0) {
$errors[] = 'Debe seleccionar un cliente.';
}
if (!array_key_exists($tipo, $tipoLabels)) {
$errors[] = 'El tipo de tr&aacute;mite no es v&aacute;lido.';
}
if (!array_key_exists($estado, $estadoLabels)) {
$errors[] = 'El estado no es v&aacute;lido.';
}
// Sanitize optional dates
$fechaSolicitud = $fechaSolicitud !== '' ? $fechaSolicitud : null;
$fechaCita = $fechaCita !== '' ? $fechaCita : null;
$fechaResolucion = $fechaResolucion !== '' ? $fechaResolucion : null;
$precio = $precio !== '' ? (float)$precio : null;
if (empty($errors)) {
if ($isEdit) {
$stmt = $db->prepare("UPDATE tramites
SET cliente_id=?, tipo=?, estado=?, fecha_solicitud=?, fecha_cita=?,
fecha_resolucion=?, precio=?, notas=?, updated_at=NOW()
WHERE id=?");
$stmt->execute([
$clienteId, $tipo, $estado,
$fechaSolicitud, $fechaCita, $fechaResolucion,
$precio, $notas, $tramiteId
]);
header("Location: tramite-detalle.php?id=$tramiteId&saved=1");
exit;
} else {
$stmt = $db->prepare("INSERT INTO tramites
(cliente_id, tipo, estado, fecha_solicitud, fecha_cita, fecha_resolucion, precio, notas)
VALUES (?,?,?,?,?,?,?,?)");
$stmt->execute([
$clienteId, $tipo, $estado,
$fechaSolicitud, $fechaCita, $fechaResolucion,
$precio, $notas
]);
$newId = $db->lastInsertId();
header("Location: tramite-detalle.php?id=$newId&saved=1");
exit;
}
}
break;
// ── Upload document ─────────────────────────────────────
case 'upload_document':
if (!$isEdit) break;
$docNombre = trim($_POST['doc_nombre'] ?? '');
$file = $_FILES['doc_archivo'] ?? null;
if ($docNombre === '') {
$errors[] = 'El nombre del documento es obligatorio.';
}
if (!$file || $file['error'] !== UPLOAD_ERR_OK) {
$errors[] = 'Debe seleccionar un archivo v&aacute;lido.';
} else {
$ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
if (!in_array($ext, ALLOWED_EXTENSIONS)) {
$errors[] = 'Tipo de archivo no permitido. Permitidos: ' . implode(', ', ALLOWED_EXTENSIONS);
}
if ($file['size'] > MAX_FILE_SIZE) {
$errors[] = 'El archivo excede el tama&ntilde;o m&aacute;ximo de ' . (MAX_FILE_SIZE / 1024 / 1024) . ' MB.';
}
}
if (empty($errors)) {
// Load tramite to get cliente_id for directory
$stmtT = $db->prepare("SELECT cliente_id FROM tramites WHERE id=?");
$stmtT->execute([$tramiteId]);
$tData = $stmtT->fetch();
if ($tData) {
$uploadDir = UPLOAD_DIR . 'client_' . $tData['cliente_id'] . '/';
if (!is_dir($uploadDir)) {
mkdir($uploadDir, 0755, true);
}
$safeFilename = preg_replace('/[^a-zA-Z0-9_\-.]/', '_', $file['name']);
$filename = time() . '_' . $safeFilename;
$destPath = $uploadDir . $filename;
if (move_uploaded_file($file['tmp_name'], $destPath)) {
$rutaArchivo = 'client_' . $tData['cliente_id'] . '/' . $filename;
$stmt = $db->prepare("INSERT INTO documentos (cliente_id, tramite_id, nombre, ruta_archivo, tipo) VALUES (?,?,?,?,?)");
$stmt->execute([$tData['cliente_id'], $tramiteId, $docNombre, $rutaArchivo, $ext]);
header("Location: tramite-detalle.php?id=$tramiteId&doc_saved=1#documentos");
exit;
} else {
$errors[] = 'Error al subir el archivo. Intente de nuevo.';
}
}
}
break;
// ── Delete document ─────────────────────────────────────
case 'delete_document':
if (!$isEdit) break;
$docId = (int)($_POST['doc_id'] ?? 0);
if ($docId > 0) {
$stmt = $db->prepare("SELECT ruta_archivo FROM documentos WHERE id=? AND tramite_id=?");
$stmt->execute([$docId, $tramiteId]);
$doc = $stmt->fetch();
if ($doc) {
$filePath = UPLOAD_DIR . $doc['ruta_archivo'];
if (file_exists($filePath)) {
unlink($filePath);
}
$stmt = $db->prepare("DELETE FROM documentos WHERE id=? AND tramite_id=?");
$stmt->execute([$docId, $tramiteId]);
}
}
header("Location: tramite-detalle.php?id=$tramiteId&doc_deleted=1#documentos");
exit;
// ── Delete tramite ──────────────────────────────────────
case 'delete_tramite':
if (!$isEdit) break;
// Delete associated documents from disk
$stmtDocs = $db->prepare("SELECT ruta_archivo FROM documentos WHERE tramite_id=?");
$stmtDocs->execute([$tramiteId]);
$docsToDelete = $stmtDocs->fetchAll();
foreach ($docsToDelete as $doc) {
$filePath = UPLOAD_DIR . $doc['ruta_archivo'];
if (file_exists($filePath)) {
unlink($filePath);
}
}
// Delete document records
$stmt = $db->prepare("DELETE FROM documentos WHERE tramite_id=?");
$stmt->execute([$tramiteId]);
// Delete tramite
$stmt = $db->prepare("DELETE FROM tramites WHERE id=?");
$stmt->execute([$tramiteId]);
header("Location: tramites.php?deleted=1");
exit;
}
}
// ── Handle document download (GET) ──────────────────────────────
if (isset($_GET['download']) && $isEdit) {
$docId = (int)$_GET['download'];
$stmt = $db->prepare("SELECT nombre, ruta_archivo, tipo FROM documentos WHERE id=? AND tramite_id=?");
$stmt->execute([$docId, $tramiteId]);
$doc = $stmt->fetch();
if ($doc) {
$filePath = UPLOAD_DIR . $doc['ruta_archivo'];
if (file_exists($filePath)) {
$mimeTypes = [
'pdf' => 'application/pdf',
'jpg' => 'image/jpeg',
'jpeg' => 'image/jpeg',
'png' => 'image/png',
'doc' => 'application/msword',
'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
];
$mime = $mimeTypes[$doc['tipo']] ?? 'application/octet-stream';
$downloadName = $doc['nombre'] . '.' . $doc['tipo'];
header('Content-Type: ' . $mime);
header('Content-Disposition: attachment; filename="' . $downloadName . '"');
header('Content-Length: ' . filesize($filePath));
header('Cache-Control: no-cache, must-revalidate');
readfile($filePath);
exit;
}
}
$errors[] = 'Documento no encontrado.';
}
// ── Load tramite data for edit mode ─────────────────────────────
if ($isEdit) {
$stmt = $db->prepare("SELECT t.*, c.nombre AS cliente_nombre
FROM tramites t
LEFT JOIN clientes c ON t.cliente_id = c.id
WHERE t.id=?");
$stmt->execute([$tramiteId]);
$tramite = $stmt->fetch();
if (!$tramite) {
header('Location: tramites.php');
exit;
}
// Fetch documents linked to this tramite
$stmtDoc = $db->prepare("SELECT * FROM documentos WHERE tramite_id=? ORDER BY created_at DESC");
$stmtDoc->execute([$tramiteId]);
$documentos = $stmtDoc->fetchAll();
}
// ── Load all clients for dropdown ───────────────────────────────
$stmtClientes = $db->prepare("SELECT id, nombre FROM clientes ORDER BY nombre ASC");
$stmtClientes->execute();
$allClientes = $stmtClientes->fetchAll();
// ── Pre-select client from query param ──────────────────────────
$preClienteId = 0;
if (!$isEdit && isset($_GET['cliente_id'])) {
$preClienteId = (int)$_GET['cliente_id'];
}
// ── Preserve POST data on validation errors ─────────────────────
$prefill = [
'cliente_id' => $preClienteId,
'tipo' => '',
'estado' => 'nuevo',
'fecha_solicitud' => date('Y-m-d'),
'fecha_cita' => '',
'fecha_resolucion' => '',
'precio' => '',
'notas' => '',
];
if (!empty($errors) && ($_POST['action'] ?? '') === 'save_tramite') {
$prefill['cliente_id'] = (int)($_POST['cliente_id'] ?? 0);
$prefill['tipo'] = $_POST['tipo'] ?? '';
$prefill['estado'] = $_POST['estado'] ?? 'nuevo';
$prefill['fecha_solicitud'] = $_POST['fecha_solicitud'] ?? '';
$prefill['fecha_cita'] = $_POST['fecha_cita'] ?? '';
$prefill['fecha_resolucion'] = $_POST['fecha_resolucion'] ?? '';
$prefill['precio'] = $_POST['precio'] ?? '';
$prefill['notas'] = $_POST['notas'] ?? '';
}
// Success messages from redirects
if (isset($_GET['saved'])) $success = 'Tr&aacute;mite guardado correctamente.';
if (isset($_GET['doc_saved'])) $success = 'Documento subido correctamente.';
if (isset($_GET['doc_deleted'])) $success = 'Documento eliminado.';
$pageTitle = $isEdit ? 'Tr&aacute;mite: ' . ($tipoLabels[$tramite['tipo']] ?? $tramite['tipo']) : 'Nuevo Tr&aacute;mite';
?>
<div class="admin-content__header">
<h1>
<i class="fas fa-<?= $isEdit ? 'edit' : 'plus-circle' ?>"></i>
<?= $isEdit ? htmlspecialchars($tipoLabels[$tramite['tipo']] ?? $tramite['tipo']) : 'Nuevo Tr&aacute;mite' ?>
<?php if ($isEdit): ?>
<span class="badge badge--<?= htmlspecialchars($tramite['estado']) ?>" style="font-size: 0.6em; vertical-align: middle;">
<?= $estadoLabels[$tramite['estado']] ?? htmlspecialchars($tramite['estado']) ?>
</span>
<?php endif; ?>
</h1>
<p>
<a href="tramites.php" class="btn btn--sm btn--secondary">
<i class="fas fa-arrow-left"></i> Volver a Tr&aacute;mites
</a>
<?php if ($isEdit && $tramite['cliente_id']): ?>
<a href="cliente-detalle.php?id=<?= (int)$tramite['cliente_id'] ?>" class="btn btn--sm btn--info" style="margin-left: 0.5rem;">
<i class="fas fa-user"></i> Ver Cliente: <?= htmlspecialchars($tramite['cliente_nombre'] ?? '') ?>
</a>
<?php endif; ?>
</p>
</div>
<!-- Alerts -->
<?php if ($success): ?>
<div class="alert alert--success alert--dismissible">
<i class="fas fa-check-circle"></i>
<span><?= $success ?></span>
<button class="alert__close" onclick="this.parentElement.remove()">&times;</button>
</div>
<?php endif; ?>
<?php if (!empty($errors)): ?>
<div class="alert alert--danger">
<i class="fas fa-exclamation-circle"></i>
<div>
<?php foreach ($errors as $err): ?>
<div><?= $err ?></div>
<?php endforeach; ?>
</div>
</div>
<?php endif; ?>
<!-- ============================================================
SECTION A: Tramite Data Form
============================================================ -->
<div class="card" style="margin-bottom: 1.5rem;">
<div class="card__header">
<h2><i class="fas fa-file-alt"></i> Datos del Tr&aacute;mite</h2>
</div>
<div class="card__body">
<form method="POST" action="tramite-detalle.php<?= $isEdit ? '?id=' . $tramiteId : '' ?>">
<?= csrfField() ?>
<input type="hidden" name="action" value="save_tramite">
<div class="form-row">
<div class="form-group">
<label>Cliente <span class="required">*</span></label>
<select name="cliente_id" class="form-control" required id="clienteSelect">
<option value="">-- Seleccionar cliente --</option>
<?php
$selectedClienteId = $isEdit ? $tramite['cliente_id'] : $prefill['cliente_id'];
foreach ($allClientes as $cli):
?>
<option value="<?= (int)$cli['id'] ?>" <?= (int)$cli['id'] === $selectedClienteId ? 'selected' : '' ?>>
<?= htmlspecialchars($cli['nombre']) ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="form-group">
<label>Tipo <span class="required">*</span></label>
<select name="tipo" class="form-control" required>
<option value="">-- Seleccionar tipo --</option>
<?php
$selectedTipo = $isEdit ? $tramite['tipo'] : $prefill['tipo'];
foreach ($tipoLabels as $val => $label):
?>
<option value="<?= htmlspecialchars($val) ?>" <?= $selectedTipo === $val ? 'selected' : '' ?>>
<?= htmlspecialchars($label) ?>
</option>
<?php endforeach; ?>
</select>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label>Estado</label>
<select name="estado" class="form-control">
<?php
$selectedEstado = $isEdit ? $tramite['estado'] : $prefill['estado'];
foreach ($estadoLabels as $val => $label):
?>
<option value="<?= htmlspecialchars($val) ?>" <?= $selectedEstado === $val ? 'selected' : '' ?>>
<?= $label ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="form-group">
<label>Precio</label>
<input type="number" name="precio" class="form-control" step="0.01" min="0"
placeholder="0.00"
value="<?= htmlspecialchars($isEdit ? ($tramite['precio'] ?? '') : $prefill['precio']) ?>">
</div>
</div>
<div class="form-row">
<div class="form-group">
<label>Fecha Solicitud</label>
<input type="date" name="fecha_solicitud" class="form-control"
value="<?= htmlspecialchars($isEdit ? ($tramite['fecha_solicitud'] ?? '') : $prefill['fecha_solicitud']) ?>">
</div>
<div class="form-group">
<label>Fecha Cita</label>
<input type="date" name="fecha_cita" class="form-control"
value="<?= htmlspecialchars($isEdit ? ($tramite['fecha_cita'] ?? '') : $prefill['fecha_cita']) ?>">
</div>
<div class="form-group">
<label>Fecha Resoluci&oacute;n</label>
<input type="date" name="fecha_resolucion" class="form-control"
value="<?= htmlspecialchars($isEdit ? ($tramite['fecha_resolucion'] ?? '') : $prefill['fecha_resolucion']) ?>">
</div>
</div>
<div class="form-group">
<label>Notas</label>
<textarea name="notas" class="form-control" rows="3" placeholder="Observaciones, detalles adicionales..."><?= htmlspecialchars($isEdit ? ($tramite['notas'] ?? '') : $prefill['notas']) ?></textarea>
</div>
<div style="display: flex; gap: 0.75rem; align-items: center;">
<button type="submit" class="btn btn--primary">
<i class="fas fa-save"></i> <?= $isEdit ? 'Guardar Cambios' : 'Crear Tr&aacute;mite' ?>
</button>
<?php if ($isEdit): ?>
<span style="font-size: var(--admin-font-xs); color: var(--admin-gray-500);">
Creado: <?= date('d/m/Y H:i', strtotime($tramite['created_at'])) ?>
<?php if ($tramite['updated_at'] && $tramite['updated_at'] !== $tramite['created_at']): ?>
&mdash; Actualizado: <?= date('d/m/Y H:i', strtotime($tramite['updated_at'])) ?>
<?php endif; ?>
</span>
<?php endif; ?>
</div>
</form>
</div>
</div>
<?php if ($isEdit): ?>
<!-- ============================================================
SECTION B: Documents
============================================================ -->
<div class="card" id="documentos" style="margin-bottom: 1.5rem;">
<div class="card__header">
<h2><i class="fas fa-folder-open"></i> Documentos del Tr&aacute;mite</h2>
</div>
<div class="card__body">
<!-- Existing documents -->
<?php if (!empty($documentos)): ?>
<div class="table-responsive" style="margin-bottom: 1.5rem;">
<table class="admin-table">
<thead>
<tr>
<th>Nombre</th>
<th>Tipo</th>
<th>Fecha</th>
<th>Acciones</th>
</tr>
</thead>
<tbody>
<?php foreach ($documentos as $doc): ?>
<tr>
<td>
<i class="fas fa-file-<?= in_array($doc['tipo'], ['jpg','jpeg','png']) ? 'image' : ($doc['tipo'] === 'pdf' ? 'pdf' : 'word') ?>"></i>
<?= htmlspecialchars($doc['nombre']) ?>
</td>
<td><span class="badge badge--secondary"><?= htmlspecialchars(strtoupper($doc['tipo'] ?? 'N/A')) ?></span></td>
<td><?= date('d/m/Y', strtotime($doc['created_at'])) ?></td>
<td style="display: flex; gap: 0.5rem;">
<a href="tramite-detalle.php?id=<?= $tramiteId ?>&download=<?= (int)$doc['id'] ?>" class="btn btn--sm btn--info" title="Descargar">
<i class="fas fa-download"></i>
</a>
<form method="POST" style="display:inline;" onsubmit="return confirm('&iquest;Eliminar este documento? El archivo se borrar&aacute; permanentemente.')">
<?= csrfField() ?>
<input type="hidden" name="action" value="delete_document">
<input type="hidden" name="doc_id" value="<?= (int)$doc['id'] ?>">
<button type="submit" class="btn btn--sm btn--outline-danger" title="Eliminar">
<i class="fas fa-trash"></i>
</button>
</form>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php else: ?>
<p class="text-muted" style="margin-bottom: 1rem;">No hay documentos subidos para este tr&aacute;mite.</p>
<?php endif; ?>
<!-- Upload document form -->
<div class="detail-section">
<h3 class="detail-section__title"><i class="fas fa-upload"></i> Subir Documento</h3>
<form method="POST" action="tramite-detalle.php?id=<?= $tramiteId ?>" enctype="multipart/form-data">
<?= csrfField() ?>
<input type="hidden" name="action" value="upload_document">
<div class="form-row">
<div class="form-group">
<label>Nombre del Documento <span class="required">*</span></label>
<input type="text" name="doc_nombre" class="form-control" placeholder="Ej: Comprobante pago, Forma DS-160..." required>
</div>
<div class="form-group">
<label>Archivo <span class="required">*</span></label>
<input type="file" name="doc_archivo" class="form-control" accept=".pdf,.jpg,.jpeg,.png,.doc,.docx" required>
<span class="form-text">M&aacute;x. <?= MAX_FILE_SIZE / 1024 / 1024 ?> MB. Formatos: PDF, JPG, PNG, DOC, DOCX</span>
</div>
</div>
<button type="submit" class="btn btn--success btn--sm">
<i class="fas fa-upload"></i> Subir Documento
</button>
</form>
</div>
</div>
</div>
<!-- ============================================================
SECTION C: Delete Tramite
============================================================ -->
<div class="card" style="margin-bottom: 1.5rem; border-color: var(--admin-danger, #dc3545);">
<div class="card__header" style="background: rgba(220,53,69,0.05);">
<h2 style="color: var(--admin-danger, #dc3545);"><i class="fas fa-exclamation-triangle"></i> Zona de Peligro</h2>
</div>
<div class="card__body">
<p style="margin-bottom: 1rem; color: var(--admin-gray-600);">
Eliminar este tr&aacute;mite borrar&aacute; tambi&eacute;n todos los documentos asociados. Esta acci&oacute;n no se puede deshacer.
</p>
<form method="POST" action="tramite-detalle.php?id=<?= $tramiteId ?>"
onsubmit="return confirm('&iquest;Est&aacute; seguro de eliminar este tr&aacute;mite y todos sus documentos? Esta acci&oacute;n NO se puede deshacer.')">
<?= csrfField() ?>
<input type="hidden" name="action" value="delete_tramite">
<button type="submit" class="btn btn--danger">
<i class="fas fa-trash"></i> Eliminar Tr&aacute;mite
</button>
</form>
</div>
</div>
<?php endif; /* end isEdit sections */ ?>
<!-- JavaScript for searchable client select -->
<script>
(function() {
var select = document.getElementById('clienteSelect');
if (!select) return;
// Simple search filter for the client dropdown
var wrapper = document.createElement('div');
wrapper.style.position = 'relative';
select.parentNode.insertBefore(wrapper, select);
wrapper.appendChild(select);
var searchInput = document.createElement('input');
searchInput.type = 'text';
searchInput.className = 'form-control';
searchInput.placeholder = 'Buscar cliente...';
searchInput.style.marginBottom = '0.5rem';
wrapper.insertBefore(searchInput, select);
searchInput.addEventListener('input', function() {
var filter = this.value.toLowerCase();
var options = select.options;
for (var i = 1; i < options.length; i++) {
var text = options[i].textContent.toLowerCase();
options[i].style.display = text.indexOf(filter) > -1 ? '' : 'none';
}
});
// If client is already selected, show name in search
if (select.selectedIndex > 0) {
searchInput.value = select.options[select.selectedIndex].textContent.trim();
}
select.addEventListener('change', function() {
if (this.selectedIndex > 0) {
searchInput.value = this.options[this.selectedIndex].textContent.trim();
} else {
searchInput.value = '';
}
});
})();
</script>
<?php require_once __DIR__ . '/includes/admin-footer.php'; ?>

224
admin/tramites.php Normal file
View File

@@ -0,0 +1,224 @@
<?php
$pageTitle = 'Tr&aacute;mites';
require_once __DIR__ . '/../includes/db.php';
require_once __DIR__ . '/includes/admin-header.php';
$db = getDB();
// Label maps
$tipoLabels = [
'visa' => 'Visa',
'sentri' => 'Sentri/Global',
'pasaporte' => 'Pasaporte',
'adelanto_cita' => 'Adelanto Cita',
'doble_nacionalidad' => 'Doble Nacionalidad',
];
$estadoLabels = [
'nuevo' => 'Nuevo',
'en_proceso' => 'En Proceso',
'en_revision' => 'En Revisi&oacute;n',
'completado' => 'Completado',
'cancelado' => 'Cancelado',
];
// Filters & Pagination
$q = trim($_GET['q'] ?? '');
$fEstado = trim($_GET['estado'] ?? '');
$fTipo = trim($_GET['tipo'] ?? '');
$page = max(1, (int)($_GET['page'] ?? 1));
$perPage = 15;
$offset = ($page - 1) * $perPage;
// Validate filter values against allowed enums
$validEstados = array_keys($estadoLabels);
$validTipos = array_keys($tipoLabels);
if ($fEstado !== '' && !in_array($fEstado, $validEstados, true)) $fEstado = '';
if ($fTipo !== '' && !in_array($fTipo, $validTipos, true)) $fTipo = '';
// Build WHERE clause dynamically
$conditions = [];
$params = [];
if ($q !== '') {
$conditions[] = 'c.nombre LIKE ?';
$params[] = '%' . $q . '%';
}
if ($fEstado !== '') {
$conditions[] = 't.estado = ?';
$params[] = $fEstado;
}
if ($fTipo !== '') {
$conditions[] = 't.tipo = ?';
$params[] = $fTipo;
}
$where = '';
if (!empty($conditions)) {
$where = 'WHERE ' . implode(' AND ', $conditions);
}
// Count total
$countSql = "SELECT COUNT(*) FROM tramites t LEFT JOIN clientes c ON t.cliente_id = c.id $where";
$countStmt = $db->prepare($countSql);
$countStmt->execute($params);
$totalTramites = $countStmt->fetchColumn();
$totalPages = max(1, (int)ceil($totalTramites / $perPage));
// Ensure page is within range
if ($page > $totalPages) $page = $totalPages;
// Fetch tramites with client name
$sql = "SELECT t.*, c.nombre AS cliente_nombre
FROM tramites t
LEFT JOIN clientes c ON t.cliente_id = c.id
$where
ORDER BY t.created_at DESC
LIMIT $perPage OFFSET $offset";
$stmt = $db->prepare($sql);
$stmt->execute($params);
$tramites = $stmt->fetchAll();
// Check if any filters are active
$hasFilters = ($q !== '' || $fEstado !== '' || $fTipo !== '');
?>
<div class="admin-content__header">
<h1><i class="fas fa-file-alt"></i> Tr&aacute;mites</h1>
<p>Gesti&oacute;n de procesos y tr&aacute;mites</p>
</div>
<!-- Toolbar: Filters + New Tramite -->
<div class="toolbar">
<div class="toolbar__left">
<form method="GET" class="search-bar search-bar--wide" action="tramites.php" style="display: flex; gap: 0.75rem; align-items: center; flex-wrap: wrap;">
<div style="position: relative;">
<i class="fas fa-search search-bar__icon"></i>
<input type="text" name="q" class="search-bar__input" placeholder="Buscar por cliente..." value="<?= htmlspecialchars($q) ?>">
</div>
<select name="estado" class="form-control" style="width: auto; min-width: 140px;">
<option value="">-- Estado --</option>
<?php foreach ($estadoLabels as $val => $label): ?>
<option value="<?= htmlspecialchars($val) ?>" <?= $fEstado === $val ? 'selected' : '' ?>>
<?= $label ?>
</option>
<?php endforeach; ?>
</select>
<select name="tipo" class="form-control" style="width: auto; min-width: 160px;">
<option value="">-- Tipo --</option>
<?php foreach ($tipoLabels as $val => $label): ?>
<option value="<?= htmlspecialchars($val) ?>" <?= $fTipo === $val ? 'selected' : '' ?>>
<?= htmlspecialchars($label) ?>
</option>
<?php endforeach; ?>
</select>
<button type="submit" class="btn btn--sm btn--secondary">
<i class="fas fa-filter"></i> Filtrar
</button>
</form>
<?php if ($hasFilters): ?>
<a href="tramites.php" class="btn btn--sm btn--secondary" style="margin-left: 0.5rem;">
<i class="fas fa-times"></i> Limpiar
</a>
<?php endif; ?>
</div>
<div class="toolbar__right">
<a href="tramite-detalle.php" class="btn btn--primary">
<i class="fas fa-plus"></i> Nuevo Tr&aacute;mite
</a>
</div>
</div>
<!-- Tramites Table -->
<div class="card">
<div class="card__body">
<?php if (empty($tramites)): ?>
<div class="table-empty">
<i class="fas fa-file-alt"></i>
<p><?= $hasFilters ? 'No se encontraron tr&aacute;mites con esos filtros.' : 'No hay tr&aacute;mites registrados a&uacute;n.' ?></p>
<?php if ($hasFilters): ?>
<a href="tramites.php" class="btn btn--sm btn--outline">Ver todos</a>
<?php endif; ?>
</div>
<?php else: ?>
<div class="table-responsive">
<table class="admin-table">
<thead>
<tr>
<th>Cliente</th>
<th>Tipo</th>
<th>Estado</th>
<th>Fecha Solicitud</th>
<th>Fecha Cita</th>
<th>Precio</th>
<th>Acci&oacute;n</th>
</tr>
</thead>
<tbody>
<?php foreach ($tramites as $t): ?>
<tr>
<td>
<strong><?= htmlspecialchars($t['cliente_nombre'] ?? 'Sin cliente') ?></strong>
</td>
<td><?= htmlspecialchars($tipoLabels[$t['tipo']] ?? $t['tipo']) ?></td>
<td>
<span class="badge badge--<?= htmlspecialchars($t['estado']) ?>">
<?= $estadoLabels[$t['estado']] ?? htmlspecialchars($t['estado']) ?>
</span>
</td>
<td><?= $t['fecha_solicitud'] ? date('d/m/Y', strtotime($t['fecha_solicitud'])) : '-' ?></td>
<td><?= $t['fecha_cita'] ? date('d/m/Y', strtotime($t['fecha_cita'])) : '-' ?></td>
<td><?= $t['precio'] ? '$' . number_format((float)$t['precio'], 2) : '-' ?></td>
<td>
<a href="tramite-detalle.php?id=<?= (int)$t['id'] ?>" class="btn btn--sm btn--primary">
<i class="fas fa-eye"></i> Ver
</a>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<!-- Pagination -->
<?php if ($totalPages > 1): ?>
<div class="pagination">
<?php
$queryParams = '';
if ($q !== '') $queryParams .= '&q=' . urlencode($q);
if ($fEstado !== '') $queryParams .= '&estado=' . urlencode($fEstado);
if ($fTipo !== '') $queryParams .= '&tipo=' . urlencode($fTipo);
?>
<a href="?page=1<?= $queryParams ?>" class="pagination__link <?= $page <= 1 ? 'pagination__link--disabled' : '' ?>">
<i class="fas fa-angle-double-left"></i>
</a>
<a href="?page=<?= $page - 1 ?><?= $queryParams ?>" class="pagination__link <?= $page <= 1 ? 'pagination__link--disabled' : '' ?>">
<i class="fas fa-angle-left"></i>
</a>
<?php
$start = max(1, $page - 2);
$end = min($totalPages, $page + 2);
for ($i = $start; $i <= $end; $i++):
?>
<a href="?page=<?= $i ?><?= $queryParams ?>" class="pagination__link <?= $i === $page ? 'pagination__link--active' : '' ?>">
<?= $i ?>
</a>
<?php endfor; ?>
<a href="?page=<?= $page + 1 ?><?= $queryParams ?>" class="pagination__link <?= $page >= $totalPages ? 'pagination__link--disabled' : '' ?>">
<i class="fas fa-angle-right"></i>
</a>
<a href="?page=<?= $totalPages ?><?= $queryParams ?>" class="pagination__link <?= $page >= $totalPages ? 'pagination__link--disabled' : '' ?>">
<i class="fas fa-angle-double-right"></i>
</a>
<span class="pagination__info">
P&aacute;gina <?= $page ?> de <?= $totalPages ?> (<?= $totalTramites ?> tr&aacute;mites)
</span>
</div>
<?php endif; ?>
<?php endif; ?>
</div>
</div>
<?php require_once __DIR__ . '/includes/admin-footer.php'; ?>