From 676e8981b83d007baa669b3830408d394c360f1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gestor=C3=ADa=20LP?= Date: Mon, 2 Mar 2026 00:33:31 +0000 Subject: [PATCH] feat: admin requests inbox with conversion to clients Add solicitudes list page with estado filtering, pagination, and new request count badge. Add detail page showing all form data with actions to change status, create new client, link to existing client, and create tramite from linked client. Co-Authored-By: Claude Opus 4.6 --- admin/solicitud-detalle.php | 404 ++++++++++++++++++++++++++++++++++++ admin/solicitudes.php | 213 +++++++++++++++++++ 2 files changed, 617 insertions(+) create mode 100644 admin/solicitud-detalle.php create mode 100644 admin/solicitudes.php diff --git a/admin/solicitud-detalle.php b/admin/solicitud-detalle.php new file mode 100644 index 0000000..28ff6f7 --- /dev/null +++ b/admin/solicitud-detalle.php @@ -0,0 +1,404 @@ + 'Visa', + 'sentri' => 'Sentri/Global', + 'pasaporte' => 'Pasaporte', + 'adelanto_cita' => 'Adelanto Cita', + 'doble_nacionalidad' => 'Doble Nacionalidad', +]; +$estadoSolicitudLabels = [ + 'nueva' => 'Nueva', + 'contactada' => 'Contactada', + 'convertida' => 'Convertida', + 'descartada' => 'Descartada', +]; +$estadoBadgeClass = [ + 'nueva' => 'primary', + 'contactada' => 'warning', + 'convertida' => 'success', + 'descartada' => 'danger', +]; + +// Must have an ID +$solicitudId = isset($_GET['id']) ? (int)$_GET['id'] : 0; +if ($solicitudId <= 0) { + header('Location: solicitudes.php'); + exit; +} + +$errors = []; +$success = ''; + +// ── Handle POST actions ───────────────────────────────────────── +if ($_SERVER['REQUEST_METHOD'] === 'POST' && csrfValidate()) { + $action = $_POST['action'] ?? ''; + + switch ($action) { + + // ── Update estado ──────────────────────────────────────── + case 'update_estado': + $nuevoEstado = trim($_POST['estado'] ?? ''); + if (!array_key_exists($nuevoEstado, $estadoSolicitudLabels)) { + $errors[] = 'El estado seleccionado no es válido.'; + } else { + $stmt = $db->prepare("UPDATE solicitudes SET estado = ? WHERE id = ?"); + $stmt->execute([$nuevoEstado, $solicitudId]); + header("Location: solicitud-detalle.php?id=$solicitudId&estado_saved=1"); + exit; + } + break; + + // ── Vincular a cliente existente ───────────────────────── + case 'vincular_cliente': + $clienteId = (int)($_POST['cliente_id'] ?? 0); + if ($clienteId <= 0) { + $errors[] = 'Debe seleccionar un cliente.'; + } else { + // Verify client exists + $stmtCheck = $db->prepare("SELECT id FROM clientes WHERE id = ?"); + $stmtCheck->execute([$clienteId]); + if (!$stmtCheck->fetch()) { + $errors[] = 'El cliente seleccionado no existe.'; + } else { + $stmt = $db->prepare("UPDATE solicitudes SET cliente_id = ?, estado = 'convertida' WHERE id = ?"); + $stmt->execute([$clienteId, $solicitudId]); + header("Location: solicitud-detalle.php?id=$solicitudId&vinculado=1"); + exit; + } + } + break; + } +} + +// ── Load solicitud data ────────────────────────────────────────── +$stmt = $db->prepare("SELECT s.*, c.nombre AS cliente_nombre + FROM solicitudes s + LEFT JOIN clientes c ON s.cliente_id = c.id + WHERE s.id = ?"); +$stmt->execute([$solicitudId]); +$solicitud = $stmt->fetch(); + +if (!$solicitud) { + header('Location: solicitudes.php'); + exit; +} + +// Decode form data +$datosFormulario = []; +if (!empty($solicitud['datos_formulario'])) { + $datosFormulario = json_decode($solicitud['datos_formulario'], true) ?: []; +} + +// Build human-readable labels for form fields from servicios-data +$campoLabels = []; +$servicio = $solicitud['servicio'] ?? ''; +if (isset($SERVICIOS[$servicio]['campos_formulario'])) { + foreach ($SERVICIOS[$servicio]['campos_formulario'] as $campo) { + $campoLabels[$campo['name']] = $campo['label']; + } +} + +// Load all clients for vincular dropdown (only if not already linked) +$allClientes = []; +if (empty($solicitud['cliente_id'])) { + $stmtClientes = $db->prepare("SELECT id, nombre FROM clientes ORDER BY nombre ASC"); + $stmtClientes->execute(); + $allClientes = $stmtClientes->fetchAll(); +} + +// Success messages from redirects +if (isset($_GET['estado_saved'])) $success = 'Estado actualizado correctamente.'; +if (isset($_GET['vinculado'])) $success = 'Solicitud vinculada al cliente y marcada como convertida.'; + +$pageTitle = 'Solicitud: ' . $solicitud['nombre']; +?> + +
+

