Files
workshop-manager/src/services/sessions.ts

346 lines
8.8 KiB
TypeScript

import { prisma } from '@/services/database';
import { resolveCollaborator } 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 type { SwotCategory, ShareRole } from '@prisma/client';
const sessionInclude = {
user: { select: { id: true, name: true, email: true } },
shares: { include: { user: { select: { id: true, name: true, email: true } } } },
_count: { select: { items: true, actions: true } },
};
// ============================================
// Session CRUD
// ============================================
export async function getSessionsByUserId(userId: string) {
return mergeSessionsByUserId(
(uid) =>
prisma.session.findMany({
where: { userId: uid },
include: sessionInclude,
orderBy: { updatedAt: 'desc' },
}),
(uid) =>
prisma.sessionShare.findMany({
where: { userId: uid },
include: { session: { include: sessionInclude } },
}),
userId,
(s) => resolveCollaborator(s.collaborator).then((r) => ({ resolvedCollaborator: r }))
);
}
/** Sessions owned by team members (where user is team admin) that are NOT shared with the user. */
export async function getTeamCollaboratorSessionsForAdmin(userId: string) {
return fetchTeamCollaboratorSessions(
(teamMemberIds, uid) =>
prisma.session.findMany({
where: { userId: { in: teamMemberIds }, shares: { none: { userId: uid } } },
include: sessionInclude,
orderBy: { updatedAt: 'desc' },
}),
getTeamMemberIdsForAdminTeams,
userId,
(s) => resolveCollaborator(s.collaborator).then((r) => ({ resolvedCollaborator: r }))
);
}
const sessionByIdInclude = {
user: { select: { id: true, name: true, email: true } },
items: { orderBy: { order: 'asc' } as const },
actions: {
include: { links: { include: { swotItem: true } } },
orderBy: { createdAt: 'asc' } as const,
},
shares: { include: { user: { select: { id: true, name: true, email: true } } } },
};
export async function getSessionById(sessionId: string, userId: string) {
return getSessionByIdGeneric(
sessionId,
userId,
(sid, uid) =>
prisma.session.findFirst({
where: { id: sid, OR: [{ userId: uid }, { shares: { some: { userId: uid } } }] },
include: sessionByIdInclude,
}),
(sid) => prisma.session.findUnique({ where: { id: sid }, include: sessionByIdInclude }),
(s) => resolveCollaborator(s.collaborator).then((r) => ({ resolvedCollaborator: r }))
);
}
const sessionPermissions = createSessionPermissionChecks(prisma.session);
const sessionShareEvents = createShareAndEventHandlers<
| 'ITEM_CREATED'
| 'ITEM_UPDATED'
| 'ITEM_DELETED'
| 'ITEM_MOVED'
| 'ACTION_CREATED'
| 'ACTION_UPDATED'
| 'ACTION_DELETED'
| 'SESSION_UPDATED'
>(prisma.session, prisma.sessionShare, prisma.sessionEvent, sessionPermissions.canAccess);
export const canAccessSession = sessionPermissions.canAccess;
export const canEditSession = sessionPermissions.canEdit;
export const canDeleteSession = sessionPermissions.canDelete;
export async function createSession(userId: string, data: { title: string; collaborator: string }) {
return prisma.session.create({
data: {
...data,
userId,
},
});
}
export async function updateSession(
sessionId: string,
userId: string,
data: { title?: string; collaborator?: string }
) {
if (!(await canEditSession(sessionId, userId))) {
return { count: 0 };
}
return prisma.session.updateMany({
where: { id: sessionId },
data,
});
}
export async function deleteSession(sessionId: string, userId: string) {
if (!(await canDeleteSession(sessionId, userId))) {
return { count: 0 };
}
return prisma.session.deleteMany({
where: { id: sessionId },
});
}
// ============================================
// SWOT Items CRUD
// ============================================
export async function createSwotItem(
sessionId: string,
data: { content: string; category: SwotCategory }
) {
// Get max order for this category
const maxOrder = await prisma.swotItem.aggregate({
where: { sessionId, category: data.category },
_max: { order: true },
});
return prisma.swotItem.create({
data: {
...data,
sessionId,
order: (maxOrder._max.order ?? -1) + 1,
},
});
}
export async function updateSwotItem(
itemId: string,
data: { content?: string; category?: SwotCategory; order?: number }
) {
return prisma.swotItem.update({
where: { id: itemId },
data,
});
}
export async function deleteSwotItem(itemId: string) {
return prisma.swotItem.delete({
where: { id: itemId },
});
}
export async function duplicateSwotItem(itemId: string) {
const original = await prisma.swotItem.findUnique({
where: { id: itemId },
});
if (!original) {
throw new Error('Item not found');
}
// Get max order for this category
const maxOrder = await prisma.swotItem.aggregate({
where: { sessionId: original.sessionId, category: original.category },
_max: { order: true },
});
return prisma.swotItem.create({
data: {
content: original.content,
category: original.category,
sessionId: original.sessionId,
order: (maxOrder._max.order ?? -1) + 1,
},
});
}
export async function reorderSwotItems(
sessionId: string,
category: SwotCategory,
itemIds: string[]
) {
const updates = itemIds.map((id, index) =>
prisma.swotItem.update({
where: { id },
data: { order: index },
})
);
return prisma.$transaction(updates);
}
export async function moveSwotItem(itemId: string, newCategory: SwotCategory, newOrder: number) {
return prisma.swotItem.update({
where: { id: itemId },
data: {
category: newCategory,
order: newOrder,
},
});
}
// ============================================
// Actions CRUD
// ============================================
export async function createAction(
sessionId: string,
data: {
title: string;
description?: string;
priority?: number;
linkedItemIds: string[];
}
) {
return prisma.action.create({
data: {
title: data.title,
description: data.description,
priority: data.priority ?? 0,
sessionId,
links: {
create: data.linkedItemIds.map((swotItemId) => ({
swotItemId,
})),
},
},
include: {
links: {
include: {
swotItem: true,
},
},
},
});
}
export async function updateAction(
actionId: string,
data: {
title?: string;
description?: string;
priority?: number;
status?: string;
dueDate?: Date | null;
linkedItemIds?: string[];
}
) {
const { linkedItemIds, ...updateData } = data;
// If linkedItemIds is provided, update the links
if (linkedItemIds !== undefined) {
// Delete all existing links
await prisma.actionLink.deleteMany({
where: { actionId },
});
// Create new links
if (linkedItemIds.length > 0) {
await prisma.actionLink.createMany({
data: linkedItemIds.map((swotItemId) => ({
actionId,
swotItemId,
})),
});
}
}
return prisma.action.update({
where: { id: actionId },
data: updateData,
include: {
links: {
include: {
swotItem: true,
},
},
},
});
}
export async function deleteAction(actionId: string) {
return prisma.action.delete({
where: { id: actionId },
});
}
export async function linkItemToAction(actionId: string, swotItemId: string) {
return prisma.actionLink.create({
data: {
actionId,
swotItemId,
},
});
}
export async function unlinkItemFromAction(actionId: string, swotItemId: string) {
return prisma.actionLink.deleteMany({
where: {
actionId,
swotItemId,
},
});
}
// ============================================
// Session Sharing
// ============================================
export const shareSession = sessionShareEvents.share;
export const removeShare = sessionShareEvents.removeShare;
export const getSessionShares = sessionShareEvents.getShares;
// ============================================
// Session Events (for real-time sync)
// ============================================
export type SessionEventType =
| 'ITEM_CREATED'
| 'ITEM_UPDATED'
| 'ITEM_DELETED'
| 'ITEM_MOVED'
| 'ACTION_CREATED'
| 'ACTION_UPDATED'
| 'ACTION_DELETED'
| 'SESSION_UPDATED';
export const createSessionEvent = sessionShareEvents.createEvent;
export const getSessionEvents = sessionShareEvents.getEvents;
export const getLatestEventTimestamp = sessionShareEvents.getLatestEventTimestamp;