landonalas/Vida-Real-Booking-System1
0
1// ============================================================2// APP STATE (localStorage-based, no Supabase needed)3// ============================================================4let currentUser = null;5let currentProfile = null;6let currentView = 'dashboard';7let bookingsCache = [];8let selectedDate = null;9let calendarDate = new Date();10 11// Production Registry state12let productionSession = null;13let productionItems = [];14let productionUsers = [];15 16// Initialize localStorage defaults17function initStorage() {18 if (!localStorage.getItem('vr_users')) {19 localStorage.setItem('vr_users', JSON.stringify([20 { id: 'admin-1', email: 'admin@demo.com', password: 'demo123', full_name: 'Admin User', role: 'admin' },21 { id: 'leader-1', email: 'leader@demo.com', password: 'demo123', full_name: 'Leader User', role: 'leader' }22 ]));23 }24 if (!localStorage.getItem('vr_bookings')) {25 localStorage.setItem('vr_bookings', JSON.stringify([26 {27 id: 'b1', user_id: 'leader-1', room: 'Sanctuary 4F',28 event_name: 'Sunday Worship', event_description: 'Weekly Sunday service',29 event_date: getTodayStr(), start_time: '09:00', end_time: '11:00',30 tech_needs: 'All', status: 'approved', admin_notes: '', created_at: new Date().toISOString(),31 profiles: { full_name: 'Leader User' }32 },33 {34 id: 'b2', user_id: 'leader-1', room: 'Conference Room 3F',35 event_name: 'Team Meeting', event_description: 'Weekly team sync',36 event_date: getTomorrowStr(), start_time: '14:00', end_time: '15:30',37 tech_needs: 'Multimedia', status: 'pending', admin_notes: '', created_at: new Date().toISOString(),38 profiles: { full_name: 'Leader User' }39 },40 {41 id: 'b3', user_id: 'admin-1', room: 'Mirrors Room 2F',42 event_name: 'Dance Practice', event_description: 'Choreography rehearsal',43 event_date: getTomorrowStr(), start_time: '18:00', end_time: '20:00',44 tech_needs: 'Sound', status: 'pending', admin_notes: '', created_at: new Date().toISOString(),45 profiles: { full_name: 'Admin User' }46 }47 ]));48 }49 if (!localStorage.getItem('vr_production_users')) {50 localStorage.setItem('vr_production_users', JSON.stringify([51 { id: 'pu1', username: 'production', password: 'prod123', full_name: 'Production Team' }52 ]));53 }54 if (!localStorage.getItem('vr_production_items')) {55 localStorage.setItem('vr_production_items', JSON.stringify([]));56 }57}58 59function getTodayStr() {60 const d = new Date();61 return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;62}63 64function getTomorrowStr() {65 const d = new Date();66 d.setDate(d.getDate() + 1);67 return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;68}69 70function genId() {71 return 'id-' + Date.now() + '-' + Math.random().toString(36).substr(2, 9);72}73 74// Room definitions75const ROOMS = [76 { id: 'Orange Room 2F', label: 'Orange Room', floor: '2F', color: 'orange', icon: 'coffee', gradient: 'from-orange-400 to-orange-600' },77 { id: 'Mirrors Room 2F', label: 'Mirrors Room', floor: '2F', color: 'purple', icon: 'sparkles', gradient: 'from-purple-400 to-purple-600' },78 { id: 'Conference Room 3F', label: 'Conference Room', floor: '3F', color: 'blue', icon: 'presentation', gradient: 'from-blue-400 to-blue-600' },79 { id: 'Servers Room 3F', label: 'Servers Room', floor: '3F', color: 'teal', icon: 'server', gradient: 'from-teal-400 to-teal-600' },80 { id: 'Sanctuary 4F', label: 'Sanctuary', floor: '4F', color: 'amber', icon: 'music', gradient: 'from-amber-400 to-amber-600' },81];82 83const TECH_OPTIONS = ['Multimedia', 'Sound', 'Lighting', 'All'];84 85const STATUS_CONFIG = {86 pending: { label: 'Pending', class: 'badge-pending', icon: 'clock', color: 'amber' },87 approved: { label: 'Approved', class: 'badge-approved', icon: 'check-circle', color: 'emerald' },88 rejected: { label: 'Rejected', class: 'badge-rejected', icon: 'x-circle', color: 'red' },89};90 91// ============================================================92// AUTH FUNCTIONS (localStorage)93// ============================================================94function signIn(email, password) {95 const users = JSON.parse(localStorage.getItem('vr_users') || '[]');96 const user = users.find(u => u.email === email && u.password === password);97 if (!user) return { error: { message: 'Invalid email or password' } };98 currentUser = user;99 currentProfile = { id: user.id, full_name: user.full_name, role: user.role };100 localStorage.setItem('vr_session', JSON.stringify({ userId: user.id }));101 return { data: { user } };102}103 104function signUp(email, password, fullName) {105 const users = JSON.parse(localStorage.getItem('vr_users') || '[]');106 if (users.find(u => u.email === email)) return { error: { message: 'Email already registered' } };107 const newUser = { id: genId(), email, password, full_name: fullName, role: 'leader' };108 users.push(newUser);109 localStorage.setItem('vr_users', JSON.stringify(users));110 currentUser = newUser;111 currentProfile = { id: newUser.id, full_name: newUser.full_name, role: newUser.role };112 localStorage.setItem('vr_session', JSON.stringify({ userId: newUser.id }));113 return { data: { user: newUser } };114}115 116function signOut() {117 currentUser = null;118 currentProfile = null;119 bookingsCache = [];120 productionSession = null;121 productionItems = [];122 localStorage.removeItem('vr_session');123 renderApp();124}125 126function checkAuth() {127 const session = JSON.parse(localStorage.getItem('vr_session') || 'null');128 if (session && session.userId) {129 const users = JSON.parse(localStorage.getItem('vr_users') || '[]');130 const user = users.find(u => u.id === session.userId);131 if (user) {132 currentUser = user;133 currentProfile = { id: user.id, full_name: user.full_name, role: user.role };134 fetchBookings();135 }136 }137 renderApp();138}139 140// ============================================================141// BOOKING FUNCTIONS (localStorage)142// ============================================================143function fetchBookings() {144 const all = JSON.parse(localStorage.getItem('vr_bookings') || '[]');145 const users = JSON.parse(localStorage.getItem('vr_users') || '[]');146 bookingsCache = all.map(b => {147 const user = users.find(u => u.id === b.user_id);148 return { ...b, profiles: { full_name: user?.full_name || 'Unknown' } };149 });150 return bookingsCache;151}152 153function createBooking(bookingData) {154 const bookings = JSON.parse(localStorage.getItem('vr_bookings') || '[]');155 const newBooking = {156 id: genId(),157 ...bookingData,158 user_id: currentUser.id,159 status: 'pending',160 admin_notes: '',161 created_at: new Date().toISOString()162 };163 bookings.push(newBooking);164 localStorage.setItem('vr_bookings', JSON.stringify(bookings));165 bookingsCache = fetchBookings();166 return { data: newBooking };167}168 169function updateBookingStatus(id, status, adminNotes) {170 const bookings = JSON.parse(localStorage.getItem('vr_bookings') || '[]');171 const idx = bookings.findIndex(b => b.id === id);172 if (idx === -1) return { error: { message: 'Booking not found' } };173 bookings[idx].status = status;174 if (adminNotes !== undefined) bookings[idx].admin_notes = adminNotes;175 localStorage.setItem('vr_bookings', JSON.stringify(bookings));176 bookingsCache = fetchBookings();177 return { data: bookings[idx] };178}179 180// ============================================================181// PRODUCTION FUNCTIONS (localStorage)182// ============================================================183function productionLogin(username, password) {184 const users = JSON.parse(localStorage.getItem('vr_production_users') || '[]');185 const user = users.find(u => u.username === username && u.password === password);186 if (!user) return { error: { message: 'Invalid credentials' } };187 productionSession = user;188 localStorage.setItem('vr_prod_session', JSON.stringify({ userId: user.id }));189 return { data: user };190}191 192function productionLogout() {193 productionSession = null;194 localStorage.removeItem('vr_prod_session');195 renderCurrentView();196}197 198function fetchProductionItems() {199 productionItems = JSON.parse(localStorage.getItem('vr_production_items') || '[]');200 return productionItems;201}202 203function createProductionItem(itemData) {204 const items = JSON.parse(localStorage.getItem('vr_production_items') || '[]');205 const newItem = {206 id: genId(),207 ...itemData,208 status: 'unreturned',209 return_photo: null,210 returned_at: null,211 created_at: new Date().toISOString()212 };213 items.push(newItem);214 localStorage.setItem('vr_production_items', JSON.stringify(items));215 productionItems = items;216 return { data: newItem };217}218 219function returnProductionItem(id, photoUrl) {220 const items = JSON.parse(localStorage.getItem('vr_production_items') || '[]');221 const idx = items.findIndex(i => i.id === id);222 if (idx === -1) return { error: { message: 'Item not found' } };223 items[idx].status = 'returned';224 items[idx].return_photo = photoUrl || null;225 items[idx].returned_at = new Date().toISOString();226 localStorage.setItem('vr_production_items', JSON.stringify(items));227 productionItems = items;228 return { data: items[idx] };229}230 231function fetchProductionUsers() {232 productionUsers = JSON.parse(localStorage.getItem('vr_production_users') || '[]');233 return productionUsers;234}235 236function createProductionUser(username, password, fullName) {237 const users = JSON.parse(localStorage.getItem('vr_production_users') || '[]');238 if (users.find(u => u.username === username)) return { error: { message: 'Username already exists' } };239 const newUser = { id: genId(), username, password, full_name: fullName };240 users.push(newUser);241 localStorage.setItem('vr_production_users', JSON.stringify(users));242 productionUsers = users;243 return { data: newUser };244}245 246function deleteProductionUser(id) {247 let users = JSON.parse(localStorage.getItem('vr_production_users') || '[]');248 users = users.filter(u => u.id !== id);249 localStorage.setItem('vr_production_users', JSON.stringify(users));250 productionUsers = users;251}252 253// ============================================================254// TOAST NOTIFICATIONS255// ============================================================256function showToast(message, type = 'info') {257 const container = document.getElementById('toast-container');258 const colors = { success: 'bg-emerald-600', error: 'bg-red-600', info: 'bg-blue-600', warning: 'bg-amber-600' };259 const icons = { success: 'check-circle', error: 'alert-circle', info: 'info', warning: 'alert-triangle' };260 const toast = document.createElement('div');261 toast.className = `toast ${colors[type]} text-white px-4 py-3 rounded-xl shadow-lg flex items-center gap-3 min-w-[280px] max-w-sm`;262 toast.innerHTML = `263 <i data-lucide="${icons[type]}" class="w-5 h-5 flex-shrink-0"></i>264 <span class="text-sm font-medium">${message}</span>265 <button onclick="this.parentElement.remove()" class="ml-auto opacity-70 hover:opacity-100">266 <i data-lucide="x" class="w-4 h-4"></i>267 </button>268 `;269 container.appendChild(toast);270 lucide.createIcons({ nodes: [toast] });271 setTimeout(() => { toast.classList.add('toast-exit'); setTimeout(() => toast.remove(), 300); }, 4000);272}273 274// ============================================================275// MODAL276// ============================================================277function showModal(content) {278 const overlay = document.getElementById('modal-overlay');279 overlay.innerHTML = `<div class="animate-scale-in bg-white rounded-2xl shadow-2xl max-w-lg w-full max-h-[90vh] overflow-y-auto">${content}</div>`;280 overlay.classList.remove('hidden');281 overlay.onclick = (e) => { if (e.target === overlay) closeModal(); };282 lucide.createIcons({ nodes: [overlay] });283}284 285function closeModal() {286 document.getElementById('modal-overlay').classList.add('hidden');287 document.getElementById('modal-overlay').innerHTML = '';288}289 290// ============================================================291// RENDER APP292// ============================================================293function renderApp() {294 const app = document.getElementById('app');295 if (!currentUser) {296 app.innerHTML = renderLoginScreen();297 lucide.createIcons();298 return;299 }300 app.innerHTML = renderMainApp();301 lucide.createIcons();302 renderCurrentView();303}304 305// ============================================================306// LOGIN SCREEN307// ============================================================308function renderLoginScreen() {309 return `310 <div class="min-h-screen flex">311 <div class="hidden lg:flex lg:w-1/2 login-bg items-center justify-center p-12 relative">312 <div class="relative z-10 text-white text-center">313 <img src="https://huggingface.co/spaces/landonalas/deepsite-project-gvccn/resolve/main/images/VR logo.png" alt="Vida Real" class="h-20 w-auto mx-auto mb-4">314 <p class="text-xl text-white/80 font-light tracking-wide">Booking System</p>315 <div class="mt-12 grid grid-cols-3 gap-4 max-w-sm mx-auto">316 <div class="bg-white/10 backdrop-blur-sm rounded-xl p-4 border border-white/10">317 <i data-lucide="calendar-check" class="w-6 h-6 mx-auto mb-2 text-white/80"></i>318 <p class="text-xs text-white/70">Book Spaces</p>319 </div>320 <div class="bg-white/10 backdrop-blur-sm rounded-xl p-4 border border-white/10">321 <i data-lucide="check-circle" class="w-6 h-6 mx-auto mb-2 text-white/80"></i>322 <p class="text-xs text-white/70">Get Approved</p>323 </div>324 <div class="bg-white/10 backdrop-blur-sm rounded-xl p-4 border border-white/10">325 <i data-lucide="music" class="w-6 h-6 mx-auto mb-2 text-white/80"></i>326 <p class="text-xs text-white/70">Tech Support</p>327 </div>328 </div>329 </div>330 </div>331 332 <div class="w-full lg:w-1/2 flex items-center justify-center p-6 bg-white">333 <div class="max-w-md w-full animate-slide-up">334 <div class="lg:hidden text-center mb-8">335 <img src="https://huggingface.co/spaces/landonalas/deepsite-project-gvccn/resolve/main/images/VR logo.png" alt="Vida Real" class="h-14 w-auto mx-auto mb-2">336 <p class="text-slate-500">Booking System</p>337 </div>338 339 <div id="auth-form-container">340 <div id="login-form">341 <h2 class="text-2xl font-bold text-slate-800 mb-1">Welcome back</h2>342 <p class="text-slate-500 mb-8">Sign in to manage your bookings</p>343 <div class="space-y-4">344 <div>345 <label class="text-sm font-medium text-slate-700 mb-1 block">Email</label>346 <div class="relative">347 <i data-lucide="mail" class="w-5 h-5 text-slate-400 absolute left-3 top-1/2 -translate-y-1/2"></i>348 <input id="login-email" type="email" placeholder="admin@demo.com"349 class="w-full pl-10 pr-4 py-3 border border-slate-200 rounded-xl text-slate-800 placeholder-slate-400">350 </div>351 </div>352 <div>353 <label class="text-sm font-medium text-slate-700 mb-1 block">Password</label>354 <div class="relative">355 <i data-lucide="lock" class="w-5 h-5 text-slate-400 absolute left-3 top-1/2 -translate-y-1/2"></i>356 <input id="login-password" type="password" placeholder="โขโขโขโขโขโขโขโข"357 class="w-full pl-10 pr-4 py-3 border border-slate-200 rounded-xl text-slate-800 placeholder-slate-400"358 onkeydown="if(event.key==='Enter')handleLogin()">359 </div>360 </div>361 <button onclick="handleLogin()" id="login-btn"362 class="w-full py-3 bg-brand-500 hover:bg-brand-600 text-white font-semibold rounded-xl transition-colors flex items-center justify-center gap-2">363 <span>Sign In</span>364 </button>365 </div>366 <p class="text-center text-sm text-slate-500 mt-6">367 Don't have an account?368 <button onclick="showSignUp()" class="text-brand-600 font-semibold hover:underline">Create one</button>369 </p>370 371 </div>372 373 <div id="signup-form" class="hidden">374 <h2 class="text-2xl font-bold text-slate-800 mb-1">Create account</h2>375 <p class="text-slate-500 mb-8">Join the Vida Real booking system</p>376 <div class="space-y-4">377 <div>378 <label class="text-sm font-medium text-slate-700 mb-1 block">Full Name</label>379 <div class="relative">380 <i data-lucide="user" class="w-5 h-5 text-slate-400 absolute left-3 top-1/2 -translate-y-1/2"></i>381 <input id="signup-name" type="text" placeholder="Your full name"382 class="w-full pl-10 pr-4 py-3 border border-slate-200 rounded-xl text-slate-800 placeholder-slate-400">383 </div>384 </div>385 <div>386 <label class="text-sm font-medium text-slate-700 mb-1 block">Email</label>387 <div class="relative">388 <i data-lucide="mail" class="w-5 h-5 text-slate-400 absolute left-3 top-1/2 -translate-y-1/2"></i>389 <input id="signup-email" type="email" placeholder="you@demo.com"390 class="w-full pl-10 pr-4 py-3 border border-slate-200 rounded-xl text-slate-800 placeholder-slate-400">391 </div>392 </div>393 <div>394 <label class="text-sm font-medium text-slate-700 mb-1 block">Password</label>395 <div class="relative">396 <i data-lucide="lock" class="w-5 h-5 text-slate-400 absolute left-3 top-1/2 -translate-y-1/2"></i>397 <input id="signup-password" type="password" placeholder="Min 6 characters"398 class="w-full pl-10 pr-4 py-3 border border-slate-200 rounded-xl text-slate-800 placeholder-slate-400">399 </div>400 </div>401 <button onclick="handleSignUp()" id="signup-btn"402 class="w-full py-3 bg-brand-500 hover:bg-brand-600 text-white font-semibold rounded-xl transition-colors flex items-center justify-center gap-2">403 <span>Create Account</span>404 </button>405 </div>406 <p class="text-center text-sm text-slate-500 mt-6">407 Already have an account?408 <button onclick="showLogin()" class="text-brand-600 font-semibold hover:underline">Sign in</button>409 </p>410 </div>411 </div>412 </div>413 </div>414 </div>415 `;416}417 418function showLogin() {419 document.getElementById('login-form').classList.remove('hidden');420 document.getElementById('signup-form').classList.add('hidden');421}422 423function showSignUp() {424 document.getElementById('login-form').classList.add('hidden');425 document.getElementById('signup-form').classList.remove('hidden');426}427 428async function handleLogin() {429 const email = document.getElementById('login-email').value.trim();430 const password = document.getElementById('login-password').value;431 if (!email || !password) { showToast('Please fill in all fields', 'warning'); return; }432 433 const btn = document.getElementById('login-btn');434 btn.innerHTML = '<span class="w-5 h-5 border-2 border-white border-t-transparent rounded-full animate-spin"></span><span>Signing in...</span>';435 btn.disabled = true;436 437 const result = signIn(email, password);438 if (result.error) {439 showToast(result.error.message, 'error');440 btn.innerHTML = '<span>Sign In</span>';441 btn.disabled = false;442 } else {443 showToast('Welcome back!', 'success');444 fetchBookings();445 renderApp();446 }447}448 449async function handleSignUp() {450 const name = document.getElementById('signup-name').value.trim();451 const email = document.getElementById('signup-email').value.trim();452 const password = document.getElementById('signup-password').value;453 if (!name || !email || !password) { showToast('Please fill in all fields', 'warning'); return; }454 if (password.length < 6) { showToast('Password must be at least 6 characters', 'warning'); return; }455 456 const btn = document.getElementById('signup-btn');457 btn.innerHTML = '<span class="w-5 h-5 border-2 border-white border-t-transparent rounded-full animate-spin"></span><span>Creating account...</span>';458 btn.disabled = true;459 460 const result = signUp(email, password, name);461 if (result.error) {462 showToast(result.error.message, 'error');463 btn.innerHTML = '<span>Create Account</span>';464 btn.disabled = false;465 } else {466 showToast('Account created! Welcome!', 'success');467 fetchBookings();468 renderApp();469 }470}471 472// ============================================================473// MAIN APP LAYOUT474// ============================================================475function renderMainApp() {476 const isAdmin = currentProfile?.role === 'admin';477 const navItems = [478 { id: 'dashboard', label: 'Dashboard', icon: 'layout-dashboard' },479 { id: 'new-booking', label: 'New Booking', icon: 'plus-circle' },480 { id: 'my-bookings', label: 'My Bookings', icon: 'clipboard-list' },481 { id: 'calendar', label: 'Calendar', icon: 'calendar-days' },482 { id: 'production-registry', label: 'Production', icon: 'package' },483 ...(isAdmin ? [{ id: 'approvals', label: 'Approvals', icon: 'check-circle' }] : []),484 ...(isAdmin ? [{ id: 'all-bookings', label: 'All Bookings', icon: 'book-open' }] : []),485 ...(isAdmin ? [{ id: 'prod-users', label: 'Prod. Users', icon: 'users' }] : []),486 ];487 488 const pendingCount = bookingsCache.filter(b => b.status === 'pending').length;489 490 return `491 <aside class="sidebar fixed left-0 top-0 bottom-0 w-64 bg-white border-r border-slate-200 z-40 flex flex-col">492 <div class="p-6 border-b border-slate-100">493 <div class="flex items-center gap-3">494 <img src="https://huggingface.co/spaces/landonalas/deepsite-project-gvccn/resolve/main/images/VR logo.png" alt="Vida Real" class="h-10 w-auto">495 </div>496 </div>497 498 <nav class="flex-1 py-4 px-3 space-y-1 overflow-y-auto">499 ${navItems.map(item => `500 <button onclick="navigateTo('${item.id}')" class="sidebar-link ${currentView === item.id ? 'active' : ''} w-full flex items-center gap-3 px-4 py-2.5 rounded-lg text-sm font-medium text-slate-600 relative">501 <i data-lucide="${item.icon}" class="w-5 h-5"></i>502 <span>${item.label}</span>503 ${item.id === 'approvals' && pendingCount > 0 ? `<span class="absolute right-3 bg-red-500 text-white text-xs font-bold rounded-full w-5 h-5 flex items-center justify-center">${pendingCount}</span>` : ''}504 </button>505 `).join('')}506 </nav>507 508 <div class="p-4 border-t border-slate-100">509 <div class="flex items-center gap-3">510 <div class="w-9 h-9 bg-brand-100 rounded-full flex items-center justify-center">511 <span class="text-sm font-bold text-brand-700">${(currentProfile?.full_name || 'U')[0].toUpperCase()}</span>512 </div>513 <div class="flex-1 min-w-0">514 <p class="text-sm font-semibold text-slate-800 truncate">${currentProfile?.full_name || 'User'}</p>515 <p class="text-xs text-slate-400">${isAdmin ? 'โญ Admin' : '๐ค Leader'}</p>516 </div>517 <button onclick="signOut()" class="p-2 hover:bg-slate-100 rounded-lg transition-colors" title="Sign Out">518 <i data-lucide="log-out" class="w-4 h-4 text-slate-400"></i>519 </button>520 </div>521 </div>522 </aside>523 524 <main class="main-content ml-64 min-h-screen bg-slate-50">525 <header class="sticky top-0 bg-white/80 backdrop-blur-lg border-b border-slate-100 z-30 px-6 py-4 flex items-center justify-between lg:hidden">526 <div class="flex items-center gap-2">527 <img src="https://huggingface.co/spaces/landonalas/deepsite-project-gvccn/resolve/main/images/VR logo.png" alt="Vida Real" class="h-8 w-auto">528 </div>529 <button onclick="signOut()" class="p-2 hover:bg-slate-100 rounded-lg">530 <i data-lucide="log-out" class="w-5 h-5 text-slate-500"></i>531 </button>532 </header>533 534 <div id="view-container" class="p-4 md:p-6 lg:p-8 max-w-6xl"></div>535 </main>536 537 <nav class="mobile-nav fixed bottom-0 left-0 right-0 bg-white border-t border-slate-200 z-40 px-1 py-1 flex overflow-x-auto gap-0.5">538 ${navItems.map(item => `539 <button onclick="navigateTo('${item.id}')" class="flex flex-col items-center gap-0.5 px-2.5 py-2 rounded-lg whitespace-nowrap ${currentView === item.id ? 'text-brand-600' : 'text-slate-400'} relative flex-shrink-0">540 <i data-lucide="${item.icon}" class="w-5 h-5"></i>541 <span class="text-[10px] font-medium">${item.label}</span>542 ${item.id === 'approvals' && pendingCount > 0 ? `<span class="absolute -top-0.5 right-0 bg-red-500 text-white text-[9px] font-bold rounded-full w-4 h-4 flex items-center justify-center">${pendingCount}</span>` : ''}543 </button>544 `).join('')}545 </nav>546 `;547}548 549// ============================================================550// NAVIGATION551// ============================================================552async function navigateTo(view) {553 currentView = view;554 document.querySelectorAll('.sidebar-link').forEach(btn => {555 const targetView = btn.getAttribute('onclick')?.match(/'(.+?)'/)?.[1];556 if (targetView === view) btn.classList.add('active');557 else btn.classList.remove('active');558 });559 if (view === 'production-registry') fetchProductionItems();560 if (view === 'prod-users') fetchProductionUsers();561 renderCurrentView();562}563 564function renderCurrentView() {565 const container = document.getElementById('view-container');566 if (!container) return;567 568 switch (currentView) {569 case 'dashboard': container.innerHTML = renderDashboard(); break;570 case 'new-booking': container.innerHTML = renderNewBooking(); break;571 case 'my-bookings': container.innerHTML = renderMyBookings(); break;572 case 'calendar': container.innerHTML = renderCalendar(); break;573 case 'approvals': container.innerHTML = renderApprovals(); break;574 case 'all-bookings': container.innerHTML = renderAllBookings(); break;575 case 'production-registry': container.innerHTML = renderProductionRegistry(); break;576 case 'prod-users': container.innerHTML = renderProdUsers(); break;577 default: container.innerHTML = renderDashboard();578 }579 lucide.createIcons({ nodes: [container] });580}581 582// ============================================================583// DASHBOARD VIEW584// ============================================================585function renderDashboard() {586 const isAdmin = currentProfile?.role === 'admin';587 const myBookings = bookingsCache.filter(b => b.user_id === currentUser.id);588 const pendingCount = myBookings.filter(b => b.status === 'pending').length;589 const approvedCount = myBookings.filter(b => b.status === 'approved').length;590 const allPending = bookingsCache.filter(b => b.status === 'pending').length;591 592 return `593 <div class="view-enter stagger-children">594 <div class="mb-8">595 <h2 class="text-2xl font-bold text-slate-800">Welcome, ${currentProfile?.full_name || 'User'} ๐</h2>596 <p class="text-slate-500 mt-1">${isAdmin ? 'Manage bookings and approvals from your admin dashboard.' : 'View your bookings and request new spaces.'}</p>597 </div>598 599 <div class="grid grid-cols-2 lg:grid-cols-4 gap-4 mb-8">600 <div class="bg-white rounded-2xl p-5 border border-slate-100 shadow-sm">601 <div class="flex items-center gap-3 mb-3">602 <div class="w-10 h-10 bg-blue-50 rounded-xl flex items-center justify-center">603 <i data-lucide="clipboard-list" class="w-5 h-5 text-blue-500"></i>604 </div>605 </div>606 <p class="text-2xl font-bold text-slate-800">${myBookings.length}</p>607 <p class="text-sm text-slate-500">My Bookings</p>608 </div>609 <div class="bg-white rounded-2xl p-5 border border-slate-100 shadow-sm">610 <div class="flex items-center gap-3 mb-3">611 <div class="w-10 h-10 bg-amber-50 rounded-xl flex items-center justify-center">612 <i data-lucide="clock" class="w-5 h-5 text-amber-500"></i>613 </div>614 </div>615 <p class="text-2xl font-bold text-slate-800">${pendingCount}</p>616 <p class="text-sm text-slate-500">Pending</p>617 </div>618 <div class="bg-white rounded-2xl p-5 border border-slate-100 shadow-sm">619 <div class="flex items-center gap-3 mb-3">620 <div class="w-10 h-10 bg-emerald-50 rounded-xl flex items-center justify-center">621 <i data-lucide="check-circle" class="w-5 h-5 text-emerald-500"></i>622 </div>623 </div>624 <p class="text-2xl font-bold text-slate-800">${approvedCount}</p>625 <p class="text-sm text-slate-500">Approved</p>626 </div>627 ${isAdmin ? `628 <div class="bg-white rounded-2xl p-5 border border-slate-100 shadow-sm">629 <div class="flex items-center gap-3 mb-3">630 <div class="w-10 h-10 bg-red-50 rounded-xl flex items-center justify-center">631 <i data-lucide="alert-circle" class="w-5 h-5 text-red-500"></i>632 </div>633 </div>634 <p class="text-2xl font-bold text-slate-800">${allPending}</p>635 <p class="text-sm text-slate-500">Awaiting Review</p>636 </div>637 ` : `638 <div class="bg-white rounded-2xl p-5 border border-slate-100 shadow-sm">639 <div class="flex items-center gap-3 mb-3">640 <div class="w-10 h-10 bg-brand-50 rounded-xl flex items-center justify-center">641 <i data-lucide="building-2" class="w-5 h-5 text-brand-500"></i>642 </div>643 </div>644 <p class="text-2xl font-bold text-slate-800">5</p>645 <p class="text-sm text-slate-500">Available Rooms</p>646 </div>647 `}648 </div>649 650 <div class="mb-8">651 <h3 class="text-lg font-semibold text-slate-800 mb-4">Quick Actions</h3>652 <div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">653 <button onclick="navigateTo('new-booking')" class="group bg-gradient-to-br from-brand-500 to-brand-600 text-white rounded-2xl p-6 text-left transition-all hover:shadow-lg hover:shadow-brand-500/25 hover:-translate-y-1">654 <div class="w-10 h-10 bg-white/20 rounded-xl flex items-center justify-center mb-3 group-hover:scale-110 transition-transform">655 <i data-lucide="plus-circle" class="w-5 h-5 text-white"></i>656 </div>657 <h4 class="font-semibold text-lg">New Booking</h4>658 <p class="text-white/70 text-sm mt-1">Request a space for your event</p>659 </button>660 <button onclick="navigateTo('calendar')" class="group bg-white border border-slate-200 rounded-2xl p-6 text-left transition-all hover:shadow-lg hover:-translate-y-1">661 <div class="w-10 h-10 bg-brand-50 rounded-xl flex items-center justify-center mb-3 group-hover:scale-110 transition-transform">662 <i data-lucide="calendar-days" class="w-5 h-5 text-brand-500"></i>663 </div>664 <h4 class="font-semibold text-lg text-slate-800">View Calendar</h4>665 <p class="text-slate-500 text-sm mt-1">See upcoming approved events</p>666 </button>667 <button onclick="navigateTo('my-bookings')" class="group bg-white border border-slate-200 rounded-2xl p-6 text-left transition-all hover:shadow-lg hover:-translate-y-1">668 <div class="w-10 h-10 bg-blue-50 rounded-xl flex items-center justify-center mb-3 group-hover:scale-110 transition-transform">669 <i data-lucide="clipboard-list" class="w-5 h-5 text-blue-500"></i>670 </div>671 <h4 class="font-semibold text-lg text-slate-800">My Bookings</h4>672 <p class="text-slate-500 text-sm mt-1">Track your booking requests</p>673 </button>674 </div>675 </div>676 677 <div>678 <h3 class="text-lg font-semibold text-slate-800 mb-4">Our Spaces</h3>679 <div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">680 ${ROOMS.map(room => {681 const roomBookings = bookingsCache.filter(b => b.room === room.id && b.status === 'approved');682 return `683 <div class="bg-white rounded-2xl overflow-hidden border border-slate-100 shadow-sm hover:shadow-md transition-shadow">684 <div class="h-24 bg-gradient-to-br ${room.gradient} flex items-center justify-center">685 <i data-lucide="${room.icon}" class="w-10 h-10 text-white/90"></i>686 </div>687 <div class="p-4">688 <div class="flex items-center justify-between">689 <h4 class="font-semibold text-slate-800">${room.label}</h4>690 <span class="text-xs font-medium text-slate-400 bg-slate-100 px-2 py-0.5 rounded-full">${room.floor}</span>691 </div>692 <p class="text-sm text-slate-500 mt-1">${roomBookings.length} upcoming event${roomBookings.length !== 1 ? 's' : ''}</p>693 </div>694 </div>695 `;696 }).join('')}697 </div>698 </div>699 </div>700 `;701}702 703// ============================================================704// NEW BOOKING VIEW705// ============================================================706function renderNewBooking() {707 return `708 <div class="view-enter">709 <div class="mb-6">710 <h2 class="text-2xl font-bold text-slate-800">New Booking Request</h2>711 <p class="text-slate-500 mt-1">Fill in the details to request a space for your event.</p>712 </div>713 714 <div class="max-w-2xl">715 <div class="mb-8">716 <label class="text-sm font-semibold text-slate-700 mb-3 block">Select a Room</label>717 <div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3" id="room-selector">718 ${ROOMS.map(room => `719 <button onclick="selectRoom('${room.id}')" data-room="${room.id}"720 class="room-card bg-white border-2 border-slate-200 rounded-xl p-4 text-left transition-all hover:border-brand-300">721 <div class="w-10 h-10 bg-gradient-to-br ${room.gradient} rounded-lg flex items-center justify-center mb-3">722 <i data-lucide="${room.icon}" class="w-5 h-5 text-white"></i>723 </div>724 <h4 class="font-semibold text-slate-800 text-sm">${room.label}</h4>725 <p class="text-xs text-slate-400 mt-0.5">${room.floor}</p>726 </button>727 `).join('')}728 </div>729 <input type="hidden" id="booking-room">730 </div>731 732 <div class="mb-8">733 <label class="text-sm font-semibold text-slate-700 mb-3 block">Technical Requirements</label>734 <div class="flex flex-wrap gap-3" id="tech-selector">735 ${TECH_OPTIONS.map(tech => {736 const icons = { Multimedia: 'monitor-play', Sound: 'volume-2', Lighting: 'sun', All: 'layers' };737 return `738 <button onclick="selectTech('${tech}')" data-tech="${tech}"739 class="tech-chip flex items-center gap-2 px-4 py-2.5 bg-white border-2 border-slate-200 rounded-xl text-sm font-medium text-slate-600 transition-all hover:border-brand-300">740 <i data-lucide="${icons[tech]}" class="w-4 h-4"></i>741 <span>${tech}</span>742 </button>743 `;744 }).join('')}745 </div>746 <input type="hidden" id="booking-tech">747 </div>748 749 <div class="bg-white rounded-2xl border border-slate-100 shadow-sm p-6 mb-6 space-y-5">750 <h3 class="font-semibold text-slate-800 flex items-center gap-2">751 <i data-lucide="file-text" class="w-5 h-5 text-brand-500"></i>752 Event Details753 </h3>754 <div>755 <label class="text-sm font-medium text-slate-700 mb-1 block">Event Name *</label>756 <input id="booking-event-name" type="text" placeholder="e.g., Youth Group Meeting"757 class="w-full px-4 py-3 border border-slate-200 rounded-xl text-slate-800 placeholder-slate-400">758 </div>759 <div>760 <label class="text-sm font-medium text-slate-700 mb-1 block">Description</label>761 <textarea id="booking-description" rows="3" placeholder="Brief description of your event..."762 class="w-full px-4 py-3 border border-slate-200 rounded-xl text-slate-800 placeholder-slate-400 resize-none"></textarea>763 </div>764 <div class="grid grid-cols-1 sm:grid-cols-3 gap-4">765 <div>766 <label class="text-sm font-medium text-slate-700 mb-1 block">Date *</label>767 <input id="booking-date" type="date"768 class="w-full px-4 py-3 border border-slate-200 rounded-xl text-slate-800">769 </div>770 <div>771 <label class="text-sm font-medium text-slate-700 mb-1 block">Start Time *</label>772 <input id="booking-start" type="time"773 class="w-full px-4 py-3 border border-slate-200 rounded-xl text-slate-800">774 </div>775 <div>776 <label class="text-sm font-medium text-slate-700 mb-1 block">End Time *</label>777 <input id="booking-end" type="time"778 class="w-full px-4 py-3 border border-slate-200 rounded-xl text-slate-800">779 </div>780 </div>781 </div>782 783 <button onclick="handleSubmitBooking()" id="submit-booking-btn"784 class="w-full py-3.5 bg-brand-500 hover:bg-brand-600 text-white font-semibold rounded-xl transition-colors flex items-center justify-center gap-2 shadow-lg shadow-brand-500/25">785 <i data-lucide="send" class="w-5 h-5"></i>786 <span>Submit Booking Request</span>787 </button>788 </div>789 </div>790 `;791}792 793let selectedRoom = null;794let selectedTech = null;795 796function selectRoom(roomId) {797 selectedRoom = roomId;798 document.getElementById('booking-room').value = roomId;799 document.querySelectorAll('#room-selector .room-card').forEach(card => {800 const isSelected = card.dataset.room === roomId;801 card.classList.toggle('border-brand-400', isSelected);802 card.classList.toggle('border-slate-200', !isSelected);803 card.classList.toggle('bg-brand-50', isSelected);804 card.classList.toggle('bg-white', !isSelected);805 });806}807 808function selectTech(tech) {809 selectedTech = tech;810 document.getElementById('booking-tech').value = tech;811 document.querySelectorAll('#tech-selector .tech-chip').forEach(chip => {812 const isActive = chip.dataset.tech === tech;813 chip.classList.toggle('active', isActive);814 chip.classList.toggle('border-brand-400', isActive);815 chip.classList.toggle('bg-brand-50', isActive);816 chip.classList.toggle('text-brand-700', isActive);817 chip.classList.toggle('border-slate-200', !isActive);818 chip.classList.toggle('text-slate-600', !isActive);819 });820}821 822async function handleSubmitBooking() {823 const room = selectedRoom;824 const techNeeds = selectedTech;825 const eventName = document.getElementById('booking-event-name').value.trim();826 const description = document.getElementById('booking-description').value.trim();827 const date = document.getElementById('booking-date').value;828 const startTime = document.getElementById('booking-start').value;829 const endTime = document.getElementById('booking-end').value;830 831 if (!room) { showToast('Please select a room', 'warning'); return; }832 if (!techNeeds) { showToast('Please select technical requirements', 'warning'); return; }833 if (!eventName) { showToast('Please enter an event name', 'warning'); return; }834 if (!date) { showToast('Please select a date', 'warning'); return; }835 if (!startTime || !endTime) { showToast('Please set start and end times', 'warning'); return; }836 if (startTime >= endTime) { showToast('End time must be after start time', 'warning'); return; }837 838 const btn = document.getElementById('submit-booking-btn');839 btn.innerHTML = '<span class="w-5 h-5 border-2 border-white border-t-transparent rounded-full animate-spin"></span><span>Submitting...</span>';840 btn.disabled = true;841 842 const result = createBooking({843 room,844 event_name: eventName,845 event_description: description,846 event_date: date,847 start_time: startTime,848 end_time: endTime,849 tech_needs: techNeeds,850 });851 852 if (result.error) {853 showToast('Error: ' + result.error.message, 'error');854 btn.innerHTML = '<i data-lucide="send" class="w-5 h-5"></i><span>Submit Booking Request</span>';855 btn.disabled = false;856 lucide.createIcons({ nodes: [btn] });857 } else {858 showToast('Booking request submitted!', 'success');859 selectedRoom = null;860 selectedTech = null;861 navigateTo('my-bookings');862 }863}864 865// ============================================================866// MY BOOKINGS VIEW867// ============================================================868function renderMyBookings() {869 const myBookings = bookingsCache.filter(b => b.user_id === currentUser.id);870 const pending = myBookings.filter(b => b.status === 'pending');871 const approved = myBookings.filter(b => b.status === 'approved');872 const rejected = myBookings.filter(b => b.status === 'rejected');873 874 return `875 <div class="view-enter">876 <div class="mb-6">877 <h2 class="text-2xl font-bold text-slate-800">My Bookings</h2>878 <p class="text-slate-500 mt-1">Track and manage your booking requests.</p>879 </div>880 881 <div class="flex gap-2 mb-6 overflow-x-auto pb-1">882 <button onclick="filterMyBookings('all')" class="filter-tab active px-4 py-2 rounded-lg text-sm font-medium bg-brand-500 text-white whitespace-nowrap" data-filter="all">883 All (${myBookings.length})884 </button>885 <button onclick="filterMyBookings('pending')" class="filter-tab px-4 py-2 rounded-lg text-sm font-medium bg-slate-100 text-slate-600 whitespace-nowrap" data-filter="pending">886 Pending (${pending.length})887 </button>888 <button onclick="filterMyBookings('approved')" class="filter-tab px-4 py-2 rounded-lg text-sm font-medium bg-slate-100 text-slate-600 whitespace-nowrap" data-filter="approved">889 Approved (${approved.length})890 </button>891 <button onclick="filterMyBookings('rejected')" class="filter-tab px-4 py-2 rounded-lg text-sm font-medium bg-slate-100 text-slate-600 whitespace-nowrap" data-filter="rejected">892 Rejected (${rejected.length})893 </button>894 </div>895 896 <div id="my-bookings-list" class="space-y-3 stagger-children">897 ${renderBookingsList(myBookings)}898 </div>899 </div>900 `;901}902 903function filterMyBookings(status) {904 let filtered = bookingsCache.filter(b => b.user_id === currentUser.id);905 if (status !== 'all') filtered = filtered.filter(b => b.status === status);906 907 document.querySelectorAll('.filter-tab').forEach(tab => {908 tab.classList.toggle('bg-brand-500', tab.dataset.filter === status);909 tab.classList.toggle('text-white', tab.dataset.filter === status);910 tab.classList.toggle('bg-slate-100', tab.dataset.filter !== status);911 tab.classList.toggle('text-slate-600', tab.dataset.filter !== status);912 });913 914 const list = document.getElementById('my-bookings-list');915 list.innerHTML = renderBookingsList(filtered);916 lucide.createIcons({ nodes: [list] });917}918 919function renderBookingsList(bookings) {920 if (bookings.length === 0) {921 return `922 <div class="text-center py-16">923 <div class="w-16 h-16 bg-slate-100 rounded-2xl flex items-center justify-center mx-auto mb-4">924 <i data-lucide="inbox" class="w-8 h-8 text-slate-300"></i>925 </div>926 <p class="text-slate-500 font-medium">No bookings found</p>927 <p class="text-slate-400 text-sm mt-1">Create a new booking request to get started.</p>928 </div>929 `;930 }931 932 return bookings.sort((a, b) => new Date(b.created_at) - new Date(a.created_at)).map(booking => {933 const room = ROOMS.find(r => r.id === booking.room);934 const statusCfg = STATUS_CONFIG[booking.status];935 const dateStr = new Date(booking.event_date + 'T00:00:00').toLocaleDateString('en-US', { weekday: 'short', month: 'short', day: 'numeric' });936 937 return `938 <div class="bg-white rounded-xl border border-slate-100 shadow-sm p-4 hover:shadow-md transition-shadow">939 <div class="flex items-start gap-4">940 <div class="w-12 h-12 bg-gradient-to-br ${room?.gradient || 'from-slate-400 to-slate-600'} rounded-xl flex items-center justify-center flex-shrink-0">941 <i data-lucide="${room?.icon || 'building-2'}" class="w-6 h-6 text-white"></i>942 </div>943 <div class="flex-1 min-w-0">944 <div class="flex items-start justify-between gap-2">945 <div>946 <h4 class="font-semibold text-slate-800">${booking.event_name}</h4>947 <p class="text-sm text-slate-500">${room?.label || booking.room}</p>948 </div>949 <span class="${statusCfg.class} text-xs font-semibold px-2.5 py-1 rounded-full whitespace-nowrap">950 ${statusCfg.label}951 </span>952 </div>953 <div class="flex items-center gap-4 mt-2 text-sm text-slate-500">954 <span class="flex items-center gap-1"><i data-lucide="calendar" class="w-3.5 h-3.5"></i>${dateStr}</span>955 <span class="flex items-center gap-1"><i data-lucide="clock" class="w-3.5 h-3.5"></i>${formatTime(booking.start_time)} - ${formatTime(booking.end_time)}</span>956 <span class="flex items-center gap-1"><i data-lucide="layers" class="w-3.5 h-3.5"></i>${booking.tech_needs}</span>957 </div>958 ${booking.admin_notes ? `<p class="mt-2 text-sm text-slate-600 bg-slate-50 rounded-lg px-3 py-2">๐ ${booking.admin_notes}</p>` : ''}959 </div>960 </div>961 </div>962 `;963 }).join('');964}965 966// ============================================================967// CALENDAR VIEW968// ============================================================969function renderCalendar() {970 const year = calendarDate.getFullYear();971 const month = calendarDate.getMonth();972 const firstDay = new Date(year, month, 1);973 const lastDay = new Date(year, month + 1, 0);974 const startDay = firstDay.getDay();975 const daysInMonth = lastDay.getDate();976 const prevMonthDays = new Date(year, month, 0).getDate();977 978 const monthNames = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];979 const dayNames = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];980 981 const today = new Date();982 const todayStr = `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, '0')}-${String(today.getDate()).padStart(2, '0')}`;983 984 const approvedBookings = bookingsCache.filter(b => b.status === 'approved');985 986 let cells = '';987 for (let i = startDay - 1; i >= 0; i--) {988 const day = prevMonthDays - i;989 cells += `<div class="cal-day other-month rounded-lg p-2 text-sm text-slate-400">${day}</div>`;990 }991 for (let d = 1; d <= daysInMonth; d++) {992 const dateStr = `${year}-${String(month + 1).padStart(2, '0')}-${String(d).padStart(2, '0')}`;993 const isToday = dateStr === todayStr;994 const isSelected = selectedDate === dateStr;995 const dayBookings = approvedBookings.filter(b => b.event_date === dateStr);996 997 cells += `998 <div onclick="selectCalendarDate('${dateStr}')" class="cal-day ${isToday ? 'today' : ''} ${isSelected ? 'selected' : ''} rounded-lg p-2 border border-transparent hover:border-brand-200">999 <span class="text-sm font-medium ${isToday ? 'text-brand-600 font-bold' : 'text-slate-700'}">${d}</span>1000 <div class="mt-1 space-y-0.5">1001 ${dayBookings.slice(0, 2).map(b => {1002 const room = ROOMS.find(r => r.id === b.room);1003 return `<div class="text-[10px] font-medium px-1 py-0.5 rounded bg-${room?.color || 'slate'}-100 text-${room?.color || 'slate'}-700 truncate">${b.event_name}</div>`;1004 }).join('')}1005 ${dayBookings.length > 2 ? `<div class="text-[10px] text-slate-400 font-medium">+${dayBookings.length - 2} more</div>` : ''}1006 </div>1007 </div>1008 `;1009 }1010 const totalCells = startDay + daysInMonth;1011 const remaining = totalCells % 7 === 0 ? 0 : 7 - (totalCells % 7);1012 for (let i = 1; i <= remaining; i++) {1013 cells += `<div class="cal-day other-month rounded-lg p-2 text-sm text-slate-400">${i}</div>`;1014 }1015 1016 let dayDetail = '';1017 if (selectedDate) {1018 const dayBookings = approvedBookings.filter(b => b.event_date === selectedDate);1019 const dateObj = new Date(selectedDate + 'T00:00:00');1020 const dateLabel = dateObj.toLocaleDateString('en-US', { weekday: 'long', month: 'long', day: 'numeric', year: 'numeric' });1021 1022 dayDetail = `1023 <div class="mt-6 bg-white rounded-2xl border border-slate-100 shadow-sm p-5 animate-slide-up">1024 <h3 class="font-semibold text-slate-800 mb-1">${dateLabel}</h3>1025 <p class="text-sm text-slate-500 mb-4">${dayBookings.length} approved event${dayBookings.length !== 1 ? 's' : ''}</p>1026 ${dayBookings.length === 0 ? `<p class="text-slate-400 text-sm py-4 text-center">No events scheduled for this day.</p>` : ''}1027 <div class="space-y-3">1028 ${dayBookings.map(b => {1029 const room = ROOMS.find(r => r.id === b.room);1030 return `1031 <div class="flex items-start gap-3 p-3 bg-slate-50 rounded-xl">1032 <div class="w-10 h-10 bg-gradient-to-br ${room?.gradient || 'from-slate-400 to-slate-600'} rounded-lg flex items-center justify-center flex-shrink-0">1033 <i data-lucide="${room?.icon || 'building-2'}" class="w-5 h-5 text-white"></i>1034 </div>1035 <div>1036 <h4 class="font-semibold text-slate-800 text-sm">${b.event_name}</h4>1037 <p class="text-xs text-slate-500">${room?.label || b.room} โข ${formatTime(b.start_time)} - ${formatTime(b.end_time)}</p>1038 <p class="text-xs text-slate-400 mt-0.5">Tech: ${b.tech_needs}</p>1039 </div>1040 </div>1041 `;1042 }).join('')}1043 </div>1044 </div>1045 `;1046 }1047 1048 return `1049 <div class="view-enter">1050 <div class="flex items-center justify-between mb-6">1051 <div>1052 <h2 class="text-2xl font-bold text-slate-800">Calendar</h2>1053 <p class="text-slate-500 mt-1">View approved events across all spaces.</p>1054 </div>1055 <div class="flex items-center gap-2">1056 <button onclick="changeMonth(-1)" class="w-9 h-9 bg-white border border-slate-200 rounded-lg flex items-center justify-center hover:bg-slate-50 transition-colors">1057 <i data-lucide="chevron-left" class="w-4 h-4"></i>1058 </button>1059 <span class="text-sm font-semibold text-slate-800 min-w-[160px] text-center">${monthNames[month]} ${year}</span>1060 <button onclick="changeMonth(1)" class="w-9 h-9 bg-white border border-slate-200 rounded-lg flex items-center justify-center hover:bg-slate-50 transition-colors">1061 <i data-lucide="chevron-right" class="w-4 h-4"></i>1062 </button>1063 </div>1064 </div>1065 1066 <div class="bg-white rounded-2xl border border-slate-100 shadow-sm overflow-hidden">1067 <div class="grid grid-cols-7 bg-slate-50 border-b border-slate-100">1068 ${dayNames.map(d => `<div class="py-2 text-center text-xs font-semibold text-slate-500">${d}</div>`).join('')}1069 </div>1070 <div class="grid grid-cols-7">1071 ${cells}1072 </div>1073 </div>1074 1075 <div id="day-detail">${dayDetail}</div>1076 </div>1077 `;1078}1079 1080function selectCalendarDate(dateStr) {1081 selectedDate = dateStr;1082 renderCurrentView();1083}1084 1085function changeMonth(delta) {1086 calendarDate.setMonth(calendarDate.getMonth() + delta);1087 selectedDate = null;1088 renderCurrentView();1089}1090 1091// ============================================================1092// APPROVALS VIEW (Admin Only)1093// ============================================================1094function renderApprovals() {1095 const pendingBookings = bookingsCache.filter(b => b.status === 'pending');1096 1097 return `1098 <div class="view-enter">1099 <div class="mb-6">1100 <h2 class="text-2xl font-bold text-slate-800">Pending Approvals</h2>1101 <p class="text-slate-500 mt-1">Review and approve or reject booking requests.</p>1102 </div>1103 1104 ${pendingBookings.length === 0 ? `1105 <div class="text-center py-16">1106 <div class="w-16 h-16 bg-emerald-50 rounded-2xl flex items-center justify-center mx-auto mb-4">1107 <i data-lucide="check-circle" class="w-8 h-8 text-emerald-300"></i>1108 </div>1109 <p class="text-slate-500 font-medium">All caught up! ๐</p>1110 <p class="text-slate-400 text-sm mt-1">No pending requests to review.</p>1111 </div>1112 ` : `1113 <div class="space-y-4 stagger-children">1114 ${pendingBookings.sort((a, b) => new Date(a.event_date) - new Date(b.event_date)).map(booking => {1115 const room = ROOMS.find(r => r.id === booking.room);1116 const dateStr = new Date(booking.event_date + 'T00:00:00').toLocaleDateString('en-US', { weekday: 'long', month: 'long', day: 'numeric' });1117 const createdStr = new Date(booking.created_at).toLocaleDateString('en-US', { month: 'short', day: 'numeric' });1118 1119 return `1120 <div class="bg-white rounded-2xl border border-slate-100 shadow-sm p-5 hover:shadow-md transition-shadow">1121 <div class="flex items-start gap-4">1122 <div class="w-14 h-14 bg-gradient-to-br ${room?.gradient || 'from-slate-400 to-slate-600'} rounded-xl flex items-center justify-center flex-shrink-0">1123 <i data-lucide="${room?.icon || 'building-2'}" class="w-7 h-7 text-white"></i>1124 </div>1125 <div class="flex-1 min-w-0">1126 <div class="flex items-start justify-between gap-2">1127 <div>1128 <h4 class="font-bold text-slate-800 text-lg">${booking.event_name}</h4>1129 <p class="text-sm text-slate-500">${room?.label || booking.room} โข ${dateStr}</p>1130 </div>1131 <span class="badge-pending text-xs font-semibold px-2.5 py-1 rounded-full whitespace-nowrap">Pending</span>1132 </div>1133 <div class="flex flex-wrap items-center gap-3 mt-3 text-sm text-slate-500">1134 <span class="flex items-center gap-1"><i data-lucide="clock" class="w-3.5 h-3.5"></i>${formatTime(booking.start_time)} - ${formatTime(booking.end_time)}</span>1135 <span class="flex items-center gap-1"><i data-lucide="layers" class="w-3.5 h-3.5"></i>${booking.tech_needs}</span>1136 <span class="flex items-center gap-1"><i data-lucide="user" class="w-3.5 h-3.5"></i>${booking.profiles?.full_name || 'Unknown'}</span>1137 </div>1138 ${booking.event_description ? `<p class="mt-2 text-sm text-slate-600">${booking.event_description}</p>` : ''}1139 <p class="mt-1 text-xs text-slate-400">Requested on ${createdStr}</p>1140 </div>1141 </div>1142 <div class="flex items-center gap-3 mt-4 pt-4 border-t border-slate-100">1143 <button onclick="handleApprove('${booking.id}')" class="flex-1 py-2.5 bg-emerald-500 hover:bg-emerald-600 text-white font-semibold rounded-xl transition-colors flex items-center justify-center gap-2">1144 <i data-lucide="check" class="w-4 h-4"></i>1145 Approve1146 </button>1147 <button onclick="showRejectModal('${booking.id}')" class="flex-1 py-2.5 bg-red-500 hover:bg-red-600 text-white font-semibold rounded-xl transition-colors flex items-center justify-center gap-2">1148 <i data-lucide="x" class="w-4 h-4"></i>1149 Reject1150 </button>1151 </div>1152 </div>1153 `;1154 }).join('')}1155 </div>1156 `}1157 </div>1158 `;1159}1160 1161async function handleApprove(bookingId) {1162 const result = updateBookingStatus(bookingId, 'approved', '');1163 if (result.error) {1164 showToast('Error: ' + result.error.message, 'error');1165 } else {1166 showToast('Booking approved! โ
', 'success');1167 renderApp();1168 }1169}1170 1171function showRejectModal(bookingId) {1172 showModal(`1173 <div class="p-6">1174 <div class="flex items-center gap-3 mb-4">1175 <div class="w-10 h-10 bg-red-100 rounded-xl flex items-center justify-center">1176 <i data-lucide="x-circle" class="w-5 h-5 text-red-500"></i>1177 </div>1178 <h3 class="text-lg font-bold text-slate-800">Reject Booking</h3>1179 </div>1180 <p class="text-sm text-slate-500 mb-4">Please provide a reason for rejecting this booking request.</p>1181 <textarea id="reject-notes" rows="3" placeholder="Reason for rejection..."1182 class="w-full px-4 py-3 border border-slate-200 rounded-xl text-slate-800 placeholder-slate-400 resize-none mb-4"></textarea>1183 <div class="flex gap-3">1184 <button onclick="closeModal()" class="flex-1 py-2.5 bg-slate-100 hover:bg-slate-200 text-slate-700 font-semibold rounded-xl transition-colors">Cancel</button>1185 <button onclick="handleReject('${bookingId}')" class="flex-1 py-2.5 bg-red-500 hover:bg-red-600 text-white font-semibold rounded-xl transition-colors">Reject</button>1186 </div>1187 </div>1188 `);1189}1190 1191async function handleReject(bookingId) {1192 const notes = document.getElementById('reject-notes')?.value.trim() || '';1193 const result = updateBookingStatus(bookingId, 'rejected', notes);1194 closeModal();1195 if (result.error) {1196 showToast('Error: ' + result.error.message, 'error');1197 } else {1198 showToast('Booking rejected.', 'warning');1199 renderApp();1200 }