'use client'; import { useEffect, useMemo, useState } from 'react'; import { useRouter } from 'next/navigation'; import { useAuth } from '@/contexts/AuthContext'; import type { CallTask } from '@/lib/types'; import { CalendarDays, PhoneCall } from 'lucide-react'; import toast from 'react-hot-toast'; const STATUS_LABELS: Record = { pending: 'Pending', in_progress: 'In Progress', completed: 'Completed', failed: 'Failed', }; const STATUS_STYLES: Record = { pending: 'bg-yellow-500/20 text-yellow-200 border-yellow-400/30', in_progress: 'bg-blue-500/20 text-blue-200 border-blue-400/30', completed: 'bg-emerald-500/20 text-emerald-200 border-emerald-400/30', failed: 'bg-red-500/20 text-red-200 border-red-400/30', }; const FILTERS: Array<{ key: 'all' | 'pending' | 'completed' | 'failed'; label: string }> = [ { key: 'all', label: 'All' }, { key: 'pending', label: 'Pending' }, { key: 'completed', label: 'Completed' }, { key: 'failed', label: 'Failed' }, ]; const formatTimestamp = (value: any) => { if (!value) return ''; if (typeof value === 'string') return new Date(value).toLocaleString(); if (value?.seconds) return new Date(value.seconds * 1000).toLocaleString(); return value.toString(); }; export default function ScheduledCallsPage() { const { user, isLoading } = useAuth(); const router = useRouter(); const [tasks, setTasks] = useState([]); const [activeFilter, setActiveFilter] = useState<'all' | 'pending' | 'completed' | 'failed'>( 'all' ); const [isSubmitting, setIsSubmitting] = useState(false); const [formData, setFormData] = useState({ businessName: '', phoneNumber: '', preferredLanguage: 'English', callGoals: '', scheduledAt: '', timezone: Intl.DateTimeFormat().resolvedOptions().timeZone, maxMinutes: 10, allowRecalls: false, maxRecalls: 0, }); const [callNowNumber, setCallNowNumber] = useState(''); const [callNowError, setCallNowError] = useState(null); const [callNowStatus, setCallNowStatus] = useState<'idle' | 'connecting' | 'started'>('idle'); const [creditsBalance, setCreditsBalance] = useState(null); const [calendarMonth, setCalendarMonth] = useState(() => { const date = new Date(); return new Date(date.getFullYear(), date.getMonth(), 1); }); const [selectedDate, setSelectedDate] = useState(() => new Date()); const [selectedTime, setSelectedTime] = useState('10:00'); useEffect(() => { if (!isLoading && !user) { router.push('/auth'); } }, [user, isLoading, router]); const fetchTasks = async () => { try { const response = await fetch('/api/scheduled-calls'); const data = await response.json(); setTasks(data.tasks || []); } catch (error) { console.error('Error loading tasks', error); toast.error('Unable to load scheduled calls.'); } }; useEffect(() => { if (user) { fetchTasks(); fetch('/api/credits') .then((res) => res.json()) .then((data) => setCreditsBalance(data.balance ?? 0)) .catch(() => { setCreditsBalance(0); toast.error('Unable to load credits.'); }); } }, [user]); const filteredTasks = useMemo(() => { if (activeFilter === 'all') return tasks; return tasks.filter((task) => task.status === activeFilter); }, [tasks, activeFilter]); const updateField = (key: keyof typeof formData, value: string | number | boolean) => { setFormData((prev) => ({ ...prev, [key]: value })); }; const handleSubmit = async (event: React.FormEvent) => { event.preventDefault(); if (isSubmitting) return; setIsSubmitting(true); try { const response = await fetch('/api/scheduled-calls', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ ...formData, scheduledAt: new Date( `${selectedDate.toDateString()} ${selectedTime}` ).toISOString(), maxMinutes: Number(formData.maxMinutes), maxRecalls: Number(formData.maxRecalls), }), }); if (!response.ok) { throw new Error('Failed to schedule call'); } await fetchTasks(); toast.success('Call scheduled successfully.'); setFormData({ businessName: '', phoneNumber: '', preferredLanguage: 'English', callGoals: '', scheduledAt: '', timezone: Intl.DateTimeFormat().resolvedOptions().timeZone, maxMinutes: 10, allowRecalls: false, maxRecalls: 0, }); } catch (error) { console.error('Error scheduling call', error); toast.error('Unable to schedule the call.'); } finally { setIsSubmitting(false); } }; const handleCallNow = async () => { setCallNowError(null); if (!callNowNumber) { setCallNowError('Please enter a phone number.'); toast.error('Please enter a phone number.'); return; } if (creditsBalance !== null && creditsBalance < 1) { setCallNowError('Insufficient credits to start a call.'); toast.error('Insufficient credits to start a call.'); return; } setCallNowStatus('connecting'); try { const response = await fetch('/api/vapi/create-call', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ phoneNumber: callNowNumber, userId: user?.uid, }), }); if (!response.ok) { const data = await response.json(); throw new Error(data?.error || 'Failed to start call.'); } setCallNowStatus('started'); toast.success('Call started.'); } catch (error) { console.error('Call now error', error); setCallNowError( error instanceof Error ? error.message : 'Failed to start call.' ); toast.error('Unable to start the call.'); setCallNowStatus('idle'); } }; const handleCancelTask = async (taskId: string) => { try { const response = await fetch(`/api/scheduled-calls/${taskId}`, { method: 'DELETE', }); if (!response.ok) { throw new Error('Failed to cancel task.'); } await fetchTasks(); toast.success('Scheduled call canceled.'); } catch (error) { console.error('Cancel task error', error); toast.error('Unable to cancel the scheduled call.'); } }; const daysInMonth = new Date( calendarMonth.getFullYear(), calendarMonth.getMonth() + 1, 0 ).getDate(); const firstWeekday = new Date( calendarMonth.getFullYear(), calendarMonth.getMonth(), 1 ).getDay(); const calendarDays = Array.from({ length: daysInMonth }, (_, i) => i + 1); if (isLoading) { return (
Loading...
); } if (!user) return null; return (

Scheduled Calls

Agent online · 42 mins left

{FILTERS.map((filter) => ( ))}

Make Call Now

Start an immediate outbound call for quick tests.

setCallNowNumber(event.target.value)} className="w-full rounded-xl bg-[#0f0b1a] border border-purple-500/20 px-4 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-[#8b5cf6]" placeholder="+34 600 000 000" /> {creditsBalance !== null && (

Credits available: {creditsBalance}

)} {callNowError &&

{callNowError}

}

Schedule a Call

{['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'].map((day) => ( {day} ))} {Array.from({ length: firstWeekday }).map((_, index) => ( ))} {calendarDays.map((day) => { const current = new Date( calendarMonth.getFullYear(), calendarMonth.getMonth(), day ); const isSelected = current.toDateString() === selectedDate.toDateString(); return ( ); })}
setSelectedTime(event.target.value)} className="w-full rounded-xl bg-[#0f0b1a] border border-purple-500/20 px-4 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-[#8b5cf6]" /> updateField('timezone', event.target.value)} className="w-full rounded-xl bg-[#0f0b1a] border border-purple-500/20 px-4 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-[#8b5cf6]" />
updateField('businessName', event.target.value)} className="w-full rounded-xl bg-[#0f0b1a] border border-purple-500/20 px-4 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-[#8b5cf6]" placeholder="Business name" /> updateField('phoneNumber', event.target.value)} className="w-full rounded-xl bg-[#0f0b1a] border border-purple-500/20 px-4 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-[#8b5cf6]" placeholder="+34 600 000 000" />