import React, { useState } from 'react' import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' import { useNavigate } from 'react-router-dom' import { appointmentsAPI, chatbotsAPI } from '@/services/api' import { Card, Button, Spinner } from '@/components/ui' import { Calendar, Clock, User, Phone, Filter, Lock, CheckCircle2, XCircle, RotateCcw, ChevronDown, Settings, CalendarDays, } from 'lucide-react' import type { Appointment, Chatbot, BusinessHoursEntry } from '@/types' import { cn } from '@/lib/utils' const STATUS_CONFIG: Record = { pending: { label: 'Pending', color: 'bg-yellow-100 text-yellow-700', icon: Clock }, confirmed: { label: 'Confirmed', color: 'bg-green-100 text-green-700', icon: CheckCircle2 }, cancelled: { label: 'Cancelled', color: 'bg-red-100 text-red-600', icon: XCircle }, completed: { label: 'Completed', color: 'bg-gray-100 text-gray-600', icon: CheckCircle2 }, } const DAY_LABELS = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'] const DEFAULT_HOURS: BusinessHoursEntry[] = DAY_LABELS.map((_, i) => ({ day_of_week: i, is_open: i < 5, // Mon–Fri open by default open_time: '09:00', close_time: '17:00', slot_duration_minutes: 60, })) // ── Business Hours Settings Panel ───────────────────────────────────────────── const HoursSettings: React.FC<{ chatbotId: string; onClose: () => void }> = ({ chatbotId, onClose }) => { const queryClient = useQueryClient() const [hours, setHours] = useState(DEFAULT_HOURS) const [saved, setSaved] = useState(false) const { isLoading } = useQuery({ queryKey: ['business-hours', chatbotId], queryFn: () => appointmentsAPI.getHours(chatbotId), onSuccess: (data) => { if (data && data.length > 0) { // Merge fetched data with defaults const merged = DEFAULT_HOURS.map(d => { const found = data.find(h => h.day_of_week === d.day_of_week) return found ? { ...d, ...found } : d }) setHours(merged) } }, } as any) const save = useMutation({ mutationFn: () => appointmentsAPI.saveHours(chatbotId, hours), onSuccess: () => { setSaved(true) setTimeout(() => setSaved(false), 2000) queryClient.invalidateQueries({ queryKey: ['business-hours', chatbotId] }) }, }) const update = (idx: number, field: keyof BusinessHoursEntry, value: any) => { setHours(prev => prev.map((h, i) => i === idx ? { ...h, [field]: value } : h)) } if (isLoading) return
return (

Business Hours

Configure when customers can book appointments.

{hours.map((h, i) => (
{h.is_open ? ( <> update(i, 'open_time', e.target.value)} className="text-xs border border-gray-200 rounded-lg px-2 py-1.5 bg-white focus:outline-none focus:ring-1 focus:ring-primary-400" /> to update(i, 'close_time', e.target.value)} className="text-xs border border-gray-200 rounded-lg px-2 py-1.5 bg-white focus:outline-none focus:ring-1 focus:ring-primary-400" /> ) : ( Closed )}
))}
) } // ── Main Page ───────────────────────────────────────────────────────────────── export const AppointmentsPage: React.FC = () => { const navigate = useNavigate() const queryClient = useQueryClient() const [chatbotFilter, setChatbotFilter] = useState('') const [statusFilter, setStatusFilter] = useState('') const [settingsChatbotId, setSettingsChatbotId] = useState(null) const { data: chatbots = [] } = useQuery({ queryKey: ['chatbots'], queryFn: chatbotsAPI.list, }) const { data: appointments = [], isLoading, error } = useQuery({ queryKey: ['appointments', chatbotFilter, statusFilter], queryFn: () => appointmentsAPI.list({ chatbot_id: chatbotFilter || undefined, status: statusFilter || undefined, }), retry: false, }) const updateStatus = useMutation({ mutationFn: ({ id, status }: { id: string; status: string }) => appointmentsAPI.updateStatus(id, status), onSuccess: () => queryClient.invalidateQueries({ queryKey: ['appointments'] }), }) const isPlanError = (error as { response?: { status?: number } })?.response?.status === 402 if (isPlanError) { return (

Appointment Booking

Upgrade to Starter to enable appointment booking for your chatbots.

) } const upcoming = appointments.filter(a => new Date(a.slot_start) >= new Date() && a.status !== 'cancelled') const today = appointments.filter(a => { const d = new Date(a.slot_start) const now = new Date() return d.toDateString() === now.toDateString() }) const bookingEnabledChatbots = chatbots.filter(c => c.booking_enabled) return (
{/* Header */}

Appointments

Bookings made through your chatbots

{/* Business hours settings panel */} {settingsChatbotId && ( setSettingsChatbotId(null)} /> )} {/* Setup prompt when no chatbots have booking enabled */} {!isLoading && bookingEnabledChatbots.length === 0 && (

Enable booking on a chatbot

Go to a chatbot's Deploy tab and enable "Appointment Booking" to start accepting bookings.

{chatbots.length > 0 && ( )}
)} {/* Stats cards */} {appointments.length > 0 && (
{[ { label: 'Today', count: today.length, color: 'text-blue-600', bg: 'bg-blue-50' }, { label: 'Upcoming', count: upcoming.length, color: 'text-primary-600', bg: 'bg-primary-50' }, { label: 'Confirmed', count: appointments.filter(a => a.status === 'confirmed').length, color: 'text-green-600', bg: 'bg-green-50' }, { label: 'Pending', count: appointments.filter(a => a.status === 'pending').length, color: 'text-yellow-600', bg: 'bg-yellow-50' }, ].map(stat => (

{stat.label}

{stat.count}

))}
)} {/* Filters */}
Filter
{/* Per-chatbot hours settings */} {bookingEnabledChatbots.length > 0 && (
Hours:
)}
{/* Appointments list */} {isLoading ? (
) : appointments.length === 0 ? (

No appointments yet

Once customers book through your chatbot, appointments will appear here.

) : (
{appointments.map(appt => { const sc = STATUS_CONFIG[appt.status] || STATUS_CONFIG.pending const Icon = sc.icon const slotDate = new Date(appt.slot_start) const slotEnd = new Date(appt.slot_end) const isToday = slotDate.toDateString() === new Date().toDateString() return (
{/* Date block */}

{slotDate.toLocaleDateString(undefined, { month: 'short' })}

{slotDate.getDate()}

{isToday &&

Today

}
{/* Details */}

{appt.customer_name}

{appt.service &&

{appt.service}

}
{sc.label}
{slotDate.toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit' })} – {slotEnd.toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit' })} {appt.customer_contact}
{appt.notes && (

"{appt.notes}"

)} {/* Actions */} {appt.status === 'pending' && (
)} {appt.status === 'confirmed' && (
)} {appt.status === 'cancelled' && ( )}
) })}
)}
) }