diff --git a/.vscode/settings.json b/.vscode/settings.json index 1bfdd24..8a4aef0 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,5 +1,3 @@ { - "conventionalCommits.scopes": [ - "database" - ] -} \ No newline at end of file + "conventionalCommits.scopes": ["database", "display ganttchart"] +} diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 47ad553..edfca78 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -20,7 +20,8 @@ "react-dom": "^18.3.1", "react-router-dom": "^6.26.1", "read-excel-file": "^5.8.5", - "serve": "^6.5.8" + "serve": "^6.5.8", + "vis-timeline": "^7.7.3" }, "devDependencies": { "@eslint/js": "^9.9.0", @@ -372,6 +373,19 @@ "node": ">=6.9.0" } }, + "node_modules/@egjs/hammerjs": { + "version": "2.0.17", + "resolved": "https://registry.npmjs.org/@egjs/hammerjs/-/hammerjs-2.0.17.tgz", + "integrity": "sha512-XQsZgjm2EcVUiZQf11UBJQfmZeEmOW8DpI1gsFeln6w0ae0ii4dMQEQ0kjl6DspdWX1aGY1/loyXnP0JS06e/A==", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/hammerjs": "^2.0.36" + }, + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/@emotion/babel-plugin": { "version": "11.12.0", "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.12.0.tgz", @@ -1728,6 +1742,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/hammerjs": { + "version": "2.0.45", + "resolved": "https://registry.npmjs.org/@types/hammerjs/-/hammerjs-2.0.45.tgz", + "integrity": "sha512-qkcUlZmX6c4J8q45taBKTL3p+LbITgyx7qhlPYOdOHZB7B31K0mXbP5YA7i7SgDeEGuI9MnumiKPEMrxg8j3KQ==", + "license": "MIT", + "peer": true + }, "node_modules/@types/parse-json": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", @@ -2709,6 +2730,23 @@ "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", "license": "MIT" }, + "node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "license": "MIT", + "peer": true + }, + "node_modules/component-emitter": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz", + "integrity": "sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==", + "license": "MIT", + "peer": true, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/compressible": { "version": "2.0.18", "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", @@ -2813,6 +2851,13 @@ "node": ">= 8" } }, + "node_modules/cssfilter": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/cssfilter/-/cssfilter-0.0.10.tgz", + "integrity": "sha512-FAaLDaplstoRsDR8XGYH51znUN0UY7nMc6Z9/fvE8EXGwvJE9hu7W2vHwx1+bd6gCYnln9nLbzxFTrcO9YQDZw==", + "license": "MIT", + "peer": true + }, "node_modules/csstype": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", @@ -4066,6 +4111,13 @@ "resolved": "https://registry.npmjs.org/jsuites/-/jsuites-5.4.6.tgz", "integrity": "sha512-/Do37lqZ+EhBKvWi3L1r7wHjP9PHAtjDPLNepp84Pqi4pWrH6ZisTuJZyI6SRHYqshsfMr+cg5CkPbPC6J6o4Q==" }, + "node_modules/keycharm": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/keycharm/-/keycharm-0.4.0.tgz", + "integrity": "sha512-TyQTtsabOVv3MeOpR92sIKk/br9wxS+zGj4BG7CR8YbK4jM3tyIBaF0zhzeBUMx36/Q/iQLOKKOT+3jOQtemRQ==", + "license": "(Apache-2.0 OR MIT)", + "peer": true + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -4337,7 +4389,6 @@ "version": "2.30.1", "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", - "dev": true, "license": "MIT", "engines": { "node": "*" @@ -4731,6 +4782,16 @@ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", "license": "MIT" }, + "node_modules/propagating-hammerjs": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/propagating-hammerjs/-/propagating-hammerjs-2.0.1.tgz", + "integrity": "sha512-PH3zG5whbSxMocphXJzVtvKr+vWAgfkqVvtuwjSJ/apmEACUoiw6auBAT5HYXpZOR0eGcTAfYG5Yl8h91O5Elg==", + "license": "MIT", + "peer": true, + "peerDependencies": { + "@egjs/hammerjs": "^2.0.17" + } + }, "node_modules/pseudomap": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", @@ -5820,6 +5881,20 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "license": "MIT" }, + "node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "peer": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", @@ -5829,6 +5904,60 @@ "node": ">= 0.8" } }, + "node_modules/vis-data": { + "version": "7.1.9", + "resolved": "https://registry.npmjs.org/vis-data/-/vis-data-7.1.9.tgz", + "integrity": "sha512-COQsxlVrmcRIbZMMTYwD+C2bxYCFDNQ2EHESklPiInbD/Pk3JZ6qNL84Bp9wWjYjAzXfSlsNaFtRk+hO9yBPWA==", + "license": "(Apache-2.0 OR MIT)", + "peer": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/visjs" + }, + "peerDependencies": { + "uuid": "^3.4.0 || ^7.0.0 || ^8.0.0 || ^9.0.0", + "vis-util": "^5.0.1" + } + }, + "node_modules/vis-timeline": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/vis-timeline/-/vis-timeline-7.7.3.tgz", + "integrity": "sha512-hGMzTttdOFWaw1PPlJuCXU2/4UjnsIxT684Thg9fV6YU1JuKZJs3s3BrJgZ4hO3gu5i1hsMe1YIi96o+eNT0jg==", + "license": "(Apache-2.0 OR MIT)", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/visjs" + }, + "peerDependencies": { + "@egjs/hammerjs": "^2.0.0", + "component-emitter": "^1.3.0", + "keycharm": "^0.2.0 || ^0.3.0 || ^0.4.0", + "moment": "^2.24.0", + "propagating-hammerjs": "^1.4.0 || ^2.0.0", + "uuid": "^3.4.0 || ^7.0.0 || ^8.0.0 || ^9.0.0", + "vis-data": "^6.3.0 || ^7.0.0", + "vis-util": "^5.0.1", + "xss": "^1.0.0" + } + }, + "node_modules/vis-util": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/vis-util/-/vis-util-5.0.7.tgz", + "integrity": "sha512-E3L03G3+trvc/X4LXvBfih3YIHcKS2WrP0XTdZefr6W6Qi/2nNCqZfe4JFfJU6DcQLm6Gxqj2Pfl+02859oL5A==", + "license": "(Apache-2.0 OR MIT)", + "peer": true, + "engines": { + "node": ">=8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/visjs" + }, + "peerDependencies": { + "@egjs/hammerjs": "^2.0.0", + "component-emitter": "^1.3.0 || ^2.0.0" + } + }, "node_modules/vite": { "version": "5.4.7", "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.7.tgz", @@ -6049,6 +6178,23 @@ "node": ">=0.4.0" } }, + "node_modules/xss": { + "version": "1.0.15", + "resolved": "https://registry.npmjs.org/xss/-/xss-1.0.15.tgz", + "integrity": "sha512-FVdlVVC67WOIPvfOwhoMETV72f6GbW7aOabBC3WxN/oUdoEMDyLz4OgRv5/gck2ZeNqEQu+Tb0kloovXOfpYVg==", + "license": "MIT", + "peer": true, + "dependencies": { + "commander": "^2.20.3", + "cssfilter": "0.0.10" + }, + "bin": { + "xss": "bin/xss" + }, + "engines": { + "node": ">= 0.10.0" + } + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", diff --git a/frontend/package.json b/frontend/package.json index 34068cd..e760a99 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -24,7 +24,8 @@ "react-dom": "^18.3.1", "react-router-dom": "^6.26.1", "read-excel-file": "^5.8.5", - "serve": "^6.5.8" + "serve": "^6.5.8", + "vis-timeline": "^7.7.3" }, "devDependencies": { "@eslint/js": "^9.9.0", diff --git a/frontend/src/components/GanttChart.tsx b/frontend/src/components/GanttChart.tsx new file mode 100644 index 0000000..1968654 --- /dev/null +++ b/frontend/src/components/GanttChart.tsx @@ -0,0 +1,155 @@ +import { memo, useEffect, useRef } from "react"; +import { Id } from "vis-data/declarations/data-interface"; +import { DataGroupCollectionType, DataItemCollectionType, DataSet, Timeline, TimelineItem } from "vis-timeline/standalone"; +import "vis-timeline/styles/vis-timeline-graph2d.min.css"; +import "../styles/ganttUnassignable.css"; + +import { + findCampusSolution, + GanttGroup, + GanttItems, + getGanttItems, +} from "../scripts/solutionParsing"; +import { TimetableSolution } from "../scripts/api"; +import { useParams } from "react-router-dom"; + +export default memo(function GanttChart() { + const params = useParams(); + const timelineRef = useRef(null); + const items = useRef(new DataSet()); + const groups = useRef(new DataSet()); + + let campusSolutions: TimetableSolution[]; + let check: string | null = sessionStorage.getItem("campusSolutions"); + if (check !== null) { + campusSolutions = JSON.parse(check); + } else { + throw new Error("campusSolutions is not in campus, in GanttChart"); + } + + const solution: TimetableSolution | null = params?.location ? findCampusSolution( + params.location, + campusSolutions + ): null; + if (solution === null) { + throw new Error("solution is null after findCampusSolution (possibly an error related to campus name)") + } + + let timelineData: GanttItems = getGanttItems(solution); + console.log("processed data in Gantt", timelineData); + groups.current.clear(); + groups.current.add(timelineData.buildings); + groups.current.add(timelineData.rooms); + items.current.clear() + items.current.add(timelineData.activities); + + useEffect(() => { + if (timelineRef.current) { + let prevSelected: Id | null = null; + + const options = { + start: "2000-01-01", + end: "2000-01-06", + min: "2000-01-01", + max: "2000-01-07", + editable: true, + }; + + // Initialize the timeline + const timeline = new Timeline( + timelineRef.current, + items.current as DataItemCollectionType, + groups.current as DataGroupCollectionType, + options + ); + + const hasOverlap = (newItem: TimelineItem | null) => { + const existingItems = items.current.get(); + return existingItems.some((item: TimelineItem) => { + if ( + newItem == null || + item.id === newItem.id || + item.group !== newItem.group || + item.type === "background" + ) + return false; + + const newStart = new Date(newItem.start).getTime(); + const newEnd = newItem.end + ? new Date(newItem.end).getTime() + : newStart; + const itemStart = new Date(item.start).getTime(); + const itemEnd = item.end ? new Date(item.end).getTime() : itemStart; + return newStart < itemEnd && newEnd > itemStart; + }); + }; + + const validGroup = (item: TimelineItem | null) => { + if (item == null) return true; + const group: GanttGroup|null = groups.current.get(item.group as Id); + return (group !== null && group.treeLevel === 2); + + }; + + timeline.on("select", (properties) => { + if (prevSelected !== null) { + const overlaps = hasOverlap(items.current.get(prevSelected)); + const inRoom = validGroup(items.current.get(prevSelected)); + if (overlaps) { + alert("OVERLAPPED"); + } + if (!inRoom) { + alert("ASSIGN ACTIVITIES TO ROOMS ONLY"); + } + } + + if (properties.items.length > 0) { + prevSelected = properties.items[0]; + } else { + prevSelected = null; + } + }); + + return () => { + timeline.destroy(); + }; + } + }, []); + + const convertToCSV = () => { + let csvContent = "id,content,start,end,group\n"; + const itemList = items.current.get(); + itemList.forEach((item) => { + const start = item.start.toString(); + const end = item.end ? item.end.toString() : ""; + csvContent += `${item.id},${item.content},${start},${end},${groups.current.get(item.group as Id)?.content}\n`; + }); + + return csvContent; + }; + + const downloadCSV = () => { + const csvData = convertToCSV(); + const blob = new Blob([csvData], { type: "text/csv;charset=utf-8;" }); + const link = document.createElement("a"); + const url = URL.createObjectURL(blob); + link.setAttribute("href", url); + link.setAttribute("download", "timetable.csv"); + link.style.visibility = "hidden"; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + }; + + const saveData = () => { + + }; + + return ( +
+
+ + +
+ ); +}) diff --git a/frontend/src/components/ModSiderbar.tsx b/frontend/src/components/ModSiderbar.tsx new file mode 100644 index 0000000..280c4a2 --- /dev/null +++ b/frontend/src/components/ModSiderbar.tsx @@ -0,0 +1,84 @@ +import Drawer from "@mui/material/Drawer"; +import List from "@mui/material/List"; +import ListItem from "@mui/material/ListItem"; +import ListItemButton from "@mui/material/ListItemButton"; +import ListItemIcon from "@mui/material/ListItemIcon"; +import ListItemText from "@mui/material/ListItemText"; +import InboxIcon from "@mui/icons-material/MoveToInbox"; +import { Routes, Route, Link } from "react-router-dom"; + +import GanttChart from "./GanttChart"; +import { TimetableSolution } from "../scripts/api"; + +interface SidebarProps { + marginTop: number; + width: number; +} +const drawerWidth = 240; + +export default function ModSidebar({ marginTop, width }: SidebarProps) { + let campusSolutions: TimetableSolution[]; + let campusSolutionsStr: string|null = sessionStorage.getItem("campusSolutions"); + if (campusSolutionsStr !== null) { + campusSolutions = JSON.parse(campusSolutionsStr); + } else { + throw new Error("campusSolutionStr is NULL in ModSidebar") + } + return ( + <> + + + {campusSolutions && campusSolutions.length > 0 ? ( + campusSolutions.map((solution) => { + let campusName = solution.campusName; + return ( + + + + + + + + + ); + }) + ) : ( +
Loading...
+ )} +
+
+ +
+ + {campusSolutions && + campusSolutions.length > 0 && + campusSolutions.map((solution) => { + const campusName = solution.campusName; + return ( + } + /> + ); + })} + +
+ + ); +} diff --git a/frontend/src/pages/TimetableMod.tsx b/frontend/src/pages/TimetableMod.tsx index ff9a38b..bcb7fa6 100644 --- a/frontend/src/pages/TimetableMod.tsx +++ b/frontend/src/pages/TimetableMod.tsx @@ -1,18 +1,65 @@ import { Link } from "react-router-dom"; +import { useEffect, useState } from "react"; +import Header from "../components/Header"; +import Footer from "../components/Footer"; +import BackButton from "../components/BackButton"; +import { Outlet } from "react-router-dom"; +import ModSidebar from "../components/ModSiderbar"; +import { TimetableSolution } from "../scripts/api"; /** - * Renders the TimetableMod component to display and modify the generated + * Renders the TimetableMod component to display and modify the generated * timetable. - * Allows users to navigate back to the campus information page and proceed to + * 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() { + const [loading, setLoading] = useState(true); + + useEffect(() => { + fetch("http://localhost:8080/timetabling/view") + .then((response) => { + if (!response.ok) { + throw new Error("Network response was not ok"); + } + return response.json(); + }) + .then((data) => { + const timetableSolutions: TimetableSolution[] = + data as TimetableSolution[]; + sessionStorage.setItem("campusSolutions", JSON.stringify(timetableSolutions)); + setLoading(false); + }); + }, []); + + if (loading) { + return
Loading...
; + } return ( - <> -

This is the page to modify generated timetable

- Go Back - Go to Next - - ) +
+
+ + +
+ +
+ +
+
+ + + + +
+
+
+ ); } \ No newline at end of file diff --git a/frontend/src/routes.tsx b/frontend/src/routes.tsx index a0d04eb..48d05c2 100644 --- a/frontend/src/routes.tsx +++ b/frontend/src/routes.tsx @@ -6,7 +6,7 @@ import Unit from './pages/spreadsheets/Unit.tsx' import Download from './pages/Download.tsx' import Enrolment from './pages/Enrolment.tsx' import SendData from './pages/SendData.tsx' - +import GanttChart from './components/GanttChart.tsx' /** * Defines the routes configuration for the application. * Each route specifies a path and the corresponding component to render. @@ -32,8 +32,11 @@ const routes = [ element: , }, { - path: "timetablemod", + path: "timetablemod/*", element: , + children: [ + {path: ":location", element: } + ], }, { path: "download", diff --git a/frontend/src/scripts/api.ts b/frontend/src/scripts/api.ts index b986400..c49eff6 100644 --- a/frontend/src/scripts/api.ts +++ b/frontend/src/scripts/api.ts @@ -1,5 +1,5 @@ /* Timetable solver backend endpoint URL */ -const API_URL = 'http://localhost:8080/timetabling'; +const API_URL = "http://localhost:8080/timetabling"; /* =========================================== Defining types =========================================== */ diff --git a/frontend/src/scripts/solutionParsing.ts b/frontend/src/scripts/solutionParsing.ts new file mode 100644 index 0000000..c5a6118 --- /dev/null +++ b/frontend/src/scripts/solutionParsing.ts @@ -0,0 +1,172 @@ +import { TimetableSolution } from "./api"; +import { + TimelineGroup, + TimelineItem, +} from "vis-timeline/standalone"; + + +export type GanttGroup = TimelineGroup & { + treeLevel: number; +}; + +export type GanttItems = { + activities: TimelineItem[]; + rooms: GanttGroup[]; + buildings: GanttGroup[]; +}; + +const startDate = "2000-01-03"; + +export function getGanttItems(campusSolution: TimetableSolution): GanttItems { + let ganttActivities: TimelineItem[] = []; + let ganttRooms: GanttGroup[] = []; + let ganttBuildings: GanttGroup[] = []; + const seenRoom = new Set(); + const buildingLookup = new Map(); + let _return: GanttItems; + const activityEnum = new Map(); + const groupEnum = new Map(); + let counter = 1; + + + campusSolution.units.forEach((activity) => { + //=============================Handle Rooms================================= + if (!groupEnum.has(activity.room.roomCode)) { + groupEnum.set(activity.room.roomCode, counter); + counter++; + + const ganttRoom: GanttGroup = { + id: groupEnum.get(activity.room.roomCode) || 0, + content: activity.room.roomCode, + treeLevel: 2, + }; + seenRoom.add(activity.room.roomCode); + ganttRooms.push(ganttRoom); + } + //=============================Handle Activities============================ + if (!activityEnum.has(activity.unitId)) { + activityEnum.set(activity.unitId, counter); + counter++; + } + + const ganttActivity: TimelineItem = { + id: activityEnum.get(activity.unitId) || 0, + content: activity.name, + start: translateToDate(startDate, activity.dayOfWeek, activity.startTime), + end: translateToDate(startDate, activity.dayOfWeek, activity.end), + group: groupEnum.get(activity.room.roomCode) || 0, + }; + ganttActivities.push(ganttActivity); + //=============================Handle Buildings============================= + if (!groupEnum.has(activity.room.buildingId)) { + groupEnum.set(activity.room.buildingId, counter); + counter++; + const ganttBuilding: GanttGroup = { + id: groupEnum.get(activity.room.buildingId) || 0, + content: activity.room.buildingId, + treeLevel: 1, + nestedGroups: [groupEnum.get(activity.room.roomCode) ?? -1], + }; + buildingLookup.set(activity.room.buildingId, ganttBuilding); + ganttBuildings.push(ganttBuilding); + + const unassignable: TimelineItem = { + id: counter++, + content: "", + start: new Date("1000-01-01T05:00:00"), + end: new Date("3000-01-01T05:00:00"), + group: ganttBuilding.id, + type: "background", + className: "negative", + }; + ganttActivities.push(unassignable); + } + + const buildingCheck = buildingLookup.get(activity.room.buildingId); + const roomGroup = groupEnum.get(activity.room.roomCode); + if (buildingCheck && roomGroup) { + if ( + buildingCheck.nestedGroups !== undefined && + !buildingCheck.nestedGroups.includes(roomGroup) + ) { + buildingCheck.nestedGroups = buildingCheck.nestedGroups || []; + buildingCheck.nestedGroups.push(roomGroup); + } + } else { + throw new Error("LOGIC ERROR IN getGanttItems"); + } + }); + + _return = { + activities: ganttActivities, + rooms: ganttRooms, + buildings: ganttBuildings, + }; + return _return; +} + +function translateToDate(startDate: string, dayOfWeek: string, startTime: string):Date { + const daysOfWeek = ['SUNDAY', 'MONDAY', 'TUESDAY', 'WEDNESDAY', 'THURSDAY', 'FRIDAY', 'SATURDAY']; + + const baseDate = new Date(startDate); + + const targetDayIndex = daysOfWeek.indexOf(dayOfWeek.toUpperCase()); + const currentDayIndex = baseDate.getDay(); + + const dayDifference = (targetDayIndex + 7 - currentDayIndex) % 7; + + const [hours, minutes, seconds] = startTime.split(':').map(Number); + + const finalDate = new Date(baseDate); + finalDate.setDate(baseDate.getDate() + dayDifference); + finalDate.setHours(hours, minutes, seconds, 0); + console.log(finalDate); + return finalDate; +} + +// function dateToDayOfWeekAndTime(date: Date) { +// const daysOfWeek = [ +// "SUNDAY", +// "MONDAY", +// "TUESDAY", +// "WEDNESDAY", +// "THURSDAY", +// "FRIDAY", +// "SATURDAY", +// ]; + +// // Get the day of the week +// const dayOfWeek = daysOfWeek[date.getDay()]; + +// // Get the time in "HH:MM:SS" format +// const hours = String(date.getHours()).padStart(2, "0"); +// const minutes = String(date.getMinutes()).padStart(2, "0"); +// const seconds = String(date.getSeconds()).padStart(2, "0"); + +// const startTime = `${hours}:${minutes}:${seconds}`; + +// return { dayOfWeek, startTime }; +// } + +//TODO: Parse data to send to backend +export function formatSolution2Save(items: GanttItems) { + items +} + +//TODO: Parse data for downloading +export function format2CSV(items: GanttItems) { + items +} + +export function findCampusSolution( + campus: string, + solutions: TimetableSolution[] +): TimetableSolution | null { + let _return: TimetableSolution | null = null; + solutions.forEach((campusSolution) => { + if (campusSolution.campusName.toLowerCase() === campus) { + _return = campusSolution; + } + }); + return _return; +} diff --git a/frontend/src/styles/ganttUnassignable.css b/frontend/src/styles/ganttUnassignable.css new file mode 100644 index 0000000..ed6eff8 --- /dev/null +++ b/frontend/src/styles/ganttUnassignable.css @@ -0,0 +1,3 @@ +.vis-item.vis-background.negative { + background-color: rgba(255, 0, 0, 0.2); +} \ No newline at end of file