Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

bug/265 - enable working hour function and issue/272 - new UI for setting page to work with working hour function #279

Merged
merged 15 commits into from
Apr 25, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion doto-backend/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,4 @@ ACCESS_TOKEN_SECRET=
GOOGLE_API_SECRET=
GOOGLE_API_CLIENT=
VAPID_PUBLIC_KEY=
VAPID_PRIVATE_KEY=
VAPID_PRIVATE_KEY=
10 changes: 8 additions & 2 deletions doto-frontend/src/components/pages/Calendar/Calendar.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import "./Calendar.css";
import "../Pages.css";
import { v4 as uuidv4 } from "uuid";
import { Themes } from "../../../constants/Themes";
import { ActiveHoursContext } from "../../../context/ActiveHoursContext";

const classnames = require("classnames");

Expand All @@ -44,6 +45,9 @@ const Calendar = () => {
const [tasks, setTasks] = useState([]);
const [open, setOpen] = useState(false);
const [theme, setTheme] = useContext(ThemeContext);
const { activeHoursStart, activeHoursEnd } = useContext(ActiveHoursContext);
const [startTime, setStartTime] = activeHoursStart;
const [endTime, setEndTime] = activeHoursEnd;

const handleOpen = () => {
setOpen(true);
Expand All @@ -61,14 +65,16 @@ const Calendar = () => {
const fetchUserInfo = async () => {
const userInfo = await DotoService.getUserInfo();
setTheme(userInfo.themePreference);
setStartTime(userInfo.startTime);
setEndTime(userInfo.endTime);
};
fetchUserInfo();
fetchTasks();
}, [setTheme]);
}, [setTheme, setStartTime, setEndTime]);

