import { unstable_cache } from 'next/cache'; import { prisma } from '@/services/database'; import { resolveCollaborator, batchResolveCollaborators } from '@/services/auth'; import { getTeamMemberIdsForAdminTeams } from '@/services/teams'; import { createSessionPermissionChecks } from '@/services/session-permissions'; import { createShareAndEventHandlers } from '@/services/session-share-events'; import { mergeSessionsByUserId, fetchTeamCollaboratorSessions, getSessionByIdGeneric, } from '@/services/session-queries'; import { sessionsListTag } from '@/lib/cache-tags'; import type { YearReviewCategory } from '@prisma/client'; const yearReviewListSelect = { id: true, title: true, participant: true, year: true, updatedAt: true, userId: true, user: { select: { id: true, name: true, email: true } }, shares: { select: { id: true, role: true, user: { select: { id: true, name: true, email: true } } } }, _count: { select: { items: true } }, } as const; // ============================================ // Year Review Session CRUD // ============================================ export async function getYearReviewSessionsByUserId(userId: string) { return unstable_cache( async () => { const sessions = await mergeSessionsByUserId( (uid) => prisma.yearReviewSession.findMany({ where: { userId: uid }, select: yearReviewListSelect, orderBy: { updatedAt: 'desc' }, }), (uid) => prisma.yRSessionShare.findMany({ where: { userId: uid }, select: { role: true, createdAt: true, session: { select: yearReviewListSelect } }, }), userId ); 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 }, })); }, [`year-review-sessions-list-${userId}`], { tags: [sessionsListTag(userId)], revalidate: 60 } )(); } /** Sessions owned by team members (where user is team admin) that are NOT shared with the user. */ export async function getTeamCollaboratorSessionsForAdmin(userId: string) { const sessions = await fetchTeamCollaboratorSessions( (teamMemberIds, uid) => prisma.yearReviewSession.findMany({ where: { userId: { in: teamMemberIds }, shares: { none: { userId: uid } } }, select: yearReviewListSelect, orderBy: { updatedAt: 'desc' }, }), getTeamMemberIdsForAdminTeams, userId ); 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 = { user: { select: { id: true, name: true, email: true } }, items: { orderBy: [...([{ category: 'asc' }, { order: 'asc' }] as const)] }, shares: { include: { user: { select: { id: true, name: true, email: true } } } }, }; export async function getYearReviewSessionById(sessionId: string, userId: string) { return getSessionByIdGeneric( sessionId, userId, (sid, uid) => prisma.yearReviewSession.findFirst({ where: { id: sid, OR: [{ userId: uid }, { shares: { some: { userId: uid } } }] }, include: yearReviewByIdInclude, }), (sid) => prisma.yearReviewSession.findUnique({ where: { id: sid }, include: yearReviewByIdInclude }), (s) => resolveCollaborator(s.participant).then((r) => ({ resolvedParticipant: r })) ); } const yearReviewPermissions = createSessionPermissionChecks(prisma.yearReviewSession); const yearReviewShareEvents = createShareAndEventHandlers< | 'ITEM_CREATED' | 'ITEM_UPDATED' | 'ITEM_DELETED' | 'ITEM_MOVED' | 'ITEMS_REORDERED' | 'SESSION_UPDATED' >( prisma.yearReviewSession, prisma.yRSessionShare, prisma.yRSessionEvent, yearReviewPermissions.canAccess ); export const canAccessYearReviewSession = yearReviewPermissions.canAccess; export const canEditYearReviewSession = yearReviewPermissions.canEdit; export const canDeleteYearReviewSession = yearReviewPermissions.canDelete; export async function createYearReviewSession( userId: string, data: { title: string; participant: string; year: number } ) { return prisma.yearReviewSession.create({ data: { ...data, userId, }, include: { items: { orderBy: [{ category: 'asc' }, { order: 'asc' }], }, }, }); } export async function updateYearReviewSession( sessionId: string, userId: string, data: { title?: string; participant?: string; year?: number } ) { if (!(await canEditYearReviewSession(sessionId, userId))) { return { count: 0 }; } return prisma.yearReviewSession.updateMany({ where: { id: sessionId }, data, }); } export async function deleteYearReviewSession(sessionId: string, userId: string) { if (!(await canDeleteYearReviewSession(sessionId, userId))) { return { count: 0 }; } return prisma.yearReviewSession.deleteMany({ where: { id: sessionId }, }); } // ============================================ // Year Review Items CRUD // ============================================ export async function createYearReviewItem( sessionId: string, data: { content: string; category: YearReviewCategory } ) { // Get max order for this category in this session const maxOrder = await prisma.yearReviewItem.findFirst({ where: { sessionId, category: data.category }, orderBy: { order: 'desc' }, select: { order: true }, }); return prisma.yearReviewItem.create({ data: { ...data, sessionId, order: (maxOrder?.order ?? -1) + 1, }, }); } export async function updateYearReviewItem( itemId: string, data: { content?: string; category?: YearReviewCategory; order?: number } ) { return prisma.yearReviewItem.update({ where: { id: itemId }, data, }); } export async function deleteYearReviewItem(itemId: string) { return prisma.yearReviewItem.delete({ where: { id: itemId }, }); } export async function moveYearReviewItem( itemId: string, newCategory: YearReviewCategory, newOrder: number ) { return prisma.yearReviewItem.update({ where: { id: itemId }, data: { category: newCategory, order: newOrder, }, }); } export async function reorderYearReviewItems( sessionId: string, category: YearReviewCategory, itemIds: string[] ) { const updates = itemIds.map((id, index) => prisma.yearReviewItem.update({ where: { id }, data: { order: index }, }) ); return prisma.$transaction(updates); } // ============================================ // Session Sharing // ============================================ export const shareYearReviewSession = yearReviewShareEvents.share; export const removeYearReviewShare = yearReviewShareEvents.removeShare; export const getYearReviewSessionShares = yearReviewShareEvents.getShares; // ============================================ // Session Events (for real-time sync) // ============================================ export type YRSessionEventType = | 'ITEM_CREATED' | 'ITEM_UPDATED' | 'ITEM_DELETED' | 'ITEM_MOVED' | 'ITEMS_REORDERED' | 'SESSION_UPDATED'; export const createYearReviewSessionEvent = yearReviewShareEvents.createEvent; export const getYearReviewSessionEvents = yearReviewShareEvents.getEvents; export const getLatestYearReviewEventTimestamp = yearReviewShareEvents.getLatestEventTimestamp;