feat: admin processes module with CRUD and filtering
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
585
admin/tramite-detalle.php
Normal file
585
admin/tramite-detalle.php
Normal file
@@ -0,0 +1,585 @@
|
|||||||
|
<?php
|
||||||
|
$pageTitle = 'Detalle Trá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ó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';
|
||||||
|
?>
|
||||||
|
|
||||||
|
<div class="admin-content__header">
|
||||||
|
<h1>
|
||||||
|
<i class="fas fa-<?= $isEdit ? 'edit' : 'plus-circle' ?>"></i>
|
||||||
|
<?= $isEdit ? htmlspecialchars($tipoLabels[$tramite['tipo']] ?? $tramite['tipo']) : 'Nuevo Trá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á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()">×</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á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ó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á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']): ?>
|
||||||
|
— 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á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('¿Eliminar este documento? El archivo se borrará 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á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á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ámite borrará también todos los documentos asociados. Esta acción no se puede deshacer.
|
||||||
|
</p>
|
||||||
|
<form method="POST" action="tramite-detalle.php?id=<?= $tramiteId ?>"
|
||||||
|
onsubmit="return confirm('¿Está seguro de eliminar este trámite y todos sus documentos? Esta acció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á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
224
admin/tramites.php
Normal file
@@ -0,0 +1,224 @@
|
|||||||
|
<?php
|
||||||
|
$pageTitle = 'Trá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ó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ámites</h1>
|
||||||
|
<p>Gestión de procesos y trá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á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ámites con esos filtros.' : 'No hay trámites registrados aú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ó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ágina <?= $page ?> de <?= $totalPages ?> (<?= $totalTramites ?> trámites)
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php require_once __DIR__ . '/includes/admin-footer.php'; ?>
|
||||||
Reference in New Issue
Block a user