276 lines
8.6 KiB
TypeScript
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,
|
|
};
|
|
} |