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:
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>
|
||||
);
|
||||
};
|
Reference in New Issue
Block a user