Skip to content

Commit b882ffc

Browse files
authored
Merge pull request #2234 from Northeastern-Electric-Racing/#2228-confirm-availability-flow
#2228 - update slack ping, setup confirm availability flow
2 parents 374fc2d + 6029a08 commit b882ffc

10 files changed

Lines changed: 125 additions & 29 deletions

File tree

src/backend/src/services/design-reviews.services.ts

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,11 @@ export default class DesignReviewsService {
162162
for (const memberUserSetting of memberUserSettings) {
163163
if (memberUserSetting.slackId) {
164164
try {
165-
await sendSlackDesignReviewNotification(memberUserSetting.slackId, designReview.designReviewId);
165+
await sendSlackDesignReviewNotification(
166+
memberUserSetting.slackId,
167+
designReview.designReviewId,
168+
designReview.wbsElement.name
169+
);
166170
} catch (err: unknown) {
167171
if (err instanceof Error) {
168172
throw new HttpException(500, `Failed to send slack notification: ${err.message}`);
@@ -321,19 +325,22 @@ export default class DesignReviewsService {
321325
if (!isUserOnDesignReview(submitter, designReviewTransformer(designReview)))
322326
throw new HttpException(400, 'Current user is not in the list of this design reviews members');
323327

324-
// Update user schedule settings
325-
const validAvailability = validateMeetingTimes(availability);
328+
availability.forEach((time) => {
329+
if (time < 0 || time > 83) {
330+
throw new HttpException(400, 'Availability times have to be in range 0-83');
331+
}
332+
});
326333

327334
await prisma.schedule_Settings.upsert({
328335
where: { userId: submitter.userId },
329336
update: {
330-
availability: validAvailability
337+
availability
331338
},
332339
create: {
333340
userId: submitter.userId,
334341
personalGmail: '',
335342
personalZoomLink: '',
336-
availability: validAvailability
343+
availability
337344
}
338345
});
339346

@@ -353,7 +360,7 @@ export default class DesignReviewsService {
353360

354361
// If all requested attendees have confirmed their schedule, mark design review as confirmed
355362
if (
356-
designReview.confirmedMembers.length ===
363+
updatedDesignReview.confirmedMembers.length ===
357364
designReview.requiredMembers.length + designReview.optionalMembers.length
358365
) {
359366
await prisma.design_Review.update({

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

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -91,11 +91,15 @@ export const sendReimbursementRequestDeniedNotification = async (slackId: string
9191
}
9292
};
9393

94-
export const sendSlackDesignReviewNotification = async (slackId: string, designReviewId: string) => {
94+
export const sendSlackDesignReviewNotification = async (
95+
slackId: string,
96+
designReviewId: string,
97+
designReviewName: string
98+
) => {
9599
if (process.env.NODE_ENV !== 'production') return; // don't send msgs unless in prod
96-
const msg = `You have been invited to a Design Review!`;
97-
const fullLink = `https://finishlinebyner.com/design-reviews/${designReviewId}`;
98-
const linkButtonText = 'RSVP for the Design Review';
100+
const msg = `You have been invited to the ${designReviewName} Design Review!`;
101+
const fullLink = `https://finishlinebyner.com/settings/preferences?drId=${designReviewId}`;
102+
const linkButtonText = 'Confirm Availability';
99103

100104
try {
101105
await sendMessage(slackId, msg, fullLink, linkButtonText);

src/backend/tests/design-reviews.test.ts

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -535,14 +535,7 @@ describe('Design Reviews', () => {
535535
vi.spyOn(prisma.design_Review, 'findUnique').mockResolvedValue(prismaDesignReview5);
536536
await expect(() =>
537537
DesignReviewsService.markUserConfirmed(prismaDesignReview5.designReviewId, [0, 85], batman)
538-
).rejects.toThrow(new HttpException(400, 'Meeting times have to be in range 0-83'));
539-
});
540-
541-
test('Availabilities were invalid - non-consecutive', async () => {
542-
vi.spyOn(prisma.design_Review, 'findUnique').mockResolvedValue(prismaDesignReview5);
543-
await expect(() =>
544-
DesignReviewsService.markUserConfirmed(prismaDesignReview5.designReviewId, [1, 3], batman)
545-
).rejects.toThrow(new HttpException(400, 'Meeting times have to be consecutive'));
538+
).rejects.toThrow(new HttpException(400, 'Availability times have to be in range 0-83'));
546539
});
547540
});
548541
});

src/frontend/src/apis/design-reviews.api.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,3 +57,7 @@ export const getSingleDesignReview = async (id: string) => {
5757
transformResponse: (data) => designReviewTransformer(JSON.parse(data))
5858
});
5959
};
60+
61+
export const markUserConfirmed = async (id: string, payload: { availability: number[] }) => {
62+
return axios.post<DesignReview>(apiUrls.designReviewMarkUserConfirmed(id), payload);
63+
};

src/frontend/src/hooks/design-reviews.hooks.ts

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,10 @@ import {
99
createDesignReviews,
1010
getAllDesignReviews,
1111
getAllTeamTypes,
12-
getSingleDesignReview
12+
getSingleDesignReview,
13+
markUserConfirmed
1314
} from '../apis/design-reviews.api';
15+
import { useCurrentUser } from './users.hooks';
1416

1517
export interface CreateDesignReviewsPayload {
1618
dateScheduled: Date;
@@ -101,9 +103,31 @@ export const useAllTeamTypes = () => {
101103
*
102104
* @returns a single design review
103105
*/
104-
export const useSingleDesignReview = (id: string) => {
105-
return useQuery<DesignReview, Error>(['design-reviews', id], async () => {
106-
const { data } = await getSingleDesignReview(id);
107-
return data;
108-
});
106+
export const useSingleDesignReview = (id?: string) => {
107+
return useQuery<DesignReview, Error>(
108+
['design-reviews', id],
109+
async () => {
110+
const { data } = await getSingleDesignReview(id!);
111+
return data;
112+
},
113+
{ enabled: !!id }
114+
);
115+
};
116+
117+
export const useMarkUserConfirmed = (id: string) => {
118+
const user = useCurrentUser();
119+
const queryClient = useQueryClient();
120+
return useMutation<DesignReview, Error, { availability: number[] }>(
121+
['design-reviews', 'mark-confirmed'],
122+
async (designReviewPayload: { availability: number[] }) => {
123+
const { data } = await markUserConfirmed(id, designReviewPayload);
124+
return data;
125+
},
126+
{
127+
onSuccess: () => {
128+
queryClient.invalidateQueries(['design-reviews']);
129+
queryClient.invalidateQueries(['users', user.userId, 'schedule-settings']);
130+
}
131+
}
132+
);
109133
};

src/frontend/src/pages/SettingsPage/UserScheduleSettings/Availability/AvailabilityEditModal.tsx

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,21 @@ interface DRCEditModalProps {
77
availabilites: number[];
88
setAvailabilities: (availabilities: number[]) => void;
99
onHide: () => void;
10+
onSubmit: () => void;
1011
}
1112

12-
const AvailabilityEditModal: React.FC<DRCEditModalProps> = ({ open, onHide, header, availabilites, setAvailabilities }) => {
13+
const AvailabilityEditModal: React.FC<DRCEditModalProps> = ({
14+
open,
15+
onHide,
16+
header,
17+
availabilites,
18+
setAvailabilities,
19+
onSubmit
20+
}) => {
1321
const existingMeetingData = new Map<number, string>();
1422

1523
return (
16-
<NERModal open={open} onHide={onHide} title={header} onSubmit={onHide} submitText="Save">
24+
<NERModal open={open} onHide={onHide} title={header} onSubmit={onSubmit} submitText="Save">
1725
<EditAvailability
1826
selectedTimes={availabilites}
1927
setSelectedTimes={setAvailabilities}

src/frontend/src/pages/SettingsPage/UserScheduleSettings/UserScheduleSettings.tsx

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ import { useUpdateUserScheduleSettings, useUserScheduleSettings } from '../../..
1616
import LoadingIndicator from '../../../components/LoadingIndicator';
1717
import ErrorPage from '../../ErrorPage';
1818
import { useToast } from '../../../hooks/toasts.hooks';
19+
import { useSingleDesignReview } from '../../../hooks/design-reviews.hooks';
20+
import { useQuery } from '../../../hooks/utils.hooks';
1921

2022
export interface ScheduleSettingsFormInput {
2123
personalGmail: string;
@@ -29,6 +31,8 @@ export interface ScheduleSettingsPayload extends ScheduleSettingsFormInput {
2931
const UserScheduleSettings = ({ user }: { user: User }) => {
3032
const [edit, setEdit] = useState(false);
3133
const toast = useToast();
34+
const query = useQuery();
35+
const designReviewId = query.get('drId');
3236

3337
const { data, isLoading, isError, error } = useUserScheduleSettings(user.userId);
3438
const {
@@ -37,9 +41,17 @@ const UserScheduleSettings = ({ user }: { user: User }) => {
3741
isError: updateUserScheduleSettingsIsError,
3842
error: updateUserScheduleSettingsError
3943
} = useUpdateUserScheduleSettings();
44+
const {
45+
data: designReview,
46+
isError: designReviewIsError,
47+
error: designReviewError,
48+
isLoading: designReviewIsLoading
49+
} = useSingleDesignReview(designReviewId ?? undefined);
4050

51+
if (designReviewId && (!designReview || designReviewIsLoading)) return <LoadingIndicator />;
4152
if (!data || isLoading || updateUserScheduleSettingsIsLoading) return <LoadingIndicator />;
4253

54+
if (designReviewId && designReviewIsError) return <ErrorPage message={designReviewError.message} />;
4355
if (isError) return <ErrorPage error={error} message={error.message} />;
4456
if (updateUserScheduleSettingsIsError)
4557
return <ErrorPage error={updateUserScheduleSettingsError!} message={updateUserScheduleSettingsError?.message} />;
@@ -81,7 +93,7 @@ const UserScheduleSettings = ({ user }: { user: User }) => {
8193
}
8294
>
8395
{!edit ? (
84-
<UserScheduleSettingsView scheduleSettings={data} />
96+
<UserScheduleSettingsView scheduleSettings={data} designReview={designReview} />
8597
) : (
8698
<UserScheduleSettingsEdit onSubmit={handleConfirm} defaultValues={data} />
8799
)}

src/frontend/src/pages/SettingsPage/UserScheduleSettings/UserScheduleSettingsEdit.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ const UserScheduleSettingsEdit: React.FC<UserScheduleSettingsEditProps> = ({ onS
5353
<AvailabilityEditModal
5454
open={editAvailabilityOpen}
5555
onHide={() => setEditAvailability(false)}
56+
onSubmit={() => setEditAvailability(false)}
5657
header="Edit Availability"
5758
availabilites={availabilities}
5859
setAvailabilities={setAvailabilities}

src/frontend/src/pages/SettingsPage/UserScheduleSettings/UserScheduleSettingsView.tsx

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,45 @@
66
import { Grid } from '@mui/material';
77
import DetailDisplay from '../../../components/DetailDisplay';
88
import { NERButton } from '../../../components/NERButton';
9-
import { UserScheduleSettings } from 'shared';
9+
import { DesignReview, UserScheduleSettings } from 'shared';
1010
import { useState } from 'react';
1111
import SingleAvailabilityModal from './Availability/SingleAvailabilityModal';
12+
import { useCurrentUser } from '../../../hooks/users.hooks';
13+
import AvailabilityEditModal from './Availability/AvailabilityEditModal';
14+
import { useMarkUserConfirmed } from '../../../hooks/design-reviews.hooks';
15+
import { useToast } from '../../../hooks/toasts.hooks';
1216

13-
const UserScheduleSettingsView = ({ scheduleSettings }: { scheduleSettings: UserScheduleSettings }) => {
17+
const UserScheduleSettingsView = ({
18+
scheduleSettings,
19+
designReview
20+
}: {
21+
scheduleSettings: UserScheduleSettings;
22+
designReview?: DesignReview;
23+
}) => {
1424
const [availabilityOpen, setAvailabilityOpen] = useState(false);
25+
const toast = useToast();
26+
const user = useCurrentUser();
27+
const defaultOpen = designReview && !designReview.confirmedMembers.map((user) => user.userId).includes(user.userId);
28+
const [confirmAvailabilityOpen, setConfirmAvailabilityOpen] = useState(defaultOpen || false);
29+
const [confirmedAvailabilities, setConfirmedAvailabilities] = useState(scheduleSettings.availability);
30+
const { mutateAsync } = useMarkUserConfirmed(designReview?.designReviewId || '');
31+
const confirmModalTitle = designReview
32+
? `Update your availability for the ${designReview?.wbsName} Design Review on the week of ${new Date(
33+
designReview.dateScheduled.getTime() - designReview.dateScheduled.getTimezoneOffset() * -60000
34+
).toLocaleDateString()}`
35+
: '';
36+
37+
const handleConfirm = async (payload: { availability: number[] }) => {
38+
setConfirmAvailabilityOpen(false);
39+
try {
40+
await mutateAsync(payload);
41+
toast.success('Availability Confirmed!');
42+
} catch (e) {
43+
if (e instanceof Error) {
44+
toast.error(e.message);
45+
}
46+
}
47+
};
1548

1649
return (
1750
<Grid container spacing={6} sx={{ pt: '10px' }}>
@@ -21,6 +54,14 @@ const UserScheduleSettingsView = ({ scheduleSettings }: { scheduleSettings: User
2154
header={'Availability'}
2255
availabilites={scheduleSettings.availability}
2356
/>
57+
<AvailabilityEditModal
58+
open={confirmAvailabilityOpen}
59+
onHide={() => setConfirmAvailabilityOpen(false)}
60+
header={confirmModalTitle}
61+
availabilites={confirmedAvailabilities}
62+
setAvailabilities={setConfirmedAvailabilities}
63+
onSubmit={() => handleConfirm({ availability: confirmedAvailabilities })}
64+
/>
2465
<Grid item xs={12} sm={6} lg={4}>
2566
<DetailDisplay label="Personal Google Email" content={scheduleSettings.personalGmail} />
2667
</Grid>

src/frontend/src/utils/urls.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,7 @@ const designReviewsCreate = () => `${designReviews()}/create`;
137137
const teamTypes = () => `${designReviews()}/teamType/all`;
138138
const designReviewsEdit = (designReviewId: string) => `${designReviews()}/${designReviewId}/edit`;
139139
const designReviewById = (id: string) => `${designReviews()}/${id}`;
140+
const designReviewMarkUserConfirmed = (id: string) => `${designReviewById(id)}/confirm-schedule`;
140141

141142
/**************** Other Endpoints ****************/
142143
const version = () => `https://api.github.com/repos/Northeastern-Electric-Racing/FinishLine/releases/latest`;
@@ -248,6 +249,7 @@ export const apiUrls = {
248249
designReviewsCreate,
249250
designReviewById,
250251
designReviewsEdit,
252+
designReviewMarkUserConfirmed,
251253
teamTypes,
252254

253255
version

0 commit comments

Comments
 (0)