Эх сурвалжийг харах

feat(call-logs): add realtime updates for incoming calls

- Subscribe to call_logs table using Supabase Realtime
- Auto-update UI when new calls come in (INSERT)
- Auto-update when call data changes (UPDATE)
- Show live/offline status indicator in header
- Add translations for realtime status (HU, EN, DE)

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

Co-Authored-By: Claude <noreply@anthropic.com>
Fszontagh 4 сар өмнө
parent
commit
a65639b9ad

+ 136 - 4
shopcall.ai-main/src/components/CallLogsContent.tsx

@@ -1,8 +1,8 @@
 import { Button } from "@/components/ui/button";
 import { Input } from "@/components/ui/input";
 import { Card } from "@/components/ui/card";
-import { Search, Download, Calendar, Filter, ChevronRight, Phone, Clock, CheckCircle, XCircle, Loader2 } from "lucide-react";
-import { useState, useEffect } from "react";
+import { Search, Download, Calendar, Filter, ChevronRight, Phone, Clock, CheckCircle, XCircle, Loader2, Radio } from "lucide-react";
+import { useState, useEffect, useCallback, useRef } from "react";
 import { useNavigate } from "react-router-dom";
 import { ExportModal, ExportFormat } from "./ExportModal";
 import { exportCallLogs } from "@/lib/exportUtils";
@@ -11,6 +11,7 @@ import { supabase } from "@/lib/supabase";
 import { API_URL } from "@/lib/config";
 import { LoadingScreen } from "@/components/ui/loading-screen";
 import { AnalyticsStatus, ANALYTICS_STATUS } from "@/types/database.types";
+import { RealtimeChannel } from "@supabase/supabase-js";
 import {
   Tooltip,
   TooltipContent,
@@ -136,6 +137,30 @@ const formatDateTime = (dateStr: string | null): string => {
   return date.toLocaleString();
 };
 
+// Realtime connection status indicator
+const RealtimeStatusIndicator = ({ isConnected, t }: { isConnected: boolean; t: (key: string) => string }) => {
+  return (
+    <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`}>
+            {isConnected && (
+              <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 ${isConnected ? 'bg-green-500' : 'bg-slate-500'}`}></span>
+          </span>
+          <span className="text-xs text-slate-400">
+            {isConnected ? t('callLogs.realtime.live') : t('callLogs.realtime.disconnected')}
+          </span>
+        </div>
+      </TooltipTrigger>
+      <TooltipContent>
+        <p>{isConnected ? t('callLogs.realtime.liveDescription') : t('callLogs.realtime.disconnectedDescription')}</p>
+      </TooltipContent>
+    </Tooltip>
+  );
+};
+
 export function CallLogsContent() {
   const { t } = useTranslation();
   const navigate = useNavigate();
@@ -144,6 +169,9 @@ export function CallLogsContent() {
   const [isLoading, setIsLoading] = useState(true);
   const [error, setError] = useState<string | null>(null);
   const [searchQuery, setSearchQuery] = useState("");
+  const [isRealtimeConnected, setIsRealtimeConnected] = useState(false);
+  const [userStoreIds, setUserStoreIds] = useState<string[]>([]);
+  const channelRef = useRef<RealtimeChannel | null>(null);
 
   const fetchCallLogs = async () => {
     setIsLoading(true);
@@ -175,10 +203,110 @@ export function CallLogsContent() {
     }
   };
 
-  useEffect(() => {
-    fetchCallLogs();
+  // 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 [];
+    }
+  }, []);
+
+  // Handle realtime call log updates
+  const handleRealtimeUpdate = useCallback((payload: { eventType: string; new: CallLog; old: { id: string } }) => {
+    console.log('[Realtime] Received event:', payload.eventType, payload);
+
+    if (payload.eventType === 'INSERT') {
+      // Add new call log at the beginning of the list
+      setCallLogs(prev => {
+        // Check if already exists (prevent duplicates)
+        if (prev.some(log => log.id === payload.new.id)) {
+          return prev;
+        }
+        return [payload.new, ...prev];
+      });
+    } else if (payload.eventType === 'UPDATE') {
+      // Update existing call log
+      setCallLogs(prev =>
+        prev.map(log => log.id === payload.new.id ? payload.new : log)
+      );
+    } else if (payload.eventType === 'DELETE') {
+      // Remove deleted call log
+      setCallLogs(prev => prev.filter(log => log.id !== payload.old.id));
+    }
   }, []);
 
+  // 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('[Realtime] No stores to subscribe to');
+      return;
+    }
+
+    console.log('[Realtime] Setting up subscription for stores:', storeIds);
+
+    // Create channel with unique name
+    const channel = supabase
+      .channel(`call_logs_realtime_${Date.now()}`)
+      .on(
+        'postgres_changes',
+        {
+          event: '*',
+          schema: 'public',
+          table: 'call_logs',
+          // Filter by user's store IDs - Supabase realtime supports 'in' filter
+          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 } });
+        }
+      )
+      .subscribe((status) => {
+        console.log('[Realtime] Subscription status:', status);
+        setIsRealtimeConnected(status === 'SUBSCRIBED');
+      });
+
+    channelRef.current = channel;
+  }, [handleRealtimeUpdate]);
+
+  // Initial load and realtime setup
+  useEffect(() => {
+    const initialize = async () => {
+      await fetchCallLogs();
+      const storeIds = await fetchUserStoreIds();
+      setUserStoreIds(storeIds);
+      await setupRealtimeSubscription(storeIds);
+    };
+
+    initialize();
+
+    // Cleanup on unmount
+    return () => {
+      if (channelRef.current) {
+        console.log('[Realtime] Cleaning up subscription');
+        supabase.removeChannel(channelRef.current);
+        channelRef.current = null;
+      }
+    };
+  }, [fetchUserStoreIds, setupRealtimeSubscription]);
+
   const handleRowClick = (callId: string) => {
     navigate(`/call-logs/${callId}`);
   };
