Skip to content
11 changes: 11 additions & 0 deletions src/backend/src/controllers/calendar.controllers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -498,6 +498,17 @@ export default class CalendarController {
}
}

static async getSingleEventWithMembers(req: Request, res: Response, next: NextFunction) {
try {
const { eventId } = req.params;

const event = await CalendarService.getSingleEventWithMembers(req.currentUser, eventId, req.organization);
res.status(200).json(event);
} catch (error: unknown) {
next(error);
}
}

static async getConflictingEvent(req: Request, res: Response, next: NextFunction) {
try {
const { eventId } = req.params;
Expand Down
66 changes: 66 additions & 0 deletions src/backend/src/prisma-query-args/event.query-args.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { getUserQueryArgs, getUserWithSettingsQueryArgs } from './user.query-arg

export type EventQueryArgs = ReturnType<typeof getEventQueryArgs>;

export type EventWithMembersQueryArgs = ReturnType<typeof getEventWithMembersQueryArgs>;

export const getEventQueryArgs = (organizationId: string) =>
Prisma.validator<Prisma.EventDefaultArgs>()({
include: {
Expand Down Expand Up @@ -60,3 +62,67 @@ export const getEventQueryArgs = (organizationId: string) =>
documents: true
}
});

export const getEventWithMembersQueryArgs = (organizationId: string) =>
Prisma.validator<Prisma.EventDefaultArgs>()({
include: {
userCreated: getUserWithSettingsQueryArgs(organizationId),
requiredMembers: getUserQueryArgs(organizationId),
optionalMembers: getUserQueryArgs(organizationId),
confirmedMembers: getUserWithSettingsQueryArgs(organizationId),
deniedMembers: getUserQueryArgs(organizationId),
teams: {
include: {
members: getUserQueryArgs(organizationId),
leads: getUserQueryArgs(organizationId),
head: getUserQueryArgs(organizationId)
}
},
teamType: {
include: {
teams: {
include: {
members: getUserQueryArgs(organizationId),
leads: getUserQueryArgs(organizationId),
head: getUserQueryArgs(organizationId)
}
}
}
},
shops: {
select: {
name: true,
shopId: true
}
},
machinery: {
select: {
name: true,
machineryId: true
}
},
workPackages: {
select: {
wbsElement: {
select: {
name: true,
carNumber: true,
projectNumber: true,
workPackageNumber: true
}
},
project: {
include: {
wbsElement: true,
teams: true
}
},
workPackageId: true
}
},
approvalRequiredBy: getUserQueryArgs(organizationId),
scheduledTimes: true,
notificationSlackThreads: true,
documents: true
}
});
2 changes: 2 additions & 0 deletions src/backend/src/routes/calendar.routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,8 @@ calendarRouter.get('/event/:eventId/conflict', CalendarController.getConflicting

calendarRouter.get('/event/:eventId', CalendarController.getSingleEvent);

calendarRouter.get('/event-members/:eventId', CalendarController.getSingleEventWithMembers);

calendarRouter.get('/events', CalendarController.getAllEvents);

calendarRouter.get('/event-types', CalendarController.getAllEventTypes);
Expand Down
34 changes: 30 additions & 4 deletions src/backend/src/services/calendar.services.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import { calendarTransformer, eventTransformer, machineryTransformer } from '../transformers/calendar.transformer';
import {
calendarTransformer,
eventTransformer,
eventWithMembersTransformer,
machineryTransformer
} from '../transformers/calendar.transformer';
import { getMachineryQueryArgs } from '../prisma-query-args/machinery.query-args';
import { Conflict_Status, Event_Status, Organization } from '@prisma/client';
import {
Expand Down Expand Up @@ -39,7 +44,7 @@ import { getEventTypeQueryArgs } from '../prisma-query-args/event-type.query-arg
import { shopTransformer } from '../transformers/calendar.transformer';
import { getShopQueryArgs } from '../prisma-query-args/shop.query-args';
import { getCalendarQueryArgs } from '../prisma-query-args/calendar.query-args';
import { getEventQueryArgs } from '../prisma-query-args/event.query-args';
import { getEventQueryArgs, getEventWithMembersQueryArgs } from '../prisma-query-args/event.query-args';
import {
buildScheduledTimesOverlap,
checkEventConflicts,
Expand Down Expand Up @@ -1144,13 +1149,13 @@ export default class CalendarService {
): Promise<Event> {
const event = await prisma.event.findUnique({
where: { eventId },
...getEventQueryArgs(organization.organizationId)
...getEventWithMembersQueryArgs(organization.organizationId)
});

if (!event) throw new NotFoundException('Event', eventId);
if (event.dateDeleted) throw new DeletedException('Event', eventId);

if (!isUserOnEvent(submitter, eventTransformer(event)))
if (!isUserOnEvent(submitter, eventWithMembersTransformer(event)))
throw new HttpException(400, 'Current user is not in the list of this events members');

let userSettings = await prisma.schedule_Settings.findUnique({
Expand Down Expand Up @@ -2374,6 +2379,27 @@ export default class CalendarService {
return eventTransformer(event);
}

/**
* Retrieves a single event
*
* @param submitter the user who is trying to retrieve the event
* @param eventId the id of the event to retrieve
* @param organizationId the organization that the user is currently in
* @returns the event
*/
static async getSingleEventWithMembers(_submitter: User, eventId: string, organization: Organization): Promise<Event> {
const event = await prisma.event.findUnique({
where: { eventId },
...getEventWithMembersQueryArgs(organization.organizationId)
});

if (!event) throw new NotFoundException('Event', eventId);

if (event.dateDeleted) throw new DeletedException('Event', eventId);

return eventTransformer(event);
}

/**
* Retrieves a potential conflicting event
* If no conflict exists, returns the original event
Expand Down
48 changes: 46 additions & 2 deletions src/backend/src/transformers/calendar.transformer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,14 @@ import {
EventPreview,
DayOfWeek,
ConflictStatus,
Document
Document,
EventWithMembers
} from 'shared';
import { MachineryQueryArgs, ShopMachineryQueryArgs } from '../prisma-query-args/machinery.query-args';
import { userTransformer, userWithScheduleSettingsTransformer } from './user.transformer';
import { EventTypeQueryArgs } from '../prisma-query-args/event-type.query-args';
import { CalendarQueryArgs } from '../prisma-query-args/calendar.query-args';
import { EventQueryArgs } from '../prisma-query-args/event.query-args';
import { EventQueryArgs, EventWithMembersQueryArgs } from '../prisma-query-args/event.query-args';
import { ShopQueryArgs } from '../prisma-query-args/shop.query-args';

export const documentTransformer = (document: Prisma.DocumentGetPayload<null>): Document => {
Expand Down Expand Up @@ -140,6 +141,49 @@ export const eventTransformer = (event: Prisma.EventGetPayload<EventQueryArgs>):
};
};

export const eventWithMembersTransformer = (event: Prisma.EventGetPayload<EventWithMembersQueryArgs>): EventWithMembers => {
return {
eventId: event.eventId,
title: event.title,
userCreated: userWithScheduleSettingsTransformer(event.userCreated),
dateCreated: event.dateCreated,
eventTypeId: event.eventTypeId,
requiredMembers: event.requiredMembers.map(userTransformer),
optionalMembers: event.optionalMembers.map(userTransformer),
confirmedMembers: event.confirmedMembers.map(userWithScheduleSettingsTransformer),
deniedMembers: event.deniedMembers.map(userTransformer),
teams: event.teams.map((team) => ({
...team,
members: team.members.map(userTransformer),
leads: team.leads.map(userTransformer),
head: userTransformer(team.head)
})),
teamType: event.teamType
? {
teamTypeId: event.teamType.teamTypeId,
name: event.teamType.name,
teams: event.teamType.teams.map((team) => ({
members: team.members.map(userTransformer),
leads: team.leads.map(userTransformer),
head: userTransformer(team.head)
}))
}
: undefined,
shops: event.shops,
machinery: event.machinery,
workPackages: event.workPackages,
documents: event.documents.filter((document) => !document.dateDeleted).map(documentTransformer),
scheduledTimes: event.scheduledTimes.map(scheduleTimesTransformer),
approved: conflictStatusTransformer(event.approved),
approvalRequiredFrom: event.approvalRequiredBy ?? undefined,
location: event.location ?? undefined,
zoomLink: event.zoomLink ?? undefined,
questionDocumentLink: event.questionDocumentLink ?? undefined,
description: event.description ?? undefined,
status: eventStatusTransformer(event.status)
};
};

export const eventPreviewTransformer = (event: Prisma.EventGetPayload<EventQueryArgs>, wbsName: string): EventPreview => {
// Get the earliest scheduled date from scheduledTimes
const dateScheduled = event.scheduledTimes.length > 0 ? event.scheduledTimes[0].initialDateScheduled : new Date();
Expand Down
55 changes: 50 additions & 5 deletions src/backend/src/utils/calendar.utils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
import { Prisma, Event_Type, Organization } from '@prisma/client';
import { User, ScheduleSlotCreateArgs, Event, EventDocumentCreateArgs, Document, ConflictStatus } from 'shared';
import {
User,
ScheduleSlotCreateArgs,
EventDocumentCreateArgs,
Document,
ConflictStatus,
Event,
EventWithMembers
} from 'shared';
import { InvalidEventTypeConfigurationException } from './errors.utils';
import prisma from '../prisma/prisma';
import { getEventQueryArgs } from '../prisma-query-args/event.query-args';
Expand All @@ -15,10 +23,47 @@ export function buildScheduledTimesOverlap(start?: Date, end?: Date): Prisma.Sch
return { some: { AND } };
}

export const isUserOnEvent = (user: User, event: Event): boolean => {
const requiredMembers = event.requiredMembers.map((user) => user.userId);
const optionalMembers = event.optionalMembers.map((user) => user.userId);
return requiredMembers.includes(user.userId) || optionalMembers.includes(user.userId);
export const isUserOnEvent = (user: User, event: EventWithMembers): boolean => {
// Check if user is directly a required or optional member
const isDirectMember =
event.requiredMembers.some((member) => member.userId === user.userId) ||
event.optionalMembers.some((member) => member.userId === user.userId);

if (isDirectMember) {
return true;
}

// Check if user is on any of the event's teams (as member, lead, or head)
const isOnEventTeam = event.teams.some(
(team) =>
team.members.some((member) => member.userId === user.userId) ||
team.leads.some((lead) => lead.userId === user.userId) ||
team.head.userId === user.userId
);

if (isOnEventTeam) {
return true;
}

// Check if user is on any team that belongs to the event's team type
if (event.teamType) {
const isOnTeamType = event.teamType.teams.some(
(team) =>
team.members.some((member) => member.userId === user.userId) ||
team.leads.some((lead) => lead.userId === user.userId) ||
team.head.userId === user.userId
);

if (isOnTeamType) {
return true;
}
}

if (event.userCreated.userId === user.userId) {
return true;
}

return false;
};

/**
Expand Down
6 changes: 6 additions & 0 deletions src/frontend/src/apis/calendar.api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,12 @@ export const getSingleEvent = async (id: string) => {
});
};

export const getSingleEventWithMembers = async (id: string) => {
return axios.get(apiUrls.calendarGetSingleEventWithMembers(id), {
transformResponse: (data) => eventTransformer(JSON.parse(data))
});
};

export const getConflictingEvent = async (id: string) => {
return axios.get(apiUrls.calendarGetConflictingEvent(id), {
transformResponse: (data) => eventTransformer(JSON.parse(data))
Expand Down
16 changes: 15 additions & 1 deletion src/frontend/src/apis/transformers/calendar.transformer.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Shop, Event, EventPreview } from 'shared';
import { Shop, Event, EventPreview, EventWithMembers } from 'shared';
import { userTransformer } from './users.transformers';

export const shopTransformer = (shop: Shop): Shop => {
Expand Down Expand Up @@ -27,6 +27,20 @@ export const eventTransformer = (event: Event): Event => {
};
};

export const eventWithMembersTransformer = (event: EventWithMembers): EventWithMembers => {
return {
...event,
dateCreated: new Date(event.dateCreated),
scheduledTimes: event.scheduledTimes.map((slot: any) => ({
...slot,
startTime: slot.startTime ? new Date(slot.startTime) : undefined,
endTime: slot.endTime ? new Date(slot.endTime) : undefined,
initialDateScheduled: new Date(slot.initialDateScheduled),
endDate: new Date(slot.endDate)
}))
};
};

export const eventPreviewTransformer = (event: EventPreview): EventPreview => {
return {
eventId: event.eventId,
Expand Down
2 changes: 1 addition & 1 deletion src/frontend/src/app/AppAuthenticated.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ import { useHomePageContext } from './HomePageContext';
import { useCurrentOrganization } from '../hooks/organizations.hooks';
import Statistics from '../pages/StatisticsPage/Statistics';
import RetrospectiveGanttChartPage from '../pages/RetrospectivePage/Retrospective';
import NewCalendar from '../pages/NewCalendarPage/NewCalendar';
import NewCalendar from '../pages/CalendarPage/NewCalendar';

interface AppAuthenticatedProps {
userId: string;
Expand Down
20 changes: 18 additions & 2 deletions src/frontend/src/hooks/calendar.hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ import {
EventStatus,
EventType,
FilterArgs,
ScheduleSlotCreateArgs
ScheduleSlotCreateArgs,
EventWithMembers
} from 'shared';
import {
getAllShops,
Expand Down Expand Up @@ -41,7 +42,8 @@ import {
postCreateEvent,
uploadSingleDocument,
downloadDocumentPdf,
postEditEvent
postEditEvent,
getSingleEventWithMembers
} from '../apis/calendar.api';
import { useCurrentUser } from './users.hooks';
import { PDFDocument } from 'pdf-lib';
Expand Down Expand Up @@ -311,6 +313,9 @@ export const useMarkUserConfirmed = (id: string) => {
onSuccess: () => {
queryClient.invalidateQueries(EVENT_KEY);
queryClient.invalidateQueries(['users', user.userId, 'schedule-settings']);
queryClient.invalidateQueries(['users', user.userId, 'schedule-settings']);
queryClient.invalidateQueries(['users', 'many-with-schedule-settings']);
queryClient.invalidateQueries(['users']);
}
}
);
Expand Down Expand Up @@ -342,6 +347,17 @@ export const useSingleEvent = (id?: string) => {
);
};

export const useSingleEventWithMembers = (id?: string) => {
return useQuery<EventWithMembers, Error>(
['events', id, 'with-members'],
async () => {
const { data } = await getSingleEventWithMembers(id!);
return data;
},
{ enabled: !!id }
);
};

export const useConflictingEvents = (ids: string[]) => {
return useQuery<Event[], Error>(['events', 'conflicting', ids], async () => {
const results = await Promise.all(
Expand Down
Loading