import { useState, useEffect, useRef } from 'react'
import { useNavigate } from 'react-router-dom'
import {
ArrowRight, User, Mail, Lock, Building2, Coffee,
UtensilsCrossed, Beer, ShoppingBag, CheckCircle2, ChevronRight, Hash,
} from 'lucide-react'
import client from '../api/client'
import Button from '../ui/Button'
import { LabelledInput } from '../ui/Input'
// ─── Step indicator ───────────────────────────────────────────────────────────
function StepDots({ total, current }) {
return (
{Array.from({ length: total }).map((_, i) => (
))}
)
}
// ─── Venue type selector (multi-select) ───────────────────────────────────────
const VENUE_TYPES = [
{ id: 'restaurant', label: 'Restaurant', Icon: UtensilsCrossed },
{ id: 'bar', label: 'Bar', Icon: Beer },
{ id: 'coffee_shop', label: 'Coffee Shop', Icon: Coffee },
{ id: 'fast_food', label: 'Fast Food', Icon: ShoppingBag },
]
function VenueTypeButton({ type, selected, onToggle }) {
const { label, Icon } = type
return (
onToggle(type.id)}
className={`relative flex flex-col items-center gap-2 rounded-xl border-2 p-4 text-[13px] font-medium transition-all ${
selected
? 'border-sky-500 bg-sky-50 text-sky-700'
: 'border-slate-200 bg-white text-slate-600 hover:border-slate-300 hover:bg-slate-50'
}`}
>
{selected && (
)}
{label}
)
}
// ─── PIN pad ──────────────────────────────────────────────────────────────────
const PIN_DIGITS = ['1','2','3','4','5','6','7','8','9','','0','⌫']
function PinPad({ pin, onChange }) {
function press(d) {
if (d === '⌫') { onChange(p => p.slice(0, -1)); return }
if (d === '') return
if (pin.length >= 6) return
onChange(p => p + d)
}
return (
{Array.from({ length: 4 }).map((_, i) => (
))}
{PIN_DIGITS.map((d, i) => (
press(d)}
disabled={d === ''}
className={`h-12 rounded-xl text-lg font-semibold transition-colors ${
d === ''
? 'invisible'
: d === '⌫'
? 'bg-slate-100 hover:bg-slate-200 text-slate-600'
: 'bg-slate-100 hover:bg-sky-100 active:bg-sky-200 text-slate-800'
}`}
>
{d}
))}
)
}
// ─── Steps ───────────────────────────────────────────────────────────────────
function StepWelcome({ onNext }) {
return (
Welcome to Xenia POS
The complete point-of-sale system for your venue. Let's get you set up
in just a few steps — it won't take long.
{[
'Create your manager account',
'Tell us about your venue',
'You\'re ready to go',
].map((s, i) => (
))}
Get Started
Thank you for choosing Xenia POS.
)
}
function StepAccount({ data, onChange, onNext, onBack }) {
const [errors, setErrors] = useState({})
// Live password match: debounce 1s after confirm changes
const confirmTimer = useRef(null)
function handleConfirmChange(value) {
onChange('confirm', value)
clearTimeout(confirmTimer.current)
confirmTimer.current = setTimeout(() => {
if (value && data.password && value !== data.password) {
setErrors(e => ({ ...e, confirm: 'Passwords do not match' }))
} else {
setErrors(e => { const next = { ...e }; delete next.confirm; return next })
}
}, 1000)
}
// Also clear the error immediately if they now match
function handlePasswordChange(value) {
onChange('password', value)
if (data.confirm && value === data.confirm) {
setErrors(e => { const next = { ...e }; delete next.confirm; return next })
}
}
function validate() {
const e = {}
if (!data.username.trim()) e.username = 'Username is required'
if (!data.fullName.trim()) e.fullName = 'Full name is required'
if (!data.email.trim()) e.email = 'Email is required'
else if (!/\S+@\S+\.\S+/.test(data.email)) e.email = 'Enter a valid email'
if (data.password.length < 6) e.password = 'At least 6 characters'
if (data.password !== data.confirm) e.confirm = 'Passwords do not match'
setErrors(e)
return Object.keys(e).length === 0
}
return (
Create your manager account
This will be the primary administrator account.
onChange('username', e.target.value)}
autoComplete="off"
autoFocus
/>
{errors.username && {errors.username}
}
onChange('fullName', e.target.value)}
autoComplete="off"
/>
{errors.fullName && {errors.fullName}
}
onChange('email', e.target.value)}
type="email"
autoComplete="off"
/>
{errors.email && {errors.email}
}
handlePasswordChange(e.target.value)}
type="password"
autoComplete="new-password"
/>
{errors.password && {errors.password}
}
handleConfirmChange(e.target.value)}
type="password"
autoComplete="new-password"
/>
{errors.confirm && {errors.confirm}
}
Back
{ if (validate()) onNext() }}>
Continue
)
}
function StepVenue({ data, onChange, onNext, onBack }) {
const [errors, setErrors] = useState({})
function toggleType(id) {
const current = data.venueTypes
const next = current.includes(id)
? current.filter(v => v !== id)
: [...current, id]
onChange('venueTypes', next)
}
function validate() {
const e = {}
if (data.venueTypes.length === 0) e.venueType = 'Please select at least one type'
if (!data.venueName.trim()) e.venueName = 'Venue name is required'
setErrors(e)
return Object.keys(e).length === 0
}
return (
Tell us about your venue
Select all that apply — you can always change this later.
Venue type
{VENUE_TYPES.map(type => (
))}
{errors.venueType &&
{errors.venueType}
}
Venue name
onChange('venueName', e.target.value)}
autoComplete="off"
/>
{errors.venueName && {errors.venueName}
}
Back
{ if (validate()) onNext() }}>
Continue
)
}
function StepPin({ pin, onChange, onNext, onBack }) {
const hasPin = pin.length >= 4
return (
Set a quick-access PIN
Optional. Used to unlock the dashboard quickly when it's locked,
without typing your full password.
{hasPin && (
PIN set — {pin.length} digits
)}
Back
{hasPin ? <>Continue > : 'Skip for now'}
)
}
function StepDone({ onGoToLogin, loading, error }) {
return (
You're all set!
Your manager account is ready. Log in and start configuring your
products, tables, and staff.
{['Add your menu and products', 'Set up tables and zones', 'Add your waiters'].map((tip, i) => (
{tip}
))}
{error &&
{error}
}
{loading ? 'Setting up…' : 'Go to Login'}
{!loading && }
)
}
// ─── Main wizard ──────────────────────────────────────────────────────────────
// Steps: 0=welcome, 1=account, 2=venue, 3=pin, 4=done
const INNER_STEPS = 3 // steps 1-3 show the dot indicator
export default function SetupWizard() {
const navigate = useNavigate()
const [step, setStep] = useState(0)
const [loading, setLoading] = useState(false)
const [error, setError] = useState('')
const [account, setAccount] = useState({
username: '', fullName: '', email: '', password: '', confirm: '',
})
const [venue, setVenue] = useState({ venueTypes: [], venueName: '' })
const [pin, setPin] = useState('')
function updateAccount(key, value) {
setAccount(prev => ({ ...prev, [key]: value }))
}
function updateVenue(key, value) {
setVenue(prev => ({ ...prev, [key]: value }))
}
async function handleFinish() {
setLoading(true)
setError('')
try {
await client.post('/api/setup/init', {
username: account.username.trim(),
password: account.password,
email: account.email.trim(),
full_name: account.fullName.trim(),
venue_type: venue.venueTypes.join(','),
venue_name: venue.venueName.trim(),
pin: pin.length >= 4 ? pin : undefined,
})
navigate('/login', { replace: true })
} catch (err) {
setError(err.response?.data?.detail || 'Something went wrong. Please try again.')
setLoading(false)
}
}
return (
{/* Dot indicator for inner steps only */}
{step >= 1 && step <= INNER_STEPS && (
)}
{step === 0 &&
setStep(1)} />}
{step === 1 && (
setStep(2)}
onBack={() => setStep(0)}
/>
)}
{step === 2 && (
setStep(3)}
onBack={() => setStep(1)}
/>
)}
{step === 3 && (
setStep(4)}
onBack={() => setStep(2)}
/>
)}
{step === 4 && (
)}
Xenia POS · First-time Setup
)
}