Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
73 changes: 58 additions & 15 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,
SlackMentionType
} from 'shared';
Expand Down Expand Up @@ -502,11 +502,7 @@ export default class CalendarService {

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

// get the user settings for all the members invited, who are leaderingship
Expand Down Expand Up @@ -799,13 +795,12 @@ 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) {
await sendEventConfirmationToThread(updatedEvent.notificationSlackThreads, updatedEvent.userCreated);
}

return edittedEvent;
}

Expand Down Expand Up @@ -941,7 +936,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 @@ -1422,10 +1421,48 @@ 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');
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 }
});
}

// Only the event creator can schedule the event
Expand Down Expand Up @@ -1462,9 +1499,12 @@ export default class CalendarService {
allDay: false
}
},

initialDateScheduled: event.initialDateScheduled ?? null,
approved: hasConflict ? Conflict_Status.PENDING : event.approved,
approvalRequiredFromUserId: hasConflict ? conflictingEvent?.userCreated.userId : event.approvalRequiredFromUserId
},

...getEventQueryArgs(organization.organizationId)
});

Expand All @@ -1474,7 +1514,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 @@ -1492,9 +1532,12 @@ 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
15 changes: 11 additions & 4 deletions src/backend/src/utils/slack.utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -473,9 +473,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 Down Expand Up @@ -507,9 +511,12 @@ export const sendEventScheduledSlackNotif = async (threads: SlackMessageThread[]
const validSlackIds = resolvedSlackIds.filter((id): id is string => !!id);
const mentionPrefix = buildSlackMentionPrefix(SlackMentionType.USER, validSlackIds);

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 = `${mentionPrefix}This event has been Scheduled! \n` + docLink;
const threadMsg = `${mentionPrefix}This event has been ` + scheduledOrRescheduled + ` \n` + docLink;

if (threads && threads.length !== 0) {
const msgs = threads.map((thread) => editMessage(thread.channelId, thread.timestamp, msg));
Expand Down
1 change: 0 additions & 1 deletion src/frontend/src/hooks/calendar.hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,6 @@ const SHOP_KEY = ['shops'] as const;
const CALENDAR_KEY = ['calendars'] as const;
export const EVENT_TYPE_KEY = ['event-types'] as const;
export const EVENT_KEY = ['events'] as const;

export interface EventCreateArgs {
title: string;
eventTypeId: string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,9 @@ const AvailabilityScheduleView: React.FC<AvailabilityScheduleViewProps> = ({
}) => {
const totalUsers = usersToAvailabilities.size;
const [selectedTimeslot, setSelectedTimeslot] = useState<number | null>(null);

// Use displayDate if provided, otherwise fall back to event's initial date.
const initialDate = displayDate || (event.initialDateScheduled ?? new Date());
const initialDate = event.initialDateScheduled || displayDate || new Date();
Comment thread
wavehassman marked this conversation as resolved.
const potentialDays = getNextSevenDays(initialDate);

// Handle hover - updates the sidebar with available/unavailable users and slot info
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ import {
User,
UserWithScheduleSettings,
EventWithMembers,
isAdmin,
EventStatus
EventStatus,
isAdmin
} from 'shared';
import PageLayout from '../../../components/PageLayout';
import LoadingIndicator from '../../../components/LoadingIndicator';
Expand Down Expand Up @@ -401,10 +401,10 @@ export const EventAvailabilityPage: React.FC = () => {
)}

{/* Schedule button for creators - only show if event is not already scheduled */}
{(isCreator || isAdmin(currentUser.role)) && selectedSlot && event.status !== EventStatus.SCHEDULED && (
{(isCreator || isAdmin(currentUser.role)) && selectedSlot && (
<Box sx={{ mt: 3 }}>
<NERSuccessButton variant="contained" onClick={handleScheduleClick} fullWidth>
Schedule Event
{event.status === EventStatus.SCHEDULED ? 'Reschedule Event' : 'Schedule Event'}
</NERSuccessButton>
</Box>
)}
Expand Down Expand Up @@ -440,6 +440,7 @@ export const EventAvailabilityPage: React.FC = () => {
selectedDay={selectedSlot.day}
startHour={selectedSlot.startHour}
endHour={selectedSlot.endHour}
beingRescheduled={event.status === EventStatus.SCHEDULED}
/>
)}
</PageLayout>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ interface ScheduleEventModalProps {
selectedDay: Date;
startHour: number;
endHour: number;
beingRescheduled: boolean;
}

const ScheduleEventModal: React.FC<ScheduleEventModalProps> = ({
Expand All @@ -31,7 +32,8 @@ const ScheduleEventModal: React.FC<ScheduleEventModalProps> = ({
eventName,
selectedDay,
startHour,
endHour
endHour,
beingRescheduled
}) => {
const toast = useToast();
const history = useHistory();
Expand All @@ -47,7 +49,7 @@ const ScheduleEventModal: React.FC<ScheduleEventModalProps> = ({
const handleConfirm = async () => {
try {
await scheduleEvent({ startTime, endTime });
toast.success('Event scheduled successfully!');
toast.success(beingRescheduled ? 'Event rescheduled successfully!' : 'Event scheduled successfully!');
onClose();
history.push(routes.CALENDAR);
} catch (e) {
Expand All @@ -59,12 +61,12 @@ const ScheduleEventModal: React.FC<ScheduleEventModalProps> = ({

return (
<Dialog open={open} onClose={onClose} maxWidth="sm" fullWidth>
<DialogTitle>Schedule {eventName}</DialogTitle>
<DialogTitle>
{beingRescheduled ? 'Reschedule' : 'Schedule'} {eventName}
</DialogTitle>
<DialogContent>
<Box sx={{ py: 2 }}>
<Typography variant="body1" gutterBottom>
You are about to schedule this event for:
</Typography>
<Typography>You are about to {beingRescheduled ? 'reschedule' : 'schedule'} this event for:</Typography>
<Box
sx={{ mt: 2, p: 2, bgcolor: 'background.paper', borderRadius: 1, border: '1px solid', borderColor: 'divider' }}
>
Expand All @@ -73,17 +75,21 @@ const ScheduleEventModal: React.FC<ScheduleEventModalProps> = ({
{formatEventTime(startTime)} - {formatEventTime(endTime)}
</Typography>
</Box>

<Typography variant="body2" color="text.secondary" sx={{ mt: 2 }}>
This will change the event status to SCHEDULED and notify all members.
{beingRescheduled
? 'All members will be notified about the rescheduled time once changed.'
: 'This will change the event status to SCHEDULED and notify all members.'}
</Typography>
</Box>
</DialogContent>
<DialogActions sx={{ px: 3, pb: 2 }}>
<NERFailButton onClick={onClose} disabled={isLoading}>
Cancel
</NERFailButton>

<NERSuccessButton onClick={handleConfirm} disabled={isLoading}>
{isLoading ? 'Scheduling...' : 'Confirm Schedule'}
{beingRescheduled ? 'Confirm Reschedule' : 'Confirm Schedule'}
</NERSuccessButton>
</DialogActions>
</Dialog>
Expand Down
Loading
Loading