'use client'; import { useAuth } from '@/contexts/AuthContext'; import { usePathname, useRouter } from 'next/navigation'; import { useEffect, useMemo, useState } from 'react'; import Link from 'next/link'; import Joyride, { CallBackProps, STATUS, EVENTS } from 'react-joyride'; import { Bell, CalendarClock, CreditCard, Gauge, MessageSquare, Phone, Clock, TrendingUp, Coins, Settings, Sparkles, Timer, X, Search, Download, ChevronLeft, ChevronRight, } from 'lucide-react'; import { motion, AnimatePresence } from 'framer-motion'; import { ResponsiveContainer, LineChart, Line, XAxis, YAxis, Tooltip, CartesianGrid, } from 'recharts'; import toast from 'react-hot-toast'; import { getUser, updateUserOnboardingCompleted } from '@/lib/firestore'; interface Call { id: number; contactName: string; date: string; duration: string; status: 'Completed' | 'Failed'; credits: number; } interface AnalyticsData { totalCalls: number; totalMinutes: number; avgDuration: number; creditsRemaining: number; } interface CallAnalyticsPoint { day: string; calls: number; minutes: number; } export default function DashboardPage() { const { user, isLoading, logout } = useAuth(); const router = useRouter(); const pathname = usePathname(); const [creditsRemaining, setCreditsRemaining] = useState(null); const [isStatsLoading, setIsStatsLoading] = useState(true); const [isLowCreditsDismissed, setIsLowCreditsDismissed] = useState(true); const [searchTerm, setSearchTerm] = useState(''); const [dateRange, setDateRange] = useState<'7' | '30' | 'custom'>('7'); const [customStart, setCustomStart] = useState(''); const [customEnd, setCustomEnd] = useState(''); const [sortKey, setSortKey] = useState<'date' | 'duration' | 'status'>('date'); const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('desc'); const [currentPage, setCurrentPage] = useState(1); const [selectedCall, setSelectedCall] = useState(null); const [runTour, setRunTour] = useState(false); const [tourStepIndex, setTourStepIndex] = useState(0); const tourSteps = useMemo( () => [ { target: '[data-tour="welcome"]', content: 'Welcome to HolaCompi! Here is your dashboard overview.', placement: 'bottom', }, { target: '[data-tour="buy-credits"]', content: 'Buy credits to power your calls whenever you need.', placement: 'left', }, { target: '[data-tour="make-call"]', content: 'Schedule a call to reach clients instantly.', placement: 'top', }, { target: '[data-tour="view-settings"]', content: 'Adjust preferences and manage your account settings here.', placement: 'bottom', }, ], [] ); const mockChartData: CallAnalyticsPoint[] = [ { day: 'Mon', calls: 8, minutes: 24 }, { day: 'Tue', calls: 12, minutes: 38 }, { day: 'Wed', calls: 6, minutes: 19 }, { day: 'Thu', calls: 10, minutes: 31 }, { day: 'Fri', calls: 7, minutes: 22 }, { day: 'Sat', calls: 2, minutes: 12 }, { day: 'Sun', calls: 2, minutes: 10 }, ]; const mockRecentCalls: Call[] = [ { id: 1, contactName: 'Lucia Morales', date: '2026-01-21 18:30', duration: '4m 15s', status: 'Completed', credits: 4, }, { id: 2, contactName: 'Clinic Valencia', date: '2026-01-21 14:20', duration: '2m 45s', status: 'Completed', credits: 3, }, { id: 3, contactName: 'Miguel Ramos', date: '2026-01-20 16:10', duration: '5m 32s', status: 'Completed', credits: 6, }, { id: 4, contactName: 'Barcelona Dental', date: '2026-01-20 11:45', duration: '1m 23s', status: 'Failed', credits: 0, }, { id: 5, contactName: 'Sofia Ortega', date: '2026-01-19 09:15', duration: '3m 58s', status: 'Completed', credits: 4, }, ]; const analyticsData: AnalyticsData = useMemo( () => ({ totalCalls: 47, totalMinutes: 156, avgDuration: 3.3, creditsRemaining: creditsRemaining ?? 0, }), [creditsRemaining] ); const fadeInUp = { hidden: { opacity: 0, y: 16 }, visible: { opacity: 1, y: 0 }, }; const slideUp = { hidden: { opacity: 0, y: 24 }, visible: { opacity: 1, y: 0 }, }; const parseCallDate = (value: string) => new Date(value.replace(' ', 'T')); const durationToSeconds = (value: string) => { const match = value.match(/(\d+)m\s*(\d+)s/); if (!match) return 0; return Number(match[1]) * 60 + Number(match[2]); }; useEffect(() => { if (!isLoading && !user) { router.push('/auth'); } }, [user, isLoading, router]); useEffect(() => { const fetchCredits = async () => { setIsStatsLoading(true); try { const response = await fetch('/api/credits'); if (!response.ok) { toast.error('Unable to load credits. Please try again.'); setCreditsRemaining(0); return; } const data = await response.json(); setCreditsRemaining(data.balance ?? 0); } catch (error) { console.error('Failed to load credits', error); toast.error('Unable to load credits. Please check your connection.'); setCreditsRemaining(0); } finally { setIsStatsLoading(false); } }; if (user) { fetchCredits(); } }, [user]); useEffect(() => { if (typeof window === 'undefined') return; const dismissed = window.localStorage.getItem('lowCreditsBannerDismissed'); setIsLowCreditsDismissed(dismissed === 'true'); }, []); useEffect(() => { let isActive = true; const loadOnboardingStatus = async () => { if (!user) return; try { const userDoc = await getUser(user.uid); const completed = userDoc?.onboardingCompleted ?? false; if (!completed && isActive) { setRunTour(true); setTourStepIndex(0); } } catch (error) { console.error('Failed to load onboarding status', error); } }; loadOnboardingStatus(); return () => { isActive = false; }; }, [user]); const handleDismissLowCredits = () => { setIsLowCreditsDismissed(true); if (typeof window !== 'undefined') { window.localStorage.setItem('lowCreditsBannerDismissed', 'true'); } }; useEffect(() => { setCurrentPage(1); }, [searchTerm, dateRange, customStart, customEnd, sortKey, sortDirection]); const filteredCalls = useMemo(() => { const normalizedSearch = searchTerm.trim().toLowerCase(); const now = new Date(); let startDate: Date | null = null; let endDate: Date | null = null; if (dateRange === '7') { startDate = new Date(now); startDate.setDate(now.getDate() - 7); } else if (dateRange === '30') { startDate = new Date(now); startDate.setDate(now.getDate() - 30); } else if (dateRange === 'custom') { startDate = customStart ? new Date(customStart) : null; endDate = customEnd ? new Date(customEnd) : null; if (endDate) { endDate.setHours(23, 59, 59, 999); } } return mockRecentCalls.filter((call) => { const matchesSearch = !normalizedSearch || call.contactName.toLowerCase().includes(normalizedSearch); const callDate = parseCallDate(call.date); const afterStart = !startDate || callDate >= startDate; const beforeEnd = !endDate || callDate <= endDate; return matchesSearch && afterStart && beforeEnd; }); }, [searchTerm, dateRange, customStart, customEnd, mockRecentCalls]); const sortedCalls = useMemo(() => { const sorted = [...filteredCalls]; sorted.sort((a, b) => { let comparison = 0; if (sortKey === 'date') { comparison = parseCallDate(a.date).getTime() - parseCallDate(b.date).getTime(); } else if (sortKey === 'duration') { comparison = durationToSeconds(a.duration) - durationToSeconds(b.duration); } else { comparison = a.status.localeCompare(b.status); } return sortDirection === 'asc' ? comparison : -comparison; }); return sorted; }, [filteredCalls, sortKey, sortDirection]); const callsPerPage = 10; const totalPages = Math.max(1, Math.ceil(sortedCalls.length / callsPerPage)); const paginatedCalls = sortedCalls.slice( (currentPage - 1) * callsPerPage, currentPage * callsPerPage ); const handleSort = (key: 'date' | 'duration' | 'status') => { if (sortKey === key) { setSortDirection((prev) => (prev === 'asc' ? 'desc' : 'asc')); return; } setSortKey(key); setSortDirection('desc'); }; const handleExportCsv = () => { if (sortedCalls.length === 0) { toast.error('No calls available to export.'); return; } const rows = [ ['Contact', 'Date', 'Duration', 'Status', 'Credits'], ...sortedCalls.map((call) => [ call.contactName, call.date, call.duration, call.status, call.credits.toString(), ]), ]; const csvContent = rows .map((row) => row.map((value) => `"${value.replace(/"/g, '""')}"`).join(',')) .join('\n'); const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' }); const url = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; link.setAttribute('download', 'recent-calls.csv'); document.body.appendChild(link); link.click(); document.body.removeChild(link); URL.revokeObjectURL(url); }; const handleJoyride = async (data: CallBackProps) => { const { status, index, type } = data; if (type === EVENTS.STEP_AFTER) { setTourStepIndex(index + 1); } if (status === STATUS.FINISHED || status === STATUS.SKIPPED) { setRunTour(false); setTourStepIndex(0); if (user) { try { await updateUserOnboardingCompleted(user.uid, true); } catch (error) { console.error('Failed to save onboarding status', error); } } } }; if (isLoading) { return (
Loading...
); } if (!user) { return null; } const handleLogout = async () => { try { await logout(); router.push('/auth'); toast.success('Signed out successfully.'); } catch (error) { console.error('Logout failed', error); toast.error('Unable to log out. Please try again.'); } }; const navLinks = [ { label: 'Scheduled Calls', href: '/dashboard/scheduled-calls', icon: CalendarClock }, { label: 'Voice Agent', href: '/dashboard/agent-settings', icon: Sparkles }, { label: 'Credits & Limits', href: '/dashboard/credits', icon: CreditCard }, { label: 'Notifications', href: '/dashboard/notifications', icon: Bell }, { label: 'Chat', href: '/dashboard/chat', icon: MessageSquare }, { label: 'Settings', href: '/dashboard/settings', icon: Settings }, ]; return (
{!isLowCreditsDismissed && creditsRemaining !== null && creditsRemaining < 10 && (
⚠️ Running low on credits! You have {creditsRemaining} credits remaining.
)}

Welcome back, {user.name || user.email}! 👋

Schedule calls, monitor credits, and tune your voice agent.

Usage Overview

{(isStatsLoading ? new Array(4).fill(null) : [ { label: 'Total Calls Made', value: `${analyticsData.totalCalls} calls`, icon: Phone, gradient: 'from-[#8b5cf6]/30 to-[#4f46e5]/30', }, { label: 'Total Minutes Used', value: `${analyticsData.totalMinutes} minutes`, icon: Clock, gradient: 'from-[#4f46e5]/30 to-[#3b82f6]/30', }, { label: 'Average Call Duration', value: `${analyticsData.avgDuration} minutes`, icon: TrendingUp, gradient: 'from-[#8b5cf6]/30 to-[#ec4899]/30', }, { label: 'Credits Remaining', value: creditsRemaining === null ? 'Loading...' : `${analyticsData.creditsRemaining} credits`, icon: Coins, gradient: 'from-[#10b981]/20 to-[#8b5cf6]/30', }, ]).map((card, index) => { if (isStatsLoading) { return (
); } const Icon = card.icon; const action = card.label === 'Total Calls Made' ? () => { const section = document.getElementById('call-history'); section?.scrollIntoView({ behavior: 'smooth', block: 'start' }); } : card.label === 'Credits Remaining' ? () => router.push('/dashboard/credits') : null; const isClickable = Boolean(action); return (

{card.label}

{card.value}

); })}

Weekly Usage

Last 7 days

Calls & Minutes
[ value, name === 'calls' ? 'Calls' : 'Minutes', ]} labelFormatter={(label) => `Day: ${label}`} />

