Files
workshop-manager/src/services/weekly-checkin.ts
Julien Froidefond 53ee344ae7
All checks were successful
Deploy with Docker Compose / deploy (push) Successful in 6m24s
feat: add Weekly Check-in feature with models, UI components, and session management for enhanced team collaboration
2026-01-14 10:23:58 +01:00

372 lines
9.4 KiB
TypeScript

import { prisma } from '@/services/database';
import { resolveCollaborator } from '@/services/auth';
import type { ShareRole, WeeklyCheckInCategory, Emotion } from '@prisma/client';
// ============================================
// Weekly Check-in Session CRUD
// ============================================
export async function getWeeklyCheckInSessionsByUserId(userId: string) {
// Get owned sessions + shared sessions
const [owned, shared] = await Promise.all([
prisma.weeklyCheckInSession.findMany({
where: { userId },
include: {
user: { select: { id: true, name: true, email: true } },
shares: {
include: {
user: { select: { id: true, name: true, email: true } },
},
},
_count: {
select: {
items: true,
},
},
},
orderBy: { updatedAt: 'desc' },
}),
prisma.wCISessionShare.findMany({
where: { userId },
include: {
session: {
include: {
user: { select: { id: true, name: true, email: true } },
shares: {
include: {
user: { select: { id: true, name: true, email: true } },
},
},
_count: {
select: {
items: true,
},
},
},
},
},
}),
]);
// Mark owned sessions and merge with shared
const ownedWithRole = owned.map((s) => ({
...s,
isOwner: true as const,
role: 'OWNER' as const,
}));
const sharedWithRole = shared.map((s) => ({
...s.session,
isOwner: false as const,
role: s.role,
sharedAt: s.createdAt,
}));
const allSessions = [...ownedWithRole, ...sharedWithRole].sort(
(a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()
);
// Resolve participants to users
const sessionsWithResolved = await Promise.all(
allSessions.map(async (s) => ({
...s,
resolvedParticipant: await resolveCollaborator(s.participant),
}))
);
return sessionsWithResolved;
}
export async function getWeeklyCheckInSessionById(sessionId: string, userId: string) {
// Check if user owns the session OR has it shared
const session = await prisma.weeklyCheckInSession.findFirst({
where: {
id: sessionId,
OR: [
{ userId }, // Owner
{ shares: { some: { userId } } }, // Shared with user
],
},
include: {
user: { select: { id: true, name: true, email: true } },
items: {
orderBy: [{ category: 'asc' }, { order: 'asc' }],
},
shares: {
include: {
user: { select: { id: true, name: true, email: true } },
},
},
},
});
if (!session) return null;
// Determine user's role
const isOwner = session.userId === userId;
const share = session.shares.find((s) => s.userId === userId);
const role = isOwner ? ('OWNER' as const) : share?.role || ('VIEWER' as const);
const canEdit = isOwner || role === 'EDITOR';
// Resolve participant to user if it's an email
const resolvedParticipant = await resolveCollaborator(session.participant);
return { ...session, isOwner, role, canEdit, resolvedParticipant };
}
// Check if user can access session (owner or shared)
export async function canAccessWeeklyCheckInSession(sessionId: string, userId: string) {
const count = await prisma.weeklyCheckInSession.count({
where: {
id: sessionId,
OR: [{ userId }, { shares: { some: { userId } } }],
},
});
return count > 0;
}
// Check if user can edit session (owner or EDITOR role)
export async function canEditWeeklyCheckInSession(sessionId: string, userId: string) {
const count = await prisma.weeklyCheckInSession.count({
where: {
id: sessionId,
OR: [{ userId }, { shares: { some: { userId, role: 'EDITOR' } } }],
},
});
return count > 0;
}
export async function createWeeklyCheckInSession(
userId: string,
data: { title: string; participant: string; date?: Date }
) {
return prisma.weeklyCheckInSession.create({
data: {
...data,
date: data.date || new Date(),
userId,
},
include: {
items: {
orderBy: [{ category: 'asc' }, { order: 'asc' }],
},
},
});
}
export async function updateWeeklyCheckInSession(
sessionId: string,
userId: string,
data: { title?: string; participant?: string; date?: Date }
) {
return prisma.weeklyCheckInSession.updateMany({
where: { id: sessionId, userId },
data,
});
}
export async function deleteWeeklyCheckInSession(sessionId: string, userId: string) {
return prisma.weeklyCheckInSession.deleteMany({
where: { id: sessionId, userId },
});
}
// ============================================
// Weekly Check-in Items CRUD
// ============================================
export async function createWeeklyCheckInItem(
sessionId: string,
data: { content: string; category: WeeklyCheckInCategory; emotion?: Emotion }
) {
// Get max order for this category in this session
const maxOrder = await prisma.weeklyCheckInItem.findFirst({
where: { sessionId, category: data.category },
orderBy: { order: 'desc' },
select: { order: true },
});
return prisma.weeklyCheckInItem.create({
data: {
...data,
emotion: data.emotion || 'NONE',
sessionId,
order: (maxOrder?.order ?? -1) + 1,
},
});
}
export async function updateWeeklyCheckInItem(
itemId: string,
data: { content?: string; category?: WeeklyCheckInCategory; emotion?: Emotion; order?: number }
) {
return prisma.weeklyCheckInItem.update({
where: { id: itemId },
data,
});
}
export async function deleteWeeklyCheckInItem(itemId: string) {
return prisma.weeklyCheckInItem.delete({
where: { id: itemId },
});
}
export async function moveWeeklyCheckInItem(
itemId: string,
newCategory: WeeklyCheckInCategory,
newOrder: number
) {
return prisma.weeklyCheckInItem.update({
where: { id: itemId },
data: {
category: newCategory,
order: newOrder,
},
});
}
export async function reorderWeeklyCheckInItems(
sessionId: string,
category: WeeklyCheckInCategory,
itemIds: string[]
) {
const updates = itemIds.map((id, index) =>
prisma.weeklyCheckInItem.update({
where: { id },
data: { order: index },
})
);
return prisma.$transaction(updates);
}
// ============================================
// Session Sharing
// ============================================
export async function shareWeeklyCheckInSession(
sessionId: string,
ownerId: string,
targetEmail: string,
role: ShareRole = 'EDITOR'
) {
// Verify owner
const session = await prisma.weeklyCheckInSession.findFirst({
where: { id: sessionId, userId: ownerId },
});
if (!session) {
throw new Error('Session not found or not owned');
}
// Find target user
const targetUser = await prisma.user.findUnique({
where: { email: targetEmail },
});
if (!targetUser) {
throw new Error('User not found');
}
// Can't share with yourself
if (targetUser.id === ownerId) {
throw new Error('Cannot share session with yourself');
}
// Create or update share
return prisma.wCISessionShare.upsert({
where: {
sessionId_userId: { sessionId, userId: targetUser.id },
},
update: { role },
create: {
sessionId,
userId: targetUser.id,
role,
},
include: {
user: { select: { id: true, name: true, email: true } },
},
});
}
export async function removeWeeklyCheckInShare(
sessionId: string,
ownerId: string,
shareUserId: string
) {
// Verify owner
const session = await prisma.weeklyCheckInSession.findFirst({
where: { id: sessionId, userId: ownerId },
});
if (!session) {
throw new Error('Session not found or not owned');
}
return prisma.wCISessionShare.deleteMany({
where: { sessionId, userId: shareUserId },
});
}
export async function getWeeklyCheckInSessionShares(sessionId: string, userId: string) {
// Verify access
if (!(await canAccessWeeklyCheckInSession(sessionId, userId))) {
throw new Error('Access denied');
}
return prisma.wCISessionShare.findMany({
where: { sessionId },
include: {
user: { select: { id: true, name: true, email: true } },
},
});
}
// ============================================
// Session Events (for real-time sync)
// ============================================
export type WCISessionEventType =
| 'ITEM_CREATED'
| 'ITEM_UPDATED'
| 'ITEM_DELETED'
| 'ITEM_MOVED'
| 'ITEMS_REORDERED'
| 'SESSION_UPDATED';
export async function createWeeklyCheckInSessionEvent(
sessionId: string,
userId: string,
type: WCISessionEventType,
payload: Record<string, unknown>
) {
return prisma.wCISessionEvent.create({
data: {
sessionId,
userId,
type,
payload: JSON.stringify(payload),
},
});
}
export async function getWeeklyCheckInSessionEvents(sessionId: string, since?: Date) {
return prisma.wCISessionEvent.findMany({
where: {
sessionId,
...(since && { createdAt: { gt: since } }),
},
include: {
user: { select: { id: true, name: true, email: true } },
},
orderBy: { createdAt: 'asc' },
});
}
export async function getLatestWeeklyCheckInEventTimestamp(sessionId: string) {
const event = await prisma.wCISessionEvent.findFirst({
where: { sessionId },
orderBy: { createdAt: 'desc' },
select: { createdAt: true },
});
return event?.createdAt;
}