From 434840907f929f35365e234d48473d883512d2da Mon Sep 17 00:00:00 2001 From: Marco Pisco Date: Sun, 5 Jul 2026 03:34:13 +0100 Subject: [PATCH] feat(reservar): load weekly statuses asynchronously Adds a small reservation statuses API for weekly room availability. The reservation page now renders the existing server-side table as a fallback, then fetches the weekly statuses on load and when navigating between weeks. The navigation links and browser URL stay in sync with the selected week. Tested with PHP syntax checks for all PHP files, a JavaScript syntax check, and git diff whitespace checks. Co-authored-by: Codex --- api/reservation_statuses.php | 121 ++++++++++++++++++++++++++++++++ assets/reservation-statuses.js | 124 +++++++++++++++++++++++++++++++++ reservar/index.php | 13 ++-- 3 files changed, 252 insertions(+), 6 deletions(-) create mode 100644 api/reservation_statuses.php create mode 100644 assets/reservation-statuses.js diff --git a/api/reservation_statuses.php b/api/reservation_statuses.php new file mode 100644 index 0000000..2a29bfb --- /dev/null +++ b/api/reservation_statuses.php @@ -0,0 +1,121 @@ + 'Autenticação incompleta']); + exit; +} + +if (!isset($_SESSION['validity']) || $_SESSION['validity'] < time()) { + http_response_code(403); + echo json_encode(['error' => 'Sessão expirada']); + exit; +} + +$sala = trim($_GET['sala'] ?? ''); +$before = trim($_GET['before'] ?? ''); + +if ($sala === '') { + http_response_code(400); + echo json_encode(['error' => 'Sala inválida']); + exit; +} + +$stmt = $db->prepare('SELECT id, tipo_sala, bloqueado FROM salas WHERE id = ?'); +$stmt->bind_param('s', $sala); +$stmt->execute(); +$salaData = $stmt->get_result()->fetch_assoc(); +$stmt->close(); + +if (!$salaData) { + http_response_code(404); + echo json_encode(['error' => 'Sala não encontrada']); + exit; +} + +if ($before !== '') { + $date = DateTime::createFromFormat('d-m-Y', $before); + $dateErrors = DateTime::getLastErrors(); + $hasDateErrors = is_array($dateErrors) && ($dateErrors['warning_count'] > 0 || $dateErrors['error_count'] > 0); + if (!$date || $hasDateErrors) { + http_response_code(400); + echo json_encode(['error' => 'Data inválida']); + exit; + } + $segunda = strtotime($date->format('Y-m-d')); +} else { + $segunda = strtotime('monday this week'); +} + +$today = date('Y-m-d'); +$canCreateReservation = ($salaData['bloqueado'] != 1 || !empty($_SESSION['admin'])); +$days = []; +for ($i = 0; $i < 7; $i++) { + $dayDate = date('Y-m-d', strtotime("+{$i} day", $segunda)); + $days[] = [ + 'date' => $dayDate, + 'label' => date('d/m', strtotime($dayDate)) . '
' . date('Y', strtotime($dayDate)), + 'isToday' => $dayDate === $today, + 'isPast' => $dayDate < $today, + ]; +} + +$tempos = []; +$temposResult = $db->query('SELECT id, horashumanos FROM tempos ORDER BY horashumanos ASC'); +while ($tempo = $temposResult->fetch_assoc()) { + $tempos[] = $tempo; +} + +$statusBySlot = []; +$startDate = $days[0]['date']; +$endDate = $days[6]['date']; +$stmt = $db->prepare("SELECT r.tempo, r.data, r.aprovado, c.nome AS requisitor_nome + FROM reservas r + LEFT JOIN cache c ON c.id = r.requisitor + WHERE r.sala = ? AND r.data BETWEEN ? AND ?"); +$stmt->bind_param('sss', $sala, $startDate, $endDate); +$stmt->execute(); +$result = $stmt->get_result(); +while ($reservation = $result->fetch_assoc()) { + if ($reservation['aprovado'] == -1) { + continue; + } + $nome = $reservation['requisitor_nome'] ?? ''; + $nome = preg_replace('/^(\S+).*?(\S+)$/u', '$1 $2', $nome); + $statusBySlot[$reservation['tempo'] . '|' . $reservation['data']] = [ + 'status' => $reservation['aprovado'] == 0 ? 'pending' : 'occupied', + 'label' => $reservation['aprovado'] == 0 ? 'Pendente' : 'Ocupado', + 'requisitor' => $nome, + ]; +} +$stmt->close(); + +$slots = []; +foreach ($tempos as $tempo) { + foreach ($days as $day) { + $key = $tempo['id'] . '|' . $day['date']; + $canInteract = (!$day['isPast'] || !empty($_SESSION['admin'])); + $slots[$tempo['id']][$day['date']] = $statusBySlot[$key] ?? [ + 'status' => 'free', + 'label' => 'Livre', + 'requisitor' => '', + ]; + $slots[$tempo['id']][$day['date']]['canInteract'] = $canInteract; + $slots[$tempo['id']][$day['date']]['canCreateReservation'] = $canCreateReservation; + } +} + +echo json_encode([ + 'sala' => $sala, + 'before' => $before, + 'previousWeek' => date('d-m-Y', strtotime('-1 week', $segunda)), + 'nextWeek' => date('d-m-Y', strtotime('+1 week', $segunda)), + 'currentWeek' => '', + 'days' => $days, + 'tempos' => $tempos, + 'slots' => $slots, +]); diff --git a/assets/reservation-statuses.js b/assets/reservation-statuses.js new file mode 100644 index 0000000..4a7cd23 --- /dev/null +++ b/assets/reservation-statuses.js @@ -0,0 +1,124 @@ +function reservationEscapeHtml(value) { + const div = document.createElement('div'); + div.textContent = value || ''; + return div.innerHTML; +} + +function buildReservationSlotCell(slot, tempoId, salaId, date) { + const cellClass = slot.status === 'free' ? 'bg-success' : (slot.status === 'pending' ? 'bg-warning' : 'bg-danger'); + const label = reservationEscapeHtml(slot.label); + const requisitor = slot.requisitor ? '
' + reservationEscapeHtml(slot.requisitor) : ''; + const link = '/reservar/manage.php?tempo=' + encodeURIComponent(tempoId) + '&sala=' + encodeURIComponent(salaId) + '&data=' + encodeURIComponent(date); + const checkboxValue = encodeURIComponent(tempoId) + '|' + encodeURIComponent(salaId) + '|' + encodeURIComponent(date); + let content = ''; + + if (slot.status === 'free' && slot.canCreateReservation && slot.canInteract) { + content = '' + + '' + label + ''; + } else if (slot.canInteract && slot.status !== 'free') { + content = '' + label + requisitor + ''; + } else { + content = '' + label + requisitor + ''; + } + + return '' + + '
' + content + '
'; +} + +function setReservationSkeleton() { + const tbody = document.getElementById('reservationTableBody'); + if (!tbody) return; + + tbody.querySelectorAll('td').forEach(cell => { + cell.className = 'text-center'; + cell.innerHTML = ' '; + }); + if (typeof clearBulkSelection === 'function') clearBulkSelection(); +} + +function renderReservationStatuses(data) { + const table = document.getElementById('reservationTable'); + const tbody = document.getElementById('reservationTableBody'); + if (!table || !tbody) return; + + table.dataset.before = data.before || ''; + document.querySelectorAll('.reservation-day-header').forEach((header, index) => { + if (data.days[index]) header.innerHTML = data.days[index].label; + }); + tbody.innerHTML = ''; + data.tempos.forEach(tempo => { + let row = '' + reservationEscapeHtml(tempo.horashumanos) + ''; + data.days.forEach(day => { + row += buildReservationSlotCell(data.slots[tempo.id][day.date], tempo.id, data.sala, day.date); + }); + row += ''; + tbody.insertAdjacentHTML('beforeend', row); + }); + + document.querySelectorAll('.bulk-checkbox').forEach(checkbox => { + checkbox.addEventListener('change', updateBulkControls); + }); + if (typeof clearBulkSelection === 'function') clearBulkSelection(); +} + +function updateReservationLinks(data) { + const salaId = encodeURIComponent(data.sala); + const links = [ + ['previousWeekLink', data.previousWeek, '/reservar/?before=' + encodeURIComponent(data.previousWeek) + '&sala=' + salaId], + ['currentWeekLink', '', '/reservar/?sala=' + salaId], + ['nextWeekLink', data.nextWeek, '/reservar/?before=' + encodeURIComponent(data.nextWeek) + '&sala=' + salaId] + ]; + + links.forEach(([id, before, href]) => { + const link = document.getElementById(id); + if (link) { + link.dataset.weekBefore = before; + link.href = href; + } + }); +} + +function loadReservationStatuses(before, pushState) { + const table = document.getElementById('reservationTable'); + if (!table) return; + + const params = new URLSearchParams(); + params.set('sala', table.dataset.sala); + if (before) params.set('before', before); + + setReservationSkeleton(); + fetch('/api/reservation_statuses.php?' + params.toString(), { credentials: 'same-origin' }) + .then(response => { + if (!response.ok) throw new Error('Erro'); + return response.json(); + }) + .then(data => { + renderReservationStatuses(data); + updateReservationLinks(data); + if (pushState) { + const url = new URL(window.location.href); + url.searchParams.set('sala', data.sala); + if (data.before) url.searchParams.set('before', data.before); + else url.searchParams.delete('before'); + window.history.pushState({ before: data.before || '' }, '', url.toString()); + } + }) + .catch(() => window.location.reload()); +} + +document.addEventListener('DOMContentLoaded', function() { + document.querySelectorAll('[data-week-before]').forEach(link => { + link.addEventListener('click', function(event) { + event.preventDefault(); + loadReservationStatuses(this.dataset.weekBefore, true); + }); + }); + + window.addEventListener('popstate', function() { + const params = new URLSearchParams(window.location.search); + loadReservationStatuses(params.get('before') || '', false); + }); + + const table = document.getElementById('reservationTable'); + if (table) loadReservationStatuses(table.dataset.before || '', false); +}); diff --git a/reservar/index.php b/reservar/index.php index 2b11a2c..c351bf3 100644 --- a/reservar/index.php +++ b/reservar/index.php @@ -28,6 +28,7 @@ +