// Adds new task based on input fields from Modal
const addNewTask = async newTask => {
const { newTaskOrder, updatedTask } = addTaskToSchedule(newTask, tasks);
const { newTaskOrder, updatedTask } = addTaskToSchedule(newTask, tasks, new Date(startTime), new Date(endTime));
newTask.taskId = uuidv4();
newTask.id = newTask.taskId;
setTasks(newTaskOrder);
Expand Down
18 changes: 17 additions & 1 deletion doto-frontend/src/components/pages/Calendar/Calendar.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,25 +7,41 @@ import CalendarTodayIcon from "@material-ui/icons/CalendarToday";
import Calendar from "./Calendar";
import CalendarListView from "./CalendarListView";
import { ThemeContext } from "../../../context/ThemeContext";
import { ActiveHoursContext } from "../../../context/ActiveHoursContext";

describe("<Calendar /> component being rendered", () => {
let theme;
let activeHourStartTime;
let activeHourEndTime;
const setTheme = jest.fn();
const setActiveHourStartTime = jest.fn();
const setActiveHourEndTime = jest.fn();
const useStateSpy = jest.spyOn(React, "useState");
useStateSpy.mockImplementation(theme => [theme, setTheme]);
useStateSpy.mockImplementation(activeHourStartTime => [activeHourStartTime, setActiveHourStartTime]);
useStateSpy.mockImplementation(activeHourEndTime => [activeHourEndTime, setActiveHourEndTime]);

const Wrapper = () => {
return (
<Router>
<ThemeContext.Provider value={[theme, setTheme]}>
<Calendar />
<ActiveHoursContext.Provider
value={{
activeHoursStart: [activeHourStartTime, setActiveHourStartTime],
activeHoursEnd: [activeHourEndTime, setActiveHourEndTime],
}}
>
<Calendar />
</ActiveHoursContext.Provider>
</ThemeContext.Provider>
</Router>
);
};

beforeEach(() => {
useStateSpy.mockImplementation(theme => [theme, setTheme]);
useStateSpy.mockImplementation(activeHourStartTime => [activeHourStartTime, setActiveHourStartTime]);
useStateSpy.mockImplementation(activeHourEndTime => [activeHourEndTime, setActiveHourEndTime]);
});

afterEach(() => {
Expand Down
15 changes: 12 additions & 3 deletions doto-frontend/src/components/pages/Calendar/TaskScheduler.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { shiftTasks } from "./TaskShifter";
const MILLISECONDS_PER_MINUTE = 60000;

/**
Expand All @@ -12,10 +13,11 @@ const MILLISECONDS_PER_MINUTE = 60000;
* @returns A chronologically ordered (based on startDate) array of all tasks scheduled with start/end
* datetimes - essentially existingTasks + newTasks
*/
const addTaskToSchedule = (newTask, existingTasks) => {
const addTaskToSchedule = (newTask, existingTasks, startTime, endTime) => {
// TODO: Take into account any possible gap between datetime and startDate of the first task in oldTasks
// TODO: Take into account priority of tasks
// TODO: Take into account location of tasks, add time gaps to allow for travel
// TODO: Take into account working hour restriction

const competingTasks = [];
const oldTasks = [];
Expand Down Expand Up @@ -92,8 +94,12 @@ const addTaskToSchedule = (newTask, existingTasks) => {
}
// Insert the new task at the specified index
competingTasks.splice(i, 0, newTask);

// Shift the Tasks based on working hours
const { shiftedTasks } = shiftTasks([...oldTasks, ...competingTasks], startTime, endTime);

return {
newTaskOrder: [...oldTasks, ...competingTasks],
newTaskOrder: shiftedTasks,
updatedTask: newTask,
};
}
Expand Down Expand Up @@ -121,8 +127,11 @@ const addTaskToSchedule = (newTask, existingTasks) => {
newTask.travelTime * MILLISECONDS_PER_MINUTE,
);

// Shift the Tasks based on working hours
const { shiftedTasks } = shiftTasks([...existingTasks, newTask], startTime, endTime);

return {
newTaskOrder: [...existingTasks, newTask],
newTaskOrder: shiftedTasks,
updatedTask: newTask,
};
};
Expand Down
47 changes: 47 additions & 0 deletions doto-frontend/src/components/pages/Calendar/TaskShifter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/**
* shift tasks within working hours
*
* NOTE: This function assumes the duration of tasks is not exceeding the working hours
*
* @param {Array<object>} scheduledTasks The scheduled tasks to be shifted, without considering working hours
* @param {Date} startTime the start working hour
* @param {Date} endTime the end working hour
* @returns An array of tasks and their startDate and endDate are modified based on working hours.
*/
const shiftTasks = (scheduledTasks, startTime, endTime) => {
// TODO: Take into account the active hours the user specifies in the settings menu
const tasks = scheduledTasks;
const MILLISECONDS_PER_MINUTE = 60000;

// transforming startTime and endTime to minitues format
const startActingHour = startTime.getHours() * 60 + startTime.getMinutes();
const endActingHour = endTime.getHours() * 60 + endTime.getMinutes();

for (let i = 0; i < tasks.length; i++) {
const taskStart = tasks[i].startDate.getHours() * 60 + tasks[i].startDate.getMinutes();
const taskEnd = tasks[i].endDate.getHours() * 60 + tasks[i].endDate.getMinutes();
const shiftToTomorrow = startActingHour + 1440 - taskStart;

// if the start time of task is earlier than start working time, then shift it and all tasks after it based on startActingHour - taskStart
// if the end time of task is later than end working time, then shift it and all tasks after it based on startActingHour + 1440 - taskStart
if (taskStart < startActingHour) {
for (let j = i; j < tasks.length; j++) {
tasks[j].startDate = new Date(
tasks[j].startDate.getTime() + (startActingHour - taskStart) * MILLISECONDS_PER_MINUTE,
);
tasks[j].endDate = new Date(
tasks[j].endDate.getTime() + (startActingHour - taskStart) * MILLISECONDS_PER_MINUTE,
);
}
} else if (taskEnd > endActingHour) {
for (let j = i; j < tasks.length; j++) {
tasks[j].startDate = new Date(tasks[j].startDate.getTime() + shiftToTomorrow * MILLISECONDS_PER_MINUTE);
tasks[j].endDate = new Date(tasks[j].endDate.getTime() + shiftToTomorrow * MILLISECONDS_PER_MINUTE);
}
}
}
return {
shiftedTasks: tasks,
};
};
export { shiftTasks };
66 changes: 64 additions & 2 deletions doto-frontend/src/components/pages/Settings/SettingsPage.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@ import { ActiveHoursContext } from "../../../context/ActiveHoursContext";
import { Themes } from "../../../constants/Themes";
import "./SettingsPage.css";
import "../Pages.css";
import Dialog from "@material-ui/core/Dialog";
import DialogActions from "@material-ui/core/DialogActions";
import DialogContent from "@material-ui/core/DialogContent";
import DialogContentText from "@material-ui/core/DialogContentText";
import DialogTitle from "@material-ui/core/DialogTitle";
import { shiftTasks } from "../Calendar/TaskShifter";

const classnames = require("classnames");

Expand Down Expand Up @@ -62,6 +68,21 @@ const ProfilePhoto = props => {

// TODO: Implement logic for working hours in sync with task-scheduling algorithm
const WorkingHoursPicker = props => {
const [dialog, setDialog] = useState(false);

const handleClickOpen = () => {
setDialog(true);
};

const handleClose = () => {
setDialog(false);
};

const handleCloseAndSave = () => {
setDialog(false);
props.saveChanges(props.startTime, props.endTime);
};

const handleStartTimeChange = date => {
props.changeStartTime(date);
};
Expand Down Expand Up @@ -100,6 +121,32 @@ const WorkingHoursPicker = props => {
/>
</MuiPickersUtilsProvider>
</div>
<div style={{ marginLeft: "3vw", marginTop: "3vh", textAlign: "left" }}>
<Button variant="contained" color="secondary" onClick={handleClickOpen}>
Save
</Button>
<Dialog
open={dialog}
onClose={handleClose}
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
>
<DialogTitle id="alert-dialog-title">{"Want to save those changes?"}</DialogTitle>
<DialogContent>
<DialogContentText id="alert-dialog-description">
Your time-table will be re-managed automatically. Please check again.
</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={handleClose} color="primary">
Cancle
</Button>
<Button onClick={handleCloseAndSave} color="primary" autoFocus>
Ok
</Button>
</DialogActions>
</Dialog>
</div>
</Grid>
);
};
Expand Down Expand Up @@ -135,8 +182,13 @@ const SettingsPage = () => {
const [email, setEmail] = useState();
const [startTime, setStartTime] = activeHoursStart;
const [endTime, setEndTime] = activeHoursEnd;
const [tasks, setTasks] = useState([]);

useEffect(() => {
const fetchTasks = async () => {
const tasks = await DotoService.getTasks();
setTasks(tasks);
};
const fetchUserInfo = async () => {
const userInfo = await DotoService.getUserInfo();
setTheme(userInfo.themePreference);
Expand All @@ -147,18 +199,27 @@ const SettingsPage = () => {
setEndTime(userInfo.endTime);
};
fetchUserInfo();
fetchTasks();
}, [setTheme, setStartTime, setEndTime]);

const changeTheme = newTheme => {
DotoService.updateUserInfo(newTheme, startTime, endTime).then(setTheme(newTheme));
};

const changeStartTime = newTime => {
DotoService.updateUserInfo(theme, newTime, endTime).then(setStartTime(newTime));
setStartTime(newTime);
};

const changeEndTime = newTime => {
DotoService.updateUserInfo(theme, startTime, newTime).then(setEndTime(newTime));
setEndTime(newTime);
};

const saveChanges = (newStartTime, newEndTime) => {
DotoService.updateUserInfo(theme, newStartTime, newEndTime);
const { shiftedTasks } = shiftTasks(tasks, new Date(newStartTime), new Date(newEndTime));
for (let i = 0; i < shiftedTasks.length; i++) {
DotoService.updateTask(shiftedTasks[i]);
}
};

return (
Expand Down Expand Up @@ -187,6 +248,7 @@ const SettingsPage = () => {
endTime={endTime}
changeStartTime={changeStartTime}
changeEndTime={changeEndTime}
saveChanges={saveChanges}
/>
</div>
</span>
Expand Down