Skip to content

Commit 23c291a

Browse files
authored
Merge branch 'develop' into kl/scrum-41-ai-timetable-generate
2 parents d8c50fc + ea8454e commit 23c291a

File tree

8 files changed

+154
-46
lines changed

8 files changed

+154
-46
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,8 @@ The `DATABASE_URL` variable should contain your Supabase project url and the `DA
6161

6262
```
6363
VITE_SERVER_URL="http://localhost:8081"
64+
VITE_PUBLIC_ASSISTANT_BASE_URL=[Insert vite public assistant bas URL]
65+
VITE_ASSISTANT_UI_KEY=[Insert vite assistant UI key]
6466
```
6567

6668
### Running the Application

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

Lines changed: 41 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -18,26 +18,51 @@ export default {
1818
const user_id = (req as any).user.id;
1919

2020
//Retrieve timetable title
21-
const { timetable_title, semester } = req.body;
22-
if (!timetable_title) {
23-
return res.status(400).json({ error: "timetable title is required" });
21+
const { timetable_title, semester, favorite = false } = req.body;
22+
if (!timetable_title || !semester) {
23+
return res
24+
.status(400)
25+
.json({ error: "timetable title and semester are required" });
26+
}
27+
28+
// Check if a timetable with the same title already exist for this user
29+
const { data: existingTimetable, error: existingTimetableError } =
30+
await supabase
31+
.schema("timetable")
32+
.from("timetables")
33+
.select("id")
34+
.eq("user_id", user_id)
35+
.eq("timetable_title", timetable_title)
36+
.maybeSingle();
37+
38+
if (existingTimetableError) {
39+
return res.status(400).json({ error: existingTimetableError.message });
2440
}
2541

26-
if (!semester) {
42+
if (existingTimetable) {
2743
return res
2844
.status(400)
29-
.json({ error: "timetable semester is required" });
45+
.json({ error: "A timetable with this title already exists" });
3046
}
3147

3248
//Create query to insert the user_id and timetable_title into the db
3349
let insertTimetable = supabase
3450
.schema("timetable")
3551
.from("timetables")
36-
.insert([{ user_id, timetable_title, semester }])
37-
.select();
52+
.insert([
53+
{
54+
user_id,
55+
timetable_title,
56+
semester,
57+
favorite,
58+
},
59+
])
60+
.select()
61+
.single();
3862

3963
const { data: timetableData, error: timetableError } =
4064
await insertTimetable;
65+
4166
if (timetableError) {
4267
return res.status(400).json({ error: timetableError.message });
4368
}
@@ -91,11 +116,11 @@ export default {
91116
const { id } = req.params;
92117

93118
//Retrieve timetable title
94-
const { timetable_title, semester } = req.body;
95-
if (!timetable_title && !semester) {
119+
const { timetable_title, semester, favorite } = req.body;
120+
if (!timetable_title && !semester && favorite === undefined) {
96121
return res.status(400).json({
97122
error:
98-
"New timetable title or semester is required when updating a timetable",
123+
"New timetable title or semester or updated favorite status is required when updating a timetable",
99124
});
100125
}
101126

@@ -112,26 +137,15 @@ export default {
112137
.eq("user_id", user_id)
113138
.maybeSingle();
114139

115-
const timetable_user_id = timetableUserData?.user_id;
116-
117-
if (timetableUserError)
118-
return res.status(400).json({ error: timetableUserError.message });
119-
120-
//Validate timetable validity:
121-
if (!timetableUserData || timetableUserData.length === 0) {
122-
return res.status(404).json({ error: "Calendar id not found" });
123-
}
124-
125-
//Validate user access
126-
if (user_id !== timetable_user_id) {
140+
if (timetableUserError || !timetableUserData)
127141
return res
128-
.status(401)
129-
.json({ error: "Unauthorized access to timetable events" });
130-
}
142+
.status(400)
143+
.json({ error: "Timetable not found or unauthorized" });
131144

132145
let updateData: any = {};
133146
if (timetable_title) updateData.timetable_title = timetable_title;
134147
if (semester) updateData.semester = semester;
148+
if (favorite !== undefined) updateData.favorite = favorite;
135149

136150
//Update timetable title, for authenticated user only
137151
let updateTimetableQuery = supabase
@@ -140,7 +154,8 @@ export default {
140154
.update(updateData)
141155
.eq("id", id)
142156
.eq("user_id", user_id)
143-
.select();
157+
.select()
158+
.single();
144159

145160
const { data: timetableData, error: timetableError } =
146161
await updateTimetableQuery;
@@ -180,7 +195,6 @@ export default {
180195
.eq("id", id)
181196
.eq("user_id", user_id)
182197
.maybeSingle();
183-
const timetable_user_id = timetableUserData?.user_id;
184198

185199
if (timetableUserError)
186200
return res.status(400).json({ error: timetableUserError.message });
@@ -190,13 +204,6 @@ export default {
190204
return res.status(404).json({ error: "Calendar id not found" });
191205
}
192206

193-
//Validate user access
194-
if (user_id !== timetable_user_id) {
195-
return res
196-
.status(401)
197-
.json({ error: "Unauthorized access to timetable events" });
198-
}
199-
200207
// Delete only if the timetable belongs to the authenticated user
201208
let deleteTimetableQuery = supabase
202209
.schema("timetable")

course-matrix/backend/src/routes/aiRouter.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export const aiRouter = express.Router();
99
* @description Handles user queries and generates responses using GPT-4o, with optional knowledge retrieval.
1010
*/
1111
aiRouter.post("/chat", authHandler, chat);
12+
1213
/**
1314
* @route POST /api/ai/test-similarity-search
1415
* @description Test vector database similarity search feature

course-matrix/frontend/src/components/time-picker-hr.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ export function TimePickerHr({ date, setDate }: TimePickerHrProps) {
6060
</div> */}
6161
<div className="grid gap-1 text-center">
6262
<Label htmlFor="period" className="text-xs">
63-
Period
63+
AM/PM
6464
</Label>
6565
<TimePeriodSelect
6666
period={period}

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

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,40 @@ export const RestrictionSchema = z
177177
message: "Number must be at least 1",
178178
path: ["numDays"],
179179
},
180+
)
181+
.refine(
182+
(data) => {
183+
if (
184+
data.type &&
185+
data.type === "Restrict Before" &&
186+
data.endTime?.getHours() === 0 &&
187+
data.endTime?.getMinutes() === 0
188+
) {
189+
return false;
190+
}
191+
return true;
192+
},
193+
{
194+
message: "Cannot restrict whole day",
195+
path: ["endTime"],
196+
},
197+
)
198+
.refine(
199+
(data) => {
200+
if (
201+
data.type &&
202+
data.type === "Restrict After" &&
203+
data.startTime?.getHours() === 0 &&
204+
data.startTime?.getMinutes() === 0
205+
) {
206+
return false;
207+
}
208+
return true;
209+
},
210+
{
211+
message: "Cannot restrict whole day",
212+
path: ["startTime"],
213+
},
180214
);
181215

182216
export const TimetableFormSchema: ZodType<TimetableForm> = z
@@ -208,6 +242,26 @@ export const TimetableFormSchema: ZodType<TimetableForm> = z
208242
message: "Cannot pick more than 8 courses",
209243
path: ["search"],
210244
},
245+
)
246+
.refine(
247+
(data) => {
248+
return !(
249+
data.restrictions.filter((r) => r.type === "Days Off").length > 1
250+
);
251+
},
252+
{
253+
message: "Already added minimum days off per week",
254+
path: ["restrictions"],
255+
},
256+
)
257+
.refine(
258+
(data) => {
259+
return !hasDuplicate(data.restrictions);
260+
},
261+
{
262+
message: "Duplicate restriction detected. Please remove.",
263+
path: ["restrictions"],
264+
},
211265
);
212266

213267
export const baseTimetableForm: TimetableForm = {
@@ -224,3 +278,23 @@ export const baseRestrictionForm: RestrictionForm = {
224278
days: [],
225279
disabled: false,
226280
};
281+
282+
function hasDuplicate(restrictions: RestrictionForm[]) {
283+
const seen: RestrictionForm[] = [];
284+
for (const r of restrictions) {
285+
if (
286+
seen.some(
287+
(s) =>
288+
s.type === r.type &&
289+
((s.numDays && r.numDays && s.numDays === r.numDays) ||
290+
(s.days?.sort().join(" ") === r.days?.sort().join(" ") &&
291+
s.startTime?.getHours() === r.startTime?.getHours() &&
292+
s.endTime?.getHours() === s.endTime?.getHours())),
293+
)
294+
) {
295+
return true;
296+
}
297+
seen.push(r);
298+
}
299+
return false;
300+
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ const CourseSearch = ({
8181
const handleAddCourse = (item: CourseModel) => {
8282
if (!form) return;
8383
const currentList = form.getValues("courses") || [];
84+
if (currentList.length > 7) return; // ensure max courses added is 8
8485
if (currentList.find((c) => c.id === item.id)) return; // ensure uniqueness
8586
const newList = [...currentList, item];
8687
console.log(newList);

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

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,16 @@ const CreateCustomSetting = ({
9696
closeHandler();
9797
};
9898

99+
const form = useContext(FormContext);
100+
101+
const isDaysOffRestrictionApplied = () => {
102+
const val = form
103+
?.getValues("restrictions")
104+
.some((r) => r.type === "Days Off");
105+
console.log(val);
106+
return val;
107+
};
108+
99109
const getRestrictionType = (value: string) => {
100110
if (
101111
value === "Restrict Before" ||
@@ -166,7 +176,10 @@ const CreateCustomSetting = ({
166176
<SelectItem value="Restrict Day">
167177
Restrict Entire Day
168178
</SelectItem>
169-
<SelectItem value="Days Off">
179+
<SelectItem
180+
value="Days Off"
181+
disabled={isDaysOffRestrictionApplied()}
182+
>
170183
Enforce Minimum Days Off Per Week
171184
</SelectItem>
172185
</SelectContent>

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

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -292,7 +292,7 @@ const TimetableBuilder = () => {
292292
</div>
293293
<div className="flex flex-col">
294294
<p className="text-sm pb-2">
295-
Selected courses: {selectedCourses.length}
295+
Selected courses: {selectedCourses.length} (Max 8)
296296
</p>
297297
<div className="flex gap-2 flex-col">
298298
{selectedCourses.map((course, index) => (
@@ -332,9 +332,18 @@ const TimetableBuilder = () => {
332332
</div>
333333

334334
<div className="flex flex-col">
335-
<p className="text-sm pb-2">
336-
Enabled Restrictions: {enabledRestrictions.length}
337-
</p>
335+
<FormField
336+
control={form.control}
337+
name="restrictions"
338+
render={({ field }) => (
339+
<FormItem className="pb-2">
340+
<p className="text-sm">
341+
Enabled Restrictions: {enabledRestrictions.length}
342+
</p>
343+
<FormMessage />
344+
</FormItem>
345+
)}
346+
/>
338347
<div className="flex gap-2 flex-col">
339348
{enabledRestrictions.map((restric, index) => (
340349
<div
@@ -372,18 +381,19 @@ const TimetableBuilder = () => {
372381

373382
<Button type="submit">Generate</Button>
374383
</form>
384+
385+
{isCustomSettingsOpen && (
386+
<CreateCustomSetting
387+
submitHandler={handleAddRestriction}
388+
closeHandler={() => setIsCustomSettingsOpen(false)}
389+
/>
390+
)}
375391
</FormContext.Provider>
376392
</Form>
377393
</div>
378394
<div className="w-3/5">
379395
<Calendar courseEvents={courseEvents} userEvents={userEvents} />
380396
</div>
381-
{isCustomSettingsOpen && (
382-
<CreateCustomSetting
383-
submitHandler={handleAddRestriction}
384-
closeHandler={() => setIsCustomSettingsOpen(false)}
385-
/>
386-
)}
387397

388398
{showFilters && (
389399
<SearchFilters

0 commit comments

Comments
 (0)