diff --git a/frontend/src/pages/SemesterInfo.tsx b/frontend/src/pages/SemesterInfo.tsx
index 6c4576b..4a00ccc 100644
--- a/frontend/src/pages/SemesterInfo.tsx
+++ b/frontend/src/pages/SemesterInfo.tsx
@@ -6,6 +6,12 @@ import '../styles/seminfo.css';
import NextButton from "../components/NextButton.tsx";
import BackButton from "../components/BackButton.tsx";
+/**
+ * Renders the SemesterInfo component with a header, sidebar, spreadsheet,
+ * and navigation buttons.
+ *
+ * @returns JSX element representing the SemesterInfo component
+ */
export default function SemesterInfo() {
return (
diff --git a/frontend/src/pages/SendData.tsx b/frontend/src/pages/SendData.tsx
index d35e43e..597084e 100644
--- a/frontend/src/pages/SendData.tsx
+++ b/frontend/src/pages/SendData.tsx
@@ -8,6 +8,14 @@ import { getTimetableProblem } from "../scripts/handleInput";
import { useState } from "react";
import { fetchTimetableSolution } from "../scripts/api";
+/**
+ * Page for containing UI elements that allow user to send input data to backend.
+ * Temporarily has a display for backend's response to confirm successful sending
+ * (will remove and replace with display page in next sprint).
+ *
+ * @returns button for sending timetable problem and temporary display for timetable solution.
+ * TODO: change button and UI elements to fit with VIT themes.
+ */
export default function SendData() {
const [isGenerated, setIsGenerated] = useState("");
diff --git a/frontend/src/pages/TimetableMod.tsx b/frontend/src/pages/TimetableMod.tsx
index 9872287..ff9a38b 100644
--- a/frontend/src/pages/TimetableMod.tsx
+++ b/frontend/src/pages/TimetableMod.tsx
@@ -1,5 +1,12 @@
import { Link } from "react-router-dom";
+/**
+ * Renders the TimetableMod component to display and modify the generated
+ * timetable.
+ * Allows users to navigate back to the campus information page and proceed to
+ * the download page.
+ * @returns JSX element containing the page content with navigation links
+ */
export default function TimetableMod() {
return (
<>
diff --git a/frontend/src/pages/spreadsheets/Building.tsx b/frontend/src/pages/spreadsheets/Building.tsx
index d195d23..be3ccc9 100644
--- a/frontend/src/pages/spreadsheets/Building.tsx
+++ b/frontend/src/pages/spreadsheets/Building.tsx
@@ -1,6 +1,10 @@
import Spreadsheet from '../../components/Spreadsheet.tsx'
import { DB_BUILDINGS } from '../../scripts/persistence.ts';
+/**
+ *
+ * @returns Spreadsheet input page for buildings information.
+ */
export default function Building() {
return (
diff --git a/frontend/src/pages/spreadsheets/Room.tsx b/frontend/src/pages/spreadsheets/Room.tsx
index 42b78bd..e07db9d 100644
--- a/frontend/src/pages/spreadsheets/Room.tsx
+++ b/frontend/src/pages/spreadsheets/Room.tsx
@@ -1,6 +1,10 @@
import Spreadsheet from '../../components/Spreadsheet.tsx'
import { DB_ROOMS } from '../../scripts/persistence.ts';
+/**
+ *
+ * @returns Spreadsheet input page for rooms information.
+ */
export default function Room() {
return (
diff --git a/frontend/src/pages/spreadsheets/Unit.tsx b/frontend/src/pages/spreadsheets/Unit.tsx
index 876cd71..b747cc3 100644
--- a/frontend/src/pages/spreadsheets/Unit.tsx
+++ b/frontend/src/pages/spreadsheets/Unit.tsx
@@ -1,6 +1,10 @@
import Spreadsheet from '../../components/Spreadsheet.tsx'
import { DB_UNITS } from '../../scripts/persistence.ts';
+/**
+ *
+ * @returns Spreadsheet input page for units information.
+ */
export default function Unit() {
return (
@@ -9,7 +13,7 @@ export default function Unit() {
>
);
diff --git a/frontend/src/routes.tsx b/frontend/src/routes.tsx
index 14b1270..a0d04eb 100644
--- a/frontend/src/routes.tsx
+++ b/frontend/src/routes.tsx
@@ -7,7 +7,12 @@ import Download from './pages/Download.tsx'
import Enrolment from './pages/Enrolment.tsx'
import SendData from './pages/SendData.tsx'
-
+/**
+ * Defines the routes configuration for the application.
+ * Each route specifies a path and the corresponding component to render.
+ *
+ * An array of route objects, each containing path and element information.
+ */
const routes = [
{
path: "/",
diff --git a/frontend/src/scripts/api.ts b/frontend/src/scripts/api.ts
index e157de8..81fa9af 100644
--- a/frontend/src/scripts/api.ts
+++ b/frontend/src/scripts/api.ts
@@ -1,11 +1,14 @@
+/* Timetable solver backend endpoint URL */
const API_URL = 'http://localhost:8080/timetabling';
+/* =========================================== Defining types =========================================== */
+
export type TimetableProblem = TimetableBase & {
units: Unit[],
}
export type TimetableSolution = TimetableBase & {
- units: Required,
+ units: Required[],
}
export type TimetableBase = {
@@ -24,7 +27,7 @@ export type Unit = {
room?: Room,
studentSize?: number
dayOfWeek?: Weekday,
- start?: Time,
+ startTime?: Time,
end?: Time,
};
@@ -42,8 +45,15 @@ export type Weekday = "MONDAY" | "TUESDAY" | "WEDNESDAY" | "THURSDAY" | "FRIDAY"
export type Time = string;
+/* ====================================================================================================== */
+
-// API function(s)
+/**
+ * Sends the timetabling problem to backend for solving. Return the solution received.
+ *
+ * @param problem A TimetableProblem is a list of units with no allocated time and room.
+ * @returns A TimetableSolution with all units allocated a time and a room.
+ */
export async function fetchTimetableSolution(problem: TimetableProblem): Promise {
try {
const response = await fetch(API_URL, {
@@ -55,11 +65,13 @@ export async function fetchTimetableSolution(problem: TimetableProblem): Promise
});
if (!response.ok) {
+ if (response.status === 500) {
+ alert(response.statusText + " " + response.status + ": server was not able to solve the problem. Please check for missing input (i.e. make sure there are at least 1 available room and no rooms with duplicate ID).");
+ }
throw new Error(`HTTP error! Status: ${response.status} ${response.statusText}`);
}
const solution: TimetableSolution = await response.json();
- console.log(solution);
return solution;
}
catch (error) {
diff --git a/frontend/src/scripts/handleInput.ts b/frontend/src/scripts/handleInput.ts
index 1bb1f99..629f45a 100644
--- a/frontend/src/scripts/handleInput.ts
+++ b/frontend/src/scripts/handleInput.ts
@@ -3,6 +3,12 @@ import { CellValue } from 'jspreadsheet-ce';
import { TimetableProblem, Unit, Room } from './api';
import { DB_UNITS, storeSpreadsheetData } from './persistence';
+/**
+ * Function to validate uploaded enrolment data file.
+ *
+ * @param file enrolment data Excel file
+ * @returns true if uploaded file is an Excel file
+ */
function isExcelFile(file: File) {
const fileExtension = file.name.split('.').pop();
if (fileExtension === undefined || !['xlsx', 'xls'].includes(fileExtension)) {
@@ -12,6 +18,12 @@ function isExcelFile(file: File) {
return true;
}
+/**
+ * Function to validate uploaded enrolment data file.
+ *
+ * @param inputHeader header row of enrolment data Excel file
+ * @returns true if header row matches expected format for parsing.
+ */
function validateEnrolmentHeader(inputHeader: Row) {
const header = ['StudentID', 'Student Name', 'Personal Email', 'University Email',
'Student Type', 'Offer Type', 'Course Name', 'Campus', 'Original COE Start Date',
@@ -26,6 +38,12 @@ function validateEnrolmentHeader(inputHeader: Row) {
}
}
+/**
+ * Extract list of units from enrolment data and prefill spreadsheet input page.
+ *
+ * @param enrolmentExcel enrolment data Excel file
+ * @returns enrolment data Excel file
+ */
export async function getUnitsList(enrolmentExcel: File) {
if (!isExcelFile(enrolmentExcel)) {
throw new Error(
@@ -44,7 +62,7 @@ export async function getUnitsList(enrolmentExcel: File) {
// console.log(header.slice(14));
const unitsList = header.slice(14).map(elem => elem.toString());
const unitsData: Record[] = unitsList.map((u) => {
- return { 0: u };
+ return { 0: u, 1: '', 2: '', 3: '' };
});
storeSpreadsheetData(unitsData, DB_UNITS);
@@ -52,6 +70,14 @@ export async function getUnitsList(enrolmentExcel: File) {
return enrolmentExcel;
}
+/**
+ * Parse user input to create the timetabling problem.
+ *
+ * @param enrolmentExcel enrolment data Excel file
+ * @param roomSpreadsheet information of all rooms (spreadsheet input from user)
+ * @param unitSpreadsheet information of all units (spreadsheet input from user)
+ * @returns a TimetableProblem, which includes all available rooms, start times and unallocated units
+ */
export async function getTimetableProblem(enrolmentExcel: File, roomSpreadsheet: Record[], unitSpreadsheet: Record[]) {
if (!isExcelFile(enrolmentExcel)) {
throw new Error(
@@ -79,10 +105,14 @@ export async function getTimetableProblem(enrolmentExcel: File, roomSpreadsheet:
});
unitSpreadsheet.map((record, index) => {
- const totalDuration = (parseInt(record['1'].toString()) + parseInt(record['2'].toString()) + parseInt(record['3'].toString())) * 60;
- const wantsLab = parseInt(record['3'].toString()) > 0;
- units[index].duration = totalDuration;
- units[index].wantsLab = wantsLab;
+ if (index >= units.length) {
+ }
+ else {
+ const totalDuration = (Number(record['1']) + Number(record['2']) + Number(record['3'])) * 60;
+ const wantsLab = Number(record['3']) > 0;
+ units[index].duration = totalDuration;
+ units[index].wantsLab = wantsLab;
+ }
})
// check each row and add students to each unit they're enrolled in
diff --git a/frontend/src/scripts/persistence.ts b/frontend/src/scripts/persistence.ts
index 1ac5808..ecb9516 100644
--- a/frontend/src/scripts/persistence.ts
+++ b/frontend/src/scripts/persistence.ts
@@ -1,23 +1,25 @@
import Dexie, { EntityTable } from "dexie";
import { CellValue } from "jspreadsheet-ce";
+/* Define storage keys */
const DB_NAME = 'TimetableInput';
const DB_BUILDINGS = 'buildings';
const DB_ROOMS = 'rooms';
const DB_UNITS = 'units';
-
+/* Define record type of `file` objectStore */
interface FileRecord {
id: number;
file: File;
}
+/* Define record type of each spreadsheets objectStore */
interface SpreadsheetRecord {
id?: number;
record: Record;
}
-
+/* Initialise Dexie (indexedDB wrapper) */
const db = new Dexie(DB_NAME) as Dexie & {
files: EntityTable;
buildings: EntityTable;
@@ -25,6 +27,7 @@ const db = new Dexie(DB_NAME) as Dexie & {
units: EntityTable;
};
+/* Initialise indexedDB objectStores */
db.version(1).stores({
files: '++id, file',
buildings: '++id, record',
@@ -32,6 +35,14 @@ db.version(1).stores({
units: '++id, record'
});
+/**
+ * Save 1 file to the first row of the file table.
+ * Delete all other files.
+ * Intend for storing enrolment data Excel file.
+ *
+ * @param file any file
+ * @returns id of saved row
+ */
export async function storeFile(file: File): Promise {
await db.files.clear();
const id = await db.files.add({
@@ -41,6 +52,12 @@ export async function storeFile(file: File): Promise {
return id;
}
+/**
+ * Get first file saved to the `file` objectStore.
+ * Intend for getting saved enrolment data Excel file.
+ *
+ * @returns file saved
+ */
export async function getFile(): Promise {
const file = await db.files.orderBy('id').first();
@@ -52,6 +69,13 @@ export async function getFile(): Promise {
return file.file;
}
+/**
+ * Store data of an input spreadsheet to the corresponding objectStore in indexedDB.
+ *
+ * @param data data in one spreadsheet page
+ * @param storageObject the key of the objectStore (table) of the database we want to store data into
+ * @returns void
+ */
export async function storeSpreadsheetData(data: Record[], storageObject: string) {
if (!data) {
return;
@@ -66,12 +90,15 @@ export async function storeSpreadsheetData(data: Record[], st
try {
if (storageObject === DB_BUILDINGS) {
+ await db.buildings.clear();
await db.buildings.bulkPut(records);
}
else if (storageObject === DB_ROOMS) {
+ await db.rooms.clear();
await db.rooms.bulkPut(records);
}
else if (storageObject === DB_UNITS) {
+ await db.units.clear();
await db.units.bulkPut(records);
}
else {
@@ -85,6 +112,12 @@ export async function storeSpreadsheetData(data: Record[], st
}
}
+/**
+ * Get data from a specified indexedDB objectStore.
+ *
+ * @param storageObject the key of the objectStore (table) that we want to get data out of
+ * @returns spreadsheet data
+ */
export async function getSpreadsheetData(storageObject: string): Promise[] | null> {
try {
if (storageObject === "buildings") {
diff --git a/frontend/src/styles/enrolment.css b/frontend/src/styles/enrolment.css
index ca0602e..f2bcc26 100644
--- a/frontend/src/styles/enrolment.css
+++ b/frontend/src/styles/enrolment.css
@@ -1,13 +1,11 @@
-/* Make sure the entire viewport height is used */
.app-container {
display: flex;
flex-direction: column;
- min-height: 100vh; /* Full viewport height */
+ min-height: 100vh;
}
-/* Content area that grows to fill space */
.content {
- flex: 1; /* This allows the content to grow and push the footer down */
+ flex: 1;
padding: 0;
display: grid;
grid-template-columns: 1fr 1fr;
@@ -27,7 +25,7 @@
}
.imageBox {
- overflow: hidden; /* Hide any overflow content */
+ overflow: hidden;
position: relative;
}
body {
diff --git a/frontend/src/tests/api.test.ts b/frontend/src/tests/api.test.ts
new file mode 100644
index 0000000..e6bffd0
--- /dev/null
+++ b/frontend/src/tests/api.test.ts
@@ -0,0 +1,67 @@
+import { describe, it, expect } from 'vitest';
+import { fetchTimetableSolution, TimetableProblem } from '../scripts/api';
+import moment from 'moment';
+
+/**
+ * Test fetchTimetableSolution API method.
+ * Check if connection to backend is working.
+ * Check that output matches expected output.
+ */
+describe('fetchTimetableSolution', () => {
+ /**
+ * Validate end-to-end scheduling and data consistency of 1 API method call.
+ */
+ it('return TimetableSolution', async () => {
+ const problem: TimetableProblem = {
+ units: [{ unitID: 0, name: "Unit0", duration: 1200, students: [], wantsLab: true }],
+ daysOfWeek: ["MONDAY"],
+ startTimes: ["11:00:00"],
+ rooms: [{ id: "Room A", capacity: 10, lab: true }]
+ };
+
+ const solution = await fetchTimetableSolution(problem);
+ expect(solution).not.toBeNull();
+ expect(solution?.units[0].dayOfWeek).toEqual(problem.daysOfWeek[0]);
+ expect(solution?.units[0].startTime).toEqual(problem.startTimes[0]);
+ expect(solution?.units[0].end).toEqual(addSecondsToTimeString(problem.startTimes[0], problem.units[0].duration));
+ expect(solution?.units[0].room).toEqual(problem.rooms[0]);
+ expect(solution?.daysOfWeek).toEqual(problem.daysOfWeek);
+ expect(solution?.startTimes).toEqual(problem.startTimes);
+ expect(solution?.rooms).toEqual(problem.rooms);
+
+ });
+
+ /**
+ * Validate that backend server can handle multiple solve requests concurrently.
+ */
+ it ('can be called multiple times', async () => {
+ const problem: TimetableProblem = {
+ units: [{ unitID: 0, name: "Unit0", duration: 1200, students: [], wantsLab: true }],
+ daysOfWeek: ["MONDAY"],
+ startTimes: ["11:00:00"],
+ rooms: [{ id: "Room A", capacity: 10, lab: true }]
+ };
+
+ const solutions = await Promise.all([fetchTimetableSolution(problem), fetchTimetableSolution(problem), fetchTimetableSolution(problem)]);
+
+ for (let i = 0; i < solutions.length; i++) {
+ expect(solutions[i]).not.toBeNull();
+ }
+
+ });
+
+});
+
+/**
+ * Helper function.
+ * Add a string representing time with a number representing number of seconds to add.
+ *
+ * @param timeString string representing time
+ * @param secondsToAdd number in seconds
+ * @returns string representing time after specified seconds have been added to it
+ */
+function addSecondsToTimeString(timeString: string, secondsToAdd: number) {
+ const time = moment(timeString, 'HH:mm:ss');
+ time.add(secondsToAdd, 'seconds');
+ return time.format('HH:mm:ss');
+}
\ No newline at end of file
diff --git a/frontend/static.json b/frontend/static.json
new file mode 100644
index 0000000..94fb8eb
--- /dev/null
+++ b/frontend/static.json
@@ -0,0 +1,3 @@
+{
+ "root": "./dist"
+}
\ No newline at end of file