diff --git a/admin/api/pedidos_action.php b/admin/api/pedidos_action.php new file mode 100644 index 0000000..8e9afb7 --- /dev/null +++ b/admin/api/pedidos_action.php @@ -0,0 +1,109 @@ + $message]); + exit; +} + +if (!isset($_SESSION['admin']) || !$_SESSION['admin']) { fail_json('Acesso negado.', 403); } +if (!isset($_SESSION['validity']) || $_SESSION['validity'] < time()) { fail_json('Sessão expirada', 403); } +if (isset($_SESSION['pending_totp_user']) || isset($_SESSION['pending_user_setup'])) { fail_json('Autenticação incompleta', 403); } +if ($_SERVER['REQUEST_METHOD'] !== 'POST') { fail_json('Método inválido.', 405); } +if (!verify_csrf_token($_POST['csrf_token'] ?? '')) { fail_json('Pedido inválido.', 403); } + +$action = $_POST['action'] ?? ''; +$rawReservations = $_POST['reservations'] ?? '[]'; +$reservations = json_decode($rawReservations, true); +if (!is_array($reservations)) { fail_json('Dados inválidos.'); } +if (($action === 'aprovar' || $action === 'rejeitar') && empty($reservations)) { + $reservations[] = ['sala' => $_POST['sala'] ?? '', 'tempo' => $_POST['tempo'] ?? '', 'data' => $_POST['data'] ?? '']; +} +if (!in_array($action, ['aprovar', 'rejeitar'], true)) { fail_json('Ação inválida.'); } + +$processed = 0; +$failed = 0; +$emailErrors = []; +$emailGroups = []; +$singleAction = count($reservations) === 1; + +foreach ($reservations as $res) { + if (empty($res['sala']) || empty($res['tempo']) || empty($res['data'])) { + $failed++; + continue; + } + + $stmt = $db->prepare('SELECT r.requisitor, s.nome as sala_nome, t.horashumanos as tempo_nome, c.nome as requisitor_nome FROM reservas r LEFT JOIN salas s ON r.sala = s.id LEFT JOIN tempos t ON r.tempo = t.id LEFT JOIN cache c ON r.requisitor = c.id WHERE r.sala=? AND r.tempo=? AND r.data=? AND r.aprovado=0'); + $stmt->bind_param('sss', $res['sala'], $res['tempo'], $res['data']); + $stmt->execute(); + $details = $stmt->get_result()->fetch_assoc(); + $stmt->close(); + + if (!$details || empty($details['requisitor'])) { + $failed++; + continue; + } + + if ($action === 'aprovar') { + $stmt = $db->prepare('UPDATE reservas SET aprovado=1 WHERE sala=? AND tempo=? AND data=? AND aprovado=0'); + } else { + $stmt = $db->prepare('DELETE FROM reservas WHERE sala=? AND tempo=? AND data=? AND aprovado=0'); + } + $stmt->bind_param('sss', $res['sala'], $res['tempo'], $res['data']); + $stmt->execute(); + $ok = $stmt->affected_rows > 0; + $stmt->close(); + + if (!$ok) { + $failed++; + continue; + } + + $reqId = $details['requisitor']; + if ($singleAction) { + $emailResult = $action === 'aprovar' + ? sendReservationApprovedEmail($db, $reqId, $res['sala'], $res['tempo'], $res['data']) + : sendReservationRejectedEmail($db, $reqId, $res['sala'], $res['tempo'], $res['data']); + if (!$emailResult['success'] && $emailResult['error'] !== 'Email not enabled') { + $emailErrors[] = "Utilizador ID: {$reqId}"; + } + } else { + $emailGroups[$reqId][] = [ + 'requisitor' => $reqId, + 'sala_nome' => $details['sala_nome'], + 'tempo_nome' => $details['tempo_nome'], + 'data' => $res['data'], + ]; + } + + $verb = $action === 'aprovar' ? 'Aprovou' : 'Rejeitou'; + logaction("{$verb} a reserva do utilizador '{$details['requisitor_nome']}': sala '{$details['sala_nome']}' no dia {$res['data']} às {$details['tempo_nome']}", $_SESSION['id']); + $processed++; +} + +foreach ($emailGroups as $reqId => $items) { + $emailResult = $action === 'aprovar' + ? sendBulkReservationApprovedEmail($db, $items) + : sendBulkReservationRejectedEmail($db, $items); + if (!$emailResult['success'] && $emailResult['error'] !== 'Email not enabled') { + $emailErrors[] = "Utilizador ID: {$reqId}"; + } +} + +echo json_encode([ + 'success' => true, + 'processed' => $processed, + 'failed' => $failed, + 'emailErrors' => $emailErrors, + 'message' => $action === 'aprovar' ? 'Reserva(s) aprovada(s).' : 'Reserva(s) rejeitada(s).', +]); + +$db->close(); +?> diff --git a/admin/api/pedidos_list.php b/admin/api/pedidos_list.php new file mode 100644 index 0000000..3b23ff5 --- /dev/null +++ b/admin/api/pedidos_list.php @@ -0,0 +1,134 @@ + 'Acesso negado.']); + exit; +} +if (!isset($_SESSION['validity']) || $_SESSION['validity'] < time()) { + http_response_code(403); + echo json_encode(['error' => 'Sessão expirada']); + exit; +} +if (isset($_SESSION['pending_totp_user']) || isset($_SESSION['pending_user_setup'])) { + http_response_code(403); + echo json_encode(['error' => 'Autenticação incompleta']); + exit; +} + +$page = max(1, intval($_GET['page'] ?? 1)); +$limit = 20; +$offset = ($page - 1) * $limit; +$sala = $_GET['sala'] ?? '0'; +$requisitor = trim($_GET['requisitor'] ?? ''); +$search = trim($_GET['q'] ?? ''); + +$where = ['r.aprovado = 0']; +$params = []; +$types = ''; + +if ($requisitor !== '') { + $where[] = 'r.requisitor = ?'; + $params[] = $requisitor; + $types .= 's'; +} elseif ($sala !== '' && $sala !== '0') { + $where[] = 'r.sala = ?'; + $params[] = $sala; + $types .= 's'; +} + +if ($search !== '') { + $where[] = '(s.nome LIKE ? OR c.nome LIKE ? OR r.motivo LIKE ? OR r.data LIKE ? OR t.horashumanos LIKE ?)'; + $searchParam = '%' . $search . '%'; + for ($i = 0; $i < 5; $i++) { + $params[] = $searchParam; + $types .= 's'; + } +} + +$whereSql = implode(' AND ', $where); + +$countStmt = $db->prepare("SELECT COUNT(*) as total + FROM reservas r + LEFT JOIN salas s ON r.sala = s.id + LEFT JOIN cache c ON r.requisitor = c.id + LEFT JOIN tempos t ON r.tempo = t.id + WHERE {$whereSql}"); +if ($types !== '') { + $countStmt->bind_param($types, ...$params); +} +$countStmt->execute(); +$total = intval($countStmt->get_result()->fetch_assoc()['total'] ?? 0); +$countStmt->close(); + +$query = "SELECT r.sala, r.tempo, r.data, r.motivo, r.requisitor, s.nome as sala_nome, c.nome as requisitor_nome, t.horashumanos as tempo_nome + FROM reservas r + LEFT JOIN salas s ON r.sala = s.id + LEFT JOIN cache c ON r.requisitor = c.id + LEFT JOIN tempos t ON r.tempo = t.id + WHERE {$whereSql} + ORDER BY r.data ASC, r.tempo ASC, r.sala ASC + LIMIT ? OFFSET ?"; +$stmt = $db->prepare($query); +$listParams = array_merge($params, [$limit, $offset]); +$listTypes = $types . 'ii'; +$stmt->bind_param($listTypes, ...$listParams); +$stmt->execute(); +$result = $stmt->get_result(); + +$pedidos = []; +while ($row = $result->fetch_assoc()) { + $pedidos[] = [ + 'sala' => $row['sala'], + 'tempo' => $row['tempo'], + 'data' => $row['data'], + 'motivo' => $row['motivo'] ?? '', + 'sala_nome' => $row['sala_nome'] ?? 'N/A', + 'requisitor_nome' => $row['requisitor_nome'] ?? 'N/A', + 'tempo_nome' => $row['tempo_nome'] ?? 'N/A', + 'data_formatada' => date('d/m/Y', strtotime($row['data'])), + 'is_today' => $row['data'] === date('Y-m-d'), + 'is_past' => strtotime($row['data']) < strtotime(date('Y-m-d')), + ]; +} +$stmt->close(); + +$title = 'Todos os Pedidos Pendentes'; +if ($requisitor !== '') { + $nameStmt = $db->prepare('SELECT nome FROM cache WHERE id = ?'); + $nameStmt->bind_param('s', $requisitor); + $nameStmt->execute(); + $name = $nameStmt->get_result()->fetch_assoc()['nome'] ?? 'Utilizador'; + $nameStmt->close(); + $title = 'Pedidos Pendentes - ' . $name; +} elseif ($sala !== '' && $sala !== '0') { + $nameStmt = $db->prepare('SELECT nome FROM salas WHERE id = ?'); + $nameStmt->bind_param('s', $sala); + $nameStmt->execute(); + $name = $nameStmt->get_result()->fetch_assoc()['nome'] ?? 'Sala'; + $nameStmt->close(); + $title = 'Pedidos Pendentes - ' . $name; +} + +$stats = [ + 'pendentes' => intval($db->query("SELECT COUNT(*) as total FROM reservas WHERE aprovado = 0")->fetch_assoc()['total'] ?? 0), + 'aprovadas' => intval($db->query("SELECT COUNT(*) as total FROM reservas WHERE aprovado = 1")->fetch_assoc()['total'] ?? 0), + 'hoje' => intval($db->query("SELECT COUNT(*) as total FROM reservas WHERE aprovado = 0 AND data = CURDATE()")->fetch_assoc()['total'] ?? 0), +]; + +echo json_encode([ + 'pedidos' => $pedidos, + 'total' => $total, + 'page' => $page, + 'limit' => $limit, + 'hasMore' => ($offset + count($pedidos)) < $total, + 'title' => $title, + 'stats' => $stats, +]); + +$db->close(); +?> diff --git a/admin/pedidos.php b/admin/pedidos.php index 3be1b85..c6b11b5 100644 --- a/admin/pedidos.php +++ b/admin/pedidos.php @@ -145,6 +145,17 @@ .pedidos-admin-header h3 { margin-bottom: 0; } + .skeleton-line { + height: 1rem; + border-radius: 4px; + background: linear-gradient(90deg, #e9ecef 25%, #f8f9fa 50%, #e9ecef 75%); + background-size: 200% 100%; + animation: skeletonLoading 1.2s infinite; + } + @keyframes skeletonLoading { + 0% { background-position: 200% 0; } + 100% { background-position: -200% 0; } + }
@@ -168,14 +179,12 @@ ⏳
-
+
Pedidos Pendentes
- 0): ?> -
+
Requer Atenção
-
@@ -186,7 +195,7 @@ ✅
-
+
Reservas Aprovadas
@@ -199,14 +208,12 @@ 📅
-
+
Pendentes para Hoje
- 0): ?> -
+
Urgente
-
@@ -219,8 +226,8 @@
- prepare("SELECT nome FROM cache WHERE id = ?"); $userStmt->bind_param("s", $_POST['requisitor']); @@ -289,9 +296,9 @@ $userId = htmlspecialchars($user['id'], ENT_QUOTES, 'UTF-8'); $userName = htmlspecialchars($user['nome'], ENT_QUOTES, 'UTF-8'); $userEmail = htmlspecialchars($user['email'], ENT_QUOTES, 'UTF-8'); - echo "
+
+
+
+
+ +
+
+ +
+
+
Erro! Parâmetros inválidos. @@ -327,12 +345,12 @@ $stmt->execute(); $requisitor = $stmt->get_result()->fetch_assoc()['requisitor']; $stmt->close(); - + $stmt = $db->prepare("UPDATE reservas SET aprovado=1 WHERE sala=? AND tempo=? AND data=?"); $stmt->bind_param("sss", $_GET['sala'], $_GET['tempo'], $_GET['data']); $stmt->execute(); $stmt->close(); - + // Log the approval require_once(__DIR__ . '/../func/logaction.php'); $stmt = $db->prepare("SELECT nome FROM salas WHERE id=?"); @@ -340,21 +358,21 @@ $stmt->execute(); $salaNome = $stmt->get_result()->fetch_assoc()['nome'] ?? $_GET['sala']; $stmt->close(); - + $stmt = $db->prepare("SELECT horashumanos FROM tempos WHERE id=?"); $stmt->bind_param("s", $_GET['tempo']); $stmt->execute(); $tempoNome = $stmt->get_result()->fetch_assoc()['horashumanos'] ?? $_GET['tempo']; $stmt->close(); - + $stmt = $db->prepare("SELECT nome FROM cache WHERE id=?"); $stmt->bind_param("s", $requisitor); $stmt->execute(); $requisitorNome = $stmt->get_result()->fetch_assoc()['nome'] ?? 'Utilizador'; $stmt->close(); - + logaction("Aprovou a reserva do utilizador '{$requisitorNome}': sala '{$salaNome}' no dia {$_GET['data']} às {$tempoNome}", $_SESSION['id']); - + echo "
🎉
@@ -371,7 +389,7 @@
"; - + // Send approval email using the email helper $emailResult = sendReservationApprovedEmail($db, $requisitor, $_GET['sala'], $_GET['tempo'], $_GET['data']); if (!$emailResult['success'] && $emailResult['error'] !== 'Email not enabled') { @@ -389,7 +407,7 @@ $stmt->execute(); $requisitor = $stmt->get_result()->fetch_assoc()['requisitor']; $stmt->close(); - + // Get details for log before deletion require_once(__DIR__ . '/../func/logaction.php'); $stmt = $db->prepare("SELECT nome FROM salas WHERE id=?"); @@ -397,30 +415,30 @@ $stmt->execute(); $salaNome = $stmt->get_result()->fetch_assoc()['nome'] ?? $_GET['sala']; $stmt->close(); - + $stmt = $db->prepare("SELECT horashumanos FROM tempos WHERE id=?"); $stmt->bind_param("s", $_GET['tempo']); $stmt->execute(); $tempoNome = $stmt->get_result()->fetch_assoc()['horashumanos'] ?? $_GET['tempo']; $stmt->close(); - + $stmt = $db->prepare("SELECT nome FROM cache WHERE id=?"); $stmt->bind_param("s", $requisitor); $stmt->execute(); $requisitorNome = $stmt->get_result()->fetch_assoc()['nome'] ?? 'Utilizador'; $stmt->close(); - + // Send rejection email BEFORE deleting the reservation $emailResult = sendReservationRejectedEmail($db, $requisitor, $_GET['sala'], $_GET['tempo'], $_GET['data']); - + $stmt = $db->prepare("DELETE FROM reservas WHERE sala=? AND tempo=? AND data=?"); $stmt->bind_param("sss", $_GET['sala'], $_GET['tempo'], $_GET['data']); $stmt->execute(); $stmt->close(); - + // Log the rejection logaction("Rejeitou a reserva do utilizador '{$requisitorNome}': sala '{$salaNome}' no dia {$_GET['data']} às {$tempoNome}", $_SESSION['id']); - + echo "
🚫
@@ -431,7 +449,7 @@
"; - + if (!$emailResult['success'] && $emailResult['error'] !== 'Email not enabled') { echo "