Skip to content

Commit 22956a8

Browse files
committed
Merge branch 'develop' into feature/seed-data-revamp
2 parents ea3f6f3 + 6cbe307 commit 22956a8

23 files changed

Lines changed: 314 additions & 119 deletions

src/backend/src/controllers/reimbursement-requests.controllers.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -496,14 +496,14 @@ export default class ReimbursementRequestsController {
496496
const editedVendor = await ReimbursementRequestService.editVendor(
497497
name,
498498
vendorId,
499-
username,
500-
password,
501-
discountCode,
502499
taxExempt,
503500
twoFactorContacts,
504-
notes,
505501
req.currentUser,
506-
req.organization
502+
req.organization,
503+
username,
504+
password,
505+
discountCode,
506+
notes
507507
);
508508
res.status(200).json(editedVendor);
509509
} catch (error: unknown) {

src/backend/src/routes/reimbursement-requests.routes.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -96,13 +96,13 @@ reimbursementRequestsRouter.get('/reimbursements', ReimbursementRequestControlle
9696
reimbursementRequestsRouter.post(
9797
'/:vendorId/vendors/edit',
9898
nonEmptyString(body('name')),
99-
nonEmptyString(body('username')).optional(),
100-
nonEmptyString(body('password')).optional(),
101-
nonEmptyString(body('discountCode')).optional(),
99+
nonEmptyString(body('username')).optional({ checkFalsy: true }),
100+
nonEmptyString(body('password')).optional({ checkFalsy: true }),
101+
nonEmptyString(body('discountCode')).optional({ checkFalsy: true }),
102102
body('taxExempt').isBoolean(),
103103
body('twoFactorContacts').isArray(),
104104
nonEmptyString(body('twoFactorContacts.*')),
105-
nonEmptyString(body('notes')).optional(),
105+
nonEmptyString(body('notes')).optional({ checkFalsy: true }),
106106
validateInputs,
107107
ReimbursementRequestController.editVendor
108108
);

src/backend/src/routes/tasks.routes.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ tasksRouter.post(
4242
tasksRouter.post(
4343
'/:taskId/edit',
4444
nonEmptyString(body('title')),
45-
nonEmptyString(body('notes')),
45+
body('notes').isString(),
4646
isOptionalDateOnly(body('deadline')),
4747
isOptionalDateOnly(body('startDate')),
4848
isTaskPriority(body('priority')),

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

Lines changed: 60 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';
@@ -274,6 +274,7 @@ export default class CalendarService {
274274
description?: string,
275275
mention?: SlackMentionType
276276
): Promise<Event> {
277+
if (!title.trim()) throw new HttpException(400, 'Title cannot be only whitespace');
277278
// Validate eventTypeId
278279
const foundEventType = await prisma.event_Type.findUnique({
279280
where: { eventTypeId }
@@ -502,11 +503,7 @@ export default class CalendarService {
502503

503504
if (foundEventType.sendSlackNotifications) {
504505
const members = await prisma.user.findMany({
505-
where: {
506-
userId: {
507-
in: optionalMemberIds.concat(allRequiredMembers)
508-
}
509-
}
506+
where: { userId: { in: optionalMemberIds.concat(allRequiredMembers) } }
510507
});
511508

512509
// get the user settings for all the members invited, who are leaderingship
@@ -604,6 +601,7 @@ export default class CalendarService {
604601
zoomLink?: string,
605602
description?: string
606603
): Promise<Event> {
604+
if (!title.trim()) throw new HttpException(400, 'Title cannot be only whitespace');
607605
// validate eventId
608606
const foundEvent = await prisma.event.findUnique({
609607
where: { eventId },
@@ -799,13 +797,12 @@ export default class CalendarService {
799797
const edittedEvent = eventTransformer(updatedEvent);
800798

801799
if (status === Event_Status.SCHEDULED && foundEventType.sendSlackNotifications) {
802-
await sendEventScheduledSlackNotif(updatedEvent.notificationSlackThreads, edittedEvent);
800+
await sendEventScheduledSlackNotif(updatedEvent.notificationSlackThreads, edittedEvent, true);
803801
}
804802

805803
if (status === Event_Status.CONFIRMED && foundEventType.sendSlackNotifications) {
806804
await sendEventConfirmationToThread(updatedEvent.notificationSlackThreads, updatedEvent.userCreated);
807805
}
808-
809806
return edittedEvent;
810807
}
811808

@@ -941,7 +938,11 @@ export default class CalendarService {
941938
userCreatedId: true,
942939
location: true,
943940
dateDeleted: true,
944-
approved: true
941+
approved: true,
942+
status: true,
943+
title: true,
944+
workPackages: true,
945+
scheduledTimes: true
945946
}
946947
});
947948

@@ -1422,10 +1423,48 @@ export default class CalendarService {
14221423

14231424
if (!event) throw new NotFoundException('Event', eventId);
14241425
if (event.dateDeleted) throw new DeletedException('Event', eventId);
1425-
1426-
// Cannot schedule an already scheduled event
14271426
if (event.status === Event_Status.SCHEDULED) {
1428-
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+
});
14291468
}
14301469

14311470
// Only the event creator can schedule the event
@@ -1462,9 +1501,12 @@ export default class CalendarService {
14621501
allDay: false
14631502
}
14641503
},
1504+
1505+
initialDateScheduled: event.initialDateScheduled ?? null,
14651506
approved: hasConflict ? Conflict_Status.PENDING : event.approved,
14661507
approvalRequiredFromUserId: hasConflict ? conflictingEvent?.userCreated.userId : event.approvalRequiredFromUserId
14671508
},
1509+
14681510
...getEventQueryArgs(organization.organizationId)
14691511
});
14701512

@@ -1474,7 +1516,7 @@ export default class CalendarService {
14741516
const endHour = endTime.getHours();
14751517
for (const member of event.confirmedMembers) {
14761518
if (!member.drScheduleSettings) continue;
1477-
const existingAvailability = member.drScheduleSettings.availabilities.find((a) => isSameDay(a.dateSet, startTime));
1519+
const existingAvailability = member.drScheduleSettings.availabilities.find((a) => isSameDayUTC(a.dateSet, startTime));
14781520
if (!existingAvailability) continue;
14791521
// Availability index i represents local hour (10 + i); remove indices that fall within [startHour, endHour)
14801522
const updatedAvailability = existingAvailability.availability.filter(
@@ -1492,9 +1534,12 @@ export default class CalendarService {
14921534
});
14931535

14941536
if (foundEventType?.sendSlackNotifications) {
1495-
await sendEventScheduledSlackNotif(updatedEvent.notificationSlackThreads, eventTransformer(updatedEvent));
1537+
await sendEventScheduledSlackNotif(
1538+
updatedEvent.notificationSlackThreads,
1539+
eventTransformer(updatedEvent),
1540+
event.status === Event_Status.SCHEDULED
1541+
);
14961542
}
1497-
14981543
return eventTransformer(updatedEvent);
14991544
}
15001545

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

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,9 @@ export default class NotificationsService {
4444
},
4545
dateDeleted: null
4646
},
47+
orderBy: {
48+
deadline: 'asc' // earliest (most overdue) first
49+
},
4750
include: {
4851
assignees: {
4952
include: {
@@ -78,9 +81,10 @@ export default class NotificationsService {
7881
});
7982
});
8083

81-
// send the notifications to each team for their respective tasks
84+
// send the notifications to each team for their respective tasks sorted by deadline
8285
const promises = Array.from(teamTaskMap).map(async ([slackId, tasks]) => {
8386
const messageBlock = tasks
87+
.sort((a, b) => a.deadline!.getTime() - b.deadline!.getTime())
8488
.map((task) => {
8589
// prisma call earlier allows the forced unwrap (deadline is guaranteed to be a non-null value)
8690
const todayMidnightUTC = new Date(new Date().setUTCHours(0, 0, 0, 0));
@@ -236,7 +240,6 @@ export default class NotificationsService {
236240
static async sendSponsorTaskNotifications() {
237241
const startOfToday = new Date(new Date().setUTCHours(0, 0, 0, 0));
238242
const endOfToday = startOfDayTomorrow();
239-
240243
const sponsorTasks = await prisma.sponsor_Task.findMany({
241244
where: {
242245
notifyDate: {

src/backend/src/services/reimbursement-requests.services.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1510,14 +1510,14 @@ export default class ReimbursementRequestService {
15101510
static async editVendor(
15111511
name: string,
15121512
vendorId: string,
1513-
username: string,
1514-
password: string,
1515-
discountCode: string,
15161513
taxExempt: boolean,
15171514
twoFactorContacts: string[],
1518-
notes: string,
15191515
submitter: User,
1520-
organization: Organization
1516+
organization: Organization,
1517+
username?: string,
1518+
password?: string,
1519+
discountCode?: string,
1520+
notes?: string
15211521
): Promise<Vendor> {
15221522
const existingVendor = await prisma.vendor.findUnique({
15231523
where: { vendorId, dateDeleted: null },

src/backend/src/services/tasks.services.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,10 @@ export default class TasksService {
104104
if (!isUnderWordCount(title, 15)) throw new HttpException(400, 'Title must be less than 15 words');
105105
if (!isUnderWordCount(notes, 250)) throw new HttpException(400, 'Notes must be less than 250 words');
106106

107+
if (startDate && deadline && startDate > deadline) {
108+
throw new HttpException(400, 'Start date must be before or on the same day as the deadline');
109+
}
110+
107111
if (status === 'IN_PROGRESS' && (!deadline || assignees.length === 0)) {
108112
throw new HttpException(400, 'Tasks in progress must have a dealine and assignees');
109113
}
@@ -175,6 +179,13 @@ export default class TasksService {
175179
if (!isUnderWordCount(title, 15)) throw new HttpException(400, 'Title must be less than 15 words');
176180
if (!isUnderWordCount(notes, 250)) throw new HttpException(400, 'Notes must be less than 250 words');
177181

182+
const effectiveStartDate = startDate ?? originalTask.startDate ?? undefined;
183+
const effectiveDeadline = deadline ?? originalTask.deadline ?? undefined;
184+
185+
if (effectiveStartDate && effectiveDeadline && effectiveStartDate > effectiveDeadline) {
186+
throw new HttpException(400, 'Start date must be before or on the same day as the deadline');
187+
}
188+
178189
// if wbsNum passed, error if there's a problem with the wbs element
179190
if (wbsNum) {
180191
const newWbsElement = await prisma.wBS_Element.findUnique({

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/backend/src/utils/validation.utils.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ export const decimalMinZero = (validationObject: ValidationChain): ValidationCha
2424
};
2525

2626
export const nonEmptyString = (validationObject: ValidationChain): ValidationChain => {
27-
return validationObject.isString().not().isEmpty({ ignore_whitespace: true });
27+
return validationObject.isString().not().isEmpty();
2828
};
2929

3030
export const isRole = (validationObject: ValidationChain): ValidationChain => {

src/backend/tests/unit/reimbursement-requests.test.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -359,14 +359,14 @@ describe('Reimbursement Requests', () => {
359359
const editedVendor = await ReimbursementRequestService.editVendor(
360360
'Google',
361361
createdVendor.vendorId,
362-
'ner@gmail.com',
363-
'racecar',
364-
'DISCOUNT',
365362
false,
366363
[],
367-
'no notes',
368364
createdUser,
369-
org
365+
org,
366+
'ner@gmail.com',
367+
'racecar',
368+
'DISCOUNT',
369+
'no notes'
370370
);
371371
expect(editedVendor.name).toEqual('Google');
372372
expect(editedVendor.username).toEqual('ner@gmail.com');

0 commit comments

Comments
 (0)