Skip to content

Commit

Permalink
MVP
Browse files Browse the repository at this point in the history
  • Loading branch information
Qwebeck authored and Bohdan Forostianyi committed Oct 16, 2020
2 parents 561cd53 + 6ad8a27 commit 59ef3a6
Show file tree
Hide file tree
Showing 83 changed files with 3,428 additions and 1,515 deletions.
3 changes: 3 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"editor.formatOnSave": true
}
34 changes: 33 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,33 @@
## Nurse scheduling problem
# Nurse Scheduling Problem

The algorithm implementation is a part of solution created for [Fundacja Rodzin Adopcyjnych](https://adopcja.org.pl), the adoption foundation in Warsaw (Poland) during Project Summer [AILab](http://www.ailab.agh.edu.pl) & [Glider](http://www.glider.agh.edu.pl) 2020 event. The aim of the system is to improve the operation of the foundation by easily and quickly creating work schedules for its employees and volunteers. So far, this has been done manually in spreadsheets, which is a cumbersome and tedious job.

The solution presented here is problem-specific. It assumes a specific form of input and output schedules, which was adopted in the foundation for which the system is created. The schedules themselves are adjusted based on the rules of the Polish Labour Code.

The system consists of three components which are on two GitHub repositories:

- web application which allows the user to load a schedule, modify it, set basic requirements, have it sent to solver service to be adjusted for another month and display discrepancies between shifts as seen in schedule and the rules of the Polish Labour Code
- solver written in Julia which adjusts schedules and provides information about points where the schedules do not adhere to the Polish Labour Code, if they arise (detailed information [here](https://github.com/Project-Summer-AI-Lab-Glider/nurse-scheduling-problem-solver))
- backend also written in Julia ([Genie framework](https://genieframework.com/)) which allows for communication of both aforementioned components (detailed information [here](https://github.com/Project-Summer-AI-Lab-Glider/nurse-scheduling-problem-solver))

This repository contains the frontend web application, and also a mock backend server created for testing purposes.

## Running an app

```
git clone https://github.com/Project-Summer-AI-Lab-Glider/nurse-scheduling-problem-frontend.git nurse-scheduling
cd nurse-scheduling/frontend
npm install
npm start
```
## Running Mock Backend Server
Have python, pip and venv installed, cd into backend/server, then

```
python3 -m venv venv
source venv/bin/activate
pip install -r requirements.txt
source venv/bin/activate
export FLASK_APP=example_server.py
python -m flask run
```
6 changes: 5 additions & 1 deletion backend/server/example_server.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from flask import Flask, jsonify
from flask import Flask, jsonify, request
from flask_cors import CORS

app = Flask(__name__)
Expand All @@ -10,6 +10,10 @@ def hello():
return 'hello'


@app.route('/fix_schedule/', methods=['POST'])
def fix_schedule():
return request.data

@app.route('/schedule_errors/', methods=['POST'])
def hello_world():
return jsonify([
Expand Down
Binary file added docs/example.xlsx
Binary file not shown.
Binary file removed docs/example_schedule.xlsx
Binary file not shown.
3 changes: 0 additions & 3 deletions frontend/README.md

This file was deleted.

1,676 changes: 1,164 additions & 512 deletions frontend/package-lock.json

Large diffs are not rendered by default.

4 changes: 3 additions & 1 deletion frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,13 @@
"@testing-library/user-event": "^7.2.1",
"axios": "^0.20.0",
"date-fns": "^2.15.0",
"exceljs": "^4.1.1",
"file-saver": "^2.0.2",
"react": "^16.13.1",
"react-datasheet": "^1.4.8",
"react-dom": "^16.13.1",
"react-redux": "^7.2.1",
"react-scripts": "3.4.1",
"react-scripts": "^3.4.3",
"redux": "^4.0.5",
"typescript": "^3.9.7",
"xlsx": "^0.16.6"
Expand Down
49 changes: 5 additions & 44 deletions frontend/src/api/backend.ts
Original file line number Diff line number Diff line change
@@ -1,61 +1,22 @@
import axios, { AxiosInstance } from "axios";
import { ScheduleDataModel } from "../state/models/schedule-data/schedule-data.model";
import { ScheduleErrorModel } from "../state/models/schedule-data/schedule-error.model";
import { ScheduleErrorMessageModel } from "../state/models/schedule-data/schedule-error-message.model";

class Backend {
axios: AxiosInstance;

constructor() {
this.axios = axios.create({
baseURL: "http://127.0.0.1:5000/",
baseURL: "http://127.0.0.1:8000/",
});
}

mapErrorResponseToErrorMessage(error: ScheduleErrorModel): ScheduleErrorMessageModel {
const dayTimeTranslations = {
MORNING: "porannej",
AFTERNOON: "popołudniowej",
NIGHT: "nocnej",
};

const code = error.code;

let message = "";

switch (code) {
case "AON":
message = `Brak pielęgniarek w dniu ${error.day} na zmianie ${
error.day_time ? dayTimeTranslations[error.day_time] : ""
}`;
break;
case "WND":
message = `Za mało pracowników w trakcie dnia w dniu ${error.day}, potrzeba ${error.required}, jest ${error.actual}`;
break;
case "WNN":
message = `Za mało pracowników w nocy w dniu ${error.day}, potrzeba ${error.required}, jest ${error.actual}`;
break;
case "DSS":
message = `Niedozwolona sekwencja zmian dla pracownika ${error.worker} w dniu ${error.day}: ${error.succeeding} po ${error.preceding}`;
break;
case "LLB":
message = `Brak wymaganej długiej przerwy dla pracownika ${error.worker} w tygodniu ${error.week}`;
break;
case "WUH":
message = `Pracownik ${error.worker} ma ${error.hours} niedogodzin`;
break;
case "WOH":
message = `Pracownik ${error.worker} ma ${error.hours} nadgodzin`;
break;
}

return { code, message, worker: error.worker, day: error.day, week: error.week };
getErrors(schedule: ScheduleDataModel): Promise<ScheduleErrorModel[]> {
return this.axios.post("/schedule_errors", schedule).then((resp) => resp.data);
}

getErrors(schedule: ScheduleDataModel): Promise<ScheduleErrorMessageModel[]> {
return this.axios
.post("/schedule_errors/", schedule)
.then((resp) => resp.data.map(this.mapErrorResponseToErrorMessage));
fixSchedule(schedule: ScheduleDataModel): Promise<ScheduleDataModel[]> {
return this.axios.post("/fix_schedule", schedule).then((resp) => resp.data);
}
}

Expand Down
2 changes: 0 additions & 2 deletions frontend/src/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import React from "react";
import "./app.css";
import { TableComponent } from "./components/table/table.component";
import { ToolbarComponent } from "./components/toolbar/toolbar.component";
import { ValidationWindowComponent } from "./components/validation-window/validation-window.component";
function App() {
return (
<React.Fragment>
Expand All @@ -11,7 +10,6 @@ function App() {
</div>
<div className="cols-3-to-1">
<TableComponent />
<ValidationWindowComponent />
</div>
</React.Fragment>
);
Expand Down
10 changes: 5 additions & 5 deletions frontend/src/components/table/legend/legend-component.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import React from "react";

function LegendComponent () {
// in future here will be added some more logic:
// for example editing of colors for shifts
return <React.Fragment></React.Fragment>
function LegendComponent() {
// in future here will be added some more logic:
// for example editing of colors for shifts
return <React.Fragment></React.Fragment>;
}
export default LegendComponent;
export default LegendComponent;
17 changes: 17 additions & 0 deletions frontend/src/components/table/modal/add-worker-modal.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
.add-worker-button {
left: 50%;
transform: translate(-50%);
width: 50%;
}

.worker-modal {
position: absolute;
width: 400px;
background-color: white;
border: 2px solid #000;
padding: 20px;
left: 50%;
margin-left: -200px;
top: 50%;
margin-top: -200px;
}
146 changes: 146 additions & 0 deletions frontend/src/components/table/modal/add-worker-modal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
import React, { useEffect, useState } from "react";
import { Modal, TextField } from "@material-ui/core";
import "./add-worker-modal.css";
import {
WorkerType,
WorkerTypeHelper,
} from "../../../state/models/schedule-data/employee-info.model";
import Button from "@material-ui/core/Button";

const initialState = {
name: "",
nameError: false,
time: "",
timeError: false,
actionName: "Dodaj nowego pracownika do sekcji",
isNewWorker: false,
};

export interface WorkerInfo {
name?: string;
time?: number;
}

interface AddWorkerModalOptions {
isOpened: boolean;
setIsOpened: (status: boolean) => void;
submit: (workerInfo: WorkerInfo) => void;
workerType: WorkerType;
workerInfo?: WorkerInfo;
}
const NAME_MIN_LENGTH = 5;

export function AddWorkerModal({
isOpened,
setIsOpened,
submit,
workerType,
workerInfo,
}: AddWorkerModalOptions) {
const [{ name, nameError, time, timeError, actionName, isNewWorker }, setState] = useState(
initialState
);

useEffect(() => {
const { name = "", time = 0 } = workerInfo || {};
const isNewWorker = name.length === 0;
const actionName = isNewWorker
? `Dodaj nowego pracownika do sekcji ${WorkerTypeHelper.translate(workerType, true)}`
: `Edytuj pracownika: ${name}`;
setState((prev) => ({
...prev,
name,
time: time + "",
actionName,
isNewWorker,
}));
}, [workerInfo, workerType]);

const clearState = () => {
setState({ ...initialState });
};

const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name: controlName, value } = e.target;
setState((prevState) => ({ ...prevState, [controlName]: value }));
};

const parseTimeIfPossible = (time) => {
if (new RegExp("([0].[0-9])|(1.0)|(1)").test(time)) {
return { isTimeFormatValid: true, parsedTime: Number(time) };
}
if (new RegExp("[1-9]/[0-9]").test(time)) {
const timerArray = time.split("/");
if (timerArray[0] <= timerArray[1]) {
return { isTimeFormatValid: true, parsedTime: Number(timerArray[0] / timerArray[1]) };
}
}
return { isTimeFormatValid: false };
};

const validateName = (name) => {
return name.length >= NAME_MIN_LENGTH;
};

const handleSubmit = () => {
const { isTimeFormatValid, parsedTime } = parseTimeIfPossible(time);
const isNameValid = validateName(name);

if (isTimeFormatValid) {
setState((prevState) => ({ ...prevState, timeError: false }));
if (isNameValid) {
submit({ name, time: parsedTime });
handleClose();
} else {
setState((prevState) => ({ ...prevState, nameError: true }));
}
} else {
setState((prevState) => ({ ...prevState, timeError: true }));
}
};

const handleClose = () => {
clearState();
setIsOpened(false);
};

const body = (
<div className="worker-modal">
<h2 id="modal-title">{actionName}</h2>
<form>
<TextField
id="name-input"
label="Imię i nazwisko"
value={name}
name="name"
inputProps={{
readOnly: !isNewWorker,
}}
onChange={onChange}
required
error={nameError}
helperText={`Musi mieć co najmniej ${NAME_MIN_LENGTH} znaków`}
/>
<TextField
id="time-input"
label="Etat"
value={time}
name={"time"}
onChange={onChange}
required
helperText={"Obsługiwane formaty to: dziesiętny np. 0.1 i ułamkowy np. 3/5"}
error={timeError}
/>
<Button onClick={handleSubmit} className="add-worker-button" variant="outlined">
Dodaj
</Button>
</form>
</div>
);

return (
<Modal open={isOpened} onClose={handleClose}>
{body}
</Modal>
);
}
Loading

0 comments on commit 59ef3a6

Please sign in to comment.