Skip to content

Commit 6cbe307

Browse files
authored
Merge pull request #4257 from Northeastern-Electric-Racing/#4238-reschedule-meetings
#4238 reschedule meetings
2 parents 1627131 + abd8d01 commit 6cbe307

8 files changed

Lines changed: 209 additions & 72 deletions

File tree

src/backend/src/services/calendar.services.ts

Lines changed: 58 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import {
1515
Machinery,
1616
ScheduleSlot,
1717
notGuest,
18-
isSameDay,
18+
isSameDayUTC,
1919
EventInstance,
2020
SlackMentionType
2121
} from 'shared';
@@ -503,11 +503,7 @@ export default class CalendarService {
503503

504504
if (foundEventType.sendSlackNotifications) {
505505
const members = await prisma.user.findMany({
506-
where: {
507-
userId: {
508-
in: optionalMemberIds.concat(allRequiredMembers)
509-
}
510-
}
506+
where: { userId: { in: optionalMemberIds.concat(allRequiredMembers) } }
511507
});
512508

513509
// get the user settings for all the members invited, who are leaderingship
@@ -801,13 +797,12 @@ export default class CalendarService {
801797
const edittedEvent = eventTransformer(updatedEvent);
802798

803799
if (status === Event_Status.SCHEDULED && foundEventType.sendSlackNotifications) {
804-
await sendEventScheduledSlackNotif(updatedEvent.notificationSlackThreads, edittedEvent);
800+
await sendEventScheduledSlackNotif(updatedEvent.notificationSlackThreads, edittedEvent, true);
805801
}
806802

807803
if (status === Event_Status.CONFIRMED && foundEventType.sendSlackNotifications) {
808804
await sendEventConfirmationToThread(updatedEvent.notificationSlackThreads, updatedEvent.userCreated);
809805
}
810-
811806
return edittedEvent;
812807
}
813808

@@ -943,7 +938,11 @@ export default class CalendarService {
943938
userCreatedId: true,
944939
location: true,
945940
dateDeleted: true,
946-
approved: true
941+
approved: true,
942+
status: true,
943+
title: true,
944+
workPackages: true,
945+
scheduledTimes: true
947946
}
948947
});
949948

@@ -1424,10 +1423,48 @@ export default class CalendarService {
14241423

14251424
if (!event) throw new NotFoundException('Event', eventId);
14261425
if (event.dateDeleted) throw new DeletedException('Event', eventId);
1427-
1428-
// Cannot schedule an already scheduled event
14291426
if (event.status === Event_Status.SCHEDULED) {
1430-
throw new HttpException(400, 'Event is already scheduled');
1427+
const timeSlots = await prisma.schedule_Slot.findMany({
1428+
where: { eventId: event.eventId }
1429+
});
1430+
1431+
// Restore the old scheduled time from confirmed members' availabilities
1432+
// so they get their time back from the old scheduled event
1433+
for (const slot of timeSlots) {
1434+
if (!slot.startTime || !slot.endTime) continue;
1435+
const startHour = new Date(slot.startTime).getHours();
1436+
const endHour = new Date(slot.endTime).getHours();
1437+
1438+
for (const member of event.confirmedMembers) {
1439+
if (!member.drScheduleSettings) continue;
1440+
const existingAvailability = member.drScheduleSettings.availabilities.find((a) =>
1441+
isSameDayUTC(a.dateSet, slot.startTime)
1442+
);
1443+
if (!existingAvailability) continue;
1444+
// Availability index i represents local hour (10 + i); remove indices that fall within [startHour, endHour)
1445+
const returnedAvailability = Array.from({ length: endHour - startHour }, (_, i) => startHour + i - 10).filter(
1446+
(i) => i >= 0
1447+
);
1448+
1449+
const updatedAvailability = [...new Set([...existingAvailability.availability, ...returnedAvailability])].sort(
1450+
(a, b) => a - b
1451+
);
1452+
1453+
await prisma.availability.update({
1454+
where: { availabilityId: existingAvailability.availabilityId },
1455+
data: { availability: updatedAvailability }
1456+
});
1457+
}
1458+
}
1459+
1460+
await prisma.event.update({
1461+
where: { eventId: event.eventId },
1462+
data: { status: Event_Status.SCHEDULED }
1463+
});
1464+
1465+
await prisma.schedule_Slot.deleteMany({
1466+
where: { eventId: event.eventId }
1467+
});
14311468
}
14321469

14331470
// Only the event creator can schedule the event
@@ -1464,9 +1501,12 @@ export default class CalendarService {
14641501
allDay: false
14651502
}
14661503
},
1504+
1505+
initialDateScheduled: event.initialDateScheduled ?? null,
14671506
approved: hasConflict ? Conflict_Status.PENDING : event.approved,
14681507
approvalRequiredFromUserId: hasConflict ? conflictingEvent?.userCreated.userId : event.approvalRequiredFromUserId
14691508
},
1509+
14701510
...getEventQueryArgs(organization.organizationId)
14711511
});
14721512

