Initial commit: MSPE website - full site with admin panel, API, and public pages
This commit is contained in:
Executable
+605
@@ -0,0 +1,605 @@
|
||||
<?php
|
||||
/**
|
||||
* MSPE Calendar/Booking API
|
||||
*/
|
||||
|
||||
require_once 'config.php';
|
||||
|
||||
$method = $_SERVER['REQUEST_METHOD'];
|
||||
$id = $_GET['id'] ?? null;
|
||||
|
||||
switch ($method) {
|
||||
case 'GET':
|
||||
if ($id) {
|
||||
getBooking($id);
|
||||
} else {
|
||||
$action = $_GET['action'] ?? '';
|
||||
if ($action === 'availability') {
|
||||
getAvailability();
|
||||
} elseif ($action === 'public-availability') {
|
||||
getPublicAvailability();
|
||||
} elseif ($action === 'blocked-times') {
|
||||
requireAuth();
|
||||
getBlockedTimes();
|
||||
} elseif ($action === 'upcoming') {
|
||||
getUpcomingBookings();
|
||||
} else {
|
||||
getBookings();
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 'POST':
|
||||
$action = $_GET['action'] ?? '';
|
||||
if ($action === 'create-slot') {
|
||||
requireAuth();
|
||||
createAvailabilitySlot();
|
||||
} elseif ($action === 'block-time') {
|
||||
requireAuth();
|
||||
blockTime();
|
||||
} elseif ($action === 'book') {
|
||||
createBooking(true);
|
||||
} else {
|
||||
requireAuth();
|
||||
createBooking(false);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'PUT':
|
||||
requireAuth();
|
||||
if (!$id) {
|
||||
jsonResponse(['success' => 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 = '<h2>Booking Received</h2>'
|
||||
. '<p>Thank you for booking a consultation with MSPE!</p>'
|
||||
. '<p><strong>Date:</strong> ' . $date . '</p>'
|
||||
. '<p><strong>Time:</strong> ' . $time . '</p>'
|
||||
. '<p><strong>Duration:</strong> ' . $booking['duration'] . ' minutes</p>'
|
||||
. '<p>Your booking is currently pending confirmation.</p>';
|
||||
|
||||
$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 = '<h2>Booking Confirmed!</h2>'
|
||||
. '<p>Your consultation with MSPE has been confirmed.</p>'
|
||||
. '<p><strong>Date:</strong> ' . $date . '</p>'
|
||||
. '<p><strong>Time:</strong> ' . $time . '</p>'
|
||||
. '<p><strong>Duration:</strong> ' . $booking['duration'] . ' minutes</p>'
|
||||
. '<p>We look forward to speaking with you!</p>';
|
||||
|
||||
$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 = '<h2>Booking Cancelled</h2>'
|
||||
. '<p>Your consultation with MSPE has been cancelled.</p>'
|
||||
. '<p>If you would like to reschedule, please contact us at ' . ADMIN_EMAIL . '</p>';
|
||||
|
||||
$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 = '<h2>New Booking Request</h2>'
|
||||
. '<p><strong>Name:</strong> ' . htmlspecialchars($booking['first_name'] . ' ' . $booking['last_name']) . '</p>'
|
||||
. '<p><strong>Email:</strong> ' . htmlspecialchars($booking['email']) . '</p>'
|
||||
. '<p><strong>Phone:</strong> ' . htmlspecialchars($booking['phone']) . '</p>'
|
||||
. '<p><strong>Company:</strong> ' . htmlspecialchars($booking['company']) . '</p>'
|
||||
. '<p><strong>Date:</strong> ' . $booking['booking_date'] . '</p>'
|
||||
. '<p><strong>Time:</strong> ' . $booking['booking_time'] . '</p>'
|
||||
. '<p><strong>Duration:</strong> ' . $booking['duration'] . ' minutes</p>'
|
||||
. '<p><strong>Message:</strong><br>' . nl2br(htmlspecialchars($booking['message'])) . '</p>';
|
||||
|
||||
$result = sendEmail($to, $subject, $html, $plain, $booking['email']);
|
||||
if (!$result['success']) {
|
||||
error_log('MSPE admin booking notification failed: ' . ($result['message'] ?? 'unknown'));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user