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 { MotivatorType } from '@prisma/client'; const motivatorListSelect = { id: true, title: true, participant: 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: { cards: true } }, } as const; // ============================================ // Moving Motivators Session CRUD // ============================================ export async function getMotivatorSessionsByUserId(userId: string) { return unstable_cache( async () => { const sessions = await mergeSessionsByUserId( (uid) => prisma.movingMotivatorsSession.findMany({ where: { userId: uid }, select: motivatorListSelect, orderBy: { updatedAt: 'desc' }, }), (uid) => prisma.mMSessionShare.findMany({ where: { userId: uid }, select: { role: true, createdAt: true, session: { select: motivatorListSelect } }, }), 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 }, })); }, [`motivator-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.movingMotivatorsSession.findMany({ where: { userId: { in: teamMemberIds }, shares: { none: { userId: uid } } }, select: motivatorListSelect, 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 motivatorByIdInclude = { user: { select: { id: true, name: true, email: true } }, cards: { orderBy: { orderIndex: 'asc' } as const }, shares: { include: { user: { select: { id: true, name: true, email: true } } } }, }; export async function getMotivatorSessionById(sessionId: string, userId: string) { return getSessionByIdGeneric( sessionId, userId, (sid, uid) => prisma.movingMotivatorsSession.findFirst({ where: { id: sid, OR: [{ userId: uid }, { shares: { some: { userId: uid } } }] }, include: motivatorByIdInclude, }), (sid) => prisma.movingMotivatorsSession.findUnique({ where: { id: sid }, include: motivatorByIdInclude, }), (s) => resolveCollaborator(s.participant).then((r) => ({ resolvedParticipant: r })) ); } const motivatorPermissions = createSessionPermissionChecks(prisma.movingMotivatorsSession); const motivatorShareEvents = createShareAndEventHandlers< 'CARD_MOVED' | 'CARD_INFLUENCE_CHANGED' | 'CARDS_REORDERED' | 'SESSION_UPDATED' >( prisma.movingMotivatorsSession, prisma.mMSessionShare, prisma.mMSessionEvent, motivatorPermissions.canAccess ); export const canAccessMotivatorSession = motivatorPermissions.canAccess; export const canEditMotivatorSession = motivatorPermissions.canEdit; export const canDeleteMotivatorSession = motivatorPermissions.canDelete; const DEFAULT_MOTIVATOR_TYPES: MotivatorType[] = [ 'STATUS', 'POWER', 'ORDER', 'ACCEPTANCE', 'HONOR', 'MASTERY', 'SOCIAL', 'FREEDOM', 'CURIOSITY', 'PURPOSE', ]; export async function createMotivatorSession( userId: string, data: { title: string; participant: string } ) { // Create session with all 10 cards initialized return prisma.movingMotivatorsSession.create({ data: { ...data, userId, cards: { create: DEFAULT_MOTIVATOR_TYPES.map((type, index) => ({ type, orderIndex: index + 1, influence: 0, })), }, }, include: { cards: { orderBy: { orderIndex: 'asc' }, }, }, }); } export async function updateMotivatorSession( sessionId: string, userId: string, data: { title?: string; participant?: string } ) { if (!(await canEditMotivatorSession(sessionId, userId))) { return { count: 0 }; } return prisma.movingMotivatorsSession.updateMany({ where: { id: sessionId }, data, }); } export async function deleteMotivatorSession(sessionId: string, userId: string) { if (!(await canDeleteMotivatorSession(sessionId, userId))) { return { count: 0 }; } return prisma.movingMotivatorsSession.deleteMany({ where: { id: sessionId }, }); } // ============================================ // Motivator Cards CRUD // ============================================ export async function updateMotivatorCard( cardId: string, data: { orderIndex?: number; influence?: number } ) { return prisma.motivatorCard.update({ where: { id: cardId }, data, }); } export async function reorderMotivatorCards(sessionId: string, cardIds: string[]) { const updates = cardIds.map((id, index) => prisma.motivatorCard.update({ where: { id }, data: { orderIndex: index + 1 }, }) ); return prisma.$transaction(updates); } export async function updateCardInfluence(cardId: string, influence: number) { // Clamp influence between -3 and +3 const clampedInfluence = Math.max(-3, Math.min(3, influence)); return prisma.motivatorCard.update({ where: { id: cardId }, data: { influence: clampedInfluence }, }); } // ============================================ // Session Sharing // ============================================ export const shareMotivatorSession = motivatorShareEvents.share; export const removeMotivatorShare = motivatorShareEvents.removeShare; export const getMotivatorSessionShares = motivatorShareEvents.getShares; // ============================================ // Session Events (for real-time sync) // ============================================ export type MMSessionEventType = | 'CARD_MOVED' | 'CARD_INFLUENCE_CHANGED' | 'CARDS_REORDERED' | 'SESSION_UPDATED'; export const createMotivatorSessionEvent = motivatorShareEvents.createEvent; export const getMotivatorSessionEvents = motivatorShareEvents.getEvents; export const getLatestMotivatorEventTimestamp = motivatorShareEvents.getLatestEventTimestamp;