@@ -1476,7 +1516,7 @@ export default class CalendarService {
14761516
const endHour = endTime.getHours();
14771517
for (const member of event.confirmedMembers) {
14781518
if (!member.drScheduleSettings) continue;
1479-
const existingAvailability = member.drScheduleSettings.availabilities.find((a) => isSameDay(a.dateSet, startTime));
1519+
const existingAvailability = member.drScheduleSettings.availabilities.find((a) => isSameDayUTC(a.dateSet, startTime));
14801520
if (!existingAvailability) continue;
14811521
// Availability index i represents local hour (10 + i); remove indices that fall within [startHour, endHour)
14821522
const updatedAvailability = existingAvailability.availability.filter(
@@ -1494,9 +1534,12 @@ export default class CalendarService {
14941534
});
14951535

14961536
if (foundEventType?.sendSlackNotifications) {
1497-
await sendEventScheduledSlackNotif(updatedEvent.notificationSlackThreads, eventTransformer(updatedEvent));
1537+
await sendEventScheduledSlackNotif(
1538+
updatedEvent.notificationSlackThreads,
1539+
eventTransformer(updatedEvent),
1540+
event.status === Event_Status.SCHEDULED
1541+
);
14981542
}
1499-
15001543
return eventTransformer(updatedEvent);
15011544
}
15021545

src/backend/src/utils/slack.utils.ts

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -473,9 +473,13 @@ export const sendEventConfirmationToThread = async (threads: SlackMessageThread[
473473
}
474474
};
475475

476-
export const sendEventScheduledSlackNotif = async (threads: SlackMessageThread[], event: Event) => {
476+
export const sendEventScheduledSlackNotif = async (
477+
threads: SlackMessageThread[],
478+
event: Event,
479+
beingRescheduled: boolean = false
480+
) => {
477481
if (process.env.NODE_ENV !== 'production' && !DEV_TESTING_OVERRIDE) return; // don't send msgs unless in prod
478-
482+
const scheduledOrRescheduled = beingRescheduled ? 'rescheduled' : 'scheduled';
479483
// Get work package names
480484
const wpNames = event.workPackages.map((wp) => wp.wbsElement.name).join(', ');
481485
const drName = event.title + (wpNames ? ` (${wpNames})` : '');
@@ -507,9 +511,12 @@ export const sendEventScheduledSlackNotif = async (threads: SlackMessageThread[]
507511
const validSlackIds = resolvedSlackIds.filter((id): id is string => !!id);
508512
const mentionPrefix = buildSlackMentionPrefix(SlackMentionType.USER, validSlackIds);
509513

510-
const msg = `:spiral_calendar_pad: ${event.title} for *${drName}* has been scheduled for *${drTime}* ${location} by ${drSubmitter}`;
514+
const msg =
515+
`:spiral_calendar_pad: ${event.title} for *${drName}* has been ` +
516+
scheduledOrRescheduled +
517+
` for *${drTime}* ${location} by ${drSubmitter}`;
511518
const docLink = event.questionDocumentLink ? `<${event.questionDocumentLink}|Doc Link>` : '';
512-
const threadMsg = `${mentionPrefix}This event has been Scheduled! \n` + docLink;
519+
const threadMsg = `${mentionPrefix}This event has been ` + scheduledOrRescheduled + ` \n` + docLink;
513520

514521
if (threads && threads.length !== 0) {
515522
const msgs = threads.map((thread) => editMessage(thread.channelId, thread.timestamp, msg));

src/frontend/src/hooks/calendar.hooks.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,6 @@ const SHOP_KEY = ['shops'] as const;
6565
const CALENDAR_KEY = ['calendars'] as const;
6666
export const EVENT_TYPE_KEY = ['event-types'] as const;
6767
export const EVENT_KEY = ['events'] as const;
68-
6968
export interface EventCreateArgs {
7069
title: string;
7170
eventTypeId: string;

src/frontend/src/pages/CalendarPage/AvailabilityScheduleView.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,9 @@ const AvailabilityScheduleView: React.FC<AvailabilityScheduleViewProps> = ({
3030
}) => {
3131
const totalUsers = usersToAvailabilities.size;
3232
const [selectedTimeslot, setSelectedTimeslot] = useState<number | null>(null);
33+
3334
// Use displayDate if provided, otherwise fall back to event's initial date.
34-
const initialDate = displayDate || (event.initialDateScheduled ?? new Date());
35+
const initialDate = event.initialDateScheduled || displayDate || new Date();
3536
const potentialDays = getNextSevenDays(initialDate);
3637

3738
// Handle hover - updates the sidebar with available/unavailable users and slot info

src/frontend/src/pages/CalendarPage/Components/EventAvailabilityPage.tsx

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ import {
1212
User,
1313
UserWithScheduleSettings,
1414
EventWithMembers,
15-
isAdmin,
16-
EventStatus
15+
EventStatus,
16+
isAdmin
1717
} from 'shared';
1818
import PageLayout from '../../../components/PageLayout';
1919
import LoadingIndicator from '../../../components/LoadingIndicator';
@@ -401,10 +401,10 @@ export const EventAvailabilityPage: React.FC = () => {
401401
)}
402402

403403
{/* Schedule button for creators - only show if event is not already scheduled */}
404-
{(isCreator || isAdmin(currentUser.role)) && selectedSlot && event.status !== EventStatus.SCHEDULED && (
404+
{(isCreator || isAdmin(currentUser.role)) && selectedSlot && (
405405
<Box sx={{ mt: 3 }}>
406406
<NERSuccessButton variant="contained" onClick={handleScheduleClick} fullWidth>
407-
Schedule Event
407+
{event.status === EventStatus.SCHEDULED ? 'Reschedule Event' : 'Schedule Event'}
408408
</NERSuccessButton>
409409
</Box>
410410
)}
@@ -440,6 +440,7 @@ export const EventAvailabilityPage: React.FC = () => {
440440
selectedDay={selectedSlot.day}
441441
startHour={selectedSlot.startHour}
442442
endHour={selectedSlot.endHour}
443+
beingRescheduled={event.status === EventStatus.SCHEDULED}
443444
/>
444445
)}
445446
</PageLayout>

src/frontend/src/pages/CalendarPage/Components/ScheduleEventModal.tsx

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ interface ScheduleEventModalProps {
2222
selectedDay: Date;
2323
startHour: number;
2424
endHour: number;
25+
beingRescheduled: boolean;
2526
}
2627

2728
const ScheduleEventModal: React.FC<ScheduleEventModalProps> = ({
@@ -31,7 +32,8 @@ const ScheduleEventModal: React.FC<ScheduleEventModalProps> = ({
3132
eventName,
3233
selectedDay,
3334
startHour,
34-
endHour
35+
endHour,
36+
beingRescheduled
3537
}) => {
3638
const toast = useToast();
3739
const history = useHistory();
@@ -47,7 +49,7 @@ const ScheduleEventModal: React.FC<ScheduleEventModalProps> = ({
4749
const handleConfirm = async () => {
4850
try {
4951
await scheduleEvent({ startTime, endTime });
50-
toast.success('Event scheduled successfully!');
52+
toast.success(beingRescheduled ? 'Event rescheduled successfully!' : 'Event scheduled successfully!');
5153
onClose();
5254
history.push(routes.CALENDAR);
5355
} catch (e) {
@@ -59,12 +61,12 @@ const ScheduleEventModal: React.FC<ScheduleEventModalProps> = ({
5961

6062
return (
6163
<Dialog open={open} onClose={onClose} maxWidth="sm" fullWidth>
62-
<DialogTitle>Schedule {eventName}</DialogTitle>
64+
<DialogTitle>
65+
{beingRescheduled ? 'Reschedule' : 'Schedule'} {eventName}
66+
</DialogTitle>
6367
<DialogContent>
6468
<Box sx={{ py: 2 }}>
65-
<Typography variant="body1" gutterBottom>
66-
You are about to schedule this event for:
67-
</Typography>
69+
<Typography>You are about to {beingRescheduled ? 'reschedule' : 'schedule'} this event for:</Typography>
6870
<Box
6971
sx={{ mt: 2, p: 2, bgcolor: 'background.paper', borderRadius: 1, border: '1px solid', borderColor: 'divider' }}
7072
>
@@ -73,17 +75,21 @@ const ScheduleEventModal: React.FC<ScheduleEventModalProps> = ({
7375
{formatEventTime(startTime)} - {formatEventTime(endTime)}
7476
</Typography>
7577
</Box>
78+
7679
<Typography variant="body2" color="text.secondary" sx={{ mt: 2 }}>
77-
This will change the event status to SCHEDULED and notify all members.
80+
{beingRescheduled
81+
? 'All members will be notified about the rescheduled time once changed.'
82+
: 'This will change the event status to SCHEDULED and notify all members.'}
7883
</Typography>
7984
</Box>
8085
</DialogContent>
8186
<DialogActions sx={{ px: 3, pb: 2 }}>
8287
<NERFailButton onClick={onClose} disabled={isLoading}>
8388
Cancel
8489
</NERFailButton>
90+
8591
<NERSuccessButton onClick={handleConfirm} disabled={isLoading}>
86-
{isLoading ? 'Scheduling...' : 'Confirm Schedule'}
92+
{beingRescheduled ? 'Confirm Reschedule' : 'Confirm Schedule'}
8793
</NERSuccessButton>
8894
</DialogActions>
8995
</Dialog>

0 commit comments

Comments
 (0)