perf(quick-wins): batch collaborator resolution, debounce SSE refresh, loading states
- Eliminate N+1 on resolveCollaborator: add batchResolveCollaborators() in auth.ts (2 DB queries max regardless of session count), update all 4 workshop services to use post-batch mapping - Debounce router.refresh() in useLive.ts (300ms) to group simultaneous SSE events and avoid cascade re-renders - Call cleanupOldEvents fire-and-forget in createEvent to purge old SSE events inline without blocking the response - Add loading.tsx skeletons on /sessions and /users matching actual page layout (PageHeader + content structure) - Lazy-load ShareModal via next/dynamic in BaseSessionLiveWrapper to reduce initial JS bundle Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
32
src/app/sessions/loading.tsx
Normal file
32
src/app/sessions/loading.tsx
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
export default function SessionsLoading() {
|
||||||
|
return (
|
||||||
|
<main className="mx-auto max-w-7xl px-4">
|
||||||
|
{/* PageHeader skeleton */}
|
||||||
|
<div className="py-6 flex items-start justify-between gap-4">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="h-10 w-10 bg-card rounded-xl animate-pulse" />
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="h-7 w-40 bg-card rounded animate-pulse" />
|
||||||
|
<div className="h-4 w-64 bg-card rounded animate-pulse" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="h-9 w-36 bg-card rounded-lg animate-pulse" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* Tabs skeleton */}
|
||||||
|
<div className="flex gap-2 pb-2">
|
||||||
|
{[120, 100, 110, 90, 105].map((w, i) => (
|
||||||
|
<div key={i} className="h-9 bg-card animate-pulse rounded-full" style={{ width: w }} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
{/* Cards grid skeleton */}
|
||||||
|
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||||
|
{Array.from({ length: 6 }).map((_, i) => (
|
||||||
|
<div key={i} className="h-44 bg-card animate-pulse rounded-xl border border-border" />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
42
src/app/users/loading.tsx
Normal file
42
src/app/users/loading.tsx
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
export default function UsersLoading() {
|
||||||
|
return (
|
||||||
|
<main className="mx-auto max-w-6xl px-4">
|
||||||
|
{/* PageHeader skeleton */}
|
||||||
|
<div className="py-6 flex items-start gap-3">
|
||||||
|
<div className="h-10 w-10 bg-card rounded-xl animate-pulse" />
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="h-7 w-36 bg-card rounded animate-pulse" />
|
||||||
|
<div className="h-4 w-72 bg-card rounded animate-pulse" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Stats grid skeleton */}
|
||||||
|
<div className="mb-8 grid grid-cols-2 gap-4 sm:grid-cols-4">
|
||||||
|
{Array.from({ length: 4 }).map((_, i) => (
|
||||||
|
<div key={i} className="rounded-xl border border-border bg-card p-4 space-y-2 animate-pulse">
|
||||||
|
<div className="h-8 w-12 bg-muted/40 rounded" />
|
||||||
|
<div className="h-4 w-24 bg-muted/30 rounded" />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* User rows skeleton */}
|
||||||
|
<div className="space-y-3">
|
||||||
|
{Array.from({ length: 8 }).map((_, i) => (
|
||||||
|
<div key={i} className="flex items-center gap-4 rounded-xl border border-border bg-card p-4 animate-pulse">
|
||||||
|
<div className="h-12 w-12 rounded-full bg-muted/40 flex-shrink-0" />
|
||||||
|
<div className="flex-1 space-y-2 min-w-0">
|
||||||
|
<div className="h-4 w-32 bg-muted/40 rounded" />
|
||||||
|
<div className="h-3 w-48 bg-muted/30 rounded" />
|
||||||
|
</div>
|
||||||
|
<div className="hidden sm:flex gap-2">
|
||||||
|
<div className="h-6 w-16 bg-muted/30 rounded-full" />
|
||||||
|
<div className="h-6 w-16 bg-muted/30 rounded-full" />
|
||||||
|
<div className="h-6 w-16 bg-muted/30 rounded-full" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,10 +1,12 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState, useCallback } from 'react';
|
import { useState, useCallback } from 'react';
|
||||||
|
import dynamic from 'next/dynamic';
|
||||||
import { useLive, type LiveEvent } from '@/hooks/useLive';
|
import { useLive, type LiveEvent } from '@/hooks/useLive';
|
||||||
import { CollaborationToolbar } from './CollaborationToolbar';
|
import { CollaborationToolbar } from './CollaborationToolbar';
|
||||||
import { ShareModal } from './ShareModal';
|
|
||||||
import type { ShareRole } from '@prisma/client';
|
import type { ShareRole } from '@prisma/client';
|
||||||
|
|
||||||
|
const ShareModal = dynamic(() => import('./ShareModal').then((m) => m.ShareModal), { ssr: false });
|
||||||
import type { TeamWithMembers, Share } from '@/lib/share-utils';
|
import type { TeamWithMembers, Share } from '@/lib/share-utils';
|
||||||
|
|
||||||
export type LiveApiPath = 'sessions' | 'motivators' | 'weather' | 'year-review' | 'weekly-checkin' | 'gif-mood';
|
export type LiveApiPath = 'sessions' | 'motivators' | 'weather' | 'year-review' | 'weekly-checkin' | 'gif-mood';
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ export function useLive({
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const eventSourceRef = useRef<EventSource | null>(null);
|
const eventSourceRef = useRef<EventSource | null>(null);
|
||||||
const reconnectTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
const reconnectTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
|
const refreshTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
const reconnectAttemptsRef = useRef(0);
|
const reconnectAttemptsRef = useRef(0);
|
||||||
const onEventRef = useRef(onEvent);
|
const onEventRef = useRef(onEvent);
|
||||||
const currentUserIdRef = useRef(currentUserId);
|
const currentUserIdRef = useRef(currentUserId);
|
||||||
@@ -88,8 +89,9 @@ export function useLive({
|
|||||||
setLastEvent(data);
|
setLastEvent(data);
|
||||||
onEventRef.current?.(data);
|
onEventRef.current?.(data);
|
||||||
|
|
||||||
// Refresh the page data when we receive an event from another user
|
// Debounce refresh to group simultaneous SSE events (~300ms window)
|
||||||
router.refresh();
|
if (refreshTimeoutRef.current) clearTimeout(refreshTimeoutRef.current);
|
||||||
|
refreshTimeoutRef.current = setTimeout(() => router.refresh(), 300);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Failed to parse SSE event:', e);
|
console.error('Failed to parse SSE event:', e);
|
||||||
}
|
}
|
||||||
@@ -126,6 +128,10 @@ export function useLive({
|
|||||||
clearTimeout(reconnectTimeoutRef.current);
|
clearTimeout(reconnectTimeoutRef.current);
|
||||||
reconnectTimeoutRef.current = null;
|
reconnectTimeoutRef.current = null;
|
||||||
}
|
}
|
||||||
|
if (refreshTimeoutRef.current) {
|
||||||
|
clearTimeout(refreshTimeoutRef.current);
|
||||||
|
refreshTimeoutRef.current = null;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}, [sessionId, apiPath, enabled, router]);
|
}, [sessionId, apiPath, enabled, router]);
|
||||||
|
|
||||||
|
|||||||
@@ -74,6 +74,48 @@ export interface ResolvedCollaborator {
|
|||||||
} | null;
|
} | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Batch resolve multiple collaborator strings — 2 DB queries max regardless of count
|
||||||
|
export async function batchResolveCollaborators(
|
||||||
|
collaborators: string[]
|
||||||
|
): Promise<Map<string, ResolvedCollaborator>> {
|
||||||
|
if (collaborators.length === 0) return new Map();
|
||||||
|
|
||||||
|
const unique = [...new Set(collaborators.map((c) => c.trim()))];
|
||||||
|
const emails = unique.filter(isEmail).map((e) => e.toLowerCase());
|
||||||
|
const names = unique.filter((c) => !isEmail(c));
|
||||||
|
|
||||||
|
const [byEmail, byName] = await Promise.all([
|
||||||
|
emails.length > 0
|
||||||
|
? prisma.user.findMany({
|
||||||
|
where: { email: { in: emails } },
|
||||||
|
select: { id: true, email: true, name: true },
|
||||||
|
})
|
||||||
|
: [],
|
||||||
|
names.length > 0
|
||||||
|
? prisma.user.findMany({
|
||||||
|
where: { OR: names.map((n) => ({ name: { contains: n } })) },
|
||||||
|
select: { id: true, email: true, name: true },
|
||||||
|
})
|
||||||
|
: [],
|
||||||
|
]);
|
||||||
|
|
||||||
|
const emailMap = new Map(byEmail.map((u) => [u.email.toLowerCase(), u]));
|
||||||
|
const nameMap = new Map(
|
||||||
|
byName.filter((u) => u.name).map((u) => [u.name!.toLowerCase(), u])
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = new Map<string, ResolvedCollaborator>();
|
||||||
|
for (const c of unique) {
|
||||||
|
if (isEmail(c)) {
|
||||||
|
result.set(c, { raw: c, matchedUser: emailMap.get(c.toLowerCase()) ?? null });
|
||||||
|
} else {
|
||||||
|
const match = nameMap.get(c.toLowerCase()) ?? null;
|
||||||
|
result.set(c, { raw: c, matchedUser: match });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
// Resolve collaborator string to user - try email first, then name
|
// Resolve collaborator string to user - try email first, then name
|
||||||
export async function resolveCollaborator(collaborator: string): Promise<ResolvedCollaborator> {
|
export async function resolveCollaborator(collaborator: string): Promise<ResolvedCollaborator> {
|
||||||
const trimmed = collaborator.trim();
|
const trimmed = collaborator.trim();
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { prisma } from '@/services/database';
|
import { prisma } from '@/services/database';
|
||||||
import { resolveCollaborator } from '@/services/auth';
|
import { resolveCollaborator, batchResolveCollaborators } from '@/services/auth';
|
||||||
import { getTeamMemberIdsForAdminTeams } from '@/services/teams';
|
import { getTeamMemberIdsForAdminTeams } from '@/services/teams';
|
||||||
import { createSessionPermissionChecks } from '@/services/session-permissions';
|
import { createSessionPermissionChecks } from '@/services/session-permissions';
|
||||||
import { createShareAndEventHandlers } from '@/services/session-share-events';
|
import { createShareAndEventHandlers } from '@/services/session-share-events';
|
||||||
@@ -21,7 +21,7 @@ const motivatorInclude = {
|
|||||||
// ============================================
|
// ============================================
|
||||||
|
|
||||||
export async function getMotivatorSessionsByUserId(userId: string) {
|
export async function getMotivatorSessionsByUserId(userId: string) {
|
||||||
return mergeSessionsByUserId(
|
const sessions = await mergeSessionsByUserId(
|
||||||
(uid) =>
|
(uid) =>
|
||||||
prisma.movingMotivatorsSession.findMany({
|
prisma.movingMotivatorsSession.findMany({
|
||||||
where: { userId: uid },
|
where: { userId: uid },
|
||||||
@@ -33,14 +33,18 @@ export async function getMotivatorSessionsByUserId(userId: string) {
|
|||||||
where: { userId: uid },
|
where: { userId: uid },
|
||||||
include: { session: { include: motivatorInclude } },
|
include: { session: { include: motivatorInclude } },
|
||||||
}),
|
}),
|
||||||
userId,
|
userId
|
||||||
(s) => resolveCollaborator(s.participant).then((r) => ({ resolvedParticipant: r }))
|
|
||||||
);
|
);
|
||||||
|
const resolved = await batchResolveCollaborators(sessions.map((s) => s.participant));
|
||||||
|
return sessions.map((s) => ({
|
||||||
|
...s,
|
||||||
|
resolvedParticipant: resolved.get(s.participant.trim()) ?? { raw: s.participant, matchedUser: null },
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Sessions owned by team members (where user is team admin) that are NOT shared with the user. */
|
/** Sessions owned by team members (where user is team admin) that are NOT shared with the user. */
|
||||||
export async function getTeamCollaboratorSessionsForAdmin(userId: string) {
|
export async function getTeamCollaboratorSessionsForAdmin(userId: string) {
|
||||||
return fetchTeamCollaboratorSessions(
|
const sessions = await fetchTeamCollaboratorSessions(
|
||||||
(teamMemberIds, uid) =>
|
(teamMemberIds, uid) =>
|
||||||
prisma.movingMotivatorsSession.findMany({
|
prisma.movingMotivatorsSession.findMany({
|
||||||
where: { userId: { in: teamMemberIds }, shares: { none: { userId: uid } } },
|
where: { userId: { in: teamMemberIds }, shares: { none: { userId: uid } } },
|
||||||
@@ -48,9 +52,13 @@ export async function getTeamCollaboratorSessionsForAdmin(userId: string) {
|
|||||||
orderBy: { updatedAt: 'desc' },
|
orderBy: { updatedAt: 'desc' },
|
||||||
}),
|
}),
|
||||||
getTeamMemberIdsForAdminTeams,
|
getTeamMemberIdsForAdminTeams,
|
||||||
userId,
|
userId
|
||||||
(s) => resolveCollaborator(s.participant).then((r) => ({ resolvedParticipant: r }))
|
|
||||||
);
|
);
|
||||||
|
const resolved = await batchResolveCollaborators(sessions.map((s) => s.participant));
|
||||||
|
return sessions.map((s) => ({
|
||||||
|
...s,
|
||||||
|
resolvedParticipant: resolved.get(s.participant.trim()) ?? { raw: s.participant, matchedUser: null },
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
const motivatorByIdInclude = {
|
const motivatorByIdInclude = {
|
||||||
|
|||||||
@@ -131,14 +131,21 @@ export function createShareAndEventHandlers<TEventType extends string>(
|
|||||||
type: TEventType,
|
type: TEventType,
|
||||||
payload: Record<string, unknown>
|
payload: Record<string, unknown>
|
||||||
): Promise<SessionEventWithUser> {
|
): Promise<SessionEventWithUser> {
|
||||||
return eventModel.create({
|
const event = await eventModel.create({
|
||||||
data: {
|
data: {
|
||||||
sessionId,
|
sessionId,
|
||||||
userId,
|
userId,
|
||||||
type,
|
type,
|
||||||
payload: JSON.stringify(payload),
|
payload: JSON.stringify(payload),
|
||||||
},
|
},
|
||||||
}) as Promise<SessionEventWithUser>;
|
});
|
||||||
|
|
||||||
|
// Fire-and-forget: purge old events without blocking the response
|
||||||
|
eventModel.deleteMany({ where: { createdAt: { lt: new Date(Date.now() - 24 * 60 * 60 * 1000) } } }).catch((err: unknown) => {
|
||||||
|
console.error('[cleanupOldEvents] Failed to purge old events:', err);
|
||||||
|
});
|
||||||
|
|
||||||
|
return event as SessionEventWithUser;
|
||||||
},
|
},
|
||||||
|
|
||||||
async getEvents(sessionId: string, since?: Date): Promise<SessionEventWithUser[]> {
|
async getEvents(sessionId: string, since?: Date): Promise<SessionEventWithUser[]> {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { prisma } from '@/services/database';
|
import { prisma } from '@/services/database';
|
||||||
import { resolveCollaborator } from '@/services/auth';
|
import { resolveCollaborator, batchResolveCollaborators } from '@/services/auth';
|
||||||
import { getTeamMemberIdsForAdminTeams } from '@/services/teams';
|
import { getTeamMemberIdsForAdminTeams } from '@/services/teams';
|
||||||
import { createSessionPermissionChecks } from '@/services/session-permissions';
|
import { createSessionPermissionChecks } from '@/services/session-permissions';
|
||||||
import { createShareAndEventHandlers } from '@/services/session-share-events';
|
import { createShareAndEventHandlers } from '@/services/session-share-events';
|
||||||
@@ -21,7 +21,7 @@ const sessionInclude = {
|
|||||||
// ============================================
|
// ============================================
|
||||||
|
|
||||||
export async function getSessionsByUserId(userId: string) {
|
export async function getSessionsByUserId(userId: string) {
|
||||||
return mergeSessionsByUserId(
|
const sessions = await mergeSessionsByUserId(
|
||||||
(uid) =>
|
(uid) =>
|
||||||
prisma.session.findMany({
|
prisma.session.findMany({
|
||||||
where: { userId: uid },
|
where: { userId: uid },
|
||||||
@@ -33,14 +33,18 @@ export async function getSessionsByUserId(userId: string) {
|
|||||||
where: { userId: uid },
|
where: { userId: uid },
|
||||||
include: { session: { include: sessionInclude } },
|
include: { session: { include: sessionInclude } },
|
||||||
}),
|
}),
|
||||||
userId,
|
userId
|
||||||
(s) => resolveCollaborator(s.collaborator).then((r) => ({ resolvedCollaborator: r }))
|
|
||||||
);
|
);
|
||||||
|
const resolved = await batchResolveCollaborators(sessions.map((s) => s.collaborator));
|
||||||
|
return sessions.map((s) => ({
|
||||||
|
...s,
|
||||||
|
resolvedCollaborator: resolved.get(s.collaborator.trim()) ?? { raw: s.collaborator, matchedUser: null },
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Sessions owned by team members (where user is team admin) that are NOT shared with the user. */
|
/** Sessions owned by team members (where user is team admin) that are NOT shared with the user. */
|
||||||
export async function getTeamCollaboratorSessionsForAdmin(userId: string) {
|
export async function getTeamCollaboratorSessionsForAdmin(userId: string) {
|
||||||
return fetchTeamCollaboratorSessions(
|
const sessions = await fetchTeamCollaboratorSessions(
|
||||||
(teamMemberIds, uid) =>
|
(teamMemberIds, uid) =>
|
||||||
prisma.session.findMany({
|
prisma.session.findMany({
|
||||||
where: { userId: { in: teamMemberIds }, shares: { none: { userId: uid } } },
|
where: { userId: { in: teamMemberIds }, shares: { none: { userId: uid } } },
|
||||||
@@ -48,9 +52,13 @@ export async function getTeamCollaboratorSessionsForAdmin(userId: string) {
|
|||||||
orderBy: { updatedAt: 'desc' },
|
orderBy: { updatedAt: 'desc' },
|
||||||
}),
|
}),
|
||||||
getTeamMemberIdsForAdminTeams,
|
getTeamMemberIdsForAdminTeams,
|
||||||
userId,
|
userId
|
||||||
(s) => resolveCollaborator(s.collaborator).then((r) => ({ resolvedCollaborator: r }))
|
|
||||||
);
|
);
|
||||||
|
const resolved = await batchResolveCollaborators(sessions.map((s) => s.collaborator));
|
||||||
|
return sessions.map((s) => ({
|
||||||
|
...s,
|
||||||
|
resolvedCollaborator: resolved.get(s.collaborator.trim()) ?? { raw: s.collaborator, matchedUser: null },
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
const sessionByIdInclude = {
|
const sessionByIdInclude = {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { prisma } from '@/services/database';
|
import { prisma } from '@/services/database';
|
||||||
import { resolveCollaborator } from '@/services/auth';
|
import { resolveCollaborator, batchResolveCollaborators } from '@/services/auth';
|
||||||
import { getTeamMemberIdsForAdminTeams } from '@/services/teams';
|
import { getTeamMemberIdsForAdminTeams } from '@/services/teams';
|
||||||
import { createSessionPermissionChecks } from '@/services/session-permissions';
|
import { createSessionPermissionChecks } from '@/services/session-permissions';
|
||||||
import { createShareAndEventHandlers } from '@/services/session-share-events';
|
import { createShareAndEventHandlers } from '@/services/session-share-events';
|
||||||
@@ -21,7 +21,7 @@ const weeklyCheckInInclude = {
|
|||||||
// ============================================
|
// ============================================
|
||||||
|
|
||||||
export async function getWeeklyCheckInSessionsByUserId(userId: string) {
|
export async function getWeeklyCheckInSessionsByUserId(userId: string) {
|
||||||
return mergeSessionsByUserId(
|
const sessions = await mergeSessionsByUserId(
|
||||||
(uid) =>
|
(uid) =>
|
||||||
prisma.weeklyCheckInSession.findMany({
|
prisma.weeklyCheckInSession.findMany({
|
||||||
where: { userId: uid },
|
where: { userId: uid },
|
||||||
@@ -33,14 +33,18 @@ export async function getWeeklyCheckInSessionsByUserId(userId: string) {
|
|||||||
where: { userId: uid },
|
where: { userId: uid },
|
||||||
include: { session: { include: weeklyCheckInInclude } },
|
include: { session: { include: weeklyCheckInInclude } },
|
||||||
}),
|
}),
|
||||||
userId,
|
userId
|
||||||
(s) => resolveCollaborator(s.participant).then((r) => ({ resolvedParticipant: r }))
|
|
||||||
);
|
);
|
||||||
|
const resolved = await batchResolveCollaborators(sessions.map((s) => s.participant));
|
||||||
|
return sessions.map((s) => ({
|
||||||
|
...s,
|
||||||
|
resolvedParticipant: resolved.get(s.participant.trim()) ?? { raw: s.participant, matchedUser: null },
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Sessions owned by team members (where user is team admin) that are NOT shared with the user. */
|
/** Sessions owned by team members (where user is team admin) that are NOT shared with the user. */
|
||||||
export async function getTeamCollaboratorSessionsForAdmin(userId: string) {
|
export async function getTeamCollaboratorSessionsForAdmin(userId: string) {
|
||||||
return fetchTeamCollaboratorSessions(
|
const sessions = await fetchTeamCollaboratorSessions(
|
||||||
(teamMemberIds, uid) =>
|
(teamMemberIds, uid) =>
|
||||||
prisma.weeklyCheckInSession.findMany({
|
prisma.weeklyCheckInSession.findMany({
|
||||||
where: { userId: { in: teamMemberIds }, shares: { none: { userId: uid } } },
|
where: { userId: { in: teamMemberIds }, shares: { none: { userId: uid } } },
|
||||||
@@ -48,9 +52,13 @@ export async function getTeamCollaboratorSessionsForAdmin(userId: string) {
|
|||||||
orderBy: { updatedAt: 'desc' },
|
orderBy: { updatedAt: 'desc' },
|
||||||
}),
|
}),
|
||||||
getTeamMemberIdsForAdminTeams,
|
getTeamMemberIdsForAdminTeams,
|
||||||
userId,
|
userId
|
||||||
(s) => resolveCollaborator(s.participant).then((r) => ({ resolvedParticipant: r }))
|
|
||||||
);
|
);
|
||||||
|
const resolved = await batchResolveCollaborators(sessions.map((s) => s.participant));
|
||||||
|
return sessions.map((s) => ({
|
||||||
|
...s,
|
||||||
|
resolvedParticipant: resolved.get(s.participant.trim()) ?? { raw: s.participant, matchedUser: null },
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
const weeklyCheckInByIdInclude = {
|
const weeklyCheckInByIdInclude = {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { prisma } from '@/services/database';
|
import { prisma } from '@/services/database';
|
||||||
import { resolveCollaborator } from '@/services/auth';
|
import { resolveCollaborator, batchResolveCollaborators } from '@/services/auth';
|
||||||
import { getTeamMemberIdsForAdminTeams } from '@/services/teams';
|
import { getTeamMemberIdsForAdminTeams } from '@/services/teams';
|
||||||
import { createSessionPermissionChecks } from '@/services/session-permissions';
|
import { createSessionPermissionChecks } from '@/services/session-permissions';
|
||||||
import { createShareAndEventHandlers } from '@/services/session-share-events';
|
import { createShareAndEventHandlers } from '@/services/session-share-events';
|
||||||
@@ -21,7 +21,7 @@ const yearReviewInclude = {
|
|||||||
// ============================================
|
// ============================================
|
||||||
|
|
||||||
export async function getYearReviewSessionsByUserId(userId: string) {
|
export async function getYearReviewSessionsByUserId(userId: string) {
|
||||||
return mergeSessionsByUserId(
|
const sessions = await mergeSessionsByUserId(
|
||||||
(uid) =>
|
(uid) =>
|
||||||
prisma.yearReviewSession.findMany({
|
prisma.yearReviewSession.findMany({
|
||||||
where: { userId: uid },
|
where: { userId: uid },
|
||||||
@@ -33,14 +33,18 @@ export async function getYearReviewSessionsByUserId(userId: string) {
|
|||||||
where: { userId: uid },
|
where: { userId: uid },
|
||||||
include: { session: { include: yearReviewInclude } },
|
include: { session: { include: yearReviewInclude } },
|
||||||
}),
|
}),
|
||||||
userId,
|
userId
|
||||||
(s) => resolveCollaborator(s.participant).then((r) => ({ resolvedParticipant: r }))
|
|
||||||
);
|
);
|
||||||
|
const resolved = await batchResolveCollaborators(sessions.map((s) => s.participant));
|
||||||
|
return sessions.map((s) => ({
|
||||||
|
...s,
|
||||||
|
resolvedParticipant: resolved.get(s.participant.trim()) ?? { raw: s.participant, matchedUser: null },
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Sessions owned by team members (where user is team admin) that are NOT shared with the user. */
|
/** Sessions owned by team members (where user is team admin) that are NOT shared with the user. */
|
||||||
export async function getTeamCollaboratorSessionsForAdmin(userId: string) {
|
export async function getTeamCollaboratorSessionsForAdmin(userId: string) {
|
||||||
return fetchTeamCollaboratorSessions(
|
const sessions = await fetchTeamCollaboratorSessions(
|
||||||
(teamMemberIds, uid) =>
|
(teamMemberIds, uid) =>
|
||||||
prisma.yearReviewSession.findMany({
|
prisma.yearReviewSession.findMany({
|
||||||
where: { userId: { in: teamMemberIds }, shares: { none: { userId: uid } } },
|
where: { userId: { in: teamMemberIds }, shares: { none: { userId: uid } } },
|
||||||
@@ -48,9 +52,13 @@ export async function getTeamCollaboratorSessionsForAdmin(userId: string) {
|
|||||||
orderBy: { updatedAt: 'desc' },
|
orderBy: { updatedAt: 'desc' },
|
||||||
}),
|
}),
|
||||||
getTeamMemberIdsForAdminTeams,
|
getTeamMemberIdsForAdminTeams,
|
||||||
userId,
|
userId
|
||||||
(s) => resolveCollaborator(s.participant).then((r) => ({ resolvedParticipant: r }))
|
|
||||||
);
|
);
|
||||||
|
const resolved = await batchResolveCollaborators(sessions.map((s) => s.participant));
|
||||||
|
return sessions.map((s) => ({
|
||||||
|
...s,
|
||||||
|
resolvedParticipant: resolved.get(s.participant.trim()) ?? { raw: s.participant, matchedUser: null },
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
const yearReviewByIdInclude = {
|
const yearReviewByIdInclude = {
|
||||||
|
|||||||
Reference in New Issue
Block a user