From b78b18b65ac2eda69ceca518e54107f17e14c788 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gestor=C3=ADa=20LP?= Date: Mon, 2 Mar 2026 00:30:03 +0000 Subject: [PATCH] feat: admin processes module with CRUD and filtering Co-Authored-By: Claude Opus 4.6 --- admin/tramite-detalle.php | 585 ++++++++++++++++++++++++++++++++++++++ admin/tramites.php | 224 +++++++++++++++ 2 files changed, 809 insertions(+) create mode 100644 admin/tramite-detalle.php create mode 100644 admin/tramites.php diff --git a/admin/tramite-detalle.php b/admin/tramite-detalle.php new file mode 100644 index 0000000..eb3d5ba --- /dev/null +++ b/admin/tramite-detalle.php @@ -0,0 +1,585 @@ + '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ó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ámite no es válido.'; + } + if (!array_key_exists($estado, $estadoLabels)) { + $errors[] = 'El estado no es vá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á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ño má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ámite guardado correctamente.'; +if (isset($_GET['doc_saved'])) $success = 'Documento subido correctamente.'; +if (isset($_GET['doc_deleted'])) $success = 'Documento eliminado.'; + +$pageTitle = $isEdit ? 'Trámite: ' . ($tipoLabels[$tramite['tipo']] ?? $tramite['tipo']) : 'Nuevo Trámite'; +?> + +
+

+ + + + + + + +

+

+ + Volver a Trámites + + + + Ver Cliente: + + +

+
+ + + +
+ + + +
+ + + +
+ +
+ +
+ +
+
+ + + +
+
+

Datos del Trámite

+
+
+
+ + + +
+
+ + +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+ + +
+ +
+ + + + Creado: + + — Actualizado: + + + +
+
+
+
+ + + + +
+
+

Documentos del Trámite

+
+
+ + +
+ + + + + + + + + + + + + + + + + + + +
NombreTipoFechaAcciones
+ + + + + + +
+ + + + +
+
+
+ +

No hay documentos subidos para este trámite.

+ + + +
+

Subir Documento

+
+ + + +
+
+ + +
+
+ + + Máx. MB. Formatos: PDF, JPG, PNG, DOC, DOCX +
+
+ +
+
+
+
+ + +
+
+

Zona de Peligro

+
+
+

+ Eliminar este trámite borrará también todos los documentos asociados. Esta acción no se puede deshacer. +

+
+ + + +
+
+
+ + + + + + + diff --git a/admin/tramites.php b/admin/tramites.php new file mode 100644 index 0000000..fc0a756 --- /dev/null +++ b/admin/tramites.php @@ -0,0 +1,224 @@ + '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ó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 !== ''); +?> + +
+

Trámites

+

Gestión de procesos y trámites

+
+ + +
+
+ + + + Limpiar + + +
+ +
+ + +
+
+ +
+ +

+ + Ver todos + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + +
ClienteTipoEstadoFecha SolicitudFecha CitaPrecioAcción
+ + + + + + + + Ver + +
+
+ + + 1): ?> + + + +
+
+ +