Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 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
74 changes: 59 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 @@ -1423,9 +1422,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');
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 }
});
}

// Only the event creator can schedule the event
Expand Down Expand Up @@ -1462,9 +1500,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 +1515,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 +1533,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
13 changes: 10 additions & 3 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,7 +511,10 @@ 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;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On line 519 can you update threadMsg to say scheduled/rescheduled depending on which it is


Expand Down
2 changes: 1 addition & 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 Expand Up @@ -588,6 +587,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
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,7 @@ import {
User,
UserWithScheduleSettings,
EventWithMembers,
isAdmin,
EventStatus
isAdmin
} from 'shared';
import PageLayout from '../../../components/PageLayout';
import LoadingIndicator from '../../../components/LoadingIndicator';
Expand Down Expand Up @@ -399,12 +398,17 @@ export const EventAvailabilityPage: React.FC = () => {
</Typography>
</Typography>
)}
{(currentAvailableUsers.length > 0 || currentUnavailableUsers.length > 0) && (
<Typography variant="caption" color="text.secondary" sx={{ mt: 2, display: 'block' }}>
<span style={{ textDecoration: 'underline' }}>Underline</span> means required for meeting
Comment thread
glickgNU marked this conversation as resolved.
Outdated
</Typography>
)}

{/* 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 === 'SCHEDULED' ? 'Reschedule Event' : 'Schedule Event'}
Comment thread
glickgNU marked this conversation as resolved.
Outdated
</NERSuccessButton>
</Box>
)}
Expand Down Expand Up @@ -440,6 +444,7 @@ export const EventAvailabilityPage: React.FC = () => {
selectedDay={selectedSlot.day}
startHour={selectedSlot.startHour}
endHour={selectedSlot.endHour}
beingRescheduled={event.status === 'SCHEDULED'}
Comment thread
glickgNU marked this conversation as resolved.
Outdated
/>
)}
</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,8 @@ const ScheduleEventModal: React.FC<ScheduleEventModalProps> = ({
const handleConfirm = async () => {
try {
await scheduleEvent({ startTime, endTime });
toast.success('Event scheduled successfully!');
if (beingRescheduled) toast.success('Event rescheduled successfully!');
Comment thread
glickgNU marked this conversation as resolved.
Outdated
if (!beingRescheduled) toast.success('Event scheduled successfully!');
onClose();
history.push(routes.CALENDAR);
} catch (e) {
Expand All @@ -62,9 +65,16 @@ const ScheduleEventModal: React.FC<ScheduleEventModalProps> = ({
<DialogTitle>Schedule {eventName}</DialogTitle>
Comment thread
glickgNU marked this conversation as resolved.
Outdated
<DialogContent>
<Box sx={{ py: 2 }}>
<Typography variant="body1" gutterBottom>
You are about to schedule this event for:
</Typography>
{!beingRescheduled && (
Comment thread
glickgNU marked this conversation as resolved.
Outdated
<Typography variant="body1" gutterBottom>
You are about to schedule this event for:
</Typography>
)}
{beingRescheduled && (
<Typography variant="body1" gutterBottom>
You are about to reschedule this event for:
</Typography>
)}
<Box
sx={{ mt: 2, p: 2, bgcolor: 'background.paper', borderRadius: 1, border: '1px solid', borderColor: 'divider' }}
>
Expand All @@ -73,18 +83,33 @@ 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.
</Typography>
{beingRescheduled && (
<Typography variant="body2" color="text.secondary" sx={{ mt: 2 }}>
All members will be notified about the rescheduled time once changed.
</Typography>
)}
{!beingRescheduled && (
<Typography variant="body2" color="text.secondary" sx={{ mt: 2 }}>
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'}
</NERSuccessButton>

{beingRescheduled && (
<NERSuccessButton onClick={handleConfirm} disabled={isLoading}>
{isLoading ? 'Scheduling...' : 'Confirm Reschedule'}
Comment thread
glickgNU marked this conversation as resolved.
Outdated
</NERSuccessButton>
)}
{!beingRescheduled && (
<NERSuccessButton onClick={handleConfirm} disabled={isLoading}>
{isLoading ? 'Scheduling...' : 'Confirm Schedule'}
</NERSuccessButton>
)}
</DialogActions>
</Dialog>
);
Expand Down
Loading
Loading