<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0">
<title>Purveyor — House Operations</title>
<link href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,300;0,400;0,600;1,300;1,400&family=Montserrat:wght@200;300;400;500;600&display=swap" rel="stylesheet">
<!-- ══════ FIREBASE SDK ══════ -->
<script src="https://www.gstatic.com/firebasejs/10.12.2/firebase-app-compat.js"></script>
<script src="https://www.gstatic.com/firebasejs/10.12.2/firebase-auth-compat.js"></script>
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--cream: #F7F3ED;
--cream2: #EDE7DB;
--cream3: #E5DDD0;
--gold: #A07828;
--gold-l: #C9A84C;
--gold-d: #6E5210;
--charcoal:#1A1612;
--mid: #6B5E4E;
--light: #9A8E80;
--white: #FFFFFF;
}
html, body {
min-height: 100vh;
background: var(--cream);
font-family: 'Montserrat', sans-serif;
color: var(--charcoal);
direction: ltr;
overflow-x: hidden;
}
body::before {
content: '';
position: fixed; inset: 0;
background-image:
radial-gradient(ellipse 60% 50% at 80% 10%, rgba(160,120,40,0.05) 0%, transparent 60%),
radial-gradient(ellipse 40% 40% at 10% 90%, rgba(160,120,40,0.04) 0%, transparent 60%),
repeating-linear-gradient(45deg, transparent, transparent 60px, rgba(160,120,40,0.012) 60px, rgba(160,120,40,0.012) 61px);
pointer-events: none;
z-index: 0;
}
/* ══════════════════════════════
PAGES SYSTEM
══════════════════════════════ */
.page {
position: fixed; inset: 0;
opacity: 0;
pointer-events: none;
transition: opacity 0.4s ease;
overflow-y: auto;
z-index: 1;
}
.page.active {
opacity: 1;
pointer-events: all;
position: relative;
}
/* ══════════════════════════════
PAGE 1 — LOGIN
══════════════════════════════ */
#page-login {
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
padding: 40px 20px;
}
.corner { position: fixed; width: 50px; height: 50px; z-index: 5; }
.corner-tl { top: 18px; left: 18px; border-top: 1.5px solid rgba(160,120,40,0.3); border-left: 1.5px solid rgba(160,120,40,0.3); }
.corner-tr { top: 18px; right: 18px; border-top: 1.5px solid rgba(160,120,40,0.3); border-right: 1.5px solid rgba(160,120,40,0.3); }
.corner-bl { bottom: 18px; left: 18px; border-bottom: 1.5px solid rgba(160,120,40,0.3); border-left: 1.5px solid rgba(160,120,40,0.3); }
.corner-br { bottom: 18px; right: 18px; border-bottom: 1.5px solid rgba(160,120,40,0.3); border-right: 1.5px solid rgba(160,120,40,0.3); }
.login-card {
position: relative;
z-index: 10;
width: 100%;
max-width: 400px;
background: rgba(255,255,255,0.78);
backdrop-filter: blur(20px);
border: 1px solid rgba(160,120,40,0.18);
border-radius: 24px;
padding: 48px 40px 44px;
box-shadow: 0 2px 4px rgba(90,60,20,0.1), 0 12px 40px rgba(90,60,20,0.1), inset 0 1px 0 rgba(255,255,255,0.8);
animation: cardIn 0.8s cubic-bezier(0.16,1,0.3,1) forwards;
opacity: 0; transform: translateY(20px);
}
@keyframes cardIn { to { opacity:1; transform:translateY(0); } }
.card-accent {
position: absolute; top:0; left:40px; right:40px;
height:2px;
background: linear-gradient(90deg, transparent, var(--gold-l), transparent);
border-radius: 2px;
}
.login-icon {
width: 64px; height: 64px;
background: linear-gradient(135deg, var(--gold-l), var(--gold-d));
border-radius: 18px;
display: flex; align-items: center; justify-content: center;
margin: 0 auto 22px;
box-shadow: 0 4px 16px rgba(160,120,40,0.3);
animation: iconIn 0.8s cubic-bezier(0.16,1,0.3,1) 0.1s both;
}
@keyframes iconIn { from { opacity:0; transform:scale(0.8); } to { opacity:1; transform:scale(1); } }
.login-brand {
font-family: 'Cormorant Garamond', serif;
font-size: 30px; font-weight: 600;
color: var(--charcoal);
text-align: center; letter-spacing: 0.5px;
margin-bottom: 5px;
animation: fadeUp 0.6s ease 0.2s both;
}
.login-tagline {
font-size: 9px; font-weight: 500;
letter-spacing: 3.5px; color: var(--light);
text-align: center; text-transform: uppercase;
margin-bottom: 34px;
animation: fadeUp 0.6s ease 0.3s both;
}
.steps {
display: flex; align-items: center; justify-content: center;
gap: 8px; margin-bottom: 26px;
animation: fadeUp 0.6s ease 0.35s both;
}
.step-dot { width: 24px; height: 3px; border-radius: 2px; background: var(--cream2); transition: all 0.3s; }
.step-dot.active { background: var(--gold-l); width: 32px; }
.step-dot.done { background: var(--gold-d); }
/* Login screens */
.login-screen { display: none; }
.login-screen.active { display: block; }
.input-label {
font-size: 10px; font-weight: 500;
letter-spacing: 2px; color: var(--mid);
text-transform: uppercase; margin-bottom: 9px; display: block;
animation: fadeUp 0.6s ease 0.4s both;
}
.phone-wrapper {
display: flex; align-items: center;
background: var(--white);
border: 1.5px solid rgba(160,120,40,0.18);
border-radius: 14px; overflow: hidden;
transition: border-color 0.3s, box-shadow 0.3s;
margin-bottom: 10px;
animation: fadeUp 0.6s ease 0.45s both;
}
.phone-wrapper:focus-within {
border-color: var(--gold-l);
box-shadow: 0 0 0 4px rgba(160,120,40,0.08);
}
.phone-flag {
display: flex; align-items: center; gap: 5px;
padding: 14px 12px 14px 15px;
border-right: 1px solid rgba(160,120,40,0.12);
font-size: 12px; color: var(--mid); font-weight: 500; white-space: nowrap;
}
.phone-input {
flex: 1; padding: 14px 15px;
border: none; outline: none;
font-family: 'Montserrat', sans-serif;
font-size: 15px; font-weight: 300;
color: var(--charcoal); background: transparent;
letter-spacing: 1.5px; direction: ltr;
}
.phone-input::placeholder { color: var(--light); letter-spacing: 0.5px; }
.note {
font-size: 10px; color: var(--light);
text-align: center; margin-bottom: 26px; line-height: 1.6;
animation: fadeUp 0.6s ease 0.5s both;
}
.otp-label {
font-size: 10px; font-weight: 500;
letter-spacing: 2px; color: var(--mid);
text-transform: uppercase; text-align: center; margin-bottom: 6px; display: block;
}
.otp-sublabel {
font-size: 11px; color: var(--light);
text-align: center; margin-bottom: 22px;
}
.otp-sublabel strong { color: var(--gold); font-weight: 500; }
.otp-row {
display: flex; gap: 8px; justify-content: center;
direction: ltr; margin-bottom: 8px;
}
.otp-input {
width: 50px; height: 56px;
border: 1.5px solid rgba(160,120,40,0.18);
border-radius: 14px; background: var(--white);
font-family: 'Cormorant Garamond', serif;
font-size: 24px; font-weight: 600;
color: var(--charcoal); text-align: center;
outline: none; transition: all 0.2s;
direction: ltr; caret-color: var(--gold);
}
.otp-input:focus {
border-color: var(--gold-l);
box-shadow: 0 0 0 4px rgba(160,120,40,0.1);
transform: translateY(-2px); background: #FFFDF9;
}
.otp-input.filled { border-color: var(--gold); background: #FFFBF0; color: var(--gold-d); }
.otp-input.error { border-color: #C04040; animation: shake 0.4s ease; }
.otp-input.success { border-color: #50A070; background: #F0FFF6; color: #2A7048; }
@keyframes shake { 0%,100%{transform:translateX(0)} 20%{transform:translateX(-5px)} 60%{transform:translateX(5px)} }
.timer-row {
display: flex; align-items: center; justify-content: center;
gap: 8px; margin-bottom: 26px;
font-size: 11px; color: var(--light);
}
.timer-badge {
background: var(--cream2); border-radius: 20px;
padding: 4px 10px; font-weight: 500; color: var(--mid);
font-size: 11px; min-width: 44px; text-align: center;
}
.resend-btn {
background: none; border: none; cursor: pointer;
font-family: 'Montserrat', sans-serif;
font-size: 11px; color: var(--gold); font-weight: 500;
text-decoration: underline; padding: 0;
opacity: 0; pointer-events: none; transition: opacity 0.3s;
}
.resend-btn.visible { opacity: 1; pointer-events: all; }
.btn {
width: 100%; padding: 16px;
background: linear-gradient(135deg, var(--gold-l), var(--gold));
border: none; border-radius: 14px;
font-family: 'Montserrat', sans-serif;
font-size: 11px; font-weight: 600;
letter-spacing: 2.5px; color: white;
text-transform: uppercase; cursor: pointer;
transition: all 0.3s;
box-shadow: 0 4px 16px rgba(160,120,40,0.25);
animation: fadeUp 0.6s ease 0.55s both;
position: relative; overflow: hidden;
}
.btn:hover:not(:disabled) { transform: translateY(-2px); box-shadow: 0 8px 24px rgba(160,120,40,0.35); }
.btn:disabled { background: var(--cream2); color: var(--light); cursor: not-allowed; box-shadow: none; }
.back-link {
display: flex; align-items: center; justify-content: center;
gap: 6px; margin-top: 14px;
font-size: 11px; color: var(--light); cursor: pointer;
background: none; border: none;
font-family: 'Montserrat', sans-serif; width: 100%;
transition: color 0.2s;
}
.back-link:hover { color: var(--mid); }
.login-footer {
margin-top: 26px; padding-top: 18px;
border-top: 1px solid rgba(160,120,40,0.1);
text-align: center; font-size: 9px;
letter-spacing: 2.5px; color: var(--light); text-transform: uppercase;
animation: fadeUp 0.6s ease 0.6s both;
}
@keyframes fadeUp { from { opacity:0; transform:translateY(10px); } to { opacity:1; transform:translateY(0); } }
/* ══════════════════════════════
PAGE 2 — REQUESTS
══════════════════════════════ */
#page-requests { min-height: 100vh; }
.header {
position: sticky; top: 0; z-index: 100;
background: rgba(247,243,237,0.93);
backdrop-filter: blur(16px);
border-bottom: 1px solid rgba(160,120,40,0.14);
padding: 0 20px;
display: flex; align-items: center;
justify-content: space-between; height: 62px;
}
.header-left { display: flex; align-items: center; gap: 10px; }
.header-icon {
width: 34px; height: 34px;
background: linear-gradient(135deg, var(--gold-l), var(--gold-d));
border-radius: 10px;
display: flex; align-items: center; justify-content: center;
}
.header-title {
font-family: 'Cormorant Garamond', serif;
font-size: 20px; font-weight: 600; color: var(--charcoal);
}
.header-sub {
font-size: 9px; letter-spacing: 2px; color: var(--light);
text-transform: uppercase; margin-top: 1px;
}
.header-right { display: flex; align-items: center; gap: 10px; }
.icon-btn {
width: 34px; height: 34px; border-radius: 10px;
background: var(--cream2); border: none; cursor: pointer;
display: flex; align-items: center; justify-content: center;
position: relative; transition: background 0.2s;
}
.icon-btn:hover { background: var(--cream3); }
.notif-badge {
position: absolute; top: 5px; right: 5px;
width: 8px; height: 8px; background: var(--gold-l);
border-radius: 50%; border: 2px solid var(--cream);
}
.user-chip {
display: flex; align-items: center; gap: 7px;
background: var(--cream2); border-radius: 20px;
padding: 5px 12px 5px 5px; cursor: pointer;
}
.user-avatar {
width: 24px; height: 24px;
background: linear-gradient(135deg, var(--gold-l), var(--gold-d));
border-radius: 50%;
display: flex; align-items: center; justify-content: center;
font-size: 10px; font-weight: 600; color: white;
}
.user-name { font-size: 11px; font-weight: 500; color: var(--mid); }
/* Main content */
.main { max-width: 640px; margin: 0 auto; padding: 28px 18px 90px; }
.greeting { margin-bottom: 24px; }
.greeting-top { font-size: 10px; letter-spacing: 2px; color: var(--light); text-transform: uppercase; margin-bottom: 3px; }
.greeting-name { font-family: 'Cormorant Garamond', serif; font-size: 26px; font-weight: 600; color: var(--charcoal); }
.stats-row {
display: grid; grid-template-columns: repeat(3,1fr);
gap: 10px; margin-bottom: 28px;
}
.stat-card {
background: var(--white);
border: 1px solid rgba(160,120,40,0.1);
border-radius: 16px; padding: 16px;
text-align: center;
box-shadow: 0 1px 6px rgba(90,60,20,0.06);
}
.stat-num {
font-family: 'Cormorant Garamond', serif;
font-size: 28px; font-weight: 600; color: var(--gold); line-height: 1; margin-bottom: 3px;
}
.stat-label { font-size: 9px; letter-spacing: 1.5px; color: var(--light); text-transform: uppercase; }
.section-title {
font-size: 10px; letter-spacing: 2.5px; color: var(--light);
text-transform: uppercase; margin-bottom: 12px;
display: flex; align-items: center; gap: 10px;
}
.section-title::after { content:''; flex:1; height:1px; background:rgba(160,120,40,0.15); }
/* Categories */
.categories {
display: grid; grid-template-columns: repeat(2,1fr);
gap: 12px; margin-bottom: 28px;
}
@media(min-width:480px) { .categories { grid-template-columns: repeat(3,1fr); } }
.cat-card {
background: var(--white);
border: 1px solid rgba(160,120,40,0.1);
border-radius: 18px; padding: 22px 16px;
cursor: pointer; transition: all 0.25s;
box-shadow: 0 1px 6px rgba(90,60,20,0.06);
position: relative; overflow: hidden;
}
.cat-card::before {
content:''; position:absolute; top:0; left:0; right:0;
height:3px; background: linear-gradient(90deg, var(--gold-l), transparent);
opacity:0; transition: opacity 0.3s;
}
.cat-card:hover { transform:translateY(-3px); box-shadow:0 8px 24px rgba(90,60,20,0.12); border-color:rgba(160,120,40,0.28); }
.cat-card:hover::before { opacity:1; }
.cat-card:active { transform:translateY(-1px); }
.cat-icon { font-size: 26px; margin-bottom: 12px; display: block; }
.cat-name { font-family:'Cormorant Garamond',serif; font-size:16px; font-weight:600; color:var(--charcoal); margin-bottom:3px; }
.cat-desc { font-size:9.5px; color:var(--light); line-height:1.4; }
.cat-arrow {
position:absolute; bottom:14px; right:14px;
width:24px; height:24px; background:var(--cream);
border-radius:8px; display:flex; align-items:center; justify-content:center;
transition: all 0.2s;
}
.cat-card:hover .cat-arrow { background:var(--gold-l); }
.cat-card:hover .cat-arrow svg { stroke: white; }
/* Recent */
.request-item {
background: var(--white);
border: 1px solid rgba(160,120,40,0.1);
border-radius: 14px; padding: 14px 16px;
margin-bottom: 9px;
display: flex; align-items: center; gap: 12px;
box-shadow: 0 1px 4px rgba(90,60,20,0.05);
transition: all 0.2s;
}
.request-item:hover { border-color:rgba(160,120,40,0.22); transform:translateX(3px); }
.req-icon {
width: 38px; height: 38px; border-radius: 11px;
display: flex; align-items: center; justify-content: center;
font-size: 17px; flex-shrink: 0; background: var(--cream);
}
.req-info { flex:1; min-width:0; }
.req-title { font-size:13px; font-weight:500; color:var(--charcoal); margin-bottom:2px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
.req-meta { font-size:10px; color:var(--light); }
.req-status {
flex-shrink:0; font-size:9px; font-weight:600;
letter-spacing:1px; text-transform:uppercase;
padding:4px 9px; border-radius:20px;
}
.status-pending { background:#FFF8E6; color:#A07828; }
.status-approved { background:#E6FFF0; color:#2A7048; }
.status-done { background:#E6F0FF; color:#284870; }
/* Bottom nav */
.bottom-nav {
position: fixed; bottom:0; left:0; right:0;
background: rgba(247,243,237,0.95);
backdrop-filter: blur(16px);
border-top: 1px solid rgba(160,120,40,0.12);
display: flex; justify-content: space-around;
padding: 8px 0 20px; z-index: 50;
}
.nav-item {
display:flex; flex-direction:column; align-items:center;
gap:3px; cursor:pointer; padding:4px 18px;
border:none; background:none;
font-family:'Montserrat',sans-serif;
}
.nav-icon { font-size:19px; color:var(--light); }
.nav-label { font-size:9px; letter-spacing:1px; color:var(--light); text-transform:uppercase; }
.nav-item.active .nav-icon, .nav-item.active .nav-label { color:var(--gold); font-weight:600; }
/* ══════════════════════════════
MODAL
══════════════════════════════ */
.modal-overlay {
position: fixed; inset:0;
background: rgba(26,22,18,0.5);
backdrop-filter: blur(4px);
z-index: 200;
display: flex; align-items: flex-end; justify-content: center;
opacity:0; pointer-events:none; transition: opacity 0.3s;
}
.modal-overlay.open { opacity:1; pointer-events:all; }
.modal {
background: var(--white);
border-radius: 26px 26px 0 0;
width: 100%; max-width: 640px;
padding: 10px 22px 36px;
transform: translateY(100%);
transition: transform 0.4s cubic-bezier(0.16,1,0.3,1);
max-height: 88vh; overflow-y: auto;
}
.modal-overlay.open .modal { transform: translateY(0); }
.modal-handle { width:38px; height:4px; background:var(--cream3); border-radius:2px; margin:8px auto 22px; }
.modal-header { display:flex; align-items:center; gap:12px; margin-bottom:22px; }
.modal-icon { width:50px; height:50px; border-radius:15px; display:flex; align-items:center; justify-content:center; font-size:24px; background:var(--cream); flex-shrink:0; }
.modal-title { font-family:'Cormorant Garamond',serif; font-size:22px; font-weight:600; color:var(--charcoal); }
.modal-sub { font-size:10px; color:var(--light); margin-top:2px; }
.field { margin-bottom:16px; }
.field-label { font-size:10px; font-weight:500; letter-spacing:1.5px; color:var(--mid); text-transform:uppercase; margin-bottom:7px; display:block; }
.field-input {
width:100%; padding:13px 15px;
background:var(--cream); border:1.5px solid transparent;
border-radius:13px;
font-family:'Montserrat',sans-serif; font-size:14px; font-weight:300;
color:var(--charcoal); outline:none; transition:all 0.2s; direction:ltr;
}
.field-input:focus { border-color:var(--gold-l); background:var(--white); box-shadow:0 0 0 4px rgba(160,120,40,0.08); }
.field-input::placeholder { color:var(--light); }
textarea.field-input { resize:none; height:90px; line-height:1.6; }
.priority-row { display:flex; gap:8px; }
.priority-btn {
flex:1; padding:9px; border-radius:12px;
border:1.5px solid rgba(160,120,40,0.18);
background:var(--cream);
font-family:'Montserrat',sans-serif; font-size:11px; font-weight:500;
color:var(--mid); cursor:pointer; transition:all 0.2s; text-align:center;
}
.priority-btn.sel-normal { background:#E6F0FF; border-color:#4870A0; color:#284870; }
.priority-btn.sel-urgent { background:#FFF5E6; border-color:var(--gold-l); color:var(--gold-d); }
.priority-btn.sel-critical { background:#FFE6E6; border-color:#A04040; color:#6A2020; }
.submit-btn {
width:100%; padding:17px;
background: linear-gradient(135deg, var(--gold-l), var(--gold));
border:none; border-radius:15px;
font-family:'Montserrat',sans-serif; font-size:11px; font-weight:600;
letter-spacing:2px; color:white; text-transform:uppercase;
cursor:pointer; transition:all 0.3s;
box-shadow:0 4px 16px rgba(160,120,40,0.3); margin-top:6px;
}
.submit-btn:hover { transform:translateY(-2px); box-shadow:0 8px 24px rgba(160,120,40,0.4); }
/* Toast */
.toast {
position: fixed; bottom:90px; left:50%;
transform: translateX(-50%) translateY(120px);
background: var(--charcoal); color: white;
padding:13px 22px; border-radius:50px;
font-size:12px; font-weight:400;
display:flex; align-items:center; gap:9px;
z-index:999;
transition: transform 0.4s cubic-bezier(0.16,1,0.3,1);
white-space:nowrap;
box-shadow:0 8px 32px rgba(0,0,0,0.2);
}
.toast.show { transform: translateX(-50%) translateY(0); }
/* reCAPTCHA hidden */
#recaptcha-container { position: absolute; bottom: 0; left: 0; visibility: hidden; }
</style>
</head>
<body>
<!-- ══════ LOGIN PAGE ══════ -->
<div class="page active" id="page-login">
<div class="corner corner-tl"></div>
<div class="corner corner-tr"></div>
<div class="corner corner-bl"></div>
<div class="corner corner-br"></div>
<div class="login-card">
<div class="card-accent"></div>
<div class="login-icon">
<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/>
<polyline points="9 12 11 14 15 10"/>
</svg>
</div>
<div class="login-brand">Purveyor</div>
<div class="login-tagline">House Operations Access</div>
<div class="steps" id="steps">
<div class="step-dot active" id="dot1"></div>
<div class="step-dot" id="dot2"></div>
</div>
<!-- Screen 1: Phone -->
<div class="login-screen active" id="screen-phone">
<label class="input-label">Mobile Number</label>
<div class="phone-wrapper">
<div class="phone-flag">
<span style="font-size:17px;">🇸🇦</span>
<span style="font-size:12px;color:#9A8E80;">+966</span>
<svg width="9" height="9" viewBox="0 0 24 24" fill="none" stroke="#9A8E80" stroke-width="2"><polyline points="6 9 12 15 18 9"/></svg>
</div>
<input class="phone-input" id="phone-input" type="tel"
placeholder="5X XXX XXXX" maxlength="10"
inputmode="numeric" autocomplete="tel" dir="ltr" />
</div>
<p class="note">A 6-digit code will be sent to your registered number</p>
<button class="btn" id="send-btn" disabled onclick="sendOTP()">Send Verification Code</button>
<!-- Invisible reCAPTCHA mounts here -->
<div id="recaptcha-container"></div>
</div>
<!-- Screen 2: OTP -->
<div class="login-screen" id="screen-otp">
<label class="otp-label">Verification Code</label>
<p class="otp-sublabel">Sent to <strong id="phone-display"></strong></p>
<div class="otp-row" id="otp-row">
<input class="otp-input" type="text" inputmode="numeric" maxlength="1" dir="ltr" autocomplete="one-time-code"/>
<input class="otp-input" type="text" inputmode="numeric" maxlength="1" dir="ltr"/>
<input class="otp-input" type="text" inputmode="numeric" maxlength="1" dir="ltr"/>
<input class="otp-input" type="text" inputmode="numeric" maxlength="1" dir="ltr"/>
<input class="otp-input" type="text" inputmode="numeric" maxlength="1" dir="ltr"/>
<input class="otp-input" type="text" inputmode="numeric" maxlength="1" dir="ltr"/>
</div>
<div class="timer-row">
<span>Resend in</span>
<span class="timer-badge" id="timer">01:00</span>
<button class="resend-btn" id="resend-btn" onclick="resendOTP()">Resend</button>
</div>
<button class="btn" id="verify-btn" disabled onclick="verifyOTP()">Authenticate System</button>
<button class="back-link" onclick="goBack()">← Change number</button>
</div>
<div class="login-footer">Nexalys · Private Infrastructure</div>
</div>
</div>
<!-- ══════ REQUESTS PAGE ══════ -->
<div class="page" id="page-requests">
<!-- Header -->
<div class="header">
<div class="header-left">
<div class="header-icon">
<svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/>
<polyline points="9 12 11 14 15 10"/>
</svg>
</div>
<div>
<div class="header-title">Purveyor</div>
<div class="header-sub">House Operations</div>
</div>
</div>
<div class="header-right">
<button class="icon-btn">
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="#9A8E80" stroke-width="2" stroke-linecap="round"><path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"/><path d="M13.73 21a2 2 0 0 1-3.46 0"/></svg>
<div class="notif-badge"></div>
</button>
<div class="user-chip">
<div class="user-avatar" id="user-initial">S</div>
<span class="user-name" id="user-phone-display">Staff</span>
</div>
</div>
</div>
<div class="main">
<div class="greeting">
<div class="greeting-top" id="greeting-time">Good morning</div>
<div class="greeting-name">What do you need today?</div>
</div>
<div class="stats-row">
<div class="stat-card">
<div class="stat-num" id="stat-total">0</div>
<div class="stat-label">My Requests</div>
</div>
<div class="stat-card">
<div class="stat-num" id="stat-pending">0</div>
<div class="stat-label">Pending</div>
</div>
<div class="stat-card">
<div class="stat-num" id="stat-done">0</div>
<div class="stat-label">Completed</div>
</div>
</div>
<div class="section-title">New Request</div>
<div class="categories">
<div class="cat-card" onclick="openModal('food')">
<span class="cat-icon">🍽️</span>
<div class="cat-name">Food & Beverage</div>
<div class="cat-desc">Meals, drinks and kitchen supplies</div>
<div class="cat-arrow"><svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="#9A8E80" stroke-width="2.5"><polyline points="9 18 15 12 9 6"/></svg></div>
</div>
<div class="cat-card" onclick="openModal('cleaning')">
<span class="cat-icon">🧹</span>
<div class="cat-name">Cleaning</div>
<div class="cat-desc">Housekeeping and supplies</div>
<div class="cat-arrow"><svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="#9A8E80" stroke-width="2.5"><polyline points="9 18 15 12 9 6"/></svg></div>
</div>
<div class="cat-card" onclick="openModal('medical')">
<span class="cat-icon">💊</span>
<div class="cat-name">Medical</div>
<div class="cat-desc">Medications and first aid</div>
<div class="cat-arrow"><svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="#9A8E80" stroke-width="2.5"><polyline points="9 18 15 12 9 6"/></svg></div>
</div>
<div class="cat-card" onclick="openModal('maintenance')">
<span class="cat-icon">🔧</span>
<div class="cat-name">Maintenance</div>
<div class="cat-desc">Repairs and technical issues</div>
<div class="cat-arrow"><svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="#9A8E80" stroke-width="2.5"><polyline points="9 18 15 12 9 6"/></svg></div>
</div>
<div class="cat-card" onclick="openModal('other')">
<span class="cat-icon">📋</span>
<div class="cat-name">Other</div>
<div class="cat-desc">Any other request</div>
<div class="cat-arrow"><svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="#9A8E80" stroke-width="2.5"><polyline points="9 18 15 12 9 6"/></svg></div>
</div>
<div class="cat-card" onclick="openModal('urgent')">
<span class="cat-icon">🚨</span>
<div class="cat-name">Urgent</div>
<div class="cat-desc">Immediate attention needed</div>
<div class="cat-arrow"><svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="#9A8E80" stroke-width="2.5"><polyline points="9 18 15 12 9 6"/></svg></div>
</div>
</div>
<div class="section-title">Recent Requests</div>
<div id="requests-list">
<div style="text-align:center;padding:30px;color:var(--light);font-size:12px;">No requests yet — submit your first request above</div>
</div>
</div>
<!-- Bottom Nav -->
<div class="bottom-nav">
<button class="nav-item active">
<div class="nav-icon">🏠</div>
<div class="nav-label">Home</div>
</button>
<button class="nav-item">
<div class="nav-icon">📋</div>
<div class="nav-label">Requests</div>
</button>
<button class="nav-item">
<div class="nav-icon">📦</div>
<div class="nav-label">Inventory</div>
</button>
<button class="nav-item">
<div class="nav-icon">👤</div>
<div class="nav-label">Profile</div>
</button>
</div>
</div>
<!-- Modal -->
<div class="modal-overlay" id="modal-overlay" onclick="closeModalOutside(event)">
<div class="modal">
<div class="modal-handle"></div>
<div class="modal-header">
<div class="modal-icon" id="modal-icon">🍽️</div>
<div>
<div class="modal-title" id="modal-title">Food & Beverage</div>
<div class="modal-sub">Fill in the details below</div>
</div>
</div>
<div class="field">
<label class="field-label">Item / Description</label>
<input class="field-input" id="req-item" type="text" placeholder="e.g. Breakfast for 2 persons" />
</div>
<div class="field">
<label class="field-label">Quantity</label>
<input class="field-input" id="req-qty" type="number" placeholder="e.g. 2" min="1" />
</div>
<div class="field">
<label class="field-label">Notes (optional)</label>
<textarea class="field-input" id="req-notes" placeholder="Any additional details..."></textarea>
</div>
<div class="field">
<label class="field-label">Priority</label>
<div class="priority-row">
<button class="priority-btn sel-normal" onclick="setPriority(this,'normal')">Normal</button>
<button class="priority-btn" onclick="setPriority(this,'urgent')">Urgent</button>
<button class="priority-btn" onclick="setPriority(this,'critical')">Critical</button>
</div>
</div>
<button class="submit-btn" id="submit-btn" onclick="submitRequest()">Submit Request</button>
</div>
</div>
<!-- Toast -->
<div class="toast" id="toast">
<span>✓</span>
<span id="toast-msg">Request submitted</span>
</div>
<script>
// ══════════════════════════════════════════════════════
// ⚙️ FIREBASE CONFIG — أضف قيمك من Firebase Console
// Project Settings → General → Your apps → SDK setup
// ══════════════════════════════════════════════════════
const firebaseConfig = {
apiKey: "AIzaSyD6xXghMTa89V6Z8izqI8bjln60u_mGcvo",
authDomain: "aura-concierge-9d3d5.firebaseapp.com",
projectId: "aura-concierge-9d3d5",
storageBucket: "aura-concierge-9d3d5.firebasestorage.app",
messagingSenderId: "827785489832",
appId: "1:827785489832:web:9304fd86a1334d9e84c19d",
measurementId: "G-7L9EDP1KRN"
};
firebase.initializeApp(firebaseConfig);
const auth = firebase.auth();
// ══════ WHITELIST — أرقام مصرّح لها فقط ══════
// مخزّنة بصيغة 05XXXXXXXX للمقارنة المبسّطة
const ALLOWED = [
'0591800720',
'0500379291',
'0557024813',
'0555411407'
];
// ══════ STATE ══════
let requests = [];
let currentCat = 'food';
let currentPriority = 'normal';
let timerInterval;
let seconds = 60;
let confirmationResult = null; // Firebase OTP result
let recaptchaVerifier = null; // Firebase reCAPTCHA
const cats = {
food: { icon:'🍽️', title:'Food & Beverage' },
cleaning: { icon:'🧹', title:'Cleaning' },
medical: { icon:'💊', title:'Medical' },
maintenance: { icon:'🔧', title:'Maintenance' },
other: { icon:'📋', title:'Other Request' },
urgent: { icon:'🚨', title:'Urgent Request' },
};
// ══════ PAGE SWITCH ══════
function showPage(id) {
document.querySelectorAll('.page').forEach(p => p.classList.remove('active'));
document.getElementById(id).classList.add('active');
window.scrollTo(0,0);
}
// ══════ GREETING ══════
function setGreeting() {
const h = new Date().getHours();
const g = h < 12 ? 'Good morning' : h < 17 ? 'Good afternoon' : 'Good evening';
document.getElementById('greeting-time').textContent = g;
}
// ══════ LOGIN INPUTS ══════
const phoneInput = document.getElementById('phone-input');
const sendBtn = document.getElementById('send-btn');
const otpInputs = document.querySelectorAll('.otp-input');
const verifyBtn = document.getElementById('verify-btn');
phoneInput.addEventListener('input', () => {
phoneInput.value = phoneInput.value.replace(/\D/g,'');
sendBtn.disabled = phoneInput.value.length < 9;
});
phoneInput.addEventListener('keydown', e => {
if (e.key === 'Enter' && !sendBtn.disabled) sendOTP();
});
// ══════ HELPERS ══════
// تحويل الرقم المُدخل إلى صيغة +966
function toE164(raw) {
const digits = raw.replace(/\D/g, '');
// إذا يبدأ بـ 0 → نحذفه ونضيف +966
if (digits.startsWith('0')) return '+966' + digits.slice(1);
// إذا يبدأ بـ 966 → نضيف +
if (digits.startsWith('966')) return '+' + digits;
// غير ذلك نضيف +966 مباشرة
return '+966' + digits;
}
// تطبيع للمقارنة مع الـ whitelist (05XXXXXXXXX)
function toLocalFormat(raw) {
const digits = raw.replace(/\D/g, '');
if (digits.startsWith('966')) return '0' + digits.slice(3);
if (!digits.startsWith('0')) return '0' + digits;
return digits;
}
function isAllowed(raw) {
const local = toLocalFormat(raw);
return ALLOWED.includes(local);
}
// تهيئة reCAPTCHA Invisible — مرة واحدة فقط
function initRecaptcha() {
if (recaptchaVerifier) return;
recaptchaVerifier = new firebase.auth.RecaptchaVerifier('recaptcha-container', {
size: 'invisible',
callback: () => {},
'expired-callback': () => { recaptchaVerifier = null; }
});
}
// ══════ SEND OTP — Firebase ══════
async function sendOTP() {
const raw = phoneInput.value.trim();
// 1. فحص الـ whitelist أولاً — قبل أي طلب لـ Firebase
if (!isAllowed(raw)) {
showPhoneError('⛔ This number is not authorized to access Purveyor.');
return;
}
const phoneNumber = toE164(raw);
sendBtn.textContent = 'Sending...';
sendBtn.disabled = true;
try {
initRecaptcha();
confirmationResult = await auth.signInWithPhoneNumber(phoneNumber, recaptchaVerifier);
showOTPScreen();
} catch (err) {
console.error('SMS Error:', err);
// إعادة تهيئة reCAPTCHA عند الخطأ
if (recaptchaVerifier) { recaptchaVerifier.clear(); recaptchaVerifier = null; }
sendBtn.textContent = 'Send Verification Code';
sendBtn.disabled = false;
const msg = err.code === 'auth/invalid-phone-number'
? '⚠️ Invalid phone number format.'
: err.code === 'auth/too-many-requests'
? '⚠️ Too many attempts. Please wait and try again.'
: '⚠️ Failed to send code. Please try again.';
showPhoneError(msg);
}
}
function showPhoneError(msg) {
const wrapper = document.querySelector('.phone-wrapper');
const note = document.querySelector('.note');
wrapper.style.borderColor = '#C04040';
wrapper.style.boxShadow = '0 0 0 4px rgba(192,64,64,0.1)';
note.style.color = '#C04040';
note.textContent = msg;
setTimeout(() => {
wrapper.style.borderColor = '';
wrapper.style.boxShadow = '';
note.style.color = '';
note.textContent = 'A 6-digit code will be sent to your registered number';
}, 3500);
}
function showOTPScreen() {
document.getElementById('screen-phone').classList.remove('active');
document.getElementById('screen-otp').classList.add('active');
document.getElementById('phone-display').textContent = '+966 ' + phoneInput.value.replace(/^0/,'');
document.getElementById('dot1').classList.replace('active','done');
document.getElementById('dot2').classList.add('active');
otpInputs.forEach(i => { i.value=''; i.classList.remove('filled','error','success'); });
verifyBtn.disabled = true;
startTimer();
setTimeout(() => otpInputs[0].focus(), 100);
}
// ══════ OTP INPUTS ══════
otpInputs.forEach((input, idx) => {
input.addEventListener('input', e => {
const val = e.target.value.replace(/\D/g,'');
e.target.value = val.slice(-1);
if (val) {
input.classList.add('filled');
input.classList.remove('error');
if (idx < otpInputs.length - 1) otpInputs[idx+1].focus();
} else {
input.classList.remove('filled');
}
checkOTP();
});
input.addEventListener('keydown', e => {
if (e.key === 'Backspace' && !input.value && idx > 0) {
otpInputs[idx-1].focus();
otpInputs[idx-1].value = '';
otpInputs[idx-1].classList.remove('filled');
checkOTP();
}
if (e.key === 'ArrowLeft' && idx > 0) otpInputs[idx-1].focus();
if (e.key === 'ArrowRight' && idx < otpInputs.length-1) otpInputs[idx+1].focus();
});
input.addEventListener('paste', e => {
e.preventDefault();
const text = (e.clipboardData||window.clipboardData).getData('text').replace(/\D/g,'');
text.split('').slice(0,6).forEach((c,i) => {
if (otpInputs[i]) { otpInputs[i].value=c; otpInputs[i].classList.add('filled'); }
});
const nx = [...otpInputs].findIndex(i => !i.value);
(nx !== -1 ? otpInputs[nx] : otpInputs[5]).focus();
checkOTP();
});
});
function checkOTP() {
verifyBtn.disabled = ![...otpInputs].every(i => i.value.length === 1);
}
// ══════ TIMER ══════
function startTimer() {
clearInterval(timerInterval);
seconds = 60;
document.getElementById('resend-btn').classList.remove('visible');
updateTimer();
timerInterval = setInterval(() => {
seconds--;
updateTimer();
if (seconds <= 0) {
clearInterval(timerInterval);
document.getElementById('resend-btn').classList.add('visible');
document.getElementById('timer').textContent = '00:00';
}
}, 1000);
}
function updateTimer() {
const m = String(Math.floor(seconds/60)).padStart(2,'0');
const s = String(seconds%60).padStart(2,'0');
document.getElementById('timer').textContent = `${m}:${s}`;
}
// ══════ RESEND ══════
async function resendOTP() {
otpInputs.forEach(i => { i.value=''; i.classList.remove('filled','error','success'); });
verifyBtn.disabled = true;
// نعيد تهيئة reCAPTCHA ثم نرسل من جديد
if (recaptchaVerifier) { recaptchaVerifier.clear(); recaptchaVerifier = null; }
await sendOTP();
}
// ══════ VERIFY OTP — Firebase ══════
async function verifyOTP() {
if (!confirmationResult) return;
const code = [...otpInputs].map(i => i.value).join('');
verifyBtn.textContent = 'Verifying...';
verifyBtn.disabled = true;
try {
await confirmationResult.confirm(code);
// نجاح ✅
otpInputs.forEach(i => i.classList.add('success'));
clearInterval(timerInterval);
setTimeout(() => {
const phone = phoneInput.value;
document.getElementById('user-phone-display').textContent = '+966 ' + phone.replace(/^0/,'');
document.getElementById('user-initial').textContent = phone.slice(-1).toUpperCase();
setGreeting();
showPage('page-requests');
verifyBtn.textContent = 'Authenticate System';
verifyBtn.disabled = false;
}, 600);
} catch (err) {
console.error('Verify Error:', err);
// رمز خاطئ
otpInputs.forEach(i => i.classList.add('error'));
setTimeout(() => otpInputs.forEach(i => i.classList.remove('error')), 700);
verifyBtn.textContent = 'Authenticate System';
verifyBtn.disabled = false;
showToast('⛔ Incorrect code — please try again');
}
}
// ══════ BACK ══════
function goBack() {
clearInterval(timerInterval);
confirmationResult = null;
document.getElementById('screen-otp').classList.remove('active');
document.getElementById('screen-phone').classList.add('active');
document.getElementById('dot1').classList.replace('done','active');
document.getElementById('dot2').classList.remove('active');
sendBtn.textContent = 'Send Verification Code';
sendBtn.disabled = false;
}
// ══════ MODAL ══════
function openModal(cat) {
currentCat = cat;
document.getElementById('modal-icon').textContent = cats[cat].icon;
document.getElementById('modal-title').textContent = cats[cat].title;
document.getElementById('req-item').value = '';
document.getElementById('req-qty').value = '';
document.getElementById('req-notes').value = '';
document.querySelectorAll('.priority-btn').forEach(b => b.className = 'priority-btn');
document.querySelectorAll('.priority-btn')[0].classList.add('sel-normal');
currentPriority = 'normal';
document.getElementById('modal-overlay').classList.add('open');
setTimeout(() => document.getElementById('req-item').focus(), 400);
}
function closeModalOutside(e) {
if (e.target === document.getElementById('modal-overlay')) closeModal();
}
function closeModal() {
document.getElementById('modal-overlay').classList.remove('open');
}
function setPriority(btn, type) {
document.querySelectorAll('.priority-btn').forEach(b => b.className = 'priority-btn');
btn.classList.add('sel-' + type);
currentPriority = type;
}
function submitRequest() {
const item = document.getElementById('req-item').value.trim();
if (!item) {
const inp = document.getElementById('req-item');
inp.style.borderColor = '#C04040';
inp.focus();
setTimeout(() => inp.style.borderColor='', 1500);
return;
}
const btn = document.getElementById('submit-btn');
btn.textContent = 'Submitting...';
btn.disabled = true;
const req = {
id: Date.now(),
cat: currentCat,
icon: cats[currentCat].icon,
title: cats[currentCat].title,
item,
priority: currentPriority,
qty: document.getElementById('req-qty').value || '1',
notes: document.getElementById('req-notes').value,
time: new Date().toLocaleTimeString('en-US',{hour:'2-digit',minute:'2-digit'}),
status: 'pending'
};
requests.unshift(req);
updateStats();
renderRequests();
closeModal();
showToast('✓ Request submitted successfully');
btn.textContent = 'Submit Request';
btn.disabled = false;
}
function updateStats() {
document.getElementById('stat-total').textContent = requests.length;
document.getElementById('stat-pending').textContent = requests.filter(r=>r.status==='pending').length;
document.getElementById('stat-done').textContent = requests.filter(r=>r.status==='done').length;
}
function renderRequests() {
const list = document.getElementById('requests-list');
if (requests.length === 0) {
list.innerHTML = '<div style="text-align:center;padding:30px;color:var(--light);font-size:12px;">No requests yet</div>';
return;
}
list.innerHTML = requests.map(r => `
<div class="request-item">
<div class="req-icon">${r.icon}</div>
<div class="req-info">
<div class="req-title">${r.item} × ${r.qty}</div>
<div class="req-meta">${r.title} · Today ${r.time}</div>
</div>
<div class="req-status status-${r.status}">${r.status.charAt(0).toUpperCase()+r.status.slice(1)}</div>
</div>
`).join('');
}
function showToast(msg) {
const t = document.getElementById('toast');
document.getElementById('toast-msg').textContent = msg;
t.classList.add('show');
setTimeout(() => t.classList.remove('show'), 3000);
}
document.addEventListener('keydown', e => { if (e.key==='Escape') closeModal(); });
</script>
</body>
</html>