From e41e07bf52d42a7dde93d8449139cf1ff64cb75b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gestor=C3=ADa=20LP?= Date: Mon, 2 Mar 2026 00:25:50 +0000 Subject: [PATCH] feat: admin clients module with CRUD, credentials, documents Co-Authored-By: Claude Opus 4.6 --- admin/cliente-detalle.php | 649 ++++++++++++++++++++++++++++++++++++++ admin/clientes.php | 152 +++++++++ 2 files changed, 801 insertions(+) create mode 100644 admin/cliente-detalle.php create mode 100644 admin/clientes.php diff --git a/admin/cliente-detalle.php b/admin/cliente-detalle.php new file mode 100644 index 0000000..4fa0f5b --- /dev/null +++ b/admin/cliente-detalle.php @@ -0,0 +1,649 @@ + 0; +$cliente = null; +$errors = []; +$success = ''; + +// Labels for tramites +$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ón', + 'completado' => 'Completado', + 'cancelado' => 'Cancelado', +]; + +// ── Handle POST actions ───────────────────────────────────────── +if ($_SERVER['REQUEST_METHOD'] === 'POST' && csrfValidate()) { + $action = $_POST['action'] ?? 'save_client'; + + switch ($action) { + + // ── Save client (create / update) ───────────────────────── + case 'save_client': + $nombre = trim($_POST['nombre'] ?? ''); + $telefono = trim($_POST['telefono'] ?? ''); + $email = trim($_POST['email'] ?? ''); + $direccion = trim($_POST['direccion'] ?? ''); + $notas = trim($_POST['notas'] ?? ''); + + if ($nombre === '') { + $errors[] = 'El nombre es obligatorio.'; + } + if ($email !== '' && !filter_var($email, FILTER_VALIDATE_EMAIL)) { + $errors[] = 'El email no es válido.'; + } + + if (empty($errors)) { + if ($isEdit) { + $stmt = $db->prepare("UPDATE clientes SET nombre=?, telefono=?, email=?, direccion=?, notas=? WHERE id=?"); + $stmt->execute([$nombre, $telefono, $email, $direccion, $notas, $clienteId]); + header("Location: cliente-detalle.php?id=$clienteId&saved=1"); + exit; + } else { + $stmt = $db->prepare("INSERT INTO clientes (nombre, telefono, email, direccion, notas) VALUES (?,?,?,?,?)"); + $stmt->execute([$nombre, $telefono, $email, $direccion, $notas]); + $newId = $db->lastInsertId(); + + // If created from solicitud, link them + $fromSolicitud = (int)($_POST['from_solicitud'] ?? 0); + if ($fromSolicitud > 0) { + $stmt2 = $db->prepare("UPDATE solicitudes SET cliente_id=?, estado='convertida' WHERE id=?"); + $stmt2->execute([$newId, $fromSolicitud]); + } + + header("Location: cliente-detalle.php?id=$newId&saved=1"); + exit; + } + } + break; + + // ── Add credential ──────────────────────────────────────── + case 'add_credential': + if (!$isEdit) break; + + $portal = trim($_POST['cred_portal'] ?? ''); + $usuario = trim($_POST['cred_usuario'] ?? ''); + $password = $_POST['cred_password'] ?? ''; + $credNotas = trim($_POST['cred_notas'] ?? ''); + + if ($portal === '' || $usuario === '' || $password === '') { + $errors[] = 'Portal, usuario y contraseña son obligatorios.'; + } else { + $passwordEnc = encryptData($password); + $stmt = $db->prepare("INSERT INTO credenciales (cliente_id, portal, usuario, password_enc, notas) VALUES (?,?,?,?,?)"); + $stmt->execute([$clienteId, $portal, $usuario, $passwordEnc, $credNotas]); + header("Location: cliente-detalle.php?id=$clienteId&cred_saved=1#credenciales"); + exit; + } + break; + + // ── Delete credential ───────────────────────────────────── + case 'delete_credential': + if (!$isEdit) break; + + $credId = (int)($_POST['cred_id'] ?? 0); + if ($credId > 0) { + $stmt = $db->prepare("DELETE FROM credenciales WHERE id=? AND cliente_id=?"); + $stmt->execute([$credId, $clienteId]); + } + header("Location: cliente-detalle.php?id=$clienteId&cred_deleted=1#credenciales"); + exit; + + // ── 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 { + // Validate extension + $ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION)); + if (!in_array($ext, ALLOWED_EXTENSIONS)) { + $errors[] = 'Tipo de archivo no permitido. Permitidos: ' . implode(', ', ALLOWED_EXTENSIONS); + } + // Validate size + 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)) { + // Create client upload directory + $uploadDir = UPLOAD_DIR . 'client_' . $clienteId . '/'; + if (!is_dir($uploadDir)) { + mkdir($uploadDir, 0755, true); + } + + // Generate unique filename + $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_' . $clienteId . '/' . $filename; + $stmt = $db->prepare("INSERT INTO documentos (cliente_id, nombre, ruta_archivo, tipo) VALUES (?,?,?,?)"); + $stmt->execute([$clienteId, $docNombre, $rutaArchivo, $ext]); + header("Location: cliente-detalle.php?id=$clienteId&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) { + // Get file path before deleting + $stmt = $db->prepare("SELECT ruta_archivo FROM documentos WHERE id=? AND cliente_id=?"); + $stmt->execute([$docId, $clienteId]); + $doc = $stmt->fetch(); + + if ($doc) { + // Remove file from disk + $filePath = UPLOAD_DIR . $doc['ruta_archivo']; + if (file_exists($filePath)) { + unlink($filePath); + } + // Remove DB record + $stmt = $db->prepare("DELETE FROM documentos WHERE id=? AND cliente_id=?"); + $stmt->execute([$docId, $clienteId]); + } + } + header("Location: cliente-detalle.php?id=$clienteId&doc_deleted=1#documentos"); + exit; + } +} + +// ── Handle download (GET) ─────────────────────────────────────── +if (isset($_GET['download']) && $isEdit) { + $docId = (int)$_GET['download']; + $stmt = $db->prepare("SELECT nombre, ruta_archivo, tipo FROM documentos WHERE id=? AND cliente_id=?"); + $stmt->execute([$docId, $clienteId]); + $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; + } + } + // File not found — continue to page with error + $errors[] = 'Documento no encontrado.'; +} + +// ── Load client data for edit mode ────────────────────────────── +if ($isEdit) { + $stmt = $db->prepare("SELECT * FROM clientes WHERE id=?"); + $stmt->execute([$clienteId]); + $cliente = $stmt->fetch(); + + if (!$cliente) { + header('Location: clientes.php'); + exit; + } + + // Fetch credentials + $stmtCred = $db->prepare("SELECT * FROM credenciales WHERE cliente_id=? ORDER BY created_at DESC"); + $stmtCred->execute([$clienteId]); + $credenciales = $stmtCred->fetchAll(); + + // Fetch documents + $stmtDoc = $db->prepare("SELECT * FROM documentos WHERE cliente_id=? ORDER BY created_at DESC"); + $stmtDoc->execute([$clienteId]); + $documentos = $stmtDoc->fetchAll(); + + // Fetch tramites + $stmtTram = $db->prepare("SELECT * FROM tramites WHERE cliente_id=? ORDER BY fecha_solicitud DESC, created_at DESC"); + $stmtTram->execute([$clienteId]); + $tramites = $stmtTram->fetchAll(); +} + +// ── Pre-fill from solicitud if creating new ───────────────────── +$fromSolicitud = 0; +$prefill = ['nombre' => '', 'telefono' => '', 'email' => '', 'direccion' => '', 'notas' => '']; + +if (!$isEdit && isset($_GET['from_solicitud'])) { + $fromSolicitud = (int)$_GET['from_solicitud']; + if ($fromSolicitud > 0) { + $stmt = $db->prepare("SELECT nombre, telefono, email FROM solicitudes WHERE id=?"); + $stmt->execute([$fromSolicitud]); + $sol = $stmt->fetch(); + if ($sol) { + $prefill['nombre'] = $sol['nombre']; + $prefill['telefono'] = $sol['telefono'] ?? ''; + $prefill['email'] = $sol['email'] ?? ''; + } + } +} + +// If POST had errors, preserve submitted data +if (!empty($errors) && ($_POST['action'] ?? '') === 'save_client') { + $prefill['nombre'] = $_POST['nombre'] ?? ''; + $prefill['telefono'] = $_POST['telefono'] ?? ''; + $prefill['email'] = $_POST['email'] ?? ''; + $prefill['direccion'] = $_POST['direccion'] ?? ''; + $prefill['notas'] = $_POST['notas'] ?? ''; +} + +// Success messages from redirects +if (isset($_GET['saved'])) $success = 'Datos del cliente guardados correctamente.'; +if (isset($_GET['cred_saved'])) $success = 'Credencial agregada correctamente.'; +if (isset($_GET['cred_deleted'])) $success = 'Credencial eliminada.'; +if (isset($_GET['doc_saved'])) $success = 'Documento subido correctamente.'; +if (isset($_GET['doc_deleted'])) $success = 'Documento eliminado.'; + +$pageTitle = $isEdit ? 'Cliente: ' . ($cliente['nombre'] ?? '') : 'Nuevo Cliente'; +?> + + + + + +
+ + + +
+ + + +
+ +
+ +
+ +
+
+ + + +
+
+

