"use client"; import { useState, useEffect, useCallback } from "react"; import { supabaseClient } from "@/lib/supabase/client"; import type { Database, ActivityItem, TaskComment } from "@/lib/supabase/database.types"; type Task = Database['public']['Tables']['tasks']['Row']; type Project = Database['public']['Tables']['projects']['Row']; interface User { id: string; name: string; email: string; avatar_url: string | null; } export type ActivityFilterType = 'all' | 'task_created' | 'task_completed' | 'task_updated' | 'comment_added' | 'task_assigned'; interface UseActivityFeedOptions { limit?: number; projectId?: string; filterType?: ActivityFilterType; } // User name cache to avoid repeated lookups const userNameCache: Record = { // Hardcoded known users "9c29cc99-81a1-4e75-8dff-cd7cc5ceb5aa": "Max", "0a3e400c-3932-48ae-9b65-f3f9c6f26fe9": "Matt", }; function getUserName(userId: string | null, users: User[]): string { if (!userId) return 'Unknown'; // Check cache first if (userNameCache[userId]) { return userNameCache[userId]; } // Look up in users array const user = users.find(u => u.id === userId); if (user?.name) { userNameCache[userId] = user.name; return user.name; } return 'Unknown'; } export function useActivityFeed(options: UseActivityFeedOptions = {}) { const { limit = 50, projectId, filterType = 'all' } = options; const [activities, setActivities] = useState([]); const [projects, setProjects] = useState([]); const [users, setUsers] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const fetchUsers = useCallback(async () => { try { const { data, error } = await supabaseClient .from('users') .select('*') .order('name'); if (error) throw error; const typedUsers: User[] = (data || []).map((u: any) => ({ id: u.id, name: u.name, email: u.email, avatar_url: u.avatar_url, })); setUsers(typedUsers); // Populate cache typedUsers.forEach(user => { if (user.name) { userNameCache[user.id] = user.name; } }); } catch (err) { console.error('Error fetching users:', err); } }, []); const fetchProjects = useCallback(async () => { try { const { data, error } = await supabaseClient .from('projects') .select('*') .order('name'); if (error) throw error; setProjects(data || []); } catch (err) { console.error('Error fetching projects:', err); } }, []); const fetchActivities = useCallback(async () => { setLoading(true); setError(null); try { // Build the query for tasks let query = supabaseClient .from('tasks') .select(` *, projects:project_id (*), assignee:assignee_id (id, name) `) .order('updated_at', { ascending: false }) .limit(limit); if (projectId) { query = query.eq('project_id', projectId); } const { data: tasks, error: tasksError } = await query; if (tasksError) throw tasksError; // Convert tasks to activity items const activityItems: ActivityItem[] = []; tasks?.forEach((task: Task & { projects: Project; assignee?: User }) => { const project = task.projects; // Get user names from our users array const createdByName = getUserName(task.created_by_id, users); const updatedByName = getUserName(task.updated_by_id, users); const assigneeName = task.assignee?.name || getUserName(task.assignee_id, users); // Task creation activity if (filterType === 'all' || filterType === 'task_created') { activityItems.push({ id: `${task.id}-created`, type: 'task_created', task_id: task.id, task_title: task.title, project_id: task.project_id, project_name: project?.name || 'Unknown Project', project_color: project?.color || '#6B7280', user_id: task.created_by_id || '', user_name: createdByName, timestamp: task.created_at, }); } // Task completion activity if (task.status === 'done' && (filterType === 'all' || filterType === 'task_completed')) { activityItems.push({ id: `${task.id}-completed`, type: 'task_completed', task_id: task.id, task_title: task.title, project_id: task.project_id, project_name: project?.name || 'Unknown Project', project_color: project?.color || '#6B7280', user_id: task.updated_by_id || task.created_by_id || '', user_name: updatedByName || createdByName, timestamp: task.updated_at, }); } // Assignment activity if (task.assignee_id && (filterType === 'all' || filterType === 'task_assigned')) { activityItems.push({ id: `${task.id}-assigned`, type: 'task_assigned', task_id: task.id, task_title: task.title, project_id: task.project_id, project_name: project?.name || 'Unknown Project', project_color: project?.color || '#6B7280', user_id: task.updated_by_id || task.created_by_id || '', user_name: updatedByName || createdByName, timestamp: task.updated_at, details: `Assigned to ${assigneeName}`, }); } // Task update activity (if updated after creation and not completed) if (task.updated_at !== task.created_at && task.status !== 'done' && (filterType === 'all' || filterType === 'task_updated')) { activityItems.push({ id: `${task.id}-updated`, type: 'task_updated', task_id: task.id, task_title: task.title, project_id: task.project_id, project_name: project?.name || 'Unknown Project', project_color: project?.color || '#6B7280', user_id: task.updated_by_id || '', user_name: updatedByName, timestamp: task.updated_at, details: `Status: ${task.status}`, }); } // Comment activities if (task.comments && Array.isArray(task.comments) && (filterType === 'all' || filterType === 'comment_added')) { const comments: TaskComment[] = task.comments; comments.forEach((comment) => { activityItems.push({ id: `${task.id}-comment-${comment.id}`, type: 'comment_added', task_id: task.id, task_title: task.title, project_id: task.project_id, project_name: project?.name || 'Unknown Project', project_color: project?.color || '#6B7280', user_id: comment.user_id, user_name: comment.user_name || getUserName(comment.user_id, users), timestamp: comment.created_at, comment_text: comment.text, }); }); } }); // Sort by timestamp descending activityItems.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime() ); // Apply limit after all activities are collected setActivities(activityItems.slice(0, limit)); } catch (err) { console.error('Error fetching activities:', err); setError(err instanceof Error ? err.message : 'Failed to fetch activities'); } finally { setLoading(false); } }, [limit, projectId, filterType, users]); useEffect(() => { fetchUsers(); fetchProjects(); }, [fetchUsers, fetchProjects]); useEffect(() => { if (users.length > 0) { fetchActivities(); } }, [fetchActivities, users]); // Poll for updates every 30 seconds (since realtime WebSocket is disabled) useEffect(() => { const interval = setInterval(() => { fetchActivities(); }, 30000); // 30 seconds return () => clearInterval(interval); }, [fetchActivities]); // Note: Real-time subscription disabled due to WebSocket connection issues // The activity feed uses regular HTTP polling instead (30s interval) // To re-enable realtime, configure Supabase Realtime in your project settings const refresh = useCallback(() => { fetchActivities(); }, [fetchActivities]); return { activities, projects, loading, error, refresh, }; }