Complete rewrite of all of the hooks and actions for statuses as well as for the Tech Table. Need to rewrite history component & then I will be happy & ready to continue
This commit is contained in:
@ -1,15 +1,18 @@
|
|||||||
'use client';
|
'use server';
|
||||||
|
|
||||||
import { TechTable } from '@/components/status';
|
import { TechTable } from '@/components/status';
|
||||||
import { useAuth } from '@/components/context';
|
import { getUser, getRecentUsersWithStatuses } from '@/lib/actions';
|
||||||
import { redirect } from 'next/navigation';
|
import { redirect } from 'next/navigation';
|
||||||
|
|
||||||
const Status = () => {
|
const Status = async () => {
|
||||||
const { isAuthenticated } = useAuth();
|
const userRespoonse = await getUser();
|
||||||
if (!isAuthenticated) {
|
if (!userRespoonse.success) {
|
||||||
redirect('/sign-in');
|
redirect('/sign-in');
|
||||||
} else {
|
} else {
|
||||||
return <TechTable />;
|
const response = await getRecentUsersWithStatuses();
|
||||||
|
if (!response.success) throw new Error(response.error);
|
||||||
|
const usersWithStatuses = response.data;
|
||||||
|
return <TechTable initialStatuses={usersWithStatuses} />;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
export default Status;
|
export default Status;
|
||||||
|
207
src/components/status/TechTable.tsx
Normal file
207
src/components/status/TechTable.tsx
Normal file
@ -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<string[]>([]);
|
||||||
|
const [selectAll, setSelectAll] = useState(false);
|
||||||
|
const [statusInput, setStatusInput] = useState('');
|
||||||
|
const [usersWithStatuses, setUsersWithStatuses] = useState<UserWithStatus[]>(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 (
|
||||||
|
<div className='flex justify-center items-center min-h-[400px]'>
|
||||||
|
<Progress value={33} className='w-64' />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={className}>
|
||||||
|
<table className={`w-full text-center border-collapse ${tvMode ? 'text-4xl lg:text-5xl' : 'text-base lg:text-lg'}`}>
|
||||||
|
<thead>
|
||||||
|
<tr className='bg-muted'>
|
||||||
|
{!tvMode && (
|
||||||
|
<th className='py-3 px-3 border'>
|
||||||
|
<input
|
||||||
|
type='checkbox'
|
||||||
|
className='scale-125 cursor-pointer'
|
||||||
|
checked={selectAll}
|
||||||
|
onChange={handleSelectAllChange}
|
||||||
|
/>
|
||||||
|
</th>
|
||||||
|
)}
|
||||||
|
<th className='py-3 px-4 border font-semibold'>Name</th>
|
||||||
|
<th className='py-3 px-4 border font-semibold'>
|
||||||
|
<Drawer>
|
||||||
|
<DrawerTrigger className='hover:underline'>
|
||||||
|
Status
|
||||||
|
</DrawerTrigger>
|
||||||
|
<HistoryDrawer user_id='' />
|
||||||
|
</Drawer>
|
||||||
|
</th>
|
||||||
|
<th className='py-3 px-4 border font-semibold'>Updated At</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{usersWithStatuses.map((userWithStatus, index) => (
|
||||||
|
<tr
|
||||||
|
key={userWithStatus.user.id}
|
||||||
|
className={`
|
||||||
|
${index % 2 === 0 ? 'bg-muted/50' : 'bg-background'}
|
||||||
|
hover:bg-muted/75 transition-all duration-300
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
{!tvMode && (
|
||||||
|
<td className='py-2 px-3 border'>
|
||||||
|
<input
|
||||||
|
type='checkbox'
|
||||||
|
className='scale-125 cursor-pointer'
|
||||||
|
checked={selectedIds.includes(userWithStatus.user.id)}
|
||||||
|
onChange={() => handleCheckboxChange(userWithStatus.user.id)}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
)}
|
||||||
|
<td className='py-3 px-4 border font-medium'>
|
||||||
|
{userWithStatus.user.full_name ?? 'Unknown User'}
|
||||||
|
</td>
|
||||||
|
<td className='py-3 px-4 border'>
|
||||||
|
<Drawer>
|
||||||
|
<DrawerTrigger
|
||||||
|
className='text-left w-full p-2 rounded hover:bg-muted transition-colors'
|
||||||
|
onClick={() => setSelectedHistoryUserId(userWithStatus.user.id)}
|
||||||
|
>
|
||||||
|
{userWithStatus.status}
|
||||||
|
</DrawerTrigger>
|
||||||
|
{selectedHistoryUserId === userWithStatus.user.id && (
|
||||||
|
<HistoryDrawer
|
||||||
|
key={selectedHistoryUserId}
|
||||||
|
user_id={selectedHistoryUserId}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Drawer>
|
||||||
|
</td>
|
||||||
|
<td className='py-3 px-4 border text-muted-foreground'>
|
||||||
|
{formatTime(userWithStatus.created_at)}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
};
|
@ -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<string[]>([]);
|
|
||||||
const [selectAll, setSelectAll] = useState(false);
|
|
||||||
const [statusInput, setStatusInput] = useState('');
|
|
||||||
const [technicianData, setTechnicianData] = useState<UserStatus[]>([]);
|
|
||||||
const [selectedUserId, setSelectedUserId] = useState('');
|
|
||||||
const [recentlyUpdatedIds, setRecentlyUpdatedIds] = useState<Set<string>>(new Set());
|
|
||||||
|
|
||||||
const channelRef = useRef<RealtimeChannel | null>(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 (
|
|
||||||
<div className='flex justify-center items-center min-h-[400px]'>
|
|
||||||
<Progress value={33} className='w-64' />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='w-full max-w-7xl mx-auto px-4'>
|
|
||||||
<table className={`w-full text-center border-collapse ${tvMode ? 'text-4xl lg:text-5xl' : 'text-base lg:text-lg'}`}>
|
|
||||||
<thead>
|
|
||||||
<tr className='bg-muted'>
|
|
||||||
{!tvMode && (
|
|
||||||
<th className='py-3 px-3 border'>
|
|
||||||
<input
|
|
||||||
type='checkbox'
|
|
||||||
className='scale-125 cursor-pointer'
|
|
||||||
checked={selectAll}
|
|
||||||
onChange={handleSelectAllChange}
|
|
||||||
/>
|
|
||||||
</th>
|
|
||||||
)}
|
|
||||||
<th className='py-3 px-4 border font-semibold'>Name</th>
|
|
||||||
<th className='py-3 px-4 border font-semibold'>
|
|
||||||
<Drawer>
|
|
||||||
<DrawerTrigger className='hover:underline'>
|
|
||||||
Status
|
|
||||||
</DrawerTrigger>
|
|
||||||
<HistoryDrawer user_id='' />
|
|
||||||
</Drawer>
|
|
||||||
</th>
|
|
||||||
<th className='py-3 px-4 border font-semibold'>Updated At</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{technicianData.map((technician, index) => (
|
|
||||||
<tr
|
|
||||||
key={technician.id}
|
|
||||||
className={`
|
|
||||||
${index % 2 === 0 ? 'bg-muted/50' : 'bg-background'}
|
|
||||||
hover:bg-muted/75 transition-all duration-300
|
|
||||||
${recentlyUpdatedIds.has(technician.id) ? 'ring-2 ring-blue-500 ring-opacity-75 bg-blue-50 dark:bg-blue-900/20' : ''}
|
|
||||||
`}
|
|
||||||
>
|
|
||||||
{!tvMode && (
|
|
||||||
<td className='py-2 px-3 border'>
|
|
||||||
<input
|
|
||||||
type='checkbox'
|
|
||||||
className='scale-125 cursor-pointer'
|
|
||||||
checked={selectedIds.includes(technician.id)}
|
|
||||||
onChange={() => handleCheckboxChange(technician.id)}
|
|
||||||
/>
|
|
||||||
</td>
|
|
||||||
)}
|
|
||||||
<td className='py-3 px-4 border font-medium'>
|
|
||||||
{technician.full_name ?? 'Unknown User'}
|
|
||||||
</td>
|
|
||||||
<td className='py-3 px-4 border'>
|
|
||||||
<Drawer>
|
|
||||||
<DrawerTrigger
|
|
||||||
className='text-left w-full p-2 rounded hover:bg-muted transition-colors'
|
|
||||||
onClick={() => setSelectedUserId(technician.id)}
|
|
||||||
>
|
|
||||||
{technician.status}
|
|
||||||
</DrawerTrigger>
|
|
||||||
{selectedUserId === technician.id && (
|
|
||||||
<HistoryDrawer
|
|
||||||
key={selectedUserId}
|
|
||||||
user_id={selectedUserId}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Drawer>
|
|
||||||
</td>
|
|
||||||
<td className='py-3 px-4 border text-muted-foreground'>
|
|
||||||
{formatTime(technician.created_at)}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
{!tvMode && (
|
|
||||||
<div className='mx-auto flex flex-row items-center justify-center py-5 gap-4'>
|
|
||||||
<input
|
|
||||||
autoFocus
|
|
||||||
type='text'
|
|
||||||
placeholder='New Status'
|
|
||||||
className='min-w-[120px] lg:min-w-[400px] py-2 px-3 rounded-xl border bg-background lg:text-2xl focus:outline-none focus:ring-2 focus:ring-primary'
|
|
||||||
value={statusInput}
|
|
||||||
onChange={(e) => setStatusInput(e.target.value)}
|
|
||||||
onKeyDown={(e) => {
|
|
||||||
if (e.key === 'Enter') {
|
|
||||||
void updateStatus();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
type='submit'
|
|
||||||
className='min-w-[100px] lg:min-w-[160px] py-2 px-4 rounded-xl text-center font-semibold lg:text-2xl bg-primary text-primary-foreground hover:bg-primary/90 transition-colors disabled:opacity-50 disabled:cursor-not-allowed'
|
|
||||||
onClick={() => void updateStatus()}
|
|
||||||
disabled={!statusInput.trim()}
|
|
||||||
>
|
|
||||||
Update
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
@ -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<Result<string[]>> => {
|
|
||||||
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<Result<UserStatus[]>> => {
|
|
||||||
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<string, typeof statusData[0]>();
|
|
||||||
|
|
||||||
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',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
@ -1,21 +1,18 @@
|
|||||||
'use server'
|
'use server';
|
||||||
|
import { createServerClient } from '@/utils/supabase';
|
||||||
import 'server-only';
|
import type { Profile, Result, Status } from '@/utils/supabase';
|
||||||
import {
|
import { getUser, getProfile } from '@/lib/hooks'
|
||||||
createServerClient,
|
|
||||||
type Profile,
|
|
||||||
type Result,
|
|
||||||
type Status,
|
|
||||||
} from '@/utils/supabase';
|
|
||||||
|
|
||||||
type UserWithStatus = {
|
type UserWithStatus = {
|
||||||
user: Profile;
|
user: Profile;
|
||||||
status: string;
|
status: string;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
updated_by: Profile;
|
updated_by?: Profile;
|
||||||
};
|
};
|
||||||
|
|
||||||
type PaginatedHistory = UserWithStatus[] & {
|
type PaginatedHistory = {
|
||||||
|
profile?: Profile;
|
||||||
|
statuses: Status[];
|
||||||
meta: {
|
meta: {
|
||||||
current_page: number;
|
current_page: number;
|
||||||
per_page: number;
|
per_page: number;
|
||||||
@ -24,15 +21,14 @@ type PaginatedHistory = UserWithStatus[] & {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getUsersWithStatuses = async () => {
|
export const getRecentUsersWithStatuses = async (): Promise<
|
||||||
|
Result<UserWithStatus[]>
|
||||||
|
> => {
|
||||||
try {
|
try {
|
||||||
const supabase = await createServerClient();
|
const supabase = await createServerClient();
|
||||||
|
const oneDayAgo = new Date(Date.now() - 1000 * 60 * 60 * 24);
|
||||||
|
|
||||||
// Get only users with recent statuses (Past 7 days)
|
const { data, error } = await supabase
|
||||||
const oneWeekAgo = new Date();
|
|
||||||
oneWeekAgo.setDate(oneWeekAgo.getDate() - 7);
|
|
||||||
|
|
||||||
const { data: recentStatuses, error } = await supabase
|
|
||||||
.from('statuses')
|
.from('statuses')
|
||||||
.select(`
|
.select(`
|
||||||
user:profiles!user_id(*),
|
user:profiles!user_id(*),
|
||||||
@ -40,14 +36,245 @@ export const getUsersWithStatuses = async () => {
|
|||||||
created_at,
|
created_at,
|
||||||
updated_by:profiles!updated_by_id(*)
|
updated_by:profiles!updated_by_id(*)
|
||||||
`)
|
`)
|
||||||
.gte('created_at', oneWeekAgo.toISOString())
|
.gte('created_at', oneDayAgo.toISOString())
|
||||||
.order('created_at', { ascending: false });
|
.order('created_at', { ascending: false }) as { data: UserWithStatus[], error: unknown };
|
||||||
|
|
||||||
if (error) throw error;
|
if (error) throw error as Error;
|
||||||
if (!recentStatuses.length) return { success: true, data: []};
|
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<string>();
|
||||||
|
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) {
|
} 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<Result<void>> => {
|
||||||
|
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<Result<void>> => {
|
||||||
|
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<UserWithStatus>(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<Result<void>> => {
|
||||||
|
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<Result<PaginatedHistory>> => {
|
||||||
|
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<Result<PaginatedHistory>> => {
|
||||||
|
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}`,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -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<Result<UserStatus[]>> => {
|
|
||||||
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<string, Status>();
|
|
||||||
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<Result<void>> => {
|
|
||||||
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<Result<void>> => {
|
|
||||||
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<Result<void>> => {
|
|
||||||
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<Result<PaginatedHistory>> => {
|
|
||||||
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<Result<PaginatedHistory>> => {
|
|
||||||
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',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
@ -146,6 +146,6 @@ export const getUser = async (): Promise<Result<User>> => {
|
|||||||
if (error) throw error;
|
if (error) throw error;
|
||||||
return { success: true, data: data.user };
|
return { success: true, data: data.user };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return { success: false, error: 'Could not get user!' };
|
return { success: false, error: `Could not get user! Error: ${error as Error}` };
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -1,20 +1,18 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
import { createClient } from '@/utils/supabase';
|
||||||
|
import type { Profile, Result, Status } from '@/utils/supabase';
|
||||||
|
import { getUser, getProfile } from '@/lib/hooks'
|
||||||
|
|
||||||
import {
|
export type UserWithStatus = {
|
||||||
createClient,
|
|
||||||
type Profile,
|
|
||||||
type Result,
|
|
||||||
type Status,
|
|
||||||
} from '@/utils/supabase';
|
|
||||||
|
|
||||||
type UserWithStatus = {
|
|
||||||
user: Profile;
|
user: Profile;
|
||||||
status: string;
|
status: string;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
updated_by: Profile;
|
updated_by?: Profile;
|
||||||
};
|
};
|
||||||
|
|
||||||
type PaginatedHistory = UserWithStatus[] & {
|
type PaginatedHistory = {
|
||||||
|
profile?: Profile;
|
||||||
|
statuses: Status[];
|
||||||
meta: {
|
meta: {
|
||||||
current_page: number;
|
current_page: number;
|
||||||
per_page: number;
|
per_page: number;
|
||||||
@ -23,15 +21,14 @@ type PaginatedHistory = UserWithStatus[] & {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getUsersWithStatuses = async (): Promise<Result<UserWithStatus[]>> => {
|
export const getRecentUsersWithStatuses = async (): Promise<
|
||||||
|
Result<UserWithStatus[]>
|
||||||
|
> => {
|
||||||
try {
|
try {
|
||||||
const supabase = createClient();
|
const supabase = createClient();
|
||||||
|
const oneDayAgo = new Date(Date.now() - 1000 * 60 * 60 * 24);
|
||||||
|
|
||||||
// Get only users with recent statuses (Past 7 days)
|
const { data, error } = await supabase
|
||||||
const oneWeekAgo = new Date();
|
|
||||||
oneWeekAgo.setDate(oneWeekAgo.getDate() - 7);
|
|
||||||
|
|
||||||
const { data: recentStatuses, error } = await supabase
|
|
||||||
.from('statuses')
|
.from('statuses')
|
||||||
.select(`
|
.select(`
|
||||||
user:profiles!user_id(*),
|
user:profiles!user_id(*),
|
||||||
@ -39,14 +36,246 @@ export const getUsersWithStatuses = async (): Promise<Result<UserWithStatus[]>>
|
|||||||
created_at,
|
created_at,
|
||||||
updated_by:profiles!updated_by_id(*)
|
updated_by:profiles!updated_by_id(*)
|
||||||
`)
|
`)
|
||||||
.gte('created_at', oneWeekAgo.toISOString())
|
.gte('created_at', oneDayAgo.toISOString())
|
||||||
.order('created_at', { ascending: false }) as { data: UserWithStatus[], error: unknown };
|
.order('created_at', { ascending: false }) as { data: UserWithStatus[], error: unknown };
|
||||||
|
|
||||||
if (error) throw error;
|
if (error) throw error as Error;
|
||||||
if (!recentStatuses.length) return { success: true, data: []};
|
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<string>();
|
||||||
|
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) {
|
} 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<Result<void>> => {
|
||||||
|
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<Result<void>> => {
|
||||||
|
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<UserWithStatus>(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<Result<void>> => {
|
||||||
|
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<Result<PaginatedHistory>> => {
|
||||||
|
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<Result<PaginatedHistory>> => {
|
||||||
|
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}`,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
Reference in New Issue
Block a user