Datos del Cliente

+
+
+
+ + + 0): ?> + + + +
+
+ + +
+
+ + +
+
+ +
+
+ + +
+
+ +
+ + +
+ +
+ + +
+ +
+ + + + Registrado: + + — Actualizado: + + + +
+
+
+
+ + + + +
+
+

Credenciales de Portales

+
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + +
PortalUsuarioContraseñaNotasAcciones
+ + •••••••• + + + + +
+ + + + +
+
+
+ +

No hay credenciales registradas para este cliente.

+ + + +
+

Agregar Credencial

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

Documentos

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

No hay documentos subidos para este cliente.

+ + + +
+

Subir Documento

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

Historial de Trámites

+ + Nuevo Trámite + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + +
TipoEstadoFecha SolicitudFecha CitaPrecioAcción
+ + + + + + Ver + +
+
+ +
+ +

No hay trámites registrados para este cliente.

+ Crear primer trámite +
+ +
+
+ + + + + + + + + diff --git a/admin/clientes.php b/admin/clientes.php new file mode 100644 index 0000000..21333f2 --- /dev/null +++ b/admin/clientes.php @@ -0,0 +1,152 @@ +prepare($countSql); +$countStmt->execute($params); +$totalClientes = $countStmt->fetchColumn(); +$totalPages = max(1, (int)ceil($totalClientes / $perPage)); + +// Fetch clients with tramites count +$sql = "SELECT c.*, COUNT(t.id) AS total_tramites + FROM clientes c + LEFT JOIN tramites t ON c.id = t.cliente_id + $where + GROUP BY c.id + ORDER BY c.created_at DESC + LIMIT $perPage OFFSET $offset"; +$stmt = $db->prepare($sql); +$stmt->execute($params); +$clientes = $stmt->fetchAll(); +?> + +
+

Clientes

+

Gestión de clientes registrados

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

+ + Ver todos + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + +
NombreTeléfonoEmailTrámitesFecha RegistroAcción
+ + + + Ver + +
+
+ + + 1): ?> + + + +
+
+ +