Weird stopping point but whatever
This commit is contained in:
@@ -26,6 +26,12 @@ type StatusRow = {
|
|||||||
} | null,
|
} | null,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type Paginated<T> = {
|
||||||
|
page: T[],
|
||||||
|
isDone: boolean,
|
||||||
|
continueCursor: string | null,
|
||||||
|
};
|
||||||
|
|
||||||
// CHANGED: typed helpers
|
// CHANGED: typed helpers
|
||||||
const ensureUser = async (ctx: RWCtx, userId: Id<'users'>) => {
|
const ensureUser = async (ctx: RWCtx, userId: Id<'users'>) => {
|
||||||
const user = await ctx.db.get(userId);
|
const user = await ctx.db.get(userId);
|
||||||
@@ -228,35 +234,76 @@ export const getCurrentForAll = query({
|
|||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Paginated history for a specific user (newest first).
|
* Paginated history for all users or for a specific user.
|
||||||
*/
|
*/
|
||||||
export const listHistoryByUser = query({
|
export const listHistory = query({
|
||||||
args: {
|
args: {
|
||||||
userId: v.id('users'),
|
userId: v.optional(v.id('users')),
|
||||||
paginationOpts: paginationOptsValidator,
|
paginationOpts: paginationOptsValidator,
|
||||||
},
|
},
|
||||||
handler: async (ctx, { userId, paginationOpts }) => {
|
handler: async (ctx, { userId, paginationOpts }): Promise<
|
||||||
await ensureUser(ctx, userId);
|
Paginated<StatusRow>
|
||||||
|
> => {
|
||||||
|
// Query statuses newest-first, optionally filtered by user
|
||||||
|
const result = userId
|
||||||
|
? await ctx.db
|
||||||
|
.query('statuses')
|
||||||
|
.withIndex('by_user_updatedAt', (q) => q.eq('userId', userId))
|
||||||
|
.order('desc')
|
||||||
|
.paginate(paginationOpts)
|
||||||
|
: await ctx.db
|
||||||
|
.query('statuses')
|
||||||
|
.order('desc')
|
||||||
|
.paginate(paginationOpts);
|
||||||
|
|
||||||
return await ctx.db
|
// Cache user display objects to avoid refetching repeatedly
|
||||||
.query('statuses')
|
const displayCache = new Map<string, StatusRow['user']>();
|
||||||
.withIndex('by_user_updatedAt', (q) => q.eq('userId', userId))
|
|
||||||
.order('desc')
|
const getDisplay = async (
|
||||||
.paginate(paginationOpts);
|
uid: Id<'users'>,
|
||||||
|
): Promise<StatusRow['user']> => {
|
||||||
|
const key = uid as unknown as string;
|
||||||
|
const cached = displayCache.get(key);
|
||||||
|
if (cached) return cached;
|
||||||
|
|
||||||
|
const user = await ctx.db.get(uid);
|
||||||
|
if (!user) throw new ConvexError('User not found.');
|
||||||
|
|
||||||
|
const imgId = getImageId(user);
|
||||||
|
const imgUrl = imgId ? await ctx.storage.getUrl(imgId) : null;
|
||||||
|
|
||||||
|
const display: StatusRow['user'] = {
|
||||||
|
id: user._id,
|
||||||
|
name: getName(user),
|
||||||
|
imageUrl: imgUrl,
|
||||||
|
};
|
||||||
|
displayCache.set(key, display);
|
||||||
|
return display;
|
||||||
|
};
|
||||||
|
|
||||||
|
const page: StatusRow[] = [];
|
||||||
|
for (const s of result.page) {
|
||||||
|
const owner = await getDisplay(s.userId);
|
||||||
|
const updatedBy =
|
||||||
|
s.updatedBy !== s.userId ? await getDisplay(s.updatedBy) : null;
|
||||||
|
|
||||||
|
page.push({
|
||||||
|
user: owner,
|
||||||
|
status: {
|
||||||
|
id: s._id,
|
||||||
|
message: s.message,
|
||||||
|
updatedAt: s.updatedAt,
|
||||||
|
updatedBy,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
page,
|
||||||
|
isDone: result.isDone,
|
||||||
|
continueCursor: result.continueCursor,
|
||||||
|
};
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
|
||||||
* Global paginated history (all users, newest first).
|
|
||||||
* - Add an index on updatedAt if you want to avoid full-table scans
|
|
||||||
* when the collection grows large.
|
|
||||||
*/
|
|
||||||
export const listHistoryAll = query({
|
|
||||||
args: { paginationOpts: paginationOptsValidator },
|
|
||||||
handler: async (ctx, { paginationOpts }) => {
|
|
||||||
return await ctx.db
|
|
||||||
.query('statuses')
|
|
||||||
.order('desc')
|
|
||||||
.paginate(paginationOpts);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
@@ -24,7 +24,7 @@ import {
|
|||||||
TabsTrigger,
|
TabsTrigger,
|
||||||
} from '@/components/ui';
|
} from '@/components/ui';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { PASSWORD_MIN, PASSWORD_MAX, PASSWORD_REGEX } from '~/convex/auth';
|
import { PASSWORD_MIN, PASSWORD_MAX, PASSWORD_REGEX } from '@/lib/utils';
|
||||||
|
|
||||||
const signInFormSchema = z.object({
|
const signInFormSchema = z.object({
|
||||||
email: z.email({
|
email: z.email({
|
||||||
|
@@ -21,7 +21,7 @@ import {
|
|||||||
SubmitButton,
|
SubmitButton,
|
||||||
} from '@/components/ui';
|
} from '@/components/ui';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { PASSWORD_MIN, PASSWORD_MAX, PASSWORD_REGEX } from '~/convex/auth';
|
import { PASSWORD_MIN, PASSWORD_MAX, PASSWORD_REGEX } from '@/lib/utils';
|
||||||
|
|
||||||
const formSchema = z
|
const formSchema = z
|
||||||
.object({
|
.object({
|
||||||
|
@@ -29,15 +29,14 @@ import {
|
|||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
type StatusHistoryProps = {
|
type StatusHistoryProps = {
|
||||||
preloadedUser: Preloaded<typeof api.auth.getUser>;
|
userId?: Id<'users'>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const StatusHistory = ({
|
export const StatusHistory = ({
|
||||||
preloadedUser,
|
userId,
|
||||||
}: StatusHistoryProps) => {
|
}: StatusHistoryProps) => {
|
||||||
const user = usePreloadedQuery(preloadedUser);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<div/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@@ -18,6 +18,7 @@ import {
|
|||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { ccn, formatTime, formatDate } from '@/lib/utils';
|
import { ccn, formatTime, formatDate } from '@/lib/utils';
|
||||||
import { Clock, Calendar, CheckCircle2 } from 'lucide-react';
|
import { Clock, Calendar, CheckCircle2 } from 'lucide-react';
|
||||||
|
import { StatusHistory } from '@/components/layout/status';
|
||||||
|
|
||||||
type StatusListProps = {
|
type StatusListProps = {
|
||||||
preloadedUser: Preloaded<typeof api.auth.getUser>;
|
preloadedUser: Preloaded<typeof api.auth.getUser>;
|
||||||
@@ -36,6 +37,7 @@ export const StatusList = ({
|
|||||||
const [selectAll, setSelectAll] = useState(false);
|
const [selectAll, setSelectAll] = useState(false);
|
||||||
const [statusInput, setStatusInput] = useState('');
|
const [statusInput, setStatusInput] = useState('');
|
||||||
const [updatingStatus, setUpdatingStatus] = useState(false);
|
const [updatingStatus, setUpdatingStatus] = useState(false);
|
||||||
|
const [selectedHistoryUserId, setSelectedHistoryUserId] = useState<Id<'users'>>();
|
||||||
|
|
||||||
const bulkCreate = useMutation(api.statuses.bulkCreate);
|
const bulkCreate = useMutation(api.statuses.bulkCreate);
|
||||||
|
|
||||||
@@ -155,19 +157,25 @@ export const StatusList = ({
|
|||||||
)}
|
)}
|
||||||
<CardContent className='p-0'>
|
<CardContent className='p-0'>
|
||||||
<div className='flex items-start gap-3'>
|
<div className='flex items-start gap-3'>
|
||||||
<div
|
<Drawer>
|
||||||
data-profile-trigger
|
<DrawerTrigger asChild>
|
||||||
className='flex-shrink-0 cursor-pointer
|
<div
|
||||||
hover:opacity-80 transition-opacity'
|
data-profile-trigger
|
||||||
// TODO: open history drawer
|
className='flex-shrink-0 cursor-pointer
|
||||||
>
|
hover:opacity-80 transition-opacity'
|
||||||
<BasedAvatar
|
onClick={() => setSelectedHistoryUserId(u.id)}
|
||||||
// Swap to a URL once you resolve storage URLs
|
>
|
||||||
src={u.imageUrl}
|
<BasedAvatar
|
||||||
fullName={u.name ?? 'Technician'}
|
src={u.imageUrl}
|
||||||
className={tvMode ? 'w-16 h-16' : 'w-12 h-12'}
|
fullName={u.name ?? 'Technician'}
|
||||||
/>
|
className={tvMode ? 'w-16 h-16' : 'w-12 h-12'}
|
||||||
</div>
|
/>
|
||||||
|
</div>
|
||||||
|
</DrawerTrigger>
|
||||||
|
{selectedHistoryUserId === u.id && (
|
||||||
|
<StatusHistory userId={u.id} />
|
||||||
|
)}
|
||||||
|
</Drawer>
|
||||||
|
|
||||||
<div className='flex-1'>
|
<div className='flex-1'>
|
||||||
<div className='flex items-start justify-between mb-2'>
|
<div className='flex items-start justify-between mb-2'>
|
||||||
|
@@ -19,6 +19,10 @@ export const ccn = ({
|
|||||||
return twMerge(className, context ? on : off);
|
return twMerge(className, context ? on : off);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const PASSWORD_MIN = 8;
|
||||||
|
export const PASSWORD_MAX = 100;
|
||||||
|
export const PASSWORD_REGEX = /^(?=.{8,100}$)(?!.*\s)(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[\p{P}\p{S}]).*$/u;
|
||||||
|
|
||||||
type Timestamp = number | string | Date;
|
type Timestamp = number | string | Date;
|
||||||
|
|
||||||
const toDate = (ts: Timestamp): Date | null => {
|
const toDate = (ts: Timestamp): Date | null => {
|
||||||
|
Reference in New Issue
Block a user