Pārlūkot izejas kodu

feat(dashboard): add realtime updates for stats and recent calls

- Add realtime subscription to DashboardContext for KPI stats
- Add realtime subscription to RecentCallsTable for recent calls
- Show outcome and analytics status columns in recent calls table
- Add live indicator to dashboard header and recent calls section
- Remove phone number masking (show full caller number)
- Stats refresh automatically when new calls come in

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Fszontagh 4 mēneši atpakaļ
vecāks
revīzija
7711d91445

+ 34 - 3
shopcall.ai-main/src/components/DashboardHeader.tsx

@@ -10,9 +10,17 @@ import { format } from "date-fns";
 import { DateRange } from "react-day-picker";
 import { DateRange } from "react-day-picker";
 import { cn } from "@/lib/utils";
 import { cn } from "@/lib/utils";
 import { useTranslation } from "react-i18next";
 import { useTranslation } from "react-i18next";
+import { useDashboard } from "./context/DashboardContext";
+import {
+  Tooltip,
+  TooltipContent,
+  TooltipProvider,
+  TooltipTrigger,
+} from "@/components/ui/tooltip";
 
 
 export function DashboardHeader() {
 export function DashboardHeader() {
   const { t } = useTranslation();
   const { t } = useTranslation();
+  const { isRealtimeConnected } = useDashboard();
   const [dateRange, setDateRange] = useState<DateRange | undefined>();
   const [dateRange, setDateRange] = useState<DateRange | undefined>();
   const [quickSelect, setQuickSelect] = useState<string>("1");
   const [quickSelect, setQuickSelect] = useState<string>("1");
 
 
@@ -34,9 +42,32 @@ export function DashboardHeader() {
   return (
   return (
     <div className="border-b border-slate-800 bg-slate-900">
     <div className="border-b border-slate-800 bg-slate-900">
       <div className="flex items-center justify-between p-6">
       <div className="flex items-center justify-between p-6">
-        <div>
-          <h1 className="text-2xl font-semibold text-white">{t('dashboard.title')}</h1>
-          <p className="text-slate-400">{t('dashboard.subtitle')} for {format(new Date(), 'MMMM dd, yyyy')}</p>
+        <div className="flex items-center gap-4">
+          <div>
+            <h1 className="text-2xl font-semibold text-white">{t('dashboard.title')}</h1>
+            <p className="text-slate-400">{t('dashboard.subtitle')} for {format(new Date(), 'MMMM dd, yyyy')}</p>
+          </div>
+          {/* Realtime indicator */}
+          <TooltipProvider>
+            <Tooltip>
+              <TooltipTrigger asChild>
+                <div className="flex items-center gap-1.5 px-2 py-1 rounded-full bg-slate-700/50">
+                  <span className="relative flex h-2 w-2">
+                    {isRealtimeConnected && (
+                      <span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75"></span>
+                    )}
+                    <span className={`relative inline-flex rounded-full h-2 w-2 ${isRealtimeConnected ? 'bg-green-500' : 'bg-slate-500'}`}></span>
+                  </span>
+                  <span className="text-xs text-slate-400">
+                    {isRealtimeConnected ? t('callLogs.realtime.live') : t('callLogs.realtime.disconnected')}
+                  </span>
+                </div>
+              </TooltipTrigger>
+              <TooltipContent>
+                <p>{isRealtimeConnected ? t('callLogs.realtime.liveDescription') : t('callLogs.realtime.disconnectedDescription')}</p>
+              </TooltipContent>
+            </Tooltip>
+          </TooltipProvider>
         </div>
         </div>
         
         
         <div className="flex items-center gap-4">
         <div className="flex items-center gap-4">

+ 277 - 61
shopcall.ai-main/src/components/RecentCallsTable.tsx

@@ -1,11 +1,19 @@
 import { Card } from "@/components/ui/card";
 import { Card } from "@/components/ui/card";
 import { Button } from "@/components/ui/button";
 import { Button } from "@/components/ui/button";
-import { ChevronRight, Loader2 } from "lucide-react";
-import { useState, useEffect } from "react";
+import { ChevronRight, Loader2, Clock, CheckCircle, XCircle } from "lucide-react";
+import { useState, useEffect, useCallback, useRef } from "react";
 import { useNavigate } from "react-router-dom";
 import { useNavigate } from "react-router-dom";
 import { API_URL } from "@/lib/config";
 import { API_URL } from "@/lib/config";
 import { supabase } from "@/lib/supabase";
 import { supabase } from "@/lib/supabase";
 import { useTranslation } from "react-i18next";
 import { useTranslation } from "react-i18next";
+import { RealtimeChannel } from "@supabase/supabase-js";
+import { AnalyticsStatus, ANALYTICS_STATUS } from "@/types/database.types";
+import {
+  Tooltip,
+  TooltipContent,
+  TooltipProvider,
+  TooltipTrigger,
+} from "@/components/ui/tooltip";
 
 
 interface CallLog {
 interface CallLog {
   id: string;
   id: string;
@@ -15,6 +23,8 @@ interface CallLog {
   ended_at: string | null;
   ended_at: string | null;
   duration: number | null;
   duration: number | null;
   caller: string | null;
   caller: string | null;
+  outcome: string | null;
+  analytics_status: AnalyticsStatus | null;
 }
 }
 
 
 const formatDuration = (seconds: number | null): string => {
 const formatDuration = (seconds: number | null): string => {
@@ -30,11 +40,90 @@ const formatDateTime = (dateStr: string | null): string => {
   return date.toLocaleString();
   return date.toLocaleString();
 };
 };
 
 
-const maskPhoneNumber = (phone: string | null): string => {
-  if (!phone) return "-";
-  if (phone.length <= 4) return phone;
-  const last4 = phone.slice(-4);
-  return `***${last4}`;
+const getOutcomeColor = (outcome: string | null): string => {
+  if (!outcome) return "bg-slate-600";
+
+  const colorMap: Record<string, string> = {
+    resolved: "bg-green-600",
+    order_placed: "bg-green-600",
+    order_inquiry: "bg-blue-600",
+    product_inquiry: "bg-blue-600",
+    general_question: "bg-blue-600",
+    complaint: "bg-red-600",
+    return_request: "bg-orange-600",
+    callback_requested: "bg-purple-600",
+    escalated: "bg-yellow-600",
+    no_answer: "bg-slate-500",
+    voicemail: "bg-slate-500",
+    wrong_number: "bg-slate-500",
+    abandoned: "bg-slate-500",
+  };
+
+  return colorMap[outcome] || "bg-slate-600";
+};
+
+const formatOutcome = (outcome: string | null): string => {
+  if (!outcome) return "-";
+  return outcome.split('_').map(word =>
+    word.charAt(0).toUpperCase() + word.slice(1)
+  ).join(' ');
+};
+
+// Analytics status indicator component
+const AnalyticsStatusIndicator = ({ status, t }: { status: AnalyticsStatus | null; t: (key: string) => string }) => {
+  if (!status || status === ANALYTICS_STATUS.PENDING) {
+    return (
+      <Tooltip>
+        <TooltipTrigger>
+          <Clock className="w-4 h-4 text-slate-400" />
+        </TooltipTrigger>
+        <TooltipContent>
+          <p>{t('callLogs.analyticsStatus.pending')}</p>
+        </TooltipContent>
+      </Tooltip>
+    );
+  }
+
+  if (status === ANALYTICS_STATUS.RUNNING) {
+    return (
+      <Tooltip>
+        <TooltipTrigger>
+          <Loader2 className="w-4 h-4 text-blue-400 animate-spin" />
+        </TooltipTrigger>
+        <TooltipContent>
+          <p>{t('callLogs.analyticsStatus.running')}</p>
+        </TooltipContent>
+      </Tooltip>
+    );
+  }
+
+  if (status === ANALYTICS_STATUS.COMPLETED) {
+    return (
+      <Tooltip>
+        <TooltipTrigger>
+          <CheckCircle className="w-4 h-4 text-green-400" />
+        </TooltipTrigger>
+        <TooltipContent>
+          <p>{t('callLogs.analyticsStatus.completed')}</p>
+        </TooltipContent>
+      </Tooltip>
+    );
+  }
+
+  if (status === ANALYTICS_STATUS.FAILED) {
+    return (
+      <Tooltip>
+        <TooltipTrigger>
+          <XCircle className="w-4 h-4 text-red-400" />
+        </TooltipTrigger>
+        <TooltipContent>
+          <p>{t('callLogs.analyticsStatus.failed')}</p>
+        </TooltipContent>
+      </Tooltip>
+    );
+  }
+
+  return null;
 };
 };
 
 
 export function RecentCallsTable() {
 export function RecentCallsTable() {
@@ -43,41 +132,138 @@ export function RecentCallsTable() {
   const [recentCalls, setRecentCalls] = useState<CallLog[]>([]);
   const [recentCalls, setRecentCalls] = useState<CallLog[]>([]);
   const [loading, setLoading] = useState(true);
   const [loading, setLoading] = useState(true);
   const [error, setError] = useState<string | null>(null);
   const [error, setError] = useState<string | null>(null);
+  const [isRealtimeConnected, setIsRealtimeConnected] = useState(false);
+  const channelRef = useRef<RealtimeChannel | null>(null);
 
 
-  useEffect(() => {
-    const fetchRecentCalls = async () => {
-      try {
-        const { data: { session } } = await supabase.auth.getSession();
-        if (!session) {
-          throw new Error('Not authenticated');
+  const fetchRecentCalls = useCallback(async (showLoading = true) => {
+    try {
+      if (showLoading) setLoading(true);
+      const { data: { session } } = await supabase.auth.getSession();
+      if (!session) {
+        throw new Error('Not authenticated');
+      }
+
+      const response = await fetch(`${API_URL}/api/call-logs`, {
+        headers: {
+          'Authorization': `Bearer ${session.access_token}`,
+          'Content-Type': 'application/json'
         }
         }
+      });
+
+      if (!response.ok) {
+        throw new Error('Failed to fetch call logs');
+      }
+
+      const data = await response.json();
+      if (data.success && data.call_logs) {
+        // Take only the 5 most recent calls
+        setRecentCalls(data.call_logs.slice(0, 5));
+      }
+    } catch (err) {
+      console.error('Error fetching call logs:', err);
+      setError(err instanceof Error ? err.message : 'Failed to load call logs');
+    } finally {
+      setLoading(false);
+    }
+  }, []);
+
+  // Fetch user's store IDs for realtime filtering
+  const fetchUserStoreIds = useCallback(async () => {
+    try {
+      const { data: { session } } = await supabase.auth.getSession();
+      if (!session) return [];
+
+      const { data: stores } = await supabase
+        .from('stores')
+        .select('id')
+        .eq('user_id', session.user.id);
+
+      return stores?.map(s => s.id) || [];
+    } catch (err) {
+      console.error('Failed to fetch store IDs:', err);
+      return [];
+    }
+  }, []);
 
 
-        const response = await fetch(`${API_URL}/api/call-logs`, {
-          headers: {
-            'Authorization': `Bearer ${session.access_token}`,
-            'Content-Type': 'application/json'
-          }
-        });
+  // Handle realtime updates
+  const handleRealtimeUpdate = useCallback((payload: { eventType: string; new: CallLog; old: { id: string } }) => {
+    console.log('[RecentCalls Realtime] Received event:', payload.eventType);
 
 
-        if (!response.ok) {
-          throw new Error('Failed to fetch call logs');
+    if (payload.eventType === 'INSERT') {
+      // Add new call at the beginning and keep only 5
+      setRecentCalls(prev => {
+        if (prev.some(call => call.id === payload.new.id)) {
+          return prev;
         }
         }
+        return [payload.new, ...prev].slice(0, 5);
+      });
+    } else if (payload.eventType === 'UPDATE') {
+      // Update existing call
+      setRecentCalls(prev =>
+        prev.map(call => call.id === payload.new.id ? payload.new : call)
+      );
+    } else if (payload.eventType === 'DELETE') {
+      // Remove deleted call and refetch to fill the gap
+      setRecentCalls(prev => prev.filter(call => call.id !== payload.old.id));
+      fetchRecentCalls(false);
+    }
+  }, [fetchRecentCalls]);
 
 
-        const data = await response.json();
-        if (data.success && data.call_logs) {
-          // Take only the 5 most recent calls
-          setRecentCalls(data.call_logs.slice(0, 5));
+  // Setup realtime subscription
+  const setupRealtimeSubscription = useCallback(async (storeIds: string[]) => {
+    if (channelRef.current) {
+      await supabase.removeChannel(channelRef.current);
+      channelRef.current = null;
+    }
+
+    if (storeIds.length === 0) {
+      console.log('[RecentCalls Realtime] No stores to subscribe to');
+      return;
+    }
+
+    console.log('[RecentCalls Realtime] Setting up subscription for stores:', storeIds);
+
+    const channel = supabase
+      .channel(`recent_calls_realtime_${Date.now()}`)
+      .on(
+        'postgres_changes',
+        {
+          event: '*',
+          schema: 'public',
+          table: 'call_logs',
+          filter: storeIds.length === 1
+            ? `store_id=eq.${storeIds[0]}`
+            : `store_id=in.(${storeIds.join(',')})`
+        },
+        (payload) => {
+          handleRealtimeUpdate(payload as { eventType: string; new: CallLog; old: { id: string } });
         }
         }
-      } catch (err) {
-        console.error('Error fetching call logs:', err);
-        setError(err instanceof Error ? err.message : 'Failed to load call logs');
-      } finally {
-        setLoading(false);
-      }
+      )
+      .subscribe((status) => {
+        console.log('[RecentCalls Realtime] Subscription status:', status);
+        setIsRealtimeConnected(status === 'SUBSCRIBED');
+      });
+
+    channelRef.current = channel;
+  }, [handleRealtimeUpdate]);
+
+  useEffect(() => {
+    const initialize = async () => {
+      await fetchRecentCalls();
+      const storeIds = await fetchUserStoreIds();
+      await setupRealtimeSubscription(storeIds);
     };
     };
 
 
-    fetchRecentCalls();
-  }, []);
+    initialize();
+
+    return () => {
+      if (channelRef.current) {
+        console.log('[RecentCalls Realtime] Cleaning up subscription');
+        supabase.removeChannel(channelRef.current);
+        channelRef.current = null;
+      }
+    };
+  }, [fetchRecentCalls, fetchUserStoreIds, setupRealtimeSubscription]);
 
 
   const handleRowClick = (callId: string) => {
   const handleRowClick = (callId: string) => {
     navigate(`/call-logs/${callId}`);
     navigate(`/call-logs/${callId}`);
@@ -87,7 +273,21 @@ export function RecentCallsTable() {
     <Card className="bg-slate-800 border-slate-700">
     <Card className="bg-slate-800 border-slate-700">
       <div className="p-6">
       <div className="p-6">
         <div className="flex items-center justify-between mb-6">
         <div className="flex items-center justify-between mb-6">
-          <h3 className="text-lg font-semibold text-white">{t('dashboard.recentCalls.title')}</h3>
+          <div className="flex items-center gap-3">
+            <h3 className="text-lg font-semibold text-white">{t('dashboard.recentCalls.title')}</h3>
+            {/* Realtime indicator */}
+            <div className="flex items-center gap-1.5 px-2 py-1 rounded-full bg-slate-700/50" title={isRealtimeConnected ? t('callLogs.realtime.liveDescription') : t('callLogs.realtime.disconnectedDescription')}>
+              <span className="relative flex h-2 w-2">
+                {isRealtimeConnected && (
+                  <span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75"></span>
+                )}
+                <span className={`relative inline-flex rounded-full h-2 w-2 ${isRealtimeConnected ? 'bg-green-500' : 'bg-slate-500'}`}></span>
+              </span>
+              <span className="text-xs text-slate-400">
+                {isRealtimeConnected ? t('callLogs.realtime.live') : t('callLogs.realtime.disconnected')}
+              </span>
+            </div>
+          </div>
           <Button
           <Button
             variant="ghost"
             variant="ghost"
             size="sm"
             size="sm"
@@ -112,34 +312,50 @@ export function RecentCallsTable() {
             {t('dashboard.recentCalls.noCalls')}
             {t('dashboard.recentCalls.noCalls')}
           </div>
           </div>
         ) : (
         ) : (
-          <div className="overflow-x-auto">
-            <table className="w-full">
-              <thead>
-                <tr className="border-b border-slate-700">
-                  <th className="text-left py-3 px-4 text-sm font-medium text-slate-400">{t('dashboard.recentCalls.headers.time')}</th>
-                  <th className="text-left py-3 px-4 text-sm font-medium text-slate-400">{t('dashboard.recentCalls.headers.caller')}</th>
-                  <th className="text-left py-3 px-4 text-sm font-medium text-slate-400">{t('dashboard.recentCalls.headers.duration')}</th>
-                  <th className="text-right py-3 px-4 text-sm font-medium text-slate-400"></th>
-                </tr>
-              </thead>
-              <tbody>
-                {recentCalls.map((call) => (
-                  <tr
-                    key={call.id}
-                    className="border-b border-slate-700/50 hover:bg-slate-700/30 cursor-pointer transition-colors"
-                    onClick={() => handleRowClick(call.id)}
-                  >
-                    <td className="py-3 px-4 text-sm text-slate-300">{formatDateTime(call.started_at)}</td>
-                    <td className="py-3 px-4 text-sm text-white">{maskPhoneNumber(call.caller)}</td>
-                    <td className="py-3 px-4 text-sm text-slate-300">{formatDuration(call.duration)}</td>
-                    <td className="py-3 px-4 text-right">
-                      <ChevronRight className="w-4 h-4 text-slate-400 inline-block" />
-                    </td>
+          <TooltipProvider>
+            <div className="overflow-x-auto">
+              <table className="w-full">
+                <thead>
+                  <tr className="border-b border-slate-700">
+                    <th className="text-left py-3 px-4 text-sm font-medium text-slate-400">{t('dashboard.recentCalls.headers.time')}</th>
+                    <th className="text-left py-3 px-4 text-sm font-medium text-slate-400">{t('dashboard.recentCalls.headers.caller')}</th>
+                    <th className="text-left py-3 px-4 text-sm font-medium text-slate-400">{t('dashboard.recentCalls.headers.duration')}</th>
+                    <th className="text-left py-3 px-4 text-sm font-medium text-slate-400">{t('callLogs.table.outcome')}</th>
+                    <th className="text-center py-3 px-4 text-sm font-medium text-slate-400">{t('callLogs.table.analytics')}</th>
+                    <th className="text-right py-3 px-4 text-sm font-medium text-slate-400"></th>
                   </tr>
                   </tr>
-                ))}
-              </tbody>
-            </table>
-          </div>
+                </thead>
+                <tbody>
+                  {recentCalls.map((call) => (
+                    <tr
+                      key={call.id}
+                      className="border-b border-slate-700/50 hover:bg-slate-700/30 cursor-pointer transition-colors"
+                      onClick={() => handleRowClick(call.id)}
+                    >
+                      <td className="py-3 px-4 text-sm text-slate-300">{formatDateTime(call.started_at)}</td>
+                      <td className="py-3 px-4 text-sm text-white">{call.caller || '-'}</td>
+                      <td className="py-3 px-4 text-sm text-slate-300">{formatDuration(call.duration)}</td>
+                      <td className="py-3 px-4 text-sm">
+                        {call.outcome ? (
+                          <span className={`px-2 py-0.5 rounded text-xs font-medium text-white ${getOutcomeColor(call.outcome)}`}>
+                            {formatOutcome(call.outcome)}
+                          </span>
+                        ) : (
+                          <span className="text-slate-400">-</span>
+                        )}
+                      </td>
+                      <td className="py-3 px-4 text-center">
+                        <AnalyticsStatusIndicator status={call.analytics_status} t={t} />
+                      </td>
+                      <td className="py-3 px-4 text-right">
+                        <ChevronRight className="w-4 h-4 text-slate-400 inline-block" />
+                      </td>
+                    </tr>
+                  ))}
+                </tbody>
+              </table>
+            </div>
+          </TooltipProvider>
         )}
         )}
       </div>
       </div>
     </Card>
     </Card>

+ 87 - 7
shopcall.ai-main/src/components/context/DashboardContext.tsx

@@ -1,6 +1,7 @@
-import { createContext, useContext, useState, useEffect, ReactNode } from "react";
+import { createContext, useContext, useState, useEffect, useCallback, useRef, ReactNode } from "react";
 import { API_URL } from "@/lib/config";
 import { API_URL } from "@/lib/config";
 import { supabase } from "@/lib/supabase";
 import { supabase } from "@/lib/supabase";
+import { RealtimeChannel } from "@supabase/supabase-js";
 
 
 interface RecentCall {
 interface RecentCall {
   id: string;
   id: string;
@@ -24,6 +25,7 @@ interface DashboardContextType {
   loading: boolean;
   loading: boolean;
   error: string | null;
   error: string | null;
   refetch: () => void;
   refetch: () => void;
+  isRealtimeConnected: boolean;
 }
 }
 
 
 const DashboardContext = createContext<DashboardContextType | undefined>(undefined);
 const DashboardContext = createContext<DashboardContextType | undefined>(undefined);
@@ -32,10 +34,13 @@ export function DashboardProvider({ children }: { children: ReactNode }) {
   const [stats, setStats] = useState<DashboardStats | null>(null);
   const [stats, setStats] = useState<DashboardStats | null>(null);
   const [loading, setLoading] = useState(true);
   const [loading, setLoading] = useState(true);
   const [error, setError] = useState<string | null>(null);
   const [error, setError] = useState<string | null>(null);
+  const [isRealtimeConnected, setIsRealtimeConnected] = useState(false);
+  const [userStoreIds, setUserStoreIds] = useState<string[]>([]);
+  const channelRef = useRef<RealtimeChannel | null>(null);
 
 
-  const fetchStats = async () => {
+  const fetchStats = useCallback(async (showLoading = true) => {
     try {
     try {
-      setLoading(true);
+      if (showLoading) setLoading(true);
       setError(null);
       setError(null);
 
 
       const { data: { session } } = await supabase.auth.getSession();
       const { data: { session } } = await supabase.auth.getSession();
@@ -64,14 +69,89 @@ export function DashboardProvider({ children }: { children: ReactNode }) {
     } finally {
     } finally {
       setLoading(false);
       setLoading(false);
     }
     }
-  };
+  }, []);
 
 
-  useEffect(() => {
-    fetchStats();
+  // Fetch user's store IDs for realtime filtering
+  const fetchUserStoreIds = useCallback(async () => {
+    try {
+      const { data: { session } } = await supabase.auth.getSession();
+      if (!session) return [];
+
+      const { data: stores } = await supabase
+        .from('stores')
+        .select('id')
+        .eq('user_id', session.user.id);
+
+      return stores?.map(s => s.id) || [];
+    } catch (err) {
+      console.error('Failed to fetch store IDs:', err);
+      return [];
+    }
   }, []);
   }, []);
 
 
+  // Setup realtime subscription
+  const setupRealtimeSubscription = useCallback(async (storeIds: string[]) => {
+    // Clean up existing subscription
+    if (channelRef.current) {
+      await supabase.removeChannel(channelRef.current);
+      channelRef.current = null;
+    }
+
+    if (storeIds.length === 0) {
+      console.log('[Dashboard Realtime] No stores to subscribe to');
+      return;
+    }
+
+    console.log('[Dashboard Realtime] Setting up subscription for stores:', storeIds);
+
+    const channel = supabase
+      .channel(`dashboard_realtime_${Date.now()}`)
+      .on(
+        'postgres_changes',
+        {
+          event: '*',
+          schema: 'public',
+          table: 'call_logs',
+          filter: storeIds.length === 1
+            ? `store_id=eq.${storeIds[0]}`
+            : `store_id=in.(${storeIds.join(',')})`
+        },
+        (payload) => {
+          console.log('[Dashboard Realtime] Received event:', payload.eventType);
+          // Refetch stats without showing loading indicator
+          fetchStats(false);
+        }
+      )
+      .subscribe((status) => {
+        console.log('[Dashboard Realtime] Subscription status:', status);
+        setIsRealtimeConnected(status === 'SUBSCRIBED');
+      });
+
+    channelRef.current = channel;
+  }, [fetchStats]);
+
+  useEffect(() => {
+    const initialize = async () => {
+      await fetchStats();
+      const storeIds = await fetchUserStoreIds();
+      setUserStoreIds(storeIds);
+      await setupRealtimeSubscription(storeIds);
+    };
+
+    initialize();
+
+    // Cleanup on unmount
+    return () => {
+      if (channelRef.current) {
+        console.log('[Dashboard Realtime] Cleaning up subscription');
+        supabase.removeChannel(channelRef.current);
+        channelRef.current = null;
+      }
+    };
+  }, [fetchStats, fetchUserStoreIds, setupRealtimeSubscription]);
+
   return (
   return (
-    <DashboardContext.Provider value={{ stats, loading, error, refetch: fetchStats }}>
+    <DashboardContext.Provider value={{ stats, loading, error, refetch: fetchStats, isRealtimeConnected }}>
       {children}
       {children}
     </DashboardContext.Provider>
     </DashboardContext.Provider>
   );
   );