<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>BELT Hub Valet System</title>
<script src="https://cdn.jsdelivr.net/npm/@supabase/supabase-js@2"></script>
<script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<script src="https://cdn.tailwindcss.com"></script>
<style>
body { background: linear-gradient(135deg, #f5f7fa 0%, #e8f5e9 100%); }
</style>
</head>
<body>
<div id="root"></div>
<script type="text/babel">
const { useState, useEffect, useRef } = React;
// ========== SUPABASE CONFIGURATION ==========
// 🔧 REPLACE THESE WITH YOUR SUPABASE CREDENTIALS
const SUPABASE_URL = 'https://ajslelewneljqbrusojm.supabase.co;
const SUPABASE_ANON_KEY = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImFqc2xlbGV3bmVsanFicnVzb2ptIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzAzOTc1MzAsImV4cCI6MjA4NTk3MzUzMH0.Q8HMQUWk0gCGZsBx45BNo8uYyYAESZlyjkcMQ6TulDc';
// Initialize Supabase client
const supabase = window.supabase.createClient(SUPABASE_URL, SUPABASE_ANON_KEY);
// ========== HELPER FUNCTIONS ==========
// Convert data URL to File for upload
const dataURLtoFile = (dataurl, filename) => {
const arr = dataurl.split(',');
const mime = arr[0].match(/:(.*?);/)[1];
const bstr = atob(arr[1]);
let n = bstr.length;
const u8arr = new Uint8Array(n);
while(n--) {
u8arr[n] = bstr.charCodeAt(n);
}
return new File([u8arr], filename, {type:mime});
};
// ========== MODE SELECTOR ==========
const BELTValetSystem = () => {
const [mode, setMode] = useState(null);
if (!mode) {
return (
<div className="min-h-screen flex items-center justify-center p-4">
<div className="bg-white rounded-2xl shadow-2xl p-8 max-w-md w-full">
<div className="text-center mb-8">
<div className="w-24 h-24 mx-auto mb-4 bg-[#689c54] rounded-full flex items-center justify-center">
<svg className="w-16 h-16 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" />
</svg>
</div>
<h1 className="text-3xl font-bold text-gray-800">BELT Hub Valet</h1>
<p className="text-gray-600 mt-2">Secure bike parking for Atlanta</p>
</div>
<div className="space-y-4">
<button
onClick={() => setMode('customer')}
className="w-full bg-[#689c54] hover:bg-[#5a8448] text-white font-bold py-4 px-6 rounded-lg transition-all transform hover:scale-105"
>
<div className="text-lg">I'm a Customer</div>
<div className="text-sm opacity-90">Check in / Manage my session</div>
</button>
<button
onClick={() => setMode('staff')}
className="w-full bg-gray-800 hover:bg-gray-700 text-white font-bold py-4 px-6 rounded-lg transition-all transform hover:scale-105"
>
<div className="text-lg">I'm Staff</div>
<div className="text-sm opacity-90">Access dashboard & exports</div>
</button>
</div>
</div>
</div>
);
}
if (mode === 'customer') {
return <CustomerApp onBack={() => setMode(null)} />;
}
return <StaffDashboard onBack={() => setMode(null)} />;
};
// ========== CUSTOMER APP ==========
const CustomerApp = ({ onBack }) => {
const [step, setStep] = useState('location');
const [sessionData, setSessionData] = useState(null);
const [phone, setPhone] = useState('');
const [location, setLocation] = useState('');
const [bikeCount, setBikeCount] = useState(1);
const [showCamera, setShowCamera] = useState(false);
const [photoData, setPhotoData] = useState(null);
const [waiverAccepted, setWaiverAccepted] = useState(false);
const [loading, setLoading] = useState(false);
const videoRef = useRef(null);
const canvasRef = useRef(null);
const locations = [
'The BELT Hub Ponce City Market',
'The BELT Hub West Midtown'
];
const handleLocationSelect = (loc) => {
setLocation(loc);
setStep('waiver');
};
const handleWaiverAccept = () => {
setWaiverAccepted(true);
setStep('phone');
};
const startCamera = async () => {
try {
const stream = await navigator.mediaDevices.getUserMedia({
video: { facingMode: 'environment' }
});
if (videoRef.current) {
videoRef.current.srcObject = stream;
}
setShowCamera(true);
} catch (err) {
alert('Camera access required to complete check-in. Please enable camera permissions.');
}
};
const capturePhoto = () => {
const canvas = canvasRef.current;
const video = videoRef.current;
canvas.width = video.videoWidth;
canvas.height = video.videoHeight;
canvas.getContext('2d').drawImage(video, 0, 0);
const photo = canvas.toDataURL('image/jpeg', 0.8);
const stream = video.srcObject;
stream.getTracks().forEach(track => track.stop());
setShowCamera(false);
setPhotoData(photo);
};
const handlePhoneSubmit = async (e) => {
e.preventDefault();
setLoading(true);
// Check for existing active session
const { data: existingSessions } = await supabase
.from('sessions')
.select('*')
.eq('phone', phone)
.eq('status', 'active')
.single();
if (existingSessions) {
setSessionData(existingSessions);
setLoading(false);
setStep('active');
} else {
// Require photo
if (!photoData) {
setLoading(false);
setStep('photo');
return;
}
// Upload photo to Supabase Storage
const sessionId = Date.now();
const photoFile = dataURLtoFile(photoData, `bike-${sessionId}.jpg`);
const { data: photoUpload, error: photoError } = await supabase.storage
.from('bike-photos')
.upload(`${sessionId}.jpg`, photoFile);
if (photoError) {
console.error('Photo upload error:', photoError);
alert('Error uploading photo. Please try again.');
setLoading(false);
return;
}
// Get public URL for photo
const { data: { publicUrl } } = supabase.storage
.from('bike-photos')
.getPublicUrl(`${sessionId}.jpg`);
// Create new session in database
const newSession = {
id: sessionId,
phone: phone,
location: location,
bike_count: parseInt(bikeCount),
check_in_time: new Date().toISOString(),
status: 'active',
photo_url: publicUrl,
waiver_accepted: true,
waiver_timestamp: new Date().toISOString()
};
const { data, error } = await supabase
.from('sessions')
.insert([newSession])
.select()
.single();
if (error) {
console.error('Session creation error:', error);
alert('Error creating session. Please try again.');
setLoading(false);
return;
}
// Also save to photos table for weekly dump
await supabase.from('photos').insert([{
id: Date.now(),
session_id: sessionId,
phone: phone,
photo_url: publicUrl,
location: location,
bike_count: parseInt(bikeCount),
timestamp: new Date().toISOString()
}]);
setSessionData(data);
setLoading(false);
setStep('active');
}
};
const getTimeElapsed = () => {
if (!sessionData) return { hours: 0, minutes: 0, totalMinutes: 0, days: 0 };
const start = new Date(sessionData.check_in_time);
const now = new Date();
const diff = now - start;
const totalMinutes = Math.floor(diff / 60000);
const hours = Math.floor(totalMinutes / 60);
const minutes = totalMinutes % 60;
const days = Math.floor(hours / 24);
return { hours, minutes, totalMinutes, days };
};
const calculateStorageFee = () => {
const { days } = getTimeElapsed();
if (days === 0) return 0;
return Math.min(days, 10) * 15 * (sessionData?.bike_count || 1);
};
const calculateHourlyCharge = () => {
const { totalMinutes } = getTimeElapsed();
if (totalMinutes <= 120) return 0;
const chargeableMinutes = totalMinutes - 120;
return (chargeableMinutes / 60).toFixed(2);
};
const getTotalCharge = () => {
const hourly = parseFloat(calculateHourlyCharge());
const storage = calculateStorageFee();
return (hourly + storage).toFixed(2);
};
const handleCheckOut = async () => {
setLoading(true);
const totalCharge = getTotalCharge();
// Update session status
const { error: updateError } = await supabase
.from('sessions')
.update({
status: 'completed',
check_out_time: new Date().toISOString()
})
.eq('id', sessionData.id);
if (updateError) {
console.error('Check-out error:', updateError);
alert('Error checking out. Please see staff.');
setLoading(false);
return;
}
// Save to transactions table for marketing
await supabase.from('transactions').insert([{
id: Date.now(),
session_id: sessionData.id,
phone: sessionData.phone,
location: sessionData.location,
bike_count: sessionData.bike_count,
check_in_time: sessionData.check_in_time,
check_out_time: new Date().toISOString(),
total_charge: parseFloat(totalCharge)
}]);
alert('✅ Checked out successfully! Please show this confirmation to staff to retrieve your bike(s).');
setLoading(false);
setStep('location');
setSessionData(null);
setPhotoData(null);
};
const { hours, minutes, totalMinutes, days } = sessionData ? getTimeElapsed() : { hours: 0, minutes: 0, totalMinutes: 0, days: 0 };
const storageFee = sessionData ? calculateStorageFee() : 0;
const hourlyCharge = sessionData ? calculateHourlyCharge() : 0;
const totalCharge = sessionData ? getTotalCharge() : 0;
const minutesUntilCharge = Math.max(0, 120 - totalMinutes);
const isInFreeTime = totalMinutes < 120;
const isNearExpiry = totalMinutes >= 105 && totalMinutes < 120;
const isAbandoned = days >= 10;
// Location Selection Screen
if (step === 'location') {
return (
<div className="min-h-screen p-4">
<div className="max-w-md mx-auto">
<button onClick={onBack} className="mb-4 text-gray-600 hover:text-gray-800">← Back</button>
<div className="bg-white rounded-2xl shadow-xl p-8">
<div className="text-center mb-6">
<div className="w-20 h-20 mx-auto mb-4 bg-[#689c54] rounded-full flex items-center justify-center">
<svg className="w-12 h-12 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 11a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
</div>
<h2 className="text-2xl font-bold text-gray-800">Select Your Location</h2>
<p className="text-gray-600 mt-2">Where are you parking today?</p>
</div>
<div className="space-y-3">
{locations.map(loc => (
<button
key={loc}
onClick={() => handleLocationSelect(loc)}
className="w-full p-6 border-2 border-gray-200 rounded-lg hover:border-[#689c54] hover:bg-green-50 transition-all text-left group"
>
<div className="flex items-center gap-3">
<div className="w-12 h-12 bg-[#689c54] rounded-full flex items-center justify-center group-hover:scale-110 transition-transform">
<svg className="w-7 h-7 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4" />
</svg>
</div>
<div className="flex-1">
<div className="font-bold text-lg text-gray-800">{loc}</div>
<div className="text-sm text-gray-500">Tap to check in here</div>
</div>
<svg className="w-6 h-6 text-gray-400 group-hover:text-[#689c54]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
</svg>
</div>
</button>
))}
</div>
</div>
</div>
</div>
);
}
// Waiver Screen
if (step === 'waiver') {
return (
<div className="min-h-screen p-4">
<div className="max-w-2xl mx-auto">
<button onClick={() => setStep('location')} className="mb-4 text-gray-600 hover:text-gray-800">← Back</button>
<div className="bg-white rounded-2xl shadow-xl p-8">
<h2 className="text-2xl font-bold text-gray-800 mb-4">Terms & Conditions</h2>
<div className="bg-gray-50 rounded-lg p-6 mb-6 max-h-96 overflow-y-auto">
<h3 className="font-bold text-lg mb-3">BELT Hub Valet Service Agreement</h3>
<div className="space-y-4 text-sm text-gray-700">
<div className="bg-green-50 border-2 border-green-300 rounded-lg p-4">
<h4 className="font-bold mb-2 text-green-900">✨ PRICING</h4>
<p className="mb-2"><strong>First 2 hours: FREE</strong></p>
<p><strong>After 2 hours: $1 per hour</strong></p>
</div>
<div className="bg-yellow-50 border-2 border-yellow-300 rounded-lg p-4">
<h4 className="font-bold mb-2 text-yellow-900">⚠️ OVERNIGHT STORAGE FEES</h4>
<p className="mb-2"><strong>Bikes left overnight will be charged $15 per day PER BIKE.</strong></p>
<ul className="list-disc ml-5 space-y-1">
<li>Storage fees apply for up to 10 days maximum</li>
<li>Fees accumulate daily starting at midnight</li>
</ul>
</div>
<div className="bg-red-50 border-2 border-red-300 rounded-lg p-4">
<h4 className="font-bold mb-2 text-red-900">🚨 ABANDONMENT POLICY</h4>
<p className="mb-2"><strong>After 10 days, bikes are considered ABANDONED and will be donated to local community organizations.</strong></p>
<ul className="list-disc ml-5 space-y-1">
<li>Day 8-9: Final warning notifications sent</li>
<li>Day 10: ALL bikes transferred to donation program</li>
<li>No refunds for abandoned bikes</li>
</ul>
</div>
<div>
<h4 className="font-semibold mb-1">Liability</h4>
<p>While we take every precaution to secure your bikes, BELT Hub is not responsible for theft, damage, or loss.</p>
</div>
<div>
<h4 className="font-semibold mb-1">Photo Documentation</h4>
<p>You will be required to provide a photo of your bike(s) for identification purposes when you return to collect them.</p>
</div>
</div>
</div>
<div className="bg-green-50 border border-green-200 rounded-lg p-4 mb-6">
<label className="flex items-start gap-3 cursor-pointer">
<input
type="checkbox"
checked={waiverAccepted}
onChange={(e) => setWaiverAccepted(e.target.checked)}
className="mt-1 w-5 h-5 text-[#689c54] focus:ring-[#689c54]"
/>
<div className="text-sm">
<p className="font-medium text-gray-800">I have read and agree to these terms</p>
<p className="text-gray-600 mt-1">Including $1/hour after 2 free hours, overnight fees, and 10-day donation policy</p>
</div>
</label>
</div>
<button
onClick={handleWaiverAccept}
disabled={!waiverAccepted}
className="w-full bg-[#689c54] hover:bg-[#5a8448] disabled:bg-gray-300 disabled:cursor-not-allowed text-white font-bold py-4 px-6 rounded-lg transition-all"
>
Accept & Continue
</button>
</div>
</div>
</div>
);
}
// Phone + Bike Count Screen
if (step === 'phone') {
return (
<div className="min-h-screen p-4">
<div className="max-w-md mx-auto">
<button onClick={() => setStep('waiver')} className="mb-4 text-gray-600 hover:text-gray-800">← Back</button>
<div className="bg-white rounded-2xl shadow-xl p-8">
<div className="text-center mb-6">
<div className="w-16 h-16 mx-auto mb-4 bg-green-100 rounded-full flex items-center justify-center">
<svg className="w-10 h-10 text-[#689c54]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
<h2 className="text-2xl font-bold text-gray-800">Check In Details</h2>
<p className="text-gray-600 mt-2">Location: <span className="font-medium text-[#689c54]">{location}</span></p>
</div>
<form onSubmit={handlePhoneSubmit} className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">Phone Number</label>
<input
type="tel"
value={phone}
onChange={(e) => setPhone(e.target.value)}
placeholder="(404) 555-0123"
required
className="w-full px-4 py-3 border-2 border-gray-200 rounded-lg focus:ring-2 focus:ring-[#689c54] focus:border-[#689c54] text-lg"
/>
<p className="mt-2 text-xs text-gray-500">We'll text you at 1:45 before hourly charges begin</p>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">How many bikes are you parking?</label>
<div className="grid grid-cols-4 gap-2">
{[1, 2, 3, 4].map(num => (
<button
key={num}
type="button"
onClick={() => setBikeCount(num)}
className={`py-4 px-6 rounded-lg font-bold text-lg transition-all ${
bikeCount === num
? 'bg-[#689c54] text-white scale-105'
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'
}`}
>
{num}
</button>
))}
</div>
</div>
<div className="bg-green-50 border border-green-200 rounded-lg p-4">
<div className="font-medium text-gray-800 mb-2">💰 Pricing Reminder</div>
<div className="text-sm text-gray-600">
<div>• First 2 hours: <strong>FREE</strong></div>
<div>• After 2 hours: <strong>$1 per hour</strong></div>
</div>
</div>
<button
type="submit"
disabled={loading}
className="w-full bg-[#689c54] hover:bg-[#5a8448] disabled:bg-gray-400 text-white font-bold py-4 px-6 rounded-lg transition-all"
>
{loading ? 'Processing...' : '📸 Next: Take Photo of Bike(s)'}
</button>
</form>
</div>
</div>
</div>
);
}
// Photo Capture Screen
if (step === 'photo') {
return (
<div className="min-h-screen p-4">
<div className="max-w-md mx-auto">
<button onClick={() => setStep('phone')} className="mb-4 text-gray-600 hover:text-gray-800">← Back</button>
<div className="bg-white rounded-2xl shadow-xl p-8">
<div className="text-center mb-6">
<div className="w-16 h-16 mx-auto mb-4 bg-blue-100 rounded-full flex items-center justify-center">
<svg className="w-10 h-10 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 9a2 2 0 012-2h.93a2 2 0 001.664-.89l.812-1.22A2 2 0 0110.07 4h3.86a2 2 0 011.664.89l.812 1.22A2 2 0 0018.07 7H19a2 2 0 012 2v9a2 2 0 01-2 2H5a2 2 0 01-2-2V9z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 13a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
</div>
<h2 className="text-2xl font-bold text-gray-800">Take Photo of Your Bike(s)</h2>
<p className="text-gray-600 mt-2">Staff will use this to match your bike(s) when you return</p>
</div>
{showCamera ? (
<div className="mb-6">
<div className="relative rounded-lg overflow-hidden bg-black aspect-[4/3]">
<video ref={videoRef} autoPlay playsInline className="w-full h-full object-cover" />
</div>
<canvas ref={canvasRef} className="hidden" />
<div className="mt-6 space-y-3">
<button
onClick={capturePhoto}
className="w-full bg-[#689c54] hover:bg-[#5a8448] text-white font-bold py-4 px-6 rounded-lg transition-all"
>
📸 Capture Photo
</button>
</div>
<div className="mt-4 bg-blue-50 border border-blue-200 rounded-lg p-4">
<div className="text-sm text-blue-800">
<strong>💡 Tips for a good photo:</strong>
<ul className="mt-2 space-y-1 list-disc ml-5">
<li>Make sure all bikes are visible</li>
<li>Capture distinctive features</li>
<li>Ensure good lighting</li>
</ul>
</div>
</div>
</div>
) : photoData ? (
<div className="mb-6">
<div className="text-sm text-gray-600 mb-2">Your photo:</div>
<img src={photoData} alt="Bike" className="w-full rounded-lg mb-4" />
<div className="space-y-3">
<button
onClick={handlePhoneSubmit}
disabled={loading}
className="w-full bg-[#689c54] hover:bg-[#5a8448] disabled:bg-gray-400 text-white font-bold py-4 px-6 rounded-lg transition-all"
>
{loading ? 'Creating Session...' : '✅ Looks Good - Complete Check-In'}
</button>
<button
onClick={() => setPhotoData(null)}
disabled={loading}
className="w-full bg-gray-200 hover:bg-gray-300 disabled:bg-gray-100 text-gray-800 font-bold py-3 px-6 rounded-lg transition-all"
>
🔄 Retake Photo
</button>
</div>
</div>
) : (
<div className="mb-6">
<button
onClick={startCamera}
className="w-full bg-[#689c54] hover:bg-[#5a8448] text-white font-bold py-4 px-6 rounded-lg transition-all"
>
📷 Open Camera
</button>
<div className="mt-6 bg-yellow-50 border-2 border-yellow-300 rounded-lg p-4">
<div className="font-bold text-yellow-900 mb-2">⚠️ Photo Required</div>
<div className="text-sm text-yellow-800">
A photo is required to complete check-in. This helps our staff identify and return your bike(s) when you come back.
</div>
</div>
</div>
)}
</div>
</div>
</div>
);
}
// Active Session Screen
if (step === 'active') {
return (
<div className="min-h-screen p-4">
<div className="max-w-md mx-auto">
<button onClick={onBack} className="mb-4 text-gray-600 hover:text-gray-800">← Exit Session</button>
<div className="space-y-4">
{sessionData.photo_url && (
<div className="bg-white rounded-2xl shadow-xl p-4">
<div className="text-sm text-gray-600 mb-2 font-medium">Your Bike Photo:</div>
<img src={sessionData.photo_url} alt="Bike" className="w-full rounded-lg" />
</div>
)}
<div className="bg-white rounded-2xl shadow-xl p-8">
<div className="text-center">
<div className="mb-4">
<div className="inline-flex items-center gap-2 bg-[#689c54] text-white px-4 py-2 rounded-full font-bold">
<span className="text-2xl">🚴♂️</span>
<span>{sessionData.bike_count} Bike{sessionData.bike_count > 1 ? 's' : ''} Parked</span>
</div>
</div>
<div className="text-6xl font-bold text-[#689c54] mb-2">
{days > 0 ? `${days}d ` : ''}{hours % 24}:{minutes.toString().padStart(2, '0')}
</div>
<div className="text-gray-600 mb-6">Session Time</div>
{isAbandoned ? (
<div className="bg-red-100 border-2 border-red-500 rounded-lg p-4 mb-4">
<div className="font-bold text-red-900 text-lg">🚨 BIKES ABANDONED</div>
<div className="text-red-800 mt-2">Your bike(s) have been donated after 10 days</div>
</div>
) : isInFreeTime ? (
<div className="bg-green-50 border border-green-200 rounded-lg p-4 mb-4">
<div className="font-medium text-green-800">✨ Free Time Remaining</div>
<div className="text-2xl font-bold text-green-600 mt-1">
{Math.floor(minutesUntilCharge / 60)}h {minutesUntilCharge % 60}m
</div>
{isNearExpiry && (
<div className="mt-2 text-sm text-yellow-600 font-medium">
⚠️ $1/hour charges begin in {minutesUntilCharge} minutes
</div>
)}
</div>
) : (
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-4 mb-4">
<div className="font-medium text-yellow-800">💳 Current Charges</div>
<div className="text-3xl font-bold text-yellow-600 mt-1">${totalCharge}</div>
<div className="text-sm text-gray-600 mt-1">
${hourlyCharge} hourly {storageFee > 0 ? `+ $${storageFee} storage` : ''}
</div>
</div>
)}
<div className="text-sm text-gray-500 mb-2">Location: {sessionData.location}</div>
<div className="text-xs text-gray-400">Show this screen to staff for pickup</div>
</div>
</div>
<button
onClick={handleCheckOut}
disabled={loading}
className="w-full bg-[#689c54] hover:bg-[#5a8448] disabled:bg-gray-400 text-white font-bold py-4 px-6 rounded-lg transition-all"
>
{loading ? 'Processing...' : `Check Out & Retrieve ${sessionData.bike_count} Bike${sessionData.bike_count > 1 ? 's' : ''}`}
</button>
</div>
</div>
</div>
);
}
};
// ========== STAFF DASHBOARD ==========
const StaffDashboard = ({ onBack }) => {
const [tab, setTab] = useState('lookup');
const [sessions, setSessions] = useState([]);
const [searchPhone, setSearchPhone] = useState('');
const [searchResults, setSearchResults] = useState([]);
const [loading, setLoading] = useState(false);
useEffect(() => {
loadData();
const interval = setInterval(loadData, 5000);
return () => clearInterval(interval);
}, []);
const loadData = async () => {
const { data } = await supabase
.from('sessions')
.select('*')
.eq('status', 'active')
.order('check_in_time', { ascending: false });
if (data) setSessions(data);
};
const handleBikeLookup = async (e) => {
e.preventDefault();
setLoading(true);
const { data } = await supabase
.from('sessions')
.select('*')
.ilike('phone', `%${searchPhone}%`)
.eq('status', 'active');
setSearchResults(data || []);
setLoading(false);
};
const handleExportMarketing = async () => {
setLoading(true);
// Get all transactions
const { data } = await supabase
.from('transactions')
.select('*')
.order('check_in_time', { ascending: false });
if (!data || data.length === 0) {
alert('No transaction data to export yet.');
setLoading(false);
return;
}
// Convert to CSV
const csv = [
'Phone,Location,Bike Count,Check-In,Check-Out,Total Charge',
...data.map(row =>
`${row.phone},"${row.location}",${row.bike_count},${row.check_in_time},${row.check_out_time || 'Active'},$${row.total_charge || '0.00'}`
)
].join('\n');
// Download
const blob = new Blob([csv], { type: 'text/csv' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `belt-customers-${new Date().toISOString().split('T')[0]}.csv`;
a.click();
URL.revokeObjectURL(url);
alert(`✅ Exported ${data.length} customer records!`);
setLoading(false);
};
const getSessionStats = (session) => {
const start = new Date(session.check_in_time);
const now = new Date();
const diff = now - start;
const totalMinutes = Math.floor(diff / 60000);
const hours = Math.floor(totalMinutes / 60);
const days = Math.floor(hours / 24);
const bikeCount = session.bike_count || 1;
const storageFee = Math.min(days, 10) * 15 * bikeCount;
const hourlyCharge = totalMinutes > 120 ? ((totalMinutes - 120) / 60).toFixed(2) : 0;
const totalCharge = (parseFloat(hourlyCharge) + storageFee).toFixed(2);
const isAbandoned = days >= 10;
const isNearAbandonment = days >= 8 && days < 10;
return { hours, days, storageFee, hourlyCharge, totalCharge, isAbandoned, isNearAbandonment, bikeCount };
};
const getTotalBikesParked = () => {
return sessions.reduce((sum, s) => sum + (s.bike_count || 1), 0);
};
return (
<div className="min-h-screen p-4">
<div className="max-w-7xl mx-auto">
<div className="flex items-center justify-between mb-6">
<h1 className="text-3xl font-bold text-gray-800">Staff Dashboard</h1>
<div className="flex gap-3">
<button
onClick={handleExportMarketing}
disabled={loading}
className="bg-blue-600 hover:bg-blue-700 disabled:bg-gray-400 text-white font-bold py-2 px-4 rounded-lg"
>
{loading ? 'Exporting...' : '📊 Export Marketing Data'}
</button>
<button onClick={onBack} className="text-gray-600 hover:text-gray-800">← Exit</button>
</div>
</div>
{/* Tabs */}
<div className="bg-white rounded-lg shadow mb-6">
<div className="flex border-b">
<button onClick={() => setTab('lookup')} className={`flex-1 px-6 py-4 font-medium ${tab === 'lookup' ? 'text-[#689c54] border-b-2 border-[#689c54]' : 'text-gray-600'}`}>
🔍 Bike Lookup
</button>
<button onClick={() => setTab('active')} className={`flex-1 px-6 py-4 font-medium ${tab === 'active' ? 'text-[#689c54] border-b-2 border-[#689c54]' : 'text-gray-600'}`}>
Active ({sessions.length} sessions • {getTotalBikesParked()} bikes)
</button>
</div>
</div>
{/* Bike Lookup Tab */}
{tab === 'lookup' && (
<div className="space-y-6">
<div className="bg-white rounded-lg shadow p-8">
<h2 className="text-2xl font-bold text-gray-800 mb-4">🔍 Customer Bike Lookup</h2>
<p className="text-gray-600 mb-6">Enter customer's phone number to view their bike photo</p>
<form onSubmit={handleBikeLookup} className="flex gap-3 mb-6">
<input
type="tel"
value={searchPhone}
onChange={(e) => setSearchPhone(e.target.value)}
placeholder="Enter phone number..."
required
className="flex-1 px-4 py-3 border-2 border-gray-300 rounded-lg focus:ring-2 focus:ring-[#689c54] focus:border-[#689c54] text-lg"
/>
<button
type="submit"
disabled={loading}
className="bg-[#689c54] hover:bg-[#5a8448] disabled:bg-gray-400 text-white font-bold py-3 px-8 rounded-lg"
>
{loading ? 'Searching...' : 'Search'}
</button>
</form>
{searchResults.length > 0 ? (
<div className="space-y-4">
{searchResults.map(session => {
const stats = getSessionStats(session);
return (
<div key={session.id} className="border-2 border-[#689c54] rounded-lg p-6 bg-green-50">
<div className="flex gap-6">
{session.photo_url ? (
<div className="flex-shrink-0">
<img src={session.photo_url} alt="Bike" className="w-64 h-64 object-cover rounded-lg shadow-lg border-4 border-white" />
<div className="text-center mt-2 text-sm font-medium text-[#689c54]">👆 Match this bike</div>
</div>
) : (
<div className="w-64 h-64 bg-gray-200 rounded-lg flex items-center justify-center">
<div className="text-gray-400 text-center">
<div className="text-4xl mb-2">📷</div>
<div className="text-sm">No Photo Available</div>
</div>
</div>
)}
<div className="flex-1">
<div className="bg-[#689c54] text-white px-4 py-2 rounded-lg inline-flex items-center gap-2 mb-4">
<span className="text-2xl">🚴♂️</span>
<span className="font-bold text-xl">{stats.bikeCount} Bike{stats.bikeCount > 1 ? 's' : ''} to Retrieve</span>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<div className="text-sm text-gray-600">Phone</div>
<div className="text-xl font-bold">{session.phone}</div>
</div>
<div>
<div className="text-sm text-gray-600">Location</div>
<div className="text-xl font-bold">{session.location}</div>
</div>
<div>
<div className="text-sm text-gray-600">Duration</div>
<div className="text-xl font-bold">
{stats.days > 0 ? `${stats.days}d ` : ''}{stats.hours % 24}h
</div>
</div>
<div>
<div className="text-sm text-gray-600">Amount Due</div>
<div className="text-2xl font-bold text-[#689c54]">${stats.totalCharge}</div>
</div>
</div>
{stats.storageFee > 0 && (
<div className="mt-4 bg-yellow-50 border border-yellow-300 rounded-lg p-3">
<div className="text-sm font-medium text-yellow-800">
Storage: ${stats.storageFee} ({stats.bikeCount} bike{stats.bikeCount > 1 ? 's' : ''} × {stats.days} days × $15)
</div>
</div>
)}
</div>
</div>
</div>
);
})}
</div>
) : searchPhone ? (
<div className="text-center py-12 text-gray-400">
<div className="text-6xl mb-4">🔍</div>
<div className="text-xl">No active sessions found</div>
</div>
) : (
<div className="text-center py-12 text-gray-400">
<div className="text-6xl mb-4">🚴♂️</div>
<div className="text-xl">Ready to help a customer</div>
<div className="text-sm mt-2">Enter their phone number above</div>
</div>
)}
</div>
</div>
)}
{/* Active Sessions Tab */}
{tab === 'active' && (
<>
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6">
<div className="bg-white rounded-lg shadow p-6">
<div className="text-3xl font-bold text-[#689c54]">{getTotalBikesParked()}</div>
<div className="text-gray-600">Total Bikes On-Site</div>
<div className="text-sm text-gray-500 mt-1">{sessions.length} sessions</div>
</div>
<div className="bg-white rounded-lg shadow p-6">
<div className="text-3xl font-bold text-blue-600">
{sessions.filter(s => s.photo_url).length}
</div>
<div className="text-gray-600">Sessions with Photos</div>
</div>
<div className="bg-white rounded-lg shadow p-6">
<div className="text-3xl font-bold text-orange-600">
{sessions.filter(s => getSessionStats(s).days > 0).reduce((sum, s) => sum + (s.bike_count || 1), 0)}
</div>
<div className="text-gray-600">Bikes Overnight</div>
</div>
<div className="bg-white rounded-lg shadow p-6">
<div className="text-3xl font-bold text-red-600">
{sessions.filter(s => getSessionStats(s).isAbandoned).reduce((sum, s) => sum + (s.bike_count || 1), 0)}
</div>
<div className="text-gray-600">To Donate</div>
</div>
</div>
<div className="space-y-4">
{sessions.map(session => {
const stats = getSessionStats(session);
return (
<div key={session.id} className={`bg-white rounded-lg shadow-lg p-6 border-l-4 ${
stats.isAbandoned ? 'border-gray-500' :
stats.isNearAbandonment ? 'border-red-500' :
stats.days > 0 ? 'border-orange-500' :
'border-[#689c54]'
}`}>
<div className="flex items-start gap-4">
{session.photo_url && (
<img src={session.photo_url} alt="Bike" className="w-24 h-24 object-cover rounded-lg" />
)}
<div className="flex-1">
<div className="flex justify-between items-start">
<div>
<div className="flex items-center gap-2 mb-1">
<span className="text-2xl font-bold text-gray-800">
{stats.days > 0 ? `${stats.days}d ` : ''}{stats.hours % 24}h
</span>
<span className="bg-[#689c54] text-white px-3 py-1 rounded-full text-sm font-bold">
{stats.bikeCount} bike{stats.bikeCount > 1 ? 's' : ''}
</span>
</div>
<div className="text-gray-600">{session.phone}</div>
<div className="text-sm text-gray-500">{session.location}</div>
{stats.storageFee > 0 && (
<div className="text-sm font-medium text-orange-600 mt-1">
Storage: ${stats.storageFee}
</div>
)}
</div>
<div>
{stats.isAbandoned ? (
<span className="px-3 py-1 bg-gray-200 text-gray-800 rounded-full text-sm font-medium">DONATE</span>
) : stats.isNearAbandonment ? (
<span className="px-3 py-1 bg-red-100 text-red-800 rounded-full text-sm font-medium">DAY {stats.days}/10</span>
) : stats.days > 0 ? (
<span className="px-3 py-1 bg-orange-100 text-orange-800 rounded-full text-sm font-medium">OVERNIGHT</span>
) : (
<span className="px-3 py-1 bg-green-100 text-green-800 rounded-full text-sm font-medium">ACTIVE</span>
)}
</div>
</div>
</div>
</div>
</div>
);
})}
{sessions.length === 0 && (
<div className="bg-white rounded-lg shadow p-12 text-center">
<div className="text-gray-400 text-lg">No active sessions</div>
</div>
)}
</div>
</>
)}
</div>
</div>
);
};
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<BELTValetSystem />);
</script>
</body>
</html>