feat: admin reminders module
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
389
admin/recordatorios.php
Normal file
389
admin/recordatorios.php
Normal file
@@ -0,0 +1,389 @@
|
||||
<?php
|
||||
$pageTitle = 'Recordatorios';
|
||||
require_once __DIR__ . '/../includes/db.php';
|
||||
require_once __DIR__ . '/includes/admin-header.php';
|
||||
|
||||
$db = getDB();
|
||||
|
||||
$errors = [];
|
||||
$success = '';
|
||||
$today = date('Y-m-d');
|
||||
|
||||
// ── Handle POST actions ─────────────────────────────────────────
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && csrfValidate()) {
|
||||
$action = $_POST['action'] ?? '';
|
||||
|
||||
switch ($action) {
|
||||
|
||||
// ── Add reminder ──────────────────────────────────────────
|
||||
case 'add_reminder':
|
||||
$titulo = trim($_POST['titulo'] ?? '');
|
||||
$descripcion = trim($_POST['descripcion'] ?? '');
|
||||
$fecha = trim($_POST['fecha'] ?? '');
|
||||
$clienteId = (int)($_POST['cliente_id'] ?? 0);
|
||||
$tramiteId = (int)($_POST['tramite_id'] ?? 0);
|
||||
|
||||
if ($titulo === '') {
|
||||
$errors[] = 'El título es obligatorio.';
|
||||
}
|
||||
if ($fecha === '' || !preg_match('/^\d{4}-\d{2}-\d{2}$/', $fecha)) {
|
||||
$errors[] = 'La fecha es obligatoria y debe ser válida.';
|
||||
}
|
||||
|
||||
if (empty($errors)) {
|
||||
$stmt = $db->prepare("INSERT INTO recordatorios
|
||||
(cliente_id, tramite_id, titulo, descripcion, fecha, completado, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, 0, NOW())");
|
||||
$stmt->execute([
|
||||
$clienteId > 0 ? $clienteId : null,
|
||||
$tramiteId > 0 ? $tramiteId : null,
|
||||
$titulo,
|
||||
$descripcion,
|
||||
$fecha,
|
||||
]);
|
||||
header('Location: recordatorios.php?added=1' . (!empty($_GET['show_completed']) ? '&show_completed=1' : ''));
|
||||
exit;
|
||||
}
|
||||
break;
|
||||
|
||||
// ── Complete reminder ─────────────────────────────────────
|
||||
case 'complete_reminder':
|
||||
$id = (int)($_POST['reminder_id'] ?? 0);
|
||||
if ($id > 0) {
|
||||
$stmt = $db->prepare("UPDATE recordatorios SET completado = 1 WHERE id = ?");
|
||||
$stmt->execute([$id]);
|
||||
}
|
||||
header('Location: recordatorios.php?completed=1' . (!empty($_GET['show_completed']) ? '&show_completed=1' : ''));
|
||||
exit;
|
||||
|
||||
// ── Delete reminder ───────────────────────────────────────
|
||||
case 'delete_reminder':
|
||||
$id = (int)($_POST['reminder_id'] ?? 0);
|
||||
if ($id > 0) {
|
||||
$stmt = $db->prepare("DELETE FROM recordatorios WHERE id = ?");
|
||||
$stmt->execute([$id]);
|
||||
}
|
||||
header('Location: recordatorios.php?deleted=1' . (!empty($_GET['show_completed']) ? '&show_completed=1' : ''));
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Flash messages from redirect ────────────────────────────────
|
||||
if (isset($_GET['added'])) $success = 'Recordatorio creado correctamente.';
|
||||
if (isset($_GET['completed'])) $success = 'Recordatorio marcado como completado.';
|
||||
if (isset($_GET['deleted'])) $success = 'Recordatorio eliminado correctamente.';
|
||||
|
||||
// ── Filters & pagination ────────────────────────────────────────
|
||||
$showCompleted = isset($_GET['show_completed']) && $_GET['show_completed'] === '1';
|
||||
$page = max(1, (int)($_GET['page'] ?? 1));
|
||||
$perPage = 20;
|
||||
$offset = ($page - 1) * $perPage;
|
||||
|
||||
// Build WHERE clause
|
||||
$where = '';
|
||||
$params = [];
|
||||
if (!$showCompleted) {
|
||||
$where = 'WHERE r.completado = 0';
|
||||
}
|
||||
|
||||
// Count total
|
||||
$countSql = "SELECT COUNT(*) FROM recordatorios r $where";
|
||||
$countStmt = $db->prepare($countSql);
|
||||
$countStmt->execute($params);
|
||||
$totalRecordatorios = $countStmt->fetchColumn();
|
||||
$totalPages = max(1, (int)ceil($totalRecordatorios / $perPage));
|
||||
|
||||
if ($page > $totalPages) $page = $totalPages;
|
||||
$offset = ($page - 1) * $perPage;
|
||||
|
||||
// Fetch reminders with client name
|
||||
$sql = "SELECT r.*, c.nombre AS cliente_nombre
|
||||
FROM recordatorios r
|
||||
LEFT JOIN clientes c ON r.cliente_id = c.id
|
||||
$where
|
||||
ORDER BY r.fecha ASC
|
||||
LIMIT $perPage OFFSET $offset";
|
||||
$stmt = $db->prepare($sql);
|
||||
$stmt->execute($params);
|
||||
$recordatorios = $stmt->fetchAll();
|
||||
|
||||
// Count overdue (for badge in header)
|
||||
$stmtOverdue = $db->prepare("SELECT COUNT(*) FROM recordatorios WHERE completado = 0 AND fecha < ?");
|
||||
$stmtOverdue->execute([$today]);
|
||||
$countOverdue = (int)$stmtOverdue->fetchColumn();
|
||||
|
||||
// Fetch clients for dropdown
|
||||
$clientesStmt = $db->query("SELECT id, nombre FROM clientes ORDER BY nombre ASC");
|
||||
$clientesList = $clientesStmt->fetchAll();
|
||||
|
||||
// Fetch tramites for dropdown
|
||||
$tramitesStmt = $db->query("SELECT t.id, t.tipo, c.nombre AS cliente_nombre
|
||||
FROM tramites t
|
||||
LEFT JOIN clientes c ON t.cliente_id = c.id
|
||||
ORDER BY t.created_at DESC LIMIT 100");
|
||||
$tramitesList = $tramitesStmt->fetchAll();
|
||||
$tipoLabels = [
|
||||
'visa' => 'Visa',
|
||||
'sentri' => 'Sentri/Global',
|
||||
'pasaporte' => 'Pasaporte',
|
||||
'adelanto_cita' => 'Adelanto Cita',
|
||||
'doble_nacionalidad' => 'Doble Nacionalidad',
|
||||
];
|
||||
?>
|
||||
|
||||
<div class="admin-content__header">
|
||||
<h1>
|
||||
<i class="fas fa-bell"></i> Recordatorios
|
||||
<?php if ($countOverdue > 0): ?>
|
||||
<span class="badge badge--danger" style="font-size: 0.5em; vertical-align: middle;">
|
||||
<?= $countOverdue ?> vencido<?= $countOverdue > 1 ? 's' : '' ?>
|
||||
</span>
|
||||
<?php endif; ?>
|
||||
</h1>
|
||||
<p>Gestiona tus recordatorios y tareas pendientes</p>
|
||||
</div>
|
||||
|
||||
<!-- Success / Error messages -->
|
||||
<?php if ($success !== ''): ?>
|
||||
<div class="alert alert--success" style="padding: 0.75rem 1rem; margin-bottom: 1rem; border-radius: 0.5rem; background: var(--admin-success-light); color: #155724; border: 1px solid #C3E6CB;">
|
||||
<i class="fas fa-check-circle"></i> <?= $success ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (!empty($errors)): ?>
|
||||
<div class="alert alert--danger" style="padding: 0.75rem 1rem; margin-bottom: 1rem; border-radius: 0.5rem; background: var(--admin-danger-light); color: #721C24; border: 1px solid #F5C6CB;">
|
||||
<i class="fas fa-exclamation-circle"></i>
|
||||
<ul style="margin: 0.25rem 0 0 1.25rem; padding: 0;">
|
||||
<?php foreach ($errors as $e): ?>
|
||||
<li><?= $e ?></li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- Add Reminder Form -->
|
||||
<div class="card" style="margin-bottom: 1.5rem;">
|
||||
<div class="card__header" style="padding: 1rem 1.25rem; border-bottom: 1px solid var(--admin-gray-200); font-weight: 600;">
|
||||
<i class="fas fa-plus-circle"></i> Nuevo Recordatorio
|
||||
</div>
|
||||
<div class="card__body">
|
||||
<form method="POST" action="recordatorios.php<?= $showCompleted ? '?show_completed=1' : '' ?>">
|
||||
<?= csrfField() ?>
|
||||
<input type="hidden" name="action" value="add_reminder">
|
||||
|
||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 1rem; margin-bottom: 1rem;">
|
||||
<div class="form-group" style="margin-bottom: 0;">
|
||||
<label>Título <span class="required">*</span></label>
|
||||
<input type="text" name="titulo" class="form-control" required
|
||||
value="<?= htmlspecialchars($_POST['titulo'] ?? '') ?>"
|
||||
placeholder="Ej: Llamar al cliente...">
|
||||
</div>
|
||||
|
||||
<div class="form-group" style="margin-bottom: 0;">
|
||||
<label>Fecha <span class="required">*</span></label>
|
||||
<input type="date" name="fecha" class="form-control" required
|
||||
value="<?= htmlspecialchars($_POST['fecha'] ?? $today) ?>">
|
||||
</div>
|
||||
|
||||
<div class="form-group" style="margin-bottom: 0;">
|
||||
<label>Cliente <small>(opcional)</small></label>
|
||||
<select name="cliente_id" class="form-control">
|
||||
<option value="0">-- Ninguno --</option>
|
||||
<?php foreach ($clientesList as $cl): ?>
|
||||
<option value="<?= (int)$cl['id'] ?>" <?= ((int)($_POST['cliente_id'] ?? 0) === (int)$cl['id']) ? 'selected' : '' ?>>
|
||||
<?= htmlspecialchars($cl['nombre']) ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group" style="margin-bottom: 0;">
|
||||
<label>Trámite <small>(opcional)</small></label>
|
||||
<select name="tramite_id" class="form-control">
|
||||
<option value="0">-- Ninguno --</option>
|
||||
<?php foreach ($tramitesList as $tr): ?>
|
||||
<option value="<?= (int)$tr['id'] ?>" <?= ((int)($_POST['tramite_id'] ?? 0) === (int)$tr['id']) ? 'selected' : '' ?>>
|
||||
#<?= (int)$tr['id'] ?> - <?= htmlspecialchars($tipoLabels[$tr['tipo']] ?? $tr['tipo']) ?> (<?= htmlspecialchars($tr['cliente_nombre'] ?? 'Sin cliente') ?>)
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group" style="margin-bottom: 1rem;">
|
||||
<label>Descripción <small>(opcional)</small></label>
|
||||
<textarea name="descripcion" class="form-control" rows="2"
|
||||
placeholder="Detalles adicionales del recordatorio..."><?= htmlspecialchars($_POST['descripcion'] ?? '') ?></textarea>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn--primary">
|
||||
<i class="fas fa-plus"></i> Agregar Recordatorio
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toolbar: Filter toggle -->
|
||||
<div class="toolbar">
|
||||
<div class="toolbar__left">
|
||||
<?php if ($showCompleted): ?>
|
||||
<a href="recordatorios.php" class="btn btn--sm btn--secondary">
|
||||
<i class="fas fa-eye-slash"></i> Ocultar completados
|
||||
</a>
|
||||
<?php else: ?>
|
||||
<a href="recordatorios.php?show_completed=1" class="btn btn--sm btn--secondary">
|
||||
<i class="fas fa-eye"></i> Mostrar todos
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
<span style="margin-left: 0.75rem; color: var(--admin-gray-600); font-size: var(--admin-font-sm);">
|
||||
<?= $totalRecordatorios ?> recordatorio<?= $totalRecordatorios !== 1 ? 's' : '' ?>
|
||||
<?= !$showCompleted ? ' pendiente' . ($totalRecordatorios !== 1 ? 's' : '') : ' en total' ?>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Reminders List -->
|
||||
<div class="card">
|
||||
<div class="card__body">
|
||||
<?php if (empty($recordatorios)): ?>
|
||||
<div class="table-empty">
|
||||
<i class="fas fa-bell"></i>
|
||||
<p><?= !$showCompleted ? 'No hay recordatorios pendientes.' : 'No hay recordatorios registrados aún.' ?></p>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="table-responsive">
|
||||
<table class="admin-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 110px;">Fecha</th>
|
||||
<th>Título</th>
|
||||
<th>Descripción</th>
|
||||
<th>Cliente</th>
|
||||
<?php if ($showCompleted): ?>
|
||||
<th>Estado</th>
|
||||
<?php endif; ?>
|
||||
<th style="width: 180px;">Acciones</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($recordatorios as $r):
|
||||
// Determine color coding
|
||||
$fechaR = $r['fecha'];
|
||||
$isCompleted = (int)$r['completado'] === 1;
|
||||
if ($isCompleted) {
|
||||
$rowStyle = 'opacity: 0.6;';
|
||||
$dateClass = 'secondary';
|
||||
} elseif ($fechaR < $today) {
|
||||
$rowStyle = 'background: rgba(220, 53, 69, 0.06);';
|
||||
$dateClass = 'danger';
|
||||
} elseif ($fechaR === $today) {
|
||||
$rowStyle = 'background: rgba(255, 193, 7, 0.1);';
|
||||
$dateClass = 'warning';
|
||||
} else {
|
||||
$rowStyle = '';
|
||||
$dateClass = 'success';
|
||||
}
|
||||
?>
|
||||
<tr style="<?= $rowStyle ?>">
|
||||
<td>
|
||||
<span class="badge badge--<?= $dateClass ?>">
|
||||
<?= date('d/m/Y', strtotime($fechaR)) ?>
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<strong><?= htmlspecialchars($r['titulo']) ?></strong>
|
||||
</td>
|
||||
<td>
|
||||
<?php
|
||||
$desc = $r['descripcion'] ?? '';
|
||||
echo htmlspecialchars(mb_strlen($desc) > 80 ? mb_substr($desc, 0, 80) . '...' : $desc);
|
||||
?>
|
||||
</td>
|
||||
<td>
|
||||
<?php if ($r['cliente_id'] && $r['cliente_nombre']): ?>
|
||||
<a href="cliente-detalle.php?id=<?= (int)$r['cliente_id'] ?>" style="color: var(--admin-primary); text-decoration: none;">
|
||||
<?= htmlspecialchars($r['cliente_nombre']) ?>
|
||||
</a>
|
||||
<?php else: ?>
|
||||
<span style="color: var(--admin-gray-500);">-</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<?php if ($showCompleted): ?>
|
||||
<td>
|
||||
<?php if ($isCompleted): ?>
|
||||
<span class="badge badge--success"><i class="fas fa-check"></i> Completado</span>
|
||||
<?php else: ?>
|
||||
<span class="badge badge--primary">Pendiente</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<?php endif; ?>
|
||||
<td>
|
||||
<div style="display: flex; gap: 0.5rem;">
|
||||
<?php if (!$isCompleted): ?>
|
||||
<form method="POST" action="recordatorios.php<?= $showCompleted ? '?show_completed=1' : '' ?>" style="display: inline;">
|
||||
<?= csrfField() ?>
|
||||
<input type="hidden" name="action" value="complete_reminder">
|
||||
<input type="hidden" name="reminder_id" value="<?= (int)$r['id'] ?>">
|
||||
<button type="submit" class="btn btn--sm btn--success" title="Marcar como completado">
|
||||
<i class="fas fa-check"></i>
|
||||
</button>
|
||||
</form>
|
||||
<?php endif; ?>
|
||||
<form method="POST" action="recordatorios.php<?= $showCompleted ? '?show_completed=1' : '' ?>" style="display: inline;"
|
||||
onsubmit="return confirm('¿Eliminar este recordatorio?');">
|
||||
<?= csrfField() ?>
|
||||
<input type="hidden" name="action" value="delete_reminder">
|
||||
<input type="hidden" name="reminder_id" value="<?= (int)$r['id'] ?>">
|
||||
<button type="submit" class="btn btn--sm btn--danger" title="Eliminar">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
<?php if ($totalPages > 1): ?>
|
||||
<div class="pagination">
|
||||
<?php
|
||||
$queryParams = '';
|
||||
if ($showCompleted) $queryParams .= '&show_completed=1';
|
||||
?>
|
||||
<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 ?> (<?= $totalRecordatorios ?> recordatorios)
|
||||
</span>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php require_once __DIR__ . '/includes/admin-footer.php'; ?>
|
||||
Reference in New Issue
Block a user