false, 'message' => 'Booking ID required'], 400); } updateBooking($id); break; case 'DELETE': requireAuth(); if (!$id) { jsonResponse(['success' => false, 'message' => 'ID required'], 400); } $type = $_GET['type'] ?? 'booking'; if ($type === 'blocked') { deleteBlockedTime($id); } else { deleteBooking($id); } break; default: jsonResponse(['success' => false, 'message' => 'Method not allowed'], 405); } function getBookings() { global $db; requireAuth(); $bookings = $db->getAll('bookings'); $status = $_GET['status'] ?? null; $limit = (int)($_GET['limit'] ?? 50); $offset = (int)($_GET['offset'] ?? 0); $dateFrom = $_GET['date_from'] ?? null; $dateTo = $_GET['date_to'] ?? null; if ($status) { $bookings = array_filter($bookings, function($b) use ($status) { return $b['status'] === $status; }); } if ($dateFrom) { $bookings = array_filter($bookings, function($b) use ($dateFrom) { return $b['booking_date'] >= $dateFrom; }); } if ($dateTo) { $bookings = array_filter($bookings, function($b) use ($dateTo) { return $b['booking_date'] <= $dateTo; }); } usort($bookings, function($a, $b) { return strtotime($a['booking_date'] . ' ' . $a['booking_time']) - strtotime($b['booking_date'] . ' ' . $b['booking_time']); }); $total = count($bookings); $bookings = array_slice(array_values($bookings), $offset, $limit); jsonResponse([ 'success' => true, 'data' => $bookings, 'total' => $total, 'limit' => $limit, 'offset' => $offset ]); } function getBooking($id) { global $db; requireAuth(); $booking = $db->get('bookings', $id); if (!$booking) { jsonResponse(['success' => false, 'message' => 'Booking not found'], 404); } jsonResponse([ 'success' => true, 'data' => $booking ]); } function getAvailability() { global $db; $slots = $db->getAll('availability_slots'); $bookings = $db->getAll('bookings'); $dateFrom = $_GET['date_from'] ?? date('Y-m-d'); $dateTo = $_GET['date_to'] ?? date('Y-m-d', strtotime('+30 days')); $slots = array_filter($slots, function($slot) use ($dateFrom, $dateTo) { return $slot['date'] >= $dateFrom && $slot['date'] <= $dateTo && $slot['is_available']; }); $result = []; foreach ($slots as $slot) { $slotBookings = array_filter($bookings, function($booking) use ($slot) { return $booking['booking_date'] === $slot['date'] && $booking['booking_time'] === $slot['time'] && in_array($booking['status'], ['confirmed', 'pending']); }); $slot['booked'] = count($slotBookings) > 0; $slot['remaining'] = $slot['capacity'] - count($slotBookings); $result[] = $slot; } usort($result, function($a, $b) { return strtotime($a['date'] . ' ' . $a['time']) - strtotime($b['date'] . ' ' . $b['time']); }); jsonResponse([ 'success' => true, 'data' => $result ]); } /** * Get public availability - shows only available/not available status * Does NOT expose any booking details, client info, or internal notes * This is specifically for the public calendar */ function getPublicAvailability() { global $db; $slots = $db->getAll('availability_slots'); $bookings = $db->getAll('bookings'); $blockedTimes = $db->getAll('blocked_times'); $dateFrom = $_GET['date_from'] ?? date('Y-m-d'); $dateTo = $_GET['date_to'] ?? date('Y-m-d', strtotime('+60 days')); // Filter slots within date range that are marked as available $slots = array_filter($slots, function($slot) use ($dateFrom, $dateTo) { return $slot['date'] >= $dateFrom && $slot['date'] <= $dateTo && $slot['is_available']; }); $result = []; foreach ($slots as $slot) { // Count active bookings for this slot $slotBookings = array_filter($bookings, function($booking) use ($slot) { return $booking['booking_date'] === $slot['date'] && $booking['booking_time'] === $slot['time'] && in_array($booking['status'], ['confirmed', 'pending']); }); // Check if time is blocked by admin $isBlocked = false; foreach ($blockedTimes as $blocked) { if ($blocked['date'] === $slot['date'] && $blocked['time'] === $slot['time']) { $isBlocked = true; break; } } $bookedCount = count($slotBookings); $isFullyBooked = $bookedCount >= ($slot['capacity'] ?? 1); // Only return minimal info - just what the public needs $result[] = [ 'date' => $slot['date'], 'time' => $slot['time'], 'available' => !$isFullyBooked && !$isBlocked, // Don't expose: capacity, remaining, notes, booking details, etc. ]; } usort($result, function($a, $b) { return strtotime($a['date'] . ' ' . $a['time']) - strtotime($b['date'] . ' ' . $b['time']); }); jsonResponse([ 'success' => true, 'data' => $result ]); } function getUpcomingBookings() { global $db; requireAuth(); $bookings = $db->getAll('bookings'); $today = date('Y-m-d'); $upcoming = array_filter($bookings, function($b) use ($today) { return $b['booking_date'] >= $today && in_array($b['status'], ['confirmed', 'pending']); }); usort($upcoming, function($a, $b) { return strtotime($a['booking_date'] . ' ' . $a['booking_time']) - strtotime($b['booking_date'] . ' ' . $b['booking_time']); }); jsonResponse([ 'success' => true, 'data' => array_slice(array_values($upcoming), 0, 10) ]); } /** * Block a time slot (admin only) * This makes a specific date/time unavailable without creating a booking */ function blockTime() { global $db; $data = getRequestBody(); if (empty($data['date']) || empty($data['time'])) { jsonResponse(['success' => false, 'message' => 'Date and time are required'], 400); } $blockData = [ 'date' => sanitize($data['date']), 'time' => sanitize($data['time']), 'reason' => sanitize($data['reason'] ?? ''), 'blocked_by' => 'admin', 'created_at' => date('Y-m-d H:i:s') ]; $blocked = $db->insert('blocked_times', $blockData); jsonResponse([ 'success' => true, 'message' => 'Time blocked successfully', 'data' => $blocked ], 201); } /** * Get all blocked times (admin only) */ function getBlockedTimes() { global $db; $blockedTimes = $db->getAll('blocked_times'); // Sort by date and time usort($blockedTimes, function($a, $b) { return strtotime($a['date'] . ' ' . $a['time']) - strtotime($b['date'] . ' ' . $b['time']); }); jsonResponse([ 'success' => true, 'data' => $blockedTimes ]); } /** * Delete a blocked time (admin only) */ function deleteBlockedTime($id) { global $db; $existing = $db->get('blocked_times', $id); if (!$existing) { jsonResponse(['success' => false, 'message' => 'Blocked time not found'], 404); } $db->delete('blocked_times', $id); jsonResponse([ 'success' => true, 'message' => 'Blocked time removed successfully' ]); } function createAvailabilitySlot() { global $db; $data = getRequestBody(); if (empty($data['date']) || empty($data['time'])) { jsonResponse(['success' => false, 'message' => 'Date and time are required'], 400); } $slotData = [ 'date' => sanitize($data['date']), 'time' => sanitize($data['time']), 'capacity' => (int)($data['capacity'] ?? 1), 'is_available' => true, 'notes' => sanitize($data['notes'] ?? '') ]; $slot = $db->insert('availability_slots', $slotData); jsonResponse([ 'success' => true, 'message' => 'Availability slot created', 'data' => $slot ], 201); } function createBooking($isPublicRequest = false) { global $db; if ($isPublicRequest) { requireSameOriginRequest(); // Rate limiting: max 5 booking requests per IP per hour $ip = $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0'; $oneHourAgo = date('Y-m-d H:i:s', strtotime('-1 hour')); $now = date('Y-m-d H:i:s'); $recentCount = $db->countByDateRange('bookings', 'created_at', $oneHourAgo, $now, [ 'ip_address' => $ip ]); if ($recentCount >= 5) { jsonResponse(['success' => false, 'message' => 'Too many booking requests. Please try again later.'], 429); } } $data = getRequestBody(); $required = ['first_name', 'last_name', 'email', 'booking_date', 'booking_time']; foreach ($required as $field) { if (empty($data[$field])) { jsonResponse(['success' => false, 'message' => ucfirst(str_replace('_', ' ', $field)) . ' is required'], 400); } } if (!filter_var($data['email'], FILTER_VALIDATE_EMAIL)) { jsonResponse(['success' => false, 'message' => 'Invalid email address'], 400); } $bookingDate = date('Y-m-d', strtotime($data['booking_date'])); $today = date('Y-m-d'); if ($bookingDate < $today) { jsonResponse(['success' => false, 'message' => 'Cannot book appointments in the past'], 400); } $slots = $db->query('availability_slots', [ 'date' => $bookingDate, 'time' => $data['booking_time'] ]); if (empty($slots) || !$slots[array_key_first($slots)]['is_available']) { jsonResponse(['success' => false, 'message' => 'This time slot is not available'], 400); } $slot = array_values($slots)[0]; $bookings = $db->query('bookings', [ 'booking_date' => $bookingDate, 'booking_time' => $data['booking_time'] ]); $activeBookings = array_filter($bookings, function($b) { return in_array($b['status'], ['confirmed', 'pending']); }); if (count($activeBookings) >= $slot['capacity']) { jsonResponse(['success' => false, 'message' => 'This time slot is fully booked'], 400); } $bookingData = [ 'first_name' => sanitize($data['first_name']), 'last_name' => sanitize($data['last_name']), 'email' => sanitize($data['email']), 'phone' => sanitize($data['phone'] ?? ''), 'company' => sanitize($data['company'] ?? ''), 'service_interest' => sanitize($data['service_interest'] ?? ''), 'booking_date' => $bookingDate, 'booking_time' => sanitize($data['booking_time']), 'duration' => (int)($data['duration'] ?? 60), 'message' => sanitize($data['message'] ?? ''), 'status' => 'pending', 'created_at' => date('Y-m-d H:i:s'), 'ip_address' => $_SERVER['REMOTE_ADDR'] ?? '' ]; $booking = $db->insert('bookings', $bookingData); sendBookingConfirmationEmail($bookingData); sendAdminBookingNotification($bookingData); jsonResponse([ 'success' => true, 'message' => 'Booking request submitted successfully. We will confirm your appointment shortly.', 'data' => [ 'id' => $booking['id'], 'date' => $booking['booking_date'], 'time' => $booking['booking_time'], 'status' => 'pending' ] ], 201); } function updateBooking($id) { global $db; $existing = $db->get('bookings', $id); if (!$existing) { jsonResponse(['success' => false, 'message' => 'Booking not found'], 404); } $data = getRequestBody(); $allowed = ['status', 'notes']; $updateData = []; foreach ($allowed as $field) { if (isset($data[$field])) { $updateData[$field] = $data[$field]; } } if (!empty($data['status']) && $data['status'] === 'confirmed' && $existing['status'] !== 'confirmed') { sendBookingConfirmedEmail($existing); } if (!empty($data['status']) && $data['status'] === 'cancelled' && $existing['status'] !== 'cancelled') { sendBookingCancelledEmail($existing); } $booking = $db->update('bookings', $id, $updateData); jsonResponse([ 'success' => true, 'message' => 'Booking updated successfully', 'data' => $booking ]); } function deleteBooking($id) { global $db; $existing = $db->get('bookings', $id); if (!$existing) { jsonResponse(['success' => false, 'message' => 'Booking not found'], 404); } $db->delete('bookings', $id); jsonResponse([ 'success' => true, 'message' => 'Booking deleted successfully' ]); } function sendBookingConfirmationEmail($booking) { $to = $booking['email']; $subject = 'Booking Confirmation - ' . SITE_NAME; $date = date('F j, Y', strtotime($booking['booking_date'])); $time = date('g:i A', strtotime($booking['booking_time'])); $plain = "Thank you for booking a consultation with MSPE!\n\n"; $plain .= "Booking Details:\n"; $plain .= "Date: {$date}\n"; $plain .= "Time: {$time}\n"; $plain .= "Duration: {$booking['duration']} minutes\n\n"; $plain .= "Your booking is currently pending confirmation. We will send you a confirmation email shortly.\n\n"; $plain .= "If you need to reschedule or cancel, please contact us at " . ADMIN_EMAIL . "\n\n"; $plain .= "Best regards,\nThe MSPE Team"; $html = '

Booking Received

' . '

Thank you for booking a consultation with MSPE!

' . '

Date: ' . $date . '

' . '

Time: ' . $time . '

' . '

Duration: ' . $booking['duration'] . ' minutes

' . '

Your booking is currently pending confirmation.

'; $result = sendEmail($to, $subject, $html, $plain); if (!$result['success']) { error_log('MSPE booking confirmation email failed: ' . ($result['message'] ?? 'unknown')); } } function sendBookingConfirmedEmail($booking) { $to = $booking['email']; $subject = 'Booking Confirmed - ' . SITE_NAME; $date = date('F j, Y', strtotime($booking['booking_date'])); $time = date('g:i A', strtotime($booking['booking_time'])); $plain = "Your consultation with MSPE has been confirmed!\n\n"; $plain .= "Booking Details:\n"; $plain .= "Date: {$date}\n"; $plain .= "Time: {$time}\n"; $plain .= "Duration: {$booking['duration']} minutes\n\n"; $plain .= "We look forward to speaking with you!\n\n"; $plain .= "If you need to reschedule, please contact us at " . ADMIN_EMAIL . "\n\n"; $plain .= "Best regards,\nThe MSPE Team"; $html = '

Booking Confirmed!

' . '

Your consultation with MSPE has been confirmed.

' . '

Date: ' . $date . '

' . '

Time: ' . $time . '

' . '

Duration: ' . $booking['duration'] . ' minutes

' . '

We look forward to speaking with you!

'; $result = sendEmail($to, $subject, $html, $plain); if (!$result['success']) { error_log('MSPE booking confirmed email failed: ' . ($result['message'] ?? 'unknown')); } } function sendBookingCancelledEmail($booking) { $to = $booking['email']; $subject = 'Booking Cancelled - ' . SITE_NAME; $plain = "Your consultation with MSPE has been cancelled.\n\n"; $plain .= "We apologize for any inconvenience. If you would like to reschedule, please contact us at " . ADMIN_EMAIL . "\n\n"; $plain .= "Best regards,\nThe MSPE Team"; $html = '

Booking Cancelled

' . '

Your consultation with MSPE has been cancelled.

' . '

If you would like to reschedule, please contact us at ' . ADMIN_EMAIL . '

'; $result = sendEmail($to, $subject, $html, $plain); if (!$result['success']) { error_log('MSPE booking cancelled email failed: ' . ($result['message'] ?? 'unknown')); } } function sendAdminBookingNotification($booking) { $to = getSetting('admin_email', ADMIN_EMAIL); $subject = 'New Booking Request - ' . SITE_NAME; $plain = "A new booking request has been submitted.\n\n"; $plain .= "Client Details:\n"; $plain .= "Name: {$booking['first_name']} {$booking['last_name']}\n"; $plain .= "Email: {$booking['email']}\n"; $plain .= "Phone: {$booking['phone']}\n"; $plain .= "Company: {$booking['company']}\n\n"; $plain .= "Booking Details:\n"; $plain .= "Date: {$booking['booking_date']}\n"; $plain .= "Time: {$booking['booking_time']}\n"; $plain .= "Duration: {$booking['duration']} minutes\n"; $plain .= "Service Interest: {$booking['service_interest']}\n"; $plain .= "Message: {$booking['message']}\n"; $html = '

New Booking Request

' . '

Name: ' . htmlspecialchars($booking['first_name'] . ' ' . $booking['last_name']) . '

' . '

Email: ' . htmlspecialchars($booking['email']) . '

' . '

Phone: ' . htmlspecialchars($booking['phone']) . '

' . '

Company: ' . htmlspecialchars($booking['company']) . '

' . '

Date: ' . $booking['booking_date'] . '

' . '

Time: ' . $booking['booking_time'] . '

' . '

Duration: ' . $booking['duration'] . ' minutes

' . '

Message:
' . nl2br(htmlspecialchars($booking['message'])) . '

'; $result = sendEmail($to, $subject, $html, $plain, $booking['email']); if (!$result['success']) { error_log('MSPE admin booking notification failed: ' . ($result['message'] ?? 'unknown')); } }