Skip to content

Commit 58c69a0

Browse files
committed
Merge branch 'kl/scrum-168-image-enhancements' of https://github.com/UTSC-CSCC01-Software-Engineering-I/term-group-project-c01w25-project-course-matrix into kl/scrum-168-image-enhancements
2 parents 1fe358f + c866938 commit 58c69a0

File tree

10 files changed

+129
-79
lines changed

10 files changed

+129
-79
lines changed

course-matrix/backend/__tests__/timetablesController.test.ts

Lines changed: 1 addition & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -320,27 +320,6 @@ describe("PUT /api/timetables/:id", () => {
320320
beforeEach(() => {
321321
jest.clearAllMocks();
322322
});
323-
test("should return error code 400 and message 'New timetable title or semester or updated favorite status is required when updating a timetable' if request body is empty", async () => {
324-
// Make sure the test user is authenticated
325-
const user_id = "testuser04-f84fd0da-d775-4424-ad88-d9675282453c";
326-
const timetableData = {};
327-
328-
// Mock authHandler to simulate the user being logged in
329-
(
330-
authHandler as jest.MockedFunction<typeof authHandler>
331-
).mockImplementationOnce(mockAuthHandler(user_id));
332-
333-
const response = await request(app)
334-
.put("/api/timetables/1")
335-
.send(timetableData);
336-
337-
// Check that the `update` method was called
338-
expect(response.statusCode).toBe(400);
339-
expect(response.body).toEqual({
340-
error:
341-
"New timetable title or semester or updated favorite status or email notifications enabled is required when updating a timetable",
342-
});
343-
});
344323

345324
test("should update the timetable successfully", async () => {
346325
// Make sure the test user is authenticated
@@ -361,7 +340,7 @@ describe("PUT /api/timetables/:id", () => {
361340

362341
// Check that the `update` method was called
363342
expect(response.statusCode).toBe(200);
364-
expect(response.body).toEqual({
343+
expect(response.body).toMatchObject({
365344
timetable_title: "Updated Title",
366345
semester: "Spring 2025",
367346
});

course-matrix/backend/src/constants/availableFunctions.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ export const availableFunctions: AvailableFunctions = {
116116
}
117117

118118
let updateData: any = {};
119+
updateData.updated_at = new Date().toISOString();
119120
if (timetable_title) updateData.timetable_title = timetable_title;
120121
if (semester) updateData.semester = semester;
121122

course-matrix/backend/src/controllers/timetablesController.ts

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -179,17 +179,6 @@ export default {
179179
favorite,
180180
email_notifications_enabled,
181181
} = req.body;
182-
if (
183-
!timetable_title &&
184-
!semester &&
185-
favorite === undefined &&
186-
email_notifications_enabled === undefined
187-
) {
188-
return res.status(400).json({
189-
error:
190-
"New timetable title or semester or updated favorite status or email notifications enabled is required when updating a timetable",
191-
});
192-
}
193182

194183
// Timetables cannot be longer than 50 characters.
195184
if (timetable_title && timetable_title.length > 50) {
@@ -236,6 +225,7 @@ export default {
236225
.json({ error: "Another timetable with this title already exists" });
237226
}
238227
let updateData: any = {};
228+
updateData.updated_at = new Date().toISOString();
239229
if (timetable_title) updateData.timetable_title = timetable_title;
240230
if (semester) updateData.semester = semester;
241231
if (favorite !== undefined) updateData.favorite = favorite;
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { describe, expect, it, test } from "@jest/globals";
2+
3+
import { convertTimestampToLocaleTime } from "../src/utils/convert-timestamp-to-locale-time";
4+
5+
describe("convertTimestampToLocaleTime", () => {
6+
test("should convert a valid timestamp string to a locale time string", () => {
7+
const timestamp = "2025-03-28T12:00:00Z";
8+
const result = convertTimestampToLocaleTime(timestamp);
9+
expect(typeof result).toBe("string");
10+
expect(result.length).toBeGreaterThan(0); // Ensures it returns a non-empty string
11+
});
12+
13+
test("should convert a valid numeric timestamp to a locale time string", () => {
14+
const timestamp = 1711622400000; // Equivalent to 2025-03-28T12:00:00Z
15+
// in milliseconds
16+
const result = convertTimestampToLocaleTime(timestamp);
17+
expect(typeof result).toBe("string");
18+
expect(result.length).toBeGreaterThan(0);
19+
});
20+
21+
test("convert to locale time date is different", () => {
22+
const timestamp = "2025-03-28 02:33:02.589Z";
23+
const result = convertTimestampToLocaleTime(timestamp);
24+
expect(typeof result).toBe("string");
25+
expect(result.length).toBeGreaterThan(0);
26+
});
27+
28+
test("should return 'Invalid Date' for an invalid timestamp", () => {
29+
const timestamp = "invalid";
30+
const result = convertTimestampToLocaleTime(timestamp);
31+
expect(result).toBe("Invalid Date");
32+
});
33+
});

course-matrix/frontend/src/models/timetable-form.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ export const RestrictionSchema = z
106106
})
107107
.refine(
108108
(data) => {
109-
if (data.startTime && data.endTime) {
109+
if (data.type === "Restrict Between" && data.startTime && data.endTime) {
110110
return data.startTime < data.endTime;
111111
}
112112
return true; // Allow if either undefined

course-matrix/frontend/src/pages/Home/TimetableCard.tsx

Lines changed: 21 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
import { Link } from "react-router-dom";
2121
import { TimetableModel } from "@/models/models";
2222
import { SemesterIcon } from "@/components/semester-icon";
23+
import { convertTimestampToLocaleTime } from "../../utils/convert-timestamp-to-locale-time";
2324

2425
const semesterToBgColor = (semester: string) => {
2526
if (semester.startsWith("Fall")) {
@@ -66,7 +67,6 @@ const TimetableCard = ({
6667
/// small blurred version
6768

6869
const [updateTimetable] = useUpdateTimetableMutation();
69-
7070
const timetableId = timetable.id;
7171

7272
const user_metadata = JSON.parse(localStorage.getItem("userInfo") ?? "{}");
@@ -78,20 +78,13 @@ const TimetableCard = ({
7878
? (usernameData ?? "John Doe")
7979
: loggedInUsername;
8080

81-
const lastEditedDateArray = lastEditedDate
82-
.toISOString()
83-
.split("T")[0]
84-
.split("-");
85-
const lastEditedYear = lastEditedDateArray[0];
86-
const lastEditedMonth = lastEditedDateArray[1];
87-
const lastEditedDay = lastEditedDateArray[2];
88-
const lastEditedDateTimestamp =
89-
lastEditedMonth + "/" + lastEditedDay + "/" + lastEditedYear;
90-
9181
const [timetableCardTitle, setTimetableCardTitle] = useState(title);
9282
const [isEditingTitle, setIsEditingTitle] = useState(false);
93-
const { data } = useGetTimetableQuery(timetableId);
83+
const { data, refetch } = useGetTimetableQuery(timetableId);
9484
const [toggled, setToggled] = useState(favorite);
85+
const [lastEdited, setLastEdited] = useState(
86+
convertTimestampToLocaleTime(lastEditedDate.toISOString()).split(",")[0],
87+
);
9588

9689
const handleSave = async () => {
9790
try {
@@ -105,6 +98,7 @@ const TimetableCard = ({
10598
setErrorMessage(errorData?.error ?? "Unknown error occurred");
10699
return;
107100
}
101+
refetch();
108102
};
109103

110104
useEffect(() => {
@@ -113,6 +107,11 @@ const TimetableCard = ({
113107
if (val !== undefined) {
114108
setToggled(val);
115109
}
110+
setLastEdited(
111+
convertTimestampToLocaleTime(
112+
(data as TimetableModel[])[0]?.updated_at,
113+
).split(",")[0],
114+
);
116115
}
117116
}, [data]);
118117

@@ -173,7 +172,15 @@ const TimetableCard = ({
173172
</CardHeader>
174173
<CardContent className="-mt-3">
175174
<CardDescription className="flex justify-between text-xs">
176-
<div>Last edited {lastEditedDateTimestamp}</div>
175+
<div>
176+
Last edited{" "}
177+
{
178+
convertTimestampToLocaleTime(lastEditedDate.toISOString()).split(
179+
",",
180+
)[0]
181+
}
182+
</div>
183+
177184
<div>Owned by: {ownerUsername}</div>
178185
</CardDescription>
179186
</CardContent>
@@ -247,7 +254,7 @@ const TimetableCard = ({
247254
</CardHeader>
248255
<CardContent className="-mt-3">
249256
<CardDescription className="flex justify-between text-xs">
250-
<div>Last edited {lastEditedDateTimestamp}</div>
257+
<div>Last edited {lastEdited}</div>
251258
<div>Owned by: {ownerUsername}</div>
252259
</CardDescription>
253260
</CardContent>

course-matrix/frontend/src/pages/TimetableBuilder/Calendar.tsx

Lines changed: 53 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import { TimetableFormSchema } from "@/models/timetable-form";
2828
import {
2929
useGetTimetablesQuery,
3030
useCreateTimetableMutation,
31+
useUpdateTimetableMutation,
3132
} from "@/api/timetableApiSlice";
3233
import {
3334
useGetRestrictionsQuery,
@@ -41,6 +42,7 @@ import {
4142
useCreateEventMutation,
4243
useGetEventsQuery,
4344
useDeleteEventMutation,
45+
useUpdateEventMutation,
4446
} from "@/api/eventsApiSlice";
4547
import { useGetOfferingsQuery } from "@/api/offeringsApiSlice";
4648
import { useGetOfferingEventsQuery } from "@/api/offeringsApiSlice";
@@ -89,13 +91,16 @@ const Calendar = React.memo<CalendarProps>(
8991
const editingTimetableId = parseInt(queryParams.get("edit") ?? "0");
9092

9193
const [createTimetable] = useCreateTimetableMutation();
94+
const [updateTimetable] = useUpdateTimetableMutation();
9295
const [createEvent] = useCreateEventMutation();
9396
const [deleteEvent] = useDeleteEventMutation();
9497
const [createRestriction] = useCreateRestrictionMutation();
9598
const [deleteRestriction] = useDeleteRestrictionMutation();
9699

97100
const [errorMessage, setErrorMessage] = useState<string | null>(null);
98-
101+
const [updateErrorMessage, setUpdateErrorMessage] = useState<string | null>(
102+
null,
103+
);
99104
const semesterStartDate = getSemesterStartAndEndDates(semester).start;
100105
const { start: semesterStartDatePlusOneWeek, end: semesterEndDate } =
101106
getSemesterStartAndEndDatesPlusOneWeek(semester);
@@ -262,13 +267,20 @@ const Calendar = React.memo<CalendarProps>(
262267
};
263268

264269
const handleUpdate = async () => {
270+
const timetableTitle = timetableTitleRef.current?.value ?? "";
265271
setShowLoadingPage(true);
272+
266273
const offeringIdsToDelete = oldOfferingIds.filter(
267274
(offeringId) => !newOfferingIds.includes(offeringId),
268275
);
269276
const offeringIdsToAdd = newOfferingIds.filter(
270277
(offeringId) => !oldOfferingIds.includes(offeringId),
271278
);
279+
if (offeringIdsToAdd.length === 0 && offeringIdsToDelete.length === 0) {
280+
setUpdateErrorMessage("You have made no changes to the timetable!");
281+
setShowLoadingPage(false);
282+
return;
283+
}
272284
// Delete course events
273285
for (const offeringId of offeringIdsToDelete) {
274286
const { error: deleteError } = await deleteEvent({
@@ -322,12 +334,23 @@ const Calendar = React.memo<CalendarProps>(
322334
console.error(restrictionError);
323335
}
324336
}
337+
338+
try {
339+
await updateTimetable({
340+
id: editingTimetableId,
341+
timetable_title: timetableTitle,
342+
}).unwrap();
343+
} catch (error) {
344+
setUpdateErrorMessage("You have made no changes to the timetable");
345+
setShowLoadingPage(false);
346+
return;
347+
}
325348
navigate("/home");
326349
};
327350

328351
return (
329352
<div>
330-
<h1 className="text-2xl flex flex-row justify-between font-medium tracking-tight mb-8">
353+
<h1 className="text-2xl flex flex-row justify-between font-medium tracking-tight mb-4">
331354
<div>{header}</div>
332355
<TimetableErrorDialog
333356
errorMessage={errorMessage}
@@ -381,29 +404,35 @@ const Calendar = React.memo<CalendarProps>(
381404
</DialogContent>
382405
</Dialog>
383406
) : (
384-
<div className="flex gap-2">
385-
{isChoosingSectionsManually &&
386-
!allOfferingSectionsHaveBeenSelected && (
387-
<p className="text-sm text-red-500 pr-2">
388-
Please select all LEC/TUT/PRA sections for your courses in
389-
order to save your timetable.
390-
</p>
391-
)}
407+
<div>
408+
<div className="flex gap-2">
409+
{isChoosingSectionsManually &&
410+
!allOfferingSectionsHaveBeenSelected && (
411+
<p className="text-sm text-red-500 pr-2">
412+
Please select all LEC/TUT/PRA sections for your courses in
413+
order to save your timetable.
414+
</p>
415+
)}
392416

393-
<Button
394-
size="sm"
395-
variant="outline"
396-
onClick={() => navigate("/home")}
397-
>
398-
Cancel Editing
399-
</Button>
400-
<Button
401-
size="sm"
402-
disabled={!allOfferingSectionsHaveBeenSelected}
403-
onClick={handleUpdate}
404-
>
405-
Update Timetable
406-
</Button>
417+
<Button
418+
size="sm"
419+
variant="outline"
420+
onClick={() => navigate("/home")}
421+
>
422+
Cancel Editing
423+
</Button>
424+
<Button
425+
size="sm"
426+
disabled={!allOfferingSectionsHaveBeenSelected}
427+
onClick={handleUpdate}
428+
>
429+
Update Timetable
430+
</Button>
431+
</div>
432+
<div className="mt-1 text-sm text-red-500 font-bold">
433+
{" "}
434+
{updateErrorMessage}{" "}
435+
</div>
407436
</div>
408437
)}
409438
</h1>

course-matrix/frontend/src/pages/TimetableBuilder/GeneratedCalendars.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import {
1616
} from "@/utils/semester-utils";
1717
import { courseEventStyles } from "@/constants/calendarConstants";
1818
import { createEventModalPlugin } from "@schedule-x/event-modal";
19-
import React, { useRef, useState } from "react";
19+
import React, { useEffect, useRef, useState } from "react";
2020
import { useGetOfferingEventsQuery } from "@/api/offeringsApiSlice";
2121
import { Button } from "@/components/ui/button";
2222
import {
@@ -107,7 +107,7 @@ export const GeneratedCalendars = React.memo<GeneratedCalendarsProps>(
107107

108108
const { data: courseEventsData, isLoading } = useGetOfferingEventsQuery({
109109
offering_ids: currentTimetableOfferings
110-
.map((offering) => offering.id)
110+
?.map((offering) => offering.id)
111111
.join(","),
112112
semester_start_date: semesterStartDate,
113113
semester_end_date: semesterEndDate,
@@ -200,6 +200,10 @@ export const GeneratedCalendars = React.memo<GeneratedCalendarsProps>(
200200
isResponsive: false,
201201
});
202202

203+
useEffect(() => {
204+
setCurrentTimetableIndex(0);
205+
}, [generatedTimetables]);
206+
203207
return (
204208
<>
205209
<div>
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export function convertTimestampToLocaleTime(
2+
timestampz: string | number,
3+
): string {
4+
const date = new Date(timestampz);
5+
6+
return date.toLocaleString(); // Uses system's default locale
7+
}

course-matrix/frontend/tailwind.config.js

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -78,15 +78,15 @@ export default {
7878
height: "0",
7979
},
8080
},
81-
'fade-in': {
82-
'0%': { opacity: '0' },
83-
'100%': { opacity: '1' },
84-
}
81+
"fade-in": {
82+
"0%": { opacity: "0" },
83+
"100%": { opacity: "1" },
84+
},
8585
},
8686
animation: {
8787
"accordion-down": "accordion-down 0.2s ease-out",
8888
"accordion-up": "accordion-up 0.2s ease-out",
89-
'fade-in': 'fade-in 0.4s ease-in-out',
89+
"fade-in": "fade-in 0.4s ease-in-out",
9090
},
9191
},
9292
},

0 commit comments

Comments
 (0)