|
@@ -1802,108 +1802,161 @@ serve(async (req) => {
|
|
|
)
|
|
)
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
- // GET /api/dashboard/stats - Get dashboard statistics
|
|
|
|
|
|
|
+ // GET /api/dashboard/stats - Get dashboard statistics from call_logs
|
|
|
if (path === 'dashboard/stats' && req.method === 'GET') {
|
|
if (path === 'dashboard/stats' && req.method === 'GET') {
|
|
|
- // TODO: Implement real dashboard stats calculation
|
|
|
|
|
- // For now, returning mock/empty data to unblock the frontend
|
|
|
|
|
- return new Response(
|
|
|
|
|
- JSON.stringify({
|
|
|
|
|
- success: true,
|
|
|
|
|
- stats: {
|
|
|
|
|
- totalCalls: { value: 0, change: '0%', changeType: 'neutral' },
|
|
|
|
|
- resolvedCalls: { value: 0, change: '0%', changeType: 'neutral' },
|
|
|
|
|
- avgDuration: { value: 0, formatted: '0:00', change: '0%', changeType: 'neutral' },
|
|
|
|
|
- totalCost: { value: 0, formatted: '$0.00', change: '0%', changeType: 'neutral' },
|
|
|
|
|
- timeSaved: { value: '0h', formatted: '0 hours', change: '0%', changeType: 'neutral' },
|
|
|
|
|
- humanCostSaved: { value: 0, formatted: '$0.00', change: '0%', changeType: 'neutral' },
|
|
|
|
|
- resolutionRate: { value: 0, dailyChange: '0%', weeklyChange: '0%' },
|
|
|
|
|
- topIntents: []
|
|
|
|
|
- }
|
|
|
|
|
- }),
|
|
|
|
|
- { status: 200, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
|
|
|
|
|
- )
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // GET /api/call-logs - Get call logs
|
|
|
|
|
- if (path === 'call-logs' && req.method === 'GET') {
|
|
|
|
|
try {
|
|
try {
|
|
|
- // Fetch call logs from database
|
|
|
|
|
- const { data: callLogs, error: callLogsError } = await supabase
|
|
|
|
|
- .from('call_logs')
|
|
|
|
|
- .select('*')
|
|
|
|
|
|
|
+ // Get date boundaries
|
|
|
|
|
+ const now = new Date()
|
|
|
|
|
+ const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate())
|
|
|
|
|
+ const yesterdayStart = new Date(todayStart.getTime() - 24 * 60 * 60 * 1000)
|
|
|
|
|
+ const weekStart = new Date(todayStart.getTime() - 7 * 24 * 60 * 60 * 1000)
|
|
|
|
|
+
|
|
|
|
|
+ // Get user's store IDs first
|
|
|
|
|
+ const { data: stores, error: storesError } = await supabase
|
|
|
|
|
+ .from('stores')
|
|
|
|
|
+ .select('id')
|
|
|
.eq('user_id', user.id)
|
|
.eq('user_id', user.id)
|
|
|
- .order('created_at', { ascending: false })
|
|
|
|
|
|
|
|
|
|
- if (callLogsError) {
|
|
|
|
|
- console.error('Error fetching call logs:', callLogsError)
|
|
|
|
|
|
|
+ if (storesError) {
|
|
|
|
|
+ console.error('Error fetching stores:', storesError)
|
|
|
|
|
+ throw new Error('Failed to fetch stores')
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const storeIds = (stores || []).map(s => s.id)
|
|
|
|
|
+
|
|
|
|
|
+ if (storeIds.length === 0) {
|
|
|
|
|
+ // No stores, return empty stats
|
|
|
return new Response(
|
|
return new Response(
|
|
|
- JSON.stringify({ error: 'Failed to fetch call logs' }),
|
|
|
|
|
- { status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
|
|
|
|
|
|
|
+ JSON.stringify({
|
|
|
|
|
+ success: true,
|
|
|
|
|
+ stats: {
|
|
|
|
|
+ totalCalls: { value: 0, change: '0%', changeType: 'neutral' },
|
|
|
|
|
+ todayCalls: { value: 0, change: '0%', changeType: 'neutral' },
|
|
|
|
|
+ avgDuration: { value: 0, formatted: '0:00', change: '0%', changeType: 'neutral' },
|
|
|
|
|
+ totalDuration: { value: 0, formatted: '0h 0m', change: '0%', changeType: 'neutral' },
|
|
|
|
|
+ recentCalls: []
|
|
|
|
|
+ }
|
|
|
|
|
+ }),
|
|
|
|
|
+ { status: 200, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
|
|
|
)
|
|
)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // Transform call logs to match frontend format
|
|
|
|
|
- const transformedCallLogs = (callLogs || []).map(log => {
|
|
|
|
|
- // Format duration
|
|
|
|
|
- const duration = log.duration_seconds
|
|
|
|
|
- ? `${Math.floor(log.duration_seconds / 60)}:${String(log.duration_seconds % 60).padStart(2, '0')}`
|
|
|
|
|
- : '0:00'
|
|
|
|
|
-
|
|
|
|
|
- // Determine outcome color
|
|
|
|
|
- let outcomeColor = 'text-slate-300'
|
|
|
|
|
- if (log.call_outcome === 'resolved' || log.call_outcome === 'interested') {
|
|
|
|
|
- outcomeColor = 'text-green-500'
|
|
|
|
|
- } else if (log.call_outcome === 'not_interested' || log.call_outcome === 'failed') {
|
|
|
|
|
- outcomeColor = 'text-red-500'
|
|
|
|
|
- } else if (log.call_outcome === 'pending') {
|
|
|
|
|
- outcomeColor = 'text-yellow-500'
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ // Get all call logs for user's stores
|
|
|
|
|
+ const { data: allCalls, error: callsError } = await supabase
|
|
|
|
|
+ .from('call_logs')
|
|
|
|
|
+ .select('id, created_at, started_at, ended_at, duration, caller, store_id')
|
|
|
|
|
+ .in('store_id', storeIds)
|
|
|
|
|
+ .order('created_at', { ascending: false })
|
|
|
|
|
|
|
|
- // Determine sentiment from analysis or summary
|
|
|
|
|
- let sentiment = 'Neutral'
|
|
|
|
|
- let sentimentColor = 'text-slate-300'
|
|
|
|
|
|
|
+ if (callsError) {
|
|
|
|
|
+ console.error('Error fetching call logs:', callsError)
|
|
|
|
|
+ throw new Error('Failed to fetch call logs')
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- if (log.analysis?.successEvaluation === 'true' || log.analysis?.successEvaluation === true) {
|
|
|
|
|
- sentiment = 'Positive'
|
|
|
|
|
- sentimentColor = 'text-green-500'
|
|
|
|
|
- } else if (log.analysis?.successEvaluation === 'false' || log.analysis?.successEvaluation === false) {
|
|
|
|
|
- sentiment = 'Negative'
|
|
|
|
|
- sentimentColor = 'text-red-500'
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ const calls = allCalls || []
|
|
|
|
|
+
|
|
|
|
|
+ // Calculate stats
|
|
|
|
|
+ const totalCalls = calls.length
|
|
|
|
|
+ const todayCalls = calls.filter(c => new Date(c.created_at) >= todayStart).length
|
|
|
|
|
+ const yesterdayCalls = calls.filter(c => {
|
|
|
|
|
+ const date = new Date(c.created_at)
|
|
|
|
|
+ return date >= yesterdayStart && date < todayStart
|
|
|
|
|
+ }).length
|
|
|
|
|
+ const weekCalls = calls.filter(c => new Date(c.created_at) >= weekStart).length
|
|
|
|
|
+
|
|
|
|
|
+ // Calculate average duration (in seconds)
|
|
|
|
|
+ const callsWithDuration = calls.filter(c => c.duration && parseFloat(c.duration) > 0)
|
|
|
|
|
+ const totalDurationSeconds = callsWithDuration.reduce((sum, c) => sum + parseFloat(c.duration || '0'), 0)
|
|
|
|
|
+ const avgDurationSeconds = callsWithDuration.length > 0 ? totalDurationSeconds / callsWithDuration.length : 0
|
|
|
|
|
+
|
|
|
|
|
+ // Calculate today's avg duration vs yesterday
|
|
|
|
|
+ const todayCallsWithDuration = calls.filter(c =>
|
|
|
|
|
+ new Date(c.created_at) >= todayStart && c.duration && parseFloat(c.duration) > 0
|
|
|
|
|
+ )
|
|
|
|
|
+ const yesterdayCallsWithDuration = calls.filter(c => {
|
|
|
|
|
+ const date = new Date(c.created_at)
|
|
|
|
|
+ return date >= yesterdayStart && date < todayStart && c.duration && parseFloat(c.duration) > 0
|
|
|
|
|
+ })
|
|
|
|
|
|
|
|
- // Format cost
|
|
|
|
|
- const cost = log.cost_total ? `$${Number(log.cost_total).toFixed(4)}` : '$0.00'
|
|
|
|
|
|
|
+ const todayAvgDuration = todayCallsWithDuration.length > 0
|
|
|
|
|
+ ? todayCallsWithDuration.reduce((sum, c) => sum + parseFloat(c.duration || '0'), 0) / todayCallsWithDuration.length
|
|
|
|
|
+ : 0
|
|
|
|
|
+ const yesterdayAvgDuration = yesterdayCallsWithDuration.length > 0
|
|
|
|
|
+ ? yesterdayCallsWithDuration.reduce((sum, c) => sum + parseFloat(c.duration || '0'), 0) / yesterdayCallsWithDuration.length
|
|
|
|
|
+ : 0
|
|
|
|
|
+
|
|
|
|
|
+ // Calculate changes
|
|
|
|
|
+ const callsChange = yesterdayCalls > 0
|
|
|
|
|
+ ? Math.round(((todayCalls - yesterdayCalls) / yesterdayCalls) * 100)
|
|
|
|
|
+ : (todayCalls > 0 ? 100 : 0)
|
|
|
|
|
+ const durationChange = yesterdayAvgDuration > 0
|
|
|
|
|
+ ? Math.round(((todayAvgDuration - yesterdayAvgDuration) / yesterdayAvgDuration) * 100)
|
|
|
|
|
+ : 0
|
|
|
|
|
+
|
|
|
|
|
+ // Format duration helper
|
|
|
|
|
+ const formatDuration = (seconds: number): string => {
|
|
|
|
|
+ if (seconds < 60) return `${Math.round(seconds)}s`
|
|
|
|
|
+ const mins = Math.floor(seconds / 60)
|
|
|
|
|
+ const secs = Math.round(seconds % 60)
|
|
|
|
|
+ return `${mins}:${secs.toString().padStart(2, '0')}`
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- // Format time
|
|
|
|
|
- const time = new Date(log.created_at).toLocaleString()
|
|
|
|
|
|
|
+ const formatTotalDuration = (seconds: number): string => {
|
|
|
|
|
+ const hours = Math.floor(seconds / 3600)
|
|
|
|
|
+ const mins = Math.floor((seconds % 3600) / 60)
|
|
|
|
|
+ if (hours > 0) return `${hours}h ${mins}m`
|
|
|
|
|
+ return `${mins}m`
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- return {
|
|
|
|
|
- id: log.id,
|
|
|
|
|
- time,
|
|
|
|
|
- customer: log.customer_number || 'Unknown',
|
|
|
|
|
- intent: log.summary || 'N/A',
|
|
|
|
|
- outcome: log.call_outcome || 'pending',
|
|
|
|
|
- duration,
|
|
|
|
|
- sentiment,
|
|
|
|
|
- cost,
|
|
|
|
|
- outcomeColor,
|
|
|
|
|
- sentimentColor,
|
|
|
|
|
- // Include full log data for details modal
|
|
|
|
|
- fullData: log
|
|
|
|
|
- }
|
|
|
|
|
- })
|
|
|
|
|
|
|
+ // Get recent calls (last 5)
|
|
|
|
|
+ const recentCalls = calls.slice(0, 5).map(call => ({
|
|
|
|
|
+ id: call.id,
|
|
|
|
|
+ time: call.started_at || call.created_at,
|
|
|
|
|
+ caller: call.caller || null,
|
|
|
|
|
+ duration: call.duration ? formatDuration(parseFloat(call.duration)) : '-',
|
|
|
|
|
+ store_id: call.store_id
|
|
|
|
|
+ }))
|
|
|
|
|
|
|
|
return new Response(
|
|
return new Response(
|
|
|
JSON.stringify({
|
|
JSON.stringify({
|
|
|
success: true,
|
|
success: true,
|
|
|
- call_logs: transformedCallLogs
|
|
|
|
|
|
|
+ stats: {
|
|
|
|
|
+ totalCalls: {
|
|
|
|
|
+ value: totalCalls,
|
|
|
|
|
+ change: `${callsChange >= 0 ? '+' : ''}${callsChange}%`,
|
|
|
|
|
+ changeType: callsChange >= 0 ? 'positive' : 'negative'
|
|
|
|
|
+ },
|
|
|
|
|
+ todayCalls: {
|
|
|
|
|
+ value: todayCalls,
|
|
|
|
|
+ change: `${callsChange >= 0 ? '+' : ''}${callsChange}%`,
|
|
|
|
|
+ changeType: callsChange >= 0 ? 'positive' : 'negative'
|
|
|
|
|
+ },
|
|
|
|
|
+ weekCalls: {
|
|
|
|
|
+ value: weekCalls,
|
|
|
|
|
+ change: '0%',
|
|
|
|
|
+ changeType: 'neutral'
|
|
|
|
|
+ },
|
|
|
|
|
+ avgDuration: {
|
|
|
|
|
+ value: Math.round(avgDurationSeconds),
|
|
|
|
|
+ formatted: formatDuration(avgDurationSeconds),
|
|
|
|
|
+ change: `${durationChange >= 0 ? '+' : ''}${durationChange}%`,
|
|
|
|
|
+ changeType: durationChange >= 0 ? 'positive' : 'negative'
|
|
|
|
|
+ },
|
|
|
|
|
+ totalDuration: {
|
|
|
|
|
+ value: Math.round(totalDurationSeconds),
|
|
|
|
|
+ formatted: formatTotalDuration(totalDurationSeconds),
|
|
|
|
|
+ change: '0%',
|
|
|
|
|
+ changeType: 'neutral'
|
|
|
|
|
+ },
|
|
|
|
|
+ recentCalls
|
|
|
|
|
+ }
|
|
|
}),
|
|
}),
|
|
|
{ status: 200, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
|
|
{ status: 200, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
|
|
|
)
|
|
)
|
|
|
} catch (error) {
|
|
} catch (error) {
|
|
|
- console.error('Error in call-logs endpoint:', error)
|
|
|
|
|
|
|
+ console.error('Error in dashboard/stats endpoint:', error)
|
|
|
return new Response(
|
|
return new Response(
|
|
|
- JSON.stringify({ error: 'Internal server error' }),
|
|
|
|
|
|
|
+ JSON.stringify({ error: 'Failed to fetch dashboard stats' }),
|
|
|
{ status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
|
|
{ status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
|
|
|
)
|
|
)
|
|
|
}
|
|
}
|