diff --git a/src/app/status/page.tsx b/src/app/status/page.tsx index 807dc94..4942d56 100644 --- a/src/app/status/page.tsx +++ b/src/app/status/page.tsx @@ -1,15 +1,18 @@ -'use client'; +'use server'; import { TechTable } from '@/components/status'; -import { useAuth } from '@/components/context'; +import { getUser, getRecentUsersWithStatuses } from '@/lib/actions'; import { redirect } from 'next/navigation'; -const Status = () => { - const { isAuthenticated } = useAuth(); - if (!isAuthenticated) { +const Status = async () => { + const userRespoonse = await getUser(); + if (!userRespoonse.success) { redirect('/sign-in'); } else { - return ; + const response = await getRecentUsersWithStatuses(); + if (!response.success) throw new Error(response.error); + const usersWithStatuses = response.data; + return ; } }; export default Status; diff --git a/src/components/status/History_Drawer.tsx b/src/components/status/HistoryDrawer.tsx similarity index 100% rename from src/components/status/History_Drawer.tsx rename to src/components/status/HistoryDrawer.tsx diff --git a/src/components/status/TechTable.tsx b/src/components/status/TechTable.tsx new file mode 100644 index 0000000..34fbecd --- /dev/null +++ b/src/components/status/TechTable.tsx @@ -0,0 +1,207 @@ +'use client'; + +import { createClient } from '@/utils/supabase'; +import { useState, useEffect, useCallback, useRef } from 'react'; +import { useAuth, useTVMode } from '@/components/context'; +import { + getRecentUsersWithStatuses, + updateStatuses, + updateUserStatus, + type UserWithStatus, +} from '@/lib/hooks/status'; +import { + Drawer, + DrawerTrigger, + Progress, +} from '@/components/ui'; +import { toast } from 'sonner'; +import { HistoryDrawer } from '@/components/status'; +import type { RealtimeChannel } from '@supabase/supabase-js'; + +type TechTableProps = { + initialStatuses: UserWithStatus[]; + className?: string; +}; + +export const TechTable = ({ + initialStatuses = [], + className = 'w-full max-w-7xl mx-auto px-4', +}: TechTableProps) => { + const { isAuthenticated, profile } = useAuth(); + const { tvMode } = useTVMode(); + const [loading, setLoading] = useState(true); + const [selectedIds, setSelectedIds] = useState([]); + const [selectAll, setSelectAll] = useState(false); + const [statusInput, setStatusInput] = useState(''); + const [usersWithStatuses, setUsersWithStatuses] = useState(initialStatuses); + const [selectedHistoryUserId, setSelectedHistoryUserId] = useState(''); + + const supabase = createClient(); + + const fetchRecentUsersWithStatuses = useCallback(async () => { + try { + const response = await getRecentUsersWithStatuses(); + if (!response.success) throw new Error(response.error); + return response.data; + } catch (error) { + toast.error(`Error fetching technicians: ${error as Error}`) + return []; + } + }, []); + + // Initial load + useEffect(() => { + const loadData = async () => { + const data = await fetchRecentUsersWithStatuses(); + setUsersWithStatuses(data); + setLoading(false); + }; + loadData().catch((error) => { + console.error('Error loading data:', error); + }); + }, [fetchRecentUsersWithStatuses, isAuthenticated]); + + const updateStatus = useCallback(async () => { + if (!isAuthenticated) { + toast.error('You must be signed in to update statuses.'); + return; + } + if (!statusInput.trim()) { + toast.error('Please enter a valid status.'); + return; + } + try { + if (selectedIds.length === 0) { + const result = await updateUserStatus(statusInput); + if (!result.success) throw new Error(result.error); + toast.success(`Status updated for signed in user.`); + } else { + const result = await updateStatuses(selectedIds, statusInput); + if (!result.success) throw new Error(result.error); + toast.success(`Status updated for ${selectedIds.length} selected users.`); + } + setSelectedIds([]); + setStatusInput(''); + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + toast.error(`Failed to update status: ${errorMessage}`); + } + }, [isAuthenticated, statusInput, selectedIds, usersWithStatuses, profile]); + + const handleCheckboxChange = (id: string) => { + setSelectedIds((prev) => + prev.includes(id) ? prev.filter(prevId => prevId !== id) : [...prev, id] + ); + }; + + const handleSelectAllChange = () => { + if (selectAll) { + setSelectedIds([]); + } else { + setSelectedIds(usersWithStatuses.map(tech => tech.user.id)); + } + setSelectAll(!selectAll); + }; + + useEffect(() => { + setSelectAll( + selectedIds.length === usersWithStatuses.length && + usersWithStatuses.length > 0 + ); + }, [selectedIds.length, usersWithStatuses.length]); + + const formatTime = (timestamp: string) => { + const date = new Date(timestamp); + const time = date.toLocaleTimeString('en-US', { + hour: 'numeric', + minute: 'numeric', + }); + const day = date.getDate() + const month = date.toLocaleString('default', { month: 'long' }); + return `${time} = ${month} ${day}`; + }; + + if (loading) { + return ( +
+ +
+ ); + } + + return ( +
+ + + + {!tvMode && ( + + )} + + + + + + + {usersWithStatuses.map((userWithStatus, index) => ( + + {!tvMode && ( + + )} + + + + + ))} + +
+ + Name + + + Status + + + + Updated At
+ handleCheckboxChange(userWithStatus.user.id)} + /> + + {userWithStatus.user.full_name ?? 'Unknown User'} + + + setSelectedHistoryUserId(userWithStatus.user.id)} + > + {userWithStatus.status} + + {selectedHistoryUserId === userWithStatus.user.id && ( + + )} + + + {formatTime(userWithStatus.created_at)} +
+
+ ); + +}; diff --git a/src/components/status/Tech_Table.tsx b/src/components/status/Tech_Table.tsx deleted file mode 100644 index b4e6d25..0000000 --- a/src/components/status/Tech_Table.tsx +++ /dev/null @@ -1,333 +0,0 @@ -'use client'; -import { useState, useEffect, useCallback, useRef } from 'react'; -import { useAuth, useTVMode } from '@/components/context'; -import { - getUserStatuses, - updateUserStatus, - updateCurrentUserStatus, - type UserStatus, -} from '@/lib/hooks'; -import { - Drawer, - DrawerTrigger, - Progress, -} from '@/components/ui'; -import { toast } from 'sonner'; -import { HistoryDrawer } from '@/components/status'; -import { createClient } from '@/utils/supabase'; -import type { RealtimeChannel } from '@supabase/supabase-js'; - -export const TechTable = () => { - const { isAuthenticated, profile } = useAuth(); - const { tvMode } = useTVMode(); - - const [loading, setLoading] = useState(true); - const [selectedIds, setSelectedIds] = useState([]); - const [selectAll, setSelectAll] = useState(false); - const [statusInput, setStatusInput] = useState(''); - const [technicianData, setTechnicianData] = useState([]); - const [selectedUserId, setSelectedUserId] = useState(''); - const [recentlyUpdatedIds, setRecentlyUpdatedIds] = useState>(new Set()); - - const channelRef = useRef(null); - const supabase = createClient(); - - const fetchTechnicians = useCallback(async () => { - try { - const response = await getUserStatuses(); - if (!response.success) throw new Error(response.error); - return response.data; - } catch (error) { - const errorMessage = error instanceof Error ? error.message : String(error); - toast.error(`Error fetching technicians: ${errorMessage}`); - return []; - } - }, []); - - // Setup realtime broadcast subscription - const setupRealtimeSubscription = useCallback(() => { - console.log('Setting up realtime broadcast subscription'); - - const channel = supabase.channel('status_updates'); - - channel - .on('broadcast', { event: 'status_updated' }, (payload) => { - console.log('Status update received:', payload); - - const userStatus = payload.payload.user_status as UserStatus; - - // Update the technician data - setTechnicianData(prevData => { - const newData = [...prevData]; - const existingIndex = newData.findIndex(tech => tech.id === userStatus.id); - - if (existingIndex !== -1) { - // Update existing user if this status is more recent - if (new Date(userStatus.created_at) > new Date(newData[existingIndex].created_at)) { - newData[existingIndex] = userStatus; - - // Mark as recently updated - setRecentlyUpdatedIds(prev => { - const newSet = new Set(prev); - newSet.add(userStatus.id); - return newSet; - }); - - // Remove highlight after 3 seconds - setTimeout(() => { - setRecentlyUpdatedIds(current => { - const updatedSet = new Set(current); - updatedSet.delete(userStatus.id); - return updatedSet; - }); - }, 3000); - } - } else { - // Add new user - newData.push(userStatus); - - // Mark as recently updated - setRecentlyUpdatedIds(prev => { - const newSet = new Set(prev); - newSet.add(userStatus.id); - return newSet; - }); - - // Remove highlight after 3 seconds - setTimeout(() => { - setRecentlyUpdatedIds(current => { - const updatedSet = new Set(current); - updatedSet.delete(userStatus.id); - return updatedSet; - }); - }, 3000); - } - - // Sort by most recent - newData.sort((a, b) => - new Date(b.created_at).getTime() - new Date(a.created_at).getTime() - ); - - return newData; - }); - }) - .subscribe((status) => { - console.log('Realtime subscription status:', status); - }); - - channelRef.current = channel; - return channel; - }, [supabase]); - - const updateStatus = useCallback(async () => { - if (!isAuthenticated) { - toast.error('You must be signed in to update status.'); - return; - } - if (!statusInput.trim()) { - toast.error('Please enter a status.'); - return; - } - - try { - if (selectedIds.length === 0) { - // Update current user - find them by profile match - let targetUserId = null; - if (profile?.full_name) { - const currentUserInTable = technicianData.find(tech => - tech.full_name === profile.full_name || tech.id === profile.id - ); - targetUserId = currentUserInTable?.id; - } - - if (targetUserId) { - const result = await updateUserStatus([targetUserId], statusInput); - if (!result.success) throw new Error(result.error); - toast.success('Your status has been updated.'); - } else { - const result = await updateCurrentUserStatus(statusInput); - if (!result.success) throw new Error(result.error); - toast.success('Your status has been updated.'); - } - } else { - const result = await updateUserStatus(selectedIds, statusInput); - if (!result.success) throw new Error(result.error); - toast.success(`Status updated for ${selectedIds.length} technician${selectedIds.length > 1 ? 's' : ''}.`); - } - - setSelectedIds([]); - setStatusInput(''); - // No need to manually fetch - broadcast will handle updates - } catch (error) { - const errorMessage = error instanceof Error ? error.message : String(error); - toast.error(`Failed to update status: ${errorMessage}`); - } - }, [isAuthenticated, statusInput, selectedIds, technicianData, profile]); - - const handleCheckboxChange = (id: string) => { - setSelectedIds(prev => - prev.includes(id) ? prev.filter(prevId => prevId !== id) : [...prev, id] - ); - }; - - const handleSelectAllChange = () => { - if (selectAll) { - setSelectedIds([]); - } else { - setSelectedIds(technicianData.map(tech => tech.id)); - } - setSelectAll(!selectAll); - }; - - const formatTime = (timestamp: string) => { - const date = new Date(timestamp); - const time = date.toLocaleTimeString('en-US', { - hour: 'numeric', - minute: 'numeric', - }); - const day = date.getDate(); - const month = date.toLocaleString('default', { month: 'long' }); - return `${time} - ${month} ${day}`; - }; - - // Initial load and setup realtime subscription - useEffect(() => { - const loadData = async () => { - const data = await fetchTechnicians(); - setTechnicianData(data); - setLoading(false); - }; - - void loadData(); - - // Setup realtime subscription - const channel = setupRealtimeSubscription(); - - // Cleanup function - return () => { - if (channel) { - console.log('Removing broadcast channel'); - void supabase.removeChannel(channel); - channelRef.current = null; - } - }; - }, [fetchTechnicians, setupRealtimeSubscription, supabase]); - - // Update select all state - useEffect(() => { - setSelectAll( - selectedIds.length === technicianData.length && - technicianData.length > 0 - ); - }, [selectedIds.length, technicianData.length]); - - if (loading) { - return ( -
- -
- ); - } - - return ( -
- - - - {!tvMode && ( - - )} - - - - - - - {technicianData.map((technician, index) => ( - - {!tvMode && ( - - )} - - - - - ))} - -
- - Name - - - Status - - - - Updated At
- handleCheckboxChange(technician.id)} - /> - - {technician.full_name ?? 'Unknown User'} - - - setSelectedUserId(technician.id)} - > - {technician.status} - - {selectedUserId === technician.id && ( - - )} - - - {formatTime(technician.created_at)} -
- - {!tvMode && ( -
- setStatusInput(e.target.value)} - onKeyDown={(e) => { - if (e.key === 'Enter') { - void updateStatus(); - } - }} - /> - -
- )} -
- ); -}; diff --git a/src/lib/actions/_status.ts b/src/lib/actions/_status.ts deleted file mode 100644 index 48e70e6..0000000 --- a/src/lib/actions/_status.ts +++ /dev/null @@ -1,142 +0,0 @@ -'use server'; - -import 'server-only'; -import { createServerClient } from '@/utils/supabase'; -import type { Result } from '.'; -import type { Profile } from '@/utils/supabase'; - -export type UserStatus = Profile & { - status: string; - created_at: string; - updated_by?: Profile; -} - -export const getRecentUsers = async (): Promise> => { - try { - const supabase = await createServerClient(); - - // Get users who have had status updates in the last week - const oneWeekAgo = new Date(); - oneWeekAgo.setDate(oneWeekAgo.getDate() - 7); - - const { data, error } = await supabase - .from('statuses') - .select('user_id') - .gte('created_at', oneWeekAgo.toISOString()) - .order('created_at', { ascending: false }); - - if (error) throw error; - - // Get unique user IDs - const uniqueUserIds = [...new Set(data.map(status => status.user_id))]; - - return { success: true, data: uniqueUserIds }; - } catch (error) { - return { - success: false, - error: - error instanceof Error - ? error.message - : 'Unknown error getting recent users', - }; - } -}; - -export const getUserStatuses = async (): Promise> => { - try { - const supabase = await createServerClient(); - - // First get the recent users - const recentUsersResult = await getRecentUsers(); - if (!recentUsersResult.success) { - throw new Error(recentUsersResult.error); - } - - const userIds = recentUsersResult.data; - if (userIds.length === 0) { - return { success: true, data: [] }; - } - - // Get the most recent status for each user - const { data: statusData, error: statusError } = await supabase - .from('statuses') - .select('user_id, status, created_at, updated_by_id') - .in('user_id', userIds) - .order('created_at', { ascending: false }); - - if (statusError) throw statusError; - - if (!statusData) { - return { success: true, data: [] }; - } - - // Group by user_id and get the most recent status for each user - const userStatusMap = new Map(); - - statusData.forEach(status => { - if (!userStatusMap.has(status.user_id)) { - userStatusMap.set(status.user_id, status); - } - }); - - // Get all unique user IDs from the status data - const statusUserIds = Array.from(userStatusMap.keys()); - - // Get profile information for these users - const { data: profileData, error: profileError } = await supabase - .from('profiles') - .select('id, full_name, email, avatar_url, provider, updated_at') - .in('id', statusUserIds); - - if (profileError) throw profileError; - - // Get updated_by profile information - const updatedByIds = Array.from(userStatusMap.values()) - .map(status => status.updated_by_id) - .filter((id): id is string => id !== null); - - const { data: updatedByData, error: updatedByError } = await supabase - .from('profiles') - .select('id, full_name, email, avatar_url, provider, updated_at') - .in('id', updatedByIds); - - if (updatedByError) throw updatedByError; - - // Create maps for easy lookup - const profileMap = new Map(profileData?.map(profile => [profile.id, profile]) ?? []); - const updatedByMap = new Map(updatedByData?.map(profile => [profile.id, profile]) ?? []); - - // Transform the data to match UserStatus type - const userStatuses: UserStatus[] = []; - - for (const status of userStatusMap.values()) { - const profile = profileMap.get(status.user_id); - const updatedBy = status.updated_by_id ? updatedByMap.get(status.updated_by_id) : undefined; - - if (!profile) continue; // Skip if no profile found - - userStatuses.push({ - // Profile fields - id: profile.id, - full_name: profile.full_name, - email: profile.email, - avatar_url: profile.avatar_url, - provider: profile.provider, - updated_at: profile.updated_at, - // Status fields - status: status.status, - created_at: status.created_at, - updated_by: updatedBy, - }); - } - return { success: true, data: userStatuses }; - } catch (error) { - return { - success: false, - error: - error instanceof Error - ? error.message - : 'Unknown error getting user statuses', - }; - } -}; diff --git a/src/lib/actions/status.ts b/src/lib/actions/status.ts index 9ad2ebf..12c6de4 100644 --- a/src/lib/actions/status.ts +++ b/src/lib/actions/status.ts @@ -1,21 +1,18 @@ -'use server' - -import 'server-only'; -import { - createServerClient, - type Profile, - type Result, - type Status, -} from '@/utils/supabase'; +'use server'; +import { createServerClient } from '@/utils/supabase'; +import type { Profile, Result, Status } from '@/utils/supabase'; +import { getUser, getProfile } from '@/lib/hooks' type UserWithStatus = { user: Profile; status: string; created_at: string; - updated_by: Profile; + updated_by?: Profile; }; -type PaginatedHistory = UserWithStatus[] & { +type PaginatedHistory = { + profile?: Profile; + statuses: Status[]; meta: { current_page: number; per_page: number; @@ -24,15 +21,14 @@ type PaginatedHistory = UserWithStatus[] & { }; }; -export const getUsersWithStatuses = async () => { +export const getRecentUsersWithStatuses = async (): Promise< + Result +> => { try { const supabase = await createServerClient(); + const oneDayAgo = new Date(Date.now() - 1000 * 60 * 60 * 24); - // Get only users with recent statuses (Past 7 days) - const oneWeekAgo = new Date(); - oneWeekAgo.setDate(oneWeekAgo.getDate() - 7); - - const { data: recentStatuses, error } = await supabase + const { data, error } = await supabase .from('statuses') .select(` user:profiles!user_id(*), @@ -40,14 +36,245 @@ export const getUsersWithStatuses = async () => { created_at, updated_by:profiles!updated_by_id(*) `) - .gte('created_at', oneWeekAgo.toISOString()) - .order('created_at', { ascending: false }); + .gte('created_at', oneDayAgo.toISOString()) + .order('created_at', { ascending: false }) as { data: UserWithStatus[], error: unknown }; - if (error) throw error; - if (!recentStatuses.length) return { success: true, data: []}; + if (error) throw error as Error; + if (!data?.length) return { success: true, data: [] }; - return { success: true, data: recentStatuses }; + // 3️⃣ client-side dedupe: keep the first status you see per user + const seen = new Set(); + const filtered = data.filter((row) => { + if (seen.has(row.user.id)) return false; + seen.add(row.user.id); + return true; + }); + + return { success: true, data: filtered }; } catch (error) { - return { success: false, error: `Error: ${error as string}` }; + return { success: false, error: `Error: ${error as Error}` }; + } +}; + +export const broadcastStatusUpdates = async ( + userStatuses: UserWithStatus[], +): Promise> => { + try { + const supabase = await createServerClient(); + + for (const userStatus of userStatuses) { + const broadcast = await supabase.channel('status_updates').send({ + type: 'broadcast', + event: 'status_updated', + payload: { + user_status: userStatus, + timestamp: new Date().toISOString(), + } + }); + if (broadcast === 'error' || broadcast === 'timed out') + throw new Error('Failed to broadcast status update. Timed out or errored.'); + } + return { success: true, data: undefined }; + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : 'Unknown error', + }; + } +}; + +export const updateStatuses = async ( + userIds: string[], + status: string, +): Promise> => { + try { + const supabase = await createServerClient(); + const userResponse = await getUser(); + if (!userResponse.success) throw new Error('Not authenticated!'); + const profileResponse = await getProfile(); + if (!profileResponse.success) throw new Error(profileResponse.error); + const user = userResponse.data; + const userProfile = profileResponse.data; + + const inserts = userIds.map(usersId => ({ + user_id: usersId, + status, + updated_by_id: user.id, + })); + + const { data: insertedStatuses, error: insertedStatusesError } = await supabase + .from('statuses') + .insert(inserts) + .select(); + if (insertedStatusesError) throw insertedStatusesError as Error; + + if (insertedStatuses) { + const broadcastArray = new Array(insertedStatuses.length); + for (const insertedStatus of insertedStatuses) { + const { data: profile, error: profileError } = await supabase + .from('profiles') + .select('*') + .eq('id', insertedStatus.user_id) + .single(); + if (profileError) throw profileError as Error; + + if (profile) { + broadcastArray.push({ + user: profile, + status: insertedStatus.status, + created_at: insertedStatus.created_at, + updated_by: userProfile, + }); + } + } + await broadcastStatusUpdates(broadcastArray); + } + return { success: true, data: undefined }; + + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : 'Unknown error', + }; + } +}; + +export const updateUserStatus = async (status: string): Promise> => { + try { + const supabase = await createServerClient(); + const userResponse = await getUser(); + if (!userResponse.success) throw new Error(`Not authenticated! ${userResponse.error}`); + const profileResponse = await getProfile(); + if (!profileResponse.success) throw new Error(`Could not get profile! ${profileResponse.error}`); + const user = userResponse.data; + const userProfile = profileResponse.data; + + const { data: insertedStatus, error: insertedStatusError } = await supabase + .from('statuses') + .insert({ + user_id: user.id, + status, + updated_by_id: user.id, + }) + .select() + .single(); + if (insertedStatusError) throw insertedStatusError as Error; + + const userStatus: UserWithStatus = { + user: userProfile, + status: insertedStatus.status, + created_at: insertedStatus.created_at, + }; + + await broadcastStatusUpdates([userStatus]); + return { success: true, data: undefined }; + + } catch (error) { + return { + success: false, + error: `Error updating user's status: ${error as Error}`, + } + } +}; + +export const getUserHistory = async ( + userId: string, + page = 1, + perPage = 50, +): Promise> => { + try { + const supabase = await createServerClient(); + const userResponse = await getUser(); + if (!userResponse.success) throw new Error(`Not authenticated! ${userResponse.error}`); + + const offset = (page - 1) * perPage; + const { count } = await supabase + .from('statuses') + .select('*', { count: 'exact', head: true }) + .eq('user_id', userId); + + const { data: statuses, error: statusesError } = await supabase + .from('statuses') + .select('*') + .eq('user_id', userId) + .order('created_at', { ascending: false }) + .range(offset, offset + perPage - 1) as {data: Status[], error: unknown}; + if (statusesError) throw statusesError as Error; + + const { data: profile, error: profileError } = await supabase + .from('profiles') + .select('*') + .eq('id', userId) + .single() as { data: Profile, error: unknown }; + if (profileError) throw profileError as Error; + if (!profile) throw new Error('User profile not found!'); + + const totalCount = count ?? 0; + const totalPages = Math.ceil(totalCount / perPage); + + return { + success: true, + data: { + profile, + statuses, + meta: { + current_page: page, + per_page: perPage, + total_pages: totalPages, + total_count: totalCount, + }, + }, + }; + + } catch (error) { + return { + success: false, + error: `Error getting user's history: ${error as Error}`, + }; + } +}; + +export const getAllHistory = async ( + page = 1, + perPage = 50, +): Promise> => { + try { + const supabase = await createServerClient(); + const userResponse = await getUser(); + if (!userResponse.success) throw new Error(`Not authenticated! ${userResponse.error}`); + + const offset = (page - 1) * perPage; + const { count } = await supabase + .from('statuses') + .select('*', { count: 'exact', head: true }); + + const { data: statuses, error: statusesError } = await supabase + .from('statuses') + .select('*') + .order('created_at', { ascending: false }) + .range(offset, offset + perPage - 1) as {data: Status[], error: unknown}; + if (statusesError) throw statusesError as Error; + + const totalCount = count ?? 0; + const totalPages = Math.ceil(totalCount / perPage); + + return { + success: true, + data: { + statuses, + meta: { + current_page: page, + per_page: perPage, + total_pages: totalPages, + total_count: totalCount, + }, + }, + }; + + } catch (error) { + return { + success: false, + error: `Error getting all history: ${error as Error}`, + }; } }; diff --git a/src/lib/hooks/_status.ts b/src/lib/hooks/_status.ts deleted file mode 100644 index 31a144e..0000000 --- a/src/lib/hooks/_status.ts +++ /dev/null @@ -1,401 +0,0 @@ -'use client'; -import { createClient } from '@/utils/supabase'; -import type { Result } from '.'; -import type { Profile, Status } from '@/utils/supabase'; - -export type UserStatus = Profile & { - status: string; - created_at: string; - updated_by?: Profile; -}; - -export type HistoryEntry = { - id: string; - status: string; - created_at: string; - updated_by?: Profile; - user_profile: Profile; -}; - -export type PaginatedHistory = { - data: HistoryEntry[]; - meta: { - current_page: number; - per_page: number; - total_pages: number; - total_count: number; - }; -}; - -export const getUserStatuses = async (): Promise> => { - try { - const supabase = createClient(); - - // Get users with recent activity (last 7 days) - const oneWeekAgo = new Date(); - oneWeekAgo.setDate(oneWeekAgo.getDate() - 7); - - const { data: recentStatuses, error: statusError } = await supabase - .from('statuses') - .select('*') - .gte('created_at', oneWeekAgo.toISOString()) - .order('created_at', { ascending: false }); - - if (statusError) throw statusError; - if (!recentStatuses?.length) return { success: true, data: [] }; - - // Properly type the status data - const typedStatuses: Status[] = recentStatuses; - - // Get most recent status per user - const userStatusMap = new Map(); - typedStatuses.forEach(status => { - if (!userStatusMap.has(status.user_id)) { - userStatusMap.set(status.user_id, status); - } - }); - - const userIds = Array.from(userStatusMap.keys()); - - // Get profiles - const { data: profiles, error: profileError } = await supabase - .from('profiles') - .select('*') - .in('id', userIds); - - if (profileError) throw profileError; - - // Get updated_by profiles - filter out nulls properly - const updatedByIds = Array.from(userStatusMap.values()) - .map(s => s.updated_by_id) - .filter((id): id is string => id !== null); - - let updatedByProfiles: Profile[] = []; - if (updatedByIds.length > 0) { - const { data, error } = await supabase - .from('profiles') - .select('*') - .in('id', updatedByIds); - - if (error) throw error; - updatedByProfiles = data ?? []; - } - - const profileMap = new Map((profiles ?? []).map(p => [p.id, p])); - const updatedByMap = new Map(updatedByProfiles.map(p => [p.id, p])); - - const userStatuses: UserStatus[] = []; - - for (const [userId, status] of userStatusMap) { - const profile = profileMap.get(userId); - if (!profile) continue; - - userStatuses.push({ - ...profile, - status: status.status, - created_at: status.created_at, - updated_by: status.updated_by_id ? updatedByMap.get(status.updated_by_id) : undefined, - }); - } - - return { success: true, data: userStatuses }; - } catch (error) { - return { - success: false, - error: error instanceof Error ? error.message : 'Unknown error', - }; - } -}; - -export const broadcastStatusUpdate = async ( - userStatus: UserStatus -): Promise> => { - try { - const supabase = createClient(); - - const broadcast = await supabase.channel('status_updates').send({ - type: 'broadcast', - event: 'status_updated', - payload: { - user_status: userStatus, - timestamp: new Date().toISOString(), - } - }); - - if (broadcast === 'error') throw new Error('Failed to broadcast status update'); - if (broadcast === 'ok') return { success: true, data: undefined }; - else throw new Error('Broadcast timed out!') - } catch (error) { - return { - success: false, - error: error instanceof Error ? error.message : 'Unknown error', - }; - } -}; - -// Update your existing functions to broadcast after database updates -export const updateUserStatus = async ( - userIds: string[], - status: string, -): Promise> => { - try { - const supabase = createClient(); - const { data: { user } } = await supabase.auth.getUser(); - - if (!user) throw new Error('Not authenticated'); - - const inserts = userIds.map(userId => ({ - user_id: userId, - status, - updated_by_id: user.id, - })); - - const { data: insertedStatuses, error } = await supabase - .from('statuses') - .insert(inserts) - .select(); - - if (error) throw error; - - // Broadcast the updates - if (insertedStatuses) { - for (const insertedStatus of insertedStatuses) { - // Get the user profile for broadcasting - const { data: profile } = await supabase - .from('profiles') - .select('*') - .eq('id', insertedStatus.user_id) - .single(); - - if (profile) { - const userStatus: UserStatus = { - ...profile, - status: insertedStatus.status, - created_at: insertedStatus.created_at, - }; - - await broadcastStatusUpdate(userStatus); - } - } - } - - return { success: true, data: undefined }; - } catch (error) { - return { - success: false, - error: error instanceof Error ? error.message : 'Unknown error', - }; - } -}; - -export const updateCurrentUserStatus = async ( - status: string, -): Promise> => { - try { - const supabase = createClient(); - const { data: { user } } = await supabase.auth.getUser(); - - if (!user) throw new Error('Not authenticated'); - - const { data: insertedStatus, error } = await supabase - .from('statuses') - .insert({ - user_id: user.id, - status, - updated_by_id: user.id, - }) - .select() - .single(); - - if (error) throw error; - - // Get the user profile for broadcasting - const { data: profile } = await supabase - .from('profiles') - .select('*') - .eq('id', user.id) - .single(); - - if (profile && insertedStatus) { - const userStatus: UserStatus = { - ...profile, - status: insertedStatus.status, - created_at: insertedStatus.created_at, - }; - - await broadcastStatusUpdate(userStatus); - } - - return { success: true, data: undefined }; - } catch (error) { - return { - success: false, - error: error instanceof Error ? error.message : 'Unknown error', - }; - } -}; - -export const getUserHistory = async ( - userId: string, - page = 1, - perPage = 50, -): Promise> => { - try { - const supabase = createClient(); - const offset = (page - 1) * perPage; - - // Get count - const { count } = await supabase - .from('statuses') - .select('*', { count: 'exact', head: true }) - .eq('user_id', userId); - - // Get data - const { data: statuses, error } = await supabase - .from('statuses') - .select('*') - .eq('user_id', userId) - .order('created_at', { ascending: false }) - .range(offset, offset + perPage - 1); - - if (error) throw error; - - const typedStatuses: Status[] = statuses ?? []; - - // Get user profile - const { data: userProfile, error: userProfileError } = await supabase - .from('profiles') - .select('*') - .eq('id', userId) - .single(); - - if (userProfileError) throw userProfileError; - if (!userProfile) throw new Error('User profile not found'); - - // Get updated_by profiles - filter out nulls properly - const updatedByIds = typedStatuses - .map(s => s.updated_by_id) - .filter((id): id is string => id !== null); - - let updatedByProfiles: Profile[] = []; - if (updatedByIds.length > 0) { - const { data, error: updatedByError } = await supabase - .from('profiles') - .select('*') - .in('id', updatedByIds); - - if (updatedByError) throw updatedByError; - updatedByProfiles = data ?? []; - } - - const updatedByMap = new Map(updatedByProfiles.map(p => [p.id, p])); - - const historyEntries: HistoryEntry[] = typedStatuses.map(entry => ({ - id: entry.id, - status: entry.status, - created_at: entry.created_at, - updated_by: entry.updated_by_id ? updatedByMap.get(entry.updated_by_id) : undefined, - user_profile: userProfile, - })); - - const totalCount = count ?? 0; - const totalPages = Math.ceil(totalCount / perPage); - - return { - success: true, - data: { - data: historyEntries, - meta: { - current_page: page, - per_page: perPage, - total_pages: totalPages, - total_count: totalCount - }, - }, - }; - } catch (error) { - return { - success: false, - error: error instanceof Error ? error.message : 'Unknown error', - }; - } -}; - -export const getAllHistory = async ( - page = 1, - perPage = 50, -): Promise> => { - try { - const supabase = createClient(); - const offset = (page - 1) * perPage; - - // Get count - const { count } = await supabase - .from('statuses') - .select('*', { count: 'exact', head: true }); - - // Get data - const { data: statuses, error } = await supabase - .from('statuses') - .select('*') - .order('created_at', { ascending: false }) - .range(offset, offset + perPage - 1); - - if (error) throw error; - - const typedStatuses: Status[] = statuses ?? []; - - // Get all profiles - filter out nulls properly - const userIds = [...new Set(typedStatuses.map(s => s.user_id))]; - const updatedByIds = typedStatuses - .map(s => s.updated_by_id) - .filter((id): id is string => id !== null); - const allIds = [...new Set([...userIds, ...updatedByIds])]; - - const { data: profiles, error: profileError } = await supabase - .from('profiles') - .select('*') - .in('id', allIds); - - if (profileError) throw profileError; - - const profileMap = new Map((profiles ?? []).map(p => [p.id, p])); - - const historyEntries: HistoryEntry[] = typedStatuses.map(entry => { - const userProfile = profileMap.get(entry.user_id); - if (!userProfile) { - throw new Error(`User profile not found for ID: ${entry.user_id}`); - } - - return { - id: entry.id, - status: entry.status, - created_at: entry.created_at, - updated_by: entry.updated_by_id ? profileMap.get(entry.updated_by_id) : undefined, - user_profile: userProfile, - }; - }); - - const totalCount = count ?? 0; - const totalPages = Math.ceil(totalCount / perPage); - - return { - success: true, - data: { - data: historyEntries, - meta: { - current_page: page, - per_page: perPage, - total_pages: totalPages, - total_count: totalCount - }, - }, - }; - } catch (error) { - return { - success: false, - error: error instanceof Error ? error.message : 'Unknown error', - }; - } -}; - diff --git a/src/lib/hooks/auth.ts b/src/lib/hooks/auth.ts index c381904..dab43f3 100644 --- a/src/lib/hooks/auth.ts +++ b/src/lib/hooks/auth.ts @@ -146,6 +146,6 @@ export const getUser = async (): Promise> => { if (error) throw error; return { success: true, data: data.user }; } catch (error) { - return { success: false, error: 'Could not get user!' }; + return { success: false, error: `Could not get user! Error: ${error as Error}` }; } }; diff --git a/src/lib/hooks/status.ts b/src/lib/hooks/status.ts index 76b4988..dd149c5 100644 --- a/src/lib/hooks/status.ts +++ b/src/lib/hooks/status.ts @@ -1,20 +1,18 @@ 'use client'; +import { createClient } from '@/utils/supabase'; +import type { Profile, Result, Status } from '@/utils/supabase'; +import { getUser, getProfile } from '@/lib/hooks' -import { - createClient, - type Profile, - type Result, - type Status, -} from '@/utils/supabase'; - -type UserWithStatus = { +export type UserWithStatus = { user: Profile; status: string; created_at: string; - updated_by: Profile; + updated_by?: Profile; }; -type PaginatedHistory = UserWithStatus[] & { +type PaginatedHistory = { + profile?: Profile; + statuses: Status[]; meta: { current_page: number; per_page: number; @@ -23,15 +21,14 @@ type PaginatedHistory = UserWithStatus[] & { }; }; -export const getUsersWithStatuses = async (): Promise> => { +export const getRecentUsersWithStatuses = async (): Promise< + Result +> => { try { const supabase = createClient(); + const oneDayAgo = new Date(Date.now() - 1000 * 60 * 60 * 24); - // Get only users with recent statuses (Past 7 days) - const oneWeekAgo = new Date(); - oneWeekAgo.setDate(oneWeekAgo.getDate() - 7); - - const { data: recentStatuses, error } = await supabase + const { data, error } = await supabase .from('statuses') .select(` user:profiles!user_id(*), @@ -39,14 +36,246 @@ export const getUsersWithStatuses = async (): Promise> created_at, updated_by:profiles!updated_by_id(*) `) - .gte('created_at', oneWeekAgo.toISOString()) - .order('created_at', { ascending: false }) as {data: UserWithStatus[], error: unknown}; + .gte('created_at', oneDayAgo.toISOString()) + .order('created_at', { ascending: false }) as { data: UserWithStatus[], error: unknown }; - if (error) throw error; - if (!recentStatuses.length) return { success: true, data: []}; + if (error) throw error as Error; + if (!data?.length) return { success: true, data: [] }; - return { success: true, data: recentStatuses }; + // 3️⃣ client-side dedupe: keep the first status you see per user + const seen = new Set(); + const filtered = data.filter((row) => { + if (seen.has(row.user.id)) return false; + seen.add(row.user.id); + return true; + }); + + return { success: true, data: filtered }; } catch (error) { - return { success: false, error: `Error: ${error as string}` }; + return { success: false, error: `Error: ${error as Error}` }; + } +}; + +export const broadcastStatusUpdates = async ( + userStatuses: UserWithStatus[], +): Promise> => { + try { + const supabase = createClient(); + + for (const userStatus of userStatuses) { + const broadcast = await supabase.channel('status_updates').send({ + type: 'broadcast', + event: 'status_updated', + payload: { + user_status: userStatus, + timestamp: new Date().toISOString(), + } + }); + if (broadcast === 'error' || broadcast === 'timed out') + throw new Error('Failed to broadcast status update. Timed out or errored.'); + } + return { success: true, data: undefined }; + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : 'Unknown error', + }; + } +}; + +export const updateStatuses = async ( + userIds: string[], + status: string, +): Promise> => { + try { + const supabase = createClient(); + const userResponse = await getUser(); + if (!userResponse.success) throw new Error('Not authenticated!'); + const profileResponse = await getProfile(); + if (!profileResponse.success) throw new Error(profileResponse.error); + const user = userResponse.data; + const userProfile = profileResponse.data; + + const inserts = userIds.map(usersId => ({ + user_id: usersId, + status, + updated_by_id: user.id, + })); + + const { data: insertedStatuses, error: insertedStatusesError } = await supabase + .from('statuses') + .insert(inserts) + .select(); + if (insertedStatusesError) throw insertedStatusesError as Error; + + if (insertedStatuses) { + const broadcastArray = new Array(insertedStatuses.length); + for (const insertedStatus of insertedStatuses) { + const { data: profile, error: profileError } = await supabase + .from('profiles') + .select('*') + .eq('id', insertedStatus.user_id) + .single(); + if (profileError) throw profileError as Error; + + if (profile) { + broadcastArray.push({ + user: profile, + status: insertedStatus.status, + created_at: insertedStatus.created_at, + updated_by: userProfile, + }); + } + } + await broadcastStatusUpdates(broadcastArray); + } + return { success: true, data: undefined }; + + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : 'Unknown error', + }; + } +}; + +export const updateUserStatus = async (status: string): Promise> => { + try { + const supabase = createClient(); + const userResponse = await getUser(); + if (!userResponse.success) throw new Error(`Not authenticated! ${userResponse.error}`); + const profileResponse = await getProfile(); + if (!profileResponse.success) throw new Error(`Could not get profile! ${profileResponse.error}`); + const user = userResponse.data; + const userProfile = profileResponse.data; + + const { data: insertedStatus, error: insertedStatusError } = await supabase + .from('statuses') + .insert({ + user_id: user.id, + status, + updated_by_id: user.id, + }) + .select() + .single(); + if (insertedStatusError) throw insertedStatusError as Error; + + const userStatus: UserWithStatus = { + user: userProfile, + status: insertedStatus.status, + created_at: insertedStatus.created_at, + }; + + await broadcastStatusUpdates([userStatus]); + return { success: true, data: undefined }; + + } catch (error) { + return { + success: false, + error: `Error updating user's status: ${error as Error}`, + } + } +}; + +export const getUserHistory = async ( + userId: string, + page = 1, + perPage = 50, +): Promise> => { + try { + const supabase = createClient(); + const userResponse = await getUser(); + if (!userResponse.success) throw new Error(`Not authenticated! ${userResponse.error}`); + + const offset = (page - 1) * perPage; + const { count } = await supabase + .from('statuses') + .select('*', { count: 'exact', head: true }) + .eq('user_id', userId); + + const { data: statuses, error: statusesError } = await supabase + .from('statuses') + .select('*') + .eq('user_id', userId) + .order('created_at', { ascending: false }) + .range(offset, offset + perPage - 1) as {data: Status[], error: unknown}; + if (statusesError) throw statusesError as Error; + + const { data: profile, error: profileError } = await supabase + .from('profiles') + .select('*') + .eq('id', userId) + .single() as { data: Profile, error: unknown }; + if (profileError) throw profileError as Error; + if (!profile) throw new Error('User profile not found!'); + + const totalCount = count ?? 0; + const totalPages = Math.ceil(totalCount / perPage); + + return { + success: true, + data: { + profile, + statuses, + meta: { + current_page: page, + per_page: perPage, + total_pages: totalPages, + total_count: totalCount, + }, + }, + }; + + } catch (error) { + return { + success: false, + error: `Error getting user's history: ${error as Error}`, + }; + } +}; + +export const getAllHistory = async ( + page = 1, + perPage = 50, +): Promise> => { + try { + const supabase = createClient(); + const userResponse = await getUser(); + if (!userResponse.success) throw new Error(`Not authenticated! ${userResponse.error}`); + + const offset = (page - 1) * perPage; + const { count } = await supabase + .from('statuses') + .select('*', { count: 'exact', head: true }); + + const { data: statuses, error: statusesError } = await supabase + .from('statuses') + .select('*') + .order('created_at', { ascending: false }) + .range(offset, offset + perPage - 1) as {data: Status[], error: unknown}; + if (statusesError) throw statusesError as Error; + + const totalCount = count ?? 0; + const totalPages = Math.ceil(totalCount / perPage); + + return { + success: true, + data: { + statuses, + meta: { + current_page: page, + per_page: perPage, + total_pages: totalPages, + total_count: totalCount, + }, + }, + }; + + + } catch (error) { + return { + success: false, + error: `Error getting all history: ${error as Error}`, + } } };