Files
techtracker/apps/next/src/components/layout/status/table/index.tsx

360 lines
13 KiB
TypeScript

'use client';
import Link from 'next/link';
import { useState } from 'react';
import { type Preloaded, usePreloadedQuery, useMutation } from 'convex/react';
import { api } from '~/convex/_generated/api';
import { type Id } from '~/convex/_generated/dataModel';
import { useTVMode } from '@/components/providers';
import {
BasedAvatar,
Button,
Drawer,
DrawerTrigger,
Input,
SubmitButton,
} from '@/components/ui';
import { toast } from 'sonner';
import { ccn, formatTime, formatDate } from '@/lib/utils';
import { Clock, Calendar } from 'lucide-react';
import { StatusHistory } from '@/components/layout/status';
type StatusTableProps = {
preloadedUser: Preloaded<typeof api.auth.getUser>;
preloadedStatuses: Preloaded<typeof api.statuses.getCurrentForAll>;
};
export const StatusTable = ({
preloadedUser,
preloadedStatuses,
}: StatusTableProps) => {
const user = usePreloadedQuery(preloadedUser);
const statuses = usePreloadedQuery(preloadedStatuses);
const { tvMode } = useTVMode();
const [selectedUserIds, setSelectedUserIds] = useState<Id<'users'>[]>([]);
const [selectAll, setSelectAll] = useState(false);
const [statusInput, setStatusInput] = useState('');
const [updatingStatus, setUpdatingStatus] = useState(false);
const bulkCreate = useMutation(api.statuses.bulkCreate);
const handleSelectUser = (id: Id<'users'>) => {
setSelectedUserIds((prev) =>
prev.some((i) => i === id)
? prev.filter((prevId) => prevId !== id)
: [...prev, id],
);
};
const handleSelectAll = () => {
if (selectAll) setSelectedUserIds([]);
else setSelectedUserIds(statuses.map((s) => s.user.id));
setSelectAll(!selectAll);
};
const handleUpdateStatus = async () => {
const message = statusInput.trim();
setUpdatingStatus(true);
try {
if (message.length < 3 || message.length > 80)
throw new Error('Status must be between 3 & 80 characters');
if (selectedUserIds.length === 0 && user?.id)
await bulkCreate({ message, userIds: [user.id] });
await bulkCreate({ message, userIds: selectedUserIds });
toast.success('Status updated.');
setSelectedUserIds([]);
setSelectAll(false);
setStatusInput('');
} catch (error) {
toast.error(`Update failed. ${error as Error}`);
} finally {
setUpdatingStatus(false);
}
};
const containerCn = ccn({
context: tvMode,
className: 'mx-auto px-2 sm:px-4',
on: 'lg:w-11/12 w-full',
off: 'w-full max-w-7xl',
});
const headerCn = ccn({
context: tvMode,
className: 'w-full mb-4 flex justify-between',
on: '',
off: 'mb-4',
});
const thCn = ccn({
context: tvMode,
className:
'py-3 px-2 sm:py-4 sm:px-4 border-b border-border font-semibold text-left',
on: 'text-4xl lg:text-6xl xl:min-w-[420px]',
off: 'text-sm sm:text-base md:text-lg lg:text-xl xl:min-w-[200px]',
});
const tdCn = ccn({
context: tvMode,
className: 'py-2 px-2 sm:py-3 sm:px-4 border-b border-border/50',
on: 'text-3xl lg:text-5xl',
off: 'text-xs sm:text-sm md:text-base',
});
const tCheckboxCn = ccn({
context: tvMode,
className: 'py-3 px-2 sm:px-4 border-b border-border text-center',
on: 'text-4xl',
off: 'text-sm',
});
const checkBoxCn = ccn({
context: tvMode,
className: 'cursor-pointer',
on: 'scale-150 lg:scale-200',
off: 'scale-100 sm:scale-125',
});
return (
<div className={containerCn}>
<div className={headerCn}>
<div className='flex flex-col w-full gap-2 items-end'>
{!tvMode && (
<div className='flex flex-row gap-2 text-xs'>
<p className='text-muted-foreground'>Tired of the old table? </p>
<Link
href='/'
className='italic font-semibold hover:text-primary/80'
>
Try the new status list!
</Link>
</div>
)}
</div>
</div>
<div className='overflow-x-auto rounded-lg border border-border shadow-sm'>
<table className='w-full min-w-[600px] text-left'>
<thead className='bg-muted/70 border-b border-border'>
<tr>
{!tvMode && (
<th className={tCheckboxCn}>
<input
type='checkbox'
className={checkBoxCn}
checked={selectAll}
onChange={handleSelectAll}
/>
</th>
)}
<th className={thCn}>Technician</th>
<th className={thCn}>
<Drawer>
<DrawerTrigger className='hover:text-foreground/60 cursor-pointer transition-colors'>
Status
</DrawerTrigger>
<StatusHistory />
</Drawer>
</th>
<th className={thCn}>Updated At</th>
</tr>
</thead>
<tbody>
{statuses.map((status, i) => {
const { user: u, status: s } = status;
const isSelected = selectedUserIds.includes(u.id);
return (
<tr
key={u.id}
className={`
${i % 2 === 0 ? 'bg-secondary/30 dark:bg-muted/30' : 'bg-background'}
${isSelected ? 'ring-2 ring-primary ring-inset' : ''}
hover:bg-accent/60 dark:hover:bg-accent/40 transition-colors duration-200
group
`}
>
{!tvMode && (
<td className={tCheckboxCn}>
<input
type='checkbox'
className={checkBoxCn}
checked={isSelected}
onChange={() => handleSelectUser(u.id)}
/>
</td>
)}
<td className={tdCn}>
<div className='flex items-center gap-2 sm:gap-3'>
<BasedAvatar
src={u.imageUrl}
fullName={u.name}
className={ccn({
context: tvMode,
className: 'shrink-0',
on: 'w-16 h-16',
off: 'w-8 h-8 sm:w-10 sm:h-10 md:w-12 md:h-12',
})}
/>
<div className='min-w-0 flex-1'>
<p
className={ccn({
context: tvMode,
className: 'font-medium truncate',
on: 'text-3xl lg:text-5xl',
off: 'text-xs sm:text-sm md:text-base',
})}
>
{u.name ?? 'Technician #' + (i + 1)}
</p>
{s?.updatedBy && s.updatedBy.id !== u.id && (
<div className='flex items-center gap-1 text-muted-foreground mt-1'>
<BasedAvatar
src={s.updatedBy.imageUrl}
fullName={s.updatedBy.name}
className={ccn({
context: tvMode,
className: 'shrink-0',
on: 'w-6 h-6',
off: 'w-3 h-3 sm:w-4 sm:h-4',
})}
/>
<span
className={ccn({
context: tvMode,
className: 'text-muted-foreground truncate',
on: 'text-xl',
off: 'text-xs sm:text-sm',
})}
>
Updated by {s.updatedBy.name}
</span>
</div>
)}
</div>
</div>
</td>
<td className={tdCn}>
<Drawer>
<DrawerTrigger className='text-left hover:text-primary transition-colors'>
<span
className={ccn({
context: tvMode,
className: 'line-clamp-2 sm:line-clamp-1',
on: 'text-3xl lg:text-5xl',
off: 'text-xs sm:text-sm md:text-base',
})}
>
{s?.message ?? 'No status'}
</span>
</DrawerTrigger>
<StatusHistory user={u} />
</Drawer>
</td>
<td className={tdCn}>
<Drawer>
<DrawerTrigger className='text-left hover:text-primary transition-colors'>
<div className='flex flex-col gap-1 sm:gap-2'>
<div className='flex items-center gap-1 sm:gap-2'>
<Clock
className={ccn({
context: tvMode,
className: 'shrink-0',
on: 'w-8 h-8 lg:w-11 lg:h-11',
off: 'w-3 h-3 sm:w-4 sm:h-4 md:w-5 md:h-5',
})}
/>
<span
className={ccn({
context: tvMode,
className: '',
on: 'text-3xl lg:text-4xl',
off: 'text-xs sm:text-sm md:text-base font-medium',
})}
>
{s ? formatTime(s.updatedAt) : '--:--'}
</span>
</div>
<div className='flex items-center gap-1 sm:gap-2'>
<Calendar
className={ccn({
context: tvMode,
className: 'shrink-0',
on: 'w-8 h-8 lg:w-11 lg:h-11',
off: 'w-3 h-3 sm:w-4 sm:h-4 md:w-5 md:h-5',
})}
/>
<span
className={ccn({
context: tvMode,
className: 'text-muted-foreground',
on: 'text-3xl lg:text-4xl',
off: 'text-xs sm:text-sm md:text-base',
})}
>
{s ? formatDate(s.updatedAt) : '--/--/--'}
</span>
</div>
</div>
</DrawerTrigger>
<StatusHistory user={u} />
</Drawer>
</td>
</tr>
);
})}
</tbody>
</table>
</div>
{statuses.length === 0 && (
<div className='p-8 text-center'>
<p
className={`text-muted-foreground ${tvMode ? 'text-4xl' : 'text-lg'}`}
>
No status updates yet
</p>
</div>
)}
{!tvMode && (
<div className='mx-auto flex flex-col sm:flex-row items-stretch sm:items-center justify-center py-5 gap-3 sm:gap-4 px-4'>
<Input
autoFocus
type='text'
placeholder='New Status'
className='flex-1 min-w-0 sm:min-w-[200px] sm:max-w-[400px] py-3 sm:py-4 lg:py-6 px-3 rounded-xl border bg-background text-sm sm:text-base lg:text-xl focus:outline-none focus:ring-2 focus:ring-primary transition-all'
value={statusInput}
disabled={updatingStatus}
onChange={(e) => setStatusInput(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter' && !e.shiftKey && !updatingStatus) {
e.preventDefault();
void handleUpdateStatus();
}
}}
/>
<SubmitButton
className='px-4 sm:px-6 lg:px-8 py-3 sm:py-4 lg:py-6 rounded-xl font-semibold text-sm sm:text-base lg:text-xl disabled:opacity-50 disabled:cursor-not-allowed cursor-pointer transition-all hover:scale-105 whitespace-nowrap'
onClick={handleUpdateStatus}
disabled={updatingStatus}
pendingText='Updating...'
>
{selectedUserIds.length > 0
? `Update ${selectedUserIds.length} ${selectedUserIds.length > 1 ? 'users' : 'user'}`
: 'Update status'}
</SubmitButton>
</div>
)}
{/* Global Status History Drawer */}
{!tvMode && (
<div className='flex justify-center mt-6'>
<Drawer>
<DrawerTrigger asChild>
<Button
variant='outline'
className={tvMode ? 'text-3xl p-6' : ''}
>
View All Status History
</Button>
</DrawerTrigger>
<StatusHistory />
</Drawer>
</div>
)}
</div>
);
};