beachPartyBalloons/estore/src/components/BookingRequestPanel.tsx
chris 02e49ba41b Fix checkout: block false slots when calendar down, add booking request fallback
- Change fulfillment state from RESERVED to PROPOSED (Square rejects RESERVED)
- Return 503 from slots API when CalDAV is unreachable instead of serving empty
  busy blocks that made all time slots appear falsely available
- Add BookingRequestPanel and /api/booking-request endpoint: when the calendar
  server is down, customers can submit their order and preferred time; server
  emails info@beachpartyballoons.com and sends a confirmation to the customer

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-05 19:58:22 -04:00

108 lines
3.7 KiB
TypeScript

'use client'
import { useState } from 'react'
import { BASE } from '@/lib/basepath'
import type { EmailLineItem } from '@/lib/notify'
interface Props {
address: string
defaultName: string
defaultPhone: string
defaultEmail: string
lineItems: EmailLineItem[]
}
export default function BookingRequestPanel({ address, defaultName, defaultPhone, defaultEmail, lineItems }: Props) {
const [name, setName] = useState(defaultName)
const [phone, setPhone] = useState(defaultPhone)
const [email, setEmail] = useState(defaultEmail)
const [preferredTime, setPreferredTime] = useState('')
const [submitting, setSubmitting] = useState(false)
const [success, setSuccess] = useState(false)
const [error, setError] = useState('')
const handleSubmit = async () => {
setError('')
setSubmitting(true)
try {
const res = await fetch(`${BASE}/api/booking-request`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ customerName: name, customerPhone: phone, customerEmail: email, preferredTime, address, lineItems }),
})
const data = await res.json()
if (!res.ok) { setError(data.error ?? 'Something went wrong.'); return }
setSuccess(true)
} catch {
setError('Network error — please try again.')
} finally {
setSubmitting(false)
}
}
if (success) {
return (
<div style={{ marginTop: '0.75rem', padding: '0.75rem', background: '#f0f9fa', border: '1px solid #b2e0e4', borderRadius: '8px', fontSize: '0.82rem' }}>
<strong style={{ color: '#0d6e75' }}>Request sent!</strong>
<p style={{ marginTop: '4px', color: '#444' }}>
We&rsquo;ll reach out to confirm your delivery time. Check your email for a copy of your request.
</p>
</div>
)
}
return (
<div style={{ marginTop: '0.75rem', padding: '0.75rem', background: '#fff8e1', border: '1px solid #f6c000', borderRadius: '8px' }}>
<p style={{ fontSize: '0.82rem', fontWeight: 'bold', marginBottom: '0.5rem', color: '#5a4000' }}>
Send us your order &amp; we&rsquo;ll confirm a time
</p>
<div style={{ display: 'flex', gap: '6px', marginBottom: '0.4rem' }}>
<input
className="input is-small"
placeholder="Your name"
value={name}
onChange={(e) => setName(e.target.value)}
style={{ flex: 1 }}
/>
<input
className="input is-small"
placeholder="Phone"
type="tel"
value={phone}
onChange={(e) => setPhone(e.target.value)}
style={{ flex: 1 }}
/>
</div>
<input
className="input is-small"
placeholder="Email address"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
style={{ marginBottom: '0.4rem', width: '100%' }}
/>
<input
className="input is-small"
placeholder="Preferred delivery date &amp; time (e.g. Saturday June 14, afternoon)"
value={preferredTime}
onChange={(e) => setPreferredTime(e.target.value)}
style={{ marginBottom: '0.5rem', width: '100%' }}
/>
{error && (
<p style={{ fontSize: '0.75rem', color: '#cc3333', marginBottom: '0.4rem' }}>{error}</p>
)}
<button
className={`button is-warning is-small is-fullwidth${submitting ? ' is-loading' : ''}`}
disabled={submitting || !name.trim() || !phone.trim() || !email.trim() || !preferredTime.trim()}
onClick={handleSubmit}
style={{ fontWeight: 'bold' }}
>
Send booking request
</button>
</div>
)
}