diff --git a/.gitignore b/.gitignore index 77f6ec89..30ffd2e3 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ node_modules/ .DS_Store *.lock backend/build/ -frontend/build/ \ No newline at end of file +frontend/build/ +**/.env \ No newline at end of file diff --git a/backend/.env b/backend/.env deleted file mode 100644 index a3b9b34d..00000000 --- a/backend/.env +++ /dev/null @@ -1,7 +0,0 @@ -TOKEN_KEY=hack4impactmcgillmada -ADMIN_USERNAME=admin -ADMIN_PASSWORD=pw -ADMIN_EMAIL=admin@example.com -JWT_PRIVATE_KEY=super_secret_key -TEST_VOLUNTEER_PASSWORD=pw -TEST_VOLUNTEER_EMAIL=volunteer@example.com \ No newline at end of file diff --git a/backend/.prettierrc b/backend/.prettierrc index aa458dd5..670fbffd 100644 --- a/backend/.prettierrc +++ b/backend/.prettierrc @@ -1,4 +1,5 @@ { + "tabWidth": 4, "semi": true, "trailingComma": "none", "singleQuote": true, diff --git a/backend/example.env b/backend/example.env new file mode 100644 index 00000000..1e355049 --- /dev/null +++ b/backend/example.env @@ -0,0 +1,10 @@ +TOKEN_KEY= +JWT_PRIVATE_KEY= + +# SEED OPTIONS +SEED_DEFAULT_ADMIN_EMAIL=admin@example.com +SEED_DEFAULT_ADMIN_PASSWORD=pw +SEED_DEFAULT_VOLUNTEER_EMAIL=volunteer@example.com +SEED_DEFAULT_VOLUNTEER_PASSWORD=pw +SEED_NUM_CLIENTS=10 +SEED_NUM_VOLUNTEERS=10 \ No newline at end of file diff --git a/backend/src/ExampleEntity.ts b/backend/src/ExampleEntity.ts index 96f177cb..c3ce9ad7 100644 --- a/backend/src/ExampleEntity.ts +++ b/backend/src/ExampleEntity.ts @@ -4,12 +4,12 @@ import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm'; // Entity is a class that maps to a database table : https://orkhan.gitbook.io/typeorm/docs/entities @Entity() export class ExampleEntity { - @PrimaryGeneratedColumn() - id: number; + @PrimaryGeneratedColumn() + id: number; - @Column() - stringColumn: string; + @Column() + stringColumn: string; - @Column() - boolColumn: boolean; + @Column() + boolColumn: boolean; } diff --git a/backend/src/app.ts b/backend/src/app.ts index f1d27a10..7648c726 100644 --- a/backend/src/app.ts +++ b/backend/src/app.ts @@ -14,7 +14,12 @@ const app = express(); // Using third party middleware app.use(bodyParser.json()); // body-parser is which allows express to read the body and then parse that into a Json object that we can understand. -app.use(cors()); // +app.use( + cors({ + credentials: true, + origin: ['http://localhost:5173'] + }) +); // Routes go here // Creating route : https://expressjs.com/en/guide/routing.html diff --git a/backend/src/controllers/admin.ts b/backend/src/controllers/admin.ts index b0558b05..67d7f812 100644 --- a/backend/src/controllers/admin.ts +++ b/backend/src/controllers/admin.ts @@ -1,42 +1,34 @@ import { Request, Response } from 'express'; -import { AppDataSource } from '../data-source'; -import { AdminEntity } from '../entities/AdminEntity'; import { StatusCode } from './statusCode'; -import * as bcrypt from 'bcryptjs'; -import * as jwt from 'jsonwebtoken'; - -require('dotenv').config(); -const TOKEN_KEY = process.env.TOKEN_KEY; +import AdminService from '../services/admin'; export default class AdminController { - private AdminRepository = AppDataSource.getRepository(AdminEntity); - - login = async (request: Request, response: Response) => { - console.log(request); - const { email, password } = request.body; - if (!(email && password)) { - console.log('missing params'); - } + static login = async (request: Request, response: Response) => { + const { email, password } = request.body; - const adminUser = await this.AdminRepository.findOne({ where: { email } }); + if (!(email && password)) { + response + .status(StatusCode.BAD_REQUEST) + .json({ message: 'No email or password' }); + } - // User not found - if (!adminUser) - return response - .status(StatusCode.NOT_FOUND) - .json({ message: 'User not found' }); + try { + await AdminService.getAdminByEmail(email) + } catch { + response + .status(StatusCode.NOT_FOUND) + .json({ message: 'User not found' }); + } - if (await bcrypt.compare(password, adminUser.password)) { - const token = jwt.sign({ email: email }, TOKEN_KEY, { - expiresIn: '2h' - }); - // Login successful - return response.status(StatusCode.OK).json({ token: token }); - } else { - // Wrong password - return response - .status(StatusCode.UNAUTHORIZED) - .json({ message: 'Invalid Credentials' }); - } - }; + try { + const token = await AdminService.login(email, password) + response + .status(StatusCode.OK) + .json({ token: token}); + } catch { + response + .status(StatusCode.UNAUTHORIZED) + .json({ message: 'Invalid credentials' }); + } + }; } diff --git a/backend/src/controllers/authentication.ts b/backend/src/controllers/authentication.ts deleted file mode 100644 index ed90eb4d..00000000 --- a/backend/src/controllers/authentication.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { NextFunction, Request, Response } from 'express'; -import * as bcrypt from 'bcrypt'; -import * as jwt from 'jsonwebtoken'; -import { Repository } from 'typeorm'; -import { AppDataSource } from '../data-source'; -import { VolunteerEntity } from '../entities/VolunteerEntity'; - -export default class authenticationController { - login = async (req: Request, res: Response) => { - const { email, password }: { email: string; password: string } = req.body; - const repository = AppDataSource.getRepository(VolunteerEntity); - - const volunteer: VolunteerEntity = await repository.findOne({ - where: { email: email } - }); - - if (volunteer && (await bcrypt.compare(password, volunteer.password))) { - const token = jwt.sign( - volunteer.id.toString(), - process.env.JWT_PRIVATE_KEY - ); - return res.status(200).json({ token: token, user: volunteer }); - } - return res.status(400).json({ error: 'bad login informations' }); - }; -} diff --git a/backend/src/controllers/clients.ts b/backend/src/controllers/clients.ts index 0b3c83f6..9e249597 100644 --- a/backend/src/controllers/clients.ts +++ b/backend/src/controllers/clients.ts @@ -1,78 +1,40 @@ import { Request, Response } from 'express'; -import { AppDataSource } from '../data-source'; -import { ClientEntity } from '../entities/ClientEntity'; -// import { TaskEntity } from '../entities/TaskEntity'; -import { RouteDeliveryEntity } from '../entities/RouteDeliveryEntity'; import { StatusCode } from './statusCode'; -import { ProgramType, MealType } from '../entities/types'; +import ClientService from '../services/clients'; +import { Client, CreateClientProps, UpdateClientProps } from '../types/clients'; export default class ClientController { - private ClientRepository = AppDataSource.getRepository(ClientEntity); - private RouteDeliveryRepository = - AppDataSource.getRepository(RouteDeliveryEntity); - - getClients = async (request: Request, response: Response) => { - const clients = await this.ClientRepository.find(); - response.status(StatusCode.OK).json({ clients: clients }); - }; - - createClient = async (request: Request, response: Response) => { - const client = await this.ClientRepository.create({ - name: request.body.name, - email: request.body.email, - phoneNumber: request.body.phoneNumber, - address: request.body.address, - mealType: request.body.mealType, - sts: request.body.sts, - map: request.body.map - }); - const savedClient = await this.ClientRepository.save(client); - - if (client.sts) { - const stsRouteDelivery = new RouteDeliveryEntity(); - stsRouteDelivery.client = savedClient; - stsRouteDelivery.routeNumber = 0; - stsRouteDelivery.routePosition = 0; - stsRouteDelivery.mealType = savedClient.mealType; - stsRouteDelivery.program = ProgramType.STS; - - await this.RouteDeliveryRepository.save(stsRouteDelivery); - } - - if (client.map) { - const mapRouteDelivery = new RouteDeliveryEntity(); - mapRouteDelivery.client = savedClient; - mapRouteDelivery.routeNumber = 0; - mapRouteDelivery.routePosition = 0; - mapRouteDelivery.mealType = savedClient.mealType; - mapRouteDelivery.program = ProgramType.MAP; - - await this.RouteDeliveryRepository.save(mapRouteDelivery); - } - - response.status(StatusCode.OK).json({ client }); - }; - - getClient = async (request: Request, response: Response) => { - const client = await this.ClientRepository.findOne({ - where: { id: parseInt(request.params.id) } - }); - response.status(StatusCode.OK).json({ client: client }); - }; - - editClient = async (request: Request, response: Response) => { - const client = await this.ClientRepository.update( - { id: parseInt(request.params.id) }, - { - name: request.body.name, - email: request.body.email, - phoneNumber: request.body.phoneNumber, - address: request.body.address, - mealType: request.body.mealType, - sts: request.body.sts, - map: request.body.map - } - ); - response.status(StatusCode.OK).json({ client }); - }; + static getClients = async (request: Request, response: Response) => { + const clients = await ClientService.getAllClients(); + response.status(StatusCode.OK).json({ clients: clients }); + }; + + static createClient = async (request: Request, response: Response) => { + const props: CreateClientProps = request.body; + const client = await ClientService.createClient(props); + response.status(StatusCode.OK).json({ client: client }); + }; + + static getClient = async (request: Request, response: Response) => { + const id = parseInt(request.params.id); + const client = await ClientService.getClient(id); + + if (client == null) { + response.status(StatusCode.NOT_FOUND); + } else { + response.status(StatusCode.OK).json({ client: client }); + } + }; + + static updateClient = async (request: Request, response: Response) => { + const id = parseInt(request.params.id); + const props: UpdateClientProps = request.body; + const client = await ClientService.updateClient(id, props); + + if (client == null) { + response.status(StatusCode.NOT_FOUND); + } else { + response.status(StatusCode.OK).json({ client: client }); + } + }; } diff --git a/backend/src/controllers/mealDelivery.ts b/backend/src/controllers/mealDelivery.ts deleted file mode 100644 index ec8bce7b..00000000 --- a/backend/src/controllers/mealDelivery.ts +++ /dev/null @@ -1,101 +0,0 @@ -import { Request, Response } from 'express'; -import { AppDataSource } from '../data-source'; -import { MealDeliveryEntity } from '../entities/MealDeliveryEntity'; -import { TaskEntity } from '../entities/TaskEntity'; -import { StatusCode } from './statusCode'; -import { ClientEntity } from '../entities/ClientEntity'; - -export default class MealDeliveryController { - private MealDeliveryRepository = - AppDataSource.getRepository(MealDeliveryEntity); - private TaskRepository = AppDataSource.getRepository(TaskEntity); - private ClientRepository = AppDataSource.getRepository(ClientEntity); - - getMealDeliveries = async (request: Request, response: Response) => { - const meals = await this.MealDeliveryRepository.find({}); - response.status(StatusCode.OK).json({ meals: meals }); - }; - - getMealDelivery = async (request: Request, response: Response) => { - const mealDelivery = await this.MealDeliveryRepository.findOne({ - where: { - id: parseInt(request.params.id) - }, - relations: { - task: true, - client: true - } - }); - mealDelivery - ? response.status(StatusCode.OK).json({ mealDelivery: mealDelivery }) - : response.status(StatusCode.BAD_REQUEST).json({ mealDelivery: null }); - }; - - updateOrCreateMealDelivery = async (request: Request, response: Response) => { - if (!request.params.id) { - const newMealDelivery = new MealDeliveryEntity(); - newMealDelivery.mealType = request.body.mealType; - newMealDelivery.isCompleted = request.body.isCompleted; - newMealDelivery.routePosition = request.body.routePosition; - newMealDelivery.program = request.body.program; - newMealDelivery.task = request.body.task - ? await this.TaskRepository.findOneBy({ - id: request.body.task.id - }) - : null; - newMealDelivery.client = request.body.client - ? await this.ClientRepository.findOneBy({ - id: request.body.client.id - }) - : null; - - await this.MealDeliveryRepository.save(newMealDelivery); - const mealDelivery = await this.MealDeliveryRepository.findOne({ - where: { - id: newMealDelivery.id - }, - relations: { - task: true, - client: true - } - }); - response.status(StatusCode.OK).json({ mealDelivery: mealDelivery }); - } else { - await this.MealDeliveryRepository.save({ - // edited to work with newly adopted entities - id: parseInt(request.params.id), - isCompleted: request.body.isCompleted, - routePosition: request.body.routePosition, - mealType: request.body.mealType, - program: request.body.program, - task: request.body.task - ? await this.TaskRepository.findOneBy({ - id: parseInt(request.body.task.id) - }) - : null, - client: request.body.client - ? await this.ClientRepository.findOneBy({ - id: parseInt(request.body.client.id) - }) - : null - }); - const mealDelivery = await this.MealDeliveryRepository.findOne({ - where: { - id: parseInt(request.params.id) - }, - relations: { - task: true, - client: true - } - }); - response.status(StatusCode.OK).json({ mealDelivery: mealDelivery }); - } - }; - - deleteMealDelivery = async (request: Request, response: Response) => { - const mealDeliveryDeleted = await this.MealDeliveryRepository.delete({ - id: parseInt(request.params.id) - }); - response.status(StatusCode.OK).json({}); - }; -} diff --git a/backend/src/controllers/routeDelivery.ts b/backend/src/controllers/routeDelivery.ts deleted file mode 100644 index 3f89b795..00000000 --- a/backend/src/controllers/routeDelivery.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { Request, Response } from 'express'; -import { AppDataSource } from '../data-source'; -import { RouteDeliveryEntity } from '../entities/RouteDeliveryEntity'; -import { TaskEntity } from '../entities/TaskEntity'; -import { StatusCode } from './statusCode'; - -export default class RouteDeliveryController { - private RouteDeliveryRepository = - AppDataSource.getRepository(RouteDeliveryEntity); - - getRouteDeliveries = async (request: Request, response: Response) => { - const routes = await this.RouteDeliveryRepository.find({ - relations: { - client: true - } - }); - - // Create groups of routes by routeNumber - const groups = routes.reduce((groups, route) => { - const key = route.routeNumber; - if (!groups[key]) { - groups[key] = []; - } - groups[key].push(route); - return groups; - }, {}); - - // Sort groups by routePosition - for (const routeNumber in groups) { - groups[routeNumber].sort( - (routeA, routeB) => routeA.routePosition - routeB.routePosition - ); - } - - response.status(StatusCode.OK).json({ routes: groups }); - }; - - setRouteNumber = async (request: Request, response: Response) => { - const route = await this.RouteDeliveryRepository.update( - { id: parseInt(request.params.id) }, - { routeNumber: request.body.routeNumber } - ); - - response.status(StatusCode.OK).json({ route }); - }; - - incrementRoutePosition = async (request: Request, response: Response) => { - const route = await this.RouteDeliveryRepository.increment( - { id: parseInt(request.params.id) }, - 'routePosition', - 1 - ); - response.status(StatusCode.OK).json({ route }); - }; - - decrementRoutePosition = async (request: Request, response: Response) => { - const route = await this.RouteDeliveryRepository.decrement( - { id: parseInt(request.params.id) }, - 'routePosition', - 1 - ); - response.status(StatusCode.OK).json({ route }); - }; -} diff --git a/backend/src/controllers/statusCode.ts b/backend/src/controllers/statusCode.ts index 015b4d8e..8e5a176b 100644 --- a/backend/src/controllers/statusCode.ts +++ b/backend/src/controllers/statusCode.ts @@ -1,6 +1,6 @@ export enum StatusCode { - OK = 200, - BAD_REQUEST = 400, - UNAUTHORIZED = 401, - NOT_FOUND = 404 + OK = 200, + BAD_REQUEST = 400, + UNAUTHORIZED = 401, + NOT_FOUND = 404 } diff --git a/backend/src/controllers/stops.ts b/backend/src/controllers/stops.ts new file mode 100644 index 00000000..af81938a --- /dev/null +++ b/backend/src/controllers/stops.ts @@ -0,0 +1,20 @@ +import { Request, Response } from 'express'; +import { StatusCode } from './statusCode'; +import StopService from '../services/stop'; +import { SaveRouteProps } from '../types/stop'; + +export default class StopController { + static getRoutes = async (request: Request, response: Response) => { + const routes = await StopService.getRoutes() + response.status(StatusCode.OK).json({ routes: routes }); + } + + static saveRoutes = async (request: Request, response: Response) => { + const props: SaveRouteProps = request.body; + const routes = await StopService.saveRoutes(props) + if (routes == null) { + response.status(StatusCode.BAD_REQUEST); + } + response.status(StatusCode.OK).json({ routes: routes }); + } +} diff --git a/backend/src/controllers/tasks.ts b/backend/src/controllers/tasks.ts index 5407aa7c..fd60cb15 100644 --- a/backend/src/controllers/tasks.ts +++ b/backend/src/controllers/tasks.ts @@ -1,159 +1,17 @@ import { Request, Response } from 'express'; import { AppDataSource } from '../data-source'; -import { MealDeliveryEntity } from '../entities/MealDeliveryEntity'; -import { TaskEntity } from '../entities/TaskEntity'; -import { ClientEntity } from '../entities/ClientEntity'; -import { VolunteerEntity } from '../entities/VolunteerEntity'; import { StatusCode } from './statusCode'; -import { RouteDeliveryEntity } from '../entities/RouteDeliveryEntity'; +import TaskService from '../services/task'; export default class TaskController { - private TaskRepository = AppDataSource.getRepository(TaskEntity); - private MealDeliveryRepository = - AppDataSource.getRepository(MealDeliveryEntity); - private ClientRepository = AppDataSource.getRepository(ClientEntity); - private VolunteerRepository = AppDataSource.getRepository(VolunteerEntity); - private RouteDeliveryRepository = - AppDataSource.getRepository(RouteDeliveryEntity); - - getTasksByVolunteer = async (request: Request, response: Response) => { - const volunteer = await this.VolunteerRepository.findOne({ - where: { - id: parseInt(request.params.id) - }, - relations: { - tasks: { - deliveries: { - client: true - } - } - } - }); - - volunteer - ? response.status(StatusCode.OK).json({ tasks: volunteer.tasks }) - : response.status(StatusCode.BAD_REQUEST).json({ tasks: null }); - }; - - getTask = async (request: Request, response: Response) => { - const task = await this.TaskRepository.findOne({ - where: { - id: parseInt(request.params.id) - }, - relations: { - deliveries: { - client: true - }, - volunteer: true - } - }); - task - ? response.status(StatusCode.OK).json({ task: task }) - : response.status(StatusCode.BAD_REQUEST).json({ task: null }); - }; - - getTasks = async (request: Request, response: Response) => { - const task = await this.TaskRepository.find({ - relations: { - deliveries: { - client: true - }, - volunteer: true - } - }); - response.status(StatusCode.OK).json({ tasks: task }); - }; - - createAllTasksFromRoute = async (request: Request, response: Response) => { - const routes = await this.RouteDeliveryRepository.find({}); - - // Create tasks from routeDelivery items - }; - - createTask = async (request: Request, response: Response) => { - const newTask = new TaskEntity(); - newTask.deliveries = []; - newTask.isCompleted = false; - - newTask.volunteer = await this.VolunteerRepository.findOne({ - where: { id: request.body.volunteerId } - }); - - const savedTask = await this.TaskRepository.save(newTask); - - request.body.meals?.forEach(async (meal) => { - const newMeal = new MealDeliveryEntity(); - newMeal.mealType = meal.type; - newMeal.task = savedTask; - const foundClient = await this.ClientRepository.findOne({ - where: { id: meal.clientId } - }); - newMeal.client = foundClient; - await this.MealDeliveryRepository.save(newMeal); - }); - - const task = await this.TaskRepository.findOne({ - where: { id: savedTask.id } - }); - - response.status(StatusCode.OK).json({ task: task }); - }; - - updateOrCreateTask = async (request: Request, response: Response) => { - // create - if (!request.params.id) { - const newTask = new TaskEntity(); - newTask.deliveries = []; - newTask.date = request.body.date; - newTask.isCompleted = request.body.isCompleted; - newTask.deliveries = await Promise.all( - request.body.deliveries.map(async (d) => - this.MealDeliveryRepository.findOneBy({ - id: parseInt(d.id) - }) - ) - ); - await this.TaskRepository.save(newTask); - const savedTask = await this.TaskRepository.findOne({ - where: { - id: newTask.id - }, - relations: { - deliveries: true - } - }); - - response.status(StatusCode.OK).json({ task: savedTask }); - } else { - // update - const taskToUpdate = await this.TaskRepository.findOneBy({ - id: parseInt(request.params.id) - }); - taskToUpdate.isCompleted = request.body.isCompleted; - taskToUpdate.deliveries = await Promise.all( - request.body.deliveries.map(async (d) => - this.MealDeliveryRepository.findOneBy({ - id: parseInt(d.id) - }) - ) - ); - const updatedTask = await this.TaskRepository.save(taskToUpdate); - const savedTask = await this.TaskRepository.findOne({ - where: { - id: updatedTask.id - }, - relations: { - deliveries: true - } - }); - response.status(StatusCode.OK).json({ task: savedTask }); + static getTasks = async (request: Request, response: Response) => { + const tasks = await TaskService.getTasks(); + response.status(StatusCode.OK).json({ tasks: tasks }); } - }; - deleteTask = async (request: Request, response: Response) => { - const taskDeleted = await this.TaskRepository.delete({ - id: parseInt(request.params.id) - }); - response.status(StatusCode.OK).json({}); - }; + static createTask = async (request: Request, response: Response) => { + const {volunteerId, routeNumber} = request.body + const task = await TaskService.createTask(volunteerId, routeNumber) + response.status(StatusCode.OK).json({ task: task }); + } } diff --git a/backend/src/controllers/volunteers.ts b/backend/src/controllers/volunteers.ts index 29c7c2f7..fefe16b2 100644 --- a/backend/src/controllers/volunteers.ts +++ b/backend/src/controllers/volunteers.ts @@ -1,100 +1,69 @@ import { Request, Response } from 'express'; -import { AppDataSource } from '../data-source'; -import { VolunteerEntity } from '../entities/VolunteerEntity'; -// import { TaskEntity } from '../entities/TaskEntity'; import { StatusCode } from './statusCode'; -import * as bcrypt from 'bcrypt'; -import * as jwt from 'jsonwebtoken'; +import VolunteerService from '../services/volunteer' +import {CreateVolunteerProps, UpdateVolunteerProps} from '../types/volunteer' export default class VolunteerController { - private VolunteerRepository = AppDataSource.getRepository(VolunteerEntity); + static login = async (request: Request, response: Response) => { + const { email, password } = request.body; - getVolunteers = async (request: Request, response: Response) => { - const volunteers = await this.VolunteerRepository.find({ - relations: { - tasks: true - } - }); - response.status(StatusCode.OK).json({ volunteers: volunteers }); - }; + if (!(email && password)) { + response + .status(StatusCode.BAD_REQUEST) + .json({ message: 'No email or password' }); + } - getVolunteer = async (request: Request, response: Response) => { - const volunteer = await this.VolunteerRepository.findOne({ - where: { id: parseInt(request.params.id) }, - relations: { - tasks: true - } - }); - response.status(StatusCode.OK).json({ volunteer: volunteer }); - }; + try { + await VolunteerService.getVolunteerByEmail(email) + } catch { + response + .status(StatusCode.NOT_FOUND) + .json({ message: 'User not found' }); + } - removeVolunteer = async (request: Request, response: Response) => { - await this.VolunteerRepository.delete({ - id: parseInt(request.params.id) - }); - response.status(StatusCode.OK).json({}); - }; + try { + const token = await VolunteerService.login(email, password) + response + .status(StatusCode.OK) + .json({ token: token}); + } catch { + response + .status(StatusCode.UNAUTHORIZED) + .json({ message: 'Invalid credentials' }); + } + }; - createVolunteer = async (request: Request, response: Response) => { - const volunteer = await this.VolunteerRepository.create({ - name: request.body.name, - password: request.body.password, - email: request.body.email, - phoneNumber: request.body.phoneNumber, - startDate: request.body.date, - profilePicture: '', - availabilities: request.body.availabilities - }); - await this.VolunteerRepository.save(volunteer); - response.status(StatusCode.OK).json({ volunteer }); - }; + static getVolunteers = async (request: Request, response: Response) => { + const volunteers = await VolunteerService.getAllVolunteers(); + response.status(StatusCode.OK).json({ volunteers: volunteers }); + }; - editVolunteer = async (request: Request, response: Response) => { - const volunteer = await this.VolunteerRepository.update( - { id: parseInt(request.params.id) }, - request.body - ); - response.status(StatusCode.OK).json({ volunteer }); - }; + static createVolunteer = async (request: Request, response: Response) => { + const props: CreateVolunteerProps = request.body; + const volunteer = await VolunteerService.createVolunteer(props); + response.status(StatusCode.OK).json({ volunteer: volunteer }); + }; - getVolunteerTasks = async (request: Request, response: Response) => { - const volunteer = await this.VolunteerRepository.findOne({ - where: { id: parseInt(request.params.id) }, - relations: ['tasks', 'tasks.deliveries'] - }); - volunteer == null - ? response.status(StatusCode.NOT_FOUND).json({}) - : response.status(StatusCode.OK).json({ tasks: volunteer.tasks }); - }; + static getVolunteer = async (request: Request, response: Response) => { + const id = parseInt(request.params.id); + const volunteer = await VolunteerService.getVolunteer(id); - getVolunteerAvailabilities = async (request: Request, response: Response) => { - const volunteer = await this.VolunteerRepository.findOne({ - where: { id: parseInt(request.params.id) }, - relations: ['availabilities'] - }); - volunteer == null - ? response.status(StatusCode.NOT_FOUND).json({}) - : response - .status(StatusCode.OK) - .json({ availabilities: volunteer.availabilities }); - }; + if (volunteer == null) { + response.status(StatusCode.NOT_FOUND); + } else { + response.status(StatusCode.OK).json({ volunteer: volunteer }); + } + }; - login = async (req: Request, res: Response) => { - const { email, password }: { email: string; password: string } = req.body; - const repository = AppDataSource.getRepository(VolunteerEntity); - console.log(process.env.JWT_PRIVATE_KEY); - const volunteer: VolunteerEntity = await repository.findOne({ - where: { email: email } - }); + static updateVolunteer = async (request: Request, response: Response) => { + const id = parseInt(request.params.id); + const props: UpdateVolunteerProps = request.body; + const volunteer = await VolunteerService.updateVolunteer(id, props); - if (volunteer && (await bcrypt.compare(password, volunteer.password))) { - // bad login info - const token = jwt.sign( - volunteer.id.toString(), - process.env.JWT_PRIVATE_KEY - ); - return res.status(200).json({ token: token, user: volunteer }); - } - return res.status(400).json({ error: 'bad login informations' }); - }; + if (volunteer == null) { + response.status(StatusCode.NOT_FOUND); + } else { + response.status(StatusCode.OK).json({ volunteer: volunteer }); + } + }; } diff --git a/backend/src/data-source.ts b/backend/src/data-source.ts index 85e9fa89..11ceaa1d 100644 --- a/backend/src/data-source.ts +++ b/backend/src/data-source.ts @@ -3,7 +3,7 @@ import 'reflect-metadata'; import { DataSource, DataSourceOptions } from 'typeorm'; import { SeederOptions } from 'typeorm-extension'; -import { RouteDeliveryEntity } from './entities/RouteDeliveryEntity'; +import { StopEntity } from './entities/StopEntity'; import { AdminEntity } from './entities/AdminEntity'; import { ClientEntity } from './entities/ClientEntity'; import { MealDeliveryEntity } from './entities/MealDeliveryEntity'; @@ -13,25 +13,25 @@ import { VolunteerEntity } from './entities/VolunteerEntity'; // Create a data source i.e connection settings: https://orkhan.gitbook.io/typeorm/docs/data-source#what-is-datasource export const AppDataSource = new DataSource({ - type: 'postgres', - host: 'localhost', - port: 5432, - username: 'test', - password: 'test', - database: 'test', - synchronize: true, - logging: false, - entities: [ - AdminEntity, - MealDeliveryEntity, - TaskEntity, - UserEntity, - VolunteerEntity, - ClientEntity, - RouteDeliveryEntity - ], - migrations: [], - subscribers: [], - seeds: ['src/db/*.seeder.ts'], - factories: ['src/db/*.factory.ts'] + type: 'postgres', + host: 'localhost', + port: 5432, + username: 'test', + password: 'test', + database: 'test', + synchronize: true, + logging: false, + entities: [ + AdminEntity, + MealDeliveryEntity, + TaskEntity, + UserEntity, + VolunteerEntity, + ClientEntity, + StopEntity + ], + migrations: [], + subscribers: [], + seeds: ['src/db/*.seeder.ts'], + factories: ['src/db/*.factory.ts'] } as SeederOptions & DataSourceOptions); diff --git a/backend/src/entities/AdminEntity.ts b/backend/src/entities/AdminEntity.ts index 8b64e46e..7ab4fb9a 100644 --- a/backend/src/entities/AdminEntity.ts +++ b/backend/src/entities/AdminEntity.ts @@ -4,9 +4,9 @@ import { UserEntity } from './UserEntity'; // Entity is a class that maps to a database table : https://orkhan.gitbook.io/typeorm/docs/entities @Entity() export class AdminEntity extends UserEntity { - @PrimaryGeneratedColumn() - id: number; + @PrimaryGeneratedColumn() + id: number; - @Column() - jobTitleColumn: string; + @Column() + jobTitleColumn: string; } diff --git a/backend/src/entities/ClientEntity.ts b/backend/src/entities/ClientEntity.ts index 83ad6a4c..3064394c 100644 --- a/backend/src/entities/ClientEntity.ts +++ b/backend/src/entities/ClientEntity.ts @@ -3,16 +3,34 @@ import { UserEntity } from './UserEntity'; import { MealType, Neighbourhood } from './types'; @Entity() -export class ClientEntity extends UserEntity { - @Column() - address: string; +export class ClientEntity { + @PrimaryGeneratedColumn() + id: number; - @Column() - mealType: MealType; + @Column() + name: string; - @Column() - sts: boolean; + @Column({ + unique: true, + nullable: false + }) + email: string; - @Column() - map: boolean; + @Column() + phoneNumber: string; + + @Column() + address: string; + + @Column() + mealType: MealType; + + @Column() + sts: boolean; + + @Column() + map: boolean; + + @Column() + softDelete: boolean = false; } diff --git a/backend/src/entities/MealDeliveryEntity.ts b/backend/src/entities/MealDeliveryEntity.ts index 6ab46a9d..4e1f4716 100644 --- a/backend/src/entities/MealDeliveryEntity.ts +++ b/backend/src/entities/MealDeliveryEntity.ts @@ -1,10 +1,10 @@ import { - Entity, - PrimaryGeneratedColumn, - Column, - ManyToOne, - OneToOne, - JoinColumn + Entity, + PrimaryGeneratedColumn, + Column, + ManyToOne, + OneToOne, + JoinColumn } from 'typeorm'; import { ClientEntity } from './ClientEntity'; import { TaskEntity } from './TaskEntity'; @@ -12,26 +12,26 @@ import { ProgramType, MealType } from './types'; @Entity() export class MealDeliveryEntity { - @PrimaryGeneratedColumn() - id: number; + @PrimaryGeneratedColumn() + id: number; - @Column({ default: false }) - isCompleted: boolean; + @Column({ default: false }) + isCompleted: boolean; - @Column() - routePosition: number; + @Column() + routePosition: number; - @Column() - mealType: MealType; + @Column() + mealType: MealType; - @Column() - program: ProgramType; + @Column() + program: ProgramType; - @ManyToOne(() => TaskEntity, (task) => task.deliveries, { - onDelete: 'CASCADE' - }) - task: TaskEntity; + @ManyToOne(() => TaskEntity, (task) => task.deliveries, { + onDelete: 'CASCADE' + }) + task: TaskEntity; - @ManyToOne(() => ClientEntity) - client: ClientEntity; + @ManyToOne(() => ClientEntity) + client: ClientEntity; } diff --git a/backend/src/entities/RouteDeliveryEntity.ts b/backend/src/entities/RouteDeliveryEntity.ts deleted file mode 100644 index 4606ec6c..00000000 --- a/backend/src/entities/RouteDeliveryEntity.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { Entity, PrimaryGeneratedColumn, Column, ManyToOne } from 'typeorm'; -import { ClientEntity } from './ClientEntity'; -import { ProgramType, MealType } from './types'; - -@Entity() -export class RouteDeliveryEntity { - @PrimaryGeneratedColumn() - id: number; - - @Column() - routeNumber: number; - - @Column() - routePosition: number; - - @Column() - mealType: MealType; - - @Column() - program: ProgramType; - - @ManyToOne(() => ClientEntity) - client: ClientEntity; -} diff --git a/backend/src/entities/StopEntity.ts b/backend/src/entities/StopEntity.ts new file mode 100644 index 00000000..d4bca64f --- /dev/null +++ b/backend/src/entities/StopEntity.ts @@ -0,0 +1,24 @@ +import { Entity, PrimaryGeneratedColumn, Column, ManyToOne } from 'typeorm'; +import { ClientEntity } from './ClientEntity'; +import { ProgramType, MealType } from './types'; + +@Entity() +export class StopEntity { + @PrimaryGeneratedColumn() + id: number; + + @Column() + routeNumber: number; + + @Column() + routePosition: number; + + @Column() + mealType: MealType; + + @Column() + program: ProgramType; + + @ManyToOne(() => ClientEntity) + client: ClientEntity; +} diff --git a/backend/src/entities/TaskEntity.ts b/backend/src/entities/TaskEntity.ts index 0deaf92b..f104aa40 100644 --- a/backend/src/entities/TaskEntity.ts +++ b/backend/src/entities/TaskEntity.ts @@ -1,29 +1,29 @@ import { - Column, - Entity, - ManyToOne, - OneToMany, - PrimaryGeneratedColumn + Column, + Entity, + ManyToOne, + OneToMany, + PrimaryGeneratedColumn } from 'typeorm'; import { MealDeliveryEntity } from './MealDeliveryEntity'; import { VolunteerEntity } from './VolunteerEntity'; @Entity() export class TaskEntity { - @PrimaryGeneratedColumn() - id: number; + @PrimaryGeneratedColumn() + id: number; - @Column({ nullable: true }) - date: Date; + @Column({ nullable: true }) + date: Date; - @Column({ default: false }) - isCompleted: boolean; + @Column({ default: false }) + isCompleted: boolean; - @ManyToOne(() => VolunteerEntity, (volunteer) => volunteer.tasks) - volunteer: VolunteerEntity; + @ManyToOne(() => VolunteerEntity, (volunteer) => volunteer.tasks) + volunteer: VolunteerEntity; - @OneToMany(() => MealDeliveryEntity, (delivery) => delivery.task, { - cascade: true - }) - deliveries: MealDeliveryEntity[]; + @OneToMany(() => MealDeliveryEntity, (delivery) => delivery.task, { + cascade: true + }) + deliveries: MealDeliveryEntity[]; } diff --git a/backend/src/entities/UserEntity.ts b/backend/src/entities/UserEntity.ts index ee235bb4..13ab6a7c 100644 --- a/backend/src/entities/UserEntity.ts +++ b/backend/src/entities/UserEntity.ts @@ -1,24 +1,24 @@ import { Column, OneToMany, Entity, PrimaryGeneratedColumn } from 'typeorm'; -import { RouteDeliveryEntity } from './RouteDeliveryEntity'; +import { StopEntity } from './StopEntity'; @Entity() export abstract class UserEntity { - @PrimaryGeneratedColumn() - id: number; + @PrimaryGeneratedColumn() + id: number; - // Personal info - @Column() - name: string; + // Personal info + @Column() + name: string; - @Column({ - unique: true, - nullable: false - }) - email: string; + @Column({ + unique: true, + nullable: false + }) + email: string; - @Column() - password: string; + @Column() + password: string; - @Column() - phoneNumber: string; + @Column() + phoneNumber: string; } diff --git a/backend/src/entities/VolunteerEntity.ts b/backend/src/entities/VolunteerEntity.ts index 433ee347..fa5d6d3d 100644 --- a/backend/src/entities/VolunteerEntity.ts +++ b/backend/src/entities/VolunteerEntity.ts @@ -1,85 +1,88 @@ import { - Column, - PrimaryColumn, - Entity, - OneToMany, - PrimaryGeneratedColumn + Column, + PrimaryColumn, + Entity, + OneToMany, + PrimaryGeneratedColumn } from 'typeorm'; import { TaskEntity } from './TaskEntity'; import { UserEntity } from './UserEntity'; import { Neighbourhood } from './types'; export enum DayOfWeek { - MONDAY = 'monday', - TUESDAY = 'tuesday', - WEDNESDAY = 'wednesday', - THURSDAY = 'thursday', - FRIDAY = 'friday', - SATURDAY = 'saturday', - SUNDAY = 'sunday' + MONDAY = 'monday', + TUESDAY = 'tuesday', + WEDNESDAY = 'wednesday', + THURSDAY = 'thursday', + FRIDAY = 'friday', + SATURDAY = 'saturday', + SUNDAY = 'sunday' } // Assign explicit numeric values - for seeding purposes export const indexedDayOfWeek: { [index: number]: DayOfWeek } = { - 0: DayOfWeek.MONDAY, - 1: DayOfWeek.TUESDAY, - 2: DayOfWeek.WEDNESDAY, - 3: DayOfWeek.THURSDAY, - 4: DayOfWeek.FRIDAY, - 5: DayOfWeek.SATURDAY, - 6: DayOfWeek.SUNDAY + 0: DayOfWeek.MONDAY, + 1: DayOfWeek.TUESDAY, + 2: DayOfWeek.WEDNESDAY, + 3: DayOfWeek.THURSDAY, + 4: DayOfWeek.FRIDAY, + 5: DayOfWeek.SATURDAY, + 6: DayOfWeek.SUNDAY }; export enum TimeSlots { - hour0 = '12:00PM', - hour1 = '01:00PM', - hour2 = '02:00PM', - hour3 = '03:00PM', - hour4 = '04:00PM', - hour5 = '05:00PM' + hour0 = '12:00PM', + hour1 = '01:00PM', + hour2 = '02:00PM', + hour3 = '03:00PM', + hour4 = '04:00PM', + hour5 = '05:00PM' } // Assign explicit numeric values - for seeding purposes export const indexedTimeSlots: { [index: number]: TimeSlots } = { - 0: TimeSlots.hour0, - 1: TimeSlots.hour1, - 2: TimeSlots.hour2, - 3: TimeSlots.hour3, - 4: TimeSlots.hour4, - 5: TimeSlots.hour5 + 0: TimeSlots.hour0, + 1: TimeSlots.hour1, + 2: TimeSlots.hour2, + 3: TimeSlots.hour3, + 4: TimeSlots.hour4, + 5: TimeSlots.hour5 }; export interface Availability { - day: DayOfWeek; - time: string; + day: DayOfWeek; + time: string; } export interface Availabilities { - availabilities: Availability[]; + availabilities: Availability[]; } @Entity() export class VolunteerEntity extends UserEntity { - @Column() - startDate: Date; + @Column() + startDate: Date; - @Column() - profilePicture: string; + @Column({ nullable: true }) + profilePicture: string; - @Column() - availabilities: string; + @Column({nullable: true, type: 'simple-array'}) + availabilities: string; - @Column() - availabilitiesLastUpdated: Date; + @Column({ nullable: true }) + availabilitiesLastUpdated: Date; - @OneToMany(() => TaskEntity, (task) => task.volunteer, { - cascade: true - }) - tasks: TaskEntity[]; + @OneToMany(() => TaskEntity, (task) => task.volunteer, { + cascade: true + }) + tasks: TaskEntity[]; - @Column({ nullable: true }) - token: string; + @Column({ nullable: true }) + token: string; - @Column('text', { nullable: true, array: true }) - preferredNeighbourhoods: Neighbourhood[]; + @Column('text', { nullable: true, array: true }) + preferredNeighbourhoods: Neighbourhood[]; + + @Column() + softDelete: boolean = false; } diff --git a/backend/src/entities/types.ts b/backend/src/entities/types.ts index 383da2b1..a211932d 100644 --- a/backend/src/entities/types.ts +++ b/backend/src/entities/types.ts @@ -1,25 +1,44 @@ export enum ProgramType { - MAP = 'MAP', - STS = 'STS' + MAP = 'MAP', + STS = 'STS' } export enum MealType { - VEGETARIAN = 'Vegetarian', - NOFISH = 'No Fish', - NOMEAT = 'No Meat', - REGULAR = 'Regular' + VEGETARIAN = 'Vegetarian', + NOFISH = 'No Fish', + NOMEAT = 'No Meat', + REGULAR = 'Regular' } export enum Neighbourhood { - COTEDENEIGES = 'Côte De Neiges', - COTESTLUC = 'Côte St-Luc', - DOWNTOWN = 'Downtown', - LACHINE = 'Lachine', - LAVAL = 'Laval', - MONTREAL = 'Montreal', - MONTREALWEST = 'Montreal West', - TMR = 'Town of Mount Royal', - VERDUN = 'Verdun', - VILLESTLAURENT = 'Ville St-Laurent', - WESTISLAND = 'West Island' + COTEDENEIGES = 'Côte De Neiges', + COTESTLUC = 'Côte St-Luc', + DOWNTOWN = 'Downtown', + LACHINE = 'Lachine', + LAVAL = 'Laval', + MONTREAL = 'Montreal', + MONTREALWEST = 'Montreal West', + TMR = 'Town of Mount Royal', + VERDUN = 'Verdun', + VILLESTLAURENT = 'Ville St-Laurent', + WESTISLAND = 'West Island' +} + +// Create a reverse mapping object +const neighbourhoodReverseMapping: { [key: string]: Neighbourhood } = { + [Neighbourhood.COTEDENEIGES]: Neighbourhood.COTEDENEIGES, + [Neighbourhood.COTESTLUC]: Neighbourhood.COTESTLUC, + [Neighbourhood.DOWNTOWN]: Neighbourhood.DOWNTOWN, + [Neighbourhood.LACHINE]: Neighbourhood.LACHINE, + [Neighbourhood.LAVAL]: Neighbourhood.LAVAL, + [Neighbourhood.MONTREAL]: Neighbourhood.MONTREAL, + [Neighbourhood.MONTREALWEST]: Neighbourhood.MONTREALWEST, + [Neighbourhood.TMR]: Neighbourhood.TMR, + [Neighbourhood.VERDUN]: Neighbourhood.VERDUN, + [Neighbourhood.VILLESTLAURENT]: Neighbourhood.VILLESTLAURENT, + [Neighbourhood.WESTISLAND]: Neighbourhood.WESTISLAND +}; + +export function getNeighbourhoodFromString(str: string): Neighbourhood { + return neighbourhoodReverseMapping[str]; } diff --git a/backend/src/middlewares/authentication.ts b/backend/src/middlewares/authentication.ts index d0ae966e..d919bdd7 100644 --- a/backend/src/middlewares/authentication.ts +++ b/backend/src/middlewares/authentication.ts @@ -2,11 +2,11 @@ import { NextFunction, Request, Response } from 'express'; import * as jwt from 'jsonwebtoken'; const authMiddlware = (req: Request, res: Response, next: NextFunction) => { - try { - const token = req.header('jwt-token'); - jwt.verify(token, process.env.JWT_PRIVATE_KEY); - return next(); - } catch (e) { - return res.status(400).json({ error: 'unable to authenticate user' }); - } + try { + const token = req.header('jwt-token'); + jwt.verify(token, process.env.JWT_PRIVATE_KEY); + return next(); + } catch (e) { + return res.status(400).json({ error: 'unable to authenticate user' }); + } }; diff --git a/backend/src/routes/admin.routes.ts b/backend/src/routes/admin.routes.ts index 98939f64..85410eb8 100644 --- a/backend/src/routes/admin.routes.ts +++ b/backend/src/routes/admin.routes.ts @@ -3,6 +3,5 @@ import AdminController from '../controllers/admin'; // Create a router object export const router = express.Router(); -const adminController = new AdminController(); -router.post('/admin-login', adminController.login); +router.post('/admin-login', AdminController.login); diff --git a/backend/src/routes/authentication.routes.ts b/backend/src/routes/authentication.routes.ts deleted file mode 100644 index 531599b6..00000000 --- a/backend/src/routes/authentication.routes.ts +++ /dev/null @@ -1,10 +0,0 @@ -import * as express from 'express'; -import MealDeliveryController from '../controllers/mealDelivery'; -import authenticationController from '../controllers/authentication'; -// import MealDeliveryController from '../controllers/mealDelivery'; - -// Create a router object -export const router = express.Router(); -const authController = new authenticationController(); - -router.post('/volunteer/login', authController.login); diff --git a/backend/src/routes/client.routes.ts b/backend/src/routes/client.routes.ts index 6eb3a8cf..cf8b98e0 100644 --- a/backend/src/routes/client.routes.ts +++ b/backend/src/routes/client.routes.ts @@ -3,9 +3,8 @@ import ClientController from '../controllers/clients'; // Create a router object export const router = express.Router(); -const clientController = new ClientController(); -router.get('/clients', clientController.getClients); -router.get('/clients/:id', clientController.getClient); -router.put('/clients/:id/edit', clientController.editClient); -router.post('/clients', clientController.createClient); +router.get('/clients', ClientController.getClients); +router.get('/clients/:id', ClientController.getClient); +router.put('/clients/:id/edit', ClientController.updateClient); +router.post('/clients', ClientController.createClient); diff --git a/backend/src/routes/index.ts b/backend/src/routes/index.ts index 568f8552..fba21730 100644 --- a/backend/src/routes/index.ts +++ b/backend/src/routes/index.ts @@ -1,11 +1,9 @@ import * as express from 'express'; import { router as tasks } from './task.routes'; -import { router as mealDelivery } from './mealDelivery.routes'; -import { router as routeDelivery } from './routeDelivery.routes'; +import { router as stop } from './stop.routes'; import { router as volunteers } from './volunteer.routes'; import { router as admin } from './admin.routes'; import { router as clients } from './client.routes'; -import { router as authentication } from './authentication.routes'; // import { auth } from '../middleware/auth'; // Create a router object @@ -13,9 +11,7 @@ export const api = express.Router(); // Adds the routes from todo.route to this router api.use(tasks); -api.use(mealDelivery); -api.use(routeDelivery); +api.use(stop); api.use(volunteers); api.use(clients); -api.use(authentication); api.use(admin); diff --git a/backend/src/routes/mealDelivery.routes.ts b/backend/src/routes/mealDelivery.routes.ts deleted file mode 100644 index 5fbb7679..00000000 --- a/backend/src/routes/mealDelivery.routes.ts +++ /dev/null @@ -1,19 +0,0 @@ -import * as express from 'express'; -import MealDeliveryController from '../controllers/mealDelivery'; -// import MealDeliveryController from '../controllers/mealDelivery'; - -// Create a router object -export const router = express.Router(); -const mealDeliveryController = new MealDeliveryController(); - -router.get('/meal_delivery/', mealDeliveryController.getMealDeliveries); -router.get('/meal_delivery/:id', mealDeliveryController.getMealDelivery); -router.put( - '/meal_delivery/:id', - mealDeliveryController.updateOrCreateMealDelivery -); -router.put( - '/meal_delivery/', - mealDeliveryController.updateOrCreateMealDelivery -); -router.delete('/meal_delivery/:id', mealDeliveryController.deleteMealDelivery); diff --git a/backend/src/routes/routeDelivery.routes.ts b/backend/src/routes/routeDelivery.routes.ts deleted file mode 100644 index d56a7569..00000000 --- a/backend/src/routes/routeDelivery.routes.ts +++ /dev/null @@ -1,17 +0,0 @@ -import * as express from 'express'; -import RouteDeliveryController from '../controllers/routeDelivery'; - -// Create a router object -export const router = express.Router(); -const routeDeliveryController = new RouteDeliveryController(); - -router.get('/route_delivery/', routeDeliveryController.getRouteDeliveries); -router.put('/route_delivery/:id/set', routeDeliveryController.setRouteNumber); -router.put( - '/route_delivery/:id/increment', - routeDeliveryController.incrementRoutePosition -); -router.put( - '/route_delivery/:id/decrement', - routeDeliveryController.decrementRoutePosition -); diff --git a/backend/src/routes/stop.routes.ts b/backend/src/routes/stop.routes.ts new file mode 100644 index 00000000..10ab9fae --- /dev/null +++ b/backend/src/routes/stop.routes.ts @@ -0,0 +1,8 @@ +import * as express from 'express'; +import StopController from '../controllers/stops'; + +// Create a router object +export const router = express.Router(); + +router.get('/stops/routes/', StopController.getRoutes); +router.post('/stops/routes/', StopController.saveRoutes); \ No newline at end of file diff --git a/backend/src/routes/task.routes.ts b/backend/src/routes/task.routes.ts index e047ac1a..6bb36f23 100644 --- a/backend/src/routes/task.routes.ts +++ b/backend/src/routes/task.routes.ts @@ -3,12 +3,6 @@ import TaskController from '../controllers/tasks'; // Create a router object export const router = express.Router(); -const taskController = new TaskController(); -router.get('/tasks/:id', taskController.getTask); -router.get('/tasks/volunteer/:id', taskController.getTasksByVolunteer); -router.get('/tasks', taskController.getTasks); -router.put('/tasks', taskController.updateOrCreateTask); -router.put('/tasks/:id', taskController.updateOrCreateTask); -router.post('/tasks/', taskController.createTask); -router.delete('/tasks/:id', taskController.deleteTask); +router.get('/tasks', TaskController.getTasks); +router.post('/tasks', TaskController.getTasks); diff --git a/backend/src/routes/volunteer.routes.ts b/backend/src/routes/volunteer.routes.ts index b820b32e..60d2de5d 100644 --- a/backend/src/routes/volunteer.routes.ts +++ b/backend/src/routes/volunteer.routes.ts @@ -1,14 +1,10 @@ import * as express from 'express'; import VolunteerController from '../controllers/volunteers'; -// Create a router object export const router = express.Router(); -const volunteerController = new VolunteerController(); -router.get('/volunteers', volunteerController.getVolunteers); -router.get('/volunteers/:id', volunteerController.getVolunteer); -router.get('/volunteers/:id/tasks', volunteerController.getVolunteerTasks); -router.delete('/volunteers/:id', volunteerController.removeVolunteer); -router.post('/volunteers', volunteerController.createVolunteer); -router.put('/volunteers/:id/edit', volunteerController.editVolunteer); -router.post('/volunteer/login', volunteerController.login); +router.get('/volunteers', VolunteerController.getVolunteers); +router.get('/volunteers/:id', VolunteerController.getVolunteer); +router.post('/volunteers', VolunteerController.createVolunteer); +router.put('/volunteers/:id', VolunteerController.updateVolunteer); +router.post('/volunteer/login', VolunteerController.login); diff --git a/backend/src/scripts/drop.ts b/backend/src/scripts/drop.ts index ae2cc0ee..44aea456 100644 --- a/backend/src/scripts/drop.ts +++ b/backend/src/scripts/drop.ts @@ -1,10 +1,9 @@ import { AppDataSource } from '../data-source'; export const drop = async () => { - console.log('Begin initalizing data source'); - await AppDataSource.initialize(); - await AppDataSource.dropDatabase(); - console.log('Dropped database'); + await AppDataSource.initialize(); + await AppDataSource.dropDatabase(); + console.log('Dropped database'); }; drop(); diff --git a/backend/src/scripts/seed.ts b/backend/src/scripts/seed.ts index 83241653..5617ea4e 100644 --- a/backend/src/scripts/seed.ts +++ b/backend/src/scripts/seed.ts @@ -1,25 +1,17 @@ import { runSeeder } from 'typeorm-extension'; -import VolunteerSeeder from './seeders/volunteer.seeder'; -import ClientSeeder from './seeders/client.seeder'; -import AdminSeeder from './seeders/admin.seeder'; -import TaskSeeder from './seeders/task.seeder'; +import VolunteerSeeder from './seed/volunteer.seed'; +import ClientSeeder from './seed/client.seed'; +import AdminSeeder from './seed/admin.seed'; import { AppDataSource } from '../data-source'; export const seed = async () => { - console.log('Begin initalizing data source'); - await AppDataSource.initialize(); - - console.log('Begin seeding'); - await runSeeder(AppDataSource, ClientSeeder); - console.log('Seeded Clients'); - await runSeeder(AppDataSource, VolunteerSeeder); - console.log('Seeded Volunteers'); - await runSeeder(AppDataSource, AdminSeeder); - console.log('Seeded Admin'); - await runSeeder(AppDataSource, TaskSeeder); - console.log('Seeded Tasks'); - console.log('Done seeding'); + console.log('Begin initalizing data source'); + await AppDataSource.initialize(); + await runSeeder(AppDataSource, AdminSeeder); + await runSeeder(AppDataSource, ClientSeeder); + await runSeeder(AppDataSource, VolunteerSeeder); + console.log('Done seeding'); }; seed(); diff --git a/backend/src/scripts/seed/admin.seed.ts b/backend/src/scripts/seed/admin.seed.ts new file mode 100644 index 00000000..925fd7b8 --- /dev/null +++ b/backend/src/scripts/seed/admin.seed.ts @@ -0,0 +1,32 @@ +import { Seeder, SeederFactoryManager } from 'typeorm-extension'; +import { DataSource } from 'typeorm'; +import { AdminEntity } from '../../entities/AdminEntity'; +import * as bcrypt from 'bcryptjs'; +import { generateStaffUser } from './utils'; + +require('dotenv').config(); + +const DEFAULT_ADMIN = { + password: process.env.SEED_DEFAULT_ADMIN_EMAIL, + email: process.env.SEED_DEFAULT_ADMIN_PASSWORD, + jobTitle: 'Administrator' +}; + +const generateAdmin = async () => { + const admin = (await generateStaffUser()) as any; + admin.password = await bcrypt.hash(DEFAULT_ADMIN.password, 10); + admin.email = DEFAULT_ADMIN.email; + admin.jobTitleColumn = DEFAULT_ADMIN.jobTitle; + return admin; +}; + +export default class AdminSeeder implements Seeder { + public async run( + dataSource: DataSource, + factoryManager: SeederFactoryManager + ): Promise { + const repository = dataSource.getRepository(AdminEntity); + const adminUser = await generateAdmin(); + await repository.insert(adminUser); + } +} \ No newline at end of file diff --git a/backend/src/scripts/seed/client.seed.ts b/backend/src/scripts/seed/client.seed.ts new file mode 100644 index 00000000..ca14592f --- /dev/null +++ b/backend/src/scripts/seed/client.seed.ts @@ -0,0 +1,55 @@ +import { Seeder, SeederFactoryManager } from 'typeorm-extension'; +import { DataSource } from 'typeorm'; +import { faker } from '@faker-js/faker'; +import { ClientEntity } from '../../entities/ClientEntity'; +import { StopEntity } from '../../entities/StopEntity'; +import {ProgramType} from '../../types/enums'; +import ClientService from '../../services/clients'; + +const SEED_NUM_CLIENTS = parseInt(process.env.SEED_NUM_CLIENTS) + +const generateClient = async () => { + const firstName = faker.name.firstName(); + const lastName = faker.name.lastName(); + + const user = { + name: firstName + ' ' + lastName, + email: faker.internet.email(firstName, lastName), + phoneNumber: faker.phone.number(), + address: faker.address.streetAddress(), + mealType: faker.helpers.arrayElement([ + 'Regular', + 'Vegetarian', + 'No Fish', + 'No Meat' + ]), + sts: false, + map: false, + softDelete: false + }; + user.sts = faker.datatype.boolean(); + user.map = !user.sts ? true : faker.datatype.boolean(); + + return user; +}; + +const generateClients = async (num: number) => { + const clients: any[] = []; + for (let i = 0; i < num; i++) { + const c = await generateClient(); + clients.push(c); + } + return clients; +}; + +export default class ClientSeeder implements Seeder { + public async run( + dataSource: DataSource, + factoryManager: SeederFactoryManager + ): Promise { + const clientProps = await generateClients(SEED_NUM_CLIENTS); + clientProps.forEach(async (props) => { + const client = await ClientService.createClient(props) + }) + } +} diff --git a/backend/src/scripts/seed/utils.ts b/backend/src/scripts/seed/utils.ts new file mode 100644 index 00000000..b925f9bc --- /dev/null +++ b/backend/src/scripts/seed/utils.ts @@ -0,0 +1,25 @@ +import { faker } from '@faker-js/faker'; +import * as bcrypt from 'bcryptjs'; +import * as jwt from 'jsonwebtoken'; + +require('dotenv').config(); + +const TOKEN_KEY = process.env.TOKEN_KEY; + +export const generateStaffUser = async () => { + const firstName = faker.name.firstName(); + const lastName = faker.name.lastName(); + + const user: any = { + name: firstName + ' ' + lastName, + email: faker.internet.email(firstName, lastName), + phoneNumber: faker.phone.number() + }; + + user.password = await bcrypt.hash(faker.internet.password(), 10); + const token = jwt.sign({ email: user.email }, TOKEN_KEY, { + expiresIn: '2h' + }); + user.token = token; + return user +}; diff --git a/backend/src/scripts/seed/volunteer.seed.ts b/backend/src/scripts/seed/volunteer.seed.ts new file mode 100644 index 00000000..9f89873c --- /dev/null +++ b/backend/src/scripts/seed/volunteer.seed.ts @@ -0,0 +1,52 @@ +import { Seeder, SeederFactoryManager } from 'typeorm-extension'; +import { DataSource } from 'typeorm'; +import { faker } from '@faker-js/faker'; +import { StopEntity } from '../../entities/StopEntity'; +import {ProgramType} from '../../types/enums'; +import VolunteerService from '../../services/volunteer'; +import { generateStaffUser } from './utils'; +import * as bcrypt from 'bcryptjs'; + +const DEFAULT_VOLUNTEER = { + password: process.env.SEED_DEFAULT_VOLUNTEER_EMAIL, + email: process.env.SEED_DEFAULT_VOLUNTEER_PASSWORD +}; + +const SEED_NUM_VOLUNTEER = parseInt(process.env.SEED_NUM_VOLUNTEERS) + +const generateDefaultVolunteer = async () => { + const volunteer = (await generateStaffUser()) as any; + volunteer.password = await bcrypt.hash(DEFAULT_VOLUNTEER.password, 10); + volunteer.email = DEFAULT_VOLUNTEER.email; + volunteer.startDate = faker.date.past(2); + return volunteer; +} + +const generateVolunteer = async () => { + const volunteer = (await generateStaffUser()) as any; + volunteer.startDate = faker.date.past(2); + return volunteer; +}; + +const generateVolunteers = async (num: number) => { + const volunteers: any[] = []; + for (let i = 0; i < num; i++) { + const v = await generateVolunteer(); + volunteers.push(v); + } + return volunteers; +}; + +export default class VolunteerSeeder implements Seeder { + public async run( + dataSource: DataSource, + factoryManager: SeederFactoryManager + ): Promise { + const volunteerProps = await generateVolunteers(SEED_NUM_VOLUNTEER); + const defaultVolunteer = await generateDefaultVolunteer() + await VolunteerService.createVolunteer(defaultVolunteer) + volunteerProps.forEach(async (props) => { + const volunteer = await VolunteerService.createVolunteer(props) + }) + } +} diff --git a/backend/src/scripts/seeders/admin.seeder.ts b/backend/src/scripts/seeders/admin.seeder.ts deleted file mode 100644 index 540aa680..00000000 --- a/backend/src/scripts/seeders/admin.seeder.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { Seeder, SeederFactoryManager } from 'typeorm-extension'; -import { DataSource } from 'typeorm'; -import { AdminEntity } from '../../entities/AdminEntity'; -import * as bcrypt from 'bcryptjs'; -import { generateStaffUser } from './user'; - -require('dotenv').config(); - -const DEFAULT_ADMIN = { - password: process.env.ADMIN_PASSWORD, - email: process.env.ADMIN_EMAIL, - jobTitle: 'Administrator' -}; - -const generateAdmin = async () => { - const admin = (await generateStaffUser()) as any; - admin.password = await bcrypt.hash(DEFAULT_ADMIN.password, 10); - admin.email = DEFAULT_ADMIN.email; - admin.jobTitleColumn = DEFAULT_ADMIN.jobTitle; - return admin; -}; - -export default class AdminSeeder implements Seeder { - public async run( - dataSource: DataSource, - factoryManager: SeederFactoryManager - ): Promise { - const repository = dataSource.getRepository(AdminEntity); - const adminUser = await generateAdmin(); - await repository.insert(adminUser); - } -} diff --git a/backend/src/scripts/seeders/client.seeder.ts b/backend/src/scripts/seeders/client.seeder.ts deleted file mode 100644 index ecd7b12c..00000000 --- a/backend/src/scripts/seeders/client.seeder.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { Seeder, SeederFactoryManager } from 'typeorm-extension'; -import { DataSource } from 'typeorm'; -import { faker } from '@faker-js/faker'; -import { ClientEntity } from '../../entities/ClientEntity'; -import { RouteDeliveryEntity } from '../../entities/RouteDeliveryEntity'; -import { ProgramType } from '../../entities/types'; - -import { generateStaffUser } from './user'; - -const generateRouteDelivery = async ( - program: ProgramType, - client: ClientEntity -) => { - const routeDelivery = new RouteDeliveryEntity(); - routeDelivery.routeNumber = 0; - routeDelivery.routePosition = 0; - routeDelivery.client = client; - routeDelivery.mealType = client.mealType; - routeDelivery.program = program; - return routeDelivery; -}; - -const generateClient = async () => { - const client = (await generateStaffUser()) as any; - client.address = faker.address.streetAddress(); - client.mealType = faker.helpers.arrayElement([ - 'Vegetarian', - 'No Fish', - 'No Meat' - ]); - client.sts = faker.datatype.boolean(); - client.map = !client.sts ? true : faker.datatype.boolean(); - return client; -}; - -const generateClients = async (num: number) => { - const clients: ClientEntity[] = []; - for (let i = 0; i < num; i++) { - const c = await generateClient(); - clients.push(c); - } - return clients; -}; - -export default class ClientSeeder implements Seeder { - public async run( - dataSource: DataSource, - factoryManager: SeederFactoryManager - ): Promise { - const repository = dataSource.getRepository(ClientEntity); - const clients = await generateClients(10); - await repository.insert(clients); - - clients?.forEach(async (client) => { - const routeDeliveryRepository = - dataSource.getRepository(RouteDeliveryEntity); - - if (client.sts) { - const stsRouteDelivery = await generateRouteDelivery( - ProgramType.STS, - client - ); - await routeDeliveryRepository.insert(stsRouteDelivery); - } - - if (client.map) { - const mapRouteDelivery = await generateRouteDelivery( - ProgramType.MAP, - client - ); - await routeDeliveryRepository.insert(mapRouteDelivery); - } - }); - } -} diff --git a/backend/src/scripts/seeders/task.seeder.ts b/backend/src/scripts/seeders/task.seeder.ts deleted file mode 100644 index 0bc88460..00000000 --- a/backend/src/scripts/seeders/task.seeder.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { DataSource } from 'typeorm'; -import { TaskEntity } from '../../entities/TaskEntity'; -import { ClientEntity } from '../../entities/ClientEntity'; -import { VolunteerEntity } from '../../entities/VolunteerEntity'; -import { MealDeliveryEntity } from '../../entities/MealDeliveryEntity'; -import { faker } from '@faker-js/faker'; -import { MealType, ProgramType } from '../../entities/types'; -import { Seeder, SeederFactoryManager } from 'typeorm-extension'; - -const generateMeal = (task, client, position) => { - let type = MealType.VEGETARIAN; - if (Math.random() > 0.66) { - type = MealType.NOFISH; - } else if (Math.random() < 0.33) { - type = MealType.NOMEAT; - } - - const meal = new MealDeliveryEntity(); - meal.mealType = type; - meal.task = task; - meal.client = client; - meal.routePosition = position; - meal.isCompleted = false; - meal.program = faker.helpers.arrayElement([ProgramType.STS, ProgramType.MAP]); - return meal; -}; - -export const generateTask = async ( - dataSource: DataSource, - volunteer: VolunteerEntity -) => { - const task = new TaskEntity(); - task.isCompleted = false; - task.date = faker.date.future(0.01); - const repository = dataSource.getRepository(TaskEntity); - await repository.insert(task); - await dataSource - .createQueryBuilder() - .relation(VolunteerEntity, 'tasks') - .of({ email: volunteer.email }) - .add({ id: task.id }); - await dataSource - .createQueryBuilder() - .relation(TaskEntity, 'volunteer') - .of({ id: task.id }) - .set({ id: volunteer.id, email: volunteer.email }); - - const clientRepository = dataSource.getRepository(ClientEntity); - const mealRepository = dataSource.getRepository(MealDeliveryEntity); - - const clients = await clientRepository.find(); - - const num = 1 + Math.floor(Math.random() * 3); - for (let i = 0; i <= num; i++) { - const n = Math.floor(Math.random() * clients.length); - const client = clients[n]; - const meal = generateMeal(task, client, i); - await mealRepository.insert(meal); - } - - return task; -}; - -export default class ClientSeeder implements Seeder { - public async run( - dataSource: DataSource, - factoryManager: SeederFactoryManager - ): Promise { - const volunteerRepo = dataSource.getRepository(VolunteerEntity); - const repository = dataSource.getRepository(TaskEntity); - const volunteers = await volunteerRepo.find(); - volunteers?.forEach(async (volunteer) => { - const task = await generateTask(dataSource, volunteer); - const task2 = await generateTask(dataSource, volunteer); - const task3 = await generateTask(dataSource, volunteer); - await repository.insert(task); - await repository.insert(task2); - await repository.insert(task3); - }); - } -} diff --git a/backend/src/scripts/seeders/user.ts b/backend/src/scripts/seeders/user.ts deleted file mode 100644 index 202c1301..00000000 --- a/backend/src/scripts/seeders/user.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { faker } from '@faker-js/faker'; -import * as bcrypt from 'bcryptjs'; -import * as jwt from 'jsonwebtoken'; -require('dotenv').config(); -const TOKEN_KEY = process.env.TOKEN_KEY; -const avalabilities = ['']; -export const generateUser = (first?: string, last?: string) => { - const firstName = first || faker.name.firstName(); - const lastName = last || faker.name.lastName(); - - // random avalability - everyone will have 2 avalabilities - - const availability = - avalabilities[Math.floor(Math.random() * avalabilities.length)]; - const user = { - name: firstName + ' ' + lastName, - email: faker.internet.email(firstName, lastName), - phoneNumber: faker.phone.number() - }; - - return user; -}; - -export const generateStaffUser = async () => { - const firstName = faker.name.firstName(); - const lastName = faker.name.lastName(); - - const user = generateUser(firstName, lastName) as any; - - user.password = await bcrypt.hash(faker.internet.password(), 10); - const token = jwt.sign({ email: user.email }, TOKEN_KEY, { - expiresIn: '2h' - }); - user.token = token; - - return user; -}; diff --git a/backend/src/scripts/seeders/volunteer.seeder.ts b/backend/src/scripts/seeders/volunteer.seeder.ts deleted file mode 100644 index 39458394..00000000 --- a/backend/src/scripts/seeders/volunteer.seeder.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { Seeder, SeederFactoryManager } from 'typeorm-extension'; -import { DataSource } from 'typeorm'; -import { - VolunteerEntity, - indexedDayOfWeek, - indexedTimeSlots, - TimeSlots, - Availabilities -} from '../../entities/VolunteerEntity'; -import { faker } from '@faker-js/faker'; -import { generateTask } from './task.seeder'; -import { generateStaffUser } from './user'; -import * as bcrypt from 'bcryptjs'; - -const DEFAULT_VOLUNTEER = { - password: process.env.TEST_VOLUNTEER_PASSWORD, - email: process.env.TEST_VOLUNTEER_EMAIL -}; - -const generateDefaultVolunteer = async () => { - const volunteer = (await generateStaffUser()) as any; - volunteer.password = await bcrypt.hash(DEFAULT_VOLUNTEER.password, 10); - volunteer.email = DEFAULT_VOLUNTEER.email; - volunteer.startDate = faker.date.past(2); - volunteer.profilePicture = faker.internet.avatar(); - volunteer.availabilities = generateAvailabilities(); - volunteer.availabilitiesLastUpdated = faker.date.recent(15); - return volunteer; -}; - -const generateVolunteer = async () => { - const volunteer = (await generateStaffUser()) as any; - volunteer.startDate = faker.date.past(2); - volunteer.profilePicture = faker.internet.avatar(); - volunteer.availabilities = generateAvailabilities(); - volunteer.availabilitiesLastUpdated = faker.date.recent(15); - volunteer.preferredNeighbourhoods = ['Lachine', 'Montreal', 'Downtown']; - return volunteer; -}; - -const generateAvailabilities = () => { - const availabilities: Availabilities = { availabilities: [] }; - for (let i = 0; i < 7; i++) { - availabilities.availabilities.push({ - day: indexedDayOfWeek[i], - time: 1 + faker.random.numeric(1, { bannedDigits: ['1', '8', '9'] }) - }); - } - return JSON.stringify(availabilities.availabilities); -}; - -const generateVolunteers = async (num: number) => { - const volunteers: VolunteerEntity[] = []; - for (let i = 0; i < num; i++) { - const v = await generateVolunteer(); - volunteers.push(v); - } - return volunteers; -}; - -const generateTasks = ( - dataSource: DataSource, - volunteers: VolunteerEntity[] -) => { - volunteers.forEach(async (volunteer) => { - const num = Math.floor(Math.random() * 4); - for (let i = 0; i <= num; i++) { - await generateTask(dataSource, volunteer); - } - }); -}; - -export default class VolunteerSeeder implements Seeder { - public async run( - dataSource: DataSource, - factoryManager: SeederFactoryManager - ): Promise { - const repository = dataSource.getRepository(VolunteerEntity); - const defaultVolunteer = await generateDefaultVolunteer(); - const volunteers = await generateVolunteers(10); - await repository.insert(defaultVolunteer); - await repository.insert(volunteers); - // generateTasks(dataSource, volunteers); - } -} diff --git a/backend/src/services/admin.ts b/backend/src/services/admin.ts new file mode 100644 index 00000000..061e81cf --- /dev/null +++ b/backend/src/services/admin.ts @@ -0,0 +1,37 @@ +import { AppDataSource } from '../data-source'; +import { AdminEntity } from '../entities/AdminEntity'; +import * as bcrypt from 'bcryptjs'; +import * as jwt from 'jsonwebtoken'; +require('dotenv').config(); +const TOKEN_KEY = process.env.TOKEN_KEY; + +export default class AdminService { + static readonly AdminRepository = AppDataSource.getRepository(AdminEntity); + + static getAdminByEmail = async (email: string) => { + const adminUser = await this.AdminRepository.findOne({ + where: { email } + }); + + if (!adminUser) { + throw new Error('Admin user not found'); + } + + return adminUser + } + + static login = async (email: string, password: string) => { + const adminUser = await this.AdminRepository.findOne({ + where: { email } + }); + + if (await bcrypt.compare(password, adminUser.password)) { + const token = jwt.sign({ email: email }, TOKEN_KEY, { + expiresIn: '2h' + }); + return token + } else { + throw new Error('Wrong password'); + } + } +} \ No newline at end of file diff --git a/backend/src/services/clients.ts b/backend/src/services/clients.ts new file mode 100644 index 00000000..c9e9fbcb --- /dev/null +++ b/backend/src/services/clients.ts @@ -0,0 +1,76 @@ +import { AppDataSource } from '../data-source'; +import { ClientEntity } from '../entities/ClientEntity'; +import { Client, CreateClientProps, UpdateClientProps } from '../types/clients'; +import StopService from './stop'; + +export default class ClientService { + static readonly ClientRepository = + AppDataSource.getRepository(ClientEntity); + + static getActiveClients = async (): Promise => { + const clients = await this.ClientRepository.find({ + where: { + softDelete: false + } + }); + + return clients; + }; + + static getAllClients = async (): Promise => { + const clients = await this.ClientRepository.find({}); + return clients; + }; + + static getClient = async (id: number) => { + const client = await this.ClientRepository.findOne({ + where: { + id: id + } + }); + + return client; + }; + + static createClient = async (props: CreateClientProps): Promise => { + const client = await this.ClientRepository.create({ + name: props.name, + email: props.email, + phoneNumber: props.phoneNumber, + address: props.address, + mealType: props.mealType, + sts: props.sts, + map: props.map + }); + + await this.ClientRepository.save(client); + await StopService.createStopsFromClient(client); + + return client; + }; + + static updateClient = async ( + id: number, + updateProps: UpdateClientProps + ): Promise => { + const client = await this.ClientRepository.findOne({ + where: { + id: id + } + }); + + if (!client) return null; + + await this.ClientRepository.update({ id: id }, updateProps); + + const updatedClient = await this.ClientRepository.findOne({ + where: { + id: id + } + }); + + await StopService.updateStopsFromClient(client, updatedClient); + + return updatedClient; + }; +} diff --git a/backend/src/services/meal.ts b/backend/src/services/meal.ts new file mode 100644 index 00000000..adbc1e6b --- /dev/null +++ b/backend/src/services/meal.ts @@ -0,0 +1,56 @@ +import { AppDataSource } from '../data-source'; +import { MealDeliveryEntity } from '../entities/MealDeliveryEntity'; +import StopService from './stop'; +import TaskService from './task'; +import {UpdateMealProps} from '../types/meal' + +export default class MealService { + static readonly MealRepository = + AppDataSource.getRepository(MealDeliveryEntity); + + static createMealsForTask = async (taskId: number, routeNumber: number) => { + const stops = await StopService.getRouteStops(routeNumber) + const task = await TaskService.getTask(taskId) + + const meals = [] + + for (let i = 0; i < stops.length; i++ ) { + const stop = stops[i] + const newMeal = new MealDeliveryEntity(); + newMeal.task = task; + newMeal.mealType = stop.mealType; + newMeal.program = stop.program; + newMeal.client = stop.client; + newMeal.routePosition = i; + + const savedMeal = await this.MealRepository.save(newMeal) + meals.push(savedMeal) + } + return meals + }; + + static updateMeal = async (mealId: number, updateProps: UpdateMealProps) => { + const meal = await this.MealRepository.findOne({ + where: { + id: mealId + }, + relations: { + task: true + } + }) + + if (!meal) return null; + + await this.MealRepository.update({ id: mealId }, updateProps); + + const updatedMeal = await this.MealRepository.findOne({ + where: { + id: mealId + } + }); + + await TaskService.updateMealInTask(meal.task.id, updatedMeal) + + return updatedMeal; + } +} diff --git a/backend/src/services/stop.ts b/backend/src/services/stop.ts new file mode 100644 index 00000000..0bfc58f8 --- /dev/null +++ b/backend/src/services/stop.ts @@ -0,0 +1,160 @@ +import { AppDataSource } from '../data-source'; +import { Client, UpdateClientProps } from '../types/clients'; +import { StopEntity } from '../entities/StopEntity'; +import { ProgramType } from '../types/enums'; +import { Routes, SaveRouteProps } from '../types/stop'; + +export default class StopService { + static readonly StopRepository = + AppDataSource.getRepository(StopEntity); + + static createStopsFromClient = async (client: Client) => { + if (client.sts) { + const stopSTS = await this.createDefaultStopFromClient(client); + stopSTS.program = ProgramType.STS; + await this.StopRepository.save(stopSTS); + } + if (client.map) { + const stopMAP = await this.createDefaultStopFromClient(client); + stopMAP.program = ProgramType.MAP; + await this.StopRepository.save(stopMAP); + } + }; + + static updateStopsFromClient = async ( + prevClient: Client, + updatedClient: Client + ) => { + const updatedMealType = !( + updatedClient.mealType == prevClient.mealType + ); + const updatedSTS = !(updatedClient.sts == prevClient.sts); + const updatedMAP = !(updatedClient.map == prevClient.map); + + // created STS + if (updatedSTS && updatedClient.sts) { + const stopSTS = await this.createDefaultStopFromClient( + updatedClient + ); + stopSTS.program = ProgramType.STS; + await this.StopRepository.save(stopSTS); + // deleted STS + } else if (updatedSTS && !updatedClient.sts) { + const stopSTS = await this.StopRepository.find({ + relations: { client: true }, + where: { + client: { id: updatedClient.id }, + program: ProgramType.STS + } + }); + + await this.StopRepository.remove(stopSTS); + } + + // created MAP + if (updatedMAP && updatedClient.map) { + const stopMAP = await this.createDefaultStopFromClient( + updatedClient + ); + stopMAP.program = ProgramType.STS; + await this.StopRepository.save(stopMAP); + + // deleted MAP + } else if (updatedMAP && !updatedClient.map) { + const stopMAP = await this.StopRepository.find({ + relations: { client: true }, + where: { + client: { id: updatedClient.id }, + program: ProgramType.MAP + } + }); + + await this.StopRepository.remove(stopMAP); + } + + // update meal type + if (updatedMealType) { + const stops = await this.StopRepository.find({ + relations: { client: true }, + where: { client: { id: prevClient.id } } + }); + + stops.forEach((stop) => { + stop.mealType = updatedClient.mealType; + }); + + await this.StopRepository.save(stops); + } + }; + + static getRoutes = async (): Promise => { + const stops = await this.StopRepository.find({}); + + const routes = stops.reduce((routes, stop) => { + const key = stop.routeNumber; + if (!routes[key]) { + routes[key] = []; + } + routes[key].push(stop); + return routes; + }, {}); + + for (const routeNumber in routes) { + routes[routeNumber].sort( + (stopA, stopB) => stopA.routePosition - stopB.routePosition + ); + } + + if (!(0 in routes)) { + routes[0] = []; + } + + return routes; + }; + + static saveRoutes = async (routes: SaveRouteProps): Promise => { + const stopCountFromRouteProps = Object.values(routes).reduce((total, arr) => total + arr.length, 0); + const stopCountReal = await this.StopRepository.count({}) + + if (stopCountFromRouteProps != stopCountReal) { + return null + } + + const columns = Object.keys(routes); + for (let i = 0; i < columns.length; i++) { + const column = routes[i] + for (let index = 0; index < column.length; index++) { + const stopId = column[index] + const updatedStop = await this.StopRepository.update( + { id: stopId }, + { routeNumber: i, routePosition: index } + ); + } + } + + const savedRoutes = await this.getRoutes() + return savedRoutes + } + + static getRouteStops = async (routeNumber: number) => { + const stops = await this.StopRepository.find({ + where: { + routeNumber: routeNumber + }, + relations: { + client: true + } + }); + + return stops + } + + private static createDefaultStopFromClient = async (client: Client) => { + const stop = new StopEntity(); + stop.routeNumber = 0; + stop.routePosition = 0; + stop.client = client; + stop.mealType = client.mealType; + return stop; + }; +} diff --git a/backend/src/services/task.ts b/backend/src/services/task.ts new file mode 100644 index 00000000..fb1f3558 --- /dev/null +++ b/backend/src/services/task.ts @@ -0,0 +1,72 @@ +import { AppDataSource } from '../data-source'; +import { TaskEntity } from '../entities/TaskEntity'; +import MealService from './meal'; +import VolunteerService from './volunteer'; + +export default class TaskService { + static readonly TaskRepository = + AppDataSource.getRepository(TaskEntity); + + static getTasks = async () => { + const tasks = await this.TaskRepository.find({}) + return tasks + }; + + static getTask = async (id: number) => { + const task = await this.TaskRepository.findOne({ + where: { + id: id + } + }) + return task + }; + + static createTask = async (volunteerId: number, routeNumber: number) => { + const volunteer = await VolunteerService.getVolunteer(volunteerId) + + const newTask = new TaskEntity(); + newTask.deliveries = []; + newTask.isCompleted = false; + newTask.volunteer = volunteer; + + const savedEmptyTask = await this.TaskRepository.save(newTask) + + const meals = await MealService.createMealsForTask(savedEmptyTask.id, routeNumber); + savedEmptyTask.deliveries = meals; + + const savedTask = await this.TaskRepository.save(savedEmptyTask) + + return savedTask + }; + + static updateMealInTask = async (taskId, meal) => { + const task = await this.TaskRepository.findOne({ + where: { + id: taskId + }, + relations: { + deliveries: true + } + }) + + let index: number = task.deliveries.findIndex(item => item.id === meal.id); + + if (index !== -1) { + task.deliveries[index] = meal; + } + + const updatedTask = await this.TaskRepository.save(task) + + let updatedIsCompleted = true + + updatedTask.deliveries.forEach((meal) => { + if (meal.isCompleted == false) { + updatedIsCompleted = false + } + }) + + updatedTask.isCompleted = updatedIsCompleted + + await this.TaskRepository.save(updatedTask) + } +} diff --git a/backend/src/services/volunteer.ts b/backend/src/services/volunteer.ts new file mode 100644 index 00000000..952c5061 --- /dev/null +++ b/backend/src/services/volunteer.ts @@ -0,0 +1,99 @@ +import { AppDataSource } from '../data-source'; +import * as bcrypt from 'bcryptjs'; +import * as jwt from 'jsonwebtoken'; +import { VolunteerEntity } from '../entities/VolunteerEntity'; +require('dotenv').config(); +const TOKEN_KEY = process.env.TOKEN_KEY; +import {CreateVolunteerProps, UpdateVolunteerProps, Volunteer} from '../types/volunteer' + +export default class VolunteerService { + static readonly VolunteerRepository = AppDataSource.getRepository(VolunteerEntity); + + static getVolunteerByEmail = async (email: string) => { + const volunteerUser = await this.VolunteerRepository.findOne({ + where: { email } + }); + + if (!volunteerUser) { + throw new Error('Volunteer not found'); + } + + return volunteerUser + } + + static login = async (email: string, password: string) => { + const volunteerUser = await this.VolunteerRepository.findOne({ + where: { email } + }); + + if (await bcrypt.compare(password, volunteerUser.password)) { + const token = jwt.sign({ email: email }, TOKEN_KEY, { + expiresIn: '2h' + }); + return token + } else { + throw new Error('Wrong password'); + } + } + + static getActiveVolunteers = async () => { + const volunteers = await this.VolunteerRepository.find({ + where: { + softDelete: false + } + }); + + return volunteers; + }; + + static getAllVolunteers = async () => { + const volunteers = await this.VolunteerRepository.find({}); + return volunteers; + }; + + static getVolunteer = async (id: number) => { + const volunteer = await this.VolunteerRepository.findOne({ + where: { + id: id + } + }); + + return volunteer; + }; + + static createVolunteer = async (props: CreateVolunteerProps): Promise => { + const volunteer = await this.VolunteerRepository.create({ + name: props.name, + email: props.email, + phoneNumber: props.phoneNumber, + password: await bcrypt.hash(props.password, 10), + startDate: props.startDate + }); + + await this.VolunteerRepository.save(volunteer); + return volunteer; + }; + + static updateVolunteer = async ( + id: number, + updateProps: UpdateVolunteerProps + ): Promise => { + const volunteer = await this.VolunteerRepository.findOne({ + where: { + id: id + } + }); + + if (!volunteer) return null; + + await this.VolunteerRepository.update({ id: id }, updateProps); + + const updatedVolunteer = await this.VolunteerRepository.findOne({ + where: { + id: id + } + }); + + return updatedVolunteer; + }; +} \ No newline at end of file diff --git a/backend/src/types/clients.ts b/backend/src/types/clients.ts new file mode 100644 index 00000000..ccd12476 --- /dev/null +++ b/backend/src/types/clients.ts @@ -0,0 +1,16 @@ +import { MealType } from './enums'; + +export type Client = { + id: number; + name: string; + email: string; + phoneNumber: string; + address: string; + mealType: MealType; + sts: boolean; + map: boolean; + softDelete: boolean; +}; + +export type CreateClientProps = Omit; +export type UpdateClientProps = Partial; diff --git a/backend/src/types/enums.ts b/backend/src/types/enums.ts new file mode 100644 index 00000000..eeb4b70c --- /dev/null +++ b/backend/src/types/enums.ts @@ -0,0 +1,11 @@ +export enum ProgramType { + MAP = 'MAP', + STS = 'STS' +} + +export enum MealType { + VEGETARIAN = 'Vegetarian', + NOFISH = 'No Fish', + NOMEAT = 'No Meat', + REGULAR = 'Regular' +} diff --git a/backend/src/types/meal.ts b/backend/src/types/meal.ts new file mode 100644 index 00000000..6c0e9bf0 --- /dev/null +++ b/backend/src/types/meal.ts @@ -0,0 +1,15 @@ +import { Client } from "./clients"; +import { MealType, ProgramType } from "./enums"; +import { Task } from "./task"; + +export type Meal = { + id: number; + isCompleted: boolean; + routePosition: number; + mealType: MealType; + program: ProgramType; + task: Task; + client: Client; +}; + +export type UpdateMealProps = Partial; diff --git a/backend/src/types/stop.ts b/backend/src/types/stop.ts new file mode 100644 index 00000000..00c42131 --- /dev/null +++ b/backend/src/types/stop.ts @@ -0,0 +1,18 @@ +import { Client } from './clients'; +import { MealType, ProgramType } from './enums'; + +export type Stop = { + routeNumber: number; + routePosition: number; + client: Client; + mealType: MealType; + program: ProgramType; +}; + +export type Routes = { + [key: number]: Stop[]; +}; + +export type SaveRouteProps = { + [key: number]: number[]; +} \ No newline at end of file diff --git a/backend/src/types/task.ts b/backend/src/types/task.ts new file mode 100644 index 00000000..b09ed481 --- /dev/null +++ b/backend/src/types/task.ts @@ -0,0 +1,10 @@ +import {Volunteer} from './volunteer'; +import {Meal} from './meal' + +export type Task = { + id: number; + date: Date; + isCompleted: boolean; + volunteer: Volunteer; + deliveries: Meal[]; +}; diff --git a/backend/src/types/volunteer.ts b/backend/src/types/volunteer.ts new file mode 100644 index 00000000..585c8f0b --- /dev/null +++ b/backend/src/types/volunteer.ts @@ -0,0 +1,13 @@ + +export type Volunteer = { + id: number; + name: string; + email: string; + phoneNumber: string; + softDelete: boolean; + password: string; + startDate: Date +}; + +export type CreateVolunteerProps = Omit; +export type UpdateVolunteerProps = Partial; diff --git a/backend/tests/admin/service.test.ts b/backend/tests/admin/service.test.ts new file mode 100644 index 00000000..0430e85e --- /dev/null +++ b/backend/tests/admin/service.test.ts @@ -0,0 +1,56 @@ +import { + jest, + describe, + it, + beforeAll, + afterAll, + afterEach, + expect +} from '@jest/globals'; +import DataSourceHelper from '../data.utils'; +import AdminService from '../../src/services/admin'; +import AdminUtils from './utils'; +import * as jwt from 'jsonwebtoken'; +require('dotenv').config(); +const TOKEN_KEY = process.env.TOKEN_KEY; + +describe('Admin Service Unit Tests', () => { + // Before performing any tests, sets up the datasource and clears it + beforeAll(async () => { + await DataSourceHelper.setupDataSource(); + await DataSourceHelper.clearDataSource(); + }); + + // After performing all the tests, destroys the datasource + afterAll(async () => { + await DataSourceHelper.destroyDataSource(); + }); + + // After each test, clears the datasource + afterEach(async () => { + await DataSourceHelper.clearDataSource(); + }); + + describe('login()', () => { + it('should return error, when credentials are invalid', async () => { + const admin = await AdminUtils.createAdmin({ + email: `admin@email.com`, + password: 'admin@password' + }) + + await expect(AdminService.login(`invalid@email.com`, 'invalid@password')).rejects.toThrow(); + }); + + it('should return token, when credentials are valid', async () => { + const admin = await AdminUtils.createAdmin({ + email: `admin@email.com`, + password: 'admin@password' + }) + + const token = await AdminService.login(`admin@email.com`, 'admin@password') + + const decodedToken = jwt.verify(token, TOKEN_KEY) as {email: string;}; + expect(decodedToken.email).toBe('admin@email.com'); + }); + }); +}); diff --git a/backend/tests/admin/utils.ts b/backend/tests/admin/utils.ts new file mode 100644 index 00000000..b3b835da --- /dev/null +++ b/backend/tests/admin/utils.ts @@ -0,0 +1,35 @@ +import { AppDataSource } from '../../src/data-source'; +import { AdminEntity } from '../../src/entities/AdminEntity'; +import * as bcrypt from 'bcryptjs'; + +type CreateAdminProps = { + name?: string, + email?: string, + phoneNumber?: string, + password?: string +} + +export default class AdminUtils { + static AdminRepository = AppDataSource.getRepository(AdminEntity); + + static createAdmin = async (props: Partial = {}) => { + const mergedProps: CreateAdminProps = { + name: 'Firstname Lastname', + email: `${new Date().getTime()}@email.com`, + phoneNumber: '(514) 000 0000', + password: 'password', + ...props + }; + + const newAdmin = new AdminEntity(); + newAdmin.name = mergedProps.name; + newAdmin.email = mergedProps.email; + newAdmin.phoneNumber = mergedProps.phoneNumber; + newAdmin.password = await bcrypt.hash(mergedProps.password, 10); + newAdmin.jobTitleColumn = 'Administrator' + + const savedAdmin = await this.AdminRepository.save(newAdmin); + + return savedAdmin + } +} diff --git a/backend/tests/client/client.test.ts b/backend/tests/client/client.test.ts deleted file mode 100644 index 86abf78e..00000000 --- a/backend/tests/client/client.test.ts +++ /dev/null @@ -1,123 +0,0 @@ -import { - jest, - describe, - it, - beforeAll, - afterAll, - afterEach, - expect -} from '@jest/globals'; -import * as request from 'supertest'; -import DataSourceHelper from '../data.utils'; -import ClientEntityHelper from './client.utils'; -import { AppDataSource } from '../../src/data-source'; -import { ClientEntity } from '../../src/entities/ClientEntity'; -import app from '../../src/app'; -import { StatusCode } from '../../src/controllers/statusCode'; -import { MealType } from '../../src/entities/types'; - -describe('Client tests', () => { - const ClientRepository = AppDataSource.getRepository(ClientEntity); - - const clientHelper = new ClientEntityHelper(ClientRepository); - - // Before performing any tests, sets up the datasource and clears it - beforeAll(async () => { - await DataSourceHelper.setupDataSource(); - await DataSourceHelper.clearDataSource(); - }); - - // After performing all the tests, destroys the datasource - afterAll(async () => { - await DataSourceHelper.destroyDataSource(); - }); - - // After each test, clears the datasource - afterEach(async () => { - await DataSourceHelper.clearDataSource(); - }); - - // GET /api/clients - it('should return no clients', async () => { - const res = await request(app).get('/api/clients'); - expect(res.status).toBe(StatusCode.OK); - expect(res.body).toEqual({ - clients: [] - }); - }); - - // GET /api/clients/ - it('should return all clients', async () => { - const client1 = await clientHelper.createClient({ - name: 'Test Client', - email: 'email@email.com', - phoneNumber: '1234567890', - address: '1234 Test Address', - mealType: MealType.NOFISH, - sts: true, - map: true - }); - const client2 = await clientHelper.createClient({ - name: 'Test Client2', - email: 'email2@email.com', - phoneNumber: '0987654321', - address: '1234 Test Address2', - mealType: MealType.NOMEAT, - sts: false, - map: true - }); - const res = await request(app).get('/api/clients'); - expect(res.status).toBe(StatusCode.OK); - expect(res.body).toEqual({ - clients: [ - { - id: 1, - name: 'Test Client', - email: 'email@email.com', - phoneNumber: '1234567890', - address: '1234 Test Address', - mealType: MealType.NOFISH, - sts: true, - map: true - }, - { - id: 2, - name: 'Test Client2', - email: 'email2@email.com', - phoneNumber: '0987654321', - address: '1234 Test Address2', - mealType: MealType.NOMEAT, - sts: false, - map: true - } - ] - }); - }); - - // GET /api/clients/:id - it('should return a client', async () => { - const client1 = await clientHelper.createClient({ - name: 'Test Client', - email: 'email@email.com', - phoneNumber: '1234567890', - address: '1234 Test Address', - mealType: MealType.NOFISH, - sts: true, - map: true - }); - const res = await request(app).get(`/api/clients/${client1.id}`); - expect(res.status).toBe(StatusCode.OK); - expect(res.body).toEqual({ - client: { - id: 1, - name: 'Test Client', - email: 'email@email.com', - phoneNumber: '1234567890', - address: '1234 Test Address', - mealType: MealType.NOFISH, - sts: true, - map: true - } - }); - }); -}); diff --git a/backend/tests/client/client.utils.ts b/backend/tests/client/client.utils.ts deleted file mode 100644 index 0604dbf7..00000000 --- a/backend/tests/client/client.utils.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { MealDeliveryEntity } from '../../src/entities/MealDeliveryEntity'; -import { Repository } from 'typeorm'; -import { TaskEntity } from '../../src/entities/TaskEntity'; -import { MealType, ProgramType } from '../../src/entities/types'; -import { ClientEntity } from '../../src/entities/ClientEntity'; - -type HelperCreateClientProps = { - name?: string, - email?: string, - phoneNumber?: string, - address?: string, - mealType?: MealType, - sts?: boolean, - map?: boolean -} - -export default class ClientEntityHelper { - ClientRepository: Repository; - - constructor(repository: Repository) { - this.ClientRepository = repository; - } - - createClient = async (props: HelperCreateClientProps) => { - const newClient = new ClientEntity(); - newClient.name = props.name || 'Test Client'; - newClient.email = props.email || 'email@email.com'; - newClient.phoneNumber = props.phoneNumber || '1234567890'; - newClient.address = props.address || '1234 Test Address'; - newClient.mealType = props.mealType || MealType.NOFISH; - newClient.sts = typeof props.sts !== 'undefined' ? props.sts : true; - newClient.map = typeof props.map !== 'undefined' ? props.map : true; - return await this.ClientRepository.save(newClient); - } -} diff --git a/backend/tests/client/service.test.ts b/backend/tests/client/service.test.ts new file mode 100644 index 00000000..e446d024 --- /dev/null +++ b/backend/tests/client/service.test.ts @@ -0,0 +1,283 @@ +import { + jest, + describe, + it, + beforeAll, + afterAll, + afterEach, + expect +} from '@jest/globals'; +import DataSourceHelper from '../data.utils'; +import ClientService from '../../src/services/clients'; +import ClientUtils from './utils'; +import { MealType, ProgramType } from '../../src/types/enums'; + +describe('Client Service Unit Tests', () => { + + // Before performing any tests, sets up the datasource and clears it + beforeAll(async () => { + await DataSourceHelper.setupDataSource(); + await DataSourceHelper.clearDataSource(); + }); + + // After performing all the tests, destroys the datasource + afterAll(async () => { + await DataSourceHelper.destroyDataSource(); + }); + + // After each test, clears the datasource + afterEach(async () => { + await DataSourceHelper.clearDataSource(); + }); + + describe('getActiveClients()', () => { + it('should return no clients, when there are no clients', async () => { + const clients = await ClientService.getActiveClients() + expect(clients).toEqual([]); + }); + + it('should return no clients, when there are only deactivated clients', async () => { + await ClientUtils.createClient({softDelete: true}); + await ClientUtils.createClient({softDelete: true}); + + const clients = await ClientService.getActiveClients() + expect(clients).toEqual([]); + }); + + it('should return only active clients, when there are both active and deactivated clients', async () => { + await ClientUtils.createClient({softDelete: true}); + const activeClient = await ClientUtils.createClient(); + const clients = await ClientService.getActiveClients() + + expect(clients).toEqual([activeClient]); + }); + }); + + describe('getAllClients()', () => { + it('should return no clients, when there are no clients', async () => { + const clients = await ClientService.getAllClients() + expect(clients).toEqual([]); + }); + + it('should return all clients, when there are both active and deactivated clients', async () => { + const client1 = await ClientUtils.createClient({softDelete: true}); + const client2 = await ClientUtils.createClient(); + const clients = await ClientService.getAllClients() + + expect(clients).toEqual([client1, client2]); + }); + }) + + describe('getClient()', () => { + it("should return no clients, when it doesn't exist", async () => { + const client = await ClientService.getClient(1) + expect(client).toEqual(null); + }); + + it('should return client, when it does exist', async () => { + await ClientUtils.createClient({ + name: 'Firstname Lastname', + email: `client@email.com`, + phoneNumber: '(514) 111 1111', + address: '845 Rue Sherbrooke O, Montréal, QC H3A 0G4, Canada', + mealType: MealType.REGULAR, + sts: true, + map: true, + }); + const client = await ClientService.getClient(1); + + expect(client).toMatchObject({ + id: 1, + name: 'Firstname Lastname', + email: `client@email.com`, + phoneNumber: '(514) 111 1111', + address: '845 Rue Sherbrooke O, Montréal, QC H3A 0G4, Canada', + mealType: MealType.REGULAR, + sts: true, + map: true, + }); + }); + }) + + describe('createClient()', () => { + it("should verify stops are created and returns client", async () => { + const client = await ClientService.createClient({ + name: "Firstname Lastname", + email: "email@email.com", + phoneNumber: '(514) 000 0000', + address: '1234 Test Address', + mealType: MealType.REGULAR, + sts: true, + map: true + }) + + const {stopMAP, stopSTS} = await ClientUtils.getStopsForClient(client); + + expect(stopMAP).toMatchObject({ + program: ProgramType.MAP, + mealType: MealType.REGULAR, + routeNumber: 0, + routePosition: 0 + }) + + expect(stopSTS).toMatchObject({ + program: ProgramType.STS, + mealType: MealType.REGULAR, + routeNumber: 0, + routePosition: 0 + }) + + expect(client).toEqual({ + id: 1, + name: "Firstname Lastname", + email: "email@email.com", + phoneNumber: '(514) 000 0000', + address: '1234 Test Address', + mealType: MealType.REGULAR, + sts: true, + map: true, + softDelete: false + }); + }); + }) + + describe('updateClient()', () => { + it("should return no clients, when it doesn't exist", async () => { + const client = await ClientService.updateClient(1, { + name: "Firstname Lastname", + email: "email@email.com", + phoneNumber: '(514) 000 0000', + address: '1234 Test Address', + mealType: MealType.REGULAR, + sts: true, + map: true + }) + + expect(client).toEqual(null); + }) + + it("should return updated client, when it does exist (update client properties)", async () => { + const client = await ClientUtils.createClient({ + name: "Firstname Lastname", + email: "email@email.com", + phoneNumber: '(514) 000 0000', + address: '1234 Test Address', + mealType: MealType.REGULAR, + sts: true, + map: true + }); + + const updatedClient = await ClientService.updateClient(1, { + email: "newemail@email.com", + address: '845 Rue Sherbrooke O, Montréal, QC H3A 0G4, Canada' + }) + + expect(updatedClient).toEqual({ + id: 1, + name: "Firstname Lastname", + email: "newemail@email.com", + phoneNumber: '(514) 000 0000', + address: '845 Rue Sherbrooke O, Montréal, QC H3A 0G4, Canada', + mealType: MealType.REGULAR, + sts: true, + map: true, + softDelete: false + }); + }) + + it("should return updated client, when it does exist (update stop's mealType)", async () => { + const client = await ClientUtils.createClient({ + name: "Firstname Lastname", + email: "email@email.com", + phoneNumber: '(514) 000 0000', + address: '1234 Test Address', + mealType: MealType.REGULAR, + sts: true, + map: true + }); + + const updatedClient = await ClientService.updateClient(1, { + mealType: MealType.VEGETARIAN + }) + + expect(updatedClient).toEqual({ + id: 1, + name: "Firstname Lastname", + email: "email@email.com", + phoneNumber: '(514) 000 0000', + address: '1234 Test Address', + mealType: MealType.VEGETARIAN, + sts: true, + map: true, + softDelete: false + }); + + const {stopMAP, stopSTS} = await ClientUtils.getStopsForClient(updatedClient); + + expect(stopMAP).toMatchObject({ + program: ProgramType.MAP, + mealType: MealType.VEGETARIAN, + routeNumber: 0, + routePosition: 0 + }) + + expect(stopSTS).toMatchObject({ + program: ProgramType.STS, + mealType: MealType.VEGETARIAN, + routeNumber: 0, + routePosition: 0 + }) + }) + + it("should return updated client, when it does exist (update stop's programs)", async () => { + const client = await ClientUtils.createClient({ + name: "Firstname Lastname", + email: "email@email.com", + phoneNumber: '(514) 000 0000', + address: '1234 Test Address', + mealType: MealType.REGULAR, + sts: false, + map: true + }); + + const {stopMAP: prevStopMAP, stopSTS: prevStopSTS} = await ClientUtils.getStopsForClient(client); + + expect(prevStopMAP).toMatchObject({ + program: ProgramType.MAP, + mealType: MealType.REGULAR, + routeNumber: 0, + routePosition: 0 + }) + + expect(prevStopSTS).toBeNull(); + + const updatedClient = await ClientService.updateClient(1, { + sts: true, + map: false + }) + + expect(updatedClient).toEqual({ + id: 1, + name: "Firstname Lastname", + email: "email@email.com", + phoneNumber: '(514) 000 0000', + address: '1234 Test Address', + mealType: MealType.REGULAR, + sts: true, + map: false, + softDelete: false + }); + + const {stopMAP, stopSTS} = await ClientUtils.getStopsForClient(updatedClient); + + expect(stopMAP).toBeNull(); + + expect(stopSTS).toMatchObject({ + program: ProgramType.STS, + mealType: MealType.REGULAR, + routeNumber: 0, + routePosition: 0 + }) + }) + }) +}); diff --git a/backend/tests/client/utils.ts b/backend/tests/client/utils.ts new file mode 100644 index 00000000..f97c832d --- /dev/null +++ b/backend/tests/client/utils.ts @@ -0,0 +1,89 @@ +import { AppDataSource } from '../../src/data-source'; +import { ClientEntity } from '../../src/entities/ClientEntity'; +import { MealType } from '../../src/types/enums'; +import { Client } from '../../src/types/clients'; +import { StopEntity } from '../../src/entities/StopEntity'; +import { ProgramType } from '../../src/entities/types'; + +type CreateClientProps = { + name?: string, + email?: string, + phoneNumber?: string, + address?: string, + mealType?: MealType, + sts?: boolean, + map?: boolean, + softDelete?: boolean +} + +export default class ClientUtils { + static ClientRepository = AppDataSource.getRepository(ClientEntity); + static StopRepository = AppDataSource.getRepository(StopEntity) + + static createClient = async (props: Partial = {}) => { + const mergedProps: CreateClientProps = { + name: 'Firstname Lastname', + email: `${new Date().getTime()}@email.com`, + phoneNumber: '(514) 000 0000', + address: '1234 Test Address', + mealType: MealType.REGULAR, + sts: true, + map: true, + softDelete: false, + ...props + }; + + const newClient = new ClientEntity(); + newClient.name = mergedProps.name; + newClient.email = mergedProps.email; + newClient.phoneNumber = mergedProps.phoneNumber; + newClient.address = mergedProps.address; + newClient.mealType = mergedProps.mealType; + newClient.sts = mergedProps.sts; + newClient.map = mergedProps.map; + newClient.softDelete = mergedProps.softDelete; + + const savedClient = await this.ClientRepository.save(newClient); + + if (savedClient.sts) { + const stop = new StopEntity(); + stop.routeNumber = 0; + stop.routePosition = 0; + stop.client = savedClient; + stop.mealType = savedClient.mealType; + stop.program = ProgramType.STS; + await this.StopRepository.save(stop); + } + + if (savedClient.map) { + const stop = new StopEntity(); + stop.routeNumber = 0; + stop.routePosition = 0; + stop.client = savedClient; + stop.mealType = savedClient.mealType; + stop.program = ProgramType.MAP; + await this.StopRepository.save(stop); + } + + return savedClient + } + + static getStopsForClient = async (client: Client) => { + const stops = await this.StopRepository.find({ + relations: { + client: true + }, + where: { + client: { id: client.id }, + } + }) + + const stopMAP = stops.find(stop => stop.program === ProgramType.MAP); + const stopSTS = stops.find(stop => stop.program === ProgramType.STS); + + return { + stopMAP: stopMAP || null, + stopSTS: stopSTS || null + }; + } +} diff --git a/backend/tests/example.test.ts b/backend/tests/example.test.ts deleted file mode 100644 index 3e9d5727..00000000 --- a/backend/tests/example.test.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { describe, expect, test } from '@jest/globals'; -import { sum } from './example/example'; - -test('adds 1 + 2 to equal 3', () => { - expect(sum(1, 2)).toBe(3); -}); diff --git a/backend/tests/example/example.ts b/backend/tests/example/example.ts deleted file mode 100644 index 508f35f6..00000000 --- a/backend/tests/example/example.ts +++ /dev/null @@ -1,3 +0,0 @@ -export function sum(a: number, b: number) { - return a + b; -} diff --git a/backend/tests/meal/service.test.ts b/backend/tests/meal/service.test.ts new file mode 100644 index 00000000..5f1842d9 --- /dev/null +++ b/backend/tests/meal/service.test.ts @@ -0,0 +1,57 @@ +import { + jest, + describe, + it, + beforeAll, + afterAll, + afterEach, + expect +} from '@jest/globals'; +import DataSourceHelper from '../data.utils'; +import { MealType, ProgramType } from '../../src/types/enums'; +import MealService from '../../src/services/meal'; +import MealUtils from './utils'; + +describe('Meal Service Unit Tests', () => { + // Before performing any tests, sets up the datasource and clears it + beforeAll(async () => { + await DataSourceHelper.setupDataSource(); + await DataSourceHelper.clearDataSource(); + }); + + // After performing all the tests, destroys the datasource + afterAll(async () => { + await DataSourceHelper.destroyDataSource(); + }); + + // After each test, clears the datasource + afterEach(async () => { + await DataSourceHelper.clearDataSource(); + }); + + describe('updateMeal()', () => { + it('should verify meal and task are updated, and return meal', async () => { + await MealUtils.setupTask({ + deliveries: [ + {program: ProgramType.MAP, mealType: MealType.REGULAR}, + {program: ProgramType.STS, mealType: MealType.VEGETARIAN} + ] + }) + + const updatedMeal = await MealService.updateMeal(1, {isCompleted: true}) + const task = await MealUtils.getTask(1) + + expect(updatedMeal).toMatchObject({ + isCompleted: true, + mealType: MealType.REGULAR, + program: ProgramType.MAP + }) + + expect(task.deliveries.find((meal) => meal.routePosition == 0)).toMatchObject({ + isCompleted: true, + mealType: MealType.REGULAR, + program: ProgramType.MAP + }) + }) + }); +}); diff --git a/backend/tests/meal/utils.ts b/backend/tests/meal/utils.ts new file mode 100644 index 00000000..b7076b6f --- /dev/null +++ b/backend/tests/meal/utils.ts @@ -0,0 +1,105 @@ +import { AppDataSource } from '../../src/data-source'; +import { VolunteerEntity } from '../../src/entities/VolunteerEntity'; +import * as bcrypt from 'bcryptjs'; +import { ClientEntity } from '../../src/entities/ClientEntity'; +import { StopEntity } from '../../src/entities/StopEntity'; +import { TaskEntity } from '../../src/entities/TaskEntity'; +import { MealType, ProgramType } from '../../src/types/enums'; +import { Volunteer } from '../../src/types/volunteer'; +import { MealDeliveryEntity } from '../../src/entities/MealDeliveryEntity'; + +type CreateMealProps = { + mealType: MealType, + program: ProgramType, +} + +type CreateTaskProps = { + deliveries: CreateMealProps[] +} + +export default class MealUtils { + static VolunteerRepository = AppDataSource.getRepository(VolunteerEntity); + static ClientRepository = AppDataSource.getRepository(ClientEntity); + static StopRepository = AppDataSource.getRepository(StopEntity) + static MealRepository = AppDataSource.getRepository(MealDeliveryEntity) + static TaskRepository = AppDataSource.getRepository(TaskEntity) + + static getTask = async (taskId: number) => { + const task = await this.TaskRepository.findOne({ + where: { + id: taskId + }, + relations: { + deliveries: true + } + }) + return task + } + + static setupTask = async (props: CreateTaskProps) => { + const volunteerProps = { + name: 'Firstname Lastname', + email: `${new Date().getTime()}@email.com`, + phoneNumber: '(514) 000 0000', + password: 'password', + startDate: new Date(), + softDelete: false + }; + + const newVolunteer = new VolunteerEntity(); + newVolunteer.name = volunteerProps.name; + newVolunteer.email = volunteerProps.email; + newVolunteer.phoneNumber = volunteerProps.phoneNumber; + newVolunteer.password = await bcrypt.hash(volunteerProps.password, 10); + newVolunteer.startDate = volunteerProps.startDate; + newVolunteer.softDelete = volunteerProps.softDelete; + + const savedVolunteer = await this.VolunteerRepository.save(newVolunteer); + + const newTask = new TaskEntity(); + newTask.volunteer = savedVolunteer; + + const savedTask = await this.TaskRepository.save(newTask) + + const deliveries = props.deliveries + + for (let index = 0; index < deliveries.length; index++) { + const { mealType, program } = deliveries[index]; + + const newClient = new ClientEntity(); + newClient.name = 'Firstname Lastname'; + newClient.email = `${new Date().getTime()}@email.com`; + newClient.phoneNumber = '(514) 000 0000'; + newClient.address = '1234 Test Address'; + newClient.mealType = mealType; + if (program == ProgramType.MAP) { + newClient.map = true + newClient.sts = false + } + if (program == ProgramType.STS) { + newClient.map = false + newClient.sts = true + } + newClient.softDelete = false; + + const savedClient = await this.ClientRepository.save(newClient) + + const newMeal = new MealDeliveryEntity(); + newMeal.client = savedClient + newMeal.program = program + newMeal.mealType = mealType + newMeal.routePosition = index + newMeal.task = savedTask + const savedMeal = await this.MealRepository.save(newMeal); + } + + const foundTask = await this.TaskRepository.findOne({ + where: {id: savedTask.id}, + relations: { + deliveries: true + } + }) + + return foundTask + } +} diff --git a/backend/tests/mealDelivery/mealDelivery.test.ts b/backend/tests/mealDelivery/mealDelivery.test.ts deleted file mode 100644 index a1d911a5..00000000 --- a/backend/tests/mealDelivery/mealDelivery.test.ts +++ /dev/null @@ -1,132 +0,0 @@ -import { describe, it } from '@jest/globals'; -import { MealDeliveryEntity } from '../../src/entities/MealDeliveryEntity'; -import { AppDataSource } from '../../src/data-source'; -import * as request from 'supertest'; -import MealDeliveryEntityHelper from './mealDelivery.utils'; -import DataSourceHelper from './../data.utils'; - -import app from '../../src/app'; -import { StatusCode } from '../../src/controllers/statusCode'; -import { MealType, ProgramType } from '../../src/entities/types'; - -describe('Tasks tests', () => { - const mealDeliveryRepository = - AppDataSource.getRepository(MealDeliveryEntity); - const mealDeliveryHelper = new MealDeliveryEntityHelper( - mealDeliveryRepository - ); - - // Before performing any tests, sets up the datasource and clears it - beforeAll(async () => { - await DataSourceHelper.setupDataSource(); - await DataSourceHelper.clearDataSource(); - }); - - // After performing all the tests, destroys the datasource - afterAll(async () => { - await DataSourceHelper.destroyDataSource(); - }); - - // After each test, clears the datasource - afterEach(async () => { - await DataSourceHelper.clearDataSource(); - }); - - it('should return meal delivery not found', async () => { - const res = await request(app).get('/api/meal_delivery/1'); - expect(res.status).toBe(StatusCode.BAD_REQUEST); - expect(res.body).toEqual({ - mealDelivery: null - }); - }); - - it('should return a mealDelivery', async () => { - const savedMealDelivery = await mealDeliveryHelper.createMealDelivery( - false, - 1, - MealType.NOFISH, - ProgramType.MAP, - null, - null - ); - // console.log(savedMealDelivery); - const res = await request(app).get( - `/api/meal_delivery/${savedMealDelivery.id}` - ); - expect(res.status).toBe(StatusCode.OK); - expect(res.body).toEqual({ - mealDelivery: { - id: savedMealDelivery.id, - mealType: MealType.NOFISH, - isCompleted: false, - routePosition: 1, - client: null, - task: null, - program: ProgramType.MAP - } - }); - }); - -// Should be auto created from route -// it('should create a meal delivery', async () => { -// const res = await request(app).put(`/api/meal_delivery`).send({ -// mealType: MealType.NOFISH, -// task: null -// }); -// expect(res.statusCode).toBe(200); -// expect(res.body).toEqual({ -// mealDelivery: { -// id: expect.any(Number), -// mealType: MealType.NOFISH, -// isCompleted: false, -// routePosition: 1, -// client: null, -// task: null, -// program: ProgramType.MAP -// } -// }); -// }); - -// it('should update a meal delivery', async () => { -// const savedMealDelivery = await mealDeliveryHelper.createMealDelivery( -// false, -// 1, -// MealType.NOFISH, -// ProgramType.MAP, -// null, -// null -// ); -// const res = await request(app) -// .put(`/api/meal_delivery/${savedMealDelivery.id}`) -// .send({ -// quantity: 2, -// mealType: 'lunch', -// task: null -// }); -// expect(res.statusCode).toBe(200); -// expect(res.body).toEqual({ -// mealDelivery: { -// id: expect.any(Number), -// quantity: 2, -// mealType: 'lunch', -// task: null -// } -// }); -// }); - - it('should delete meal delivery', async () => { - const savedMealDelivery = await mealDeliveryHelper.createMealDelivery( - false, - 1, - MealType.NOFISH, - ProgramType.MAP, - null, - null - ); - const res = await request(app).delete( - `/api/meal_delivery/${savedMealDelivery.id}` - ); - expect(res.status).toBe(StatusCode.OK); - expect(res.body).toEqual({}); - }); -}); diff --git a/backend/tests/mealDelivery/mealDelivery.utils.ts b/backend/tests/mealDelivery/mealDelivery.utils.ts deleted file mode 100644 index e49d8de3..00000000 --- a/backend/tests/mealDelivery/mealDelivery.utils.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { MealDeliveryEntity } from '../../src/entities/MealDeliveryEntity'; -import { Repository } from 'typeorm'; -import { TaskEntity } from '../../src/entities/TaskEntity'; -import { MealType, ProgramType } from '../../src/entities/types'; -import { ClientEntity } from '../../src/entities/ClientEntity'; - -export default class MealDeliveryEntityHelper { - MealDeliveryRepository: Repository; - - constructor(repository: Repository) { - this.MealDeliveryRepository = repository; - } - - createMealDelivery = async ( - isCompleted: boolean, - routePosition: number, - mealType: MealType, - program: ProgramType, - task: TaskEntity, - client: ClientEntity - ) => { - const newMealDelivery = new MealDeliveryEntity(); - newMealDelivery.isCompleted = isCompleted; - newMealDelivery.routePosition = routePosition; - newMealDelivery.mealType = mealType; - newMealDelivery.program = program; - newMealDelivery.task = task; - newMealDelivery.client = client; - return await this.MealDeliveryRepository.save(newMealDelivery); - }; -} diff --git a/backend/tests/stops/service.test.ts b/backend/tests/stops/service.test.ts new file mode 100644 index 00000000..a3db1c8c --- /dev/null +++ b/backend/tests/stops/service.test.ts @@ -0,0 +1,245 @@ +import { + jest, + describe, + it, + beforeAll, + afterAll, + afterEach, + expect +} from '@jest/globals'; +import DataSourceHelper from '../data.utils'; +import StopService from '../../src/services/stop'; +import { MealType, ProgramType } from '../../src/types/enums'; +import StopUtils from './utils'; + +describe('Stop Service Unit Tests', () => { + + // Before performing any tests, sets up the datasource and clears it + beforeAll(async () => { + await DataSourceHelper.setupDataSource(); + await DataSourceHelper.clearDataSource(); + }); + + // After performing all the tests, destroys the datasource + afterAll(async () => { + await DataSourceHelper.destroyDataSource(); + }); + + // After each test, clears the datasource + afterEach(async () => { + await DataSourceHelper.clearDataSource(); + }); + + describe('getRoutes()', () => { + it('should return unassigned routes, when no routes are assigned', async () => { + await StopUtils.setupRoutes({ + 0: [ + {mealType: MealType.REGULAR, program: ProgramType.MAP}, + {mealType: MealType.REGULAR, program: ProgramType.STS}, + {mealType: MealType.REGULAR, program: ProgramType.MAP}, + ] + }) + + const routes = await StopService.getRoutes(); + + expect(Object.entries(routes).length).toBe(1); + + expect(routes[0].length).toBe(3) + + expect(routes[0][0]).toMatchObject({ + routeNumber: 0, + routePosition: 0, + mealType: MealType.REGULAR, + program: ProgramType.MAP + }); + + expect(routes[0][1]).toMatchObject({ + routeNumber: 0, + routePosition: 0, + mealType: MealType.REGULAR, + program: ProgramType.STS + }); + + expect(routes[0][2]).toMatchObject({ + routeNumber: 0, + routePosition: 0, + mealType: MealType.REGULAR, + program: ProgramType.MAP + }); + }); + + // defined routes + it('should return routes, when there exists both unassigned and assigned routes', async () => { + await StopUtils.setupRoutes({ + 0: [ + {mealType: MealType.REGULAR, program: ProgramType.MAP}, + ], + 1: [ + {mealType: MealType.REGULAR, program: ProgramType.MAP}, + {mealType: MealType.REGULAR, program: ProgramType.MAP}, + {mealType: MealType.REGULAR, program: ProgramType.STS}, + ], + 2: [ + {mealType: MealType.REGULAR, program: ProgramType.STS}, + {mealType: MealType.REGULAR, program: ProgramType.MAP}, + ], + }) + + const routes = await StopService.getRoutes(); + + expect(Object.entries(routes).length).toBe(3); + expect(routes[0].length).toBe(1) + expect(routes[1].length).toBe(3) + expect(routes[2].length).toBe(2) + + expect(routes[0][0]).toMatchObject({ + routeNumber: 0, + routePosition: 0, + mealType: MealType.REGULAR, + program: ProgramType.MAP + }); + + expect(routes[1][2]).toMatchObject({ + routeNumber: 1, + routePosition: 2, + mealType: MealType.REGULAR, + program: ProgramType.STS + }); + + expect(routes[2][1]).toMatchObject({ + routeNumber: 2, + routePosition: 1, + mealType: MealType.REGULAR, + program: ProgramType.MAP + }); + }); + + it('should return routes, when all routes are assigned', async () => { + await StopUtils.setupRoutes({ + 0: [], + 1: [ + {mealType: MealType.REGULAR, program: ProgramType.MAP}, + {mealType: MealType.REGULAR, program: ProgramType.MAP}, + {mealType: MealType.REGULAR, program: ProgramType.STS}, + ], + 2: [ + {mealType: MealType.REGULAR, program: ProgramType.STS}, + {mealType: MealType.REGULAR, program: ProgramType.MAP}, + ], + }) + + const routes = await StopService.getRoutes(); + + expect(Object.entries(routes).length).toBe(3); + expect(routes[0].length).toBe(0) + expect(routes[1].length).toBe(3) + expect(routes[2].length).toBe(2) + + expect(routes[1][2]).toMatchObject({ + routeNumber: 1, + routePosition: 2, + mealType: MealType.REGULAR, + program: ProgramType.STS + }); + + expect(routes[2][1]).toMatchObject({ + routeNumber: 2, + routePosition: 1, + mealType: MealType.REGULAR, + program: ProgramType.MAP + }); + }) + }); + + describe('saveRoutes()', () => { + it('should return routes, when routes move from all unassigned to all assigned', async () => { + await StopUtils.setupRoutes({ + 0: [ + {mealType: MealType.REGULAR, program: ProgramType.MAP}, + {mealType: MealType.REGULAR, program: ProgramType.STS}, + {mealType: MealType.REGULAR, program: ProgramType.STS}, + {mealType: MealType.REGULAR, program: ProgramType.MAP}, + ] + }) + + const saveRouteProps = { + 0: [], + 1: [1, 2, 3, 4] + } + + const routes = await StopService.saveRoutes(saveRouteProps); + + expect(Object.entries(routes).length).toBe(2); + expect(routes[0].length).toBe(0) + expect(routes[1].length).toBe(4) + }) + + it('should return routes, when routes move from all assigned to all unassigned', async () => { + await StopUtils.setupRoutes({ + 0: [], + 1: [ + {mealType: MealType.REGULAR, program: ProgramType.MAP}, + {mealType: MealType.REGULAR, program: ProgramType.STS}, + {mealType: MealType.REGULAR, program: ProgramType.STS}, + {mealType: MealType.REGULAR, program: ProgramType.MAP}, + ] + }) + + const saveRouteProps = { + 0: [1, 2, 3, 4] + } + + const routes = await StopService.saveRoutes(saveRouteProps); + + expect(Object.entries(routes).length).toBe(1); + expect(routes[0].length).toBe(4) + }) + + it('should return routes, when routes move from assigned to another assignment', async () => { + await StopUtils.setupRoutes({ + 0: [], + 1: [ + {mealType: MealType.REGULAR, program: ProgramType.MAP}, + {mealType: MealType.REGULAR, program: ProgramType.MAP}, + ], + 2: [ + {mealType: MealType.REGULAR, program: ProgramType.STS}, + {mealType: MealType.REGULAR, program: ProgramType.STS}, + ] + }) + + const saveRouteProps = { + 0: [], + 1: [3, 4], + 2: [1, 2] + } + + const routes = await StopService.saveRoutes(saveRouteProps); + + expect(Object.entries(routes).length).toBe(3); + expect(routes[0].length).toBe(0) + expect(routes[1].length).toBe(2) + expect(routes[2].length).toBe(2) + }) + + it('should return null, when not all routes are assigned', async () => { + await StopUtils.setupRoutes({ + 0: [ + {mealType: MealType.REGULAR, program: ProgramType.MAP}, + {mealType: MealType.REGULAR, program: ProgramType.MAP}, + {mealType: MealType.REGULAR, program: ProgramType.STS}, + {mealType: MealType.REGULAR, program: ProgramType.STS}, + ] + }) + + const saveRouteProps = { + 0: [1, 2], + 1: [3] + } + + const routes = await StopService.saveRoutes(saveRouteProps); + + expect(routes).toBeNull(); + }) + }) +}); diff --git a/backend/tests/stops/utils.ts b/backend/tests/stops/utils.ts new file mode 100644 index 00000000..f7c1a4db --- /dev/null +++ b/backend/tests/stops/utils.ts @@ -0,0 +1,55 @@ +import { AppDataSource } from '../../src/data-source'; +import { ClientEntity } from '../../src/entities/ClientEntity'; +import { MealType, ProgramType } from '../../src/types/enums'; +import { StopEntity } from '../../src/entities/StopEntity'; + +type CreateStopProps = { + mealType: MealType, + program: ProgramType +} + +export type CreateRoutesProps = { + [key: number]: CreateStopProps[]; +} + +export default class StopUtils { + static ClientRepository = AppDataSource.getRepository(ClientEntity); + static StopRepository = AppDataSource.getRepository(StopEntity) + + static setupRoutes = async (props: CreateRoutesProps) => { + for (const [column, data] of Object.entries(props)) { + for (let index = 0; index < data.length; index++) { + const { mealType, program } = data[index]; + const newClient = new ClientEntity(); + newClient.name = 'Firstname Lastname'; + newClient.email = `${new Date().getTime()}@email.com`; + newClient.phoneNumber = '(514) 000 0000'; + newClient.address = '1234 Test Address'; + newClient.mealType = mealType; + newClient.softDelete = false; + + if (program == ProgramType.MAP) { + newClient.sts = false; + newClient.map = true; + } + if (program == ProgramType.STS) { + newClient.sts = true; + newClient.map = false; + } + + const savedClient = await this.ClientRepository.save(newClient); + + const stop = new StopEntity(); + stop.routeNumber = parseInt(column); + stop.routePosition = 0; + if (parseInt(column) != 0){ + stop.routePosition = index; + } + stop.client = savedClient; + stop.mealType = mealType; + stop.program = program; + await this.StopRepository.save(stop); + } + } + } +} diff --git a/backend/tests/task/service.test.ts b/backend/tests/task/service.test.ts new file mode 100644 index 00000000..24cb8665 --- /dev/null +++ b/backend/tests/task/service.test.ts @@ -0,0 +1,82 @@ +import { + jest, + describe, + it, + beforeAll, + afterAll, + afterEach, + expect +} from '@jest/globals'; +import DataSourceHelper from '../data.utils'; +import { MealType, ProgramType } from '../../src/types/enums'; +import TaskUtils from './utils'; +import TaskService from '../../src/services/task'; + +describe('Task Service Unit Tests', () => { + // Before performing any tests, sets up the datasource and clears it + beforeAll(async () => { + await DataSourceHelper.setupDataSource(); + await DataSourceHelper.clearDataSource(); + }); + + // After performing all the tests, destroys the datasource + afterAll(async () => { + await DataSourceHelper.destroyDataSource(); + }); + + // After each test, clears the datasource + afterEach(async () => { + await DataSourceHelper.clearDataSource(); + }); + + describe('createTask()', () => { + it('should verify meals created and return task', async () => { + const volunteer = await TaskUtils.createVolunteer({}) + + await TaskUtils.setupRoutes({ + 0: [], + 1: [ + {mealType: MealType.REGULAR, program: ProgramType.MAP}, + {mealType: MealType.REGULAR, program: ProgramType.STS}, + {mealType: MealType.REGULAR, program: ProgramType.MAP}, + ] + }) + const task = await TaskService.createTask(volunteer.id, 1) + + expect(task).toMatchObject({ + isCompleted: false, + volunteer: volunteer, + }); + + expect(task.deliveries[0]).toMatchObject({ + program: ProgramType.MAP, + mealType: MealType.REGULAR, + routePosition: 0 + }); + + expect(task.deliveries[1]).toMatchObject({ + program: ProgramType.STS, + mealType: MealType.REGULAR, + routePosition: 1 + }); + + expect(task.deliveries[2]).toMatchObject({ + program: ProgramType.MAP, + mealType: MealType.REGULAR, + routePosition: 2 + }); + + expect(task.deliveries[0].client).toMatchObject({ + id: 1 + }); + + expect(task.deliveries[1].client).toMatchObject({ + id: 2 + }); + + expect(task.deliveries[2].client).toMatchObject({ + id: 3 + }); + }); + }); +}); diff --git a/backend/tests/task/task.utils.ts b/backend/tests/task/task.utils.ts deleted file mode 100644 index 0727f28e..00000000 --- a/backend/tests/task/task.utils.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { TaskEntity } from '../../src/entities/TaskEntity'; -import { Repository } from 'typeorm'; -import { MealDeliveryEntity } from '../../src/entities/MealDeliveryEntity'; - -export default class TaskEntityHelper { - TaskRepository: Repository; - - constructor(repository: Repository) { - this.TaskRepository = repository; - } - - createTask = async ( - deliveries: MealDeliveryEntity[], - isCompleted: boolean, - ) => { - const newTask = new TaskEntity(); - newTask.deliveries = deliveries; - newTask.isCompleted = isCompleted; - return await this.TaskRepository.save(newTask); - }; -} diff --git a/backend/tests/task/tasks.test.ts b/backend/tests/task/tasks.test.ts deleted file mode 100644 index a4c1e86d..00000000 --- a/backend/tests/task/tasks.test.ts +++ /dev/null @@ -1,111 +0,0 @@ -import { describe, it } from '@jest/globals'; -import { TaskEntity } from '../../src/entities/TaskEntity'; -import { AppDataSource } from '../../src/data-source'; -import * as request from 'supertest'; -import TaskEntityHelper from './task.utils'; -import DataSourceHelper from '../data.utils'; - -import app from '../../src/app'; -import { StatusCode } from '../../src/controllers/statusCode'; -import MealDeliveryEntityHelper from '../mealDelivery/mealDelivery.utils'; -import { MealDeliveryEntity } from '../../src/entities/MealDeliveryEntity'; -import { MealType, ProgramType } from '../../src/entities/types'; - -describe('Tasks tests', () => { - const taskRepository = AppDataSource.getRepository(TaskEntity); - const taskHelper = new TaskEntityHelper(taskRepository); - const mealDeliveryHelper = new MealDeliveryEntityHelper( - AppDataSource.getRepository(MealDeliveryEntity) - ); - - // Before performing any tests, sets up the datasource and clears it - beforeAll(async () => { - await DataSourceHelper.setupDataSource(); - await DataSourceHelper.clearDataSource(); - }); - - // After performing all the tests, destroys the datasource - afterAll(async () => { - await DataSourceHelper.destroyDataSource(); - }); - - // After each test, clears the datasource - afterEach(async () => { - await DataSourceHelper.clearDataSource(); - }); - - it('should return task not found', async () => { - const res = await request(app).get('/api/tasks/1'); - expect(res.status).toBe(StatusCode.BAD_REQUEST); - expect(res.body).toEqual({ - task: null - }); - }); - - it('should return a task', async () => { - const savedTask = await taskHelper.createTask([], false); - const res = await request(app).get(`/api/tasks/${savedTask.id}`); - expect(res.status).toBe(StatusCode.OK); - expect(res.body).toEqual({ - task: { - id: savedTask.id, - isCompleted: false, - date: null, - deliveries: [], - volunteer: null - } - }); - }); - - it('should return tasks', async () => { - const savedTaskOne = await taskHelper.createTask([], false); - const savedTaskTwo = await taskHelper.createTask([], false); - const res = await request(app).get(`/api/tasks`); - expect(res.statusCode).toBe(200); - expect(res.body).toEqual({ - tasks: [ - { - id: expect.any(Number), - date: null, - isCompleted: false, - deliveries: [], - volunteer: null - }, - { - id: expect.any(Number), - date: null, - isCompleted: false, - deliveries: [], - volunteer: null - } - ] - }); - }); - - // Will be replaced with create task from route - // it('should create a task', async () => { - // const date: Date = new Date('April 20, 2001 04:20:00'); - // const res = await request(app).put(`/api/tasks`).send({ - // date: date.toISOString(), - // isCompleted: false, - // }); - // expect(res.statusCode).toBe(StatusCode.OK); - // expect(res.body).toEqual({ - // task: { - // id: expect.any(Number), - // date: date.toISOString(), - // isCompleted: false, - // deliveries: [] - // } - // }); - // }); - - it('should delete task', async () => { - await DataSourceHelper.clearDataSource(); - const date: Date = new Date('April 20, 2001 04:20:00'); - const savedTask = await taskHelper.createTask([], false); - const res = await request(app).delete(`/api/tasks/${savedTask.id}`); - expect(res.status).toBe(StatusCode.OK); - expect(res.body).toEqual({}); - }); -}); diff --git a/backend/tests/task/utils.ts b/backend/tests/task/utils.ts new file mode 100644 index 00000000..62dec506 --- /dev/null +++ b/backend/tests/task/utils.ts @@ -0,0 +1,93 @@ +import { AppDataSource } from '../../src/data-source'; +import { VolunteerEntity } from '../../src/entities/VolunteerEntity'; +import * as bcrypt from 'bcryptjs'; +import { ClientEntity } from '../../src/entities/ClientEntity'; +import { StopEntity } from '../../src/entities/StopEntity'; +import { MealType, ProgramType } from '../../src/types/enums'; +import { Volunteer } from '../../src/types/volunteer'; + +type CreateVolunteerProps = { + name?: string, + email?: string, + phoneNumber?: string, + password?: string, + startDate?: Date, + softDelete?: boolean +} + +type CreateStopProps = { + mealType: MealType, + program: ProgramType +} + +type CreateRoutesProps = { + [key: number]: CreateStopProps[]; +} + +export default class TaskUtils { + static VolunteerRepository = AppDataSource.getRepository(VolunteerEntity); + static ClientRepository = AppDataSource.getRepository(ClientEntity); + static StopRepository = AppDataSource.getRepository(StopEntity) + + static createVolunteer = async (props: Partial = {}) => { + const mergedProps: CreateVolunteerProps = { + name: 'Firstname Lastname', + email: `${new Date().getTime()}@email.com`, + phoneNumber: '(514) 000 0000', + password: 'password', + startDate: new Date(), + softDelete: false, + ...props + }; + + const newVolunteer = new VolunteerEntity(); + newVolunteer.name = mergedProps.name; + newVolunteer.email = mergedProps.email; + newVolunteer.phoneNumber = mergedProps.phoneNumber; + newVolunteer.password = await bcrypt.hash(mergedProps.password, 10); + newVolunteer.startDate = mergedProps.startDate; + newVolunteer.softDelete = mergedProps.softDelete; + + const savedVolunteer = await this.VolunteerRepository.save(newVolunteer); + + return savedVolunteer + } + + static setupRoutes = async (props: CreateRoutesProps) => { + for (const [column, data] of Object.entries(props)) { + for (let index = 0; index < data.length; index++) { + const { mealType, program } = data[index]; + const newClient = new ClientEntity(); + newClient.name = 'Firstname Lastname'; + newClient.email = `${new Date().getTime()}@email.com`; + newClient.phoneNumber = '(514) 000 0000'; + newClient.address = '1234 Test Address'; + newClient.mealType = mealType; + newClient.softDelete = false; + + if (program == ProgramType.MAP) { + newClient.sts = false; + newClient.map = true; + } + if (program == ProgramType.STS) { + newClient.sts = true; + newClient.map = false; + } + + const savedClient = await this.ClientRepository.save(newClient); + + const stop = new StopEntity(); + stop.routeNumber = parseInt(column); + stop.routePosition = 0; + if (parseInt(column) != 0){ + stop.routePosition = index; + } + stop.client = savedClient; + stop.mealType = mealType; + stop.program = program; + await this.StopRepository.save(stop); + } + } + } + +} diff --git a/backend/tests/volunteer/service.test.ts b/backend/tests/volunteer/service.test.ts new file mode 100644 index 00000000..1eb7380b --- /dev/null +++ b/backend/tests/volunteer/service.test.ts @@ -0,0 +1,181 @@ +import { + jest, + describe, + it, + beforeAll, + afterAll, + afterEach, + expect +} from '@jest/globals'; +import DataSourceHelper from '../data.utils'; +import VolunteerService from '../../src/services/volunteer'; +import * as jwt from 'jsonwebtoken'; +import VolunteerUtils from './utils'; +require('dotenv').config(); +const TOKEN_KEY = process.env.TOKEN_KEY; + +describe('Volunteer Service Unit Tests', () => { + // Before performing any tests, sets up the datasource and clears it + beforeAll(async () => { + await DataSourceHelper.setupDataSource(); + await DataSourceHelper.clearDataSource(); + }); + + // After performing all the tests, destroys the datasource + afterAll(async () => { + await DataSourceHelper.destroyDataSource(); + }); + + // After each test, clears the datasource + afterEach(async () => { + await DataSourceHelper.clearDataSource(); + }); + + describe('login()', () => { + it('should return error, when credentials are invalid', async () => { + const volunteer = await VolunteerUtils.createVolunteer({ + email: `volunteer@email.com`, + password: 'volunteer@password' + }) + + await expect(VolunteerService.login(`invalid@email.com`, 'invalid@password')).rejects.toThrow(); + + }); + + it('should return token, when credentials are valid', async () => { + const volunteer = await VolunteerUtils.createVolunteer({ + email: `volunteer@email.com`, + password: 'volunteer@password' + }) + + const token = await VolunteerService.login(`volunteer@email.com`, 'volunteer@password') + + const decodedToken = jwt.verify(token, TOKEN_KEY) as {email: string;}; + expect(decodedToken.email).toBe('volunteer@email.com'); + }); + }); + + describe('getActiveVolunteers()', () => { + it('should return no volunteers, when there are no volunteers', async () => { + const volunteers = await VolunteerService.getActiveVolunteers() + expect(volunteers).toEqual([]); + }); + + it('should return no volunteers, when there are only deactivated volunteers', async () => { + await VolunteerUtils.createVolunteer({softDelete: true}); + await VolunteerUtils.createVolunteer({softDelete: true}); + + const volunteers = await VolunteerService.getActiveVolunteers() + expect(volunteers).toEqual([]); + }); + + it('should return only active volunteers, when there are both active and deactivated volunteers', async () => { + await VolunteerUtils.createVolunteer({softDelete: true}); + const activeVolunteer = await VolunteerUtils.createVolunteer(); + const volunteers = await VolunteerService.getActiveVolunteers() + + expect(volunteers).toEqual([activeVolunteer]); + }); + }); + + describe('getAllVoluneers()', () => { + it('should return no volunteers, when there are no volunteers', async () => { + const volunteers = await VolunteerService.getAllVolunteers() + expect(volunteers).toEqual([]); + }); + + it('should return all volunteers, when there are both active and deactivated volunteers', async () => { + const volunteer1 = await VolunteerUtils.createVolunteer({softDelete: true}); + const volunteer2 = await VolunteerUtils.createVolunteer(); + const volunteers = await VolunteerService.getAllVolunteers() + + expect(volunteers).toEqual([volunteer1, volunteer2]); + }); + }) + + describe('getVolunteer()', () => { + it("should return no volunteers, when it doesn't exist", async () => { + const volunteer = await VolunteerService.getVolunteer(1) + expect(volunteer).toEqual(null); + }); + + it('should return volunteer, when it does exist', async () => { + await VolunteerUtils.createVolunteer({ + name: 'Firstname Lastname', + email: `volunteer@email.com`, + phoneNumber: '(514) 111 1111' + }); + const volunteer = await VolunteerService.getVolunteer(1); + + expect(volunteer).toMatchObject({ + id: 1, + name: 'Firstname Lastname', + email: `volunteer@email.com`, + phoneNumber: '(514) 111 1111' + }); + }); + }) + + describe('createVolunteer()', () => { + it("should return volunteer", async () => { + const startDate = new Date() + const volunteer = await VolunteerService.createVolunteer({ + name: "Firstname Lastname", + email: "email@email.com", + phoneNumber: '(514) 000 0000', + password: 'password', + startDate: startDate + }) + + expect(volunteer).toMatchObject({ + id: 1, + name: "Firstname Lastname", + email: "email@email.com", + phoneNumber: '(514) 000 0000', + softDelete: false, + availabilities: null, + availabilitiesLastUpdated: null, + preferredNeighbourhoods: null, + profilePicture: null, + startDate: startDate, + token: null + }); + }); + }) + + describe('updateVolunteer()', () => { + it("should return no volunteer, when it doesn't exist", async () => { + const volunteer = await VolunteerService.getVolunteer(1) + expect(volunteer).toEqual(null); + }); + + it("should return updated volunteer, when it does exist", async () => { + const startDate = new Date() + const volunteer = await VolunteerService.createVolunteer({ + name: "Firstname Lastname", + email: "email@email.com", + phoneNumber: '(514) 000 0000', + password: 'password', + startDate: startDate + }) + + const updatedVolunteer = await VolunteerService.updateVolunteer(1, { + email: "newemail@email.com", + }) + + expect(updatedVolunteer).toMatchObject({ + id: 1, + name: "Firstname Lastname", + email: "newemail@email.com", + phoneNumber: '(514) 000 0000', + softDelete: false, + availabilities: null, + availabilitiesLastUpdated: null, + preferredNeighbourhoods: null, + profilePicture: null, + startDate: startDate, + token: null + }); + }); + }) +}); diff --git a/backend/tests/volunteer/utils.ts b/backend/tests/volunteer/utils.ts new file mode 100644 index 00000000..85ce4649 --- /dev/null +++ b/backend/tests/volunteer/utils.ts @@ -0,0 +1,40 @@ +import { AppDataSource } from '../../src/data-source'; +import { VolunteerEntity } from '../../src/entities/VolunteerEntity'; +import * as bcrypt from 'bcryptjs'; + +type CreateVolunteerProps = { + name?: string, + email?: string, + phoneNumber?: string, + password?: string, + startDate?: Date, + softDelete?: boolean +} + +export default class VolunteerUtils { + static VolunteerRepository = AppDataSource.getRepository(VolunteerEntity); + + static createVolunteer = async (props: Partial = {}) => { + const mergedProps: CreateVolunteerProps = { + name: 'Firstname Lastname', + email: `${new Date().getTime()}@email.com`, + phoneNumber: '(514) 000 0000', + password: 'password', + startDate: new Date(), + softDelete: false, + ...props + }; + + const newVolunteer = new VolunteerEntity(); + newVolunteer.name = mergedProps.name; + newVolunteer.email = mergedProps.email; + newVolunteer.phoneNumber = mergedProps.phoneNumber; + newVolunteer.password = await bcrypt.hash(mergedProps.password, 10); + newVolunteer.startDate = mergedProps.startDate; + newVolunteer.softDelete = mergedProps.softDelete; + + const savedVolunteer = await this.VolunteerRepository.save(newVolunteer); + + return savedVolunteer + } +} diff --git a/backend/tests/volunteer/volunteers.test.ts b/backend/tests/volunteer/volunteers.test.ts deleted file mode 100644 index 76e148b9..00000000 --- a/backend/tests/volunteer/volunteers.test.ts +++ /dev/null @@ -1,264 +0,0 @@ -import { describe, it } from '@jest/globals'; -import { VolunteerEntity } from '../../src/entities/VolunteerEntity'; -import { AppDataSource } from '../../src/data-source'; -import * as request from 'supertest'; -import VolunteerEntityHelper from './volunteers.utils'; -import DataSourceHelper from '../data.utils'; - -import app from '../../src/app'; -import { StatusCode } from '../../src/controllers/statusCode'; -import { TaskEntity } from '../../src/entities/TaskEntity'; -import TaskEntityHelper from '../task/task.utils'; - -describe('Volunteers tests', () => { - const VolunteerRepository = AppDataSource.getRepository(VolunteerEntity); - const volunteerHelper = new VolunteerEntityHelper(VolunteerRepository); - const TaskRepository = AppDataSource.getRepository(TaskEntity); - const taskHelper = new TaskEntityHelper(TaskRepository); - - // Before performing any tests, sets up the datasource and clears it - beforeAll(async () => { - await DataSourceHelper.setupDataSource(); - await DataSourceHelper.clearDataSource(); - }); - - // After performing all the tests, destroys the datasource - afterAll(async () => { - await DataSourceHelper.destroyDataSource(); - }); - - // After each test, clears the datasource - afterEach(async () => { - await DataSourceHelper.clearDataSource(); - }); - - it('should return no volunteers', async () => { - const res = await request(app).get('/api/volunteers'); - expect(res.status).toBe(StatusCode.OK); - expect(res.body).toEqual({ - volunteers: [] - }); - }); - - it('should return all volunteers', async () => { - const date: Date = new Date('April 20, 2001 04:20:00'); - const lastUpdated: Date = new Date('April 20, 2002 04:20:00'); - volunteerHelper.createVolunteer( - 'name1', - 'email1', - '0123456789', - 'password1', - lastUpdated.toISOString(), - date.toISOString(), - 'link to profile', - '', - [] - ); - const res = await request(app).get('/api/volunteers'); - expect(res.status).toBe(StatusCode.OK); - expect(res.body).toEqual({ - volunteers: [ - { - availabilities: '', - email: 'email1', - phoneNumber: '0123456789', - id: 1, - name: 'name1', - profilePicture: 'link to profile', - availabilitiesLastUpdated: lastUpdated.toISOString(), - startDate: date.toISOString(), - password: 'password1', - tasks: [], - token: null - } - ] - }); - }); - - it('should return a volunteer', async () => { - const date: Date = new Date('April 20, 2001 04:20:00'); - const lastUpdated: Date = new Date('April 20, 2002 04:20:00'); - const volunteer = await volunteerHelper.createVolunteer( - 'name1', - 'email1', - '0123456789', - 'password1', - lastUpdated.toISOString(), - date.toISOString(), - 'link to profile', - '', - [] - ); - const res = await request(app).get(`/api/volunteers/${volunteer.id}`); - expect(res.status).toBe(StatusCode.OK); - expect(res.body).toEqual({ - volunteer: { - availabilities: '', - email: 'email1', - phoneNumber: '0123456789', - id: 1, - name: 'name1', - profilePicture: 'link to profile', - availabilitiesLastUpdated: lastUpdated.toISOString(), - startDate: date.toISOString(), - password: 'password1', - tasks: [], - token: null - } - }); - }); - - // it('should return a task', async () => { - // const date: Date = new Date('April 20, 2001 04:20:00'); - // const savedVolunteer = await taskHelper.createTask( - // date.toISOString(), - // [], - // false - // ); - // const res = await request(app).get(`/api/tasks/${savedVolunteer.id}`); - // expect(res.status).toBe(StatusCode.OK); - // expect(res.body).toEqual({ - // task: { - // id: savedVolunteer.id, - // deliveryTime: date.toISOString(), - // isCompleted: false, - // deliveries: [] - // } - // }); - // }); - - // it('should return tasks', async () => { - // const date: Date = new Date('April 20, 2001 04:20:00'); - // const savedVolunteerOne = await taskHelper.createTask( - // date.toISOString(), - // [], - // false - // ); - // const savedVolunteerTwo = await taskHelper.createTask( - // date.toISOString(), - // [], - // false - // ); - // const res = await request(app).get(`/api/tasks`); - // expect(res.statusCode).toBe(200); - // expect(res.body).toEqual({ - // tasks: [ - // { - // id: expect.any(Number), - // deliveryTime: date.toISOString(), - // isCompleted: false, - // deliveries: [] - // }, - // { - // id: expect.any(Number), - // deliveryTime: date.toISOString(), - // isCompleted: false, - // deliveries: [] - // } - // ] - // }); - // }); - - // it('should create a task', async () => { - // const date: Date = new Date('April 20, 2001 04:20:00'); - // const res = await request(app).put(`/api/tasks`).send({ - // deliveryTime: date.toISOString(), - // isCompleted: false, - // deliveries: [] - // }); - // expect(res.statusCode).toBe(200); - // expect(res.body).toEqual({ - // task: { - // id: expect.any(Number), - // deliveryTime: date.toISOString(), - // isCompleted: false, - // deliveries: [] - // } - // }); - // }); - - // it('should update a test', async () => { - // const date: Date = new Date('April 20, 2001 04:20:00'); - // const newDate: Date = new Date('April 21, 2001 04:20:00'); - // const newMealDelivery = await mealDeliveryHelper.createMealDelivery( - // 1, - // 'breakfast', - // null - // ); - // const savedVolunteer = await taskHelper.createTask( - // date.toISOString(), - // [newMealDelivery], - // false - // ); - // const res = await request(app).put(`/api/tasks/${savedVolunteer.id}`).send({ - // deliveryTime: newDate.toISOString(), - // isCompleted: true, - // deliveries: [] - // }); - // expect(res.statusCode).toBe(200); - // expect(res.body).toEqual({ - // task: { - // id: expect.any(Number), - // deliveryTime: newDate.toISOString(), - // isCompleted: true, - // deliveries: [] - // } - // }); - // // make sure mealDelivery's task is unset - // expect(newMealDelivery.task).toBeNull; - // }); - - it('should get volunteer tasks', async () => { - const date: Date = new Date('April 20, 2001 04:20:00'); - const lastUpdated: Date = new Date('April 20, 2002 04:20:00'); - const savedTask = await taskHelper.createTask([], false); - - const savedVolunteer = await volunteerHelper.createVolunteer( - 'name1', - 'email1', - '0123456789', - 'password1', - lastUpdated.toISOString(), - date.toISOString(), - 'link to profile', - '', - [savedTask] - ); - - const res = await request(app).get( - `/api/volunteers/${savedVolunteer.id}/tasks` - ); - expect(res.status).toBe(StatusCode.OK); - expect(res.body).toEqual({ - tasks: [ - { - deliveries: [], - date: null, - id: 1, - isCompleted: false - } - ] - }); - }); - - it('should delete task', async () => { - const date: Date = new Date('April 20, 2001 04:20:00'); - const lastUpdated: Date = new Date('April 20, 2002 04:20:00'); - const savedVolunteer = await volunteerHelper.createVolunteer( - 'name1', - 'email1', - '0123456789', - 'password1', - lastUpdated.toISOString(), - date.toISOString(), - 'link to profile', - '', - [] - ); - const res = await request(app).delete( - `/api/volunteers/${savedVolunteer.id}` - ); - expect(res.status).toBe(StatusCode.OK); - expect(res.body).toEqual({}); - }); -}); diff --git a/backend/tests/volunteer/volunteers.utils.ts b/backend/tests/volunteer/volunteers.utils.ts deleted file mode 100644 index 3658d912..00000000 --- a/backend/tests/volunteer/volunteers.utils.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { Repository } from 'typeorm'; -import { TaskEntity } from '../../src/entities/TaskEntity'; -import { DayOfWeek, VolunteerEntity } from '../../src/entities/VolunteerEntity'; - -export default class VolunteerEntityHelper { - VolunteerRepository: Repository; - - constructor(repository: Repository) { - this.VolunteerRepository = repository; - } - - createVolunteer = async ( - name: string, - email: string, - phoneNumber: string, - password: string, - availabilitiesLastUpdated: string, - startDate: string, - profilePicture: string, - availabilities: string, - tasks: TaskEntity[] - ) => { - const newVolunteer = new VolunteerEntity(); - newVolunteer.email = email; - newVolunteer.phoneNumber = phoneNumber; - newVolunteer.name = name; - newVolunteer.password = password; - newVolunteer.availabilitiesLastUpdated = new Date(availabilitiesLastUpdated); - newVolunteer.startDate = new Date(startDate); - newVolunteer.profilePicture = profilePicture; - newVolunteer.availabilities = availabilities; - newVolunteer.tasks = tasks; - return await this.VolunteerRepository.save(newVolunteer); - }; -} diff --git a/frontend-admin/package-lock.json b/frontend-admin/package-lock.json index c5005d27..ae22ec3d 100644 --- a/frontend-admin/package-lock.json +++ b/frontend-admin/package-lock.json @@ -8,6 +8,9 @@ "name": "frontend-admin", "version": "0.0.0", "dependencies": { + "@dnd-kit/core": "^6.0.8", + "@dnd-kit/sortable": "^7.0.2", + "@dnd-kit/utilities": "^3.2.1", "@emotion/react": "^11.10.6", "@emotion/styled": "^11.10.6", "@mui/icons-material": "^5.11.16", @@ -20,6 +23,7 @@ "material-ui-phone-number-2": "^1.3.0", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-router-dom": "^6.17.0", "universal-cookie": "^4.0.4", "vite-plugin-svgr": "^2.4.0", "zustand": "^4.3.7" @@ -464,6 +468,55 @@ } } }, + "node_modules/@dnd-kit/accessibility": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.0.1.tgz", + "integrity": "sha512-HXRrwS9YUYQO9lFRc/49uO/VICbM+O+ZRpFDe9Pd1rwVv2PCNkRiTZRdxrDgng/UkvdC3Re9r2vwPpXXrWeFzg==", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/core": { + "version": "6.0.8", + "resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.0.8.tgz", + "integrity": "sha512-lYaoP8yHTQSLlZe6Rr9qogouGUz9oRUj4AHhDQGQzq/hqaJRpFo65X+JKsdHf8oUFBzx5A+SJPUvxAwTF2OabA==", + "dependencies": { + "@dnd-kit/accessibility": "^3.0.0", + "@dnd-kit/utilities": "^3.2.1", + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/sortable": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@dnd-kit/sortable/-/sortable-7.0.2.tgz", + "integrity": "sha512-wDkBHHf9iCi1veM834Gbk1429bd4lHX4RpAwT0y2cHLf246GAvU2sVw/oxWNpPKQNQRQaeGXhAVgrOl1IT+iyA==", + "dependencies": { + "@dnd-kit/utilities": "^3.2.0", + "tslib": "^2.0.0" + }, + "peerDependencies": { + "@dnd-kit/core": "^6.0.7", + "react": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/utilities": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.1.tgz", + "integrity": "sha512-OOXqISfvBw/1REtkSK2N3Fi2EQiLMlWUlqnOK/UpOISqBZPWpE6TqL+jcPtMOkE8TqYGiURvRdPSI9hltNUjEA==", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, "node_modules/@emotion/babel-plugin": { "version": "11.10.6", "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.10.6.tgz", @@ -1333,6 +1386,14 @@ "url": "https://opencollective.com/popperjs" } }, + "node_modules/@remix-run/router": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.10.0.tgz", + "integrity": "sha512-Lm+fYpMfZoEucJ7cMxgt4dYt8jLfbpwRCzAjm9UgSLOkmlqo9gupxt6YX3DY0Fk155NT9l17d/ydi+964uS9Lw==", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/@rollup/pluginutils": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.0.2.tgz", @@ -2401,6 +2462,36 @@ "node": ">=0.10.0" } }, + "node_modules/react-router": { + "version": "6.17.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.17.0.tgz", + "integrity": "sha512-YJR3OTJzi3zhqeJYADHANCGPUu9J+6fT5GLv82UWRGSxu6oJYCKVmxUcaBQuGm9udpWmPsvpme/CdHumqgsoaA==", + "dependencies": { + "@remix-run/router": "1.10.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/react-router-dom": { + "version": "6.17.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.17.0.tgz", + "integrity": "sha512-qWHkkbXQX+6li0COUUPKAUkxjNNqPJuiBd27dVwQGDNsuFBdMbrS6UZ0CLYc4CsbdLYTckn4oB4tGDuPZpPhaQ==", + "dependencies": { + "@remix-run/router": "1.10.0", + "react-router": "6.17.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, "node_modules/react-transition-group": { "version": "4.4.5", "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", @@ -2537,6 +2628,11 @@ "node": ">=4" } }, + "node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + }, "node_modules/typescript": { "version": "4.9.5", "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", diff --git a/frontend-admin/package.json b/frontend-admin/package.json index 2ed223c6..712e2d5d 100644 --- a/frontend-admin/package.json +++ b/frontend-admin/package.json @@ -9,6 +9,9 @@ "preview": "vite preview" }, "dependencies": { + "@dnd-kit/core": "^6.0.8", + "@dnd-kit/sortable": "^7.0.2", + "@dnd-kit/utilities": "^3.2.1", "@emotion/react": "^11.10.6", "@emotion/styled": "^11.10.6", "@mui/icons-material": "^5.11.16", @@ -21,6 +24,7 @@ "material-ui-phone-number-2": "^1.3.0", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-router-dom": "^6.17.0", "universal-cookie": "^4.0.4", "vite-plugin-svgr": "^2.4.0", "zustand": "^4.3.7" diff --git a/frontend-admin/src/api/auth.ts b/frontend-admin/src/api/auth.ts index f40f41a8..78987fd8 100644 --- a/frontend-admin/src/api/auth.ts +++ b/frontend-admin/src/api/auth.ts @@ -1,9 +1,8 @@ import AxiosInstance from "./axios"; -export const login = async (data: any) => { - console.log("hello"); - console.log(data); - const response = await AxiosInstance.post("/admin-login", data); - console.log(response); - return response; -}; +export default class AuthenticationAPI { + static login = async (data: any) => { + const response = await AxiosInstance.post("/admin-login", data); + return response; + }; +} diff --git a/frontend-admin/src/api/axios.ts b/frontend-admin/src/api/axios.ts index fdcc5a34..14a86594 100644 --- a/frontend-admin/src/api/axios.ts +++ b/frontend-admin/src/api/axios.ts @@ -2,7 +2,7 @@ import axios from "axios"; const AxiosInstance = axios.create({ baseURL: "http://localhost:3001/api", - withCredentials: false, + withCredentials: true, }); AxiosInstance.interceptors.response.use( diff --git a/frontend-admin/src/api/clients.ts b/frontend-admin/src/api/clients.ts index 8b71a6c1..52e5408c 100644 --- a/frontend-admin/src/api/clients.ts +++ b/frontend-admin/src/api/clients.ts @@ -1,39 +1,43 @@ import AxiosInstance from "./axios"; -export const getClients = async () => { - const response = await AxiosInstance({ - method: "get", - url: "/clients", - }); +class ClientAPI { + static getClients = async () => { + const response = await AxiosInstance({ + method: "get", + url: "/clients", + }); + + return response; + }; - return response; -}; + static getClient = async (id: number) => { + const response = await AxiosInstance({ + method: "get", + url: "/clients/" + id.toString(), + }); + return response; + }; -export const getClient = async (id: number) => { - const response = await AxiosInstance({ - method: "get", - url: "/clients/" + id.toString(), - }); - return response; -}; + static editClient = async (props: { id: number; data: any }) => { + console.log("editing client"); + console.log(props); + + const response = await AxiosInstance({ + method: "put", + url: "/clients/" + props.id.toString() + "/edit", + data: props.data, + }); + return response; + }; -export const editClient = async (props: { id: number; data: any }) => { - console.log("editing client"); - console.log(props); + static createClient = async (data: any) => { + const response = await AxiosInstance({ + method: "post", + url: "/clients", + data: data, + }); + return response; + }; +} - const response = await AxiosInstance({ - method: "put", - url: "/clients/" + props.id.toString() + "/edit", - data: props.data, - }); - return response; -}; - -export const createClient = async (data: any) => { - const response = await AxiosInstance({ - method: "post", - url: "http://localhost:3001/api/clients", - data: data, - }); - return response; -}; +export default ClientAPI \ No newline at end of file diff --git a/frontend-admin/src/api/route-deliveries.ts b/frontend-admin/src/api/route-deliveries.ts index 9dc0e302..13fa1312 100644 --- a/frontend-admin/src/api/route-deliveries.ts +++ b/frontend-admin/src/api/route-deliveries.ts @@ -1,38 +1,24 @@ import AxiosInstance from './axios'; -export const getRouteDeliveries = async () => { - const response = await AxiosInstance({ - method: "get", - url: "/route_delivery", - }); - - return response -} - -export const setRouteDeliveryNumber = async (id: number, routeNumber: number) => { - const response = await AxiosInstance({ - method: "put", - url: "/route_delivery/" + id + "/set", - data: {routeNumber: routeNumber} - }); - - return response -} +class RouteDeliveryAPI { + static getRouteDeliveries = async () => { + const response = await AxiosInstance({ + method: "get", + url: "/route_delivery", + }); + + return response + } -export const increaseRouteDeliveryPosition = async (id: number) => { - const response = await AxiosInstance({ - method: "put", - url: "/route_delivery/" + id + "/increment" - }); - - return response + static saveAllRouteDeliveries = async (editRoutes: any) => { + const response = await AxiosInstance({ + method: "post", + url: "/route_delivery", + data: {routes: editRoutes} + }); + + return response + } } -export const decreaseRouteDeliveryPosition = async (id: number) => { - const response = await AxiosInstance({ - method: "put", - url: "/route_delivery/" + id + "/decrement" - }); - - return response -} +export default RouteDeliveryAPI \ No newline at end of file diff --git a/frontend-admin/src/api/tasks.ts b/frontend-admin/src/api/tasks.ts index b8eaecf5..0beca4c8 100644 --- a/frontend-admin/src/api/tasks.ts +++ b/frontend-admin/src/api/tasks.ts @@ -1,19 +1,31 @@ import AxiosInstance from './axios'; -export const getTasks = async () => { - const response = await AxiosInstance({ - method: "get", - url: "/tasks", - }); +class TaskAPI { + static getTasks = async () => { + const response = await AxiosInstance({ + method: "get", + url: "/tasks", + }); + + return response + } + + static getTaskMatchData = async () => { + const response = await AxiosInstance({ + method: "get", + url: "/tasks-match/", + }); + return response + } - return response + static saveTaskMatchData = async (matchData: any) => { + const response = await AxiosInstance({ + method: "post", + url: "/tasks-match/", + data: matchData + }); + return response + } } -export const createTask = async (data: any) => { - const response = await AxiosInstance({ - method: "post", - url: "/tasks", - data: data - }); - return response -} +export default TaskAPI; \ No newline at end of file diff --git a/frontend-admin/src/api/volunteers.ts b/frontend-admin/src/api/volunteers.ts index d768df26..3f624ba7 100644 --- a/frontend-admin/src/api/volunteers.ts +++ b/frontend-admin/src/api/volunteers.ts @@ -1,36 +1,40 @@ import AxiosInstance from './axios'; -export const getVolunteers = async () => { - const response = await AxiosInstance({ - method: "get", - url: "/volunteers", - }); - - return response -} +class VolunteerAPI { + static getVolunteers = async () => { + const response = await AxiosInstance({ + method: "get", + url: "/volunteers", + }); + + return response + } -export const getVolunteer = async (id: number) => { - const response = await AxiosInstance({ - method: "get", - url: "/volunteers/"+id.toString(), - }); - return response -} + static getVolunteer = async (id: number) => { + const response = await AxiosInstance({ + method: "get", + url: "/volunteers/"+id.toString(), + }); + return response + } -export const editVolunteer = async (props: {id: number, data: any}) => { - const response = await AxiosInstance({ - method: "put", - url: "/volunteers/"+props.id.toString()+"/edit", - data: props.data - }); - return response -} + static editVolunteer = async (props: {id: number, data: any}) => { + const response = await AxiosInstance({ + method: "put", + url: "/volunteers/"+props.id.toString()+"/edit", + data: props.data + }); + return response + } -export const createVolunteer = async (data: any) => { - const response = await AxiosInstance({ - method: "post", - url: "http://localhost:3001/api/volunteers", - data: data - }); - return response + static createVolunteer = async (data: any) => { + const response = await AxiosInstance({ + method: "post", + url: "/volunteers", + data: data + }); + return response + } } + +export default VolunteerAPI \ No newline at end of file diff --git a/frontend-admin/src/components/auth/page.tsx b/frontend-admin/src/components/auth/page.tsx index 3df94dec..145d77a8 100644 --- a/frontend-admin/src/components/auth/page.tsx +++ b/frontend-admin/src/components/auth/page.tsx @@ -3,7 +3,7 @@ import {Box, Grid, Paper, Link, Typography, TextField, Avatar, Button} from '@mu import LockOutlinedIcon from '@mui/icons-material/LockOutlined'; import madaImg from 'src/assets/mada.jpg'; import { useNavigate } from 'react-router-dom' -import {login} from 'src/api/auth'; +import AuthenticationAPI from 'src/api/auth'; import axios from 'axios'; import Cookies from "universal-cookie"; const cookies = new Cookies(); @@ -22,7 +22,7 @@ export default function LogInPage() { } try { - const response = await login({ + const response = await AuthenticationAPI.login({ password: formData.get('password'), email: formData.get('email'), }) diff --git a/frontend-admin/src/components/clients/modals/create.tsx b/frontend-admin/src/components/clients/modals/create.tsx index 16f7973f..8287ebd8 100644 --- a/frontend-admin/src/components/clients/modals/create.tsx +++ b/frontend-admin/src/components/clients/modals/create.tsx @@ -1,9 +1,9 @@ import React, { useState } from "react"; import { isAllValid, BaseModal } from "src/components/common/modal/modal"; -import { useStateSetupHandler } from "src/components/common/use-state-setup-handler"; -import { isValidEmail, isValidPhone } from "src/components/common/validators"; +import { useStateSetupHandler } from "src/components/common/modal/use-state-setup-handler"; +import { isValidEmail, isValidPhone } from "src/components/common/modal/validators"; import { useMutation, useQueryClient } from "@tanstack/react-query"; -import { createClient } from "src/api/clients"; +import ClientAPI from "src/api/clients"; import { ModalActionBar } from "src/components/common/modal/actionbar"; import { ModalSelectInput } from "src/components/common/modal/inputs/select"; import { ModalPhoneInput } from "src/components/common/modal/inputs/phone"; @@ -14,7 +14,7 @@ import { MealType } from "src/components/common/types"; export const CreateModal = (props: { handleClose: any }) => { const queryClient = useQueryClient(); - const mutation = useMutation(createClient, { + const mutation = useMutation(ClientAPI.createClient, { onSuccess: () => { // Invalidate and refetch queryClient.invalidateQueries("clients"); @@ -54,7 +54,7 @@ export const CreateModal = (props: { handleClose: any }) => { const valid = isAllValid([name, isValidEmail(email), isValidPhone(phone)]); return ( - + { const { data } = useQuery({ queryKey: ["clients", id], - queryFn: () => getClient(id), + queryFn: () => ClientAPI.getClient(id), }); const { @@ -67,7 +67,7 @@ export const EditModal = (props: { handleClose: any }) => { const queryClient = new QueryClient(); - const mutation = useMutation(editClient, { + const mutation = useMutation(ClientAPI.editClient, { onSuccess: () => { // Invalidate and refetch queryClient.invalidateQueries("clients"); diff --git a/frontend-admin/src/components/clients/page.tsx b/frontend-admin/src/components/clients/page.tsx index 5b722138..3b885cdb 100644 --- a/frontend-admin/src/components/clients/page.tsx +++ b/frontend-admin/src/components/clients/page.tsx @@ -1,21 +1,21 @@ import React from "react"; import { GridActionsCellItem, GridRowId } from "@mui/x-data-grid"; import { CreateModal, EditModal } from "./modals"; -import { getClients } from "src/api/clients"; +import ClientAPI from "src/api/clients"; import { useEditClientStore, EditClientState } from "./client.store"; import { useQuery } from "@tanstack/react-query"; import { clientColumns } from "./columns"; -import { useModalState } from "src/components/common/use-modal-state"; +import { useModalState } from "src/components/common/modal/use-modal-state"; import { ModalControl } from "src/components/common/modal/control"; -import { BasePage } from "src/components/common/base-page"; -import { ActionBar } from "src/components/common/page-actionbar"; +import { BasePage } from "src/components/common/layout/base-page"; +import { ActionBar } from "src/components/common/layout/page-actionbar"; import { DataGrid } from "@mui/x-data-grid"; import { Box } from "@mui/system"; const ClientsPage = () => { const { isLoading, isError, data, error } = useQuery(["clients"], () => - getClients() + ClientAPI.getClients() ); const { state: createModal, diff --git a/frontend-admin/src/components/common/base-page.tsx b/frontend-admin/src/components/common/layout/base-page.tsx similarity index 91% rename from frontend-admin/src/components/common/base-page.tsx rename to frontend-admin/src/components/common/layout/base-page.tsx index ba957a5a..40a2ec6f 100644 --- a/frontend-admin/src/components/common/base-page.tsx +++ b/frontend-admin/src/components/common/layout/base-page.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import {Container, Box} from '@mui/material'; -import NavigationDrawer from './drawer/drawer'; +import NavigationDrawer from '../drawer/drawer'; import {PageHeader} from './page-actionbar'; export const BasePage = (props: {header: any, children: any}) => { diff --git a/frontend-admin/src/components/common/page-actionbar.tsx b/frontend-admin/src/components/common/layout/page-actionbar.tsx similarity index 100% rename from frontend-admin/src/components/common/page-actionbar.tsx rename to frontend-admin/src/components/common/layout/page-actionbar.tsx diff --git a/frontend-admin/src/components/common/modal/inputs/boolean.tsx b/frontend-admin/src/components/common/modal/inputs/boolean.tsx index b3740ef1..47d80350 100644 --- a/frontend-admin/src/components/common/modal/inputs/boolean.tsx +++ b/frontend-admin/src/components/common/modal/inputs/boolean.tsx @@ -10,7 +10,7 @@ export const ModalBooleanInput = (props: ModalInputProps) => { <> {props.label} - + ) diff --git a/frontend-admin/src/components/common/use-modal-state.ts b/frontend-admin/src/components/common/modal/use-modal-state.ts similarity index 100% rename from frontend-admin/src/components/common/use-modal-state.ts rename to frontend-admin/src/components/common/modal/use-modal-state.ts diff --git a/frontend-admin/src/components/common/use-state-setup-handler.ts b/frontend-admin/src/components/common/modal/use-state-setup-handler.ts similarity index 100% rename from frontend-admin/src/components/common/use-state-setup-handler.ts rename to frontend-admin/src/components/common/modal/use-state-setup-handler.ts diff --git a/frontend-admin/src/components/common/validators.ts b/frontend-admin/src/components/common/modal/validators.ts similarity index 100% rename from frontend-admin/src/components/common/validators.ts rename to frontend-admin/src/components/common/modal/validators.ts diff --git a/frontend-admin/src/components/common/types.ts b/frontend-admin/src/components/common/types.ts index 86f3999a..bfa760f3 100644 --- a/frontend-admin/src/components/common/types.ts +++ b/frontend-admin/src/components/common/types.ts @@ -30,3 +30,22 @@ export enum Neighbourhood { VILLESTLAURENT = "Ville St-Laurent", WESTISLAND = "West Island", } + +// Create a reverse mapping object +const neighbourhoodReverseMapping: { [key: string]: Neighbourhood } = { + [Neighbourhood.COTEDENEIGES]: Neighbourhood.COTEDENEIGES, + [Neighbourhood.COTESTLUC]: Neighbourhood.COTESTLUC, + [Neighbourhood.DOWNTOWN]: Neighbourhood.DOWNTOWN, + [Neighbourhood.LACHINE]: Neighbourhood.LACHINE, + [Neighbourhood.LAVAL]: Neighbourhood.LAVAL, + [Neighbourhood.MONTREAL]: Neighbourhood.MONTREAL, + [Neighbourhood.MONTREALWEST]: Neighbourhood.MONTREALWEST, + [Neighbourhood.TMR]: Neighbourhood.TMR, + [Neighbourhood.VERDUN]: Neighbourhood.VERDUN, + [Neighbourhood.VILLESTLAURENT]: Neighbourhood.VILLESTLAURENT, + [Neighbourhood.WESTISLAND]: Neighbourhood.WESTISLAND, +}; + +export function getNeighborhoodFromString(str: string): Neighbourhood { + return neighbourhoodReverseMapping[str]; +} \ No newline at end of file diff --git a/frontend-admin/src/components/routes/board/list.tsx b/frontend-admin/src/components/routes/board/list.tsx deleted file mode 100644 index 0a22e43e..00000000 --- a/frontend-admin/src/components/routes/board/list.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import { - Grid, - Box, - Paper, - List, - ListItemButton, - ListItemText, -} from "@mui/material"; - -const BoardListItem = (props: { route: any; selectable?: any }) => { - const { id, client, program, mealType, routePosition } = props.route; - - const handleClick = () => { - if ( - props.selectable?.selectedRouteDelivery === null || - props.selectable?.selectedRouteDelivery.id !== id - ) { - console.log("setSelectedRouteDelivery(props.route)"); - props.selectable?.setSelectedRouteDelivery(props.route); - } else { - console.log("setSelectedRouteDelivery(null)"); - props.selectable?.setSelectedRouteDelivery(null); - } - }; - - const isSelected = - props.selectable?.selectedRouteDelivery === null - ? false - : props.selectable?.selectedRouteDelivery?.id === id; - - return ( - - - - ); -}; - -export const BoardList = (props: { - header: string; - routes: any; - selectable?: any; -}) => { - const { header } = props; - return ( - - {header} - - {props.routes && - props.routes.map((route: any) => ( - - ))} - - - ); -}; diff --git a/frontend-admin/src/components/routes/board/transfer.tsx b/frontend-admin/src/components/routes/board/transfer.tsx deleted file mode 100644 index f856d80b..00000000 --- a/frontend-admin/src/components/routes/board/transfer.tsx +++ /dev/null @@ -1,181 +0,0 @@ - -import React, {useEffect, useState} from 'react' -import {BoardList} from './list' -import {Grid, Box, Button, FormControl, Select, InputLabel, MenuItem} from '@mui/material' -import { - useQuery, - useMutation, - QueryClient -} from '@tanstack/react-query' -import {EditRouteButtons} from './transfer/route-buttons' -import {TransferButtons} from './transfer/transfer-buttons' -import {setRouteDeliveryNumber, getRouteDeliveries, increaseRouteDeliveryPosition, decreaseRouteDeliveryPosition} from 'src/api/route-deliveries' - -type routeDelivery = { - id: number - routeNumber: number - routePosition: number - program: string - mealType: string -} - -export const TransferBoard = () => { - const { isLoading, isError, data, error, refetch } = useQuery(['routeDeliveries'], () => getRouteDeliveries()) - - // List of route numbers available to transfer to - const [routeNumberList, setRouteNumberList] = useState([]) - // Route Number to transfer routeDelivery items to - const [routeNumber, setRouteNumber] = useState(-1) - // RouteDelivery items on list 2 - const [transferRoutes, setTransferRoutes] = useState([]) - // Unassigned items on list 2 - const [unassignedRoutes, setUnassignedRoutes] = useState([]) - // Selected RouteDelivery item - const [selectedRouteDelivery, setSelectedRouteDelivery] = useState(null) - const [disabledTransferLeft, setDisabledTransferLeft] = useState(true) - const [disabledTransferRight, setDisabledTransferRight] = useState(true) - - const queryClient = new QueryClient() - - const [allSavedRoutes, setAllSavedRoutes] = useState([]) - - useEffect(() => { - if (!data) return; - setAllSavedRoutes(data.data.routes) - setUnassignedRoutes(data.data.routes[0]) - setRouteNumberList(Object.keys(data.data.routes)) - if (routeNumber) { - setTransferRoutes(data.data.routes[routeNumber]) - } - }, [data]) - - const setRouteNumberMutation = useMutation({ - mutationFn: async (n: number) => await setRouteDeliveryNumber(selectedRouteDelivery!.id || 0, n), - onSuccess: async () => { - queryClient.invalidateQueries('routeDeliveries') - await refetch() - }, - }); - - const incrementRoutePositionMutation = useMutation({ - mutationFn: async () => await increaseRouteDeliveryPosition(selectedRouteDelivery!.id || 0), - onSuccess: async () => { - queryClient.invalidateQueries('routeDeliveries') - await refetch() - }, - }); - - const decrementRoutePositionMutation = useMutation({ - mutationFn: async () => await decreaseRouteDeliveryPosition(selectedRouteDelivery!.id || 0), - onSuccess: async () => { - queryClient.invalidateQueries('routeDeliveries') - await refetch() - }, - }); - - const handleChangeRouteNumber = (event: any) => { - setRouteNumber(event.target.value) - if (event.target.value in Object.keys(allSavedRoutes)) { - setTransferRoutes(allSavedRoutes[event.target.value]) - } else { - setTransferRoutes([]) - } - } - - // Button handlers for TransferButtons - const handleTransferLeft = async () => { - await setRouteNumberMutation.mutate(0) - handleSelectRouteDelivery(null) - } - - const handleTransferRight = async () => { - await setRouteNumberMutation.mutate(routeNumber) - handleSelectRouteDelivery(null) - } - - const handleIncrementPosition = async () => { - await incrementRoutePositionMutation.mutate() - } - - const handleDecrementPosition = async () => { - await decrementRoutePositionMutation.mutate() - } - - // Button handlers for EditRouteButtons - const handleCreateRoute = () => { - if (routeNumberList.length == 1 || - // new route number is not in the system yet, but the previous one is - !(routeNumberList.length in Object.keys(allSavedRoutes)) && - routeNumberList.length - 1 in Object.keys(allSavedRoutes) - ){ - setRouteNumberList([...routeNumberList, (routeNumberList.length).toString()]) - } - } - - const handleDeleteRoute = () => { - // check that theres nothign under route - console.log("delete route") - } - - const disabledDeleteRoute = true - - const handleSelectRouteDelivery = (route: any) => { - if (routeNumber === -1) { return } - - setSelectedRouteDelivery(route) - if (route === null) { - setDisabledTransferRight(true) - setDisabledTransferLeft(true) - } else if (route.routeNumber == 0) { - setDisabledTransferLeft(true) - setDisabledTransferRight(false) - } else if (route.routeNumber != 0) { - setDisabledTransferRight(true) - setDisabledTransferLeft(false) - } - } - - return ( - - - - - - - - - - - - ) -} \ No newline at end of file diff --git a/frontend-admin/src/components/routes/board/transfer/route-buttons.tsx b/frontend-admin/src/components/routes/board/transfer/route-buttons.tsx deleted file mode 100644 index e5bc0a12..00000000 --- a/frontend-admin/src/components/routes/board/transfer/route-buttons.tsx +++ /dev/null @@ -1,57 +0,0 @@ -import React, {useState} from 'react' -import {Box, Button} from '@mui/material' - -export const EditRouteButtons = (props: { - handleCreateRoute: any, - handleDeleteRoute: any, - disabledDeleteRoute: boolean, - handleIncrementPosition: any, - disabledIncrementPosition: boolean, - handleDecrementPosition: any, - disabledDecrementPosition: boolean, -}) => { - const { - handleCreateRoute, handleDeleteRoute, disabledDeleteRoute, - handleIncrementPosition, disabledIncrementPosition, - handleDecrementPosition, disabledDecrementPosition, - } = props - return ( - - - - - - - - - ) -} diff --git a/frontend-admin/src/components/routes/board/transfer/transfer-buttons.tsx b/frontend-admin/src/components/routes/board/transfer/transfer-buttons.tsx deleted file mode 100644 index 4e5dd576..00000000 --- a/frontend-admin/src/components/routes/board/transfer/transfer-buttons.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import React, {useState} from 'react' -import {Box, Button} from '@mui/material' - -export const TransferButtons = (props: { - handleTransferRight?: any, - disabledTransferRight?: boolean, - handleTransferLeft?: any, - disabledTransferLeft?: boolean -}) => { - const {handleTransferRight, disabledTransferRight, handleTransferLeft, disabledTransferLeft} = props - - return ( - - - - - ) -} diff --git a/frontend-admin/src/components/routes/board/view.tsx b/frontend-admin/src/components/routes/board/view.tsx deleted file mode 100644 index 6dee177e..00000000 --- a/frontend-admin/src/components/routes/board/view.tsx +++ /dev/null @@ -1,19 +0,0 @@ - -import React, {useState} from 'react' -import {BoardList} from './list' -import {Box} from '@mui/material' - -export const ViewBoard = (props: {groupedRoutes: any}) => { - return ( - - - { - Object.keys(props.groupedRoutes).map((key, index) => { - if (key !== "0") { - return - } - }) - } - - ) -} \ No newline at end of file diff --git a/frontend-admin/src/components/routes/columns.tsx b/frontend-admin/src/components/routes/columns.tsx deleted file mode 100644 index 58da4e69..00000000 --- a/frontend-admin/src/components/routes/columns.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import React from 'react' -import {GridColDef} from '@mui/x-data-grid'; - -function getClientName(params: any) { - if (params.row.client === null) return ''; - return `${params.row.client.name || ''}`; -} - -function getClientAddress(params: any) { - if (params.row.client === null) return ''; - return `${params.row.client.address || ''}`; -} - -export const routeColumns: GridColDef[] = [ - { - field: 'id', - type: 'number', - width: 20, - }, - { - field: 'routeNumber', - headerName: 'Route Number', - type: 'number', - }, - { - field: 'routePosition', - headerName: 'Route Position', - type: 'number', - }, - { - field: 'mealType', - headerName: 'Meal Type', - type: 'string', - }, - { - field: 'program', - headerName: 'Program Type', - type: 'string', - }, - { - field: 'clientName', - headerName: 'Client Name', - type: 'string', - width: 150, - valueGetter: getClientName, - }, - { - field: 'clientAddress', - headerName: 'Client Address', - type: 'string', - width: 200, - valueGetter: getClientAddress, - }, -]; diff --git a/frontend-admin/src/components/routes/editor/board.tsx b/frontend-admin/src/components/routes/editor/board.tsx new file mode 100644 index 00000000..4e5c9593 --- /dev/null +++ b/frontend-admin/src/components/routes/editor/board.tsx @@ -0,0 +1,274 @@ +import React, {useEffect, useState} from "react"; +import SortableRouteDetails from './route-details'; +import { + closestCenter, + DndContext, + KeyboardSensor, + PointerSensor, + useSensor, + useSensors, + DragEndEvent, + DragOverEvent, + Active, + Over, + DragStartEvent, + UniqueIdentifier, + DragCancelEvent, + DragOverlay +} from "@dnd-kit/core"; +import { arrayMove, sortableKeyboardCoordinates } from "@dnd-kit/sortable"; +import { RouteDelivery, ResponseData } from "./types"; +import {Box, Typography, Stack, Grid, IconButton, Button} from '@mui/material'; +import AddIcon from '@mui/icons-material/Add'; +import {BoardAction} from '../page'; +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { + useQuery, +} from '@tanstack/react-query' +import RouteDeliveryAPI from 'src/api/route-deliveries' +import {StopDetails} from "./stop-details"; + +type BoardProps = { + boardAction: BoardAction; + setBoardAction: any; +}; + +export default function Board({boardAction, setBoardAction}: BoardProps) { + const { isLoading, isError, data, error, refetch, isStale } = useQuery(['routeDeliveries'], () => RouteDeliveryAPI.getRouteDeliveries()) + + const [viewRoutes, setViewRoutes] = useState(null); + const [editRoutes, setEditRoutes] = useState(null); + + const [activeId, setActiveId] = useState(null); + const [clonedItems, setClonedItems] = useState(null); + + const queryClient = useQueryClient(); + + const saveAllRouteDeliveriesMutation = useMutation({ + mutationFn: async (data: any) => await RouteDeliveryAPI.saveAllRouteDeliveries(data), + onSuccess: () => { + queryClient.invalidateQueries(['routeDeliveries']) + refetch() + const d = data + }, + }); + + useEffect(() => { + if (boardAction == BoardAction.VIEW) { + setViewRoutes(data?.data.routes) + setEditRoutes(null) + } else if (boardAction == BoardAction.EDIT) { + setEditRoutes(data?.data.routes) + } else if (boardAction == BoardAction.CANCEL) { + setViewRoutes(data?.data.routes) + setEditRoutes(null) + setBoardAction(BoardAction.VIEW) + } else if (boardAction == BoardAction.SAVE) { + saveAllRouteDeliveriesMutation.mutate(editRoutes) + setBoardAction(BoardAction.VIEW) + } + }, [boardAction, data]) + + const sensors = useSensors( + useSensor(PointerSensor), + useSensor(KeyboardSensor, { + coordinateGetter: sortableKeyboardCoordinates + }) + ) + + function findContainer(id: UniqueIdentifier) { + // Route Id + if (Object.keys(editRoutes!).find((key) => key == id)) { + return id.toString(); + } + + // Stop Id + const foundId = Object.keys(editRoutes!).find(key => + editRoutes![key].findIndex(route => route.id === id) !== -1 + ); + return foundId?.toString() + } + + function findStop(id: UniqueIdentifier) { + const containerId = findContainer(id) + const stop = editRoutes![containerId!].find((stop) => stop.id == id) + return stop + } + + const handleDragStart = (event: DragStartEvent) => { + const { active } = event; + setActiveId(active.id); + setClonedItems(editRoutes); + } + + const handleDragEnd = (event: DragEndEvent) => { + const { active, over } = event; + const { id } = active; + const overId = over?.id; + + if (overId == null) { + setActiveId(null); + return; + } + + const activeContainer = findContainer(id); + const overContainer = findContainer(overId); + + if ( + !activeContainer || + !overContainer || + activeContainer !== overContainer + ) { + return; + } + + const activeIndex = editRoutes![activeContainer].findIndex((i) => i.id === activeId); + const overIndex = editRoutes![overContainer].findIndex((i) => i.id === overId); + + if (activeIndex !== overIndex) { + setEditRoutes((prevState) => { + const d:ResponseData = {} + Object.entries(prevState!).map(([column, data]) => { + if (column === overContainer) { + data = arrayMove(editRoutes![overContainer], activeIndex, overIndex); + d[column] = data + } else { + d[column] = data + } + }) + + return d + }); + } + + setActiveId(null); + }; + + const handleDragOver = (event: DragOverEvent) => { + const { active, over, delta } = event; + const { id } = active; + const overId = over!.id + + // Find the containers + const activeContainer = findContainer(id); + const overContainer = findContainer(overId); + + if ( + !activeContainer || + !overContainer || + activeContainer === overContainer + ) { + return; + } + + setEditRoutes((prevState) => { + const activeItems = editRoutes![activeContainer]; + const overItems = editRoutes![overContainer]; + const activeIndex = activeItems.findIndex((i) => i.id === activeId); + const overIndex = overItems.findIndex((i) => i.id === overId); + + let newIndex: number; + if (Object.keys(prevState!).find((key) => key == overId)) { + // We're at the root droppable of a container + newIndex = overItems.length + 1; + } else { + const putOnBelowLastItem = overIndex === overItems.length - 1 && delta.y > 0; + const modifier = putOnBelowLastItem ? 1 : 0; + newIndex = overIndex >= 0 ? overIndex + modifier : overItems.length + 1; + } + + const d: ResponseData = {} + + Object.entries(prevState!).map(([column, data]) => { + if (column === activeContainer) { + data = activeItems.filter((i) => i.id !== activeId); + d[column] = data + } else if (column === overContainer) { + data = [...overItems.slice(0, newIndex), + activeItems[activeIndex], + ...overItems.slice(newIndex, overItems.length), + ]; + d[column] = data + } else { + d[column] = data + } + }) + + return d + }); + } + + const handleDragCancelled = (event: DragCancelEvent) => { + if (clonedItems) { + // Reset items to their original state in case items have been + // Dragged across containers + setEditRoutes(clonedItems); + } + + setActiveId(null); + setClonedItems(null); + }; + + const handleCreateRoute = (event: React.MouseEvent) => { + const editRouteLength = Object.keys(editRoutes!).length; + + setEditRoutes((prevState) => { + const d: ResponseData = {} + + Object.entries(prevState!).map(([column, data]) => { + d[column] = data + }) + + d[editRouteLength] = [] + + return d + }); + } + + return ( + + + { boardAction == BoardAction.VIEW && viewRoutes && + Object.entries(viewRoutes).map(([column, data]) => ( + + )) + } + + { boardAction == BoardAction.EDIT && editRoutes && + <> + { + Object.entries(editRoutes).map(([column, data]) => ( + + )) + } + + + + + } + + + { + activeId ? : null + } + + + ); +} diff --git a/frontend-admin/src/components/routes/editor/route-details.tsx b/frontend-admin/src/components/routes/editor/route-details.tsx new file mode 100644 index 00000000..301d625f --- /dev/null +++ b/frontend-admin/src/components/routes/editor/route-details.tsx @@ -0,0 +1,56 @@ +import { RouteDelivery } from "./types"; +import { SortableContext, rectSortingStrategy } from "@dnd-kit/sortable"; +import { useDroppable } from "@dnd-kit/core"; +import SortableStopDetails from './stop-details'; +import {Box, Paper, Stack, Divider} from '@mui/material'; + +type SortableRouteDetailsProps = { + column: string; + data: RouteDelivery[]; + editEnabled: boolean; +}; + +export default function SortableRouteDetails({column, data, editEnabled}: SortableRouteDetailsProps) { + const { setNodeRef } = useDroppable({ id: column }); + + return ( + + + + ); +} + +type RouteDetailsProps = { + column: string; + data: RouteDelivery[]; + editEnabled: boolean; + setNodeRef: any; +}; + +function RouteDetails({ column, data, editEnabled, setNodeRef }: RouteDetailsProps) { + return ( + + + Route: {column} + + + + + + { + data.map((d: RouteDelivery) => ) + } + + + ); +} \ No newline at end of file diff --git a/frontend-admin/src/components/routes/editor/stop-details.tsx b/frontend-admin/src/components/routes/editor/stop-details.tsx new file mode 100644 index 00000000..06a4f66f --- /dev/null +++ b/frontend-admin/src/components/routes/editor/stop-details.tsx @@ -0,0 +1,51 @@ +import { useSortable } from "@dnd-kit/sortable"; +import {RouteDelivery} from './types'; +import { CSS } from "@dnd-kit/utilities"; +import {Typography, CardContent, Card} from '@mui/material'; + +type SortableStopDetailsProps = { + data: RouteDelivery; + editEnabled: boolean +}; + +export default function SortableStopDetails({data, editEnabled}: SortableStopDetailsProps) { + const { attributes, listeners, setNodeRef, transform } = useSortable({ + id: data.id, + disabled: !editEnabled + }); + + const style = { + transform: CSS.Transform.toString(transform) + }; + + return
+ + {/* */} +
; +} + +type StopDetailsProps = { + id: string; + mealType: string; + program: string +}; + +export function StopDetails( props: {stop: RouteDelivery}) { + const {stop} = props + + return ( + + + {stop.client.name} + {stop.mealType} + {stop.program} + {stop.id.substring(0,4)} + + + ); +} \ No newline at end of file diff --git a/frontend-admin/src/components/routes/editor/types.ts b/frontend-admin/src/components/routes/editor/types.ts new file mode 100644 index 00000000..0bd3caf1 --- /dev/null +++ b/frontend-admin/src/components/routes/editor/types.ts @@ -0,0 +1,12 @@ +export type ResponseData = { + [key: string]: RouteDelivery[]; +} + +export type RouteDelivery = { + id: string; + routeNumber: number; + routePosition: number; + mealType: string; + program: string; + client?: any; +}; \ No newline at end of file diff --git a/frontend-admin/src/components/routes/page.tsx b/frontend-admin/src/components/routes/page.tsx index 59d64737..b52727dd 100644 --- a/frontend-admin/src/components/routes/page.tsx +++ b/frontend-admin/src/components/routes/page.tsx @@ -1,40 +1,65 @@ -import React, {useState} from 'react' +import React, {useEffect, useState} from 'react' import { useQuery, } from '@tanstack/react-query' -import {getRouteDeliveries} from 'src/api/route-deliveries' -import {BasePage} from 'src/components/common/base-page' -import {ActionBar} from 'src/components/common/page-actionbar' -import {ViewBoard} from './board/view' -import {TransferBoard} from './board/transfer' +import RouteDeliveryAPI from 'src/api/route-deliveries' +import {BasePage} from 'src/components/common/layout/base-page' +import {ActionBar} from 'src/components/common/layout/page-actionbar' import {Box} from '@mui/material' +import Board from './editor/board'; +import { ResponseData } from './editor/types' + +export enum BoardAction { + VIEW = "view", + EDIT = "edit", + CANCEL = "cancel", + SAVE = "save", +} const RoutesPage = () => { - const { isLoading, isError, data, error } = useQuery(['routeDeliveries'], () => getRouteDeliveries()) - - const [mode, setMode] = useState("view") + const { isLoading, isError, data, error, refetch } = useQuery(['routeDeliveries'], () => RouteDeliveryAPI.getRouteDeliveries()) + const [boardAction, setBoardAction] = useState(BoardAction.VIEW) + + const handleEnableEdit = () => { + setBoardAction(BoardAction.EDIT) + } + + const handleCancelEdit = () => { + setBoardAction(BoardAction.CANCEL) + } + + const handleSaveEdit = () => { + setBoardAction(BoardAction.SAVE) + } const Header = () => { return ( - Mode: {mode} - mode == "view" ? setMode("transfer") : setMode("view"), - label: "Edit" - }]}/> + Mode: {boardAction.toString()} + + {boardAction == BoardAction.VIEW ? <> + + : <> + + } ) } - + return ( }> {!isLoading && <> - {mode == "view" ? - - : - - } + } diff --git a/frontend-admin/src/components/tasks/modals/create.tsx b/frontend-admin/src/components/tasks/modals/create.tsx new file mode 100644 index 00000000..9e26b3b9 --- /dev/null +++ b/frontend-admin/src/components/tasks/modals/create.tsx @@ -0,0 +1,129 @@ +import React, { useEffect, useState } from "react"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { BaseModal } from "src/components/common/modal/modal"; +import { Select, MenuItem, Box, Stack, Typography, SelectChangeEvent } from "@mui/material"; +import { ModalActionBar } from "src/components/common/modal/actionbar"; +import TaskAPI from "src/api/tasks"; + +type VolunteerOption = { + id: string, + name: string +} + +type MatchData = { + routeName: string, + volunteerId: string +} + +const MatchControl = (props: { + name: string, + handleChange: (routeName: string, volunteerId: string) => void, + options: VolunteerOption[], + matchData: MatchData[] +}) => { + + return <> + + + Route {props.name} + + + + +} + +export const CreateModal = (props: { handleClose: any }) => { + const queryClient = useQueryClient(); + const [availableVolunteers, setAvailableVolunteers] = useState([]) + const [currMatch, setCurrMatch] = useState([]); + + const { data } = useQuery({ + queryKey: ["task-match"], + queryFn: () => TaskAPI.getTaskMatchData(), + }); + + const mutation = useMutation(TaskAPI.saveTaskMatchData, { + onSuccess: () => { + // Invalidate and refetch + queryClient.invalidateQueries("task-match"); + }, + }); + + useEffect(() => { + if (data != null) { + const matches = Object.entries(data.data.routes).map(([key, value]) => { + return { + routeName: key, + volunteerId: "" + } as MatchData + }) + + setCurrMatch(matches) + setAvailableVolunteers(data.data.availableVolunteers) + } + }, [data]); + + const setMatch = (routeName: string, volunteerId: string) => { + setCurrMatch((prevMatch) => + prevMatch.map(m => (m.routeName === routeName ? { ...m, volunteerId: volunteerId } : m)) + ) + } + + const handleSave = () => { + mutation.mutate({ + matches: currMatch + }); + + props.handleClose(); + }; + + const handleCancel = () => { + props.handleClose(); + }; + + return ( + + + { + currMatch && currMatch.map((m: any) => + { + return {id: v.id, name: v.name} as VolunteerOption + })} + matchData={currMatch} + /> + ) + } + + + + ); +}; diff --git a/frontend-admin/src/components/tasks/modals/index.ts b/frontend-admin/src/components/tasks/modals/index.ts new file mode 100644 index 00000000..ba6b92b1 --- /dev/null +++ b/frontend-admin/src/components/tasks/modals/index.ts @@ -0,0 +1 @@ +export {CreateModal} from './create'; diff --git a/frontend-admin/src/components/tasks/page.tsx b/frontend-admin/src/components/tasks/page.tsx index 1655ea9b..4d36e80a 100644 --- a/frontend-admin/src/components/tasks/page.tsx +++ b/frontend-admin/src/components/tasks/page.tsx @@ -2,21 +2,52 @@ import React, {useState} from 'react' import { useQuery, } from '@tanstack/react-query' -import {getTasks} from 'src/api/tasks' -import {BasePage} from 'src/components/common/base-page' +import TaskAPI from 'src/api/tasks' +import {BasePage} from 'src/components/common/layout/base-page' import {Box} from '@mui/material' import {DataGrid} from '@mui/x-data-grid' import { taskColumns } from './columns' +import { ModalControl } from "src/components/common/modal/control"; +import { useModalState } from "src/components/common/modal/use-modal-state"; +import { CreateModal } from "./modals"; +import { ActionBar } from "src/components/common/layout/page-actionbar"; const TasksPage = () => { - const { isLoading, isError, data, error } = useQuery(['tasks'], () => getTasks()) - - if (data) { - console.log("task data ", data) - } + const { isLoading, isError, data, error } = useQuery(['tasks'], () => TaskAPI.getTasks()) + const { + state: createModal, + handleOpen: handleOpenCreateModal, + handleClose: handleCloseCreateModal, + } = useModalState(); + const Header = () => { + return ( + + ); + }; + + const CreateModalControl = () => { + return ( + , + }} + /> + ); + }; + return ( - }> + }> + { ) -} +} export default TasksPage; \ No newline at end of file diff --git a/frontend-admin/src/components/volunteers/modals/create.tsx b/frontend-admin/src/components/volunteers/modals/create.tsx index 886aa10c..ffc09ed9 100644 --- a/frontend-admin/src/components/volunteers/modals/create.tsx +++ b/frontend-admin/src/components/volunteers/modals/create.tsx @@ -1,19 +1,21 @@ import React, { useEffect } from "react"; import { useMutation, useQueryClient } from "@tanstack/react-query"; -import { createVolunteer } from "src/api/volunteers"; +import VolunteerAPI from "src/api/volunteers"; import * as dayjs from "dayjs"; -import { useStateSetupHandler } from "src/components/common/use-state-setup-handler"; -import { isValidEmail, isValidPhone } from "src/components/common/validators"; +import { useStateSetupHandler } from "src/components/common/modal/use-state-setup-handler"; +import { isValidEmail, isValidPhone } from "src/components/common/modal/validators"; import { isAllValid, BaseModal } from "src/components/common/modal/modal"; import { ModalActionBar } from "src/components/common/modal/actionbar"; import { ModalDateInput } from "src/components/common/modal/inputs/date"; import { ModalPhoneInput } from "src/components/common/modal/inputs/phone"; import { ModalTextInput } from "src/components/common/modal/inputs/text"; +import { ModalMultiselectInput } from "src/components/common/modal/inputs/multiselect"; +import { Neighbourhood } from "src/components/common/types"; export const CreateModal = (props: { handleClose: any }) => { const queryClient = useQueryClient(); - const mutation = useMutation(createVolunteer, { + const mutation = useMutation(VolunteerAPI.createVolunteer, { onSuccess: () => { // Invalidate and refetch queryClient.invalidateQueries("volunteers"); @@ -27,6 +29,8 @@ export const CreateModal = (props: { handleClose: any }) => { const { state: password, handler: handlePasswordChange } = useStateSetupHandler(""); + const [preferredNeighbourhoods, setPreferredNeighbourhoods] = React.useState([]); + const [phone, setPhone] = React.useState(""); const handlePhoneChange = (value: any) => { setPhone(value); @@ -41,6 +45,7 @@ export const CreateModal = (props: { handleClose: any }) => { email: email, phoneNumber: phone, date: dayjs(date).toDate(), + preferredNeighbourhoods: preferredNeighbourhoods }); props.handleClose(); }; @@ -50,7 +55,6 @@ export const CreateModal = (props: { handleClose: any }) => { }; useEffect(() => { - console.log("useEffect"); const valid = isAllValid([ name, isValidEmail(email), @@ -58,7 +62,6 @@ export const CreateModal = (props: { handleClose: any }) => { isValidPhone(phone), dayjs(date).isValid(), ]); - console.log(valid); setValid(valid); }, [name, email, password, phone, date]); @@ -96,6 +99,28 @@ export const CreateModal = (props: { handleClose: any }) => { }} /> + setPreferredNeighbourhoods(event.target.value), + key: "value", + options: [ + { value: Neighbourhood.COTEDENEIGES, label: "Côte De Neiges" }, + { value: Neighbourhood.COTESTLUC, label: "Côte St-Luc" }, + { value: Neighbourhood.DOWNTOWN, label: "Downtown" }, + { value: Neighbourhood.LACHINE, label: "Lachine" }, + { value: Neighbourhood.LAVAL, label: "Laval" }, + { value: Neighbourhood.MONTREAL, label: "Montreal" }, + { value: Neighbourhood.MONTREALWEST, label: "Montreal West" }, + { value: Neighbourhood.TMR, label: "Town of Mount Royal" }, + { value: Neighbourhood.VERDUN, label: "Verdun" }, + { value: Neighbourhood.VILLESTLAURENT, label: "Ville St-Laurent" }, + { value: Neighbourhood.WESTISLAND, label: "West Island" }, + ] + }} + /> + { const [valid, setValid] = React.useState(false); const { data } = useQuery({ queryKey: ["volunteers", id], - queryFn: () => getVolunteer(id), + queryFn: () => VolunteerAPI.getVolunteer(id), }); const { @@ -40,25 +42,25 @@ export const EditModal = (props: { handleClose: any }) => { const handlePhoneChange = (value: any) => { setPhone(value); }; + const [preferredNeighbourhoods, setPreferredNeighbourhoods] = React.useState([]); useEffect(() => { if (data) { setName(data!.data.volunteer.name); setEmail(data!.data.volunteer.email); setPhone(data!.data.volunteer.phoneNumber); + setPreferredNeighbourhoods(data!.data.volunteer.preferredNeighbourhoods || []) } }, [data]); useEffect(() => { - console.log("useEffect"); const valid = isAllValid([name, isValidEmail(email), isValidPhone(phone)]); - console.log(valid); setValid(valid); - }, [name, email, phone]); + }, [name, email, phone, preferredNeighbourhoods]); const queryClient = new QueryClient(); - const mutation = useMutation(editVolunteer, { + const mutation = useMutation(VolunteerAPI.editVolunteer, { onSuccess: () => { // Invalidate and refetch queryClient.invalidateQueries("volunteers"); @@ -72,6 +74,7 @@ export const EditModal = (props: { handleClose: any }) => { name: name, email: email, phoneNumber: phone, + preferredNeighbourhoods: preferredNeighbourhoods }, }); setId(-1); @@ -111,6 +114,28 @@ export const EditModal = (props: { handleClose: any }) => { }} /> + setPreferredNeighbourhoods(event.target.value), + key: "value", + options: [ + { value: Neighbourhood.COTEDENEIGES, label: "Côte De Neiges" }, + { value: Neighbourhood.COTESTLUC, label: "Côte St-Luc" }, + { value: Neighbourhood.DOWNTOWN, label: "Downtown" }, + { value: Neighbourhood.LACHINE, label: "Lachine" }, + { value: Neighbourhood.LAVAL, label: "Laval" }, + { value: Neighbourhood.MONTREAL, label: "Montreal" }, + { value: Neighbourhood.MONTREALWEST, label: "Montreal West" }, + { value: Neighbourhood.TMR, label: "Town of Mount Royal" }, + { value: Neighbourhood.VERDUN, label: "Verdun" }, + { value: Neighbourhood.VILLESTLAURENT, label: "Ville St-Laurent" }, + { value: Neighbourhood.WESTISLAND, label: "West Island" }, + ] + }} + /> + { const navigate = useNavigate(); const { isLoading, isError, data, error } = useQuery(["volunteers"], () => - getVolunteers() + VolunteerAPI.getVolunteers() ); const { state: createModal,