Skip to content

Commit a751f2e

Browse files
authored
Merge pull request #4033 from Northeastern-Electric-Racing/#4032-maintenance-calendar-notifications
#4032 notification ui in event modal
2 parents e909f36 + 375f97a commit a751f2e

5 files changed

Lines changed: 96 additions & 19 deletions

File tree

src/backend/src/services/notifications.services.ts

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ export default class NotificationsService {
118118
}
119119

120120
/**
121-
* Sends the design review slack notifications for all design reviews scheduled for today
121+
* Sends Slack notifications for all events scheduled for today whose event type has sendSlackNotifications enabled
122122
*/
123123
static async sendEventSlackNotifications() {
124124
const endOfToday = startOfDayTomorrow();
@@ -142,6 +142,8 @@ export default class NotificationsService {
142142
optionalMembers: { include: { userSettings: true } },
143143
userCreated: { include: { userSettings: true } },
144144
scheduledTimes: true,
145+
teams: true,
146+
eventType: true,
145147
workPackages: {
146148
include: {
147149
wbsElement: true,
@@ -156,12 +158,18 @@ export default class NotificationsService {
156158
}
157159
});
158160

159-
const desginReviewEventTeamMap = new Map<string, EventWithAttendees[]>();
161+
const eventTeamMap = new Map<string, EventWithAttendees[]>();
160162

161163
events.forEach((event) => {
162-
// Get all unique teams from all work packages associated with this event
164+
// Collect unique team Slack IDs: first from teams directly on the event, then from work packages
163165
const teamSlackIds = new Set<string>();
164166

167+
event.teams.forEach((team) => {
168+
if (team.slackId) {
169+
teamSlackIds.add(team.slackId);
170+
}
171+
});
172+
165173
event.workPackages.forEach((workPackage) => {
166174
workPackage.project.teams.forEach((team) => {
167175
if (team.slackId) {
@@ -171,7 +179,7 @@ export default class NotificationsService {
171179
});
172180

173181
teamSlackIds.forEach((teamSlackId) => {
174-
const currentEvents = desginReviewEventTeamMap.get(teamSlackId);
182+
const currentEvents = eventTeamMap.get(teamSlackId);
175183
const eventWithAttendees = {
176184
...event,
177185
attendees: event.requiredMembers.concat(event.optionalMembers).concat(event.userCreated),
@@ -181,20 +189,20 @@ export default class NotificationsService {
181189
if (currentEvents) {
182190
currentEvents.push(eventWithAttendees);
183191
} else {
184-
desginReviewEventTeamMap.set(teamSlackId, [eventWithAttendees]);
192+
eventTeamMap.set(teamSlackId, [eventWithAttendees]);
185193
}
186194
});
187195
});
188196

189-
// Send the notifications to each team for their respective design reviews
190-
const promises = Array.from(desginReviewEventTeamMap).map(async ([slackId, events]) => {
197+
// Send the notifications to each team for their respective events
198+
const promises = Array.from(eventTeamMap).map(async ([slackId, events]) => {
191199
const messageBlock = events
192200
.map((event) => {
193201
const zoomLink = event.zoomLink ? `<${event.zoomLink}|Zoom Link>\n` : '';
194202
const questionDocLink = event.questionDocumentLink ? `<${event.questionDocumentLink}|Question Doc Link>\n` : '';
195203

196-
// Get work package names for this event
197204
const workPackageNames = event.workPackages.map((wp) => wp.wbsElement.name).join(', ');
205+
const workPackagesPart = workPackageNames ? ` (${workPackageNames})` : '';
198206

199207
// Get the earliest scheduled start time for display
200208
const [earliestSlot] = event.scheduledTimes
@@ -203,17 +211,17 @@ export default class NotificationsService {
203211
const timeDisplay = earliestSlot ? formatTimeForSlack(new Date(earliestSlot.startTime!)) : 'TBD';
204212

205213
return (
206-
`${usersToSlackPings(event.attendees ?? [])} ${event.title} (${workPackageNames}) ` +
214+
`${usersToSlackPings(event.attendees ?? [])} *${event.eventType.name}*: ${event.title}${workPackagesPart} ` +
207215
`will be having an event today at ${timeDisplay} ET! ` +
208216
zoomLink +
209217
questionDocLink
210218
);
211219
})
212220
.join('\n\n');
213221

214-
// messageBlock will be empty if there are design reviews with no attendees
222+
// messageBlock will be empty if there are events with no attendees
215223
if (messageBlock !== '')
216-
await sendMessage(slackId, ':calendar: :clock9: Upcoming Design Reviews! :clock9: :calendar: \n\n\n' + messageBlock);
224+
await sendMessage(slackId, ':calendar: :clock9: Upcoming Events! :clock9: :calendar: \n\n\n' + messageBlock);
217225
});
218226

219227
await Promise.all(promises);

src/backend/src/utils/notifications.utils.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Task as Prisma_Task, WBS_Element, Event, Work_Package } from '@prisma/client';
1+
import { Task as Prisma_Task, WBS_Element, Event, Work_Package, Team, Event_Type } from '@prisma/client';
22
import { UserWithSettings } from './auth.utils.js';
33
import { ScheduleSlot } from 'shared';
44

@@ -10,6 +10,8 @@ export type TaskWithAssignees = Prisma_Task & {
1010
export type EventWithAttendees = Event & {
1111
attendees: UserWithSettings[];
1212
scheduledTimes: ScheduleSlot[];
13+
teams: Team[];
14+
eventType: Event_Type;
1315
workPackages: (Work_Package & {
1416
wbsElement: WBS_Element;
1517
})[];

src/frontend/src/pages/AdminToolsPage/ScheduleConfig/EventType/EventTypeFormModal.tsx

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
import { Box, FormHelperText, Typography, Checkbox, FormControl, Select, MenuItem } from '@mui/material';
1+
import { Box, FormHelperText, Typography, Checkbox, FormControl, Select, MenuItem, Tooltip } from '@mui/material';
22
import NERFormModal from '../../../../components/NERFormModal';
33
import ReactHookTextField from '../../../../components/ReactHookTextField';
44
import { useToast } from '../../../../hooks/toasts.hooks';
55
import { useForm, Controller } from 'react-hook-form';
66
import * as yup from 'yup';
77
import { yupResolver } from '@hookform/resolvers/yup';
8-
import React from 'react';
8+
import React, { useEffect } from 'react';
99
import { EventType } from 'shared';
1010
import useFormPersist from 'react-hook-form-persist';
1111
import { FormStorageKey } from '../../../../utils/form';
@@ -116,6 +116,16 @@ export const EventTypeFormModal: React.FC<EventTypeFormModalProps> = ({ open, on
116116
setValue
117117
});
118118

119+
const watchTeams = watch('teams');
120+
const watchWorkPackage = watch('workPackage');
121+
const notificationsDisabled = !watchTeams && !watchWorkPackage;
122+
123+
useEffect(() => {
124+
if (notificationsDisabled) {
125+
setValue('sendSlackNotifications', false);
126+
}
127+
}, [notificationsDisabled, setValue]);
128+
119129
const onFormSubmit = async (data: EventTypeFormValues) => {
120130
try {
121131
await onSubmit(data);
@@ -829,12 +839,22 @@ export const EventTypeFormModal: React.FC<EventTypeFormModalProps> = ({ open, on
829839
control={control}
830840
name="sendSlackNotifications"
831841
render={({ field: { onChange, value } }) => (
832-
<Checkbox checked={value} onChange={onChange} sx={{ color: 'white', '&.Mui-checked': { color: 'white' } }} />
842+
<Checkbox
843+
checked={value}
844+
onChange={onChange}
845+
disabled={notificationsDisabled}
846+
sx={{ color: 'white', '&.Mui-checked': { color: 'white' } }}
847+
/>
833848
)}
834849
/>
835-
<NotificationsIcon sx={{ color: 'white', mr: 1 }} />
850+
<Tooltip
851+
title={notificationsDisabled ? 'Slack notifications cannot be sent without teams or work packages enabled' : ''}
852+
placement="right"
853+
>
854+
<NotificationsIcon sx={{ color: notificationsDisabled ? 'rgba(255,255,255,0.3)' : 'white', mr: 1 }} />
855+
</Tooltip>
836856
<Box sx={{ flex: 1 }}>
837-
<Typography variant="body2" sx={{ color: 'white' }}>
857+
<Typography variant="body2" sx={{ color: notificationsDisabled ? 'rgba(255,255,255,0.3)' : 'white' }}>
838858
Send Slack Notifications
839859
</Typography>
840860
</Box>

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

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@ import HelpOutlineIcon from '@mui/icons-material/HelpOutline';
5454
import Tooltip from '@mui/material/Tooltip';
5555
import { convertDayToInt, convertIntToDay } from '../../../utils/calendar.utils';
5656
import EditSeriesConfirmationModal from './EditSeriesConfirmationModal';
57+
import NotificationsIcon from '@mui/icons-material/Notifications';
58+
import WarningAmberIcon from '@mui/icons-material/WarningAmber';
5759

5860
export interface EventFormValues {
5961
title: string;
@@ -237,6 +239,9 @@ const EventModal: React.FC<BaseEventModalProps> = ({
237239
const [pendingPayload, setPendingPayload] = useState<EventPayload | null>(null);
238240
const [pendingFormData, setPendingFormData] = useState<EventFormValues | null>(null);
239241

242+
// used in edit mode for ability to send notifs when wp changes
243+
const [workPackageIds, setWorkPackageIds] = useState<string[]>(initialValues?.workPackageIds ?? []);
244+
240245
// Fetch preview of other schedule slots that would be affected when editing with "edit all in series"
241246
const isEditMode = !!initialValues;
242247
const scheduleSlotId = initialValues?.selectedScheduleSlotId;
@@ -1151,6 +1156,43 @@ const EventModal: React.FC<BaseEventModalProps> = ({
11511156
)}
11521157
</Box>
11531158
)}
1159+
{/* Notification Section */}
1160+
{selectedEventType && (
1161+
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
1162+
<NotificationsIcon sx={{ color: 'text.secondary' }} />
1163+
<Tooltip
1164+
arrow
1165+
placement="right"
1166+
title={
1167+
!selectedEventType.sendSlackNotifications
1168+
? 'Slack notifications are disabled for this event type.'
1169+
: selectedTeams.length || workPackageIds.length
1170+
? 'Slack notifications will be sent for this event.'
1171+
: ''
1172+
}
1173+
>
1174+
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, cursor: 'default' }}>
1175+
<Typography variant="body2" color={'text.disabled'} fontWeight={500}>
1176+
{selectedEventType.sendSlackNotifications ? 'On' : 'Off'}
1177+
</Typography>
1178+
{selectedEventType.sendSlackNotifications && !selectedTeams.length && !workPackageIds.length && (
1179+
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
1180+
<WarningAmberIcon sx={{ color: 'error.main', fontSize: 18 }} />
1181+
<Typography variant="body2" color="error.main">
1182+
Add{' '}
1183+
{selectedEventType.teams && selectedEventType.workPackage
1184+
? 'a team or work package'
1185+
: selectedEventType.teams
1186+
? 'a team'
1187+
: 'a work package'}{' '}
1188+
to send notifications
1189+
</Typography>
1190+
</Box>
1191+
)}
1192+
</Box>
1193+
</Tooltip>
1194+
</Box>
1195+
)}
11541196
{/* Required Members Section */}
11551197
{selectedEventType?.requiredMembers && (
11561198
<Box sx={{ display: 'flex', alignItems: 'flex-start', gap: 2 }}>
@@ -1417,7 +1459,9 @@ const EventModal: React.FC<BaseEventModalProps> = ({
14171459
value={workPackageOptions.find((wp) => value?.[0] === wp.id) || null}
14181460
onChange={(_, newValue) => {
14191461
if (newValue?.id !== 'loading') {
1420-
onChange(newValue ? [newValue.id] : []);
1462+
const ids = newValue ? [newValue.id] : [];
1463+
onChange(ids);
1464+
setWorkPackageIds(ids);
14211465
}
14221466
}}
14231467
getOptionLabel={(option) => option.label}

src/frontend/src/pages/CalendarPage/EventClickPopup.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ import EditEventModal from './Components/EditEventModal';
3939
import DeleteSeriesConfirmationModal from './Components/DeleteSeriesConfirmationModal';
4040
import { useToast } from '../../hooks/toasts.hooks';
4141
import NERDeleteModal from '../../components/NERDeleteModal';
42-
42+
import NotificationsIcon from '@mui/icons-material/Notifications';
4343
import { getPendingReason } from '../../utils/calendar.utils';
4444

4545
export const getStatusIcon = (status: string, isLarge?: boolean) => {
@@ -436,6 +436,9 @@ export const EventClickContent: React.FC<EventClickContentProps> = ({
436436
<Typography variant="body2" sx={{ flex: 1 }}>
437437
<b>Status:</b> {event.status}
438438
</Typography>
439+
{specificEventType?.sendSlackNotifications && (event.teams.length > 0 || event.workPackages.length > 0) && (
440+
<NotificationsIcon sx={{ color: 'white', fontSize: 18, mt: '3px', flexShrink: 0 }} />
441+
)}
439442
</Stack>
440443
)}
441444

0 commit comments

Comments
 (0)