/* Shared store — used by both customer site and admin dashboard. Persists in localStorage. Seeded with demo data on first load. */ const STORE_KEYS = { requests: 'prestige.requests', session: 'prestige.adminSession', }; const ADMIN_USER = { username: 'admin', password: 'prestige2026' }; const SESSION_TTL_HOURS = 12; const STATUS = { PENDING: 'pending', CONFIRMED: 'confirmed', COMPLETED: 'completed', CANCELLED: 'cancelled', }; const STATUS_LABEL = { pending: 'Pending review', confirmed: 'Confirmed', completed: 'Completed', cancelled: 'Cancelled', }; /* --- Date helpers ----------------------------------------- */ const ymd = (d) => { const dt = (d instanceof Date) ? d : new Date(d); const y = dt.getFullYear(); const m = String(dt.getMonth()+1).padStart(2,'0'); const day = String(dt.getDate()).padStart(2,'0'); return `${y}-${m}-${day}`; }; const fromYmd = (s) => { const [y, m, d] = s.split('-').map(Number); return new Date(y, m-1, d); }; const today = () => { const d = new Date(); d.setHours(0,0,0,0); return d; }; const addDays = (d, n) => { const r = new Date(d); r.setDate(r.getDate()+n); return r; }; const fmtDate = (d) => (d instanceof Date ? d : fromYmd(d)).toLocaleDateString('en-US',{ weekday:'short', month:'short', day:'numeric' }); const fmtDateLong = (d) => (d instanceof Date ? d : fromYmd(d)).toLocaleDateString('en-US',{ weekday:'long', year:'numeric', month:'long', day:'numeric' }); /* --- Seed mock data --------------------------------------- */ const VEHICLE_OPTIONS = [ { id:'sedan', label:'Sedan / Hatchback', mult:1.00, base:60 }, { id:'suv', label:'SUV', mult:1.25, base:60 }, { id:'pickup', label:'Pickup Truck', mult:1.35, base:60 }, { id:'van', label:'Van / Family', mult:1.30, base:60 }, { id:'lux', label:'Luxury / Sports', mult:1.45, base:60 }, { id:'comm', label:'Commercial Van', mult:1.55, base:60 }, ]; const SERVICE_OPTIONS = [ { id:'ext', label:'Exterior Wash', duration:45, price:0 }, { id:'both', label:'Interior & Exterior', duration:75, price:30 }, { id:'detail', label:'Full Detail', duration:150, price:90 }, { id:'intd', label:'Interior Deep Cleaning', duration:120, price:70 }, { id:'wax', label:'Wax & Paint Protection', duration:90, price:60 }, { id:'spa', label:'Premium Auto Spa', duration:210, price:160 }, ]; const SLOT_OPTIONS = ['8:00 AM','9:30 AM','11:00 AM','12:30 PM','2:00 PM','3:30 PM','5:00 PM']; const buildSeed = () => { const t = today(); const make = (offset, slot, vehId, svcId, status, customer) => { const veh = VEHICLE_OPTIONS.find(v => v.id === vehId); const svc = SERVICE_OPTIONS.find(s => s.id === svcId); const subtotal = Math.round((veh.base + svc.price) * veh.mult); return { id: `REQ-${Date.now().toString(36).slice(-4)}-${Math.random().toString(36).slice(2,6).toUpperCase()}`, createdAt: addDays(t, offset - 2).toISOString(), status, paid: status !== 'cancelled', customer, address: { line: customer.addr || '1245 Sunset Blvd', city: customer.city || 'Beverly Hills', zip: customer.zip || '90210', notes: '' }, vehicle: veh, service: svc, date: ymd(addDays(t, offset)), slot, payment: 'card', promo: null, pricing: { subtotal, total: Math.round(subtotal * 1.04) }, emailSent: status === 'confirmed' || status === 'completed', emailSentAt: status === 'confirmed' || status === 'completed' ? addDays(t, offset - 1).toISOString() : null, notes: '', }; }; return [ make(-3, '11:00 AM', 'lux', 'spa', 'completed', { name:'Marisa Velasquez', email:'marisa.v@example.com', phone:'(310) 555-0142', addr:'9100 Wilshire Blvd', city:'Beverly Hills', zip:'90212' }), make(-1, '2:00 PM', 'sedan', 'both', 'completed', { name:'Daniel Kim', email:'dkim@example.com', phone:'(424) 555-0188', addr:'500 Ocean Ave', city:'Santa Monica', zip:'90402' }), make(0, '9:30 AM', 'suv', 'detail', 'confirmed', { name:'Sofia Ramos', email:'sofia.r@example.com', phone:'(310) 555-0177', addr:'250 Newport Center Dr', city:'Newport Beach', zip:'92660' }), make(0, '2:00 PM', 'lux', 'wax', 'pending', { name:'James Lambert', email:'jlambert@example.com', phone:'(310) 555-0103', addr:'27900 Pacific Coast Hwy', city:'Malibu', zip:'90265' }), make(1, '8:00 AM', 'pickup', 'ext', 'pending', { name:'Carla Mendoza', email:'cmendoza@example.com', phone:'(323) 555-0150', addr:'1100 Sunset Plaza Dr', city:'West Hollywood', zip:'90069' }), make(1, '12:30 PM', 'sedan', 'both', 'confirmed', { name:'Ethan Park', email:'epark@example.com', phone:'(310) 555-0166', addr:'620 Manhattan Ave', city:'Manhattan Beach', zip:'90266' }), make(2, '11:00 AM', 'van', 'intd', 'pending', { name:'Priya Shah', email:'pshah@example.com', phone:'(310) 555-0124', addr:'1800 Avenue of the Stars', city:'Los Angeles', zip:'90067' }), make(3, '9:30 AM', 'suv', 'detail', 'confirmed', { name:'Aaron Reyes', email:'areyes@example.com', phone:'(424) 555-0192', addr:'200 Pier Ave', city:'Hermosa Beach', zip:'90254' }), make(4, '3:30 PM', 'comm', 'ext', 'pending', { name:'Logistics Co.', email:'ops@logisticsco.example.com', phone:'(310) 555-0145', addr:'5000 W Century Blvd', city:'Los Angeles', zip:'90045' }), make(7, '11:00 AM', 'lux', 'spa', 'pending', { name:'Maya Brennan', email:'mb@example.com', phone:'(310) 555-0118', addr:'1500 Stone Canyon Rd', city:'Bel Air', zip:'90077' }), ]; }; /* --- Storage primitives ---------------------------------- */ const readJSON = (key, fallback) => { try { const v = localStorage.getItem(key); return v ? JSON.parse(v) : fallback; } catch { return fallback; } }; const writeJSON = (key, value) => { try { localStorage.setItem(key, JSON.stringify(value)); } catch {} }; /* --- Public store API ------------------------------------ */ const Store = { STATUS, STATUS_LABEL, SLOT_OPTIONS, ensureSeed(){ const cur = readJSON(STORE_KEYS.requests, null); if (!cur || !Array.isArray(cur)) writeJSON(STORE_KEYS.requests, buildSeed()); }, resetSeed(){ writeJSON(STORE_KEYS.requests, buildSeed()); }, getRequests(){ Store.ensureSeed(); return readJSON(STORE_KEYS.requests, []); }, setRequests(list){ writeJSON(STORE_KEYS.requests, list); window.dispatchEvent(new CustomEvent('prestige:requests-changed')); }, upsert(req){ const list = Store.getRequests(); const idx = list.findIndex(r => r.id === req.id); if (idx >= 0) list[idx] = req; else list.unshift(req); Store.setRequests(list); }, updateStatus(id, status, extra={}){ const list = Store.getRequests(); const idx = list.findIndex(r => r.id === id); if (idx < 0) return null; list[idx] = { ...list[idx], status, ...extra }; Store.setRequests(list); return list[idx]; }, /* Slot blocking — takes a Date or YYYY-MM-DD, returns set of taken slot strings */ takenSlotsForDate(date){ const key = (typeof date === 'string') ? date : ymd(date); const blockingStatuses = new Set([STATUS.PENDING, STATUS.CONFIRMED, STATUS.COMPLETED]); const reqs = Store.getRequests(); return new Set( reqs .filter(r => r.date === key && r.paid && blockingStatuses.has(r.status)) .map(r => r.slot) ); }, /* Auth — mock username/password, session in localStorage */ login(username, password){ const u = (username || '').trim().toLowerCase(); const p = (password || '').trim(); if (u !== ADMIN_USER.username.toLowerCase() || p !== ADMIN_USER.password) { return { ok: false, error: 'Invalid username or password' }; } const session = { user: username, issuedAt: Date.now(), expiresAt: Date.now() + SESSION_TTL_HOURS * 60 * 60 * 1000, }; writeJSON(STORE_KEYS.session, session); return { ok: true, session }; }, logout(){ try { localStorage.removeItem(STORE_KEYS.session); } catch {} }, getSession(){ const s = readJSON(STORE_KEYS.session, null); if (!s || s.expiresAt < Date.now()) return null; return s; }, /* Email mock — replace with EmailJS/backend call later */ sendConfirmationEmail(req){ return new Promise((resolve) => { setTimeout(() => { Store.updateStatus(req.id, STATUS.CONFIRMED, { emailSent: true, emailSentAt: new Date().toISOString(), }); resolve({ ok: true, to: req.customer.email }); }, 700); }); }, buildEmailPreview(req){ const dStr = fmtDateLong(req.date); const total = req.pricing?.total ? `$${req.pricing.total.toFixed(2)}` : '—'; return { to: req.customer.email, subject: `Your Prestige Auto Spa booking is confirmed — ${dStr} · ${req.slot}`, body: [ `Hi ${req.customer.name.split(' ')[0]},`, ``, `Your booking with Prestige Auto Spa Mobile is confirmed.`, ``, `Service: ${req.service.label}`, `Vehicle: ${req.vehicle.label}`, `Date: ${dStr}`, `Time: ${req.slot}`, `Where: ${req.address.line}, ${req.address.city} ${req.address.zip}`, `Total: ${total}`, ``, `Our technician will arrive on time and ready. If anything changes, reply to this email or call (310) 555-0110.`, ``, `— Prestige Auto Spa Mobile`, ].join('\n'), }; }, }; /* --- Date utils exposed for components ------------------- */ const DateUtils = { ymd, fromYmd, today, addDays, fmtDate, fmtDateLong }; window.PrestigeStore = Store; window.PrestigeDate = DateUtils;