931 lines
38 KiB
HTML
Executable File
931 lines
38 KiB
HTML
Executable File
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<meta name="robots" content="noindex, nofollow, noarchive">
|
|
<title>Availability - MSPE Admin</title>
|
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;600;700&display=swap" rel="stylesheet">
|
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" integrity="sha384-t1nt8BQoYMLFN5p42tRAtuAAFQaCQODekUVeKKZrEnEyp4H2R0RHFz0KWpmj7i8g" crossorigin="anonymous">
|
|
<link rel="stylesheet" href="/admin/css/admin.css">
|
|
<style>
|
|
.availability-grid {
|
|
display: grid;
|
|
grid-template-columns: 1fr 2fr;
|
|
gap: 2rem;
|
|
}
|
|
|
|
.add-slot-form {
|
|
background: white;
|
|
border-radius: 10px;
|
|
box-shadow: var(--shadow-sm);
|
|
padding: 1.5rem;
|
|
height: fit-content;
|
|
}
|
|
|
|
.add-slot-form h3 {
|
|
margin-bottom: 1.5rem;
|
|
}
|
|
|
|
.form-group {
|
|
margin-bottom: 1.25rem;
|
|
}
|
|
|
|
.form-group label {
|
|
display: block;
|
|
font-weight: 600;
|
|
color: var(--gray-700);
|
|
margin-bottom: 0.5rem;
|
|
font-size: 0.875rem;
|
|
}
|
|
|
|
.form-group input,
|
|
.form-group select,
|
|
.form-group textarea {
|
|
width: 100%;
|
|
padding: 0.75rem;
|
|
border: 1px solid var(--gray-300);
|
|
border-radius: var(--radius-sm);
|
|
font-size: 0.875rem;
|
|
font-family: var(--font-main);
|
|
}
|
|
|
|
.form-group input:focus,
|
|
.form-group select:focus {
|
|
outline: none;
|
|
border-color: var(--primary);
|
|
box-shadow: 0 0 0 3px rgba(14, 165, 233, 0.1);
|
|
}
|
|
|
|
.quick-slots {
|
|
margin-bottom: 1.5rem;
|
|
}
|
|
|
|
.quick-slots label {
|
|
font-weight: 600;
|
|
margin-bottom: 0.75rem;
|
|
display: block;
|
|
font-size: 0.875rem;
|
|
}
|
|
|
|
.quick-slots-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(3, 1fr);
|
|
gap: 0.5rem;
|
|
}
|
|
|
|
.quick-slot-btn {
|
|
padding: 0.5rem;
|
|
border: 1px solid var(--gray-300);
|
|
background: white;
|
|
border-radius: 6px;
|
|
cursor: pointer;
|
|
font-size: 0.875rem;
|
|
transition: all 0.3s ease;
|
|
}
|
|
|
|
.quick-slot-btn:hover {
|
|
border-color: var(--primary);
|
|
color: var(--primary);
|
|
}
|
|
|
|
.bulk-actions {
|
|
display: flex;
|
|
gap: 0.75rem;
|
|
margin-top: 1.5rem;
|
|
padding-top: 1.5rem;
|
|
border-top: 1px solid var(--gray-200);
|
|
}
|
|
|
|
.bulk-actions button {
|
|
flex: 1;
|
|
}
|
|
|
|
.slots-list {
|
|
background: white;
|
|
border-radius: 10px;
|
|
box-shadow: var(--shadow-sm);
|
|
}
|
|
|
|
.slots-list-header {
|
|
padding: 1.5rem;
|
|
border-bottom: 1px solid var(--gray-200);
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
}
|
|
|
|
.slots-list-header h3 {
|
|
margin: 0;
|
|
}
|
|
|
|
.slots-filters {
|
|
display: flex;
|
|
gap: 1rem;
|
|
}
|
|
|
|
.slot-date-filter {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
}
|
|
|
|
.slots-table {
|
|
width: 100%;
|
|
border-collapse: collapse;
|
|
}
|
|
|
|
.slots-table th {
|
|
background: var(--gray-100);
|
|
padding: 1rem;
|
|
text-align: left;
|
|
font-weight: 600;
|
|
color: var(--gray-600);
|
|
font-size: 0.875rem;
|
|
}
|
|
|
|
.slots-table td {
|
|
padding: 1rem;
|
|
border-bottom: 1px solid var(--gray-200);
|
|
font-size: 0.875rem;
|
|
}
|
|
|
|
.slots-table tr:hover {
|
|
background: var(--gray-50);
|
|
}
|
|
|
|
.slot-time {
|
|
font-weight: 600;
|
|
color: var(--primary);
|
|
}
|
|
|
|
.slot-capacity {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 0.25rem;
|
|
}
|
|
|
|
.slot-capacity i {
|
|
font-size: 0.75rem;
|
|
color: var(--gray-400);
|
|
}
|
|
|
|
.action-btn {
|
|
padding: 0.375rem 0.75rem;
|
|
border: none;
|
|
border-radius: 6px;
|
|
cursor: pointer;
|
|
font-size: 0.875rem;
|
|
margin-right: 0.5rem;
|
|
transition: all 0.3s ease;
|
|
}
|
|
|
|
.action-btn.delete {
|
|
background: #fee2e2;
|
|
color: #dc2626;
|
|
}
|
|
|
|
.action-btn.delete:hover {
|
|
background: #fecaca;
|
|
}
|
|
|
|
.action-btn.toggle {
|
|
background: #e0f2fe;
|
|
color: #0369a1;
|
|
}
|
|
|
|
.action-btn.toggle:hover {
|
|
background: #bae6fd;
|
|
}
|
|
|
|
.empty-state {
|
|
padding: 4rem 2rem;
|
|
text-align: center;
|
|
color: var(--gray-500);
|
|
}
|
|
|
|
.empty-state i {
|
|
font-size: 4rem;
|
|
margin-bottom: 1rem;
|
|
opacity: 0.3;
|
|
}
|
|
|
|
.upcoming-section {
|
|
background: var(--dark);
|
|
border-radius: 10px;
|
|
padding: 1.5rem;
|
|
margin-top: 2rem;
|
|
color: white;
|
|
}
|
|
|
|
.upcoming-section h4 {
|
|
margin-bottom: 1rem;
|
|
}
|
|
|
|
.upcoming-bookings {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0.75rem;
|
|
}
|
|
|
|
.upcoming-item {
|
|
background: rgba(255,255,255,0.05);
|
|
border-radius: 8px;
|
|
padding: 1rem;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 1rem;
|
|
}
|
|
|
|
.upcoming-item-date {
|
|
background: var(--primary);
|
|
border-radius: 6px;
|
|
padding: 0.5rem 0.75rem;
|
|
text-align: center;
|
|
min-width: 60px;
|
|
}
|
|
|
|
.upcoming-item-date .day {
|
|
font-size: 1.25rem;
|
|
font-weight: 700;
|
|
}
|
|
|
|
.upcoming-item-date .month {
|
|
font-size: 0.75rem;
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
@media (max-width: 1024px) {
|
|
.availability-grid {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
|
|
.quick-slots-grid {
|
|
grid-template-columns: repeat(4, 1fr);
|
|
}
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="admin-wrapper">
|
|
<aside class="sidebar" id="sidebar">
|
|
<div class="sidebar-header">
|
|
<a href="/admin/dashboard.html" class="sidebar-logo">
|
|
<i class="fas fa-cube"></i>
|
|
<span>MSPE</span>
|
|
</a>
|
|
<button class="sidebar-toggle" id="sidebar-toggle">
|
|
<i class="fas fa-bars"></i>
|
|
</button>
|
|
</div>
|
|
|
|
<nav class="sidebar-nav">
|
|
<div class="nav-section">
|
|
<span class="nav-section-title">Main</span>
|
|
<ul class="nav-menu">
|
|
<li class="nav-item">
|
|
<a href="/admin/dashboard.html" class="nav-link">
|
|
<i class="fas fa-th-large"></i>
|
|
<span>Dashboard</span>
|
|
</a>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
|
|
<div class="nav-section">
|
|
<span class="nav-section-title">Content</span>
|
|
<ul class="nav-menu">
|
|
<li class="nav-item">
|
|
<a href="/admin/pages.html" class="nav-link">
|
|
<i class="fas fa-file-alt"></i>
|
|
<span>Pages</span>
|
|
</a>
|
|
</li>
|
|
<li class="nav-item">
|
|
<a href="/admin/services.html" class="nav-link">
|
|
<i class="fas fa-concierge-bell"></i>
|
|
<span>Services</span>
|
|
</a>
|
|
</li>
|
|
<li class="nav-item">
|
|
<a href="/admin/portfolio.html" class="nav-link">
|
|
<i class="fas fa-briefcase"></i>
|
|
<span>Portfolio</span>
|
|
</a>
|
|
</li>
|
|
<li class="nav-item">
|
|
<a href="/admin/news.html" class="nav-link">
|
|
<i class="fas fa-newspaper"></i>
|
|
<span>News & Events</span>
|
|
</a>
|
|
</li>
|
|
<li class="nav-item">
|
|
<a href="/admin/testimonials.html" class="nav-link">
|
|
<i class="fas fa-quote-right"></i>
|
|
<span>Testimonials</span>
|
|
</a>
|
|
</li>
|
|
<li class="nav-item">
|
|
<a href="/admin/team.html" class="nav-link">
|
|
<i class="fas fa-users"></i>
|
|
<span>Team</span>
|
|
</a>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
|
|
<div class="nav-section">
|
|
<span class="nav-section-title">Media</span>
|
|
<ul class="nav-menu">
|
|
<li class="nav-item">
|
|
<a href="/admin/media.html" class="nav-link">
|
|
<i class="fas fa-images"></i>
|
|
<span>Media Library</span>
|
|
</a>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
|
|
<div class="nav-section">
|
|
<span class="nav-section-title">Inquiries</span>
|
|
<ul class="nav-menu">
|
|
<li class="nav-item">
|
|
<a href="/admin/messages.html" class="nav-link">
|
|
<i class="fas fa-envelope"></i>
|
|
<span>Messages</span>
|
|
</a>
|
|
</li>
|
|
<li class="nav-item">
|
|
<a href="/admin/subscribers.html" class="nav-link">
|
|
<i class="fas fa-bell"></i>
|
|
<span>Subscribers</span>
|
|
</a>
|
|
</li>
|
|
<li class="nav-item">
|
|
<a href="/admin/bookings.html" class="nav-link">
|
|
<i class="fas fa-calendar-check"></i>
|
|
<span>Bookings</span>
|
|
</a>
|
|
</li>
|
|
<li class="nav-item active">
|
|
<a href="/admin/availability.html" class="nav-link">
|
|
<i class="fas fa-clock"></i>
|
|
<span>Availability</span>
|
|
</a>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
|
|
<div class="nav-section">
|
|
<span class="nav-section-title">Settings</span>
|
|
<ul class="nav-menu">
|
|
<li class="nav-item">
|
|
<a href="/admin/settings.html" class="nav-link">
|
|
<i class="fas fa-cog"></i>
|
|
<span>Site Settings</span>
|
|
</a>
|
|
</li>
|
|
<li class="nav-item">
|
|
<a href="/admin/users.html" class="nav-link">
|
|
<i class="fas fa-user-shield"></i>
|
|
<span>Admin Users</span>
|
|
</a>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
</nav>
|
|
|
|
<div class="sidebar-footer">
|
|
<a href="/index.html" class="view-site-btn" target="_blank">
|
|
<i class="fas fa-external-link-alt"></i>
|
|
<span>View Website</span>
|
|
</a>
|
|
</div>
|
|
</aside>
|
|
|
|
<main class="main-content">
|
|
<header class="admin-header">
|
|
<div class="header-left">
|
|
<button class="mobile-toggle" id="mobile-toggle">
|
|
<i class="fas fa-bars"></i>
|
|
</button>
|
|
<h1 class="page-title">Manage Availability</h1>
|
|
</div>
|
|
|
|
<div class="header-right">
|
|
<div class="header-user">
|
|
<div class="user-avatar">
|
|
<img src="https://ui-avatars.com/api/?name=Admin&background=0ea5e9&color=fff" alt="Admin">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
|
|
<div class="content-wrapper">
|
|
<div class="availability-grid">
|
|
<div class="add-slot-form">
|
|
<h3><i class="fas fa-plus-circle" style="color: var(--primary); margin-right: 0.5rem;"></i> Add Time Slot</h3>
|
|
|
|
<form id="add-slot-form">
|
|
<div class="form-group">
|
|
<label>Date *</label>
|
|
<input type="date" name="date" required>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label>Time *</label>
|
|
<input type="time" name="time" required>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label>Capacity (number of clients)</label>
|
|
<input type="number" name="capacity" value="1" min="1" max="10">
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label>Notes</label>
|
|
<textarea name="notes" rows="3" placeholder="Optional notes about this slot..."></textarea>
|
|
</div>
|
|
|
|
<div class="quick-slots">
|
|
<label>Quick Add Times</label>
|
|
<div class="quick-slots-grid">
|
|
<button type="button" class="quick-slot-btn" onclick="addQuickTime('09:00')">9:00 AM</button>
|
|
<button type="button" class="quick-slot-btn" onclick="addQuickTime('10:00')">10:00 AM</button>
|
|
<button type="button" class="quick-slot-btn" onclick="addQuickTime('11:00')">11:00 AM</button>
|
|
<button type="button" class="quick-slot-btn" onclick="addQuickTime('13:00')">1:00 PM</button>
|
|
<button type="button" class="quick-slot-btn" onclick="addQuickTime('14:00')">2:00 PM</button>
|
|
<button type="button" class="quick-slot-btn" onclick="addQuickTime('15:00')">3:00 PM</button>
|
|
<button type="button" class="quick-slot-btn" onclick="addQuickTime('16:00')">4:00 PM</button>
|
|
<button type="button" class="quick-slot-btn" onclick="addQuickTime('17:00')">5:00 PM</button>
|
|
<button type="button" class="quick-slot-btn" onclick="addQuickTime('18:00')">6:00 PM</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="bulk-actions">
|
|
<button type="submit" class="btn btn-primary">
|
|
<i class="fas fa-plus"></i> Add Slot
|
|
</button>
|
|
<button type="button" class="btn btn-secondary" onclick="generateWeekSlots()">
|
|
<i class="fas fa-calendar-week"></i> Generate Week
|
|
</button>
|
|
</div>
|
|
</form>
|
|
|
|
<!-- Block Time Section -->
|
|
<div style="margin-top: 2rem; padding-top: 1.5rem; border-top: 1px solid var(--gray-200);">
|
|
<h3 style="margin-bottom: 1rem;"><i class="fas fa-ban" style="color: #ef4444; margin-right: 0.5rem;"></i> Block Time</h3>
|
|
<p style="font-size: 0.875rem; color: var(--gray-500); margin-bottom: 1rem;">Block a time slot to make it unavailable for public booking (vacation, personal time, etc.)</p>
|
|
|
|
<form id="block-time-form">
|
|
<div class="form-group">
|
|
<label>Date *</label>
|
|
<input type="date" name="block_date" required>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label>Time *</label>
|
|
<input type="time" name="block_time" required>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label>Reason (internal note)</label>
|
|
<input type="text" name="block_reason" placeholder="e.g., Personal appointment">
|
|
</div>
|
|
|
|
<button type="submit" class="btn btn-secondary" style="width: 100%; background: #fee2e2; color: #dc2626; border-color: #ef4444;">
|
|
<i class="fas fa-ban"></i> Block This Time
|
|
</button>
|
|
</form>
|
|
</div>
|
|
|
|
<div class="upcoming-section" id="upcoming-section" style="display: none;">
|
|
<h4><i class="fas fa-clock"></i> Upcoming Bookings</h4>
|
|
<div class="upcoming-bookings" id="upcoming-bookings"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="slots-list">
|
|
<div class="slots-list-header">
|
|
<h3>Availability Slots</h3>
|
|
<div class="slots-filters">
|
|
<div class="slot-date-filter">
|
|
<label>From:</label>
|
|
<input type="date" id="filter-date-from" style="padding: 0.5rem;">
|
|
</div>
|
|
<div class="slot-date-filter">
|
|
<label>To:</label>
|
|
<input type="date" id="filter-date-to" style="padding: 0.5rem;">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Blocked Times Section -->
|
|
<div id="blocked-times-section" style="display: none; background: #fef2f2; border-bottom: 1px solid #fecaca;">
|
|
<div style="padding: 1rem 1.5rem; display: flex; justify-content: space-between; align-items: center;">
|
|
<h4 style="margin: 0; color: #dc2626;"><i class="fas fa-ban" style="margin-right: 0.5rem;"></i> Blocked Times</h4>
|
|
</div>
|
|
<div id="blocked-times-list" style="padding: 0 1.5rem 1rem;"></div>
|
|
</div>
|
|
|
|
<div class="slots-table-container">
|
|
<table class="slots-table">
|
|
<thead>
|
|
<tr>
|
|
<th>Date</th>
|
|
<th>Time</th>
|
|
<th>Capacity</th>
|
|
<th>Status</th>
|
|
<th>Notes</th>
|
|
<th>Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="slots-table-body">
|
|
<tr>
|
|
<td colspan="6">
|
|
<div class="empty-state">
|
|
<i class="fas fa-calendar-alt"></i>
|
|
<p>Loading availability slots...</p>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</main>
|
|
</div>
|
|
|
|
<script src="/admin/js/admin.js"></script>
|
|
<script>
|
|
let currentSlots = [];
|
|
let blockedTimes = [];
|
|
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
loadSlots();
|
|
loadBlockedTimes();
|
|
loadUpcomingBookings();
|
|
setupEventListeners();
|
|
setDefaultDate();
|
|
});
|
|
|
|
function setupEventListeners() {
|
|
document.getElementById('add-slot-form').addEventListener('submit', handleAddSlot);
|
|
document.getElementById('block-time-form').addEventListener('submit', handleBlockTime);
|
|
document.getElementById('filter-date-from').addEventListener('change', filterSlots);
|
|
document.getElementById('filter-date-to').addEventListener('change', filterSlots);
|
|
}
|
|
|
|
function setDefaultDate() {
|
|
const today = new Date().toISOString().split('T')[0];
|
|
document.querySelector('input[name="date"]').value = today;
|
|
document.querySelector('input[name="date"]').min = today;
|
|
document.querySelector('input[name="block_date"]').value = today;
|
|
document.querySelector('input[name="block_date"]').min = today;
|
|
}
|
|
|
|
function addQuickTime(time) {
|
|
document.querySelector('input[name="time"]').value = time;
|
|
}
|
|
|
|
async function handleBlockTime(e) {
|
|
e.preventDefault();
|
|
|
|
const form = e.target;
|
|
const blockData = {
|
|
date: form.block_date.value,
|
|
time: form.block_time.value,
|
|
reason: form.block_reason.value || 'No reason provided'
|
|
};
|
|
|
|
if (!blockData.date || !blockData.time) {
|
|
MSPE.showNotification('Please select date and time', 'error');
|
|
return;
|
|
}
|
|
|
|
const submitBtn = form.querySelector('button[type="submit"]');
|
|
const originalText = submitBtn.innerHTML;
|
|
submitBtn.disabled = true;
|
|
submitBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Blocking...';
|
|
|
|
try {
|
|
const response = await MSPE.API.post('bookings.php?action=block-time', blockData);
|
|
MSPE.showNotification('Time blocked successfully!', 'success');
|
|
form.reset();
|
|
setDefaultDate();
|
|
loadBlockedTimes();
|
|
} catch (error) {
|
|
console.error('Error blocking time:', error);
|
|
MSPE.showNotification('Failed to block time', 'error');
|
|
} finally {
|
|
submitBtn.disabled = false;
|
|
submitBtn.innerHTML = originalText;
|
|
}
|
|
}
|
|
|
|
async function loadBlockedTimes() {
|
|
try {
|
|
const response = await MSPE.API.get('bookings.php?action=blocked-times');
|
|
blockedTimes = response.data || [];
|
|
renderBlockedTimes();
|
|
} catch (error) {
|
|
console.error('Error loading blocked times:', error);
|
|
}
|
|
}
|
|
|
|
function renderBlockedTimes() {
|
|
const section = document.getElementById('blocked-times-section');
|
|
const container = document.getElementById('blocked-times-list');
|
|
|
|
const today = new Date().toISOString().split('T')[0];
|
|
const futureBlocked = blockedTimes.filter(b => b.date >= today);
|
|
|
|
if (futureBlocked.length === 0) {
|
|
section.style.display = 'none';
|
|
return;
|
|
}
|
|
|
|
section.style.display = 'block';
|
|
|
|
container.innerHTML = futureBlocked.map(blocked => {
|
|
const date = new Date(blocked.date);
|
|
const formattedDate = date.toLocaleDateString('en-US', { weekday: 'short', month: 'short', day: 'numeric' });
|
|
const formattedTime = new Date(`${blocked.date}T${blocked.time}`).toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit', hour12: true });
|
|
|
|
return `
|
|
<div style="display: flex; align-items: center; justify-content: space-between; background: white; padding: 0.75rem 1rem; border-radius: 8px; margin-bottom: 0.5rem; border: 1px solid #fecaca;">
|
|
<div>
|
|
<span style="font-weight: 600; color: #dc2626;">${formattedDate}</span>
|
|
<span style="color: #64748b; margin: 0 0.5rem;">at</span>
|
|
<span style="font-weight: 600;">${formattedTime}</span>
|
|
${blocked.reason ? `<span style="color: #64748b; font-size: 0.875rem; margin-left: 0.5rem;">(${blocked.reason})</span>` : ''}
|
|
</div>
|
|
<button onclick="unblockTime('${blocked.id}')" style="background: none; border: none; color: #dc2626; cursor: pointer; padding: 0.25rem;">
|
|
<i class="fas fa-times"></i>
|
|
</button>
|
|
</div>
|
|
`;
|
|
}).join('');
|
|
}
|
|
|
|
async function unblockTime(id) {
|
|
if (!await MSPE.confirmDialog('Remove this blocked time?')) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
await MSPE.API.delete(`bookings.php?id=${id}&type=blocked`);
|
|
MSPE.showNotification('Time unblocked successfully!', 'success');
|
|
loadBlockedTimes();
|
|
} catch (error) {
|
|
console.error('Error unblocking time:', error);
|
|
MSPE.showNotification('Failed to unblock time', 'error');
|
|
}
|
|
}
|
|
|
|
async function handleAddSlot(e) {
|
|
e.preventDefault();
|
|
|
|
const form = e.target;
|
|
const data = new FormData(form);
|
|
const slotData = Object.fromEntries(data);
|
|
|
|
if (!slotData.date || !slotData.time) {
|
|
MSPE.showNotification('Please select date and time', 'error');
|
|
return;
|
|
}
|
|
|
|
const submitBtn = form.querySelector('button[type="submit"]');
|
|
const originalText = submitBtn.innerHTML;
|
|
submitBtn.disabled = true;
|
|
submitBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Adding...';
|
|
|
|
try {
|
|
const response = await MSPE.API.post('bookings.php?action=create-slot', slotData);
|
|
MSPE.showNotification('Time slot added successfully!', 'success');
|
|
form.reset();
|
|
setDefaultDate();
|
|
loadSlots();
|
|
} catch (error) {
|
|
console.error('Error adding slot:', error);
|
|
MSPE.showNotification('Failed to add time slot', 'error');
|
|
} finally {
|
|
submitBtn.disabled = false;
|
|
submitBtn.innerHTML = originalText;
|
|
}
|
|
}
|
|
|
|
async function generateWeekSlots() {
|
|
const today = new Date();
|
|
const slotsToAdd = [];
|
|
const times = ['09:00', '10:00', '11:00', '13:00', '14:00', '15:00', '16:00', '17:00'];
|
|
|
|
for (let i = 1; i <= 7; i++) {
|
|
const date = new Date(today);
|
|
date.setDate(date.getDate() + i);
|
|
const dateStr = date.toISOString().split('T')[0];
|
|
|
|
times.forEach(time => {
|
|
slotsToAdd.push({
|
|
date: dateStr,
|
|
time: time,
|
|
capacity: 1
|
|
});
|
|
});
|
|
}
|
|
|
|
if (!await MSPE.confirmDialog(`This will add ${slotsToAdd.length} time slots for the next 7 days. Continue?`)) {
|
|
return;
|
|
}
|
|
|
|
let added = 0;
|
|
let failed = 0;
|
|
|
|
for (const slot of slotsToAdd) {
|
|
try {
|
|
await MSPE.API.post('bookings.php?action=create-slot', slot);
|
|
added++;
|
|
} catch (error) {
|
|
failed++;
|
|
}
|
|
}
|
|
|
|
if (added > 0) {
|
|
MSPE.showNotification(`${added} time slots added successfully!`, 'success');
|
|
}
|
|
|
|
if (failed > 0) {
|
|
MSPE.showNotification(`${failed} slots failed to add`, 'error');
|
|
}
|
|
|
|
loadSlots();
|
|
}
|
|
|
|
async function loadSlots() {
|
|
try {
|
|
const result = await MSPE.API.get('bookings.php');
|
|
currentSlots = result.data || [];
|
|
renderSlots();
|
|
} catch (error) {
|
|
console.error('Error loading slots:', error);
|
|
}
|
|
}
|
|
|
|
function renderSlots() {
|
|
const tbody = document.getElementById('slots-table-body');
|
|
|
|
const dateFrom = document.getElementById('filter-date-from').value;
|
|
const dateTo = document.getElementById('filter-date-to').value;
|
|
|
|
let filteredSlots = [...currentSlots];
|
|
|
|
if (dateFrom) {
|
|
filteredSlots = filteredSlots.filter(s => s.date >= dateFrom);
|
|
}
|
|
|
|
if (dateTo) {
|
|
filteredSlots = filteredSlots.filter(s => s.date <= dateTo);
|
|
}
|
|
|
|
if (filteredSlots.length === 0) {
|
|
tbody.innerHTML = `
|
|
<tr>
|
|
<td colspan="6">
|
|
<div class="empty-state">
|
|
<i class="fas fa-calendar-times"></i>
|
|
<p>No availability slots found</p>
|
|
<p style="font-size: 0.875rem; margin-top: 0.5rem;">Add time slots using the form on the left</p>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
`;
|
|
return;
|
|
}
|
|
|
|
filteredSlots.sort((a, b) => {
|
|
return new Date(`${a.date}T${a.time}`) - new Date(`${b.date}T${b.time}`);
|
|
});
|
|
|
|
tbody.innerHTML = filteredSlots.map(slot => {
|
|
const date = new Date(slot.date);
|
|
const formattedDate = date.toLocaleDateString('en-US', { weekday: 'short', month: 'short', day: 'numeric' });
|
|
const formattedTime = new Date(`${slot.date}T${slot.time}`).toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit', hour12: true });
|
|
const isAvailable = slot.is_available !== false;
|
|
|
|
return `
|
|
<tr>
|
|
<td>
|
|
<div style="font-weight: 600;">${formattedDate}</div>
|
|
</td>
|
|
<td>
|
|
<span class="slot-time">${formattedTime}</span>
|
|
</td>
|
|
<td>
|
|
<span class="slot-capacity">
|
|
<i class="fas fa-users"></i> ${slot.capacity}
|
|
</span>
|
|
</td>
|
|
<td>
|
|
<span class="status-badge ${isAvailable ? 'status-confirmed' : 'status-cancelled'}">
|
|
${isAvailable ? 'Available' : 'Disabled'}
|
|
</span>
|
|
</td>
|
|
<td>
|
|
<span style="color: var(--gray-500);">${slot.notes || '-'}</span>
|
|
</td>
|
|
<td>
|
|
<button class="action-btn toggle" onclick="toggleSlot('${slot.id}')">
|
|
<i class="fas fa-${isAvailable ? 'eye-slash' : 'eye'}"></i>
|
|
</button>
|
|
<button class="action-btn delete" onclick="deleteSlot('${slot.id}')">
|
|
<i class="fas fa-trash"></i>
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
`;
|
|
}).join('');
|
|
}
|
|
|
|
function filterSlots() {
|
|
renderSlots();
|
|
}
|
|
|
|
async function toggleSlot(id) {
|
|
const slot = currentSlots.find(s => s.id === id);
|
|
if (!slot) return;
|
|
|
|
const newStatus = slot.is_available === false ? true : false;
|
|
|
|
if (!await MSPE.confirmDialog(`Are you sure you want to ${newStatus ? 'enable' : 'disable'} this slot?`)) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
await MSPE.API.put(`bookings.php?id=${id}`, { is_available: newStatus });
|
|
MSPE.showNotification(`Slot ${newStatus ? 'enabled' : 'disabled'} successfully!`, 'success');
|
|
loadSlots();
|
|
} catch (error) {
|
|
console.error('Error toggling slot:', error);
|
|
MSPE.showNotification('Failed to update slot', 'error');
|
|
}
|
|
}
|
|
|
|
async function deleteSlot(id) {
|
|
if (!await MSPE.confirmDialog('Are you sure you want to delete this availability slot?')) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
await MSPE.API.delete(`bookings.php?id=${id}`);
|
|
MSPE.showNotification('Slot deleted successfully!', 'success');
|
|
loadSlots();
|
|
} catch (error) {
|
|
console.error('Error deleting slot:', error);
|
|
MSPE.showNotification('Failed to delete slot', 'error');
|
|
}
|
|
}
|
|
|
|
async function loadUpcomingBookings() {
|
|
try {
|
|
const response = await MSPE.API.get('bookings.php?action=upcoming');
|
|
const bookings = response.data || [];
|
|
|
|
if (bookings.length === 0) {
|
|
document.getElementById('upcoming-section').style.display = 'none';
|
|
return;
|
|
}
|
|
|
|
document.getElementById('upcoming-section').style.display = 'block';
|
|
|
|
const container = document.getElementById('upcoming-bookings');
|
|
container.innerHTML = bookings.slice(0, 5).map(booking => {
|
|
const date = new Date(booking.booking_date);
|
|
const day = date.getDate();
|
|
const month = date.toLocaleDateString('en-US', { month: 'short' });
|
|
const formattedTime = new Date(`${booking.booking_date}T${booking.booking_time}`).toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit', hour12: true });
|
|
|
|
return `
|
|
<div class="upcoming-item">
|
|
<div class="upcoming-item-date">
|
|
<div class="day">${day}</div>
|
|
<div class="month">${month}</div>
|
|
</div>
|
|
<div style="flex: 1;">
|
|
<div style="font-weight: 600;">${booking.first_name} ${booking.last_name}</div>
|
|
<div style="font-size: 0.875rem; opacity: 0.8;">${formattedTime}</div>
|
|
</div>
|
|
<i class="fas fa-chevron-right" style="color: var(--gray-400);"></i>
|
|
</div>
|
|
`;
|
|
}).join('');
|
|
} catch (error) {
|
|
console.error('Error loading upcoming bookings:', error);
|
|
}
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>
|