Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
215 changes: 182 additions & 33 deletions src/backend/src/services/calendar.services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import {
Machinery,
ScheduleSlot,
notGuest,
isSameDay,
isSameDayUTC,
EventInstance
} from 'shared';
import { getCalendarQueryArgs } from '../prisma-query-args/calendar.query-args.js';
Expand Down Expand Up @@ -67,7 +67,7 @@ import {
updateUserAvailability,
areUsersinList
} from '../utils/users.utils.js';
import { Conflict_Status, Event_Status, Organization, Team } from '@prisma/client';
import { $Enums, Conflict_Status, Event_Status, Organization, Team } from '@prisma/client';

export default class CalendarService {
/**
Expand Down Expand Up @@ -311,17 +311,16 @@ export default class CalendarService {
});

// Validate required memberIds
if (requiredMemberIds.length > 0) {
const foundMembers = await prisma.user.findMany({
where: {
userId: { in: requiredMemberIds },
organizations: { some: { organizationId: organization.organizationId } }
}
});
if (foundMembers.length !== requiredMemberIds.length) {
const missingIds = requiredMemberIds.filter((id) => !foundMembers.some((user) => user.userId === id));
throw new NotFoundException('User', missingIds.join(', '));

const foundMembers = await prisma.user.findMany({
where: {
userId: { in: requiredMemberIds || submitter.userId },
organizations: { some: { organizationId: organization.organizationId } }
}
});
if (foundMembers.length !== requiredMemberIds.length) {
const missingIds = requiredMemberIds.filter((id) => !foundMembers.some((user) => user.userId === id));
throw new NotFoundException('User', missingIds.join(', '));
}

// Validate optionals memberIds
Expand Down Expand Up @@ -422,14 +421,19 @@ export default class CalendarService {
// Check for conflicts using expanded slots
const { hasConflict, conflictingEvent } = await checkEventConflicts(scheduleSlots, organization, location, undefined);

const allRequiredMembers = [
...requiredMemberIds,
...(requiredMemberIds.includes(submitter.userId) ? [] : [submitter.userId])
];

const newEvent = await prisma.event.create({
data: {
userCreatedId: submitter.userId,
dateCreated: new Date(),
title,
eventTypeId,
requiredMembers: {
connect: requiredMemberIds.map((userId) => ({ userId }))
connect: allRequiredMembers.map((userId) => ({ userId }))
},
optionalMembers: {
connect: optionalMemberIds.map((userId) => ({ userId }))
Expand Down Expand Up @@ -471,7 +475,7 @@ export default class CalendarService {
let calendarEventIds: string[] = [];
if (process.env.NODE_ENV === 'production') {
try {
const allMemberIds = [...requiredMemberIds, ...optionalMemberIds];
const allMemberIds = [...allRequiredMembers, ...optionalMemberIds];
const isInPerson = !!location;

calendarEventIds = await createCalendarEvent(
Expand All @@ -496,7 +500,7 @@ export default class CalendarService {

if (foundEventType.sendSlackNotifications) {
const members = await prisma.user.findMany({
where: { userId: { in: optionalMemberIds.concat(requiredMemberIds) } }
where: { userId: { in: optionalMemberIds.concat(allRequiredMembers) } }
});

// get the user settings for all the members invited, who are leaderingship
Expand Down Expand Up @@ -633,17 +637,16 @@ export default class CalendarService {
}

// Validate required memberIds
if (requiredMemberIds.length > 0) {
const foundMembers = await prisma.user.findMany({
where: {
userId: { in: requiredMemberIds },
organizations: { some: { organizationId: organization.organizationId } }
}
});
if (foundMembers.length !== requiredMemberIds.length) {
const missingIds = requiredMemberIds.filter((id) => !foundMembers.some((user) => user.userId === id));
throw new NotFoundException('User', missingIds.join(', '));

const foundMembers = await prisma.user.findMany({
where: {
userId: { in: requiredMemberIds || submitter.userId },
organizations: { some: { organizationId: organization.organizationId } }
}
});
if (foundMembers.length !== requiredMemberIds.length) {
const missingIds = requiredMemberIds.filter((id) => !foundMembers.some((user) => user.userId === id));
throw new NotFoundException('User', missingIds.join(', '));
}

// Validate optional memberIds
Expand Down Expand Up @@ -741,8 +744,13 @@ export default class CalendarService {
}
}

const allRequiredMembers = [
...requiredMemberIds,
...(requiredMemberIds.includes(submitter.userId) ? [] : [submitter.userId])
];

// throw if a user isn't found, then build prisma queries for connecting userIds
const updatedRequiredMembers = getPrismaQueryUserIds(await getUsers(requiredMemberIds));
const updatedRequiredMembers = getPrismaQueryUserIds(await getUsers(allRequiredMembers));
const updatedOptionalMembers = getPrismaQueryUserIds(await getUsers(optionalMemberIds));

// Update the event with new data (excluding schedule slots)
Expand Down Expand Up @@ -785,7 +793,7 @@ export default class CalendarService {
const edittedEvent = eventTransformer(updatedEvent);

if (status === Event_Status.SCHEDULED && foundEventType.sendSlackNotifications) {
await sendEventScheduledSlackNotif(updatedEvent.notificationSlackThreads, edittedEvent);
await sendEventScheduledSlackNotif(updatedEvent.notificationSlackThreads, edittedEvent, true);
}

if (status === Event_Status.CONFIRMED && foundEventType.sendSlackNotifications) {
Expand Down Expand Up @@ -927,7 +935,11 @@ export default class CalendarService {
userCreatedId: true,
location: true,
dateDeleted: true,
approved: true
approved: true,
status: true,
title: true,
workPackages: true,
scheduledTimes: true
}
});

Expand Down Expand Up @@ -1063,6 +1075,74 @@ export default class CalendarService {
return eventTransformer(updatedEvent);
}

private static async slackRescheduleNotification(
event: {
dateDeleted: Date | null;
eventId: string;
title: string;
Comment thread
glickgNU marked this conversation as resolved.
Outdated
userCreatedId: string;
approved: $Enums.Conflict_Status;
location: string | null;
status: $Enums.Event_Status;
workPackages: {
workPackageId: string;
wbsElementId: string;
projectId: string;
orderInProject: number;
startDate: Date;
duration: number;
stage: $Enums.Work_Package_Stage | null;
}[];
},
organization: {
dateDeleted: Date | null;
dateCreated: Date;
userCreatedId: string;
userDeletedId: string | null;
description: string;
name: string;
organizationId: string;
treasurerId: string | null;
advisorId: string | null;
newMemberImageId: string | null;
logoImageId: string | null;
slackWorkspaceId: string | null;
applicationLink: string | null;
onboardingText: string | null;
partReviewSampleImageId: string | null;
partReviewGuideLink: string | null;
sponsorshipNotificationsSlackChannelId: string | null;
platformDescription: string;
platformLogoImageId: string | null;
}
) {
const foundEvent = await prisma.event.findUnique({
where: { eventId: event.eventId },
...getEventQueryArgs(organization.organizationId)
});
if (!foundEvent) throw new NotFoundException('Event', event.eventId);

const foundUser = await prisma.user.findUnique({
where: { userId: event.userCreatedId }
});
if (!foundUser) throw new NotFoundException('Event', event.eventId);

const foundWorkPackage = await prisma.work_Package.findMany({
where: { workPackageId: { in: event.workPackages.map((wp) => wp.workPackageId) } }
});

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

await sendSlackEventNotifications(
foundEvent.teams,
eventTransformer(foundEvent),
foundUser,
foundEvent.workPackages.map((wp) => wp.wbsElement.name).join(', '),
organization.name,
true
);
}

/**
* Deletes a specific schedule slot from an event.
* If this is the last schedule slot, the entire event is deleted instead.
Expand Down Expand Up @@ -1409,9 +1489,74 @@ export default class CalendarService {
if (!event) throw new NotFoundException('Event', eventId);
if (event.dateDeleted) throw new DeletedException('Event', eventId);

// Cannot schedule an already scheduled event
if (event.status === Event_Status.SCHEDULED) {
throw new HttpException(400, 'Event is already scheduled');
if (event && event?.status === 'SCHEDULED') {
Comment thread
glickgNU marked this conversation as resolved.
Outdated
const timeSlots = await prisma.schedule_Slot.findMany({
where: { eventId: event.eventId }
});

// Restore the old scheduled time from confirmed members' availabilities
// so they get their time back from the old scheduled event
for (const slot of timeSlots) {
if (!slot.startTime || !slot.endTime) continue;
const startHour = new Date(slot.startTime).getHours();
const endHour = new Date(slot.endTime).getHours();

for (const member of event.confirmedMembers) {
if (!member.drScheduleSettings) continue;
const existingAvailability = member.drScheduleSettings.availabilities.find((a) =>
isSameDayUTC(a.dateSet, slot.startTime)
);
if (!existingAvailability) continue;
// Availability index i represents local hour (10 + i); remove indices that fall within [startHour, endHour)
const returnedAvailability = Array.from({ length: endHour - startHour }, (_, i) => startHour + i - 10).filter(
(i) => i >= 0
);

const updatedAvailability = [...new Set([...existingAvailability.availability, ...returnedAvailability])].sort(
(a, b) => a - b
);

await prisma.availability.update({
where: { availabilityId: existingAvailability.availabilityId },
data: { availability: updatedAvailability }
});
}
}

await prisma.event.update({
where: { eventId: event.eventId },
data: { status: Event_Status.SCHEDULED }
});

await prisma.schedule_Slot.deleteMany({
where: { eventId: event.eventId }
});

const workPackages = await prisma.work_Package.findMany({
where: { workPackageId: { in: event.workPackages.map((wp) => wp.workPackageId) } }
});

await CalendarService.slackRescheduleNotification(
{
eventId: event.eventId,
title: event.title,
userCreatedId: event.userCreatedId,
approved: event.approved,
location: event.location,
status: event.status,
dateDeleted: event.dateDeleted,
workPackages: workPackages.map((wp) => ({
workPackageId: wp.workPackageId,
wbsElementId: wp.wbsElementId,
projectId: wp.projectId,
orderInProject: wp.orderInProject,
startDate: wp.startDate,
duration: wp.duration,
stage: wp.stage
}))
},
organization
);
Comment thread
glickgNU marked this conversation as resolved.
Outdated
}

// Only the event creator can schedule the event
Expand Down Expand Up @@ -1460,7 +1605,7 @@ export default class CalendarService {
const endHour = endTime.getHours();
for (const member of event.confirmedMembers) {
if (!member.drScheduleSettings) continue;
const existingAvailability = member.drScheduleSettings.availabilities.find((a) => isSameDay(a.dateSet, startTime));
const existingAvailability = member.drScheduleSettings.availabilities.find((a) => isSameDayUTC(a.dateSet, startTime));
if (!existingAvailability) continue;
// Availability index i represents local hour (10 + i); remove indices that fall within [startHour, endHour)
const updatedAvailability = existingAvailability.availability.filter(
Expand All @@ -1478,7 +1623,11 @@ export default class CalendarService {
});

if (foundEventType?.sendSlackNotifications) {
await sendEventScheduledSlackNotif(updatedEvent.notificationSlackThreads, eventTransformer(updatedEvent));
await sendEventScheduledSlackNotif(
updatedEvent.notificationSlackThreads,
eventTransformer(updatedEvent),
event.status === Event_Status.SCHEDULED
);
}

return eventTransformer(updatedEvent);
Expand Down
30 changes: 21 additions & 9 deletions src/backend/src/utils/slack.utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -403,16 +403,21 @@ export const sendSlackEventNotifications = async (
event: Event,
submitter: User,
workPackageName: string,
projectName: string
projectName: string,
beingRescheduled?: boolean
) => {
const scheduledOrRescheduled = beingRescheduled ? 'rescheduled' : 'scheduled';
if (process.env.NODE_ENV !== 'production' && !DEV_TESTING_OVERRIDE) return []; // don't send msgs unless in prod
const notifications: { channelId: string; ts: string }[] = [];
let message;
if (workPackageName) {
message = `:spiral_calendar_pad: ${event.title} for *${workPackageName}* is being scheduled by ${submitter.firstName} ${submitter.lastName} in project ${projectName}`;
} else {
message = `:spiral_calendar_pad: ${event.title} is being scheduled by ${submitter.firstName} ${submitter.lastName} in project ${projectName}`;
}
const includeWorkPackage = workPackageName ? `for *${workPackageName}*` : '';

message =
`:spiral_calendar_pad: ${event.title}` +
includeWorkPackage +
` is being ` +
scheduledOrRescheduled +
` by ${submitter.firstName} ${submitter.lastName} in project ${projectName}`;

const completion: Promise<void>[] = teams.map(async (team) => {
const sentNotifications: { channelId: string; ts: string }[] = await sendSlackEventNotification(team, message);
Expand Down Expand Up @@ -458,9 +463,13 @@ export const sendEventConfirmationToThread = async (threads: SlackMessageThread[
}
};

export const sendEventScheduledSlackNotif = async (threads: SlackMessageThread[], event: Event) => {
export const sendEventScheduledSlackNotif = async (
threads: SlackMessageThread[],
event: Event,
beingRescheduled: boolean = false
) => {
if (process.env.NODE_ENV !== 'production' && !DEV_TESTING_OVERRIDE) return; // don't send msgs unless in prod

const scheduledOrRescheduled = beingRescheduled ? 'rescheduled' : 'scheduled';
// Get work package names
const wpNames = event.workPackages.map((wp) => wp.wbsElement.name).join(', ');
const drName = event.title + (wpNames ? ` (${wpNames})` : '');
Expand All @@ -487,7 +496,10 @@ export const sendEventScheduledSlackNotif = async (threads: SlackMessageThread[]

const location = zoomLink && inPersonLocation ? `${inPersonLocation} and ${zoomLink}` : inPersonLocation || zoomLink || '';

const msg = `:spiral_calendar_pad: ${event.title} for *${drName}* has been scheduled for *${drTime}* ${location} by ${drSubmitter}`;
const msg =
`:spiral_calendar_pad: ${event.title} for *${drName}* has been ` +
scheduledOrRescheduled +
` for *${drTime}* ${location} by ${drSubmitter}`;
const docLink = event.questionDocumentLink ? `<${event.questionDocumentLink}|Doc Link>` : '';
const threadMsg = `This event has been Scheduled! \n` + docLink;
Comment thread
glickgNU marked this conversation as resolved.
Outdated

Expand Down
1 change: 1 addition & 0 deletions src/frontend/src/hooks/calendar.hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -586,6 +586,7 @@ export const useScheduleEvent = (eventId: string) => {
queryClient.invalidateQueries(['events', eventId, 'with-members']);
queryClient.invalidateQueries(['filter-events']);
queryClient.invalidateQueries(EVENT_KEY);
queryClient.invalidateQueries(['users']);
Comment thread
glickgNU marked this conversation as resolved.
Outdated
}
}
);
Expand Down
Loading