CoolFace
Apppublic

landonalas/Vida-Real-Booking-System1

sourceHugging Faceupdated 5mo agoView on Hugging Face
0likes
script.js1662 linesDownload Raw Back to root
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  }

Showing the first 1,200 of 1662 lines. Download the file for the rest.