346 lines
8.8 KiB
TypeScript
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;
|