Skip to content

Commit

Permalink
Merge pull request #28 from hotungkhanh/kan-73/split-campus
Browse files Browse the repository at this point in the history
Kan 73/split campus
  • Loading branch information
dh-giang-vu authored Oct 4, 2024
2 parents 6c7242c + ef830a6 commit 16f18fc
Show file tree
Hide file tree
Showing 8 changed files with 153 additions and 79 deletions.
8 changes: 4 additions & 4 deletions frontend/src/components/Spreadsheet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,9 @@ export default function Spreadsheet({ headers, storageKey, ...other }: Spreadshe
// spreadsheet init options: columns property
const columns: jspreadsheet.Column[] = headers.map((headerName, idx) => {
if (other.columns && other.columns.length > idx) {
return { title: headerName, width: 200, ...other.columns[idx] };
return { title: headerName, width: headerName.length * 10 + 20, ...other.columns[idx] };
}
return { title: headerName, width: 200 };
return { title: headerName, width: headerName.length * 10 + 20 };
});

// spreadsheet init options: contextMenu property
Expand Down Expand Up @@ -95,8 +95,8 @@ export default function Spreadsheet({ headers, storageKey, ...other }: Spreadshe
allowInsertColumn: false,
includeHeadersOnDownload: true,
tableOverflow: true,
tableWidth: '1150px',
tableHeight: '400px',
tableWidth: '70vw',
tableHeight: '50vh',
contextMenu: contextMenu,
// toolbar:[
// {
Expand Down
5 changes: 3 additions & 2 deletions frontend/src/components/UploadButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { styled } from "@mui/material/styles";
import Button from "@mui/material/Button";
import UploadFileIcon from "@mui/icons-material/UploadFile";
import { getFile, storeFile } from "../scripts/persistence";
import { getUnitsList } from "../scripts/handleInput";
import { getUnitsList, prefillUnitSpreadsheet } from "../scripts/handleInput";

interface InputFileUploadProps {
setFileChosen: (file: File | null) => void;
Expand Down Expand Up @@ -37,7 +37,8 @@ export default function UploadButton ({ setFileChosen }: InputFileUploadProps) {
return getFile();
})
.then((file) => {
return getUnitsList(file);
return prefillUnitSpreadsheet(file);
// return getUnitsList(file);
})
.then((file) => {
setFileChosen(file);
Expand Down
20 changes: 10 additions & 10 deletions frontend/src/pages/SendData.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import Footer from "../components/Footer";
import BackButton from "../components/BackButton";
import NextButton from "../components/NextButton";
import Header from "../components/Header";
import { DB_ROOMS, DB_UNITS, getFile, getSpreadsheetData } from "../scripts/persistence";
import { getTimetableProblem } from "../scripts/handleInput";
import { DB_ROOMS, DB_UNITS, getSpreadsheetData } from "../scripts/persistence";
import { getTimetableProblems } from "../scripts/handleInput";
import { useState } from "react";
import { fetchTimetableSolution } from "../scripts/api";

Expand All @@ -22,23 +22,23 @@ export default function SendData() {

function generateTimetable() {
setIsGenerated("");
Promise.all([getFile(), getSpreadsheetData(DB_ROOMS), getSpreadsheetData(DB_UNITS)])
Promise.all([getSpreadsheetData(DB_ROOMS), getSpreadsheetData(DB_UNITS)])
.then((responses) => {
const [enrolment, roomData, unitData] = [...responses];
const [roomData, unitData] = [...responses];
if (!roomData) {
throw new Error("Error: room data not available");
}
else if (!unitData) {
throw new Error("Error: unit data not available");
}
return getTimetableProblem(enrolment, roomData, unitData);
return getTimetableProblems(roomData, unitData);
})
.then((problem) => {
return fetchTimetableSolution(problem);
.then((problems) => {
return Promise.all(problems.map(p => fetchTimetableSolution(p)));
})
.then((solution) => {
console.log(solution);
setIsGenerated(JSON.stringify(solution, null, 2));
.then((solutions) => {
console.log(solutions);
setIsGenerated(JSON.stringify(solutions, null, 2));
})
.catch((error) => {
alert(error);
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/pages/spreadsheets/Room.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export default function Room() {
<>
<h3>Room</h3>
<Spreadsheet
headers={["Campus Name", "Building Name", "Room Code", "Room Capacity", "Is Lab", "Is Available"]}
headers={["Campus", "Building", "Room Code", "Room Capacity", "Is Lab", "Is Available"]}
storageKey={DB_ROOMS}
columns={[{},{},{},{}, { type: 'checkbox' }, { type: 'checkbox' }]}
/>
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/pages/spreadsheets/Unit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ export default function Unit() {
<>
<h3>Unit</h3>
<Spreadsheet
headers={["Unit Code ", "Duration (Lecture)", "Duration (Tutorial)", "Duration (Lab)"]}
headers={["Campus", "Course", "Unit Code ", "Duration (Lecture)", "Duration (Tutorial)", "Duration (Lab)", "Enrolled Students"]}
storageKey={DB_UNITS}
columns={[{readOnly: true}]}
columns={[{readOnly: true}, {readOnly: true}, {readOnly: true}, {}, {}, {}, {readOnly: true}]}
/>
</>
);
Expand Down
5 changes: 5 additions & 0 deletions frontend/src/scripts/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,15 @@ export type TimetableSolution = TimetableBase & {
}

export type TimetableBase = {
campusName: string,
daysOfWeek: Weekday[],
startTimes: Time[],
rooms: Room[]
}

export type Unit = {
campus: string,
course: string,
unitId: number,
name: string,
duration: number,
Expand All @@ -36,6 +39,8 @@ export type Student = {
};

export type Room = {
campus: string,
buildingId: string,
roomCode: string,
capacity: number,
lab: boolean
Expand Down
176 changes: 121 additions & 55 deletions frontend/src/scripts/handleInput.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ function validateEnrolmentHeader(inputHeader: Row) {
}

/**
* Deprecated.
* Extract list of units from enrolment data and prefill spreadsheet input page.
*
* @param enrolmentExcel enrolment data Excel file
Expand Down Expand Up @@ -71,14 +72,14 @@ export async function getUnitsList(enrolmentExcel: File) {
}

/**
* Parse user input to create the timetabling problem.
* Check if input file is valid enrolment data Excel file.
* Extract list of units from enrolment data and prefill spreadsheet input page.
* Extract list of students per each unit from enrolment data.
*
* @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
* @returns enrolment data Excel file
*/
export async function getTimetableProblem(enrolmentExcel: File, roomSpreadsheet: Record<string, CellValue>[], unitSpreadsheet: Record<string, CellValue>[]) {
export async function prefillUnitSpreadsheet(enrolmentExcel: File) {
if (!isExcelFile(enrolmentExcel)) {
throw new Error(
"File is not .xlsx or .xls"
Expand All @@ -93,72 +94,137 @@ export async function getTimetableProblem(enrolmentExcel: File, roomSpreadsheet:
)
}

const unitsList = header.slice(14);
const units: Unit[] = unitsList.map((value, index) => {
return {
unitId: index,
name: value.toString(),
duration: 0,
students: [],
wantsLab: false
}
});
type UnitMapRecord = {
0: CellValue,
1: CellValue,
2: CellValue,
3: CellValue,
4: CellValue,
5: CellValue,
enrolment: string[]
}
type UnitMap = Map<string, UnitMapRecord>;

// array of maps, each map correspond to 1 unit
// each map maps the campus-course pair to a record that stores student enrolment
const unitsMaps: UnitMap[] = [];

for (let i = 14; i < header.length; i++) {
unitsMaps.push(new Map());
const currentMap = unitsMaps[unitsMaps.length - 1];
const unitCode = header[i] as CellValue;

for (let j = 0; j < body.length; j++) {
const row = body[j];
const campus = row[7] as CellValue;
const course = row[6] as CellValue;
const student = row[0] as CellValue;
const enrolment = row[i] as CellValue;
const key = campus.toString() + course.toString();

if (!currentMap.has(key)) {
currentMap.set(key, { 0: campus, 1: course, 2: unitCode, 3: '', 4: '', 5: '', enrolment: [] });
}

unitSpreadsheet.map((record, index) => {
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
for (let i = 0; i < body.length; i++) {
const enrolments = body[i].slice(14);
for (let j = 0; j < enrolments.length; j++) {
if (enrolments[j] === "ENRL") {
units[j].students.push({
name: body[i][0].toString()
})
if (enrolment === "ENRL") {
currentMap.get(key)?.enrolment.push(student.toString());
}
}
}

const units = unitsMaps.map(m => {
const unitsData = Array.from(m.values());
const transformed = unitsData.map(ud => { return { ...ud, enrolment: JSON.stringify(ud.enrolment) } });
return transformed;
}).flat();

storeSpreadsheetData(units, DB_UNITS);
return enrolmentExcel;
}

/**
* Parse user input to create the timetabling problems split by campus.
*
* @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 getTimetableProblems(roomSpreadsheet: Record<string, CellValue>[], unitSpreadsheet: Record<string, CellValue>[]) {
const units: Unit[] = unitSpreadsheet.map((record, index) => {
const totalDuration = (Number(record['3']) + Number(record['4']) + Number(record['5'])) * 60;
const wantsLab = Number(record['5']) > 0;

return {
campus: record['0'] as string,
course: record['1'] as string,
unitId: index,
name: record['2'] as string,
duration: totalDuration,
students: JSON.parse(record.enrolment as string),
wantsLab: wantsLab
}
});

const rooms: Room[] = roomSpreadsheet
.filter((record) => record['5'] as boolean)
.map((record) => {
return {
campus: record['0'] as string,
buildingId: record['1'] as string,
roomCode: record['2'] as string,
capacity: record['3'] as number,
lab: record['4'] as boolean
}
});

const unitsByCampus = units.reduce((acc: Record<string, Unit[]>, unit) => {
if (!acc[unit.campus]) {
acc[unit.campus] = [];
}

const problem: TimetableProblem = {
units: units,
daysOfWeek: [
"MONDAY",
"TUESDAY",
"WEDNESDAY",
"THURSDAY",
"FRIDAY"
],
startTimes: [
"08:00:00",
"09:00:00",
"10:00:00",
"11:00:00",
"12:00:00",
"13:00:00",
],
rooms: rooms
}
acc[unit.campus].push(unit);
return acc;
}, {});

console.log(problem);
const roomsByCampus = rooms.reduce((acc: Record<string, Room[]>, room) => {
if (!acc[room.campus]) {
acc[room.campus] = [];
}

return problem;
acc[room.campus].push(room);
return acc;
}, {});

const problemsByCampus: TimetableProblem[] = [];

for (const campus in unitsByCampus) {
if (roomsByCampus[campus]) {
problemsByCampus.push({
campusName: campus,
units: unitsByCampus[campus],
daysOfWeek: [
"MONDAY",
"TUESDAY",
"WEDNESDAY",
"THURSDAY",
"FRIDAY"
],
startTimes: [
"08:00:00",
"09:00:00",
"10:00:00",
"11:00:00",
"12:00:00",
"13:00:00",
],
rooms: roomsByCampus[campus]

})
}
else {
alert("This campus don't have any rooms: " + campus);
}
}
console.log(problemsByCampus);
return problemsByCampus;
}
12 changes: 7 additions & 5 deletions frontend/src/tests/api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,17 @@ import moment from 'moment';
* Check if connection to backend is working.
* Check that output matches expected output.
*/
describe('fetchTimetableSolution', () => {
describe('fetchTimetableSolution', { timeout: 60000 }, () => {
/**
* 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 }],
campusName: "A",
units: [{ campus: "A", course: "B", unitId: 0, name: "Unit0", duration: 1200, students: [], wantsLab: true }],
daysOfWeek: ["MONDAY"],
startTimes: ["11:00:00"],
rooms: [{ roomCode: "Room A", capacity: 10, lab: true }]
rooms: [{ campus: "A", buildingId: "01", roomCode: "Room A", capacity: 10, lab: true }]
};

const solution = await fetchTimetableSolution(problem);
Expand All @@ -35,10 +36,11 @@ describe('fetchTimetableSolution', () => {
*/
it ('can be called multiple times', async () => {
const problem: TimetableProblem = {
units: [{ unitId: 0, name: "Unit0", duration: 1200, students: [], wantsLab: true }],
campusName: "B",
units: [{ campus: "B", course: "C", unitId: 0, name: "Unit0", duration: 1200, students: [], wantsLab: true }],
daysOfWeek: ["MONDAY"],
startTimes: ["11:00:00"],
rooms: [{ roomCode: "Room A", capacity: 10, lab: true }]
rooms: [{ campus: "B", buildingId: "02", roomCode: "Room A", capacity: 10, lab: true }]
};

const solutions = await Promise.all([fetchTimetableSolution(problem), fetchTimetableSolution(problem), fetchTimetableSolution(problem)]);
Expand Down

0 comments on commit 16f18fc

Please sign in to comment.