Recent Activity

setSearchTerm(event.target.value)} placeholder="Search by contact name" className="w-full bg-transparent text-sm text-white placeholder:text-[#9ca3af] focus:outline-none" />
{dateRange === 'custom' && (
setCustomStart(event.target.value)} className="rounded-full bg-[#0f0b1a] border border-white/10 px-4 py-2 text-xs text-white focus:outline-none focus:ring-2 focus:ring-[#8b5cf6]" /> to setCustomEnd(event.target.value)} className="rounded-full bg-[#0f0b1a] border border-white/10 px-4 py-2 text-xs text-white focus:outline-none focus:ring-2 focus:ring-[#8b5cf6]" />
)}
{sortedCalls.length === 0 ? (

No calls match your filters yet.

) : (
{paginatedCalls.map((call, index) => ( setSelectedCall(call)} > ))}
Contact Credits
{call.contactName} {call.date} {call.duration} {call.status} {call.credits}
Page {currentPage} of {totalPages}
)}

Account Status

Email: {user.email}

UID: {user.uid}

{selectedCall && ( setSelectedCall(null)} > event.stopPropagation()} >

{selectedCall.contactName}

{selectedCall.date}

Duration {selectedCall.duration}
Status {selectedCall.status}
Credits Used {selectedCall.credits}
)}
); }