mission-control/hooks/use-activity-feed.ts

276 lines
8.6 KiB
TypeScript

"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<string, string> = {
// 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<ActivityItem[]>([]);
const [projects, setProjects] = useState<Project[]>([]);
const [users, setUsers] = useState<User[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(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,
};
}