|
|
@@ -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}
|