+ + + + + +

+

+ + Volver a Solicitudes + +

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

Datos de la Solicitud

+
+
+
+
+ Nombre + +
+
+ Teléfono + + + + + - + + +
+
+ Email + + + + + - + + +
+
+ Servicio + +
+
+ Fecha de Solicitud + +
+
+ Estado Actual + + + + + +
+
+
+
+ + + +
+
+

Datos del Formulario

+
+
+
+ $value): ?> + +
+ + +
+ +
+
+
+ + + + +
+
+

Cliente Vinculado

+
+
+

+ Esta solicitud está vinculada al cliente: + + + +

+ + Crear Trámite + +
+
+ + + +
+
+

Acciones

+
+
+ + +
+

Cambiar Estado

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

Crear Cliente Nuevo

+

+ Se creará un nuevo cliente con los datos de esta solicitud (nombre, teléfono, email). +

+ + Crear Cliente Nuevo + +
+ + +
+

Vincular a Cliente Existente

+

+ Vincule esta solicitud a un cliente ya registrado. El estado cambiará automáticamente a “Convertida”. +

+ +
+ + +
+ +
+ +
+ +

No hay clientes registrados. Crear uno nuevo.

+ +
+ + +
+
+ + + + + + + + diff --git a/admin/solicitudes.php b/admin/solicitudes.php new file mode 100644 index 0000000..5356895 --- /dev/null +++ b/admin/solicitudes.php @@ -0,0 +1,213 @@ + 'Visa', + 'sentri' => 'Sentri/Global', + 'pasaporte' => 'Pasaporte', + 'adelanto_cita' => 'Adelanto Cita', + 'doble_nacionalidad' => 'Doble Nacionalidad', +]; +$estadoSolicitudLabels = [ + 'nueva' => 'Nueva', + 'contactada' => 'Contactada', + 'convertida' => 'Convertida', + 'descartada' => 'Descartada', +]; +$estadoBadgeClass = [ + 'nueva' => 'primary', + 'contactada' => 'warning', + 'convertida' => 'success', + 'descartada' => 'danger', +]; + +// Filters & Pagination +$fEstado = trim($_GET['estado'] ?? ''); +$page = max(1, (int)($_GET['page'] ?? 1)); +$perPage = 15; +$offset = ($page - 1) * $perPage; + +// Validate filter +$validEstados = array_keys($estadoSolicitudLabels); +if ($fEstado !== '' && !in_array($fEstado, $validEstados, true)) $fEstado = ''; + +// Build WHERE clause +$conditions = []; +$params = []; + +if ($fEstado !== '') { + $conditions[] = 's.estado = ?'; + $params[] = $fEstado; +} + +$where = ''; +if (!empty($conditions)) { + $where = 'WHERE ' . implode(' AND ', $conditions); +} + +// Count total +$countSql = "SELECT COUNT(*) FROM solicitudes s $where"; +$countStmt = $db->prepare($countSql); +$countStmt->execute($params); +$totalSolicitudes = $countStmt->fetchColumn(); +$totalPages = max(1, (int)ceil($totalSolicitudes / $perPage)); + +// Ensure page is within range +if ($page > $totalPages) $page = $totalPages; + +// Count new requests for header badge +$stmtNew = $db->prepare("SELECT COUNT(*) FROM solicitudes WHERE estado = 'nueva'"); +$stmtNew->execute(); +$countNuevas = (int)$stmtNew->fetchColumn(); + +// Fetch solicitudes +$sql = "SELECT s.* + FROM solicitudes s + $where + ORDER BY s.created_at DESC + LIMIT $perPage OFFSET $offset"; +$stmt = $db->prepare($sql); +$stmt->execute($params); +$solicitudes = $stmt->fetchAll(); + +// Check if any filters are active +$hasFilters = ($fEstado !== ''); +?> + +
+

+ Solicitudes + 0): ?> + + nueva 1 ? 's' : '' ?> + + +

+

Solicitudes recibidas desde el sitio web

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

+ + Ver todas + +
+ +
+ + + + + + + + + + + + + + + > + + + + + + + + + + +
NombreServicioTeléfonoEmailEstadoFechaAcción
+ + + + + + + + + + + + Ver + +
+
+ + + 1): ?> + + + +
+
+ +