@@ -226,6 +354,10 @@ export function CallLogsContent() {
             </div>
 
             <div className="flex items-center gap-4">
+              <TooltipProvider>
+                <RealtimeStatusIndicator isConnected={isRealtimeConnected} t={t} />
+              </TooltipProvider>
+
               <Button
                 className="bg-slate-700 hover:bg-slate-600 text-white"
                 onClick={handleExportClick}

+ 6 - 0
shopcall.ai-main/src/i18n/locales/de.json

@@ -388,6 +388,12 @@
     "retryAnalytics": "Analyse wiederholen",
     "runAnalytics": "Analyse starten",
     "triggeringAnalytics": "Analyse wird gestartet...",
+    "realtime": {
+      "live": "Live",
+      "disconnected": "Offline",
+      "liveDescription": "Echtzeit-Updates sind aktiv. Neue Anrufe erscheinen automatisch.",
+      "disconnectedDescription": "Echtzeit-Updates sind getrennt. Aktualisieren Sie, um die neuesten Anrufe zu sehen."
+    },
     "noCalls": "Keine Anrufe gefunden",
     "exportDialog": {
       "title": "Anrufprotokolle Exportieren",

+ 6 - 0
shopcall.ai-main/src/i18n/locales/en.json

@@ -391,6 +391,12 @@
     "retryAnalytics": "Retry Analytics",
     "runAnalytics": "Run Analytics",
     "triggeringAnalytics": "Starting analytics...",
+    "realtime": {
+      "live": "Live",
+      "disconnected": "Offline",
+      "liveDescription": "Real-time updates are active. New calls will appear automatically.",
+      "disconnectedDescription": "Real-time updates are disconnected. Refresh to see latest calls."
+    },
     "exportDialog": {
       "title": "Export Call Logs",
       "description": "Export {{count}} call logs in your preferred format",

+ 6 - 0
shopcall.ai-main/src/i18n/locales/hu.json

@@ -390,6 +390,12 @@
     "retryAnalytics": "Elemzés újra",
     "runAnalytics": "Elemzés indítása",
     "triggeringAnalytics": "Elemzés indítása...",
+    "realtime": {
+      "live": "Élő",
+      "disconnected": "Offline",
+      "liveDescription": "Valós idejű frissítések aktívak. Az új hívások automatikusan megjelennek.",
+      "disconnectedDescription": "Valós idejű frissítések szünetelnek. Frissítsen a legújabb hívások megtekintéséhez."
+    },
     "noCalls": "Nem található hívás",
     "exportDialog": {
       "title": "Hívásnapló Exportálása",