306 lines
9.9 KiB
TypeScript
Executable File
306 lines
9.9 KiB
TypeScript
Executable File
'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, Loading } from '@/components/ui';
|
|
import { toast } from 'sonner';
|
|
import { HistoryDrawer } from '@/components/status';
|
|
import type { Profile } from '@/utils/supabase';
|
|
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 } = 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 [selectedHistoryUser, setSelectedHistoryUser] =
|
|
useState<Profile | null>(null);
|
|
const channelRef = useRef<RealtimeChannel | null>(null);
|
|
|
|
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]);
|
|
|
|
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]);
|
|
|
|
useEffect(() => {
|
|
if (!isAuthenticated) return;
|
|
const supabase = createClient();
|
|
|
|
const channel = supabase
|
|
.channel('status_updates', {
|
|
config: { broadcast: { self: true }}
|
|
})
|
|
.on('broadcast', { event: 'status_updated' }, (payload) => {
|
|
const { user_status } = payload.payload as {
|
|
user_status: UserWithStatus;
|
|
timestamp: string;
|
|
};
|
|
console.log('Received status update:', user_status);
|
|
|
|
setUsersWithStatuses((prevUsers) => {
|
|
const existingUserIndex = prevUsers.findIndex((u) =>
|
|
u.user.id === user_status.user.id,
|
|
);
|
|
|
|
if (existingUserIndex !== -1) {
|
|
const updatedUsers = [...prevUsers];
|
|
updatedUsers[existingUserIndex] = {
|
|
user: user_status.user, // Use the user from the broadcast
|
|
status: user_status.status,
|
|
created_at: user_status.created_at,
|
|
updated_by: user_status.updated_by,
|
|
};
|
|
return updatedUsers;
|
|
} else {
|
|
// Add new user to list!
|
|
return [user_status, ...prevUsers];
|
|
}
|
|
});
|
|
})
|
|
.subscribe((status) => {
|
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison
|
|
if (status === 'SUBSCRIBED') {
|
|
console.log('Successfully subscribed to status updates!');
|
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison
|
|
} else if (status === 'CHANNEL_ERROR') {
|
|
console.error('Error subscribing to status updates.')
|
|
}
|
|
});
|
|
|
|
channelRef.current = channel;
|
|
|
|
return () => {
|
|
if (channelRef.current) {
|
|
supabase.removeChannel(channelRef.current).catch((error) => {
|
|
console.error(`Error unsubscribing from status updates: ${error}`);
|
|
});
|
|
channelRef.current = null;
|
|
}
|
|
};
|
|
}, [isAuthenticated]);
|
|
|
|
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]'>
|
|
<Loading className='w-full' alpha={0.5} />
|
|
</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 />
|
|
</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={() => setSelectedHistoryUser(userWithStatus.user)}
|
|
>
|
|
{userWithStatus.status}
|
|
</DrawerTrigger>
|
|
{selectedHistoryUser === userWithStatus.user && (
|
|
<HistoryDrawer user={selectedHistoryUser} />
|
|
)}
|
|
</Drawer>
|
|
</td>
|
|
<td className='py-3 px-4 border text-muted-foreground'>
|
|
{formatTime(userWithStatus.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') {
|
|
updateStatus().catch((error) => {
|
|
toast.error(`Failed to update status: ${error as Error}`);
|
|
});
|
|
}
|
|
}}
|
|
/>
|
|
<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>
|
|
);
|
|
};
|