Spaces:
Running
Running
| <html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>UniGrub - Campus Canteen System</title> | |
| <style> | |
| /* | |
| =========================================== | |
| UNIGRUB CAMPUS CANTEEN ORDERING SYSTEM | |
| =========================================== | |
| RESET INSTRUCTIONS: | |
| To reset all data, open browser console (F12) and run: | |
| localStorage.clear(); location.reload(); | |
| SECURITY NOTICE: | |
| This is a DEMO application. All data is stored in browser localStorage. | |
| Passwords are NOT encrypted. DO NOT use real passwords. | |
| This app is for educational/prototyping purposes only. | |
| DEFAULT CREDENTIALS: | |
| Student: john / student123 | |
| Vendor: andrea / pizza123 | |
| Admin: admin / admin123 | |
| =========================================== | |
| */ | |
| :root { | |
| --primary: #4CAF50; /* CHANGED TO GREEN */ | |
| --primary-dark: #388E3C; /* CHANGED TO GREEN */ | |
| --primary-glass: rgba(76, 175, 80, 0.15); /* CHANGED TO GREEN */ | |
| --primary-dots: rgba(76, 175, 80, 0.08); /* CHANGED TO GREEN */ | |
| --primary-shadow: rgba(76, 175, 80, 0.2); /* CHANGED TO GREEN */ | |
| --dark-card: rgba(30, 30, 30, 0.55); | |
| --text: #FCFBFC; | |
| --text-secondary: #b0b0b0; | |
| --success: #4CAF50; | |
| --danger: #f44336; | |
| --glass-border: rgba(76, 175, 80, 0.3); /* CHANGED TO GREEN */ | |
| } | |
| * { margin: 0; padding: 0; box-sizing: border-box; font-family: 'Segoe UI', sans-serif; } | |
| body { | |
| background: radial-gradient(circle at 20% 30%, var(--primary-glass) 0%, transparent 50%), | |
| linear-gradient(135deg, #1a1a1a 0%, #232323 40%, #1a1a1a 100%); | |
| background-attachment: fixed; | |
| color: var(--text); | |
| min-height: 100vh; | |
| position: relative; | |
| transition: all 0.3s ease; | |
| } | |
| /* Dotty Background Overlay */ | |
| body::before { | |
| content: ''; | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| background-image: | |
| repeating-linear-gradient(90deg, transparent, transparent 2px, var(--primary-dots) 2px, var(--primary-dots) 4px), | |
| repeating-linear-gradient(0deg, transparent, transparent 2px, var(--primary-dots) 2px, var(--primary-dots) 4px); | |
| opacity: 0.6; | |
| pointer-events: none; | |
| z-index: 0; | |
| transition: all 0.3s ease; | |
| } | |
| .container { | |
| max-width: 1200px; | |
| margin: 0 auto; | |
| padding: 16px; | |
| position: relative; | |
| z-index: 1; | |
| } | |
| header { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| padding: 16px 0; | |
| border-bottom: 1px solid var(--primary); | |
| margin-bottom: 32px; | |
| flex-wrap: wrap; | |
| } | |
| .logo { | |
| display: flex; | |
| align-items: center; | |
| gap: 12px; | |
| font-size: 20px; | |
| font-weight: bold; | |
| color: var(--primary); | |
| margin-bottom: 8px; | |
| } | |
| .logo-img { | |
| width: 40px; | |
| height: 40px; | |
| border-radius: 50%; | |
| object-fit: cover; | |
| border: 2px solid var(--primary); | |
| } | |
| nav { | |
| width: 100%; | |
| margin-top: 8px; | |
| } | |
| nav ul { | |
| display: flex; | |
| list-style: none; | |
| gap: 12px; | |
| align-items: center; | |
| flex-wrap: wrap; | |
| justify-content: center; | |
| } | |
| nav a { | |
| color: var(--text); | |
| text-decoration: none; | |
| padding: 8px 12px; | |
| border-radius: 20px; | |
| background: var(--dark-card); | |
| border: 1px solid var(--glass-border); | |
| transition: all 0.3s; | |
| display: inline-block; | |
| font-size: 14px; | |
| } | |
| nav a:hover { background-color: var(--primary); color: #121212; } | |
| .card { | |
| background: var(--dark-card); | |
| backdrop-filter: blur(8px); | |
| border-radius: 12px; | |
| padding: 24px; | |
| box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3); | |
| border: 1px solid var(--glass-border); | |
| margin-bottom: 20px; | |
| transition: all 0.3s; | |
| } | |
| .card:hover { transform: translateY(-5px); box-shadow: 0 8px 25px var(--primary-shadow); } | |
| .btn { | |
| padding: 14px 20px; | |
| font-size: 16px; | |
| border: none; | |
| border-radius: 10px; | |
| cursor: pointer; | |
| color: #fff; | |
| background: linear-gradient(135deg, var(--primary) 0%, var(--primary-dark) 100%); | |
| transition: all 0.3s; | |
| font-weight: 600; | |
| display: inline-block; | |
| text-align: center; | |
| min-height: 50px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| } | |
| .btn:hover { transform: translateY(-3px); } | |
| .btn-secondary { background-color: transparent; color: var(--text); border: 1px solid var(--primary); } | |
| .btn-danger { background-color: var(--danger); } | |
| .btn-success { background-color: var(--success); } | |
| .btn-small { padding: 10px 16px; font-size: 14px; min-height: 40px; } | |
| .form-group { margin-bottom: 20px; } | |
| label { display: block; margin-bottom: 8px; color: var(--text-secondary); font-weight: 500; } | |
| input, select, textarea { | |
| width: 100%; | |
| padding: 14px; | |
| background-color: rgba(255, 255, 255, 0.1); | |
| border: 1px solid rgba(255, 255, 255, 0.2); | |
| border-radius: 8px; | |
| color: var(--text); | |
| font-size: 16px; | |
| -webkit-appearance: none; | |
| } | |
| select { | |
| appearance: none; | |
| background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%234CAF50' d='M6 9L1 4h10z'/%3E%3C/svg%3E"); /* CHANGED TO GREEN */ | |
| background-repeat: no-repeat; | |
| background-position: right 16px center; | |
| padding-right: 40px; | |
| } | |
| select option { | |
| background-color: #2a2a2a; | |
| color: var(--text); | |
| padding: 12px; | |
| } | |
| textarea { resize: vertical; min-height: 80px; } | |
| input:focus, select:focus, textarea:focus { outline: none; border-color: var(--primary); box-shadow: 0 0 0 2px var(--primary-shadow); } | |
| .page { display: none; animation: fadeIn 0.5s; } | |
| .page.active { display: block; } | |
| @keyframes fadeIn { from { opacity: 0; transform: translateY(20px); } to { opacity: 1; transform: translateY(0); } } | |
| .role-selection { | |
| display: flex; | |
| justify-content: center; | |
| align-items: center; | |
| gap: 16px; | |
| margin: 32px 0; | |
| flex-wrap: wrap; | |
| } | |
| .role-card { | |
| width: 100%; | |
| max-width: 280px; | |
| text-align: center; | |
| padding: 24px; | |
| cursor: pointer; | |
| margin-bottom: 16px; | |
| } | |
| .role-card:hover { transform: translateY(-10px); } | |
| .grid { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); | |
| gap: 20px; | |
| margin-top: 20px; | |
| } | |
| .stall-logo { | |
| width: 100%; | |
| height: 160px; | |
| object-fit: cover; | |
| border-radius: 8px; | |
| margin: 0 auto 16px; | |
| display: block; | |
| } | |
| .stall-logo-placeholder { | |
| width: 100%; | |
| height: 160px; | |
| background: linear-gradient(135deg, var(--primary), var(--primary-dark)); | |
| margin: 0 auto 16px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| font-size: 36px; | |
| font-weight: bold; | |
| color: #1a1a1a; | |
| border-radius: 8px; | |
| } | |
| /* Square logo for stall profile */ | |
| .stall-logo-square { | |
| width: 160px; | |
| height: 160px; | |
| object-fit: cover; | |
| border-radius: 8px; | |
| margin: 0 auto 16px; | |
| display: block; | |
| } | |
| .stall-logo-placeholder-square { | |
| width: 160px; | |
| height: 160px; | |
| background: linear-gradient(135deg, var(--primary), var(--primary-dark)); | |
| margin: 0 auto 16px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| font-size: 36px; | |
| font-weight: bold; | |
| color: #1a1a1a; | |
| border-radius: 8px; | |
| } | |
| .menu-item { | |
| display: flex; | |
| gap: 16px; | |
| padding: 20px; | |
| border-radius: 8px; | |
| background-color: rgba(255, 255, 255, 0.05); | |
| flex-direction: column; | |
| } | |
| .menu-item-image { | |
| position: relative; | |
| width: 100%; | |
| height: 140px; | |
| border-radius: 8px; | |
| overflow: hidden; | |
| flex-shrink: 0; | |
| } | |
| .menu-item-image img { width: 100%; height: 100%; object-fit: cover; } | |
| /* Left-to-Right Gradient Overlay (90% → 0% opacity) */ | |
| .menu-item-image::after { | |
| content: ''; | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| background: linear-gradient(to right, rgba(0, 0, 0, 0.9) 0%, rgba(0, 0, 0, 0) 100%); | |
| pointer-events: none; | |
| } | |
| .menu-item-info { flex: 1; } | |
| .menu-item-name { font-weight: bold; margin-bottom: 8px; font-size: 18px; } | |
| .menu-item-price { color: var(--primary); font-weight: bold; font-size: 16px; margin-bottom: 8px; } | |
| .menu-item-actions { display: flex; gap: 8px; margin-top: 16px; flex-wrap: wrap; } | |
| .section-header { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| margin-bottom: 20px; | |
| flex-wrap: wrap; | |
| gap: 12px; | |
| } | |
| .section-title { font-size: 22px; color: var(--primary); margin-bottom: 12px; } | |
| .section-subtitle { font-size: 16px; color: var(--text-secondary); margin-bottom: 20px; } | |
| .text-center { text-align: center; } | |
| .vendor-item { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| padding: 20px; | |
| margin-bottom: 12px; | |
| background: var(--dark-card); | |
| border-radius: 8px; | |
| border: 1px solid var(--glass-border); | |
| flex-direction: column; | |
| gap: 16px; | |
| } | |
| .vendor-info { width: 100%; } | |
| .vendor-info h4 { color: var(--primary); margin-bottom: 8px; font-size: 18px; } | |
| .vendor-actions { display: flex; gap: 8px; width: 100%; justify-content: center; } | |
| .modal { | |
| display: none; | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| background-color: rgba(0, 0, 0, 0.7); | |
| backdrop-filter: blur(5px); | |
| z-index: 1000; | |
| align-items: center; | |
| justify-content: center; | |
| animation: fadeIn 0.3s; | |
| padding: 16px; | |
| box-sizing: border-box; | |
| } | |
| .modal-content { | |
| background-color: var(--dark-card); | |
| backdrop-filter: blur(8px); | |
| border-radius: 12px; | |
| padding: 24px; | |
| width: 100%; | |
| max-width: 500px; | |
| box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5); | |
| position: relative; | |
| border: 1px solid var(--glass-border); | |
| max-height: 90vh; | |
| overflow-y: auto; | |
| animation: scaleIn 0.3s; | |
| } | |
| @keyframes scaleIn { | |
| from { transform: scale(0.9); opacity: 0; } | |
| to { transform: scale(1); opacity: 1; } | |
| } | |
| .close-modal { | |
| position: absolute; | |
| top: 12px; | |
| right: 12px; | |
| font-size: 24px; | |
| cursor: pointer; | |
| color: var(--text-secondary); | |
| width: 32px; | |
| height: 32px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| } | |
| .close-modal:hover { color: var(--text); } | |
| .order-status { padding: 4px 8px; border-radius: 4px; font-size: 12px; font-weight: bold; } | |
| .status-pending { background: rgba(255, 193, 7, 0.2); color: #ffc107; } | |
| .status-preparing { background: rgba(33, 150, 243, 0.2); color: #2196F3; } | |
| .status-ready { background: rgba(76, 175, 80, 0.2); color: #4CAF50; } | |
| .status-completed { background: rgba(156, 39, 176, 0.2); color: #9C27B0; } | |
| .qr-preview { | |
| width: 160px; | |
| height: 160px; | |
| margin: 0 auto 20px; | |
| background: rgba(255, 255, 255, 0.1); | |
| border-radius: 8px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| border: 2px dashed var(--glass-border); | |
| } | |
| .qr-preview img { max-width: 100%; max-height: 100%; border-radius: 4px; } | |
| .cart-item { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| padding: 14px 0; | |
| border-bottom: 1px solid rgba(255, 255, 255, 0.1); | |
| flex-direction: column; | |
| gap: 12px; | |
| } | |
| .cart-total { | |
| font-size: 22px; | |
| font-weight: bold; | |
| color: var(--primary); | |
| margin: 20px 0; | |
| text-align: center; | |
| } | |
| .intro { | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| background: linear-gradient(135deg, #1a1a1a 0%, #232323 40%, #1a1a1a 100%); | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| justify-content: center; | |
| z-index: 9999; | |
| animation: fadeOut 1s ease 2s forwards; | |
| padding: 16px; | |
| text-align: center; | |
| } | |
| .intro-logo { | |
| font-size: 36px; | |
| display: flex; | |
| align-items: center; | |
| gap: 12px; | |
| color: var(--primary); | |
| animation: pulse 2s infinite; | |
| flex-direction: column; | |
| } | |
| @keyframes fadeOut { to { opacity: 0; visibility: hidden; pointer-events: none; } } | |
| @keyframes pulse { 0%, 100% { transform: scale(1); } 50% { transform: scale(1.05); } } | |
| .toast { | |
| position: fixed; | |
| bottom: 20px; | |
| right: 20px; | |
| left: 20px; | |
| background: var(--dark-card); | |
| backdrop-filter: blur(8px); | |
| border: 1px solid var(--glass-border); | |
| border-radius: 8px; | |
| padding: 14px 20px; | |
| box-shadow: 0 8px 25px rgba(0, 0, 0, 0.5); | |
| z-index: 10000; | |
| animation: slideIn 0.3s; | |
| max-width: 400px; | |
| margin: 0 auto; | |
| } | |
| @keyframes slideIn { from { transform: translateX(400px); opacity: 0; } to { transform: translateX(0); opacity: 1; } } | |
| .toast.success { border-left: 4px solid var(--success); } | |
| .toast.error { border-left: 4px solid var(--danger); } | |
| .toast.info { border-left: 4px solid #2196F3; } | |
| .size-price-list { | |
| margin: 14px 0; | |
| padding: 14px; | |
| background: rgba(255, 255, 255, 0.05); | |
| border-radius: 8px; | |
| } | |
| .size-price-item { | |
| display: flex; | |
| justify-content: space-between; | |
| padding: 8px 0; | |
| border-bottom: 1px solid rgba(255, 255, 255, 0.1); | |
| } | |
| .size-price-item:last-child { | |
| border-bottom: none; | |
| } | |
| .item-detail-modal .modal-content { | |
| max-width: 600px; | |
| } | |
| .item-detail-image { | |
| width: 100%; | |
| height: 200px; | |
| object-fit: cover; | |
| border-radius: 8px; | |
| margin-bottom: 20px; | |
| } | |
| .price-display { | |
| font-size: 28px; | |
| color: var(--primary); | |
| font-weight: bold; | |
| text-align: center; | |
| margin: 20px 0; | |
| } | |
| /* Progress tracker styles */ | |
| .progress-tracker { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| margin: 16px 0; | |
| position: relative; | |
| flex-wrap: wrap; | |
| } | |
| .progress-tracker::before { | |
| content: ''; | |
| position: absolute; | |
| top: 50%; | |
| left: 0; | |
| right: 0; | |
| height: 4px; | |
| background: rgba(255, 255, 255, 0.1); | |
| transform: translateY(-50%); | |
| z-index: 1; | |
| } | |
| .progress-bar { | |
| position: absolute; | |
| top: 50%; | |
| left: 0; | |
| height: 4px; | |
| background: var(--primary); | |
| transform: translateY(-50%); | |
| z-index: 2; | |
| transition: width 0.5s ease; | |
| } | |
| .progress-step { | |
| position: relative; | |
| z-index: 3; | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| flex: 1; | |
| min-width: 70px; | |
| margin-bottom: 8px; | |
| } | |
| .progress-step:last-child { | |
| flex: 0; | |
| } | |
| .step-indicator { | |
| width: 28px; | |
| height: 28px; | |
| border-radius: 50%; | |
| background: rgba(255, 255, 255, 0.1); | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| margin-bottom: 6px; | |
| transition: all 0.3s; | |
| font-size: 14px; | |
| } | |
| .step-indicator.active { | |
| background: var(--primary); | |
| color: #1a1a1a; | |
| } | |
| .step-indicator.completed { | |
| background: var(--success); | |
| color: #fff; | |
| } | |
| .step-label { | |
| font-size: 11px; | |
| text-align: center; | |
| color: var(--text-secondary); | |
| } | |
| .step-label.active { | |
| color: var(--primary); | |
| font-weight: bold; | |
| } | |
| .step-label.completed { | |
| color: var(--success); | |
| } | |
| /* Mobile menu styles - NEW FEATURE MERGED */ | |
| .mobile-menu-btn { | |
| display: none; | |
| background: none; | |
| border: none; | |
| color: var(--text); | |
| font-size: 24px; | |
| cursor: pointer; | |
| padding: 8px; | |
| } | |
| .mobile-nav { | |
| display: none; | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| background: rgba(26, 26, 26, 0.95); | |
| z-index: 1000; | |
| flex-direction: column; | |
| justify-content: center; | |
| align-items: center; | |
| } | |
| .mobile-nav.active { | |
| display: flex; | |
| } | |
| .mobile-nav ul { | |
| list-style: none; | |
| display: flex; | |
| flex-direction: column; | |
| gap: 20px; | |
| align-items: center; | |
| } | |
| .mobile-nav a { | |
| color: var(--text); | |
| text-decoration: none; | |
| font-size: 18px; | |
| padding: 12px 24px; | |
| border-radius: 30px; | |
| background: var(--dark-card); | |
| border: 1px solid var(--glass-border); | |
| transition: all 0.3s; | |
| display: block; | |
| text-align: center; | |
| min-width: 200px; | |
| } | |
| .mobile-nav a:hover { | |
| background: var(--primary); | |
| color: #1a1a1a; | |
| } | |
| .close-mobile-nav { | |
| position: absolute; | |
| top: 20px; | |
| right: 20px; | |
| background: none; | |
| border: none; | |
| color: var(--text); | |
| font-size: 28px; | |
| cursor: pointer; | |
| } | |
| /* Color scheme styles - NEW FEATURE MERGED */ | |
| .color-scheme-selector { | |
| display: flex; | |
| gap: 12px; | |
| margin: 20px 0; | |
| flex-wrap: wrap; | |
| justify-content: center; | |
| } | |
| .color-option { | |
| width: 40px; | |
| height: 40px; | |
| border-radius: 50%; | |
| cursor: pointer; | |
| border: 3px solid transparent; | |
| transition: all 0.3s; | |
| } | |
| .color-option.active { | |
| border-color: white; | |
| transform: scale(1.1); | |
| } | |
| .color-option:hover { | |
| transform: scale(1.1); | |
| } | |
| /* Theme button in navigation - NEW FEATURE MERGED */ | |
| .theme-btn { | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| width: 40px; | |
| height: 40px; | |
| border-radius: 50%; | |
| background: var(--dark-card); | |
| border: 1px solid var(--glass-border); | |
| color: var(--primary); | |
| cursor: pointer; | |
| font-size: 18px; | |
| transition: all 0.3s; | |
| } | |
| .theme-btn:hover { | |
| background: var(--primary); | |
| color: #121212; | |
| } | |
| /* Login form button alignment fix */ | |
| .login-form-buttons { | |
| display: flex; | |
| gap: 10px; | |
| flex-wrap: wrap; | |
| } | |
| .login-form-buttons .btn { | |
| flex: 1; | |
| min-width: 120px; | |
| } | |
| /* Navigation alignment fix */ | |
| .nav-buttons { | |
| display: flex; | |
| gap: 12px; | |
| align-items: center; | |
| } | |
| /* Responsive adjustments */ | |
| @media (min-width: 768px) { | |
| .container { padding: 24px; } | |
| header { | |
| padding: 24px 0; | |
| margin-bottom: 48px; | |
| flex-wrap: nowrap; | |
| } | |
| .logo { | |
| gap: 16px; | |
| font-size: 24px; | |
| margin-bottom: 0; | |
| } | |
| .logo-img { | |
| width: 50px; | |
| height: 50px; | |
| } | |
| nav { | |
| width: auto; | |
| margin-top: 0; | |
| } | |
| nav ul { | |
| gap: 24px; | |
| justify-content: flex-end; | |
| } | |
| nav a { | |
| padding: 8px 16px; | |
| font-size: 16px; | |
| } | |
| .card { | |
| padding: 32px; | |
| margin-bottom: 24px; | |
| } | |
| .btn { | |
| padding: 16px 24px; | |
| min-height: auto; | |
| } | |
| .btn-small { padding: 8px 16px; font-size: 14px; min-height: auto; } | |
| .role-selection { | |
| gap: 24px; | |
| margin: 48px 0; | |
| } | |
| .role-card { | |
| width: 220px; | |
| padding: 32px; | |
| margin-bottom: 0; | |
| } | |
| .grid { | |
| gap: 24px; | |
| margin-top: 24px; | |
| } | |
| .stall-logo { | |
| height: 200px; | |
| } | |
| .stall-logo-placeholder { | |
| height: 200px; | |
| font-size: 48px; | |
| } | |
| .stall-logo-square { | |
| width: 200px; | |
| height: 200px; | |
| } | |
| .stall-logo-placeholder-square { | |
| width: 200px; | |
| height: 200px; | |
| font-size: 48px; | |
| } | |
| .menu-item { | |
| flex-direction: row; | |
| gap: 24px; | |
| padding: 24px; | |
| } | |
| .menu-item-image { | |
| width: 96px; | |
| height: 120px; | |
| } | |
| .section-header { | |
| margin-bottom: 24px; | |
| gap: 16px; | |
| } | |
| .section-title { font-size: 24px; margin-bottom: 16px; } | |
| .section-subtitle { font-size: 18px; margin-bottom: 24px; } | |
| .vendor-item { | |
| flex-direction: row; | |
| padding: 24px; | |
| margin-bottom: 16px; | |
| gap: 0; | |
| } | |
| .vendor-info { width: auto; } | |
| .vendor-actions { width: auto; justify-content: flex-end; } | |
| .modal-content { | |
| padding: 32px; | |
| } | |
| .close-modal { | |
| top: 15px; | |
| right: 15px; | |
| } | |
| .qr-preview { | |
| width: 200px; | |
| height: 200px; | |
| margin: 0 auto 24px; | |
| } | |
| .cart-item { | |
| flex-direction: row; | |
| padding: 16px 0; | |
| gap: 0; | |
| } | |
| .cart-total { | |
| font-size: 24px; | |
| margin: 24px 0; | |
| text-align: right; | |
| } | |
| .intro-logo { | |
| font-size: 48px; | |
| flex-direction: row; | |
| gap: 15px; | |
| } | |
| .toast { | |
| bottom: 30px; | |
| right: 30px; | |
| left: auto; | |
| max-width: none; | |
| } | |
| .progress-tracker { | |
| margin: 20px 0; | |
| flex-wrap: nowrap; | |
| } | |
| .progress-step { | |
| min-width: auto; | |
| margin-bottom: 0; | |
| } | |
| .step-indicator { | |
| width: 30px; | |
| height: 30px; | |
| margin-bottom: 8px; | |
| font-size: 16px; | |
| } | |
| .step-label { | |
| font-size: 12px; | |
| } | |
| .login-form-buttons { | |
| flex-wrap: nowrap; | |
| } | |
| .nav-buttons { | |
| gap: 16px; | |
| } | |
| } | |
| @media (max-width: 767px) { | |
| .mobile-menu-btn { | |
| display: block; | |
| } | |
| .desktop-nav { | |
| display: none; | |
| } | |
| .nav-buttons { | |
| flex-direction: column; | |
| gap: 8px; | |
| width: 100%; | |
| } | |
| .nav-buttons a { | |
| width: 100%; | |
| text-align: center; | |
| } | |
| } | |
| @media (max-width: 480px) { | |
| .container { padding: 12px; } | |
| .card { | |
| padding: 20px; | |
| margin-bottom: 16px; | |
| } | |
| .role-card { | |
| padding: 20px; | |
| } | |
| .grid { | |
| grid-template-columns: 1fr; | |
| gap: 16px; | |
| margin-top: 16px; | |
| } | |
| .menu-item { | |
| padding: 16px; | |
| } | |
| .section-header { | |
| margin-bottom: 16px; | |
| } | |
| .vendor-item { | |
| padding: 16px; | |
| } | |
| .modal-content { | |
| padding: 20px; | |
| } | |
| .progress-step { | |
| min-width: 60px; | |
| } | |
| .step-indicator { | |
| width: 24px; | |
| height: 24px; | |
| font-size: 12px; | |
| } | |
| .step-label { | |
| font-size: 10px; | |
| } | |
| .login-form-buttons { | |
| flex-direction: column; | |
| } | |
| .login-form-buttons .btn { | |
| width: 100%; | |
| } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="intro"> | |
| <div class="intro-logo"><span>🍽️</span><h1>UniGrub</h1></div> | |
| <p>Campus Canteen Ordering System</p> | |
| </div> | |
| <div class="container"> | |
| <header> | |
| <div class="logo"><span>🍽️</span><h1>UniGrub</h1></div> | |
| <!-- NEW FEATURE MERGED: Mobile menu button --> | |
| <button class="mobile-menu-btn" id="mobile-menu-toggle">☰</button> | |
| <nav class="desktop-nav"> | |
| <ul> | |
| <li><a href="#" id="nav-home">Home</a></li> | |
| <li><a href="#" id="nav-back" style="display: none;">Back</a></li> | |
| <li class="nav-buttons"> | |
| <a href="#" id="nav-login">Login</a> | |
| <a href="#" id="nav-logout" style="display: none;">Logout</a> | |
| <!-- NEW FEATURE MERGED: Theme button --> | |
| <div class="theme-btn" id="theme-toggle">🎨</div> | |
| </li> | |
| </ul> | |
| </nav> | |
| </header> | |
| <!-- NEW FEATURE MERGED: Mobile navigation --> | |
| <div class="mobile-nav" id="mobile-nav"> | |
| <button class="close-mobile-nav" id="close-mobile-nav">✕</button> | |
| <ul> | |
| <li><a href="#" id="mobile-nav-home">Home</a></li> | |
| <li><a href="#" id="mobile-nav-back" style="display: none;">Back</a></li> | |
| <li><a href="#" id="mobile-nav-login">Login</a></li> | |
| <li><a href="#" id="mobile-nav-logout" style="display: none;">Logout</a></li> | |
| <li><a href="#" id="mobile-theme-toggle">Change Theme</a></li> | |
| </ul> | |
| </div> | |
| <main> | |
| <div id="home-page" class="page active"> | |
| <div class="card text-center"> | |
| <h2 class="section-title">Welcome to UniGrub</h2> | |
| <p class="section-subtitle">Your campus canteen ordering solution</p> | |
| <div class="role-selection"> | |
| <div class="card role-card" id="student-role"> | |
| <div style="font-size: 48px; margin-bottom: 24px;">👨🎓</div> | |
| <h3>Student</h3> | |
| <p>Browse menus and place orders</p> | |
| </div> | |
| <div class="card role-card" id="vendor-role"> | |
| <div style="font-size: 48px; margin-bottom: 24px;">🏪</div> | |
| <h3>Vendor</h3> | |
| <p>Manage your stall and orders</p> | |
| </div> | |
| <div class="card role-card" id="admin-role"> | |
| <div style="font-size: 48px; margin-bottom: 24px;">🧑💼</div> | |
| <h3>Admin</h3> | |
| <p>Manage vendors and system</p> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- STUDENT LOGIN --> | |
| <div id="student-login-page" class="page"> | |
| <div class="card"> | |
| <h2 class="section-title">Student Login</h2> | |
| <form id="student-login-form"> | |
| <div class="form-group"><label>Username</label><input type="text" id="student-username" required></div> | |
| <div class="form-group"><label>Password</label><input type="password" id="student-password" required></div> | |
| <div class="form-group"><label><input type="checkbox" id="student-remember" style="width: auto;"> Remember Me</label></div> | |
| <div class="login-form-buttons"> | |
| <button type="submit" class="btn">Login</button> | |
| <button type="button" class="btn btn-secondary" id="student-register">Register</button> | |
| </div> | |
| </form> | |
| </div> | |
| </div> | |
| <!-- STUDENT DASHBOARD --> | |
| <div id="student-dashboard" class="page"> | |
| <div class="card"> | |
| <h2 class="section-title">Welcome, <span id="student-name"></span>!</h2> | |
| <p class="section-subtitle">Choose a canteen to start ordering</p> | |
| <div class="role-selection"> | |
| <div class="card canteen-card" data-canteen="upper" style="text-align: center; cursor: pointer; width: 100%; max-width: 280px;"> | |
| <div style="font-size: 48px; margin-bottom: 24px;">🏫</div> | |
| <h3>Upper Campus</h3> | |
| </div> | |
| <div class="card canteen-card" data-canteen="lower" style="text-align: center; cursor: pointer; width: 100%; max-width: 280px;"> | |
| <div style="font-size: 48px; margin-bottom: 24px;">🏢</div> | |
| <h3>Lower Campus</h3> | |
| </div> | |
| <div class="card canteen-card" data-canteen="market" style="text-align: center; cursor: pointer; width: 100%; max-width: 280px;"> | |
| <div style="font-size: 48px; margin-bottom: 24px;">🏪</div> | |
| <h3>Market</h3> | |
| </div> | |
| </div> | |
| <div class="section-header" style="margin-top: 32px;"> | |
| <h3 class="section-title">Your Orders</h3> | |
| <button class="btn btn-small" id="view-all-orders">View All Orders</button> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- NEW FEATURE MERGED: Color Scheme Modal --> | |
| <div id="color-scheme-modal" class="modal"> | |
| <div class="modal-content"> | |
| <span class="close-modal">×</span> | |
| <h2 class="section-title">Choose Color Scheme</h2> | |
| <p>Select your preferred primary color:</p> | |
| <div class="color-scheme-selector"> | |
| <div class="color-option active" data-color="#4CAF50" style="background-color: #4CAF50;"></div> <!-- CHANGED TO GREEN --> | |
| <div class="color-option" data-color="#3D52A0" style="background-color: #3D52A0;"></div> | |
| <div class="color-option" data-color="#C41E3A" style="background-color: #C41E3A;"></div> | |
| <div class="color-option" data-color="#00563B" style="background-color: #00563B;"></div> | |
| <div class="color-option" data-color="#F5FEFD" style="background-color: #F5FEFD;"></div> | |
| </div> | |
| <button class="btn" id="save-color-scheme" style="margin-top: 20px;">Save Changes</button> | |
| </div> | |
| </div> | |
| <!-- STUDENT ORDERS PAGE --> | |
| <div id="student-orders-page" class="page"> | |
| <div class="card"> | |
| <div class="section-header"> | |
| <h2 class="section-title">Your Orders</h2> | |
| <button class="btn btn-small" id="back-to-student-dashboard">Back to Dashboard</button> | |
| </div> | |
| <div id="student-orders-container"></div> | |
| </div> | |
| </div> | |
| <!-- CANTEEN STALLS --> | |
| <div id="canteen-stalls-page" class="page"> | |
| <div class="card"> | |
| <div class="section-header"> | |
| <h2 class="section-title" id="canteen-title">Stalls</h2> | |
| <button class="btn btn-small" id="back-to-student-dashboard-from-stalls">Back</button> | |
| </div> | |
| <div class="grid" id="stalls-container"></div> | |
| </div> | |
| </div> | |
| <!-- STALL MENU --> | |
| <div id="stall-menu-page" class="page"> | |
| <div class="card"> | |
| <div class="section-header"> | |
| <h2 class="section-title" id="stall-menu-title">Menu</h2> | |
| <div style="display: flex; gap: 10px; flex-wrap: wrap;"> | |
| <button class="btn btn-small btn-secondary" id="view-cart-btn">Cart (<span id="cart-count">0</span>)</button> | |
| <button class="btn btn-small" id="back-to-stalls">Back</button> | |
| </div> | |
| </div> | |
| <div class="grid" id="stall-menu-container"></div> | |
| </div> | |
| </div> | |
| <!-- CART --> | |
| <div id="student-cart-page" class="page"> | |
| <div class="card"> | |
| <div class="section-header"> | |
| <h2 class="section-title">Your Cart</h2> | |
| <div style="display: flex; gap: 10px; flex-wrap: wrap;"> | |
| <button class="btn btn-small" id="back-to-menu">Continue Shopping</button> | |
| <button class="btn btn-small" id="back-to-stalls-from-cart">Back to Stalls</button> | |
| </div> | |
| </div> | |
| <div id="cart-items-container"></div> | |
| <div class="cart-total" id="cart-total">Total: ₱0.00</div> | |
| <button class="btn" id="checkout-btn">Proceed to Checkout</button> | |
| </div> | |
| </div> | |
| <!-- CHECKOUT --> | |
| <div id="checkout-page" class="page"> | |
| <div class="card"> | |
| <div class="section-header"> | |
| <h2 class="section-title">Checkout</h2> | |
| <button class="btn btn-small" id="back-to-cart">Back to Cart</button> | |
| </div> | |
| <div id="checkout-summary"></div> | |
| <div class="form-group"><label>Order Type</label><select id="order-type"><option value="pickup">Pick Up</option><option value="delivery">Delivery</option></select></div> | |
| <div id="delivery-details" style="display: none;"> | |
| <div class="form-group"><label>Name</label><input type="text" id="delivery-name"></div> | |
| <div class="form-group"><label>Location</label><input type="text" id="delivery-location"></div> | |
| <div class="form-group"><label>Contact</label><input type="tel" id="delivery-contact" placeholder="09XXXXXXXXX"></div> | |
| </div> | |
| <div class="form-group"><label>Payment</label><select id="payment-method"><option value="cash">Cash</option><option value="gcash">GCash</option></select></div> | |
| <div id="gcash-details" style="display: none; text-align: center;"> | |
| <h3>Scan to Pay</h3> | |
| <div class="qr-preview" id="vendor-qr-preview"><span>QR Code will appear here</span></div> | |
| </div> | |
| <button class="btn" id="place-order-btn">Place Order</button> | |
| </div> | |
| </div> | |
| <!-- VENDOR LOGIN --> | |
| <div id="vendor-login-page" class="page"> | |
| <div class="card"> | |
| <h2 class="section-title">Vendor Login</h2> | |
| <form id="vendor-login-form"> | |
| <div class="form-group"><label>Username</label><input type="text" id="vendor-username" required></div> | |
| <div class="form-group"><label>Password</label><input type="password" id="vendor-password" required></div> | |
| <button type="submit" class="btn">Login</button> | |
| </form> | |
| </div> | |
| </div> | |
| <!-- VENDOR DASHBOARD --> | |
| <div id="vendor-dashboard" class="page"> | |
| <div class="card"> | |
| <div style="display: flex; align-items: center; gap: 16px; margin-bottom: 24px; flex-wrap: wrap;"> | |
| <img id="vendor-dash-logo" class="logo-img" style="display: none;"> | |
| <div id="vendor-dash-logo-placeholder" class="stall-logo-placeholder" style="display: none; margin: 0; width: 50px; height: 50px; font-size: 24px;"></div> | |
| <div> | |
| <h2 class="section-title" style="margin: 0;">Vendor Dashboard</h2> | |
| <p class="section-subtitle" style="margin: 0;">Welcome, <span id="vendor-stall-name"></span></p> | |
| </div> | |
| </div> | |
| <div class="grid"> | |
| <div class="card"><h3>Stall Profile</h3><button class="btn" id="manage-profile">Manage</button></div> | |
| <div class="card"><h3>Menu</h3><button class="btn" id="manage-menu">Manage</button></div> | |
| <div class="card"><h3>GCash QR</h3><button class="btn" id="manage-qr">Upload</button></div> | |
| <div class="card"><h3>Orders</h3><button class="btn" id="view-orders">View</button></div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- VENDOR PROFILE MANAGEMENT --> | |
| <div id="vendor-profile-page" class="page"> | |
| <div class="card"> | |
| <div class="section-header"> | |
| <h2 class="section-title">Stall Profile</h2> | |
| <button class="btn btn-small" id="back-to-vendor-dashboard-profile">Back</button> | |
| </div> | |
| <div style="text-align: center; margin-bottom: 24px;"> | |
| <img id="profile-logo-preview" class="stall-logo-square" style="display: none;"> | |
| <div id="profile-logo-placeholder" class="stall-logo-placeholder-square"></div> | |
| </div> | |
| <div class="form-group"> | |
| <label>Stall Logo (Square image recommended)</label> | |
| <input type="file" id="logo-upload" accept="image/*"> | |
| </div> | |
| <button class="btn" id="save-logo">Save Logo</button> | |
| </div> | |
| </div> | |
| <!-- VENDOR MENU MANAGEMENT --> | |
| <div id="vendor-menu-page" class="page"> | |
| <div class="card"> | |
| <div class="section-header"> | |
| <h2 class="section-title">Menu Management</h2> | |
| <button class="btn btn-small" id="back-to-vendor-dashboard">Back</button> | |
| </div> | |
| <button class="btn" id="add-menu-item-btn">Add Item</button> | |
| <div class="grid" id="vendor-menu-container"></div> | |
| </div> | |
| </div> | |
| <!-- VENDOR ORDERS --> | |
| <div id="vendor-orders-page" class="page"> | |
| <div class="card"> | |
| <div class="section-header"> | |
| <h2 class="section-title">Orders</h2> | |
| <button class="btn btn-small" id="back-to-vendor-dashboard-orders">Back</button> | |
| </div> | |
| <div id="vendor-orders-container"></div> | |
| </div> | |
| </div> | |
| <!-- VENDOR QR --> | |
| <div id="vendor-qr-page" class="page"> | |
| <div class="card"> | |
| <div class="section-header"> | |
| <h2 class="section-title">GCash QR</h2> | |
| <button class="btn btn-small" id="back-to-vendor-dashboard-qr">Back</button> | |
| </div> | |
| <div style="text-align: center;"> | |
| <div class="qr-preview" id="current-qr-preview"><span>No QR uploaded</span></div> | |
| <div class="form-group"><label>Upload QR</label><input type="file" id="qr-upload" accept="image/*"></div> | |
| <button class="btn" id="save-qr">Save</button> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- ADMIN LOGIN --> | |
| <div id="admin-login-page" class="page"> | |
| <div class="card"> | |
| <h2 class="section-title">Admin Login</h2> | |
| <form id="admin-login-form"> | |
| <div class="form-group"><label>Username</label><input type="text" id="admin-username" required></div> | |
| <div class="form-group"><label>Password</label><input type="password" id="admin-password" required></div> | |
| <button type="submit" class="btn">Login</button> | |
| </form> | |
| </div> | |
| </div> | |
| <!-- ADMIN DASHBOARD --> | |
| <div id="admin-dashboard" class="page"> | |
| <div class="card"> | |
| <h2 class="section-title">Admin Dashboard</h2> | |
| <div class="grid"> | |
| <div class="card"><h3>Vendors</h3><button class="btn" id="manage-vendors">Manage</button></div> | |
| <div class="card"><h3>All Menus</h3><button class="btn" id="view-all-menus">View</button></div> | |
| <div class="card"><h3>Settings</h3><button class="btn" id="system-settings">Configure</button></div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- VENDOR MANAGEMENT --> | |
| <div id="vendor-management-page" class="page"> | |
| <div class="card"> | |
| <div class="section-header"> | |
| <h2 class="section-title">Vendor Management</h2> | |
| <div style="display: flex; gap: 10px; flex-wrap: wrap;"> | |
| <button class="btn btn-small" id="back-to-admin-dashboard">Back</button> | |
| <button class="btn btn-small" id="add-vendor-btn">Add Vendor</button> | |
| </div> | |
| </div> | |
| <div id="vendor-list"></div> | |
| </div> | |
| </div> | |
| <!-- ALL MENUS --> | |
| <div id="view-all-menus-page" class="page"> | |
| <div class="card"> | |
| <div class="section-header"> | |
| <h2 class="section-title">All Menus</h2> | |
| <button class="btn btn-small" id="back-to-admin-dashboard-menus">Back</button> | |
| </div> | |
| <div id="all-menus-container"></div> | |
| </div> | |
| </div> | |
| <!-- SYSTEM SETTINGS --> | |
| <div id="system-settings-page" class="page"> | |
| <div class="card"> | |
| <div class="section-header"> | |
| <h2 class="section-title">System Settings</h2> | |
| <button class="btn btn-small" id="back-to-admin-dashboard-settings">Back</button> | |
| </div> | |
| <div class="form-group"><label>System Name</label><input type="text" id="system-name" value="UniGrub"></div> | |
| <div class="form-group"><label>Order Timeout (min)</label><input type="number" id="order-timeout" value="30"></div> | |
| <div class="form-group"> | |
| <label>Delete Vendor Behavior</label> | |
| <select id="delete-vendor-behavior"> | |
| <option value="preserve">Preserve Orders</option> | |
| <option value="remove">Remove Orders</option> | |
| </select> | |
| </div> | |
| <button class="btn" id="save-settings">Save Settings</button> | |
| </div> | |
| </div> | |
| </main> | |
| </div> | |
| <!-- MODALS --> | |
| <div id="order-success-modal" class="modal"> | |
| <div class="modal-content" style="text-align: center;"> | |
| <span class="close-modal">×</span> | |
| <div style="font-size: 64px; color: var(--success); margin-bottom: 24px;">✓</div> | |
| <h2 style="color: var(--success); margin-bottom: 16px;">Order Placed!</h2> | |
| <p style="margin-bottom: 32px;">Your order is being processed.</p> | |
| <button class="btn" id="close-success-modal">Close</button> | |
| </div> | |
| </div> | |
| <div id="add-vendor-modal" class="modal"> | |
| <div class="modal-content"> | |
| <span class="close-modal">×</span> | |
| <h2 class="section-title">Add Vendor</h2> | |
| <form id="add-vendor-form"> | |
| <div class="form-group"><label>Stall Name</label><input type="text" id="new-vendor-stall-name" required></div> | |
| <div class="form-group"><label>Username (lowercase)</label><input type="text" id="new-vendor-username" required></div> | |
| <div class="form-group"><label>Password (lowercase)</label><input type="password" id="new-vendor-password" required></div> | |
| <div class="form-group"><label>Canteen</label><select id="new-vendor-canteen"><option value="upper">Upper</option><option value="lower">Lower</option><option value="market">Market</option></select></div> | |
| <button type="submit" class="btn">Add</button> | |
| </form> | |
| </div> | |
| </div> | |
| <div id="edit-vendor-modal" class="modal"> | |
| <div class="modal-content"> | |
| <span class="close-modal">×</span> | |
| <h2 class="section-title">Edit Vendor</h2> | |
| <form id="edit-vendor-form"> | |
| <input type="hidden" id="edit-vendor-id"> | |
| <div class="form-group"><label>Stall Name</label><input type="text" id="edit-vendor-stall-name" required></div> | |
| <div class="form-group"><label>Username (lowercase)</label><input type="text" id="edit-vendor-username" required></div> | |
| <div class="form-group"><label>Password (lowercase)</label><input type="password" id="edit-vendor-password" required></div> | |
| <div class="form-group"><label>Canteen</label><select id="edit-vendor-canteen"><option value="upper">Upper</option><option value="lower">Lower</option><option value="market">Market</option></select></div> | |
| <button type="submit" class="btn">Update</button> | |
| </form> | |
| </div> | |
| </div> | |
| <div id="add-menu-item-modal" class="modal"> | |
| <div class="modal-content"> | |
| <span class="close-modal">×</span> | |
| <h2 class="section-title">Add Menu Item</h2> | |
| <form id="add-menu-item-form"> | |
| <div class="form-group"><label>Name</label><input type="text" id="item-name" required></div> | |
| <div class="form-group"><label>Description</label><textarea id="item-description"></textarea></div> | |
| <div class="form-group"><label>Flavors (comma separated, optional)</label><input type="text" id="item-flavors" placeholder="e.g., Original, Spicy, Cheese"></div> | |
| <div class="form-group"> | |
| <label>Size Prices (format: Size:Price, comma separated)</label> | |
| <input type="text" id="item-size-prices" placeholder="e.g., Small:50, Medium:70, Large:90" required> | |
| <small style="color: var(--text-secondary); display: block; margin-top: 8px;">Example: Small:50, Medium:70, Large:90</small> | |
| </div> | |
| <div class="form-group"><label>Image</label><input type="file" id="item-image" accept="image/*"></div> | |
| <button type="submit" class="btn">Add</button> | |
| </form> | |
| </div> | |
| </div> | |
| <div id="edit-menu-item-modal" class="modal"> | |
| <div class="modal-content"> | |
| <span class="close-modal">×</span> | |
| <h2 class="section-title">Edit Menu Item</h2> | |
| <form id="edit-menu-item-form"> | |
| <input type="hidden" id="edit-item-id"> | |
| <div class="form-group"><label>Name</label><input type="text" id="edit-item-name" required></div> | |
| <div class="form-group"><label>Description</label><textarea id="edit-item-description"></textarea></div> | |
| <div class="form-group"><label>Flavors (comma separated, optional)</label><input type="text" id="edit-item-flavors"></div> | |
| <div class="form-group"> | |
| <label>Size Prices (format: Size:Price, comma separated)</label> | |
| <input type="text" id="edit-item-size-prices" required> | |
| </div> | |
| <div class="form-group"><label>Image</label><input type="file" id="edit-item-image" accept="image/*"></div> | |
| <button type="submit" class="btn">Update</button> | |
| </form> | |
| </div> | |
| </div> | |
| <div id="item-detail-modal" class="modal item-detail-modal"> | |
| <div class="modal-content"> | |
| <span class="close-modal">×</span> | |
| <img id="detail-item-image" class="item-detail-image" style="display: none;"> | |
| <h2 id="detail-item-name" class="section-title"></h2> | |
| <p id="detail-item-description" style="color: var(--text-secondary); margin-bottom: 16px;"></p> | |
| <div class="form-group" id="flavor-group" style="display: none;"> | |
| <label>Select Flavor</label> | |
| <select id="detail-flavor-select"></select> | |
| </div> | |
| <div class="form-group"> | |
| <label>Select Size</label> | |
| <select id="detail-size-select"></select> | |
| </div> | |
| <div class="price-display" id="detail-price-display">₱0.00</div> | |
| <button class="btn" id="confirm-add-to-cart" style="width: 100%;">Add to Cart</button> | |
| </div> | |
| </div> | |
| <div id="cart-switch-modal" class="modal"> | |
| <div class="modal-content" style="text-align: center;"> | |
| <span class="close-modal">×</span> | |
| <h2 style="color: var(--primary);">Switch Vendor?</h2> | |
| <p>Your cart contains items from another vendor. You can only order from one vendor at a time.</p> | |
| <button class="btn" id="clear-cart-switch">Clear Cart & Switch</button> | |
| <button class="btn btn-secondary" id="keep-cart" style="margin-left: 10px;">Keep Current Cart</button> | |
| </div> | |
| </div> | |
| <script> | |
| // ========================================== | |
| // STORAGE & UTILITY HELPERS | |
| // ========================================== | |
| const APP_VERSION = '1.0'; | |
| let cart = []; | |
| let pendingSwitchItem = null; | |
| let navigationHistory = []; | |
| // NEW FEATURE MERGED: Color scheme definitions | |
| const colorSchemes = { | |
| '#4CAF50': { // CHANGED TO GREEN | |
| primary: '#4CAF50', | |
| primaryDark: '#388E3C', | |
| primaryGlass: 'rgba(76, 175, 80, 0.15)', | |
| primaryDots: 'rgba(76, 175, 80, 0.08)', | |
| primaryShadow: 'rgba(76, 175, 80, 0.2)', | |
| glassBorder: 'rgba(76, 175, 80, 0.3)' | |
| }, | |
| '#3D52A0': { | |
| primary: '#3D52A0', | |
| primaryDark: '#2A3A70', | |
| primaryGlass: 'rgba(61, 82, 160, 0.15)', | |
| primaryDots: 'rgba(61, 82, 160, 0.08)', | |
| primaryShadow: 'rgba(61, 82, 160, 0.2)', | |
| glassBorder: 'rgba(61, 82, 160, 0.3)' | |
| }, | |
| '#C41E3A': { | |
| primary: '#C41E3A', | |
| primaryDark: '#8A1529', | |
| primaryGlass: 'rgba(196, 30, 58, 0.15)', | |
| primaryDots: 'rgba(196, 30, 58, 0.08)', | |
| primaryShadow: 'rgba(196, 30, 58, 0.2)', | |
| glassBorder: 'rgba(196, 30, 58, 0.3)' | |
| }, | |
| '#00563B': { | |
| primary: '#00563B', | |
| primaryDark: '#003D29', | |
| primaryGlass: 'rgba(0, 86, 59, 0.15)', | |
| primaryDots: 'rgba(0, 86, 59, 0.08)', | |
| primaryShadow: 'rgba(0, 86, 59, 0.2)', | |
| glassBorder: 'rgba(0, 86, 59, 0.3)' | |
| }, | |
| '#F5FEFD': { | |
| primary: '#F5FEFD', | |
| primaryDark: '#D9E6E5', | |
| primaryGlass: 'rgba(245, 254, 253, 0.15)', | |
| primaryDots: 'rgba(245, 254, 253, 0.08)', | |
| primaryShadow: 'rgba(245, 254, 253, 0.2)', | |
| glassBorder: 'rgba(245, 254, 253, 0.3)' | |
| } | |
| }; | |
| function initData() { | |
| if (!localStorage.getItem('unigrub_init')) { | |
| const students = [ | |
| { id: 1, username: 'john', password: 'student123', name: 'John Doe' }, | |
| { id: 2, username: 'sarah', password: 'student123', name: 'Sarah Smith' } | |
| ]; | |
| const vendors = [ | |
| { | |
| id: 1, username: 'andrea', password: 'pizza123', stallName: "Andrea's Pizza", canteen: 'upper', | |
| logo: '', | |
| menu: [ | |
| { | |
| id: 1, | |
| name: 'Margherita Pizza', | |
| description: 'Classic pizza with tomato sauce, mozzarella, and basil', | |
| flavors: ['Original', 'Extra Cheese'], | |
| sizePrices: { Small: 100, Medium: 150, Large: 200 }, | |
| available: true, | |
| image: '' | |
| }, | |
| { | |
| id: 2, | |
| name: 'Pepperoni Pizza', | |
| description: 'Loaded with pepperoni and cheese', | |
| flavors: ['Classic', 'Spicy'], | |
| sizePrices: { Small: 120, Medium: 170, Large: 220 }, | |
| available: true, | |
| image: '' | |
| } | |
| ], qrCode: '', orders: [] | |
| }, | |
| { | |
| id: 2, username: 'donmachiato', password: 'coffee123', stallName: 'Don Machiattos', canteen: 'upper', | |
| logo: '', | |
| menu: [ | |
| { | |
| id: 1, | |
| name: 'Espresso', | |
| description: 'Rich and bold espresso shot', | |
| flavors: [], | |
| sizePrices: { Small: 60, Medium: 80, Large: 100 }, | |
| available: true, | |
| image: '' | |
| }, | |
| { | |
| id: 2, | |
| name: 'Cappuccino', | |
| description: 'Creamy cappuccino with foam art', | |
| flavors: ['Regular', 'Vanilla', 'Caramel'], | |
| sizePrices: { Small: 80, Medium: 100, Large: 120 }, | |
| available: true, | |
| image: '' | |
| } | |
| ], qrCode: '', orders: [] | |
| }, | |
| { | |
| id: 3, username: 'carabao', password: 'milk123', stallName: 'Carabao Milk', canteen: 'lower', | |
| logo: '', | |
| menu: [ | |
| { | |
| id: 1, | |
| name: 'Fresh Milk', | |
| description: 'Pure and fresh carabao milk', | |
| flavors: ['Plain', 'Chocolate', 'Strawberry'], | |
| sizePrices: { Small: 40, Medium: 50, Large: 60 }, | |
| available: true, | |
| image: '' | |
| } | |
| ], qrCode: '', orders: [] | |
| } | |
| ]; | |
| localStorage.setItem('students', JSON.stringify(students)); | |
| localStorage.setItem('vendors', JSON.stringify(vendors)); | |
| localStorage.setItem('orders', JSON.stringify([])); | |
| localStorage.setItem('systemSettings', JSON.stringify({ | |
| systemName: 'UniGrub', | |
| orderTimeout: 30, | |
| deleteVendorBehavior: 'preserve' | |
| })); | |
| localStorage.setItem('unigrub_init', 'true'); | |
| localStorage.setItem('app_version', APP_VERSION); | |
| } | |
| cart = JSON.parse(localStorage.getItem('currentCart') || '[]'); | |
| navigationHistory = JSON.parse(localStorage.getItem('navigationHistory') || '[]'); | |
| // NEW FEATURE MERGED: Initialize color scheme | |
| const savedColor = localStorage.getItem('unigrub_color') || '#4CAF50'; // CHANGED TO GREEN | |
| applyColorScheme(savedColor); | |
| } | |
| function getData(key) { | |
| try { | |
| const data = localStorage.getItem(key); | |
| return data ? JSON.parse(data) : null; | |
| } catch (e) { | |
| console.error('Error reading from localStorage:', e); | |
| return null; | |
| } | |
| } | |
| function setData(key, value) { | |
| try { | |
| localStorage.setItem(key, JSON.stringify(value)); | |
| } catch (e) { | |
| if (e.name === 'QuotaExceededError') { | |
| showToast('Storage limit exceeded. Please clear some data.', 'error'); | |
| } | |
| console.error('Error writing to localStorage:', e); | |
| } | |
| } | |
| function showToast(message, type = 'info') { | |
| const toast = document.createElement('div'); | |
| toast.className = `toast ${type}`; | |
| toast.textContent = message; | |
| document.body.appendChild(toast); | |
| setTimeout(() => toast.remove(), 3000); | |
| } | |
| function showPage(id) { | |
| // Add current page to navigation history | |
| const currentActive = document.querySelector('.page.active'); | |
| if (currentActive) { | |
| navigationHistory.push(currentActive.id); | |
| // Keep only last 10 pages in history | |
| if (navigationHistory.length > 10) { | |
| navigationHistory.shift(); | |
| } | |
| setData('navigationHistory', navigationHistory); | |
| } | |
| document.querySelectorAll('.page').forEach(p => p.classList.remove('active')); | |
| const page = document.getElementById(id); | |
| if (page) page.classList.add('active'); | |
| // Update back button visibility | |
| updateBackButton(); | |
| // NEW FEATURE MERGED: Close mobile nav if open | |
| closeMobileNav(); | |
| } | |
| function goBack() { | |
| if (navigationHistory.length > 0) { | |
| const previousPage = navigationHistory.pop(); | |
| setData('navigationHistory', navigationHistory); | |
| if (previousPage) { | |
| document.querySelectorAll('.page').forEach(p => p.classList.remove('active')); | |
| const page = document.getElementById(previousPage); | |
| if (page) { | |
| page.classList.add('active'); | |
| updateBackButton(); | |
| return; | |
| } | |
| } | |
| } | |
| // If no history, go to appropriate dashboard | |
| if (getData('currentStudent')) { | |
| showPage('student-dashboard'); | |
| } else if (getData('currentVendor')) { | |
| showPage('vendor-dashboard'); | |
| } else if (getData('currentAdmin')) { | |
| showPage('admin-dashboard'); | |
| } else { | |
| showPage('home-page'); | |
| } | |
| } | |
| function updateBackButton() { | |
| const backButton = document.getElementById('nav-back'); | |
| const mobileBackButton = document.getElementById('mobile-nav-back'); | |
| // Show back button if there's navigation history or if user is logged in | |
| const shouldShow = navigationHistory.length > 0 || getData('currentStudent') || getData('currentVendor') || getData('currentAdmin'); | |
| if (backButton) backButton.style.display = shouldShow ? 'block' : 'none'; | |
| if (mobileBackButton) mobileBackButton.style.display = shouldShow ? 'block' : 'none'; | |
| } | |
| function showModal(id) { | |
| const modal = document.getElementById(id); | |
| if (modal) modal.style.display = 'flex'; | |
| } | |
| function hideModal(id) { | |
| const modal = document.getElementById(id); | |
| if (modal) modal.style.display = 'none'; | |
| } | |
| // NEW FEATURE MERGED: Mobile navigation functions | |
| function toggleMobileNav() { | |
| const mobileNav = document.getElementById('mobile-nav'); | |
| if (mobileNav) { | |
| mobileNav.classList.toggle('active'); | |
| } | |
| } | |
| function closeMobileNav() { | |
| const mobileNav = document.getElementById('mobile-nav'); | |
| if (mobileNav) { | |
| mobileNav.classList.remove('active'); | |
| } | |
| } | |
| // NEW FEATURE MERGED: Color scheme functions | |
| function applyColorScheme(color) { | |
| const scheme = colorSchemes[color]; | |
| if (!scheme) return; | |
| const root = document.documentElement; | |
| root.style.setProperty('--primary', scheme.primary); | |
| root.style.setProperty('--primary-dark', scheme.primaryDark); | |
| root.style.setProperty('--primary-glass', scheme.primaryGlass); | |
| root.style.setProperty('--primary-dots', scheme.primaryDots); | |
| root.style.setProperty('--primary-shadow', scheme.primaryShadow); | |
| root.style.setProperty('--glass-border', scheme.glassBorder); | |
| // Update select background image | |
| const select = document.querySelector('select'); | |
| if (select) { | |
| select.style.backgroundImage = `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%234CAF50' d='M6 9L1 4h10z'/%3E%3C/svg%3E")`; // CHANGED TO GREEN | |
| } | |
| // Update active color option | |
| document.querySelectorAll('.color-option').forEach(option => { | |
| option.classList.remove('active'); | |
| if (option.dataset.color === color) { | |
| option.classList.add('active'); | |
| } | |
| }); | |
| localStorage.setItem('unigrub_color', color); | |
| } | |
| function showColorSchemeModal() { | |
| const savedColor = localStorage.getItem('unigrub_color') || '#4CAF50'; // CHANGED TO GREEN | |
| document.querySelectorAll('.color-option').forEach(option => { | |
| option.classList.remove('active'); | |
| if (option.dataset.color === savedColor) { | |
| option.classList.add('active'); | |
| } | |
| }); | |
| showModal('color-scheme-modal'); | |
| } | |
| function sanitizeHTML(str) { | |
| const div = document.createElement('div'); | |
| div.textContent = str; | |
| return div.innerHTML; | |
| } | |
| function validatePhone(phone) { | |
| const pattern = /^09\d{9}$/; | |
| return pattern.test(phone); | |
| } | |
| function compressImage(file, maxSizeMB = 1, maxWidth = 800) { | |
| return new Promise((resolve, reject) => { | |
| if (file.size / 1024 / 1024 > maxSizeMB) { | |
| const img = new Image(); | |
| const reader = new FileReader(); | |
| reader.onload = (e) => { | |
| img.onload = () => { | |
| const ratio = Math.min(1, maxWidth / img.width); | |
| const canvas = document.createElement('canvas'); | |
| canvas.width = Math.round(img.width * ratio); | |
| canvas.height = Math.round(img.height * ratio); | |
| const ctx = canvas.getContext('2d'); | |
| ctx.drawImage(img, 0, 0, canvas.width, canvas.height); | |
| const dataUrl = canvas.toDataURL('image/jpeg', 0.85); | |
| if ((dataUrl.length * 3 / 4) / 1024 / 1024 > maxSizeMB) { | |
| reject(new Error(`Image still exceeds ${maxSizeMB}MB after compression.`)); | |
| } else { | |
| resolve(dataUrl); | |
| } | |
| }; | |
| img.onerror = () => reject(new Error('Could not process image')); | |
| img.src = e.target.result; | |
| }; | |
| reader.onerror = () => reject(new Error('Failed to read file')); | |
| reader.readAsDataURL(file); | |
| } else { | |
| const reader = new FileReader(); | |
| reader.onload = (e) => resolve(e.target.result); | |
| reader.onerror = () => reject(new Error('Failed to read file')); | |
| reader.readAsDataURL(file); | |
| } | |
| }); | |
| } | |
| function getFreshVendor() { | |
| const current = getData('currentVendor'); | |
| if (!current) return null; | |
| const vendors = getData('vendors') || []; | |
| return vendors.find(v => v.id === current.id) || null; | |
| } | |
| function cartHasDifferentVendor(vendorId) { | |
| return cart.length > 0 && cart.some(i => i.vendorId !== vendorId); | |
| } | |
| function getStallInitials(name) { | |
| return name.split(' ').map(w => w[0]).join('').toUpperCase().slice(0, 2); | |
| } | |
| function parseSizePrices(input) { | |
| const result = {}; | |
| const pairs = input.split(',').map(p => p.trim()); | |
| pairs.forEach(pair => { | |
| const [size, price] = pair.split(':').map(s => s.trim()); | |
| if (size && price && !isNaN(price)) { | |
| result[size] = parseFloat(price); | |
| } | |
| }); | |
| return result; | |
| } | |
| function sizePricesToString(sizePrices) { | |
| return Object.entries(sizePrices).map(([size, price]) => `${size}:${price}`).join(', '); | |
| } | |
| // ========================================== | |
| // AUTHENTICATION | |
| // ========================================== | |
| function loginStudent(u, p) { | |
| const students = getData('students') || []; | |
| const student = students.find(s => s.username === u && s.password === p); | |
| if (student) { | |
| setData('currentStudent', student); | |
| const nameEl = document.getElementById('student-name'); | |
| if (nameEl) nameEl.textContent = sanitizeHTML(student.name); | |
| showPage('student-dashboard'); | |
| updateNav(true); | |
| showToast('Login successful!', 'success'); | |
| return true; | |
| } | |
| return false; | |
| } | |
| function loginVendor(u, p) { | |
| const vendors = getData('vendors') || []; | |
| const vendor = vendors.find(v => v.username === u.toLowerCase() && v.password === p.toLowerCase()); | |
| if (vendor) { | |
| setData('currentVendor', vendor); | |
| updateVendorDashboard(vendor); | |
| showPage('vendor-dashboard'); | |
| updateNav(true); | |
| showToast('Login successful!', 'success'); | |
| return true; | |
| } | |
| return false; | |
| } | |
| function loginAdmin(u, p) { | |
| if (u === 'admin' && p === 'admin123') { | |
| setData('currentAdmin', true); | |
| showPage('admin-dashboard'); | |
| updateNav(true); | |
| showToast('Admin access granted', 'success'); | |
| return true; | |
| } | |
| return false; | |
| } | |
| function logout() { | |
| localStorage.removeItem('currentStudent'); | |
| localStorage.removeItem('currentVendor'); | |
| localStorage.removeItem('currentAdmin'); | |
| localStorage.removeItem('currentCart'); | |
| localStorage.removeItem('navigationHistory'); | |
| cart = []; | |
| navigationHistory = []; | |
| showPage('home-page'); | |
| updateNav(false); | |
| updateBackButton(); | |
| showToast('Logged out successfully', 'info'); | |
| } | |
| function updateNav(loggedIn) { | |
| const loginNav = document.getElementById('nav-login'); | |
| const logoutNav = document.getElementById('nav-logout'); | |
| const mobileLoginNav = document.getElementById('mobile-nav-login'); | |
| const mobileLogoutNav = document.getElementById('mobile-nav-logout'); | |
| if (loginNav) loginNav.style.display = loggedIn ? 'none' : 'block'; | |
| if (logoutNav) logoutNav.style.display = loggedIn ? 'block' : 'none'; | |
| if (mobileLoginNav) mobileLoginNav.style.display = loggedIn ? 'none' : 'block'; | |
| if (mobileLogoutNav) mobileLogoutNav.style.display = loggedIn ? 'block' : 'none'; | |
| updateBackButton(); | |
| } | |
| function updateVendorDashboard(vendor) { | |
| const nameEl = document.getElementById('vendor-stall-name'); | |
| if (nameEl) nameEl.textContent = sanitizeHTML(vendor.stallName); | |
| const logoImg = document.getElementById('vendor-dash-logo'); | |
| const logoPlaceholder = document.getElementById('vendor-dash-logo-placeholder'); | |
| if (logoImg && logoPlaceholder) { | |
| if (vendor.logo) { | |
| logoImg.src = vendor.logo; | |
| logoImg.style.display = 'block'; | |
| logoPlaceholder.style.display = 'none'; | |
| } else { | |
| logoPlaceholder.textContent = getStallInitials(vendor.stallName); | |
| logoPlaceholder.style.display = 'flex'; | |
| logoImg.style.display = 'none'; | |
| } | |
| } | |
| } | |
| // ========================================== | |
| // STUDENT FUNCTIONS | |
| // ========================================== | |
| function loadStalls(canteen) { | |
| const vendors = (getData('vendors') || []).filter(v => v.canteen === canteen); | |
| const container = document.getElementById('stalls-container'); | |
| const titles = { upper: 'Upper Campus', lower: 'Lower Campus', market: 'Market' }; | |
| const titleEl = document.getElementById('canteen-title'); | |
| if (titleEl) titleEl.textContent = titles[canteen] + ' Stalls'; | |
| if (container) container.innerHTML = ''; | |
| vendors.forEach(v => { | |
| const card = document.createElement('div'); | |
| card.className = 'card'; | |
| card.style.cssText = 'text-align:center;cursor:pointer;'; | |
| let logoHTML = ''; | |
| if (v.logo) { | |
| logoHTML = `<img src="${v.logo}" class="stall-logo">`; | |
| } else { | |
| logoHTML = `<div class="stall-logo-placeholder">${getStallInitials(v.stallName)}</div>`; | |
| } | |
| card.innerHTML = `${logoHTML}<h3>${sanitizeHTML(v.stallName)}</h3>`; | |
| card.onclick = () => loadMenu(v.id); | |
| container.appendChild(card); | |
| }); | |
| showPage('canteen-stalls-page'); | |
| } | |
| function loadMenu(vendorId) { | |
| const vendors = getData('vendors') || []; | |
| const vendor = vendors.find(v => v.id === vendorId); | |
| if (!vendor) return; | |
| setData('currentVendorId', vendorId); | |
| const titleEl = document.getElementById('stall-menu-title'); | |
| if (titleEl) titleEl.textContent = sanitizeHTML(vendor.stallName); | |
| const container = document.getElementById('stall-menu-container'); | |
| if (container) container.innerHTML = ''; | |
| vendor.menu.filter(i => i.available).forEach(item => { | |
| const div = document.createElement('div'); | |
| div.className = 'card menu-item'; | |
| const img = item.image | |
| ? `<img src="${item.image}">` | |
| : '<div style="width:96px;height:120px;background:rgba(76,175,80,0.2);display:flex;align-items:center;justify-content:center;border-radius:8px;font-size:24px;">🍕</div>'; // CHANGED TO GREEN | |
| const sizes = Object.keys(item.sizePrices || {}); | |
| const priceRange = sizes.length > 0 | |
| ? `₱${Math.min(...Object.values(item.sizePrices))} - ₱${Math.max(...Object.values(item.sizePrices))}` | |
| : '₱0'; | |
| div.innerHTML = ` | |
| <div class="menu-item-image">${img}</div> | |
| <div class="menu-item-info"> | |
| <div class="menu-item-name">${sanitizeHTML(item.name)}</div> | |
| <div class="menu-item-price">${priceRange}</div> | |
| <div style="color: var(--text-secondary); margin-bottom: 8px;">${sanitizeHTML(item.description || '')}</div> | |
| <div class="menu-item-actions"> | |
| <button class="btn btn-small" data-item-id="${item.id}">View Details</button> | |
| </div> | |
| </div> | |
| `; | |
| div.querySelector('button').addEventListener('click', () => { | |
| showItemDetailModal(item, vendor.stallName, vendor.id); | |
| }); | |
| container.appendChild(div); | |
| }); | |
| updateCartCount(); | |
| showPage('stall-menu-page'); | |
| } | |
| function showItemDetailModal(item, vendorName, vendorId) { | |
| const modal = document.getElementById('item-detail-modal'); | |
| const img = document.getElementById('detail-item-image'); | |
| const name = document.getElementById('detail-item-name'); | |
| const desc = document.getElementById('detail-item-description'); | |
| const flavorGroup = document.getElementById('flavor-group'); | |
| const flavorSelect = document.getElementById('detail-flavor-select'); | |
| const sizeSelect = document.getElementById('detail-size-select'); | |
| const priceDisplay = document.getElementById('detail-price-display'); | |
| if (name) name.textContent = item.name; | |
| if (desc) desc.textContent = item.description || ''; | |
| if (img) { | |
| if (item.image) { | |
| img.src = item.image; | |
| img.style.display = 'block'; | |
| } else { | |
| img.style.display = 'none'; | |
| } | |
| } | |
| // Setup flavors | |
| if (flavorGroup && flavorSelect) { | |
| if (item.flavors && item.flavors.length > 0) { | |
| flavorGroup.style.display = 'block'; | |
| flavorSelect.innerHTML = ''; | |
| item.flavors.forEach(flavor => { | |
| const option = document.createElement('option'); | |
| option.value = flavor; | |
| option.textContent = flavor; | |
| flavorSelect.appendChild(option); | |
| }); | |
| } else { | |
| flavorGroup.style.display = 'none'; | |
| } | |
| } | |
| // Setup sizes | |
| if (sizeSelect) { | |
| sizeSelect.innerHTML = ''; | |
| Object.entries(item.sizePrices || {}).forEach(([size, price]) => { | |
| const option = document.createElement('option'); | |
| option.value = size; | |
| option.textContent = `${size} - ₱${price}`; | |
| option.dataset.price = price; | |
| sizeSelect.appendChild(option); | |
| }); | |
| } | |
| // Update price on size change | |
| function updatePrice() { | |
| if (sizeSelect && priceDisplay) { | |
| const selectedOption = sizeSelect.options[sizeSelect.selectedIndex]; | |
| if (selectedOption) { | |
| priceDisplay.textContent = `₱${selectedOption.dataset.price}`; | |
| } | |
| } | |
| } | |
| if (sizeSelect) sizeSelect.onchange = updatePrice; | |
| updatePrice(); | |
| // Confirm button | |
| const confirmBtn = document.getElementById('confirm-add-to-cart'); | |
| if (confirmBtn) { | |
| confirmBtn.onclick = () => { | |
| const selectedFlavor = flavorSelect ? flavorSelect.value : null; | |
| const selectedSize = sizeSelect ? sizeSelect.value : ''; | |
| const selectedPrice = sizeSelect ? parseFloat(sizeSelect.options[sizeSelect.selectedIndex].dataset.price) : 0; | |
| addToCart({ | |
| ...item, | |
| selectedFlavor, | |
| selectedSize, | |
| price: selectedPrice | |
| }, vendorName, vendorId); | |
| hideModal('item-detail-modal'); | |
| }; | |
| } | |
| showModal('item-detail-modal'); | |
| } | |
| function addToCart(item, vendorName, vendorId) { | |
| if (cartHasDifferentVendor(vendorId)) { | |
| pendingSwitchItem = { item, vendorName, vendorId }; | |
| showModal('cart-switch-modal'); | |
| return; | |
| } | |
| const existing = cart.find(i => | |
| i.id === item.id && | |
| i.selectedFlavor === item.selectedFlavor && | |
| i.selectedSize === item.selectedSize && | |
| i.vendor === vendorName | |
| ); | |
| if (existing) { | |
| existing.quantity++; | |
| } else { | |
| cart.push({ | |
| ...item, | |
| vendor: vendorName, | |
| vendorId: vendorId, | |
| quantity: 1 | |
| }); | |
| } | |
| setData('currentCart', cart); | |
| updateCartCount(); | |
| showToast('Added to cart!', 'success'); | |
| } | |
| function updateCartCount() { | |
| const count = cart.reduce((sum, i) => sum + i.quantity, 0); | |
| const el = document.getElementById('cart-count'); | |
| if (el) el.textContent = count; | |
| } | |
| function loadCart() { | |
| const container = document.getElementById('cart-items-container'); | |
| const total = document.getElementById('cart-total'); | |
| if (container) container.innerHTML = ''; | |
| let sum = 0; | |
| cart.forEach((item, idx) => { | |
| const div = document.createElement('div'); | |
| div.className = 'cart-item'; | |
| const flavorText = item.selectedFlavor ? ` - ${item.selectedFlavor}` : ''; | |
| div.innerHTML = ` | |
| <div> | |
| <strong>${sanitizeHTML(item.name)}</strong>${flavorText} (${item.selectedSize})<br> | |
| <small>${sanitizeHTML(item.vendor)}</small> | |
| </div> | |
| <div style="text-align: right;"> | |
| ₱${item.price} × ${item.quantity} = ₱${(item.price * item.quantity).toFixed(2)} | |
| <br> | |
| <button class="btn btn-small btn-danger" onclick="removeFromCart(${idx})" style="margin-top: 8px;">Remove</button> | |
| </div> | |
| `; | |
| container.appendChild(div); | |
| sum += item.price * item.quantity; | |
| }); | |
| if (total) total.textContent = `Total: ₱${sum.toFixed(2)}`; | |
| } | |
| function removeFromCart(idx) { | |
| cart.splice(idx, 1); | |
| setData('currentCart', cart); | |
| loadCart(); | |
| updateCartCount(); | |
| } | |
| function loadCheckout() { | |
| const container = document.getElementById('checkout-summary'); | |
| if (container) { | |
| container.innerHTML = '<h3>Order Summary</h3>'; | |
| let total = 0; | |
| cart.forEach(item => { | |
| const div = document.createElement('div'); | |
| div.className = 'cart-item'; | |
| const flavorText = item.selectedFlavor ? ` - ${item.selectedFlavor}` : ''; | |
| div.innerHTML = ` | |
| <div>${sanitizeHTML(item.name)}${flavorText} (${item.selectedSize}) - ${sanitizeHTML(item.vendor)}</div> | |
| <div>₱${item.price} × ${item.quantity}</div> | |
| `; | |
| container.appendChild(div); | |
| total += item.price * item.quantity; | |
| }); | |
| const totalDiv = document.createElement('div'); | |
| totalDiv.className = 'cart-total'; | |
| totalDiv.textContent = `Total: ₱${total.toFixed(2)}`; | |
| container.appendChild(totalDiv); | |
| } | |
| if (cart.length > 0) { | |
| const vendorId = cart[0].vendorId; | |
| const vendors = getData('vendors') || []; | |
| const vendor = vendors.find(v => v.id == vendorId); | |
| const qrPreview = document.getElementById('vendor-qr-preview'); | |
| if (vendor && vendor.qrCode && qrPreview) { | |
| qrPreview.innerHTML = `<img src="${vendor.qrCode}">`; | |
| } | |
| } | |
| } | |
| function placeOrder() { | |
| const student = getData('currentStudent'); | |
| if (!student) { | |
| showToast('Please log in as a student before placing an order.', 'error'); | |
| showPage('student-login-page'); | |
| return; | |
| } | |
| if (cart.length === 0) { | |
| showToast('Cart is empty!', 'error'); | |
| return; | |
| } | |
| const orderType = document.getElementById('order-type').value; | |
| const paymentMethod = document.getElementById('payment-method').value; | |
| if (orderType === 'delivery') { | |
| const name = document.getElementById('delivery-name').value; | |
| const location = document.getElementById('delivery-location').value; | |
| const contact = document.getElementById('delivery-contact').value; | |
| if (!name || !location || !contact) { | |
| showToast('Please fill in all delivery details', 'error'); | |
| return; | |
| } | |
| if (!validatePhone(contact)) { | |
| showToast('Invalid phone number. Use format: 09XXXXXXXXX', 'error'); | |
| return; | |
| } | |
| } | |
| const orders = getData('orders') || []; | |
| const newOrder = { | |
| id: Date.now(), | |
| student: student, | |
| items: [...cart], | |
| orderType: orderType, | |
| paymentMethod: paymentMethod, | |
| status: 'pending', | |
| timestamp: new Date().toISOString(), | |
| total: cart.reduce((sum, item) => sum + (item.price * item.quantity), 0), | |
| vendorId: cart[0].vendorId | |
| }; | |
| if (orderType === 'delivery') { | |
| newOrder.deliveryDetails = { | |
| name: document.getElementById('delivery-name').value, | |
| location: document.getElementById('delivery-location').value, | |
| contact: document.getElementById('delivery-contact').value | |
| }; | |
| } | |
| orders.push(newOrder); | |
| setData('orders', orders); | |
| const vendors = getData('vendors') || []; | |
| const vendorIdx = vendors.findIndex(v => v.id === newOrder.vendorId); | |
| if (vendorIdx !== -1) { | |
| vendors[vendorIdx].orders = vendors[vendorIdx].orders || []; | |
| vendors[vendorIdx].orders.push(newOrder.id); | |
| setData('vendors', vendors); | |
| } | |
| cart = []; | |
| setData('currentCart', cart); | |
| updateCartCount(); | |
| showModal('order-success-modal'); | |
| } | |
| // Load student orders with progress tracker | |
| function loadStudentOrders() { | |
| const student = getData('currentStudent'); | |
| if (!student) return; | |
| const orders = getData('orders') || []; | |
| const studentOrders = orders.filter(o => o.student.id === student.id); | |
| const container = document.getElementById('student-orders-container'); | |
| if (container) { | |
| container.innerHTML = ''; | |
| if (studentOrders.length === 0) { | |
| container.innerHTML = '<p>No orders yet.</p>'; | |
| return; | |
| } | |
| // Sort by timestamp (newest first) | |
| studentOrders.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp)); | |
| studentOrders.forEach(order => { | |
| const card = document.createElement('div'); | |
| card.className = 'card'; | |
| card.style.marginBottom = '20px'; | |
| // Get vendor name | |
| const vendors = getData('vendors') || []; | |
| const vendor = vendors.find(v => v.id === order.vendorId); | |
| const vendorName = vendor ? vendor.stallName : 'Unknown Vendor'; | |
| // Create progress tracker | |
| const statusOrder = ['pending', 'preparing', 'ready', 'completed']; | |
| const currentStatusIndex = statusOrder.indexOf(order.status); | |
| let progressHTML = ` | |
| <div class="progress-tracker"> | |
| <div class="progress-bar" style="width: ${(currentStatusIndex / (statusOrder.length - 1)) * 100}%"></div> | |
| `; | |
| statusOrder.forEach((status, index) => { | |
| const stepClass = index < currentStatusIndex ? 'completed' : | |
| index === currentStatusIndex ? 'active' : ''; | |
| progressHTML += ` | |
| <div class="progress-step"> | |
| <div class="step-indicator ${stepClass}">${index + 1}</div> | |
| <div class="step-label ${stepClass}">${status.charAt(0).toUpperCase() + status.slice(1)}</div> | |
| </div> | |
| `; | |
| }); | |
| progressHTML += `</div>`; | |
| let itemsHTML = ''; | |
| order.items.forEach(i => { | |
| const flavorText = i.selectedFlavor ? ` - ${i.selectedFlavor}` : ''; | |
| itemsHTML += `<div>${sanitizeHTML(i.name)}${flavorText} (${i.selectedSize}) x ${i.quantity} - ₱${i.price * i.quantity}</div>`; | |
| }); | |
| const delivery = order.orderType === 'delivery' && order.deliveryDetails ? | |
| `<p><strong>Delivery:</strong> ${sanitizeHTML(order.deliveryDetails.name)}, ${sanitizeHTML(order.deliveryDetails.location)}, ${sanitizeHTML(order.deliveryDetails.contact)}</p>` : ''; | |
| card.innerHTML = ` | |
| <h4>Order #${order.id}</h4> | |
| <p><strong>Vendor:</strong> ${sanitizeHTML(vendorName)}</p> | |
| <p><strong>Type:</strong> ${order.orderType}</p> | |
| ${delivery} | |
| <p><strong>Payment:</strong> ${order.paymentMethod}</p> | |
| <p><strong>Total:</strong> ₱${order.total.toFixed(2)}</p> | |
| <p><strong>Time:</strong> ${new Date(order.timestamp).toLocaleString()}</p> | |
| <div style="margin:10px 0;"><strong>Items:</strong>${itemsHTML}</div> | |
| ${progressHTML} | |
| `; | |
| container.appendChild(card); | |
| }); | |
| } | |
| } | |
| // ========================================== | |
| // VENDOR FUNCTIONS | |
| // ========================================== | |
| function loadVendorMenu() { | |
| const vendor = getFreshVendor(); | |
| if (!vendor) return; | |
| setData('currentVendor', vendor); | |
| const container = document.getElementById('vendor-menu-container'); | |
| if (container) container.innerHTML = ''; | |
| vendor.menu.forEach(item => { | |
| const div = document.createElement('div'); | |
| div.className = 'card menu-item'; | |
| const img = item.image ? `<img src="${item.image}">` : '<div style="width:96px;height:120px;background:rgba(76,175,80,0.2);display:flex;align-items:center;justify-content:center;border-radius:8px;font-size:24px;">🍕</div>'; // CHANGED TO GREEN | |
| const priceRange = item.sizePrices | |
| ? `₱${Math.min(...Object.values(item.sizePrices))} - ₱${Math.max(...Object.values(item.sizePrices))}` | |
| : '₱0'; | |
| div.innerHTML = ` | |
| <div class="menu-item-image">${img}</div> | |
| <div class="menu-item-info"> | |
| <div class="menu-item-name">${sanitizeHTML(item.name)}</div> | |
| <div class="menu-item-price">${priceRange}</div> | |
| <div style="color: var(--text-secondary); font-size: 14px; margin-bottom: 8px;">${sanitizeHTML(item.description || '')}</div> | |
| <div class="menu-item-actions"> | |
| <button class="btn btn-small btn-secondary" data-action="edit" data-id="${item.id}">Edit</button> | |
| <button class="btn btn-small btn-danger" data-action="delete" data-id="${item.id}">Delete</button> | |
| <button class="btn btn-small ${item.available ? 'btn-success' : 'btn-secondary'}" data-action="toggle" data-id="${item.id}">${item.available ? 'Available' : 'Unavailable'}</button> | |
| </div> | |
| </div> | |
| `; | |
| div.querySelectorAll('button').forEach(btn => { | |
| btn.addEventListener('click', () => { | |
| const action = btn.dataset.action; | |
| const itemId = parseInt(btn.dataset.id); | |
| if (action === 'edit') editMenuItem(itemId); | |
| else if (action === 'delete') deleteMenuItem(itemId); | |
| else if (action === 'toggle') toggleAvailability(itemId); | |
| }); | |
| }); | |
| container.appendChild(div); | |
| }); | |
| } | |
| function editMenuItem(itemId) { | |
| const vendor = getData('currentVendor') || {}; | |
| const item = vendor.menu.find(i => i.id === itemId); | |
| if (item) { | |
| document.getElementById('edit-item-id').value = item.id; | |
| document.getElementById('edit-item-name').value = item.name; | |
| document.getElementById('edit-item-description').value = item.description || ''; | |
| document.getElementById('edit-item-flavors').value = (item.flavors || []).join(', '); | |
| document.getElementById('edit-item-size-prices').value = sizePricesToString(item.sizePrices || {}); | |
| showModal('edit-menu-item-modal'); | |
| } | |
| } | |
| function deleteMenuItem(itemId) { | |
| if (!confirm('Delete this item?')) return; | |
| const vendor = getFreshVendor(); | |
| if (vendor) { | |
| vendor.menu = vendor.menu.filter(i => i.id !== itemId); | |
| setData('currentVendor', vendor); | |
| updateVendorInStorage(vendor); | |
| loadVendorMenu(); | |
| showToast('Item deleted', 'info'); | |
| } | |
| } | |
| function toggleAvailability(itemId) { | |
| const vendor = getFreshVendor(); | |
| if (vendor) { | |
| const item = vendor.menu.find(i => i.id === itemId); | |
| if (item) { | |
| item.available = !item.available; | |
| setData('currentVendor', vendor); | |
| updateVendorInStorage(vendor); | |
| loadVendorMenu(); | |
| } | |
| } | |
| } | |
| function updateVendorInStorage(vendor) { | |
| const vendors = getData('vendors') || []; | |
| const idx = vendors.findIndex(v => v.id === vendor.id); | |
| if (idx !== -1) { | |
| vendors[idx] = vendor; | |
| setData('vendors', vendors); | |
| } | |
| } | |
| function loadCurrentQR() { | |
| const vendor = getFreshVendor(); | |
| if (!vendor) return; | |
| setData('currentVendor', vendor); | |
| const preview = document.getElementById('current-qr-preview'); | |
| if (preview) { | |
| if (vendor.qrCode) { | |
| preview.innerHTML = `<img src="${vendor.qrCode}">`; | |
| preview.dataset.qrCode = vendor.qrCode; | |
| } else { | |
| preview.innerHTML = '<span>No QR uploaded</span>'; | |
| } | |
| } | |
| } | |
| function loadVendorProfile() { | |
| const vendor = getFreshVendor(); | |
| if (!vendor) return; | |
| const logoPreview = document.getElementById('profile-logo-preview'); | |
| const logoPlaceholder = document.getElementById('profile-logo-placeholder'); | |
| if (logoPreview && logoPlaceholder) { | |
| if (vendor.logo) { | |
| logoPreview.src = vendor.logo; | |
| logoPreview.style.display = 'block'; | |
| logoPlaceholder.style.display = 'none'; | |
| } else { | |
| logoPlaceholder.textContent = getStallInitials(vendor.stallName); | |
| logoPlaceholder.style.display = 'flex'; | |
| logoPreview.style.display = 'none'; | |
| } | |
| } | |
| } | |
| function loadVendorOrders() { | |
| const vendor = getData('currentVendor') || {}; | |
| const orders = getData('orders') || []; | |
| const vendorOrders = orders.filter(o => o.vendorId == vendor.id); | |
| const container = document.getElementById('vendor-orders-container'); | |
| if (container) { | |
| container.innerHTML = ''; | |
| if (vendorOrders.length === 0) { | |
| container.innerHTML = '<p>No orders yet.</p>'; | |
| return; | |
| } | |
| vendorOrders.forEach(order => { | |
| const card = document.createElement('div'); | |
| card.className = 'card'; | |
| let itemsHTML = ''; | |
| order.items.forEach(i => { | |
| const flavorText = i.selectedFlavor ? ` - ${i.selectedFlavor}` : ''; | |
| itemsHTML += `<div>${sanitizeHTML(i.name)}${flavorText} (${i.selectedSize}) x ${i.quantity} - ₱${i.price * i.quantity}</div>`; | |
| }); | |
| const delivery = order.orderType === 'delivery' && order.deliveryDetails ? | |
| `<p><strong>Delivery:</strong> ${sanitizeHTML(order.deliveryDetails.name)}, ${sanitizeHTML(order.deliveryDetails.location)}, ${sanitizeHTML(order.deliveryDetails.contact)}</p>` : ''; | |
| const statusButtons = ['preparing', 'ready', 'completed'] | |
| .filter(s => s !== order.status) | |
| .map(s => `<button class="btn btn-small" onclick="updateOrderStatus(${order.id}, '${s}')">${s.charAt(0).toUpperCase() + s.slice(1)}</button>`) | |
| .join(' '); | |
| card.innerHTML = ` | |
| <h4>Order #${order.id}</h4> | |
| <p><strong>Student:</strong> ${sanitizeHTML(order.student.name)}</p> | |
| <p><strong>Type:</strong> ${order.orderType}</p> | |
| ${delivery} | |
| <p><strong>Payment:</strong> ${order.paymentMethod}</p> | |
| <p><strong>Status:</strong> <span class="order-status status-${order.status}">${order.status}</span></p> | |
| <div style="margin:10px 0;"><strong>Items:</strong>${itemsHTML}</div> | |
| <p><strong>Total:</strong> ₱${order.total.toFixed(2)}</p> | |
| <p><strong>Time:</strong> ${new Date(order.timestamp).toLocaleString()}</p> | |
| <div style="margin-top:10px;">${statusButtons}</div> | |
| `; | |
| container.appendChild(card); | |
| }); | |
| } | |
| } | |
| function updateOrderStatus(orderId, newStatus) { | |
| const orders = getData('orders') || []; | |
| const order = orders.find(o => o.id === orderId); | |
| if (order) { | |
| order.status = newStatus; | |
| setData('orders', orders); | |
| loadVendorOrders(); | |
| showToast(`Order updated to ${newStatus}`, 'success'); | |
| } | |
| } | |
| // ========================================== | |
| // ADMIN FUNCTIONS | |
| // ========================================== | |
| function loadVendors() { | |
| const vendors = getData('vendors') || []; | |
| const list = document.getElementById('vendor-list'); | |
| if (list) { | |
| list.innerHTML = ''; | |
| vendors.forEach(v => { | |
| const div = document.createElement('div'); | |
| div.className = 'vendor-item'; | |
| div.innerHTML = ` | |
| <div class="vendor-info"> | |
| <h4>${sanitizeHTML(v.stallName)}</h4> | |
| <p>Username: ${sanitizeHTML(v.username)} | Canteen: ${v.canteen}</p> | |
| <p>Menu Items: ${v.menu.length}</p> | |
| </div> | |
| <div class="vendor-actions"> | |
| <button class="btn btn-small btn-secondary" onclick="editVendor(${v.id})">Edit</button> | |
| <button class="btn btn-small btn-danger" onclick="deleteVendor(${v.id})">Delete</button> | |
| </div> | |
| `; | |
| list.appendChild(div); | |
| }); | |
| } | |
| } | |
| function editVendor(vendorId) { | |
| const vendors = getData('vendors') || []; | |
| const vendor = vendors.find(v => v.id === vendorId); | |
| if (vendor) { | |
| document.getElementById('edit-vendor-id').value = vendor.id; | |
| document.getElementById('edit-vendor-stall-name').value = vendor.stallName; | |
| document.getElementById('edit-vendor-username').value = vendor.username; | |
| document.getElementById('edit-vendor-password').value = vendor.password; | |
| document.getElementById('edit-vendor-canteen').value = vendor.canteen; | |
| showModal('edit-vendor-modal'); | |
| } | |
| } | |
| function deleteVendor(vendorId) { | |
| if (!confirm('Delete this vendor?')) return; | |
| const vendors = getData('vendors') || []; | |
| const updated = vendors.filter(v => v.id !== vendorId); | |
| setData('vendors', updated); | |
| const settings = getData('systemSettings') || {}; | |
| if (settings.deleteVendorBehavior === 'remove') { | |
| const orders = getData('orders') || []; | |
| const filteredOrders = orders.filter(o => o.vendorId !== vendorId); | |
| setData('orders', filteredOrders); | |
| } | |
| const currentVendor = getData('currentVendor'); | |
| if (currentVendor && currentVendor.id === vendorId) { | |
| localStorage.removeItem('currentVendor'); | |
| showPage('home-page'); | |
| updateNav(false); | |
| } | |
| loadVendors(); | |
| showToast('Vendor deleted', 'info'); | |
| } | |
| function loadAllMenus() { | |
| const vendors = getData('vendors') || []; | |
| const container = document.getElementById('all-menus-container'); | |
| if (container) { | |
| container.innerHTML = ''; | |
| vendors.forEach(vendor => { | |
| const section = document.createElement('div'); | |
| section.className = 'card'; | |
| section.style.marginBottom = '30px'; | |
| let menuHTML = '<div class="grid" style="margin-top:20px;">'; | |
| vendor.menu.forEach(item => { | |
| const img = item.image ? `<img src="${item.image}">` : '<div style="width:96px;height:120px;background:rgba(76,175,80,0.2);display:flex;align-items:center;justify-content:center;border-radius:8px;font-size:24px;">🍕</div>'; // CHANGED TO GREEN | |
| const priceRange = item.sizePrices | |
| ? `₱${Math.min(...Object.values(item.sizePrices))} - ₱${Math.max(...Object.values(item.sizePrices))}` | |
| : '₱0'; | |
| menuHTML += ` | |
| <div class="card menu-item"> | |
| <div class="menu-item-image">${img}</div> | |
| <div class="menu-item-info"> | |
| <div class="menu-item-name">${sanitizeHTML(item.name)}</div> | |
| <div class="menu-item-price">${priceRange}</div> | |
| <div style="color: var(--text-secondary); font-size: 14px;">${sanitizeHTML(item.description || '')}</div> | |
| <div style="margin-top: 8px;">${item.available ? '✓ Available' : '✗ Unavailable'}</div> | |
| </div> | |
| </div> | |
| `; | |
| }); | |
| menuHTML += '</div>'; | |
| section.innerHTML = ` | |
| <h3 style="color:var(--primary);">${sanitizeHTML(vendor.stallName)}</h3> | |
| <p style="color:var(--text-secondary);">Canteen: ${vendor.canteen} | Username: ${sanitizeHTML(vendor.username)}</p> | |
| ${menuHTML} | |
| `; | |
| container.appendChild(section); | |
| }); | |
| } | |
| } | |
| function loadSystemSettings() { | |
| const settings = getData('systemSettings') || {}; | |
| if (settings.systemName) document.getElementById('system-name').value = settings.systemName; | |
| if (settings.orderTimeout) document.getElementById('order-timeout').value = settings.orderTimeout; | |
| if (settings.deleteVendorBehavior) document.getElementById('delete-vendor-behavior').value = settings.deleteVendorBehavior; | |
| } | |
| // ========================================== | |
| // EVENT LISTENERS | |
| // ========================================== | |
| document.addEventListener('DOMContentLoaded', function() { | |
| initData(); | |
| // NEW FEATURE MERGED: Mobile navigation | |
| const mobileMenuToggle = document.getElementById('mobile-menu-toggle'); | |
| const closeMobileNavBtn = document.getElementById('close-mobile-nav'); // FIXED: Changed variable name | |
| if (mobileMenuToggle) { | |
| mobileMenuToggle.addEventListener('click', toggleMobileNav); | |
| } | |
| if (closeMobileNavBtn) { | |
| closeMobileNavBtn.addEventListener('click', closeMobileNav); | |
| } | |
| // NEW FEATURE MERGED: Color scheme functionality | |
| const themeToggle = document.getElementById('theme-toggle'); | |
| const mobileThemeToggle = document.getElementById('mobile-theme-toggle'); | |
| const saveColorScheme = document.getElementById('save-color-scheme'); | |
| if (themeToggle) { | |
| themeToggle.addEventListener('click', showColorSchemeModal); | |
| } | |
| if (mobileThemeToggle) { | |
| mobileThemeToggle.addEventListener('click', () => { | |
| showColorSchemeModal(); | |
| closeMobileNav(); | |
| }); | |
| } | |
| if (saveColorScheme) { | |
| saveColorScheme.addEventListener('click', () => { | |
| const activeColor = document.querySelector('.color-option.active').dataset.color; | |
| applyColorScheme(activeColor); | |
| hideModal('color-scheme-modal'); | |
| showToast('Color scheme updated!', 'success'); | |
| }); | |
| } | |
| // NEW FEATURE MERGED: Color option selection | |
| document.querySelectorAll('.color-option').forEach(option => { | |
| option.addEventListener('click', function() { | |
| document.querySelectorAll('.color-option').forEach(opt => opt.classList.remove('active')); | |
| this.classList.add('active'); | |
| }); | |
| }); | |
| // Navigation | |
| const navHome = document.getElementById('nav-home'); | |
| const mobileNavHome = document.getElementById('mobile-nav-home'); | |
| const navBack = document.getElementById('nav-back'); | |
| const mobileNavBack = document.getElementById('mobile-nav-back'); | |
| const navLogin = document.getElementById('nav-login'); | |
| const mobileNavLogin = document.getElementById('mobile-nav-login'); | |
| const navLogout = document.getElementById('nav-logout'); | |
| const mobileNavLogout = document.getElementById('mobile-nav-logout'); | |
| if (navHome) { | |
| navHome.onclick = (e) => { | |
| e.preventDefault(); | |
| if (getData('currentStudent')) { | |
| showPage('student-dashboard'); | |
| } else if (getData('currentVendor')) { | |
| showPage('vendor-dashboard'); | |
| } else if (getData('currentAdmin')) { | |
| showPage('admin-dashboard'); | |
| } else { | |
| showPage('home-page'); | |
| } | |
| }; | |
| } | |
| if (mobileNavHome) { | |
| mobileNavHome.onclick = (e) => { | |
| e.preventDefault(); | |
| if (getData('currentStudent')) { | |
| showPage('student-dashboard'); | |
| } else if (getData('currentVendor')) { | |
| showPage('vendor-dashboard'); | |
| } else if (getData('currentAdmin')) { | |
| showPage('admin-dashboard'); | |
| } else { | |
| showPage('home-page'); | |
| } | |
| }; | |
| } | |
| if (navBack) { | |
| navBack.onclick = (e) => { | |
| e.preventDefault(); | |
| goBack(); | |
| }; | |
| } | |
| if (mobileNavBack) { | |
| mobileNavBack.onclick = (e) => { | |
| e.preventDefault(); | |
| goBack(); | |
| }; | |
| } | |
| if (navLogin) navLogin.onclick = (e) => { e.preventDefault(); showPage('home-page'); }; | |
| if (mobileNavLogin) mobileNavLogin.onclick = (e) => { e.preventDefault(); showPage('home-page'); }; | |
| if (navLogout) navLogout.onclick = (e) => { e.preventDefault(); logout(); }; | |
| if (mobileNavLogout) mobileNavLogout.onclick = (e) => { e.preventDefault(); logout(); }; | |
| // Role selection | |
| const studentRole = document.getElementById('student-role'); | |
| const vendorRole = document.getElementById('vendor-role'); | |
| const adminRole = document.getElementById('admin-role'); | |
| if (studentRole) { | |
| studentRole.addEventListener('click', function() { | |
| showPage('student-login-page'); | |
| }); | |
| } | |
| if (vendorRole) { | |
| vendorRole.addEventListener('click', function() { | |
| showPage('vendor-login-page'); | |
| }); | |
| } | |
| if (adminRole) { | |
| adminRole.addEventListener('click', function() { | |
| showPage('admin-login-page'); | |
| }); | |
| } | |
| // Student authentication | |
| const studentLoginForm = document.getElementById('student-login-form'); | |
| if (studentLoginForm) { | |
| studentLoginForm.onsubmit = (e) => { | |
| e.preventDefault(); | |
| const u = document.getElementById('student-username').value; | |
| const p = document.getElementById('student-password').value; | |
| const remember = document.getElementById('student-remember').checked; | |
| if (loginStudent(u, p)) { | |
| if (remember) setData('rememberedStudent', { username: u, password: p }); | |
| else localStorage.removeItem('rememberedStudent'); | |
| } else showToast('Invalid credentials', 'error'); | |
| }; | |
| } | |
| const vendorLoginForm = document.getElementById('vendor-login-form'); | |
| if (vendorLoginForm) { | |
| vendorLoginForm.onsubmit = (e) => { | |
| e.preventDefault(); | |
| if (!loginVendor(document.getElementById('vendor-username').value, document.getElementById('vendor-password').value)) { | |
| showToast('Invalid credentials', 'error'); | |
| } | |
| }; | |
| } | |
| const adminLoginForm = document.getElementById('admin-login-form'); | |
| if (adminLoginForm) { | |
| adminLoginForm.onsubmit = (e) => { | |
| e.preventDefault(); | |
| if (!loginAdmin(document.getElementById('admin-username').value, document.getElementById('admin-password').value)) { | |
| showToast('Invalid credentials', 'error'); | |
| } | |
| }; | |
| } | |
| const studentRegister = document.getElementById('student-register'); | |
| if (studentRegister) { | |
| studentRegister.onclick = () => { | |
| const u = document.getElementById('student-username').value; | |
| const p = document.getElementById('student-password').value; | |
| if (!u || !p) { showToast('Enter username and password', 'error'); return; } | |
| const students = getData('students') || []; | |
| if (students.find(s => s.username === u)) { showToast('Username exists', 'error'); return; } | |
| students.push({ id: Date.now(), username: u, password: p, name: u }); | |
| setData('students', students); | |
| showToast('Registration successful!', 'success'); | |
| }; | |
| } | |
| // Student navigation | |
| document.querySelectorAll('.canteen-card').forEach(card => { | |
| card.onclick = function() { loadStalls(this.dataset.canteen); }; | |
| }); | |
| const backToStalls = document.getElementById('back-to-stalls'); | |
| const viewCartBtn = document.getElementById('view-cart-btn'); | |
| const backToMenu = document.getElementById('back-to-menu'); | |
| const checkoutBtn = document.getElementById('checkout-btn'); | |
| const placeOrderBtn = document.getElementById('place-order-btn'); | |
| const viewAllOrders = document.getElementById('view-all-orders'); | |
| const backToStudentDashboard = document.getElementById('back-to-student-dashboard'); | |
| const backToStudentDashboardFromStalls = document.getElementById('back-to-student-dashboard-from-stalls'); | |
| const backToStallsFromCart = document.getElementById('back-to-stalls-from-cart'); | |
| const backToCart = document.getElementById('back-to-cart'); | |
| if (backToStalls) backToStalls.onclick = () => showPage('canteen-stalls-page'); | |
| if (viewCartBtn) viewCartBtn.onclick = () => { loadCart(); showPage('student-cart-page'); }; | |
| if (backToMenu) backToMenu.onclick = () => showPage('stall-menu-page'); | |
| if (checkoutBtn) { | |
| checkoutBtn.onclick = () => { | |
| if (cart.length === 0) { showToast('Cart is empty!', 'error'); return; } | |
| loadCheckout(); | |
| showPage('checkout-page'); | |
| }; | |
| } | |
| if (placeOrderBtn) placeOrderBtn.onclick = placeOrder; | |
| if (viewAllOrders) viewAllOrders.onclick = () => { loadStudentOrders(); showPage('student-orders-page'); }; | |
| if (backToStudentDashboard) backToStudentDashboard.onclick = () => showPage('student-dashboard'); | |
| if (backToStudentDashboardFromStalls) backToStudentDashboardFromStalls.onclick = () => showPage('student-dashboard'); | |
| if (backToStallsFromCart) backToStallsFromCart.onclick = () => showPage('canteen-stalls-page'); | |
| if (backToCart) backToCart.onclick = () => { loadCart(); showPage('student-cart-page'); }; | |
| const orderType = document.getElementById('order-type'); | |
| const paymentMethod = document.getElementById('payment-method'); | |
| if (orderType) { | |
| orderType.onchange = function() { | |
| const deliveryDetails = document.getElementById('delivery-details'); | |
| if (deliveryDetails) deliveryDetails.style.display = this.value === 'delivery' ? 'block' : 'none'; | |
| }; | |
| } | |
| if (paymentMethod) { | |
| paymentMethod.onchange = function() { | |
| const gcashDetails = document.getElementById('gcash-details'); | |
| if (gcashDetails) gcashDetails.style.display = this.value === 'gcash' ? 'block' : 'none'; | |
| }; | |
| } | |
| // Vendor navigation | |
| const manageProfile = document.getElementById('manage-profile'); | |
| const manageMenu = document.getElementById('manage-menu'); | |
| const manageQR = document.getElementById('manage-qr'); | |
| const viewOrders = document.getElementById('view-orders'); | |
| if (manageProfile) manageProfile.onclick = () => { loadVendorProfile(); showPage('vendor-profile-page'); }; | |
| if (manageMenu) manageMenu.onclick = () => { loadVendorMenu(); showPage('vendor-menu-page'); }; | |
| if (manageQR) manageQR.onclick = () => { loadCurrentQR(); showPage('vendor-qr-page'); }; | |
| if (viewOrders) viewOrders.onclick = () => { loadVendorOrders(); showPage('vendor-orders-page'); }; | |
| const logoUpload = document.getElementById('logo-upload'); | |
| if (logoUpload) { | |
| logoUpload.onchange = async function(e) { | |
| const file = e.target.files[0]; | |
| if (!file) { | |
| showToast('Please select a file first', 'error'); | |
| return; | |
| } | |
| try { | |
| const logo = await compressImage(file); | |
| const preview = document.getElementById('profile-logo-preview'); | |
| const placeholder = document.getElementById('profile-logo-placeholder'); | |
| if (preview && placeholder) { | |
| preview.src = logo; | |
| preview.style.display = 'block'; | |
| placeholder.style.display = 'none'; | |
| preview.dataset.logo = logo; | |
| } | |
| } catch (error) { | |
| showToast(error.message, 'error'); | |
| } | |
| }; | |
| } | |
| const saveLogo = document.getElementById('save-logo'); | |
| if (saveLogo) { | |
| saveLogo.onclick = () => { | |
| const preview = document.getElementById('profile-logo-preview'); | |
| const logo = preview ? preview.dataset.logo : null; | |
| if (logo) { | |
| const vendor = getFreshVendor(); | |
| if (vendor) { | |
| vendor.logo = logo; | |
| setData('currentVendor', vendor); | |
| updateVendorInStorage(vendor); | |
| updateVendorDashboard(vendor); | |
| showToast('Logo saved!', 'success'); | |
| } | |
| } else { | |
| showToast('Upload a logo first', 'error'); | |
| } | |
| }; | |
| } | |
| const qrUpload = document.getElementById('qr-upload'); | |
| if (qrUpload) { | |
| qrUpload.onchange = async function(e) { | |
| const file = e.target.files[0]; | |
| if (!file) { | |
| showToast('Please select a file first', 'error'); | |
| return; | |
| } | |
| try { | |
| const qrCode = await compressImage(file); | |
| const preview = document.getElementById('current-qr-preview'); | |
| if (preview) { | |
| preview.innerHTML = `<img src="${qrCode}">`; | |
| preview.dataset.qrCode = qrCode; | |
| } | |
| } catch (error) { | |
| showToast(error.message, 'error'); | |
| } | |
| }; | |
| } | |
| const saveQR = document.getElementById('save-qr'); | |
| if (saveQR) { | |
| saveQR.onclick = () => { | |
| const preview = document.getElementById('current-qr-preview'); | |
| const qrCode = preview ? preview.dataset.qrCode : null; | |
| if (qrCode) { | |
| const vendor = getFreshVendor(); | |
| if (vendor) { | |
| vendor.qrCode = qrCode; | |
| setData('currentVendor', vendor); | |
| updateVendorInStorage(vendor); | |
| showToast('QR Code saved!', 'success'); | |
| } | |
| } else showToast('Upload a QR code first', 'error'); | |
| }; | |
| } | |
| const backBtns = [ | |
| 'back-to-vendor-dashboard', | |
| 'back-to-vendor-dashboard-profile', | |
| 'back-to-vendor-dashboard-orders', | |
| 'back-to-vendor-dashboard-qr' | |
| ]; | |
| backBtns.forEach(id => { | |
| const btn = document.getElementById(id); | |
| if (btn) btn.onclick = () => showPage('vendor-dashboard'); | |
| }); | |
| // Admin navigation | |
| const manageVendors = document.getElementById('manage-vendors'); | |
| const viewAllMenus = document.getElementById('view-all-menus'); | |
| const systemSettings = document.getElementById('system-settings'); | |
| const backToAdminDashboard = document.getElementById('back-to-admin-dashboard'); | |
| const backToAdminDashboardMenus = document.getElementById('back-to-admin-dashboard-menus'); | |
| const backToAdminDashboardSettings = document.getElementById('back-to-admin-dashboard-settings'); | |
| if (manageVendors) manageVendors.onclick = () => { loadVendors(); showPage('vendor-management-page'); }; | |
| if (viewAllMenus) viewAllMenus.onclick = () => { loadAllMenus(); showPage('view-all-menus-page'); }; | |
| if (systemSettings) systemSettings.onclick = () => { loadSystemSettings(); showPage('system-settings-page'); }; | |
| if (backToAdminDashboard) backToAdminDashboard.onclick = () => showPage('admin-dashboard'); | |
| if (backToAdminDashboardMenus) backToAdminDashboardMenus.onclick = () => showPage('admin-dashboard'); | |
| if (backToAdminDashboardSettings) backToAdminDashboardSettings.onclick = () => showPage('admin-dashboard'); | |
| const addVendorBtn = document.getElementById('add-vendor-btn'); | |
| const addMenuItemBtn = document.getElementById('add-menu-item-btn'); | |
| if (addVendorBtn) addVendorBtn.onclick = () => showModal('add-vendor-modal'); | |
| if (addMenuItemBtn) addMenuItemBtn.onclick = () => showModal('add-menu-item-modal'); | |
| const addVendorForm = document.getElementById('add-vendor-form'); | |
| if (addVendorForm) { | |
| addVendorForm.onsubmit = (e) => { | |
| e.preventDefault(); | |
| const username = document.getElementById('new-vendor-username').value.trim(); | |
| const password = document.getElementById('new-vendor-password').value.trim(); | |
| if (username !== username.toLowerCase() || password !== password.toLowerCase()) { | |
| showToast('Username and password must be lowercase', 'error'); | |
| return; | |
| } | |
| const vendors = getData('vendors') || []; | |
| if (vendors.find(v => v.username === username)) { | |
| showToast('Username already exists', 'error'); | |
| return; | |
| } | |
| vendors.push({ | |
| id: Date.now(), | |
| username: username, | |
| password: password, | |
| stallName: document.getElementById('new-vendor-stall-name').value, | |
| canteen: document.getElementById('new-vendor-canteen').value, | |
| logo: '', | |
| menu: [], qrCode: '', orders: [] | |
| }); | |
| setData('vendors', vendors); | |
| loadVendors(); | |
| hideModal('add-vendor-modal'); | |
| e.target.reset(); | |
| showToast('Vendor added!', 'success'); | |
| }; | |
| } | |
| const editVendorForm = document.getElementById('edit-vendor-form'); | |
| if (editVendorForm) { | |
| editVendorForm.onsubmit = (e) => { | |
| e.preventDefault(); | |
| const vendorId = parseInt(document.getElementById('edit-vendor-id').value); | |
| const username = document.getElementById('edit-vendor-username').value.trim(); | |
| const password = document.getElementById('edit-vendor-password').value.trim(); | |
| if (username !== username.toLowerCase() || password !== password.toLowerCase()) { | |
| showToast('Username and password must be lowercase', 'error'); | |
| return; | |
| } | |
| const vendors = getData('vendors') || []; | |
| const vendor = vendors.find(v => v.id === vendorId); | |
| if (vendor) { | |
| if (vendors.find(v => v.username === username && v.id !== vendorId)) { | |
| showToast('Username already exists', 'error'); | |
| return; | |
| } | |
| vendor.stallName = document.getElementById('edit-vendor-stall-name').value; | |
| vendor.username = username; | |
| vendor.password = password; | |
| vendor.canteen = document.getElementById('edit-vendor-canteen').value; | |
| setData('vendors', vendors); | |
| loadVendors(); | |
| hideModal('edit-vendor-modal'); | |
| showToast('Vendor updated!', 'success'); | |
| } | |
| }; | |
| } | |
| const addMenuItemForm = document.getElementById('add-menu-item-form'); | |
| if (addMenuItemForm) { | |
| addMenuItemForm.onsubmit = async (e) => { | |
| e.preventDefault(); | |
| const vendor = getFreshVendor(); | |
| if (!vendor) return; | |
| const sizePricesInput = document.getElementById('item-size-prices').value; | |
| const sizePrices = parseSizePrices(sizePricesInput); | |
| if (Object.keys(sizePrices).length === 0) { | |
| showToast('Please enter at least one valid size:price pair', 'error'); | |
| return; | |
| } | |
| const flavorsInput = document.getElementById('item-flavors').value; | |
| const flavors = flavorsInput ? flavorsInput.split(',').map(f => f.trim()).filter(f => f) : []; | |
| const newItem = { | |
| id: Date.now(), | |
| name: document.getElementById('item-name').value, | |
| description: document.getElementById('item-description').value, | |
| flavors: flavors, | |
| sizePrices: sizePrices, | |
| available: true, | |
| image: '' | |
| }; | |
| const file = document.getElementById('item-image').files[0]; | |
| if (file) { | |
| try { | |
| newItem.image = await compressImage(file); | |
| } catch (error) { | |
| showToast(error.message, 'error'); | |
| return; | |
| } | |
| } | |
| vendor.menu.push(newItem); | |
| setData('currentVendor', vendor); | |
| updateVendorInStorage(vendor); | |
| loadVendorMenu(); | |
| hideModal('add-menu-item-modal'); | |
| e.target.reset(); | |
| showToast('Item added!', 'success'); | |
| }; | |
| } | |
| const editMenuItemForm = document.getElementById('edit-menu-item-form'); | |
| if (editMenuItemForm) { | |
| editMenuItemForm.onsubmit = async (e) => { | |
| e.preventDefault(); | |
| const itemId = parseInt(document.getElementById('edit-item-id').value); | |
| const vendor = getFreshVendor(); | |
| if (!vendor) return; | |
| const item = vendor.menu.find(i => i.id === itemId); | |
| if (item) { | |
| const sizePricesInput = document.getElementById('edit-item-size-prices').value; | |
| const sizePrices = parseSizePrices(sizePricesInput); | |
| if (Object.keys(sizePrices).length === 0) { | |
| showToast('Please enter at least one valid size:price pair', 'error'); | |
| return; | |
| } | |
| const flavorsInput = document.getElementById('edit-item-flavors').value; | |
| const flavors = flavorsInput ? flavorsInput.split(',').map(f => f.trim()).filter(f => f) : []; | |
| item.name = document.getElementById('edit-item-name').value; | |
| item.description = document.getElementById('edit-item-description').value; | |
| item.flavors = flavors; | |
| item.sizePrices = sizePrices; | |
| const file = document.getElementById('edit-item-image').files[0]; | |
| if (file) { | |
| try { | |
| item.image = await compressImage(file); | |
| } catch (error) { | |
| showToast(error.message, 'error'); | |
| return; | |
| } | |
| } | |
| setData('currentVendor', vendor); | |
| updateVendorInStorage(vendor); | |
| loadVendorMenu(); | |
| hideModal('edit-menu-item-modal'); | |
| showToast('Item updated!', 'success'); | |
| } | |
| }; | |
| } | |
| const saveSettingsBtn = document.getElementById('save-settings'); | |
| if (saveSettingsBtn) { | |
| saveSettingsBtn.onclick = () => { | |
| const settings = { | |
| systemName: document.getElementById('system-name').value, | |
| orderTimeout: parseInt(document.getElementById('order-timeout').value), | |
| deleteVendorBehavior: document.getElementById('delete-vendor-behavior').value | |
| }; | |
| setData('systemSettings', settings); | |
| showToast('Settings saved!', 'success'); | |
| }; | |
| } | |
| document.querySelectorAll('.close-modal').forEach(btn => { | |
| btn.onclick = function() { this.closest('.modal').style.display = 'none'; }; | |
| }); | |
| const closeSuccessModal = document.getElementById('close-success-modal'); | |
| if (closeSuccessModal) { | |
| closeSuccessModal.onclick = () => { | |
| hideModal('order-success-modal'); | |
| showPage('student-dashboard'); | |
| }; | |
| } | |
| const clearCartSwitch = document.getElementById('clear-cart-switch'); | |
| if (clearCartSwitch) { | |
| clearCartSwitch.onclick = () => { | |
| if (pendingSwitchItem) { | |
| cart = []; | |
| setData('currentCart', cart); | |
| updateCartCount(); | |
| addToCart(pendingSwitchItem.item, pendingSwitchItem.vendorName, pendingSwitchItem.vendorId); | |
| pendingSwitchItem = null; | |
| } | |
| hideModal('cart-switch-modal'); | |
| }; | |
| } | |
| const keepCart = document.getElementById('keep-cart'); | |
| if (keepCart) { | |
| keepCart.onclick = () => { | |
| pendingSwitchItem = null; | |
| hideModal('cart-switch-modal'); | |
| }; | |
| } | |
| window.onclick = (e) => { | |
| if (e.target.classList.contains('modal')) e.target.style.display = 'none'; | |
| }; | |
| // Auto-login if remembered | |
| const remembered = getData('rememberedStudent'); | |
| if (remembered) { | |
| const usernameInput = document.getElementById('student-username'); | |
| const passwordInput = document.getElementById('student-password'); | |
| const rememberCheck = document.getElementById('student-remember'); | |
| if (usernameInput) usernameInput.value = remembered.username; | |
| if (passwordInput) passwordInput.value = remembered.password; | |
| if (rememberCheck) rememberCheck.checked = true; | |
| } | |
| // Check if already logged in | |
| if (getData('currentStudent')) { | |
| const student = getData('currentStudent'); | |
| const nameEl = document.getElementById('student-name'); | |
| if (nameEl) nameEl.textContent = student.name; | |
| showPage('student-dashboard'); | |
| updateNav(true); | |
| } else if (getData('currentVendor')) { | |
| const vendor = getFreshVendor(); | |
| if (vendor) { | |
| updateVendorDashboard(vendor); | |
| showPage('vendor-dashboard'); | |
| updateNav(true); | |
| } | |
| } else if (getData('currentAdmin')) { | |
| showPage('admin-dashboard'); | |
| updateNav(true); | |
| } | |
| updateCartCount(); | |
| updateBackButton(); | |
| }); | |
| </script> | |
| </body> | |
| </html> | |