406 lines
22 KiB
PHP
406 lines
22 KiB
PHP
<?php
|
|
session_start();
|
|
require_once 'db/config.php';
|
|
|
|
// Fetch settings
|
|
$settings_stmt = db()->query('SELECT setting_key, setting_value FROM settings');
|
|
$settings = $settings_stmt->fetchAll(PDO::FETCH_KEY_PAIR);
|
|
$coach_mode = $settings['coach_mode'] ?? 'multi';
|
|
|
|
$coach_id = $_GET['id'] ?? null;
|
|
$coach = null;
|
|
$error_message = '';
|
|
$reviews = [];
|
|
$average_rating = 0;
|
|
|
|
if ($coach_id) {
|
|
try {
|
|
$pdo = db();
|
|
$stmt = $pdo->prepare("SELECT * FROM coaches WHERE id = ?");
|
|
$stmt->execute([$coach_id]);
|
|
$coach = $stmt->fetch();
|
|
if (!$coach) {
|
|
$error_message = 'Coach not found.';
|
|
} else {
|
|
$stmt = $pdo->prepare("SELECT r.*, c.name as client_name FROM reviews r JOIN clients c ON r.client_id = c.id WHERE r.coach_id = ? ORDER BY r.created_at DESC");
|
|
$stmt->execute([$coach_id]);
|
|
$reviews = $stmt->fetchAll();
|
|
|
|
$portfolio_stmt = $pdo->prepare("SELECT * FROM coach_portfolio_items WHERE coach_id = ? ORDER BY created_at DESC");
|
|
$portfolio_stmt->execute([$coach_id]);
|
|
$portfolio_items = $portfolio_stmt->fetchAll();
|
|
|
|
if (count($reviews) > 0) {
|
|
$total_rating = 0;
|
|
foreach ($reviews as $review) {
|
|
$total_rating += $review['rating'];
|
|
}
|
|
$average_rating = round($total_rating / count($reviews), 1);
|
|
}
|
|
}
|
|
} catch (PDOException $e) {
|
|
$error_message = 'Error fetching coach details: ' . $e->getMessage();
|
|
}
|
|
} else {
|
|
$error_message = 'No coach ID specified.';
|
|
}
|
|
?>
|
|
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title><?php echo $coach ? htmlspecialchars($coach['name']) : 'Coach Profile'; ?> - CoachConnect</title>
|
|
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">
|
|
<link href="https://cdn.jsdelivr.net/npm/vanilla-calendar-pro@2.9.6/build/vanilla-calendar.min.css" rel="stylesheet">
|
|
<link rel="stylesheet" href="assets/css/custom.css">
|
|
</head>
|
|
<body class="bg-gray-50">
|
|
|
|
<header class="bg-white shadow-md">
|
|
<nav class="container mx-auto px-6 py-4 flex justify-between items-center">
|
|
<a href="index.php" class="text-2xl font-bold text-gray-800">CoachConnect</a>
|
|
<div class="flex space-x-4">
|
|
<a href="index.php" class="text-gray-600 hover:text-blue-500">Home</a>
|
|
<?php if ($coach_mode === 'multi'): ?>
|
|
<a href="coaches.php" class="text-gray-600 hover:text-blue-500">Coaches</a>
|
|
<?php endif; ?>
|
|
<?php if (isset($_SESSION['user_id'])): ?>
|
|
<a href="dashboard.php" class="text-gray-600 hover:text-blue-500">Dashboard</a>
|
|
<a href="messages.php" class="text-gray-600 hover:text-blue-500">Messages</a>
|
|
<a href="logout.php" class="text-gray-600 hover:text-blue-500">Logout</a>
|
|
<?php else: ?>
|
|
<a href="login.php" class="text-gray-600 hover:text-blue-500">Login</a>
|
|
<a href="register-client.php" class="text-gray-600 hover:text-blue-500">Sign Up</a>
|
|
<a href="register-coach.php" class="text-gray-600 hover:text-blue-500">Become a Coach</a>
|
|
<?php endif; ?>
|
|
</div>
|
|
</nav>
|
|
</header>
|
|
|
|
<main class="container mx-auto px-6 py-12">
|
|
<?php if (isset($_GET['error']) && $_GET['error'] === 'no_package'): ?>
|
|
<div class="max-w-4xl mx-auto bg-red-100 border-l-4 border-red-500 text-red-700 p-4 rounded-lg shadow-md mt-8" role="alert">
|
|
<h3 class="font-bold">Booking Error</h3>
|
|
<p>You must purchase a package with this coach before you can book a session. Please select one of the packages below.</p>
|
|
</div>
|
|
<?php endif; ?>
|
|
|
|
<?php if ($coach): ?>
|
|
<section class="max-w-4xl mx-auto bg-white p-8 rounded-lg shadow-md">
|
|
<div class="flex flex-col md:flex-row items-center">
|
|
<div class="md:w-1/3 text-center mb-6 md:mb-0">
|
|
<img class="w-48 h-48 rounded-full mx-auto shadow-lg" src="<?php echo htmlspecialchars($coach['photo_url'] ?: 'https://i.pravatar.cc/300?img=' . $coach['id']); ?>" alt="<?php echo htmlspecialchars($coach['name']); ?>">
|
|
</div>
|
|
<div class="md:w-2/3 md:pl-8">
|
|
<h1 class="text-4xl font-bold text-gray-800 mb-2"><?php echo htmlspecialchars($coach['name']); ?></h1>
|
|
<p class="text-xl text-blue-600 font-semibold mb-4"><?php echo htmlspecialchars($coach['specialties']); ?></p>
|
|
<div class="text-gray-600 leading-relaxed"><?php echo nl2br(htmlspecialchars($coach['bio'])); ?></div>
|
|
<?php if (isset($_SESSION['user_id']) && isset($_SESSION['user_type']) && $_SESSION['user_type'] === 'client'): ?>
|
|
<a href="messages.php?user_id=<?php echo $coach['id']; ?>&user_type=coach" class="mt-4 inline-block bg-blue-600 text-white font-bold py-2 px-4 rounded-lg hover:bg-blue-700">Message</a>
|
|
<?php endif; ?>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<?php if (!empty($portfolio_items)):
|
|
<section class="max-w-4xl mx-auto bg-white p-8 rounded-lg shadow-md mt-8">
|
|
<h2 class="text-2xl font-bold text-gray-800 mb-4">Portfolio</h2>
|
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
|
<?php foreach ($portfolio_items as $item): ?>
|
|
<div class="rounded-lg overflow-hidden shadow-lg">
|
|
<img src="<?php echo htmlspecialchars($item['url']); ?>" alt="<?php echo htmlspecialchars($item['caption']); ?>" class="w-full h-48 object-cover">
|
|
<?php if (!empty($item['caption'])) : ?>
|
|
<div class="p-4">
|
|
<p class="text-gray-700"><?php echo htmlspecialchars($item['caption']); ?></p>
|
|
</div>
|
|
<?php endif; ?>
|
|
</div>
|
|
<?php endforeach; ?>
|
|
</div>
|
|
</section>
|
|
<?php endif; ?>
|
|
|
|
<?php
|
|
$package_stmt = db()->prepare('SELECT * FROM service_packages WHERE coach_id = ?');
|
|
$package_stmt->execute([$coach_id]);
|
|
$packages = $package_stmt->fetchAll(PDO::FETCH_ASSOC);
|
|
?>
|
|
<?php if (!empty($packages)) : ?>
|
|
<section class="max-w-4xl mx-auto bg-white p-8 rounded-lg shadow-md mt-8">
|
|
<h2 class="text-2xl font-bold text-gray-800 mb-4">Coaching Packages</h2>
|
|
<div class="space-y-4">
|
|
<?php foreach ($packages as $package): ?>
|
|
<?php
|
|
$items_stmt = db()->prepare('SELECT SUM(quantity) as total_sessions FROM package_service_items WHERE package_id = ? AND service_type IN ("one_on_one", "group_session")');
|
|
$items_stmt->execute([$package['id']]);
|
|
$items_result = $items_stmt->fetch();
|
|
$total_sessions = $items_result['total_sessions'] ?? 0;
|
|
?>
|
|
<div class="border rounded-lg p-4">
|
|
<h4 class="font-bold text-lg"><?= htmlspecialchars($package['name']) ?></h4>
|
|
<p class="text-gray-600 text-sm mb-2"><?= htmlspecialchars($package['description']) ?></p>
|
|
<div class="flex justify-between items-center">
|
|
<div>
|
|
<span class="font-bold text-blue-600">
|
|
<?php if ($package['payment_type'] === 'one_time'): ?>
|
|
$<?= htmlspecialchars(number_format($package['price'], 2)) ?>
|
|
<?php else: ?>
|
|
$<?= htmlspecialchars(number_format($package['price'], 2)) ?> / <?= htmlspecialchars($package['payment_plan_interval']) ?> for <?= htmlspecialchars($package['payment_plan_installments']) ?> installments
|
|
<?php endif; ?>
|
|
</span>
|
|
<?php if ($total_sessions > 0): ?>
|
|
<span class="text-gray-600 text-sm">for <?= htmlspecialchars($total_sessions) ?> sessions</span>
|
|
<?php endif; ?>
|
|
</div>
|
|
<a href="purchase-package.php?package_id=<?= $package['id'] ?>" class="inline-block bg-green-500 text-white font-bold py-2 px-4 rounded hover:bg-green-600 transition duration-300">Purchase</a>
|
|
</div>
|
|
<div class="text-sm text-gray-500 mt-2">
|
|
<span>Type: <?= htmlspecialchars(ucfirst(str_replace('_', ' ', $package['type']))) ?></span>
|
|
<?php if($package['client_limit']): ?>
|
|
| <span>Client Limit: <?= htmlspecialchars($package['client_limit']) ?></span>
|
|
<?php endif; ?>
|
|
<?php if($package['start_date']): ?>
|
|
| <span>Starts: <?= htmlspecialchars(date('M j, Y', strtotime($package['start_date']))) ?></span>
|
|
<?php endif; ?>
|
|
<?php if($package['end_date']): ?>
|
|
| <span>Ends: <?= htmlspecialchars(date('M j, Y', strtotime($package['end_date']))) ?></span>
|
|
<?php endif; ?>
|
|
</div>
|
|
</div>
|
|
<?php endforeach; ?>
|
|
</div>
|
|
</section>
|
|
<?php endif; ?>
|
|
|
|
<?php
|
|
if (isset($_SESSION['user_id']) && $_SESSION['user_role'] === 'client') {
|
|
$client_id = $_SESSION['user_id'];
|
|
$packages_stmt = $pdo->prepare("SELECT cp.*, sp.name FROM client_packages cp JOIN service_packages sp ON cp.package_id = sp.id WHERE cp.client_id = ? AND sp.coach_id = ? AND cp.sessions_remaining > 0");
|
|
$packages_stmt->execute([$client_id, $coach_id]);
|
|
$client_packages = $packages_stmt->fetchAll();
|
|
|
|
if ($client_packages) {
|
|
echo '<section class="max-w-4xl mx-auto bg-blue-100 border-l-4 border-blue-500 text-blue-700 p-4 rounded-lg shadow-md mt-8" role="alert">';
|
|
echo '<h3 class="font-bold">You have active packages!</h3>';
|
|
foreach ($client_packages as $pkg) {
|
|
echo '<p>You have ' . htmlspecialchars($pkg['sessions_remaining']) . ' sessions remaining for the \'' . htmlspecialchars($pkg['name']) . '\' package. These will be used automatically when you book a session.</p>';
|
|
}
|
|
echo '</section>';
|
|
}
|
|
}
|
|
?>
|
|
|
|
<?php if (isset($_SESSION['user_id']) && $_SESSION['user_role'] === 'client'): ?>
|
|
<section class="max-w-4xl mx-auto bg-white p-8 rounded-lg shadow-md mt-8">
|
|
<h2 class="text-2xl font-bold text-gray-800 mb-4">Select a Date</h2>
|
|
<div id="calendar" class="mb-4"></div>
|
|
<h3 class="text-xl font-bold text-gray-800 mb-2">Available Time Slots</h3>
|
|
<div id="time-slots" class="flex flex-wrap gap-2"></div>
|
|
<form action="book-session.php" method="POST" id="booking-form" class="hidden mt-4">
|
|
<input type="hidden" name="coach_id" value="<?php echo $coach['id']; ?>">
|
|
<div class="mb-4">
|
|
<label for="booking_time" class="block text-gray-700 font-bold mb-2">Selected Date and Time:</label>
|
|
<input type="datetime-local" id="booking_time" name="booking_time" class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline" required readonly>
|
|
</div>
|
|
<div class="mb-4">
|
|
<label class="block text-gray-700 font-bold mb-2">
|
|
<input type="checkbox" id="recurring" name="recurring" class="mr-2 leading-tight">Book recurring sessions
|
|
</label>
|
|
</div>
|
|
<div id="recurring-options" class="hidden mb-4">
|
|
<div class="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<label for="frequency" class="block text-gray-700 font-bold mb-2">Frequency:</label>
|
|
<select id="frequency" name="frequency" class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline">
|
|
<option value="weekly">Weekly</option>
|
|
<option value="bi-weekly">Bi-weekly</option>
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label for="recurrences" class="block text-gray-700 font-bold mb-2">Number of sessions:</label>
|
|
<input type="number" id="recurrences" name="recurrences" class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline" min="1" value="1">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<button type="submit" class="bg-blue-600 text-white font-bold py-2 px-4 rounded-lg hover:bg-blue-700">Request Booking</button>
|
|
</form>
|
|
</section>
|
|
<?php endif; ?>
|
|
|
|
<section class="max-w-4xl mx-auto bg-white p-8 rounded-lg shadow-md mt-8">
|
|
<h2 class="text-2xl font-bold text-gray-800 mb-4">Reviews</h2>
|
|
<?php if (count($reviews) > 0): ?>
|
|
<div class="mb-6">
|
|
<h3 class="text-xl font-bold text-gray-800">Average Rating: <?php echo $average_rating; ?> / 5</h3>
|
|
<div class="flex items-center mt-2">
|
|
<?php for ($i = 1; $i <= 5; $i++): ?>
|
|
<svg class="w-6 h-6 <?php echo $i <= $average_rating ? 'text-yellow-400' : 'text-gray-300'; ?>" fill="currentColor" viewBox="0 0 20 20">
|
|
<path d="M10 15l-5.878 3.09 1.123-6.545L.489 6.91l6.572-.955L10 0l2.939 5.955 6.572.955-4.756 4.635 1.123 6.545z"/>
|
|
</svg>
|
|
<?php endfor; ?>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="space-y-6">
|
|
<?php foreach ($reviews as $review): ?>
|
|
<div class="border-t border-gray-200 pt-6">
|
|
<div class="flex items-center mb-2">
|
|
<p class="font-bold text-gray-800"><?php echo htmlspecialchars($review['client_name']); ?></p>
|
|
<span class="mx-2 text-gray-400">•</span>
|
|
<div class="flex items-center">
|
|
<?php for ($i = 1; $i <= 5; $i++): ?>
|
|
<svg class="w-5 h-5 <?php echo $i <= $review['rating'] ? 'text-yellow-400' : 'text-gray-300'; ?>" fill="currentColor" viewBox="0 0 20 20">
|
|
<path d="M10 15l-5.878 3.09 1.123-6.545L.489 6.91l6.572-.955L10 0l2.939 5.955 6.572.955-4.756 4.635 1.123 6.545z"/>
|
|
</svg>
|
|
<?php endfor; ?>
|
|
</div>
|
|
</div>
|
|
<p class="text-gray-600"><?php echo nl2br(htmlspecialchars($review['review'])); ?></p>
|
|
</div>
|
|
<?php endforeach; ?>
|
|
</div>
|
|
<?php else: ?>
|
|
<p>No reviews yet.</p>
|
|
<?php endif; ?>
|
|
</section>
|
|
|
|
<?php else: ?>
|
|
<section class="text-center">
|
|
<h1 class="text-3xl font-bold text-red-500">Error</h1>
|
|
<p class="text-xl text-gray-600 mt-4"><?php echo htmlspecialchars($error_message); ?></p>
|
|
<a href="index.php" class="bg-blue-600 text-white font-bold py-3 px-8 rounded-lg hover:bg-blue-700 mt-8 inline-block">Back to Home</a>
|
|
</section>
|
|
<?php endif; ?>
|
|
</main>
|
|
|
|
<footer class="bg-gray-800 text-white py-6 mt-12">
|
|
<div class="container mx-auto px-6 text-center">
|
|
<p>© <?php echo date("Y"); ?> CoachConnect. All rights reserved.</p>
|
|
</div>
|
|
</footer>
|
|
|
|
<script src="https://cdn.jsdelivr.net/npm/vanilla-calendar-pro@2.9.6/build/vanilla-calendar.min.js"></script>
|
|
<script>
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
const coachId = <?php echo $coach['id']; ?>;
|
|
const calendarElement = document.getElementById('calendar');
|
|
const timeSlotsElement = document.getElementById('time-slots');
|
|
const bookingForm = document.getElementById('booking-form');
|
|
const bookingTimeInput = document.getElementById('booking_time');
|
|
|
|
if (!calendarElement) return;
|
|
|
|
let availableSlots = [];
|
|
|
|
const calendar = new VanillaCalendar(calendarElement, {
|
|
settings: {
|
|
selection: {
|
|
day: 'single',
|
|
},
|
|
visibility: {
|
|
daysOutside: false,
|
|
},
|
|
},
|
|
actions: {
|
|
clickDay(e, self) {
|
|
updateAvailableTimes(self.selectedDates[0]);
|
|
},
|
|
update(self) {
|
|
const year = self.viewYear;
|
|
const month = self.viewMonth;
|
|
const startDate = new Date(year, month, 1);
|
|
const endDate = new Date(year, month + 1, 0);
|
|
fetchAvailability(startDate, endDate);
|
|
}
|
|
}
|
|
});
|
|
calendar.init();
|
|
|
|
async function fetchAvailability(startDate, endDate) {
|
|
const startStr = startDate.toISOString().split('T')[0];
|
|
const endStr = endDate.toISOString().split('T')[0];
|
|
const response = await fetch(`api/availability.php?coach_id=${coachId}&start=${startStr}&end=${endStr}`);
|
|
const data = await response.json();
|
|
|
|
const { availability, bookings } = data;
|
|
|
|
const allSlots = [];
|
|
availability.forEach(slot => {
|
|
const startTime = new Date(slot.start_time);
|
|
const endTime = new Date(slot.end_time);
|
|
|
|
let currentTime = startTime;
|
|
while (currentTime < endTime) {
|
|
allSlots.push(new Date(currentTime));
|
|
currentTime.setHours(currentTime.getHours() + 1);
|
|
}
|
|
});
|
|
|
|
const bookedPeriods = bookings.map(b => ({
|
|
start: new Date(b.start_time).getTime(),
|
|
end: new Date(b.end_time).getTime()
|
|
}));
|
|
|
|
availableSlots = allSlots.filter(slot => {
|
|
const slotTime = slot.getTime();
|
|
// Check if the slot is within any of the booked periods
|
|
return !bookedPeriods.some(period => slotTime >= period.start && slotTime < period.end);
|
|
});
|
|
|
|
const availableDates = availableSlots.map(slot => slot.toISOString().split('T')[0]);
|
|
|
|
calendar.settings.selected.dates = [];
|
|
calendar.settings.selected.month = startDate.getMonth();
|
|
calendar.settings.selected.year = startDate.getFullYear();
|
|
|
|
const highlightedDates = [...new Set(availableDates)];
|
|
calendar.settings.visibility.highlightedDates = highlightedDates;
|
|
calendar.update();
|
|
}
|
|
|
|
function updateAvailableTimes(selectedDate) {
|
|
const slotsForDay = availableSlots.filter(slot => slot.toISOString().split('T')[0] === selectedDate);
|
|
|
|
timeSlotsElement.innerHTML = '';
|
|
if (slotsForDay.length > 0) {
|
|
slotsForDay.forEach(slot => {
|
|
const button = document.createElement('button');
|
|
button.classList.add('bg-gray-200', 'hover:bg-blue-500', 'hover:text-white', 'text-gray-800', 'font-bold', 'py-2', 'px-4', 'rounded');
|
|
button.textContent = slot.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
|
button.addEventListener('click', () => {
|
|
const year = slot.getFullYear();
|
|
const month = (`0` + (slot.getMonth() + 1)).slice(-2);
|
|
const day = (`0` + slot.getDate()).slice(-2);
|
|
const hours = (`0` + slot.getHours()).slice(-2);
|
|
const minutes = (`0` + slot.getMinutes()).slice(-2);
|
|
bookingTimeInput.value = `${year}-${month}-${day}T${hours}:${minutes}`;
|
|
bookingForm.classList.remove('hidden');
|
|
});
|
|
timeSlotsElement.appendChild(button);
|
|
});
|
|
} else {
|
|
timeSlotsElement.innerHTML = '<p>No available time slots for this day.</p>';
|
|
}
|
|
}
|
|
|
|
const initialDate = new Date();
|
|
const startDate = new Date(initialDate.getFullYear(), initialDate.getMonth(), 1);
|
|
const endDate = new Date(initialDate.getFullYear(), initialDate.getMonth() + 1, 0);
|
|
fetchAvailability(startDate, endDate);
|
|
|
|
const recurringCheckbox = document.getElementById('recurring');
|
|
const recurringOptions = document.getElementById('recurring-options');
|
|
|
|
recurringCheckbox.addEventListener('change', () => {
|
|
if (recurringCheckbox.checked) {
|
|
recurringOptions.classList.remove('hidden');
|
|
} else {
|
|
recurringOptions.classList.add('hidden');
|
|
}
|
|
});
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|