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:
2025-06-13 04:26:52 -05:00
parent 653fe64bbf
commit c96bdab91b
9 changed files with 718 additions and 928 deletions

View 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>
);
};

View File

@ -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>
);
};