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 @@ +