From 5371dd5de1a9a4a50b426b1a39f65a6674075dcb Mon Sep 17 00:00:00 2001 From: NguyenDonLam Date: Tue, 1 Oct 2024 19:46:02 +1000 Subject: [PATCH 1/9] feat: Add GanttChart.tsx, able to display ganttcharts --- frontend/src/components/GanttChart.tsx | 194 +++++++++++++++++++++++++ 1 file changed, 194 insertions(+) create mode 100644 frontend/src/components/GanttChart.tsx diff --git a/frontend/src/components/GanttChart.tsx b/frontend/src/components/GanttChart.tsx new file mode 100644 index 0000000..1ac5053 --- /dev/null +++ b/frontend/src/components/GanttChart.tsx @@ -0,0 +1,194 @@ +import { useEffect, useRef } from "react"; +import { Id } from "vis-data/declarations/data-interface"; +import { DataSet, Timeline } from "vis-timeline/standalone"; +import "vis-timeline/styles/vis-timeline-graph2d.min.css"; + +interface TimelineItem { + id: number; + content: string; + start: Date; + end: Date; + group: number; +} + +var sdt = { + rooms: [ + { id: 12, treeLevel: 3, content: "1A1" }, + { id: 13, treeLevel: 3, content: "1A2" }, + { id: 14, treeLevel: 3, content: "2A1" }, + { id: 15, treeLevel: 3, content: "2A2" }, + { id: 16, treeLevel: 3, content: "3A1" }, + { id: 17, treeLevel: 3, content: "3A2" }, + { id: 18, treeLevel: 3, content: "1B1" }, + { id: 19, treeLevel: 3, content: "1B2" }, + ], + grandParents: [ + { id: 1, content: "Building A", treeLevel: 1, nestedGroups: [5, 6, 7] }, + { id: 2, content: "Building B", treeLevel: 1, nestedGroups: [8, 9] }, + { id: 3, content: "Building C", treeLevel: 1, nestedGroups: [10, 11] }, + ], + parents: [ + { id: 5, treeLevel: 2, content: "Floor 1A", nestedGroups: [12, 13] }, + { id: 6, treeLevel: 2, content: "Floor 2A", nestedGroups: [14, 15] }, + { id: 7, treeLevel: 2, content: "Floor 3A", nestedGroups: [16, 17] }, + { id: 8, treeLevel: 2, content: "Floor 1B", nestedGroups: [18, 19] }, + { id: 9, treeLevel: 2, content: "Floor 2B" }, + { id: 10, treeLevel: 2, content: "Floor 1C" }, + { id: 11, treeLevel: 2, content: "Floor 1C" }, + ], +}; + +export default function GanttChart() { + const timelineRef = useRef(null); + const items = useRef(new DataSet()); + const groups = useRef(new DataSet()); + groups.current.add(sdt.grandParents); + groups.current.add(sdt.parents); + groups.current.add(sdt.rooms); + items.current.add([ + { + id: 1, + content: "Lecture", + start: new Date("2000-01-01T05:00:00"), + end: new Date("2000-01-01T09:00:00"), + group: 1, + }, + { + id: 2, + content: "Tutorial", + start: new Date("2000-01-04T06:00:00"), + end: new Date("2000-01-04T08:00:00"), + group: 2, + }, + { + id: 3, + content: "Lab 1", + start: new Date("2000-01-05T18:00:00"), + end: new Date("2000-01-05T20:00:00"), + group: 3, + }, + { + id: 4, + content: "Lab 2", + start: new Date("2000-01-06T18:00:00"), + end: new Date("2000-01-06T20:00:00"), + group: 4, + }, + ]); + + 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, + groups.current, + 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 + ) + 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 = groups.current.get(item.group); + return group.treeLevel === 3; + }; + + 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.toISOString(); + const end = item.end ? item.end.toISOString() : ""; + csvContent += `${item.id},${item.content},${start},${end},${item.group}\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 testFetch = () => { + const fetchPromise = fetch( + "https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json" + ); + + fetchPromise.then((response) => { + const jsonPromise = response.json(); + jsonPromise.then((data) => { + console.log(data[0].name); + }); + }); + }; + + return ( +
+
+ +
+ ); +} From 623de92ca096b898d411efa575fc537451fff192 Mon Sep 17 00:00:00 2001 From: NguyenDonLam Date: Tue, 1 Oct 2024 19:46:45 +1000 Subject: [PATCH 2/9] feat: Add dynamic routing, and gantt chart sidebar --- frontend/src/components/ModSiderbar.tsx | 81 +++++++++++++++++++++++++ frontend/src/pages/TimetableMod.tsx | 64 ++++++++++++++++--- frontend/src/routes.tsx | 4 ++ 3 files changed, 142 insertions(+), 7 deletions(-) create mode 100644 frontend/src/components/ModSiderbar.tsx diff --git a/frontend/src/components/ModSiderbar.tsx b/frontend/src/components/ModSiderbar.tsx new file mode 100644 index 0000000..aad59dc --- /dev/null +++ b/frontend/src/components/ModSiderbar.tsx @@ -0,0 +1,81 @@ +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 MailIcon from "@mui/icons-material/Mail"; + +import { Link } from "react-router-dom"; + +interface SidebarProps { + marginTop: number; + width: number; +} +const drawerWidth = 240; + +/** + * Renders a sidebar component with navigation links to Campus, Building, Room, + * Course, and Unit. + * + * @param {SidebarProps} props - The properties passed to Sidebar component + * @param {number} props.marginTop - The top margin of the sidebar. + * @param {number} props.width - The width of the sidebar. + * @returns Sidebar component with navigation links. + */ +export default function ModSidebar({ marginTop, width }: SidebarProps) { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +} diff --git a/frontend/src/pages/TimetableMod.tsx b/frontend/src/pages/TimetableMod.tsx index ff9a38b..413c88d 100644 --- a/frontend/src/pages/TimetableMod.tsx +++ b/frontend/src/pages/TimetableMod.tsx @@ -1,5 +1,10 @@ import { Link } from "react-router-dom"; - +import React, { 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"; /** * Renders the TimetableMod component to display and modify the generated * timetable. @@ -8,11 +13,56 @@ import { Link } from "react-router-dom"; * @returns JSX element containing the page content with navigation links */ export default function TimetableMod() { + const [data, setData] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + fetch("https://jetedge-backend-e1eeff4b0c04.herokuapp.com/timetabling/view") + .then((response) => { + if (!response.ok) { + throw new Error("Network response was not ok"); + } + return response.json(); + }) + .then((data) => { + setData(data); + console.log(data); + setLoading(false); + }) + .catch((error) => { + setError(error); + setLoading(false); + }); + }, []); + + if (loading) { + return
Loading...
; + } + + if (error) { + return
Error: {error.message}
; + } + return ( - <> -

This is the page to modify generated timetable

- Go Back - Go to Next - - ) +
+
+ + + {/* Spreadsheet */} +
+ +
+ +
+
+ + + + + +
+
+
+ ); } \ No newline at end of file diff --git a/frontend/src/routes.tsx b/frontend/src/routes.tsx index a0d04eb..21d1f03 100644 --- a/frontend/src/routes.tsx +++ b/frontend/src/routes.tsx @@ -6,6 +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. @@ -34,6 +35,9 @@ const routes = [ { path: "timetablemod", element: , + children: [ + { path: ":location", element: }, + ], }, { path: "download", From 05a2bfa2f8bb6e2188867ba45abe8e396dc7ff38 Mon Sep 17 00:00:00 2001 From: NguyenDonLam Date: Sat, 5 Oct 2024 16:37:01 +1000 Subject: [PATCH 3/9] feat(display ganttchart): dynamic routing implemented accordingly to campuses in database --- frontend/src/components/GanttChart.tsx | 25 ++---- frontend/src/components/ModSiderbar.tsx | 77 +++++++++--------- frontend/src/pages/TimetableMod.tsx | 48 ++++++++--- frontend/src/routes.tsx | 2 +- frontend/src/scripts/api.ts | 2 +- frontend/src/scripts/solutionParsing.ts | 103 ++++++++++++++++++++++++ 6 files changed, 186 insertions(+), 71 deletions(-) create mode 100644 frontend/src/scripts/solutionParsing.ts diff --git a/frontend/src/components/GanttChart.tsx b/frontend/src/components/GanttChart.tsx index 1ac5053..8c7ed5a 100644 --- a/frontend/src/components/GanttChart.tsx +++ b/frontend/src/components/GanttChart.tsx @@ -2,13 +2,11 @@ import { useEffect, useRef } from "react"; import { Id } from "vis-data/declarations/data-interface"; import { DataSet, Timeline } from "vis-timeline/standalone"; import "vis-timeline/styles/vis-timeline-graph2d.min.css"; +import { GanttItems } from "../scripts/solutionParsing"; +import { TimetableSolution } from "../scripts/api"; -interface TimelineItem { - id: number; - content: string; - start: Date; - end: Date; - group: number; +interface GanttProp { + solution: TimetableSolution; } var sdt = { @@ -38,7 +36,7 @@ var sdt = { ], }; -export default function GanttChart() { +export default function GanttChart({ solution }: GanttProp) { const timelineRef = useRef(null); const items = useRef(new DataSet()); const groups = useRef(new DataSet()); @@ -172,19 +170,6 @@ export default function GanttChart() { document.body.removeChild(link); }; - const testFetch = () => { - const fetchPromise = fetch( - "https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json" - ); - - fetchPromise.then((response) => { - const jsonPromise = response.json(); - jsonPromise.then((data) => { - console.log(data[0].name); - }); - }); - }; - return (
diff --git a/frontend/src/components/ModSiderbar.tsx b/frontend/src/components/ModSiderbar.tsx index aad59dc..8cfc8b6 100644 --- a/frontend/src/components/ModSiderbar.tsx +++ b/frontend/src/components/ModSiderbar.tsx @@ -7,11 +7,16 @@ import ListItemText from "@mui/material/ListItemText"; import InboxIcon from "@mui/icons-material/MoveToInbox"; // import MailIcon from "@mui/icons-material/Mail"; -import { Link } from "react-router-dom"; +import { Link, Route, Routes } from "react-router-dom"; +import GanttChart from "./GanttChart"; +import { GanttItems } from "../scripts/solutionParsing"; +import { TimetableSolution } from "../scripts/api"; +import { useEffect } from "react"; interface SidebarProps { marginTop: number; width: number; + campusSolutions: TimetableSolution[]; } const drawerWidth = 240; @@ -24,7 +29,10 @@ const drawerWidth = 240; * @param {number} props.width - The width of the sidebar. * @returns Sidebar component with navigation links. */ -export default function ModSidebar({ marginTop, width }: SidebarProps) { +export default function ModSidebar({ marginTop, width, campusSolutions }: SidebarProps) { + useEffect(() => { + console.log(campusSolutions.length) + }, []) return ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + {campusSolutions && campusSolutions.length > 0 ? ( + campusSolutions.map((solution, index) => { + let campusName = "HELLO WORLD"; + console.log("HEIIIII"); + return ( + + + + + + + + + // Pass the dynamic solution + } + /> + + + ); + }) + ) : ( +
Loading...
// Loading state or fallback if no data is available + )}
); } + diff --git a/frontend/src/pages/TimetableMod.tsx b/frontend/src/pages/TimetableMod.tsx index 413c88d..67e3e7d 100644 --- a/frontend/src/pages/TimetableMod.tsx +++ b/frontend/src/pages/TimetableMod.tsx @@ -5,6 +5,8 @@ import Footer from "../components/Footer"; import BackButton from "../components/BackButton"; import { Outlet } from "react-router-dom"; import ModSidebar from "../components/ModSiderbar"; +import { findCampus, findCampusSolution, GanttItems, getGanttItems } from "../scripts/solutionParsing"; +import { TimetableSolution } from "../scripts/api"; /** * Renders the TimetableMod component to display and modify the generated * timetable. @@ -16,9 +18,10 @@ export default function TimetableMod() { const [data, setData] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); + const [timetableSolutions, setTimetableSolutions] = useState(); useEffect(() => { - fetch("https://jetedge-backend-e1eeff4b0c04.herokuapp.com/timetabling/view") + fetch("http://localhost:8080/timetabling/view") .then((response) => { if (!response.ok) { throw new Error("Network response was not ok"); @@ -26,14 +29,30 @@ export default function TimetableMod() { return response.json(); }) .then((data) => { - setData(data); - console.log(data); + const timetableSolutions: TimetableSolution[] = data as TimetableSolution[]; + + // Now you can work with `timetableSolution` directly + setTimetableSolutions(timetableSolutions); + console.log("SOLUTION", timetableSolutions); + + // let asolution = findCampusSolution("Adelaide", data); + // if (asolution !== null) { + // setAdelaideSolution(getGanttItems(asolution)) + // } + // let gsolution = findCampusSolution("Geelong", data); + // if (gsolution !== null) { + // setAdelaideSolution(getGanttItems(gsolution)); + // } + // const msolution = findCampusSolution("Melbourne", data); + // if (msolution !== null) { + // setAdelaideSolution(getGanttItems(msolution)); + // } + // const ssolution = findCampusSolution("Sydney", data); + // if (ssolution !== null) { + // setAdelaideSolution(getGanttItems(ssolution)); + // } setLoading(false); }) - .catch((error) => { - setError(error); - setLoading(false); - }); }, []); if (loading) { @@ -43,14 +62,20 @@ export default function TimetableMod() { if (error) { return
Error: {error.message}
; } - return (
- + {/* Spreadsheet */} -
+
@@ -59,8 +84,7 @@ export default function TimetableMod() { - - +
diff --git a/frontend/src/routes.tsx b/frontend/src/routes.tsx index 21d1f03..0eb51b6 100644 --- a/frontend/src/routes.tsx +++ b/frontend/src/routes.tsx @@ -36,7 +36,7 @@ const routes = [ path: "timetablemod", element: , children: [ - { path: ":location", element: }, + { path: ":location" }, ], }, { 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..92be56a --- /dev/null +++ b/frontend/src/scripts/solutionParsing.ts @@ -0,0 +1,103 @@ +import GanttChart from "../components/GanttChart"; +import Room from "../pages/spreadsheets/Room"; +import { TimetableSolution } from "./api"; + +export type GanttRoom = { + id: string; + content: string; + treeLevel: 2; +}; + +export type GanttBuilding = { + id: string; + content: string; + treeLevel: 1; + nestedGroups: string[]; +}; + +export type GanttActivity = { + id: number; + content: string; + start: Date; + end: Date; + group: number|string; +}; + +export type GanttItems = { + activities: GanttActivity[], + rooms: GanttRoom[], + buildings: GanttBuilding[] +} + +export function getGanttItems(campusSolution: TimetableSolution): GanttItems { + let ganttActivities: GanttActivity[] = []; + let ganttRooms: GanttRoom[] = []; + let ganttBuildings: GanttBuilding[] = []; + const seenRoom = new Set(); + const buildingLookup = new Map() + let _return: GanttItems; + campusSolution.units.forEach((activity) => { + //=============================Handle Activities============================ + const ganttActivity: GanttActivity = { + id: activity.unitId, + content: activity.name, + start: new Date("2000-01-01T05:00:00"), + end: new Date("2000-01-01T05:00:00"), + group: activity.room.roomCode, + }; + ganttActivities.push(ganttActivity); + + //=============================Handle Buildings============================= + // if not seen building before, initiate it in the dict + if (buildingLookup.get(activity.room.buildingId) === undefined) { + const ganttBuilding: GanttBuilding = { + id: activity.room.buildingId, + content: activity.room.buildingId, + treeLevel: 1, + nestedGroups: [activity.room.roomCode], + } + buildingLookup.set(activity.room.buildingId, ganttBuilding); + ganttBuildings.push(ganttBuilding); + + // if seen it before, add room to list + } else { + buildingLookup.get(activity.room.buildingId)?.nestedGroups.push(activity.room.roomCode); + } + + //=============================Handle Room================================== + if (!seenRoom.has(activity.room.roomCode)) { + const ganttRoom: GanttRoom = { + id: activity.room.roomCode, + content: activity.room.roomCode, + treeLevel: 2, + }; + seenRoom.add(activity.room.roomCode); + ganttRooms.push(ganttRoom); + } + }); + _return = { + activities: ganttActivities, + rooms: ganttRooms, + buildings: ganttBuildings, + }; + return _return; +} + +export function formatSolution( + Buildings: GanttBuilding[], + Rooms: GanttRoom[], + activities: GanttActivity +): any {} + +export function findCampusSolution( + campus: string, + solutions: TimetableSolution[] +): TimetableSolution|null { + let _return: TimetableSolution|null = null; + solutions.forEach((campusSolution) => { + if (campusSolution.campusName === campus) { + _return = campusSolution; + } + }); + return _return; +} From 79a4c07181a84c91d95f778dae4a869be2ed0ebd Mon Sep 17 00:00:00 2001 From: NguyenDonLam Date: Sun, 6 Oct 2024 12:14:17 +1100 Subject: [PATCH 4/9] feat(display ganttchart): display ganttchart, remove all warnings in code --- .vscode/settings.json | 6 +- frontend/src/components/GanttChart.tsx | 160 +++++++++++--------- frontend/src/components/ModSiderbar.tsx | 118 ++++++++------- frontend/src/pages/TimetableMod.tsx | 83 ++++------ frontend/src/routes.tsx | 5 +- frontend/src/scripts/solutionParsing.ts | 192 ++++++++++++++++-------- 6 files changed, 316 insertions(+), 248 deletions(-) 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/src/components/GanttChart.tsx b/frontend/src/components/GanttChart.tsx index 8c7ed5a..7c4035b 100644 --- a/frontend/src/components/GanttChart.tsx +++ b/frontend/src/components/GanttChart.tsx @@ -1,78 +1,93 @@ -import { useEffect, useRef } from "react"; +import { memo, useEffect, useRef } from "react"; import { Id } from "vis-data/declarations/data-interface"; -import { DataSet, Timeline } from "vis-timeline/standalone"; +import { DataGroupCollectionType, DataItemCollectionType, DataSet, Timeline, TimelineItem } from "vis-timeline/standalone"; import "vis-timeline/styles/vis-timeline-graph2d.min.css"; -import { GanttItems } from "../scripts/solutionParsing"; +import { + findCampusSolution, + GanttGroup, + GanttItems, + getGanttItems, +} from "../scripts/solutionParsing"; import { TimetableSolution } from "../scripts/api"; - -interface GanttProp { - solution: TimetableSolution; -} +import { useParams } from "react-router-dom"; var sdt = { - rooms: [ - { id: 12, treeLevel: 3, content: "1A1" }, - { id: 13, treeLevel: 3, content: "1A2" }, - { id: 14, treeLevel: 3, content: "2A1" }, - { id: 15, treeLevel: 3, content: "2A2" }, - { id: 16, treeLevel: 3, content: "3A1" }, - { id: 17, treeLevel: 3, content: "3A2" }, - { id: 18, treeLevel: 3, content: "1B1" }, - { id: 19, treeLevel: 3, content: "1B2" }, - ], - grandParents: [ - { id: 1, content: "Building A", treeLevel: 1, nestedGroups: [5, 6, 7] }, - { id: 2, content: "Building B", treeLevel: 1, nestedGroups: [8, 9] }, - { id: 3, content: "Building C", treeLevel: 1, nestedGroups: [10, 11] }, - ], - parents: [ - { id: 5, treeLevel: 2, content: "Floor 1A", nestedGroups: [12, 13] }, - { id: 6, treeLevel: 2, content: "Floor 2A", nestedGroups: [14, 15] }, - { id: 7, treeLevel: 2, content: "Floor 3A", nestedGroups: [16, 17] }, - { id: 8, treeLevel: 2, content: "Floor 1B", nestedGroups: [18, 19] }, - { id: 9, treeLevel: 2, content: "Floor 2B" }, - { id: 10, treeLevel: 2, content: "Floor 1C" }, - { id: 11, treeLevel: 2, content: "Floor 1C" }, + buildings: [ + { + content: "A1", + id: "A1", + nestedGroups: ["2", "9"], + treeLevel: 1, + }, + { + content: "A2", + id: "A2", + nestedGroups: ["3", "10"], + treeLevel: 1, + }, ], }; -export default function GanttChart({ solution }: GanttProp) { +var activities = [ + { + id: 1, + content: "Lecture", + start: new Date("2000-01-01T05:00:00"), + end: new Date("2000-01-01T09:00:00"), + group: 1, + }, + { + id: 2, + content: "Tutorial", + start: new Date("2000-01-04T06:00:00"), + end: new Date("2000-01-04T08:00:00"), + group: 2, + }, + { + id: 3, + content: "Lab 1", + start: new Date("2000-01-05T18:00:00"), + end: new Date("2000-01-05T20:00:00"), + group: 3, + }, + { + id: 4, + content: "Lab 2", + start: new Date("2000-01-06T18:00:00"), + end: new Date("2000-01-06T20:00:00"), + group: 4, + }, +]; + +export default memo(function GanttChart() { + const params = useParams(); const timelineRef = useRef(null); const items = useRef(new DataSet()); - const groups = useRef(new DataSet()); - groups.current.add(sdt.grandParents); - groups.current.add(sdt.parents); - groups.current.add(sdt.rooms); - items.current.add([ - { - id: 1, - content: "Lecture", - start: new Date("2000-01-01T05:00:00"), - end: new Date("2000-01-01T09:00:00"), - group: 1, - }, - { - id: 2, - content: "Tutorial", - start: new Date("2000-01-04T06:00:00"), - end: new Date("2000-01-04T08:00:00"), - group: 2, - }, - { - id: 3, - content: "Lab 1", - start: new Date("2000-01-05T18:00:00"), - end: new Date("2000-01-05T20:00:00"), - group: 3, - }, - { - id: 4, - content: "Lab 2", - start: new Date("2000-01-06T18:00:00"), - end: new Date("2000-01-06T20:00:00"), - group: 4, - }, - ]); + 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) { @@ -89,8 +104,8 @@ export default function GanttChart({ solution }: GanttProp) { // Initialize the timeline const timeline = new Timeline( timelineRef.current, - items.current, - groups.current, + items.current as DataItemCollectionType, + groups.current as DataGroupCollectionType, options ); @@ -116,8 +131,9 @@ export default function GanttChart({ solution }: GanttProp) { const validGroup = (item: TimelineItem | null) => { if (item == null) return true; - const group = groups.current.get(item.group); - return group.treeLevel === 3; + const group: GanttGroup|null = groups.current.get(item.group as Id); + return (group !== null && group.treeLevel === 2); + }; timeline.on("select", (properties) => { @@ -149,8 +165,8 @@ export default function GanttChart({ solution }: GanttProp) { let csvContent = "id,content,start,end,group\n"; const itemList = items.current.get(); itemList.forEach((item) => { - const start = item.start.toISOString(); - const end = item.end ? item.end.toISOString() : ""; + const start = item.start.toString(); + const end = item.end ? item.end.toString() : ""; csvContent += `${item.id},${item.content},${start},${end},${item.group}\n`; }); @@ -176,4 +192,4 @@ export default function GanttChart({ solution }: GanttProp) {
); -} +}) diff --git a/frontend/src/components/ModSiderbar.tsx b/frontend/src/components/ModSiderbar.tsx index 8cfc8b6..0cb3765 100644 --- a/frontend/src/components/ModSiderbar.tsx +++ b/frontend/src/components/ModSiderbar.tsx @@ -6,17 +6,14 @@ import ListItemIcon from "@mui/material/ListItemIcon"; import ListItemText from "@mui/material/ListItemText"; import InboxIcon from "@mui/icons-material/MoveToInbox"; // import MailIcon from "@mui/icons-material/Mail"; +import { BrowserRouter as Router, Routes, Route, Link } from "react-router-dom"; -import { Link, Route, Routes } from "react-router-dom"; import GanttChart from "./GanttChart"; -import { GanttItems } from "../scripts/solutionParsing"; import { TimetableSolution } from "../scripts/api"; -import { useEffect } from "react"; interface SidebarProps { marginTop: number; width: number; - campusSolutions: TimetableSolution[]; } const drawerWidth = 240; @@ -29,56 +26,69 @@ const drawerWidth = 240; * @param {number} props.width - The width of the sidebar. * @returns Sidebar component with navigation links. */ -export default function ModSidebar({ marginTop, width, campusSolutions }: SidebarProps) { - useEffect(() => { - console.log(campusSolutions.length) - }, []) +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, index) => { - let campusName = "HELLO WORLD"; - console.log("HEIIIII"); - return ( - - - - - - - - - // Pass the dynamic solution - } - /> - - - ); - }) - ) : ( -
Loading...
// Loading state or fallback if no data is available - )} -
-
+ <> + + + {campusSolutions && campusSolutions.length > 0 ? ( + campusSolutions.map((solution) => { + let campusName = solution.campusName; // Assuming campusName comes from solution + return ( + + + + + + + + + ); + }) + ) : ( +
Loading...
// Fallback loading state + )} +
+
+ +
+ + {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 67e3e7d..bcb7fa6 100644 --- a/frontend/src/pages/TimetableMod.tsx +++ b/frontend/src/pages/TimetableMod.tsx @@ -1,24 +1,21 @@ import { Link } from "react-router-dom"; -import React, { useEffect, useState } from "react"; +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 { findCampus, findCampusSolution, GanttItems, getGanttItems } from "../scripts/solutionParsing"; 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 [data, setData] = useState(null); const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - const [timetableSolutions, setTimetableSolutions] = useState(); useEffect(() => { fetch("http://localhost:8080/timetabling/view") @@ -29,64 +26,40 @@ export default function TimetableMod() { return response.json(); }) .then((data) => { - const timetableSolutions: TimetableSolution[] = data as TimetableSolution[]; - - // Now you can work with `timetableSolution` directly - setTimetableSolutions(timetableSolutions); - console.log("SOLUTION", timetableSolutions); - - // let asolution = findCampusSolution("Adelaide", data); - // if (asolution !== null) { - // setAdelaideSolution(getGanttItems(asolution)) - // } - // let gsolution = findCampusSolution("Geelong", data); - // if (gsolution !== null) { - // setAdelaideSolution(getGanttItems(gsolution)); - // } - // const msolution = findCampusSolution("Melbourne", data); - // if (msolution !== null) { - // setAdelaideSolution(getGanttItems(msolution)); - // } - // const ssolution = findCampusSolution("Sydney", data); - // if (ssolution !== null) { - // setAdelaideSolution(getGanttItems(ssolution)); - // } + const timetableSolutions: TimetableSolution[] = + data as TimetableSolution[]; + sessionStorage.setItem("campusSolutions", JSON.stringify(timetableSolutions)); setLoading(false); - }) + }); }, []); if (loading) { return
Loading...
; } - - if (error) { - return
Error: {error.message}
; - } return (
-
- +
+ - {/* Spreadsheet */} -
- -
- -
-
- - - - +
+
-
+ +
+
+ + + + +
+
); } \ No newline at end of file diff --git a/frontend/src/routes.tsx b/frontend/src/routes.tsx index 0eb51b6..48d05c2 100644 --- a/frontend/src/routes.tsx +++ b/frontend/src/routes.tsx @@ -7,7 +7,6 @@ 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. @@ -33,10 +32,10 @@ const routes = [ element: , }, { - path: "timetablemod", + path: "timetablemod/*", element: , children: [ - { path: ":location" }, + {path: ":location", element: } ], }, { diff --git a/frontend/src/scripts/solutionParsing.ts b/frontend/src/scripts/solutionParsing.ts index 92be56a..11392eb 100644 --- a/frontend/src/scripts/solutionParsing.ts +++ b/frontend/src/scripts/solutionParsing.ts @@ -1,80 +1,108 @@ -import GanttChart from "../components/GanttChart"; -import Room from "../pages/spreadsheets/Room"; import { TimetableSolution } from "./api"; - -export type GanttRoom = { - id: string; - content: string; - treeLevel: 2; +import { + TimelineGroup, + TimelineItem, +} from "vis-timeline/standalone"; +export type GanttGroup = TimelineGroup & { + treeLevel: number; }; -export type GanttBuilding = { - id: string; - content: string; - treeLevel: 1; - nestedGroups: string[]; -}; +// export type GanttBuilding = { +// id: number; +// content: string; +// treeLevel: 1; +// nestedGroups: number[]; +// }; -export type GanttActivity = { - id: number; - content: string; - start: Date; - end: Date; - group: number|string; -}; +// export type GanttActivity = { +// id: number; +// content: string; +// start: Date; +// end: Date; +// group: number | string; +// }; export type GanttItems = { - activities: GanttActivity[], - rooms: GanttRoom[], - buildings: GanttBuilding[] -} + activities: TimelineItem[]; + rooms: GanttGroup[]; + buildings: GanttGroup[]; +}; + +const startDate = "2000-01-03"; export function getGanttItems(campusSolution: TimetableSolution): GanttItems { - let ganttActivities: GanttActivity[] = []; - let ganttRooms: GanttRoom[] = []; - let ganttBuildings: GanttBuilding[] = []; + let ganttActivities: TimelineItem[] = []; + let ganttRooms: GanttGroup[] = []; + let ganttBuildings: GanttGroup[] = []; const seenRoom = new Set(); - const buildingLookup = new Map() + const buildingLookup = new Map(); let _return: GanttItems; + const activityEnum = new Map(); + const groupEnum = new Map(); + let counter = 1; // Global counter for unique IDs across buildings, rooms, and activities + campusSolution.units.forEach((activity) => { //=============================Handle Activities============================ - const ganttActivity: GanttActivity = { - id: activity.unitId, + if (!activityEnum.has(activity.unitId)) { + activityEnum.set(activity.unitId, counter); + counter++; + } + + const ganttActivity: TimelineItem = { + id: activityEnum.get(activity.unitId) || 0, content: activity.name, - start: new Date("2000-01-01T05:00:00"), - end: new Date("2000-01-01T05:00:00"), - group: activity.room.roomCode, + 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 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 Buildings============================= - // if not seen building before, initiate it in the dict - if (buildingLookup.get(activity.room.buildingId) === undefined) { - const ganttBuilding: GanttBuilding = { - id: activity.room.buildingId, + 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: [activity.room.roomCode], - } + nestedGroups: [groupEnum.get(activity.room.roomCode) ?? -1], + }; buildingLookup.set(activity.room.buildingId, ganttBuilding); ganttBuildings.push(ganttBuilding); - - // if seen it before, add room to list - } else { - buildingLookup.get(activity.room.buildingId)?.nestedGroups.push(activity.room.roomCode); } - //=============================Handle Room================================== - if (!seenRoom.has(activity.room.roomCode)) { - const ganttRoom: GanttRoom = { - id: activity.room.roomCode, - content: activity.room.roomCode, - treeLevel: 2, - }; - seenRoom.add(activity.room.roomCode); - ganttRooms.push(ganttRoom); + 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) + ) { + if (buildingCheck) { + buildingCheck.nestedGroups = buildingCheck.nestedGroups || []; + if (roomGroup) { + buildingCheck.nestedGroups.push(roomGroup); + } + } + } + } else { + throw new Error("LOGIC ERROR IN getGanttItems"); } }); + _return = { activities: ganttActivities, rooms: ganttRooms, @@ -83,19 +111,63 @@ export function getGanttItems(campusSolution: TimetableSolution): GanttItems { return _return; } -export function formatSolution( - Buildings: GanttBuilding[], - Rooms: GanttRoom[], - activities: GanttActivity -): any {} +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 }; +} + + +// export function formatSolution2Save( +// items: GanttItems): TimetableSolution { +// let _return:TimetableSolution; + +// } export function findCampusSolution( campus: string, solutions: TimetableSolution[] -): TimetableSolution|null { - let _return: TimetableSolution|null = null; +): TimetableSolution | null { + let _return: TimetableSolution | null = null; solutions.forEach((campusSolution) => { - if (campusSolution.campusName === campus) { + if (campusSolution.campusName.toLowerCase() === campus) { _return = campusSolution; } }); From bd92f7aaa25ae74cb47c7dcd3d12a1cb8429c490 Mon Sep 17 00:00:00 2001 From: NguyenDonLam Date: Sun, 6 Oct 2024 12:35:38 +1100 Subject: [PATCH 5/9] feat(display ganttchart): add undraggable areas highlight undraggable areas(buildings) in red --- frontend/src/components/GanttChart.tsx | 2 ++ frontend/src/scripts/solutionParsing.ts | 23 ++++++++++++++++------- frontend/src/styles/ganttUnassignable.css | 3 +++ 3 files changed, 21 insertions(+), 7 deletions(-) create mode 100644 frontend/src/styles/ganttUnassignable.css diff --git a/frontend/src/components/GanttChart.tsx b/frontend/src/components/GanttChart.tsx index 7c4035b..435fc03 100644 --- a/frontend/src/components/GanttChart.tsx +++ b/frontend/src/components/GanttChart.tsx @@ -2,6 +2,8 @@ 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, diff --git a/frontend/src/scripts/solutionParsing.ts b/frontend/src/scripts/solutionParsing.ts index 11392eb..a0dcc2c 100644 --- a/frontend/src/scripts/solutionParsing.ts +++ b/frontend/src/scripts/solutionParsing.ts @@ -41,6 +41,7 @@ export function getGanttItems(campusSolution: TimetableSolution): GanttItems { const groupEnum = new Map(); let counter = 1; // Global counter for unique IDs across buildings, rooms, and activities + campusSolution.units.forEach((activity) => { //=============================Handle Activities============================ if (!activityEnum.has(activity.unitId)) { @@ -82,21 +83,29 @@ export function getGanttItems(campusSolution: TimetableSolution): GanttItems { }; 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.includes(roomGroup) ) { - if (buildingCheck) { - buildingCheck.nestedGroups = buildingCheck.nestedGroups || []; - if (roomGroup) { - buildingCheck.nestedGroups.push(roomGroup); - } - } + buildingCheck.nestedGroups = buildingCheck.nestedGroups || []; + buildingCheck.nestedGroups.push(roomGroup); } } else { throw new Error("LOGIC ERROR IN getGanttItems"); 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 From cf3b9683055129e783a8a0108af780f62dd11c87 Mon Sep 17 00:00:00 2001 From: NguyenDonLam Date: Sun, 6 Oct 2024 14:37:17 +1100 Subject: [PATCH 6/9] fix(display ganttchart): fix bug when some activities arent displayed in their corresponding room --- frontend/src/components/GanttChart.tsx | 10 +++- frontend/src/scripts/solutionParsing.ts | 71 +++++++++++++++---------- 2 files changed, 51 insertions(+), 30 deletions(-) diff --git a/frontend/src/components/GanttChart.tsx b/frontend/src/components/GanttChart.tsx index 435fc03..f807ed4 100644 --- a/frontend/src/components/GanttChart.tsx +++ b/frontend/src/components/GanttChart.tsx @@ -117,7 +117,8 @@ export default memo(function GanttChart() { if ( newItem == null || item.id === newItem.id || - item.group !== newItem.group + item.group !== newItem.group || + item.type === "background" ) return false; @@ -169,7 +170,7 @@ export default memo(function GanttChart() { itemList.forEach((item) => { const start = item.start.toString(); const end = item.end ? item.end.toString() : ""; - csvContent += `${item.id},${item.content},${start},${end},${item.group}\n`; + csvContent += `${item.id},${item.content},${start},${end},${groups.current.get(item.group as Id)?.content}\n`; }); return csvContent; @@ -188,10 +189,15 @@ export default memo(function GanttChart() { document.body.removeChild(link); }; + const saveData = () => { + + }; + return (
+
); }) diff --git a/frontend/src/scripts/solutionParsing.ts b/frontend/src/scripts/solutionParsing.ts index a0dcc2c..40f529e 100644 --- a/frontend/src/scripts/solutionParsing.ts +++ b/frontend/src/scripts/solutionParsing.ts @@ -1,25 +1,27 @@ -import { TimetableSolution } from "./api"; +import { TimetableSolution, Unit } from "./api"; import { TimelineGroup, TimelineItem, } from "vis-timeline/standalone"; + + export type GanttGroup = TimelineGroup & { treeLevel: number; }; -// export type GanttBuilding = { -// id: number; -// content: string; -// treeLevel: 1; -// nestedGroups: number[]; +// export type GanttGroup = { +// id: number, +// content: string, +// treeLevel: number; +// nestedGroup?: number[]; // }; -// export type GanttActivity = { +// export type TimelineItem = { // id: number; // content: string; -// start: Date; -// end: Date; -// group: number | string; +// start: string; +// end: string; +// group: number; // }; export type GanttItems = { @@ -43,6 +45,19 @@ export function getGanttItems(campusSolution: TimetableSolution): GanttItems { 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); @@ -57,20 +72,6 @@ export function getGanttItems(campusSolution: TimetableSolution): GanttItems { group: groupEnum.get(activity.room.roomCode) || 0, }; ganttActivities.push(ganttActivity); - - //=============================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 Buildings============================= if (!groupEnum.has(activity.room.buildingId)) { groupEnum.set(activity.room.buildingId, counter); @@ -99,7 +100,6 @@ export function getGanttItems(campusSolution: TimetableSolution): GanttItems { 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) @@ -163,12 +163,27 @@ function dateToDayOfWeekAndTime(date: Date) { return { dayOfWeek, startTime }; } - +//TODO: Parse data to send to backend // export function formatSolution2Save( // items: GanttItems): TimetableSolution { -// let _return:TimetableSolution; -// } +// let _return:TimetableSolution; +// let units:Unit[] = []; +// items.activities.forEach((activity)=>{ +// let unit: Unit = { +// unitId: +// name: +// campus: +// course: + +// } +// }) +// } + +//TODO: Parse data for downloading +export function format2CSV(items: GanttItems) { + items +} export function findCampusSolution( campus: string, From bf8a80e657a1e04a8ca7b2869f60543ecc6f4f72 Mon Sep 17 00:00:00 2001 From: NguyenDonLam Date: Sun, 6 Oct 2024 15:15:05 +1100 Subject: [PATCH 7/9] docs(display ganttchart): clean up unecessary comments and hardcodes --- frontend/src/components/GanttChart.tsx | 48 ------------------------- frontend/src/components/ModSiderbar.tsx | 13 ++----- frontend/src/scripts/solutionParsing.ts | 37 +++---------------- 3 files changed, 7 insertions(+), 91 deletions(-) diff --git a/frontend/src/components/GanttChart.tsx b/frontend/src/components/GanttChart.tsx index f807ed4..1968654 100644 --- a/frontend/src/components/GanttChart.tsx +++ b/frontend/src/components/GanttChart.tsx @@ -13,54 +13,6 @@ import { import { TimetableSolution } from "../scripts/api"; import { useParams } from "react-router-dom"; -var sdt = { - buildings: [ - { - content: "A1", - id: "A1", - nestedGroups: ["2", "9"], - treeLevel: 1, - }, - { - content: "A2", - id: "A2", - nestedGroups: ["3", "10"], - treeLevel: 1, - }, - ], -}; - -var activities = [ - { - id: 1, - content: "Lecture", - start: new Date("2000-01-01T05:00:00"), - end: new Date("2000-01-01T09:00:00"), - group: 1, - }, - { - id: 2, - content: "Tutorial", - start: new Date("2000-01-04T06:00:00"), - end: new Date("2000-01-04T08:00:00"), - group: 2, - }, - { - id: 3, - content: "Lab 1", - start: new Date("2000-01-05T18:00:00"), - end: new Date("2000-01-05T20:00:00"), - group: 3, - }, - { - id: 4, - content: "Lab 2", - start: new Date("2000-01-06T18:00:00"), - end: new Date("2000-01-06T20:00:00"), - group: 4, - }, -]; - export default memo(function GanttChart() { const params = useParams(); const timelineRef = useRef(null); diff --git a/frontend/src/components/ModSiderbar.tsx b/frontend/src/components/ModSiderbar.tsx index 0cb3765..f69463a 100644 --- a/frontend/src/components/ModSiderbar.tsx +++ b/frontend/src/components/ModSiderbar.tsx @@ -17,15 +17,6 @@ interface SidebarProps { } const drawerWidth = 240; -/** - * Renders a sidebar component with navigation links to Campus, Building, Room, - * Course, and Unit. - * - * @param {SidebarProps} props - The properties passed to Sidebar component - * @param {number} props.marginTop - The top margin of the sidebar. - * @param {number} props.width - The width of the sidebar. - * @returns Sidebar component with navigation links. - */ export default function ModSidebar({ marginTop, width }: SidebarProps) { let campusSolutions: TimetableSolution[]; let campusSolutionsStr: string|null = sessionStorage.getItem("campusSolutions"); @@ -52,7 +43,7 @@ export default function ModSidebar({ marginTop, width }: SidebarProps) { {campusSolutions && campusSolutions.length > 0 ? ( campusSolutions.map((solution) => { - let campusName = solution.campusName; // Assuming campusName comes from solution + let campusName = solution.campusName; return ( Loading...
// Fallback loading state +
Loading...
)} diff --git a/frontend/src/scripts/solutionParsing.ts b/frontend/src/scripts/solutionParsing.ts index 40f529e..6e1f1d9 100644 --- a/frontend/src/scripts/solutionParsing.ts +++ b/frontend/src/scripts/solutionParsing.ts @@ -1,4 +1,4 @@ -import { TimetableSolution, Unit } from "./api"; +import { TimetableSolution } from "./api"; import { TimelineGroup, TimelineItem, @@ -9,21 +9,6 @@ export type GanttGroup = TimelineGroup & { treeLevel: number; }; -// export type GanttGroup = { -// id: number, -// content: string, -// treeLevel: number; -// nestedGroup?: number[]; -// }; - -// export type TimelineItem = { -// id: number; -// content: string; -// start: string; -// end: string; -// group: number; -// }; - export type GanttItems = { activities: TimelineItem[]; rooms: GanttGroup[]; @@ -41,7 +26,7 @@ export function getGanttItems(campusSolution: TimetableSolution): GanttItems { let _return: GanttItems; const activityEnum = new Map(); const groupEnum = new Map(); - let counter = 1; // Global counter for unique IDs across buildings, rooms, and activities + let counter = 1; campusSolution.units.forEach((activity) => { @@ -164,21 +149,9 @@ function dateToDayOfWeekAndTime(date: Date) { } //TODO: Parse data to send to backend -// export function formatSolution2Save( -// items: GanttItems): TimetableSolution { - -// let _return:TimetableSolution; -// let units:Unit[] = []; -// items.activities.forEach((activity)=>{ -// let unit: Unit = { -// unitId: -// name: -// campus: -// course: - -// } -// }) -// } +export function formatSolution2Save(items: GanttItems) { + items +} //TODO: Parse data for downloading export function format2CSV(items: GanttItems) { From dccb0fd2cb02b8414dce42501506b019508619c4 Mon Sep 17 00:00:00 2001 From: NguyenDonLam Date: Sun, 6 Oct 2024 15:48:45 +1100 Subject: [PATCH 8/9] fix(display ganttchart): final clean up, project now buildable --- frontend/src/components/ModSiderbar.tsx | 3 +- frontend/src/scripts/solutionParsing.ts | 46 ++++++++++++------------- 2 files changed, 24 insertions(+), 25 deletions(-) diff --git a/frontend/src/components/ModSiderbar.tsx b/frontend/src/components/ModSiderbar.tsx index f69463a..280c4a2 100644 --- a/frontend/src/components/ModSiderbar.tsx +++ b/frontend/src/components/ModSiderbar.tsx @@ -5,8 +5,7 @@ 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 MailIcon from "@mui/icons-material/Mail"; -import { BrowserRouter as Router, Routes, Route, Link } from "react-router-dom"; +import { Routes, Route, Link } from "react-router-dom"; import GanttChart from "./GanttChart"; import { TimetableSolution } from "../scripts/api"; diff --git a/frontend/src/scripts/solutionParsing.ts b/frontend/src/scripts/solutionParsing.ts index 6e1f1d9..c5a6118 100644 --- a/frontend/src/scripts/solutionParsing.ts +++ b/frontend/src/scripts/solutionParsing.ts @@ -124,29 +124,29 @@ function translateToDate(startDate: string, dayOfWeek: string, startTime: string 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 }; -} +// 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) { From 6ca22b1bd0f14a04e46016ebdc5c8fd2da76e2a3 Mon Sep 17 00:00:00 2001 From: NguyenDonLam Date: Sun, 6 Oct 2024 18:35:46 +1100 Subject: [PATCH 9/9] fix(display ganttchart): add vis-timeline in package.json --- frontend/package-lock.json | 150 ++++++++++++++++++++++++++++++++++++- frontend/package.json | 3 +- 2 files changed, 150 insertions(+), 3 deletions(-) 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",