From feb5de7c0b86d2149cd52a17cddcbb7edbf70ce4 Mon Sep 17 00:00:00 2001 From: Sophearah Suy-Puth Date: Tue, 10 Oct 2023 17:59:54 -0400 Subject: [PATCH 01/67] auto increment when adding an unassigned route delivery to a route, disable increment/decrement route position buttons --- backend/src/controllers/routeDelivery.ts | 15 ++++++++++++++- .../src/components/routes/board/transfer.tsx | 15 +++++++++++++-- 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/backend/src/controllers/routeDelivery.ts b/backend/src/controllers/routeDelivery.ts index 3f89b795..bf1468a0 100644 --- a/backend/src/controllers/routeDelivery.ts +++ b/backend/src/controllers/routeDelivery.ts @@ -36,9 +36,11 @@ export default class RouteDeliveryController { }; setRouteNumber = async (request: Request, response: Response) => { + const routePosition = request.body.routeNumber == 0 ? 0 : await this.getNextRouteNumber(request.body.routeNumber) + const route = await this.RouteDeliveryRepository.update( { id: parseInt(request.params.id) }, - { routeNumber: request.body.routeNumber } + { routeNumber: request.body.routeNumber, routePosition: routePosition} ); response.status(StatusCode.OK).json({ route }); @@ -61,4 +63,15 @@ export default class RouteDeliveryController { ); response.status(StatusCode.OK).json({ route }); }; + + // Get the next route number + getNextRouteNumber = async (routeNumber) => { + const routes = await this.RouteDeliveryRepository.find({ + where: { + routeNumber: routeNumber + } + }); + + return routes.length || 0; + } } diff --git a/frontend-admin/src/components/routes/board/transfer.tsx b/frontend-admin/src/components/routes/board/transfer.tsx index f856d80b..29a1e035 100644 --- a/frontend-admin/src/components/routes/board/transfer.tsx +++ b/frontend-admin/src/components/routes/board/transfer.tsx @@ -34,6 +34,8 @@ export const TransferBoard = () => { const [selectedRouteDelivery, setSelectedRouteDelivery] = useState(null) const [disabledTransferLeft, setDisabledTransferLeft] = useState(true) const [disabledTransferRight, setDisabledTransferRight] = useState(true) + const [disabledDecrementPosition, setDisabledDecrementPosition] = useState(true) + const [disabledIncrementPosition, setDisabledIncrementPosition] = useState(true) const queryClient = new QueryClient() @@ -132,6 +134,15 @@ export const TransferBoard = () => { } else if (route.routeNumber != 0) { setDisabledTransferRight(true) setDisabledTransferLeft(false) + + if (route.routePosition == 0){ + setDisabledDecrementPosition(true) + } else if (route.routePosition == transferRoutes.length - 1) { + setDisabledIncrementPosition(true) + } else { + setDisabledIncrementPosition(false) + setDisabledDecrementPosition(false) + } } } @@ -173,8 +184,8 @@ export const TransferBoard = () => { disabledDeleteRoute={disabledDeleteRoute} handleIncrementPosition={handleIncrementPosition} handleDecrementPosition={handleDecrementPosition} - disabledDecrementPosition={false} - disabledIncrementPosition={false} + disabledDecrementPosition={disabledDecrementPosition} + disabledIncrementPosition={disabledIncrementPosition} /> ) From 47b6f782e5e51d0a9b48700e6fc218d073767313 Mon Sep 17 00:00:00 2001 From: Sophearah Suy-Puth Date: Tue, 10 Oct 2023 18:25:21 -0400 Subject: [PATCH 02/67] on increment/decrement, swap route position --- backend/src/controllers/routeDelivery.ts | 30 +++++++++++++++++++ .../src/components/routes/board/transfer.tsx | 8 +++++ 2 files changed, 38 insertions(+) diff --git a/backend/src/controllers/routeDelivery.ts b/backend/src/controllers/routeDelivery.ts index bf1468a0..ed212e77 100644 --- a/backend/src/controllers/routeDelivery.ts +++ b/backend/src/controllers/routeDelivery.ts @@ -47,23 +47,53 @@ export default class RouteDeliveryController { }; incrementRoutePosition = async (request: Request, response: Response) => { + const r = await this.getRouteFromId(parseInt(request.params.id)) + + await this.RouteDeliveryRepository.decrement( + { routeNumber: r.routeNumber, routePosition: r.routePosition + 1 }, + 'routePosition', + 1 + ); + 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 r = await this.getRouteFromId(parseInt(request.params.id)) + + await this.RouteDeliveryRepository.increment( + { routeNumber: r.routeNumber, routePosition: r.routePosition - 1 }, + 'routePosition', + 1 + ); + const route = await this.RouteDeliveryRepository.decrement( { id: parseInt(request.params.id) }, 'routePosition', 1 ); + response.status(StatusCode.OK).json({ route }); }; + // Helper Functions + + getRouteFromId = async (id: number) => { + const route = await this.RouteDeliveryRepository.findOne({ + where: { + id: id + } + }); + + return route + } + // Get the next route number getNextRouteNumber = async (routeNumber) => { const routes = await this.RouteDeliveryRepository.find({ diff --git a/frontend-admin/src/components/routes/board/transfer.tsx b/frontend-admin/src/components/routes/board/transfer.tsx index 29a1e035..4e7663ce 100644 --- a/frontend-admin/src/components/routes/board/transfer.tsx +++ b/frontend-admin/src/components/routes/board/transfer.tsx @@ -97,10 +97,18 @@ export const TransferBoard = () => { const handleIncrementPosition = async () => { await incrementRoutePositionMutation.mutate() + setSelectedRouteDelivery(null) + + setDisabledTransferRight(true) + setDisabledTransferLeft(true) } const handleDecrementPosition = async () => { await decrementRoutePositionMutation.mutate() + setSelectedRouteDelivery(null) + + setDisabledTransferRight(true) + setDisabledTransferLeft(true) } // Button handlers for EditRouteButtons From a75be6dcf4a3702fef307ca4252a1a6bf8ccef44 Mon Sep 17 00:00:00 2001 From: Sophearah Suy-Puth Date: Tue, 10 Oct 2023 20:27:18 -0400 Subject: [PATCH 03/67] client should not inherit from user --- backend/src/entities/ClientEntity.ts | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/backend/src/entities/ClientEntity.ts b/backend/src/entities/ClientEntity.ts index 83ad6a4c..228c35c8 100644 --- a/backend/src/entities/ClientEntity.ts +++ b/backend/src/entities/ClientEntity.ts @@ -3,7 +3,22 @@ import { UserEntity } from './UserEntity'; import { MealType, Neighbourhood } from './types'; @Entity() -export class ClientEntity extends UserEntity { +export class ClientEntity { + @PrimaryGeneratedColumn() + id: number; + + @Column() + name: string; + + @Column({ + unique: true, + nullable: false + }) + email: string; + + @Column() + phoneNumber: string; + @Column() address: string; From 45894c05fb4468fda1431804acc521affdfe1223 Mon Sep 17 00:00:00 2001 From: Sophearah Suy-Puth Date: Tue, 10 Oct 2023 20:41:18 -0400 Subject: [PATCH 04/67] nullable availabilities and availabilities last updated --- backend/src/entities/VolunteerEntity.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/src/entities/VolunteerEntity.ts b/backend/src/entities/VolunteerEntity.ts index 433ee347..68bfb7fd 100644 --- a/backend/src/entities/VolunteerEntity.ts +++ b/backend/src/entities/VolunteerEntity.ts @@ -66,10 +66,10 @@ export class VolunteerEntity extends UserEntity { @Column() profilePicture: string; - @Column() + @Column({ nullable: true }) availabilities: string; - @Column() + @Column({ nullable: true }) availabilitiesLastUpdated: Date; @OneToMany(() => TaskEntity, (task) => task.volunteer, { From 490d6e97b1724a6d3ffa9990f97313e82ac4f7fb Mon Sep 17 00:00:00 2001 From: Sophearah Suy-Puth Date: Tue, 10 Oct 2023 20:56:43 -0400 Subject: [PATCH 05/67] fix client seeder now that it doesnt inherit from the User class --- backend/src/scripts/seeders/client.seeder.ts | 33 +++++++++++++------- 1 file changed, 22 insertions(+), 11 deletions(-) diff --git a/backend/src/scripts/seeders/client.seeder.ts b/backend/src/scripts/seeders/client.seeder.ts index ecd7b12c..bc42e155 100644 --- a/backend/src/scripts/seeders/client.seeder.ts +++ b/backend/src/scripts/seeders/client.seeder.ts @@ -21,20 +21,31 @@ const generateRouteDelivery = async ( }; 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 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 + }; + user.sts = faker.datatype.boolean(); + user.map = !user.sts ? true :faker.datatype.boolean(); + + return user; }; const generateClients = async (num: number) => { - const clients: ClientEntity[] = []; + const clients: any[] = []; for (let i = 0; i < num; i++) { const c = await generateClient(); clients.push(c); From 021fb568422e76c9c6f2ae060a73deb8da160c0c Mon Sep 17 00:00:00 2001 From: Sophearah Suy-Puth Date: Tue, 10 Oct 2023 21:50:22 -0400 Subject: [PATCH 06/67] soft delete attribute for volunteer + client, filter by soft delete for get volunteer + client --- backend/src/controllers/clients.ts | 11 +++++++++-- backend/src/controllers/volunteers.ts | 8 +++++++- backend/src/entities/ClientEntity.ts | 3 +++ backend/src/entities/VolunteerEntity.ts | 3 +++ backend/src/scripts/seeders/client.seeder.ts | 3 ++- backend/src/scripts/seeders/volunteer.seeder.ts | 2 ++ 6 files changed, 26 insertions(+), 4 deletions(-) diff --git a/backend/src/controllers/clients.ts b/backend/src/controllers/clients.ts index 0b3c83f6..ec668549 100644 --- a/backend/src/controllers/clients.ts +++ b/backend/src/controllers/clients.ts @@ -12,7 +12,11 @@ export default class ClientController { AppDataSource.getRepository(RouteDeliveryEntity); getClients = async (request: Request, response: Response) => { - const clients = await this.ClientRepository.find(); + const clients = await this.ClientRepository.find({ + where: { + softDelete: false + } + }); response.status(StatusCode.OK).json({ clients: clients }); }; @@ -55,7 +59,10 @@ export default class ClientController { getClient = async (request: Request, response: Response) => { const client = await this.ClientRepository.findOne({ - where: { id: parseInt(request.params.id) } + where: { + id: parseInt(request.params.id), + softDelete: false + } }); response.status(StatusCode.OK).json({ client: client }); }; diff --git a/backend/src/controllers/volunteers.ts b/backend/src/controllers/volunteers.ts index 29c7c2f7..f296b44c 100644 --- a/backend/src/controllers/volunteers.ts +++ b/backend/src/controllers/volunteers.ts @@ -13,6 +13,9 @@ export default class VolunteerController { const volunteers = await this.VolunteerRepository.find({ relations: { tasks: true + }, + where: { + softDelete: false } }); response.status(StatusCode.OK).json({ volunteers: volunteers }); @@ -20,7 +23,10 @@ export default class VolunteerController { getVolunteer = async (request: Request, response: Response) => { const volunteer = await this.VolunteerRepository.findOne({ - where: { id: parseInt(request.params.id) }, + where: { + id: parseInt(request.params.id), + softDelete: false + }, relations: { tasks: true } diff --git a/backend/src/entities/ClientEntity.ts b/backend/src/entities/ClientEntity.ts index 228c35c8..ddc0b1e6 100644 --- a/backend/src/entities/ClientEntity.ts +++ b/backend/src/entities/ClientEntity.ts @@ -30,4 +30,7 @@ export class ClientEntity { @Column() map: boolean; + + @Column() + softDelete: boolean = false; } diff --git a/backend/src/entities/VolunteerEntity.ts b/backend/src/entities/VolunteerEntity.ts index 68bfb7fd..248b3031 100644 --- a/backend/src/entities/VolunteerEntity.ts +++ b/backend/src/entities/VolunteerEntity.ts @@ -82,4 +82,7 @@ export class VolunteerEntity extends UserEntity { @Column('text', { nullable: true, array: true }) preferredNeighbourhoods: Neighbourhood[]; + + @Column() + softDelete: boolean = false; } diff --git a/backend/src/scripts/seeders/client.seeder.ts b/backend/src/scripts/seeders/client.seeder.ts index bc42e155..c8d7cfd6 100644 --- a/backend/src/scripts/seeders/client.seeder.ts +++ b/backend/src/scripts/seeders/client.seeder.ts @@ -36,7 +36,8 @@ const generateClient = async () => { 'No Meat' ]), sts: false, - map: false + map: false, + softDelete: false }; user.sts = faker.datatype.boolean(); user.map = !user.sts ? true :faker.datatype.boolean(); diff --git a/backend/src/scripts/seeders/volunteer.seeder.ts b/backend/src/scripts/seeders/volunteer.seeder.ts index 39458394..ff83d3d3 100644 --- a/backend/src/scripts/seeders/volunteer.seeder.ts +++ b/backend/src/scripts/seeders/volunteer.seeder.ts @@ -25,6 +25,7 @@ const generateDefaultVolunteer = async () => { volunteer.profilePicture = faker.internet.avatar(); volunteer.availabilities = generateAvailabilities(); volunteer.availabilitiesLastUpdated = faker.date.recent(15); + volunteer.softDelete = false; return volunteer; }; @@ -35,6 +36,7 @@ const generateVolunteer = async () => { volunteer.availabilities = generateAvailabilities(); volunteer.availabilitiesLastUpdated = faker.date.recent(15); volunteer.preferredNeighbourhoods = ['Lachine', 'Montreal', 'Downtown']; + volunteer.softDelete = false; return volunteer; }; From fd2ce6fb23773446f7c4a91adde5966780bc4d3f Mon Sep 17 00:00:00 2001 From: Sophearah Suy-Puth Date: Mon, 16 Oct 2023 17:10:46 -0400 Subject: [PATCH 07/67] multiselect input for preferred neighborhood in create volunteer modal, set preferred neighborhood on create volunteer api --- backend/src/controllers/volunteers.ts | 4 ++- backend/src/entities/types.ts | 19 +++++++++++++ frontend-admin/src/components/common/types.ts | 19 +++++++++++++ .../components/volunteers/modals/create.tsx | 27 +++++++++++++++++++ 4 files changed, 68 insertions(+), 1 deletion(-) diff --git a/backend/src/controllers/volunteers.ts b/backend/src/controllers/volunteers.ts index f296b44c..e43ba7a5 100644 --- a/backend/src/controllers/volunteers.ts +++ b/backend/src/controllers/volunteers.ts @@ -5,6 +5,7 @@ import { VolunteerEntity } from '../entities/VolunteerEntity'; import { StatusCode } from './statusCode'; import * as bcrypt from 'bcrypt'; import * as jwt from 'jsonwebtoken'; +import {getNeighbourhoodFromString} from '../entities/types'; export default class VolunteerController { private VolunteerRepository = AppDataSource.getRepository(VolunteerEntity); @@ -49,7 +50,8 @@ export default class VolunteerController { phoneNumber: request.body.phoneNumber, startDate: request.body.date, profilePicture: '', - availabilities: request.body.availabilities + availabilities: request.body.availabilities, + preferredNeighbourhoods: request.body.preferredNeighbourhoods ? request.body.preferredNeighbourhoods.map(neighborhood => getNeighbourhoodFromString(neighborhood)) : [] }); await this.VolunteerRepository.save(volunteer); response.status(StatusCode.OK).json({ volunteer }); diff --git a/backend/src/entities/types.ts b/backend/src/entities/types.ts index 383da2b1..eec4d72b 100644 --- a/backend/src/entities/types.ts +++ b/backend/src/entities/types.ts @@ -23,3 +23,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 getNeighbourhoodFromString(str: string): Neighbourhood { + return neighbourhoodReverseMapping[str]; +} 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/volunteers/modals/create.tsx b/frontend-admin/src/components/volunteers/modals/create.tsx index 886aa10c..ae155920 100644 --- a/frontend-admin/src/components/volunteers/modals/create.tsx +++ b/frontend-admin/src/components/volunteers/modals/create.tsx @@ -9,6 +9,8 @@ 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(); @@ -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(); }; @@ -96,6 +101,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" }, + ] + }} + /> + Date: Mon, 16 Oct 2023 17:25:47 -0400 Subject: [PATCH 08/67] edit preferred neighborhood on edit volunteer modal --- .../src/components/volunteers/modals/edit.tsx | 31 +++++++++++++++++-- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/frontend-admin/src/components/volunteers/modals/edit.tsx b/frontend-admin/src/components/volunteers/modals/edit.tsx index cb56f809..a53e9807 100644 --- a/frontend-admin/src/components/volunteers/modals/edit.tsx +++ b/frontend-admin/src/components/volunteers/modals/edit.tsx @@ -10,6 +10,8 @@ import { isValidEmail, isValidPhone } from "src/components/common/validators"; import { ModalActionBar } from "src/components/common/modal/actionbar"; 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"; import { useQuery, useMutation, QueryClient } from "@tanstack/react-query"; @@ -40,21 +42,21 @@ 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(); @@ -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" }, + ] + }} + /> + Date: Mon, 16 Oct 2023 17:45:07 -0400 Subject: [PATCH 09/67] sts / map switch match value --- frontend-admin/src/components/common/modal/inputs/boolean.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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} - + ) From 0aab0def2a9dae194bdf3b962b3230e2293502df Mon Sep 17 00:00:00 2001 From: Sophearah Suy-Puth Date: Mon, 16 Oct 2023 17:58:51 -0400 Subject: [PATCH 10/67] withCredentials is true --- backend/src/app.ts | 5 ++++- frontend-admin/src/api/axios.ts | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/backend/src/app.ts b/backend/src/app.ts index f1d27a10..3c2b0bb0 100644 --- a/backend/src/app.ts +++ b/backend/src/app.ts @@ -14,7 +14,10 @@ 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/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( From 0f5321f72abf1fe14595ca41afb960cac4d36ce5 Mon Sep 17 00:00:00 2001 From: Sophearah Suy-Puth Date: Tue, 24 Oct 2023 16:32:43 -0400 Subject: [PATCH 11/67] editing client will update the associated route delivery item --- backend/src/controllers/clients.ts | 67 ++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/backend/src/controllers/clients.ts b/backend/src/controllers/clients.ts index ec668549..2dcbc4a3 100644 --- a/backend/src/controllers/clients.ts +++ b/backend/src/controllers/clients.ts @@ -68,6 +68,15 @@ export default class ClientController { }; editClient = async (request: Request, response: Response) => { + + const originalClient = await this.ClientRepository.findOne({ + where: {id: parseInt(request.params.id)} + }) + + const editedMealType = !(request.body.mealType == originalClient.mealType) + const editedSTS = !(request.body.sts == originalClient.sts) + const editedMAP = !(request.body.map == originalClient.map) + const client = await this.ClientRepository.update( { id: parseInt(request.params.id) }, { @@ -80,6 +89,64 @@ export default class ClientController { map: request.body.map } ); + + const savedClient = await this.ClientRepository.findOne({ + where: {id: parseInt(request.params.id)} + }) + + if (editedSTS) { + if (request.body.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); + } else { + const stsRouteDelivery = await this.RouteDeliveryRepository.find({ + relations: { client: true }, + where: {client: {id: parseInt(request.params.id)}, program: ProgramType.STS} + }) + + await this.RouteDeliveryRepository.remove(stsRouteDelivery) + } + } + + if (editedMAP) { + if (request.body.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); + } else { + const mapRouteDelivery = await this.RouteDeliveryRepository.find({ + relations: { client: true }, + where: {client: {id: parseInt(request.params.id)}, program: ProgramType.MAP} + }) + + await this.RouteDeliveryRepository.remove(mapRouteDelivery) + } + } + + if (editedMealType){ + const routeDeliveries = await this.RouteDeliveryRepository.find({ + relations: { client: true }, + where: {client: {id: parseInt(request.params.id)}} + }) + + routeDeliveries.forEach(routeDelivery => { + routeDelivery.mealType = savedClient.mealType + }) + + this.RouteDeliveryRepository.save(routeDeliveries) + } + response.status(StatusCode.OK).json({ client }); }; } From 4c283fbc9cfb7be985539b5aa8fe6516c5d43477 Mon Sep 17 00:00:00 2001 From: Sophearah Suy-Puth Date: Mon, 26 Feb 2024 14:25:12 +0700 Subject: [PATCH 12/67] view route data from db, scrappy implementations of dnd-kit + ok one --- backend/src/controllers/routeDelivery.ts | 11 + backend/src/routes/routeDelivery.routes.ts | 2 + frontend-admin/package-lock.json | 96 ++++++ frontend-admin/package.json | 4 + frontend-admin/src/api/route-deliveries.ts | 10 + .../components/routes/dnd-view-edit/board.tsx | 308 ++++++++++++++++++ .../components/routes/dnd-view-edit/card.tsx | 17 + .../routes/dnd-view-edit/column.tsx | 20 ++ .../components/routes/dnd-view-edit/types.ts | 12 + .../src/components/routes/dnd/board.tsx | 181 ++++++++++ .../src/components/routes/dnd/container.tsx | 38 +++ .../src/components/routes/dnd/item.tsx | 47 +++ frontend-admin/src/components/routes/page.tsx | 17 +- 13 files changed, 757 insertions(+), 6 deletions(-) create mode 100644 frontend-admin/src/components/routes/dnd-view-edit/board.tsx create mode 100644 frontend-admin/src/components/routes/dnd-view-edit/card.tsx create mode 100644 frontend-admin/src/components/routes/dnd-view-edit/column.tsx create mode 100644 frontend-admin/src/components/routes/dnd-view-edit/types.ts create mode 100644 frontend-admin/src/components/routes/dnd/board.tsx create mode 100644 frontend-admin/src/components/routes/dnd/container.tsx create mode 100644 frontend-admin/src/components/routes/dnd/item.tsx diff --git a/backend/src/controllers/routeDelivery.ts b/backend/src/controllers/routeDelivery.ts index ed212e77..d8f20789 100644 --- a/backend/src/controllers/routeDelivery.ts +++ b/backend/src/controllers/routeDelivery.ts @@ -8,6 +8,17 @@ export default class RouteDeliveryController { private RouteDeliveryRepository = AppDataSource.getRepository(RouteDeliveryEntity); + // TODO: review this? v + getRouteDeliveriesSimple = async (request: Request, response: Response) => { + const routes = await this.RouteDeliveryRepository.find({ + relations: { + client: true + } + }); + + response.status(StatusCode.OK).json({ routes: routes }); + }; + getRouteDeliveries = async (request: Request, response: Response) => { const routes = await this.RouteDeliveryRepository.find({ relations: { diff --git a/backend/src/routes/routeDelivery.routes.ts b/backend/src/routes/routeDelivery.routes.ts index d56a7569..00a0cb56 100644 --- a/backend/src/routes/routeDelivery.routes.ts +++ b/backend/src/routes/routeDelivery.routes.ts @@ -6,6 +6,8 @@ export const router = express.Router(); const routeDeliveryController = new RouteDeliveryController(); router.get('/route_delivery/', routeDeliveryController.getRouteDeliveries); +// TODO: review this? v +router.get('/route_delivery_simple/', routeDeliveryController.getRouteDeliveriesSimple); router.put('/route_delivery/:id/set', routeDeliveryController.setRouteNumber); router.put( '/route_delivery/:id/increment', 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/route-deliveries.ts b/frontend-admin/src/api/route-deliveries.ts index 9dc0e302..d8f7dd34 100644 --- a/frontend-admin/src/api/route-deliveries.ts +++ b/frontend-admin/src/api/route-deliveries.ts @@ -9,6 +9,16 @@ export const getRouteDeliveries = async () => { return response } +// TODO: review this +export const getRouteDeliveriesSimple = async () => { + const response = await AxiosInstance({ + method: "get", + url: "/route_delivery_simple", + }); + + return response +} + export const setRouteDeliveryNumber = async (id: number, routeNumber: number) => { const response = await AxiosInstance({ method: "put", diff --git a/frontend-admin/src/components/routes/dnd-view-edit/board.tsx b/frontend-admin/src/components/routes/dnd-view-edit/board.tsx new file mode 100644 index 00000000..07e6367b --- /dev/null +++ b/frontend-admin/src/components/routes/dnd-view-edit/board.tsx @@ -0,0 +1,308 @@ +import React, {useState} from "react"; +import Column from './column'; +import { + closestCorners, + 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"; + +export const response_data: ResponseData = { + "0": [ + { + "id": 851060, + "routeNumber": 0, + "routePosition": 0, + "mealType": "No Meat", + "program": "STS" + }, + { + "id": 742492, + "routeNumber": 0, + "routePosition": 0, + "mealType": "Vegetarian", + "program": "MAP" + }, + { + "id": 327428, + "routeNumber": 0, + "routePosition": 0, + "mealType": "Vegetarian", + "program": "STS" + }, + { + "id": 444406, + "routeNumber": 0, + "routePosition": 0, + "mealType": "Regular", + "program": "STS" + }, + { + "id": 943595, + "routeNumber": 0, + "routePosition": 0, + "mealType": "No Fish", + "program": "MAP" + }, + { + "id": 115110, + "routeNumber": 0, + "routePosition": 0, + "mealType": "No Meat", + "program": "STS" + }, + { + "id": 658877, + "routeNumber": 0, + "routePosition": 0, + "mealType": "No Meat", + "program": "MAP" + }, + { + "id": 510872, + "routeNumber": 0, + "routePosition": 0, + "mealType": "Vegetarian", + "program": "STS" + }, + { + "id": 557078, + "routeNumber": 0, + "routePosition": 0, + "mealType": "Regular", + "program": "STS" + }, + { + "id": 868287, + "routeNumber": 0, + "routePosition": 0, + "mealType": "No Fish", + "program": "MAP" + } + ], + "1": [ + { + "id": 111064, + "routeNumber": 0, + "routePosition": 0, + "mealType": "Vegetarian", + "program": "STS" + }, + { + "id": 265140, + "routeNumber": 0, + "routePosition": 0, + "mealType": "Vegetarian", + "program": "STS" + }, + { + "id": 664684, + "routeNumber": 0, + "routePosition": 0, + "mealType": "No Meat", + "program": "MAP" + }, + { + "id": 817055, + "routeNumber": 0, + "routePosition": 0, + "mealType": "Regular", + "program": "MAP" + }, + { + "id": 654993, + "routeNumber": 0, + "routePosition": 0, + "mealType": "No Meat", + "program": "STS" + }, + { + "id": 565347, + "routeNumber": 0, + "routePosition": 0, + "mealType": "No Fish", + "program": "STS" + }, + { + "id": 525509, + "routeNumber": 0, + "routePosition": 0, + "mealType": "No Meat", + "program": "MAP" + }, + { + "id": 724160, + "routeNumber": 0, + "routePosition": 0, + "mealType": "No Fish", + "program": "STS" + }, + { + "id": 238324, + "routeNumber": 0, + "routePosition": 0, + "mealType": "No Fish", + "program": "MAP" + }, + { + "id": 613813, + "routeNumber": 0, + "routePosition": 0, + "mealType": "Regular", + "program": "STS" + } + ] +} + +export default function Board(props: {data: any}) { + const [columns, setColumns] = useState(props.data as ResponseData); + const [activeId, setActiveId] = useState(null); + const [clonedItems, setClonedItems] = useState(null); + + const sensors = useSensors( + useSensor(PointerSensor), + useSensor(KeyboardSensor, { + coordinateGetter: sortableKeyboardCoordinates + }) + ) + + const findColumnIdFromActive = (item: Active): string => { + if (!item.data.current) return ""; + return item.data.current.sortable.containerId + } + + const findColumnIdFromOver = (item: Over): string => { + if (!item.data.current) return ""; + return item.data.current.sortable.containerId + } + + const handleDragStart = (event: DragStartEvent) => { + const { active } = event; + setActiveId(active.id); + setClonedItems(columns); + } + + const handleDragEnd = (event: DragEndEvent) => { + const { active, over } = event; + const activeId = active.id; + const overId = over ? over.id : null; + const activeColumnId = findColumnIdFromActive(active); + const overColumnId = over ? findColumnIdFromOver(over) : ""; + + if (!activeColumnId || !overColumnId || activeColumnId !== overColumnId) { + return null; + } + + const activeIndex = columns[activeColumnId].findIndex((i) => i.id === activeId); + const overIndex = columns[overColumnId].findIndex((i) => i.id === overId); + + if (activeIndex !== overIndex) { + setColumns((prevState) => { + const d:ResponseData = {} + Object.entries(prevState).map(([column, data]) => { + if (column === activeColumnId) { + data = arrayMove(columns[overColumnId], activeIndex, overIndex); + d[column] = data + } else { + d[column] = data + } + }) + + return d + }); + } + }; + + const handleDragOver = (event: DragOverEvent) => { + const { active, over, delta } = event; + const activeId = active.id; + const overId = over ? over.id : null; + const activeColumnId = findColumnIdFromActive(active); + const overColumnId = over ? findColumnIdFromOver(over) : null; + + if (!activeColumnId || !overColumnId || activeColumnId === overColumnId) { + return null; + } + + setColumns((prevState) => { + const activeItems = columns[activeColumnId]; + const overItems = columns[overColumnId]; + const activeIndex = activeItems.findIndex((i) => i.id === activeId); + const overIndex = overItems.findIndex((i) => i.id === overId); + + const newIndex = () => { + const putOnBelowLastItem = + overIndex === overItems.length - 1 && delta.y > 0; + const modifier = putOnBelowLastItem ? 1 : 0; + return overIndex >= 0 ? overIndex + modifier : overItems.length + 1; + }; + + const d: ResponseData = {} + + Object.entries(prevState).map(([column, data]) => { + if (column === activeColumnId) { + data = activeItems.filter((i) => i.id !== activeId); + d[column] = data + } else if (column === overColumnId) { + 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 + setColumns(clonedItems); + } + + setActiveId(null); + setClonedItems(null); + }; + + return (
+ +
+ { + Object.entries(columns).map(([column, data]) => ( + + )) + } +
+ + { + // how to set overlay + activeId ?
OVERLAY {activeId}
: null + } +
+
+
); +} diff --git a/frontend-admin/src/components/routes/dnd-view-edit/card.tsx b/frontend-admin/src/components/routes/dnd-view-edit/card.tsx new file mode 100644 index 00000000..332b5b49 --- /dev/null +++ b/frontend-admin/src/components/routes/dnd-view-edit/card.tsx @@ -0,0 +1,17 @@ +import { useSortable } from "@dnd-kit/sortable"; +import {RouteDelivery} from './types'; +import { CSS } from "@dnd-kit/utilities"; + +export default function Card(props: {data: RouteDelivery}) { + const { attributes, listeners, setNodeRef, transform } = useSortable({ + id: props.data.id, + // disabled: true // how to disable + }); + + const style = { + transform: CSS.Transform.toString(transform) + }; + return
+

{props.data.id}

+
; +} diff --git a/frontend-admin/src/components/routes/dnd-view-edit/column.tsx b/frontend-admin/src/components/routes/dnd-view-edit/column.tsx new file mode 100644 index 00000000..94c85605 --- /dev/null +++ b/frontend-admin/src/components/routes/dnd-view-edit/column.tsx @@ -0,0 +1,20 @@ +import { RouteDelivery } from "./types"; +import { SortableContext, rectSortingStrategy } from "@dnd-kit/sortable"; +import { useDroppable } from "@dnd-kit/core"; +import Card from './card'; + +export default function Column(props: {column: string, data: RouteDelivery[]}) { + const { setNodeRef } = useDroppable({ id: props.column }); + + return ( + +
+ +
column: {props.column}
+ { + props.data.map((d: RouteDelivery) => ) + } +
+
+ ); +} diff --git a/frontend-admin/src/components/routes/dnd-view-edit/types.ts b/frontend-admin/src/components/routes/dnd-view-edit/types.ts new file mode 100644 index 00000000..777ebfe3 --- /dev/null +++ b/frontend-admin/src/components/routes/dnd-view-edit/types.ts @@ -0,0 +1,12 @@ +export type ResponseData = { + [key: string]: RouteDelivery[]; +} + +export type RouteDelivery = { + id: number; + routeNumber: number; + routePosition: number; + mealType: string; + program: string; + client?: any; +}; \ No newline at end of file diff --git a/frontend-admin/src/components/routes/dnd/board.tsx b/frontend-admin/src/components/routes/dnd/board.tsx new file mode 100644 index 00000000..2f135e85 --- /dev/null +++ b/frontend-admin/src/components/routes/dnd/board.tsx @@ -0,0 +1,181 @@ +import React, { useState, useMemo, useEffect } from "react"; +import { + DndContext, + closestCorners, + KeyboardSensor, + PointerSensor, + useSensor, + useSensors, + DragEndEvent, +} from "@dnd-kit/core"; +import { + SortableContext, + } from "@dnd-kit/sortable"; +import { arrayMove, sortableKeyboardCoordinates } from "@dnd-kit/sortable"; +import type { + DragOverEvent, + DragStartEvent, + UniqueIdentifier +} from "@dnd-kit/core"; +import {Stack} from '@mui/material'; + +import { + useQuery, +} from '@tanstack/react-query' +import {getRouteDeliveriesSimple} from 'src/api/route-deliveries' + +import { Container } from "./container"; + +export default function App() { + const { isLoading, isError, data, error } = useQuery(['routeDeliveries'], () => getRouteDeliveriesSimple()) + + useEffect(() => { + if (data) { + setRouteItems(data!.data.routes); + try { + const numbers: [] = data!.data.routes.length > 0 ? data!.data.routes.map((route: any) => route.routeNumber) : [] + setColumnIds([...new Set(numbers)]); + } catch { + + } + } + }, [data]); + + // initalized as all routes from database + const [routeItems, setRouteItems] = useState([]); + const [activeRouteId, setActiveRouteId] = useState(null); + + // initialized as all routes from database list of ids + const [columnIds, setColumnIds] = useState([]); + const [activeColumnId, setActiveColumnId] = useState(null); + + const sensors = useSensors( + useSensor(PointerSensor), + useSensor(KeyboardSensor, { + coordinateGetter: sortableKeyboardCoordinates + }) + ); + + function handleDragStart(event: DragStartEvent) { + const { active } = event; + const { id } = active; + + setActiveRouteId(id); + } + + function handleDragEnd(event: DragEndEvent) { + setActiveColumnId(null); + setActiveRouteId(null); + + const { active, over } = event; + if (!over) return; + + const activeId = active.id; + const overId = over.id; + + if (activeId === overId) return; + + setColumnIds((columns) => { + const activeColumnIndex = columns.findIndex((col) => col === activeId); + + const overColumnIndex = columns.findIndex((col) => col === overId); + + return arrayMove(columns, activeColumnIndex, overColumnIndex); + }); + } + + + function handleDragOver(event: DragOverEvent) { + const { active, over } = event; + if (!over) return; + + const activeId = active.id; + const overId = over.id; + + if (activeId === overId) return; + + const isActiveATask = active.data.current?.type === "Task"; + const isOverATask = over.data.current?.type === "Task"; + + if (!isActiveATask) return; + + // Im dropping a Task over another Task + if (isActiveATask && isOverATask) { + setRouteItems((routeItems: any) => { + const activeIndex = routeItems.findIndex((r: any) => r.id === activeId); + const overIndex = routeItems.findIndex((r: any) => r.id === overId); + + if (routeItems[activeIndex].routeNumber != routeItems[overIndex].routeNumber) { + // Fix introduced after video recording + routeItems[activeIndex].routeNumber = routeItems[overIndex].routeNumber; + return arrayMove(routeItems, activeIndex, overIndex - 1); + } + + return arrayMove(routeItems, activeIndex, overIndex); + }); + } + + const isOverAColumn = over.data.current?.type === "Column"; + + // Im dropping a Task over a column + if (isActiveATask && isOverAColumn) { + setRouteItems((routeItems: any) => { + const activeIndex = routeItems.findIndex((r: any) => r.id === activeId); + + routeItems[activeIndex].routeNumber = overId; + console.log("DROPPING TASK OVER COLUMN", { activeIndex }); + return arrayMove(routeItems, activeIndex, activeIndex); + }); + } + } + + function createNewColumn() { + setColumnIds([...columnIds, columnIds.length + 1]); + } + + return ( + + + + {columnIds.map((col) => ( + routeItem.routeNumber === col) : []} + /> + ))} + + + + {/* + {columnIds.map((col) => ( + routeItem.routeNumber === col) : []} + /> + ))} + + */} + + + ); +} \ No newline at end of file diff --git a/frontend-admin/src/components/routes/dnd/container.tsx b/frontend-admin/src/components/routes/dnd/container.tsx new file mode 100644 index 00000000..eb389f9d --- /dev/null +++ b/frontend-admin/src/components/routes/dnd/container.tsx @@ -0,0 +1,38 @@ +import React from "react"; +import { useDroppable } from "@dnd-kit/core"; +import { + SortableContext, + verticalListSortingStrategy +} from "@dnd-kit/sortable"; +import {ItemType, SortableItem} from './item'; +import {Box, Typography} from '@mui/material'; + +type ContainerProps = { + containerId: string; + items: Array; +}; + +export function Container(props: ContainerProps) { + const { containerId, items } = props; + + const { setNodeRef } = useDroppable({ + id: containerId + }); + + return ( + + + + {containerId} + + {items.map((item) => ( + + ))} + + + ); +} diff --git a/frontend-admin/src/components/routes/dnd/item.tsx b/frontend-admin/src/components/routes/dnd/item.tsx new file mode 100644 index 00000000..bb0f263f --- /dev/null +++ b/frontend-admin/src/components/routes/dnd/item.tsx @@ -0,0 +1,47 @@ +import React from "react"; +import { useSortable } from "@dnd-kit/sortable"; +import {Box, Typography} from '@mui/material'; + +type SortableItemProps = ItemType; + +export function Item({ id, mealType, program }: ItemType) { + return ( + + {id} + {mealType} + {program} + + ); +} + +export function SortableItem(props: SortableItemProps) { + const { id, mealType, program } = props; + const { + attributes, + listeners, + setNodeRef, + transform, + transition + } = useSortable({ id }); + + const style = { + transform: transform + ? `translate3d(${transform.x}px, ${Math.round( + transform.y + )}px, 0) scaleX(${transform.scaleX})` + : "", + transition + }; + + return ( + + + + ); +} + +export type ItemType = { + id: string; + mealType: string; + program: string +}; \ 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..8d6d21f6 100644 --- a/frontend-admin/src/components/routes/page.tsx +++ b/frontend-admin/src/components/routes/page.tsx @@ -1,4 +1,4 @@ -import React, {useState} from 'react' +import React, {useEffect, useState} from 'react' import { useQuery, } from '@tanstack/react-query' @@ -8,6 +8,15 @@ import {ActionBar} from 'src/components/common/page-actionbar' import {ViewBoard} from './board/view' import {TransferBoard} from './board/transfer' import {Box} from '@mui/material' +import Board from './dnd-view-edit/board'; +import { ResponseData } from './dnd-view-edit/types' + +/* +NOTES: + routes/board: annoying click to transfer + dnd: bad implementation of dnd-kit? + dnd-view-edit: good implementation of dnd-kit +*/ const RoutesPage = () => { const { isLoading, isError, data, error } = useQuery(['routeDeliveries'], () => getRouteDeliveries()) @@ -30,11 +39,7 @@ const RoutesPage = () => { }> {!isLoading && <> - {mode == "view" ? - - : - - } + } From b5d9e11ab70fac105d013d828b014f3d9c908649 Mon Sep 17 00:00:00 2001 From: Sophearah Suy-Puth Date: Mon, 26 Feb 2024 15:42:43 +0700 Subject: [PATCH 13/67] basic style board, card, column --- .../components/routes/dnd-view-edit/board.tsx | 431 +++++++++++------- .../components/routes/dnd-view-edit/card.tsx | 29 +- .../routes/dnd-view-edit/column.tsx | 31 +- 3 files changed, 326 insertions(+), 165 deletions(-) diff --git a/frontend-admin/src/components/routes/dnd-view-edit/board.tsx b/frontend-admin/src/components/routes/dnd-view-edit/board.tsx index 07e6367b..c7c1d9e2 100644 --- a/frontend-admin/src/components/routes/dnd-view-edit/board.tsx +++ b/frontend-admin/src/components/routes/dnd-view-edit/board.tsx @@ -18,155 +18,268 @@ import { } from "@dnd-kit/core"; import { arrayMove, sortableKeyboardCoordinates } from "@dnd-kit/sortable"; import { RouteDelivery, ResponseData } from "./types"; +import {Box, Typography, Stack, Grid} from '@mui/material'; -export const response_data: ResponseData = { - "0": [ - { - "id": 851060, - "routeNumber": 0, - "routePosition": 0, - "mealType": "No Meat", - "program": "STS" - }, - { - "id": 742492, - "routeNumber": 0, - "routePosition": 0, - "mealType": "Vegetarian", - "program": "MAP" - }, - { - "id": 327428, - "routeNumber": 0, - "routePosition": 0, - "mealType": "Vegetarian", - "program": "STS" - }, - { - "id": 444406, - "routeNumber": 0, - "routePosition": 0, - "mealType": "Regular", - "program": "STS" - }, - { - "id": 943595, - "routeNumber": 0, - "routePosition": 0, - "mealType": "No Fish", - "program": "MAP" - }, - { - "id": 115110, - "routeNumber": 0, - "routePosition": 0, - "mealType": "No Meat", - "program": "STS" - }, - { - "id": 658877, - "routeNumber": 0, - "routePosition": 0, - "mealType": "No Meat", - "program": "MAP" - }, - { - "id": 510872, - "routeNumber": 0, - "routePosition": 0, - "mealType": "Vegetarian", - "program": "STS" - }, - { - "id": 557078, - "routeNumber": 0, - "routePosition": 0, - "mealType": "Regular", - "program": "STS" - }, - { - "id": 868287, - "routeNumber": 0, - "routePosition": 0, - "mealType": "No Fish", - "program": "MAP" - } - ], - "1": [ - { - "id": 111064, - "routeNumber": 0, - "routePosition": 0, - "mealType": "Vegetarian", - "program": "STS" - }, - { - "id": 265140, - "routeNumber": 0, - "routePosition": 0, - "mealType": "Vegetarian", - "program": "STS" - }, - { - "id": 664684, - "routeNumber": 0, - "routePosition": 0, - "mealType": "No Meat", - "program": "MAP" - }, - { - "id": 817055, - "routeNumber": 0, - "routePosition": 0, - "mealType": "Regular", - "program": "MAP" - }, - { - "id": 654993, - "routeNumber": 0, - "routePosition": 0, - "mealType": "No Meat", - "program": "STS" - }, - { - "id": 565347, - "routeNumber": 0, - "routePosition": 0, - "mealType": "No Fish", - "program": "STS" - }, - { - "id": 525509, - "routeNumber": 0, - "routePosition": 0, - "mealType": "No Meat", - "program": "MAP" - }, - { - "id": 724160, - "routeNumber": 0, - "routePosition": 0, - "mealType": "No Fish", - "program": "STS" - }, - { - "id": 238324, - "routeNumber": 0, - "routePosition": 0, - "mealType": "No Fish", - "program": "MAP" - }, - { - "id": 613813, - "routeNumber": 0, - "routePosition": 0, - "mealType": "Regular", - "program": "STS" - } - ] -} +// export const response_data: ResponseData = { +// "0": [ +// { +// "id": 851060, +// "routeNumber": 0, +// "routePosition": 0, +// "mealType": "No Meat", +// "program": "STS" +// }, +// { +// "id": 742492, +// "routeNumber": 0, +// "routePosition": 0, +// "mealType": "Vegetarian", +// "program": "MAP" +// }, +// { +// "id": 327428, +// "routeNumber": 0, +// "routePosition": 0, +// "mealType": "Vegetarian", +// "program": "STS" +// }, +// { +// "id": 444406, +// "routeNumber": 0, +// "routePosition": 0, +// "mealType": "Regular", +// "program": "STS" +// }, +// { +// "id": 943595, +// "routeNumber": 0, +// "routePosition": 0, +// "mealType": "No Fish", +// "program": "MAP" +// }, +// { +// "id": 115110, +// "routeNumber": 0, +// "routePosition": 0, +// "mealType": "No Meat", +// "program": "STS" +// }, +// { +// "id": 658877, +// "routeNumber": 0, +// "routePosition": 0, +// "mealType": "No Meat", +// "program": "MAP" +// }, +// { +// "id": 510872, +// "routeNumber": 0, +// "routePosition": 0, +// "mealType": "Vegetarian", +// "program": "STS" +// }, +// { +// "id": 557078, +// "routeNumber": 0, +// "routePosition": 0, +// "mealType": "Regular", +// "program": "STS" +// }, +// { +// "id": 868287, +// "routeNumber": 0, +// "routePosition": 0, +// "mealType": "No Fish", +// "program": "MAP" +// } +// ], +// "1": [ +// { +// "id": 111064, +// "routeNumber": 0, +// "routePosition": 0, +// "mealType": "Vegetarian", +// "program": "STS" +// }, +// { +// "id": 265140, +// "routeNumber": 0, +// "routePosition": 0, +// "mealType": "Vegetarian", +// "program": "STS" +// }, +// { +// "id": 664684, +// "routeNumber": 0, +// "routePosition": 0, +// "mealType": "No Meat", +// "program": "MAP" +// }, +// { +// "id": 817055, +// "routeNumber": 0, +// "routePosition": 0, +// "mealType": "Regular", +// "program": "MAP" +// }, +// { +// "id": 654993, +// "routeNumber": 0, +// "routePosition": 0, +// "mealType": "No Meat", +// "program": "STS" +// }, +// { +// "id": 565347, +// "routeNumber": 0, +// "routePosition": 0, +// "mealType": "No Fish", +// "program": "STS" +// }, +// { +// "id": 525509, +// "routeNumber": 0, +// "routePosition": 0, +// "mealType": "No Meat", +// "program": "MAP" +// }, +// { +// "id": 724160, +// "routeNumber": 0, +// "routePosition": 0, +// "mealType": "No Fish", +// "program": "STS" +// }, +// { +// "id": 238324, +// "routeNumber": 0, +// "routePosition": 0, +// "mealType": "No Fish", +// "program": "MAP" +// }, +// { +// "id": 613813, +// "routeNumber": 0, +// "routePosition": 0, +// "mealType": "Regular", +// "program": "STS" +// } +// ], +// "2": [ +// { +// "id": 440557, +// "routeNumber": 0, +// "routePosition": 0, +// "mealType": "No Fish", +// "program": "STS" +// }, +// { +// "id": 634464, +// "routeNumber": 0, +// "routePosition": 0, +// "mealType": "Regular", +// "program": "STS" +// }, +// { +// "id": 740684, +// "routeNumber": 0, +// "routePosition": 0, +// "mealType": "Vegetarian", +// "program": "MAP" +// }, +// { +// "id": 757312, +// "routeNumber": 0, +// "routePosition": 0, +// "mealType": "Vegetarian", +// "program": "STS" +// }, +// { +// "id": 954959, +// "routeNumber": 0, +// "routePosition": 0, +// "mealType": "Regular", +// "program": "MAP" +// } +// ], +// "3": [ +// { +// "id": 124694, +// "routeNumber": 0, +// "routePosition": 0, +// "mealType": "No Fish", +// "program": "STS" +// }, +// { +// "id": 621723, +// "routeNumber": 0, +// "routePosition": 0, +// "mealType": "Regular", +// "program": "MAP" +// }, +// { +// "id": 422920, +// "routeNumber": 0, +// "routePosition": 0, +// "mealType": "No Meat", +// "program": "STS" +// }, +// { +// "id": 334091, +// "routeNumber": 0, +// "routePosition": 0, +// "mealType": "No Fish", +// "program": "STS" +// }, +// { +// "id": 681240, +// "routeNumber": 0, +// "routePosition": 0, +// "mealType": "No Fish", +// "program": "STS" +// } +// ], +// "4": [ +// { +// "id": 744614, +// "routeNumber": 0, +// "routePosition": 0, +// "mealType": "Regular", +// "program": "STS" +// }, +// { +// "id": 846544, +// "routeNumber": 0, +// "routePosition": 0, +// "mealType": "No Fish", +// "program": "MAP" +// }, +// { +// "id": 727924, +// "routeNumber": 0, +// "routePosition": 0, +// "mealType": "No Meat", +// "program": "STS" +// }, +// { +// "id": 984339, +// "routeNumber": 0, +// "routePosition": 0, +// "mealType": "No Fish", +// "program": "STS" +// }, +// { +// "id": 293449, +// "routeNumber": 0, +// "routePosition": 0, +// "mealType": "No Fish", +// "program": "MAP" +// } +// ] +// } export default function Board(props: {data: any}) { + // const [columns, setColumns] = useState(response_data); const [columns, setColumns] = useState(props.data as ResponseData); const [activeId, setActiveId] = useState(null); const [clonedItems, setClonedItems] = useState(null); @@ -281,7 +394,7 @@ export default function Board(props: {data: any}) { setClonedItems(null); }; - return (
+ return ( -
- { - Object.entries(columns).map(([column, data]) => ( - - )) - } -
+ + { + Object.entries(columns).map(([column, data]) => ( + + )) + } + { // how to set overlay @@ -304,5 +425,5 @@ export default function Board(props: {data: any}) { }
-
); + ); } diff --git a/frontend-admin/src/components/routes/dnd-view-edit/card.tsx b/frontend-admin/src/components/routes/dnd-view-edit/card.tsx index 332b5b49..f9ae7533 100644 --- a/frontend-admin/src/components/routes/dnd-view-edit/card.tsx +++ b/frontend-admin/src/components/routes/dnd-view-edit/card.tsx @@ -1,8 +1,9 @@ import { useSortable } from "@dnd-kit/sortable"; import {RouteDelivery} from './types'; import { CSS } from "@dnd-kit/utilities"; +import {Box, Typography, CardContent, Card} from '@mui/material'; -export default function Card(props: {data: RouteDelivery}) { +export default function SortableCard(props: {data: RouteDelivery}) { const { attributes, listeners, setNodeRef, transform } = useSortable({ id: props.data.id, // disabled: true // how to disable @@ -11,7 +12,31 @@ export default function Card(props: {data: RouteDelivery}) { const style = { transform: CSS.Transform.toString(transform) }; + return
-

{props.data.id}

+
; } + +export type ItemType = { + id: number; + mealType: string; + program: string +}; + +export function DesignCard({ id, mealType, program }: ItemType) { + return ( + + + {id} + {mealType} + {program} + + + ); +} \ No newline at end of file diff --git a/frontend-admin/src/components/routes/dnd-view-edit/column.tsx b/frontend-admin/src/components/routes/dnd-view-edit/column.tsx index 94c85605..9fcdfaee 100644 --- a/frontend-admin/src/components/routes/dnd-view-edit/column.tsx +++ b/frontend-admin/src/components/routes/dnd-view-edit/column.tsx @@ -1,20 +1,35 @@ import { RouteDelivery } from "./types"; import { SortableContext, rectSortingStrategy } from "@dnd-kit/sortable"; import { useDroppable } from "@dnd-kit/core"; -import Card from './card'; +import SortableCard from './card'; +import {Box, Typography, Paper, Stack, Divider} from '@mui/material'; export default function Column(props: {column: string, data: RouteDelivery[]}) { const { setNodeRef } = useDroppable({ id: props.column }); return ( -
- -
column: {props.column}
- { - props.data.map((d: RouteDelivery) => ) - } -
+ + + Route: {props.column} + + + + { + props.data.map((d: RouteDelivery) => ) + } + +
); } From 1f393d03f70fa446876498ea219dd83a036cf141 Mon Sep 17 00:00:00 2001 From: Sophearah Suy-Puth Date: Mon, 26 Feb 2024 15:55:30 +0700 Subject: [PATCH 14/67] swap between view / edit mode --- .../src/components/routes/dnd-view-edit/board.tsx | 4 ++-- .../src/components/routes/dnd-view-edit/card.tsx | 4 ++-- .../src/components/routes/dnd-view-edit/column.tsx | 4 ++-- frontend-admin/src/components/routes/page.tsx | 11 +++++------ 4 files changed, 11 insertions(+), 12 deletions(-) diff --git a/frontend-admin/src/components/routes/dnd-view-edit/board.tsx b/frontend-admin/src/components/routes/dnd-view-edit/board.tsx index c7c1d9e2..e1c0a289 100644 --- a/frontend-admin/src/components/routes/dnd-view-edit/board.tsx +++ b/frontend-admin/src/components/routes/dnd-view-edit/board.tsx @@ -278,7 +278,7 @@ import {Box, Typography, Stack, Grid} from '@mui/material'; // ] // } -export default function Board(props: {data: any}) { +export default function Board(props: {data: any, editEnabled: boolean}) { // const [columns, setColumns] = useState(response_data); const [columns, setColumns] = useState(props.data as ResponseData); const [activeId, setActiveId] = useState(null); @@ -414,7 +414,7 @@ export default function Board(props: {data: any}) { > { Object.entries(columns).map(([column, data]) => ( - + )) } diff --git a/frontend-admin/src/components/routes/dnd-view-edit/card.tsx b/frontend-admin/src/components/routes/dnd-view-edit/card.tsx index f9ae7533..faecf243 100644 --- a/frontend-admin/src/components/routes/dnd-view-edit/card.tsx +++ b/frontend-admin/src/components/routes/dnd-view-edit/card.tsx @@ -3,10 +3,10 @@ import {RouteDelivery} from './types'; import { CSS } from "@dnd-kit/utilities"; import {Box, Typography, CardContent, Card} from '@mui/material'; -export default function SortableCard(props: {data: RouteDelivery}) { +export default function SortableCard(props: {data: RouteDelivery, editEnabled: boolean}) { const { attributes, listeners, setNodeRef, transform } = useSortable({ id: props.data.id, - // disabled: true // how to disable + disabled: !props.editEnabled }); const style = { diff --git a/frontend-admin/src/components/routes/dnd-view-edit/column.tsx b/frontend-admin/src/components/routes/dnd-view-edit/column.tsx index 9fcdfaee..54823235 100644 --- a/frontend-admin/src/components/routes/dnd-view-edit/column.tsx +++ b/frontend-admin/src/components/routes/dnd-view-edit/column.tsx @@ -4,7 +4,7 @@ import { useDroppable } from "@dnd-kit/core"; import SortableCard from './card'; import {Box, Typography, Paper, Stack, Divider} from '@mui/material'; -export default function Column(props: {column: string, data: RouteDelivery[]}) { +export default function Column(props: {column: string, data: RouteDelivery[], editEnabled: boolean}) { const { setNodeRef } = useDroppable({ id: props.column }); return ( @@ -26,7 +26,7 @@ export default function Column(props: {column: string, data: RouteDelivery[]}) { { - props.data.map((d: RouteDelivery) => ) + props.data.map((d: RouteDelivery) => ) } diff --git a/frontend-admin/src/components/routes/page.tsx b/frontend-admin/src/components/routes/page.tsx index 8d6d21f6..5cc5e09e 100644 --- a/frontend-admin/src/components/routes/page.tsx +++ b/frontend-admin/src/components/routes/page.tsx @@ -20,16 +20,15 @@ NOTES: const RoutesPage = () => { const { isLoading, isError, data, error } = useQuery(['routeDeliveries'], () => getRouteDeliveries()) - - const [mode, setMode] = useState("view") + const [editEnabled, setEditEnabled] = useState(false) const Header = () => { return ( - Mode: {mode} + Mode: {editEnabled.toString()} mode == "view" ? setMode("transfer") : setMode("view"), - label: "Edit" + handler: () => editEnabled == false ? setEditEnabled(true) : setEditEnabled(false), + label: editEnabled == false ? "Edit" : "View" }]}/> ) @@ -39,7 +38,7 @@ const RoutesPage = () => { }> {!isLoading && <> - + } From 347f44f6e1f484eeff1ab1b0c586c188359504d9 Mon Sep 17 00:00:00 2001 From: Sophearah Suy-Puth Date: Mon, 26 Feb 2024 16:12:43 +0700 Subject: [PATCH 15/67] setup backend functions for saving route, route page header --- backend/src/controllers/routeDelivery.ts | 4 +++ backend/src/routes/routeDelivery.routes.ts | 1 + frontend-admin/src/api/route-deliveries.ts | 9 +++++ frontend-admin/src/components/routes/page.tsx | 33 ++++++++++++++++--- 4 files changed, 43 insertions(+), 4 deletions(-) diff --git a/backend/src/controllers/routeDelivery.ts b/backend/src/controllers/routeDelivery.ts index d8f20789..d036d638 100644 --- a/backend/src/controllers/routeDelivery.ts +++ b/backend/src/controllers/routeDelivery.ts @@ -115,4 +115,8 @@ export default class RouteDeliveryController { return routes.length || 0; } + + saveAllRouteDeliveries = async (routes) => { + console.log("routes ", routes); + } } diff --git a/backend/src/routes/routeDelivery.routes.ts b/backend/src/routes/routeDelivery.routes.ts index 00a0cb56..6c5ad5f5 100644 --- a/backend/src/routes/routeDelivery.routes.ts +++ b/backend/src/routes/routeDelivery.routes.ts @@ -6,6 +6,7 @@ export const router = express.Router(); const routeDeliveryController = new RouteDeliveryController(); router.get('/route_delivery/', routeDeliveryController.getRouteDeliveries); +router.post('/route_delivery/', routeDeliveryController.saveAllRouteDeliveries); // TODO: review this? v router.get('/route_delivery_simple/', routeDeliveryController.getRouteDeliveriesSimple); router.put('/route_delivery/:id/set', routeDeliveryController.setRouteNumber); diff --git a/frontend-admin/src/api/route-deliveries.ts b/frontend-admin/src/api/route-deliveries.ts index d8f7dd34..9556e32d 100644 --- a/frontend-admin/src/api/route-deliveries.ts +++ b/frontend-admin/src/api/route-deliveries.ts @@ -46,3 +46,12 @@ export const decreaseRouteDeliveryPosition = async (id: number) => { return response } + +export const saveAllRouteDeliveries = async () => { + const response = await AxiosInstance({ + method: "post", + url: "/route_delivery", + }); + + return response +} diff --git a/frontend-admin/src/components/routes/page.tsx b/frontend-admin/src/components/routes/page.tsx index 5cc5e09e..1375dd28 100644 --- a/frontend-admin/src/components/routes/page.tsx +++ b/frontend-admin/src/components/routes/page.tsx @@ -21,15 +21,40 @@ NOTES: const RoutesPage = () => { const { isLoading, isError, data, error } = useQuery(['routeDeliveries'], () => getRouteDeliveries()) const [editEnabled, setEditEnabled] = useState(false) + + const handleEnableEdit = () => { + setEditEnabled(true) + } + + const handleCancelEdit = () => { + setEditEnabled(false) + console.log("CANCEL EDITS") + } + + const handleSaveEdit = () => { + setEditEnabled(false) + console.log("SAVE EDITS") + } const Header = () => { return ( Mode: {editEnabled.toString()} - editEnabled == false ? setEditEnabled(true) : setEditEnabled(false), - label: editEnabled == false ? "Edit" : "View" - }]}/> + + {editEnabled == false ? <> + + : <> + + } ) } From d21096eed9f4dff9f132bd81d589ea2a1f681a2a Mon Sep 17 00:00:00 2001 From: Sophearah Suy-Puth Date: Mon, 26 Feb 2024 16:28:14 +0700 Subject: [PATCH 16/67] rename column/card -> route/stop details --- .../components/routes/dnd-view-edit/board.tsx | 4 +- .../routes/dnd-view-edit/column.tsx | 35 ------------ .../routes/dnd-view-edit/route-details.tsx | 56 +++++++++++++++++++ .../{card.tsx => stop-details.tsx} | 25 +++++---- 4 files changed, 73 insertions(+), 47 deletions(-) delete mode 100644 frontend-admin/src/components/routes/dnd-view-edit/column.tsx create mode 100644 frontend-admin/src/components/routes/dnd-view-edit/route-details.tsx rename frontend-admin/src/components/routes/dnd-view-edit/{card.tsx => stop-details.tsx} (58%) diff --git a/frontend-admin/src/components/routes/dnd-view-edit/board.tsx b/frontend-admin/src/components/routes/dnd-view-edit/board.tsx index e1c0a289..d8065bda 100644 --- a/frontend-admin/src/components/routes/dnd-view-edit/board.tsx +++ b/frontend-admin/src/components/routes/dnd-view-edit/board.tsx @@ -1,5 +1,5 @@ import React, {useState} from "react"; -import Column from './column'; +import SortableRouteDetails from './route-details'; import { closestCorners, DndContext, @@ -414,7 +414,7 @@ export default function Board(props: {data: any, editEnabled: boolean}) { > { Object.entries(columns).map(([column, data]) => ( - + )) } diff --git a/frontend-admin/src/components/routes/dnd-view-edit/column.tsx b/frontend-admin/src/components/routes/dnd-view-edit/column.tsx deleted file mode 100644 index 54823235..00000000 --- a/frontend-admin/src/components/routes/dnd-view-edit/column.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import { RouteDelivery } from "./types"; -import { SortableContext, rectSortingStrategy } from "@dnd-kit/sortable"; -import { useDroppable } from "@dnd-kit/core"; -import SortableCard from './card'; -import {Box, Typography, Paper, Stack, Divider} from '@mui/material'; - -export default function Column(props: {column: string, data: RouteDelivery[], editEnabled: boolean}) { - const { setNodeRef } = useDroppable({ id: props.column }); - - return ( - - - - Route: {props.column} - - - - { - props.data.map((d: RouteDelivery) => ) - } - - - - ); -} diff --git a/frontend-admin/src/components/routes/dnd-view-edit/route-details.tsx b/frontend-admin/src/components/routes/dnd-view-edit/route-details.tsx new file mode 100644 index 00000000..301d625f --- /dev/null +++ b/frontend-admin/src/components/routes/dnd-view-edit/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/dnd-view-edit/card.tsx b/frontend-admin/src/components/routes/dnd-view-edit/stop-details.tsx similarity index 58% rename from frontend-admin/src/components/routes/dnd-view-edit/card.tsx rename to frontend-admin/src/components/routes/dnd-view-edit/stop-details.tsx index faecf243..bebc8c4d 100644 --- a/frontend-admin/src/components/routes/dnd-view-edit/card.tsx +++ b/frontend-admin/src/components/routes/dnd-view-edit/stop-details.tsx @@ -1,12 +1,17 @@ import { useSortable } from "@dnd-kit/sortable"; import {RouteDelivery} from './types'; import { CSS } from "@dnd-kit/utilities"; -import {Box, Typography, CardContent, Card} from '@mui/material'; +import {Typography, CardContent, Card} from '@mui/material'; -export default function SortableCard(props: {data: RouteDelivery, editEnabled: boolean}) { +type SortableStopDetailsProps = { + data: RouteDelivery; + editEnabled: boolean +}; + +export default function SortableStopDetails({data, editEnabled}: SortableStopDetailsProps) { const { attributes, listeners, setNodeRef, transform } = useSortable({ - id: props.data.id, - disabled: !props.editEnabled + id: data.id, + disabled: !editEnabled }); const style = { @@ -14,22 +19,22 @@ export default function SortableCard(props: {data: RouteDelivery, editEnabled: b }; return
-
; } -export type ItemType = { +type StopDetailsProps = { id: number; mealType: string; program: string }; -export function DesignCard({ id, mealType, program }: ItemType) { +function StopDetails({ id, mealType, program }: StopDetailsProps) { return ( From cc63610a08de7430a21d2b779defb87a28037570 Mon Sep 17 00:00:00 2001 From: Sophearah Suy-Puth Date: Mon, 26 Feb 2024 17:34:52 +0700 Subject: [PATCH 17/67] save edited routes, setup board actions --- backend/src/controllers/routeDelivery.ts | 17 +++- frontend-admin/src/api/route-deliveries.ts | 3 +- .../components/routes/dnd-view-edit/board.tsx | 92 +++++++++++++++---- frontend-admin/src/components/routes/page.tsx | 19 +++- 4 files changed, 104 insertions(+), 27 deletions(-) diff --git a/backend/src/controllers/routeDelivery.ts b/backend/src/controllers/routeDelivery.ts index d036d638..450c8ba2 100644 --- a/backend/src/controllers/routeDelivery.ts +++ b/backend/src/controllers/routeDelivery.ts @@ -116,7 +116,20 @@ export default class RouteDeliveryController { return routes.length || 0; } - saveAllRouteDeliveries = async (routes) => { - console.log("routes ", routes); + saveAllRouteDeliveries = async (request: Request, response: Response) => { + const routes = request.body.routes + + // for each column + Object.entries(routes).map(([column, data]) => { + // for each stop + routes[column].map(async (stop, index) => { + const updatedStop = await this.RouteDeliveryRepository.update( + { id: parseInt(stop.id) }, + { routeNumber: parseInt(column), routePosition: index} + ); + }) + }) + + response.status(StatusCode.OK).json({ routes: "fd" }); } } diff --git a/frontend-admin/src/api/route-deliveries.ts b/frontend-admin/src/api/route-deliveries.ts index 9556e32d..8fe79024 100644 --- a/frontend-admin/src/api/route-deliveries.ts +++ b/frontend-admin/src/api/route-deliveries.ts @@ -47,10 +47,11 @@ export const decreaseRouteDeliveryPosition = async (id: number) => { return response } -export const saveAllRouteDeliveries = async () => { +export const saveAllRouteDeliveries = async (editRoutes: any) => { const response = await AxiosInstance({ method: "post", url: "/route_delivery", + data: {routes: editRoutes} }); return response diff --git a/frontend-admin/src/components/routes/dnd-view-edit/board.tsx b/frontend-admin/src/components/routes/dnd-view-edit/board.tsx index d8065bda..9180cbf4 100644 --- a/frontend-admin/src/components/routes/dnd-view-edit/board.tsx +++ b/frontend-admin/src/components/routes/dnd-view-edit/board.tsx @@ -1,4 +1,4 @@ -import React, {useState} from "react"; +import React, {useEffect, useState} from "react"; import SortableRouteDetails from './route-details'; import { closestCorners, @@ -19,6 +19,9 @@ import { import { arrayMove, sortableKeyboardCoordinates } from "@dnd-kit/sortable"; import { RouteDelivery, ResponseData } from "./types"; import {Box, Typography, Stack, Grid} from '@mui/material'; +import {BoardAction} from '../page'; +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import {saveAllRouteDeliveries} from 'src/api/route-deliveries' // export const response_data: ResponseData = { // "0": [ @@ -278,12 +281,56 @@ import {Box, Typography, Stack, Grid} from '@mui/material'; // ] // } -export default function Board(props: {data: any, editEnabled: boolean}) { - // const [columns, setColumns] = useState(response_data); - const [columns, setColumns] = useState(props.data as ResponseData); + +type BoardProps = { + data: any; + editEnabled: boolean; + boardAction: BoardAction; + setBoardAction: any; + refetch: any; +}; + +export default function Board({data, editEnabled, boardAction, setBoardAction, refetch}: BoardProps) { + 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 saveAllRouteDeliveries(data), + onSuccess: async () => { + return queryClient.invalidateQueries(['routeDeliveries']) + }, + }); + + // board action + useEffect(() => { + if (boardAction == BoardAction.VIEW) { + setViewRoutes(data) + setEditRoutes(null) + } else if (boardAction == BoardAction.EDIT) { + setEditRoutes(data) + } else if (boardAction == BoardAction.CANCEL) { + setViewRoutes(data) + setEditRoutes(null) + setBoardAction(BoardAction.VIEW) + } else if (boardAction == BoardAction.SAVE) { + const req = async () => { + // TODO: not updating immediately, + // must refresh page for changes to show + await saveAllRouteDeliveriesMutation.mutate(editRoutes) + setBoardAction(BoardAction.VIEW) + setViewRoutes(data) + setEditRoutes(null) + } + + req() + } + }, [boardAction]) + const sensors = useSensors( useSensor(PointerSensor), useSensor(KeyboardSensor, { @@ -304,7 +351,7 @@ export default function Board(props: {data: any, editEnabled: boolean}) { const handleDragStart = (event: DragStartEvent) => { const { active } = event; setActiveId(active.id); - setClonedItems(columns); + setClonedItems(editRoutes); } const handleDragEnd = (event: DragEndEvent) => { @@ -318,15 +365,15 @@ export default function Board(props: {data: any, editEnabled: boolean}) { return null; } - const activeIndex = columns[activeColumnId].findIndex((i) => i.id === activeId); - const overIndex = columns[overColumnId].findIndex((i) => i.id === overId); + const activeIndex = editRoutes![activeColumnId].findIndex((i) => i.id === activeId); + const overIndex = editRoutes![overColumnId].findIndex((i) => i.id === overId); if (activeIndex !== overIndex) { - setColumns((prevState) => { + setEditRoutes((prevState) => { const d:ResponseData = {} - Object.entries(prevState).map(([column, data]) => { + Object.entries(prevState!).map(([column, data]) => { if (column === activeColumnId) { - data = arrayMove(columns[overColumnId], activeIndex, overIndex); + data = arrayMove(editRoutes![overColumnId], activeIndex, overIndex); d[column] = data } else { d[column] = data @@ -349,9 +396,9 @@ export default function Board(props: {data: any, editEnabled: boolean}) { return null; } - setColumns((prevState) => { - const activeItems = columns[activeColumnId]; - const overItems = columns[overColumnId]; + setEditRoutes((prevState) => { + const activeItems = editRoutes![activeColumnId]; + const overItems = editRoutes![overColumnId]; const activeIndex = activeItems.findIndex((i) => i.id === activeId); const overIndex = overItems.findIndex((i) => i.id === overId); @@ -364,7 +411,7 @@ export default function Board(props: {data: any, editEnabled: boolean}) { const d: ResponseData = {} - Object.entries(prevState).map(([column, data]) => { + Object.entries(prevState!).map(([column, data]) => { if (column === activeColumnId) { data = activeItems.filter((i) => i.id !== activeId); d[column] = data @@ -387,7 +434,7 @@ export default function Board(props: {data: any, editEnabled: boolean}) { if (clonedItems) { // Reset items to their original state in case items have been // Dragged across containers - setColumns(clonedItems); + setEditRoutes(clonedItems); } setActiveId(null); @@ -412,11 +459,18 @@ export default function Board(props: {data: any, editEnabled: boolean}) { overflow: 'auto' }} > - { - Object.entries(columns).map(([column, data]) => ( - + { boardAction == BoardAction.VIEW && viewRoutes && + Object.entries(viewRoutes).map(([column, data]) => ( + )) - } + } + + { boardAction == BoardAction.EDIT && editRoutes && + Object.entries(editRoutes).map(([column, data]) => ( + + )) + } + { diff --git a/frontend-admin/src/components/routes/page.tsx b/frontend-admin/src/components/routes/page.tsx index 1375dd28..1f6486ff 100644 --- a/frontend-admin/src/components/routes/page.tsx +++ b/frontend-admin/src/components/routes/page.tsx @@ -18,22 +18,31 @@ NOTES: dnd-view-edit: good implementation of dnd-kit */ +export enum BoardAction { + VIEW = "view", + EDIT = "edit", + CANCEL = "cancel", + SAVE = "save", +} + const RoutesPage = () => { - const { isLoading, isError, data, error } = useQuery(['routeDeliveries'], () => getRouteDeliveries()) + const { isLoading, isError, data, error, refetch } = useQuery(['routeDeliveries'], () => getRouteDeliveries()) const [editEnabled, setEditEnabled] = useState(false) - + const [boardAction, setBoardAction] = useState(BoardAction.VIEW) + const handleEnableEdit = () => { setEditEnabled(true) + setBoardAction(BoardAction.EDIT) } const handleCancelEdit = () => { setEditEnabled(false) - console.log("CANCEL EDITS") + setBoardAction(BoardAction.CANCEL) } const handleSaveEdit = () => { setEditEnabled(false) - console.log("SAVE EDITS") + setBoardAction(BoardAction.SAVE) } const Header = () => { @@ -63,7 +72,7 @@ const RoutesPage = () => { }> {!isLoading && <> - + } From 7a61fdf28d422f8fd4e91b3b3a96ae82f388ae84 Mon Sep 17 00:00:00 2001 From: Sophearah Suy-Puth Date: Mon, 26 Feb 2024 21:09:49 +0700 Subject: [PATCH 18/67] refresh data when saving routes --- .../components/routes/dnd-view-edit/board.tsx | 45 +++++++++---------- frontend-admin/src/components/routes/page.tsx | 12 ++--- 2 files changed, 24 insertions(+), 33 deletions(-) diff --git a/frontend-admin/src/components/routes/dnd-view-edit/board.tsx b/frontend-admin/src/components/routes/dnd-view-edit/board.tsx index 9180cbf4..38c779aa 100644 --- a/frontend-admin/src/components/routes/dnd-view-edit/board.tsx +++ b/frontend-admin/src/components/routes/dnd-view-edit/board.tsx @@ -22,6 +22,10 @@ import {Box, Typography, Stack, Grid} from '@mui/material'; import {BoardAction} from '../page'; import { useMutation, useQueryClient } from "@tanstack/react-query"; import {saveAllRouteDeliveries} from 'src/api/route-deliveries' +import { + useQuery, +} from '@tanstack/react-query' +import {getRouteDeliveries} from 'src/api/route-deliveries' // export const response_data: ResponseData = { // "0": [ @@ -281,16 +285,14 @@ import {saveAllRouteDeliveries} from 'src/api/route-deliveries' // ] // } - type BoardProps = { - data: any; - editEnabled: boolean; boardAction: BoardAction; setBoardAction: any; - refetch: any; }; -export default function Board({data, editEnabled, boardAction, setBoardAction, refetch}: BoardProps) { +export default function Board({boardAction, setBoardAction}: BoardProps) { + const { isLoading, isError, data, error, refetch, isStale } = useQuery(['routeDeliveries'], () => getRouteDeliveries()) + const [viewRoutes, setViewRoutes] = useState(null); const [editRoutes, setEditRoutes] = useState(null); @@ -301,35 +303,28 @@ export default function Board({data, editEnabled, boardAction, setBoardAction, r const saveAllRouteDeliveriesMutation = useMutation({ mutationFn: async (data: any) => await saveAllRouteDeliveries(data), - onSuccess: async () => { - return queryClient.invalidateQueries(['routeDeliveries']) + onSuccess: () => { + queryClient.invalidateQueries(['routeDeliveries']) + refetch() + const d = data }, }); - // board action useEffect(() => { if (boardAction == BoardAction.VIEW) { - setViewRoutes(data) + setViewRoutes(data?.data.routes) setEditRoutes(null) } else if (boardAction == BoardAction.EDIT) { - setEditRoutes(data) + setEditRoutes(data?.data.routes) } else if (boardAction == BoardAction.CANCEL) { - setViewRoutes(data) + setViewRoutes(data?.data.routes) setEditRoutes(null) setBoardAction(BoardAction.VIEW) } else if (boardAction == BoardAction.SAVE) { - const req = async () => { - // TODO: not updating immediately, - // must refresh page for changes to show - await saveAllRouteDeliveriesMutation.mutate(editRoutes) - setBoardAction(BoardAction.VIEW) - setViewRoutes(data) - setEditRoutes(null) - } - - req() - } - }, [boardAction]) + saveAllRouteDeliveriesMutation.mutate(editRoutes) + setBoardAction(BoardAction.VIEW) + } + }, [boardAction, data]) const sensors = useSensors( useSensor(PointerSensor), @@ -461,13 +456,13 @@ export default function Board({data, editEnabled, boardAction, setBoardAction, r > { boardAction == BoardAction.VIEW && viewRoutes && Object.entries(viewRoutes).map(([column, data]) => ( - + )) } { boardAction == BoardAction.EDIT && editRoutes && Object.entries(editRoutes).map(([column, data]) => ( - + )) } diff --git a/frontend-admin/src/components/routes/page.tsx b/frontend-admin/src/components/routes/page.tsx index 1f6486ff..a4c5a5b5 100644 --- a/frontend-admin/src/components/routes/page.tsx +++ b/frontend-admin/src/components/routes/page.tsx @@ -27,30 +27,26 @@ export enum BoardAction { const RoutesPage = () => { const { isLoading, isError, data, error, refetch } = useQuery(['routeDeliveries'], () => getRouteDeliveries()) - const [editEnabled, setEditEnabled] = useState(false) const [boardAction, setBoardAction] = useState(BoardAction.VIEW) const handleEnableEdit = () => { - setEditEnabled(true) setBoardAction(BoardAction.EDIT) } const handleCancelEdit = () => { - setEditEnabled(false) setBoardAction(BoardAction.CANCEL) } const handleSaveEdit = () => { - setEditEnabled(false) setBoardAction(BoardAction.SAVE) } const Header = () => { return ( - Mode: {editEnabled.toString()} + Mode: {boardAction.toString()} - {editEnabled == false ? <> + {boardAction == BoardAction.VIEW ? <> { ) } - + return ( }> {!isLoading && <> - + } From 1bb8bd0742f58f920bfd51e55ee6cacbfb37a2b0 Mon Sep 17 00:00:00 2001 From: Sophearah Suy-Puth Date: Tue, 19 Mar 2024 09:52:59 +0700 Subject: [PATCH 19/67] create new route in route editor --- .../components/routes/dnd-view-edit/board.tsx | 294 ++---------------- 1 file changed, 30 insertions(+), 264 deletions(-) diff --git a/frontend-admin/src/components/routes/dnd-view-edit/board.tsx b/frontend-admin/src/components/routes/dnd-view-edit/board.tsx index 38c779aa..5b0a08e4 100644 --- a/frontend-admin/src/components/routes/dnd-view-edit/board.tsx +++ b/frontend-admin/src/components/routes/dnd-view-edit/board.tsx @@ -18,7 +18,8 @@ import { } from "@dnd-kit/core"; import { arrayMove, sortableKeyboardCoordinates } from "@dnd-kit/sortable"; import { RouteDelivery, ResponseData } from "./types"; -import {Box, Typography, Stack, Grid} from '@mui/material'; +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 {saveAllRouteDeliveries} from 'src/api/route-deliveries' @@ -27,264 +28,6 @@ import { } from '@tanstack/react-query' import {getRouteDeliveries} from 'src/api/route-deliveries' -// export const response_data: ResponseData = { -// "0": [ -// { -// "id": 851060, -// "routeNumber": 0, -// "routePosition": 0, -// "mealType": "No Meat", -// "program": "STS" -// }, -// { -// "id": 742492, -// "routeNumber": 0, -// "routePosition": 0, -// "mealType": "Vegetarian", -// "program": "MAP" -// }, -// { -// "id": 327428, -// "routeNumber": 0, -// "routePosition": 0, -// "mealType": "Vegetarian", -// "program": "STS" -// }, -// { -// "id": 444406, -// "routeNumber": 0, -// "routePosition": 0, -// "mealType": "Regular", -// "program": "STS" -// }, -// { -// "id": 943595, -// "routeNumber": 0, -// "routePosition": 0, -// "mealType": "No Fish", -// "program": "MAP" -// }, -// { -// "id": 115110, -// "routeNumber": 0, -// "routePosition": 0, -// "mealType": "No Meat", -// "program": "STS" -// }, -// { -// "id": 658877, -// "routeNumber": 0, -// "routePosition": 0, -// "mealType": "No Meat", -// "program": "MAP" -// }, -// { -// "id": 510872, -// "routeNumber": 0, -// "routePosition": 0, -// "mealType": "Vegetarian", -// "program": "STS" -// }, -// { -// "id": 557078, -// "routeNumber": 0, -// "routePosition": 0, -// "mealType": "Regular", -// "program": "STS" -// }, -// { -// "id": 868287, -// "routeNumber": 0, -// "routePosition": 0, -// "mealType": "No Fish", -// "program": "MAP" -// } -// ], -// "1": [ -// { -// "id": 111064, -// "routeNumber": 0, -// "routePosition": 0, -// "mealType": "Vegetarian", -// "program": "STS" -// }, -// { -// "id": 265140, -// "routeNumber": 0, -// "routePosition": 0, -// "mealType": "Vegetarian", -// "program": "STS" -// }, -// { -// "id": 664684, -// "routeNumber": 0, -// "routePosition": 0, -// "mealType": "No Meat", -// "program": "MAP" -// }, -// { -// "id": 817055, -// "routeNumber": 0, -// "routePosition": 0, -// "mealType": "Regular", -// "program": "MAP" -// }, -// { -// "id": 654993, -// "routeNumber": 0, -// "routePosition": 0, -// "mealType": "No Meat", -// "program": "STS" -// }, -// { -// "id": 565347, -// "routeNumber": 0, -// "routePosition": 0, -// "mealType": "No Fish", -// "program": "STS" -// }, -// { -// "id": 525509, -// "routeNumber": 0, -// "routePosition": 0, -// "mealType": "No Meat", -// "program": "MAP" -// }, -// { -// "id": 724160, -// "routeNumber": 0, -// "routePosition": 0, -// "mealType": "No Fish", -// "program": "STS" -// }, -// { -// "id": 238324, -// "routeNumber": 0, -// "routePosition": 0, -// "mealType": "No Fish", -// "program": "MAP" -// }, -// { -// "id": 613813, -// "routeNumber": 0, -// "routePosition": 0, -// "mealType": "Regular", -// "program": "STS" -// } -// ], -// "2": [ -// { -// "id": 440557, -// "routeNumber": 0, -// "routePosition": 0, -// "mealType": "No Fish", -// "program": "STS" -// }, -// { -// "id": 634464, -// "routeNumber": 0, -// "routePosition": 0, -// "mealType": "Regular", -// "program": "STS" -// }, -// { -// "id": 740684, -// "routeNumber": 0, -// "routePosition": 0, -// "mealType": "Vegetarian", -// "program": "MAP" -// }, -// { -// "id": 757312, -// "routeNumber": 0, -// "routePosition": 0, -// "mealType": "Vegetarian", -// "program": "STS" -// }, -// { -// "id": 954959, -// "routeNumber": 0, -// "routePosition": 0, -// "mealType": "Regular", -// "program": "MAP" -// } -// ], -// "3": [ -// { -// "id": 124694, -// "routeNumber": 0, -// "routePosition": 0, -// "mealType": "No Fish", -// "program": "STS" -// }, -// { -// "id": 621723, -// "routeNumber": 0, -// "routePosition": 0, -// "mealType": "Regular", -// "program": "MAP" -// }, -// { -// "id": 422920, -// "routeNumber": 0, -// "routePosition": 0, -// "mealType": "No Meat", -// "program": "STS" -// }, -// { -// "id": 334091, -// "routeNumber": 0, -// "routePosition": 0, -// "mealType": "No Fish", -// "program": "STS" -// }, -// { -// "id": 681240, -// "routeNumber": 0, -// "routePosition": 0, -// "mealType": "No Fish", -// "program": "STS" -// } -// ], -// "4": [ -// { -// "id": 744614, -// "routeNumber": 0, -// "routePosition": 0, -// "mealType": "Regular", -// "program": "STS" -// }, -// { -// "id": 846544, -// "routeNumber": 0, -// "routePosition": 0, -// "mealType": "No Fish", -// "program": "MAP" -// }, -// { -// "id": 727924, -// "routeNumber": 0, -// "routePosition": 0, -// "mealType": "No Meat", -// "program": "STS" -// }, -// { -// "id": 984339, -// "routeNumber": 0, -// "routePosition": 0, -// "mealType": "No Fish", -// "program": "STS" -// }, -// { -// "id": 293449, -// "routeNumber": 0, -// "routePosition": 0, -// "mealType": "No Fish", -// "program": "MAP" -// } -// ] -// } - type BoardProps = { boardAction: BoardAction; setBoardAction: any; @@ -436,6 +179,22 @@ export default function Board({boardAction, setBoardAction}: BoardProps) { 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 ( ( - - )) + <> + { + Object.entries(editRoutes).map(([column, data]) => ( + + )) + } + + + + } - { - // how to set overlay activeId ?
OVERLAY {activeId}
: null }
From 46ba24c3d3ca8cf339644885fef717121376812e Mon Sep 17 00:00:00 2001 From: Sophearah Suy-Puth Date: Thu, 21 Mar 2024 11:46:08 +0700 Subject: [PATCH 20/67] change routedelivery id to be string, save stops dragged to empty routes --- backend/src/controllers/routeDelivery.ts | 14 +-- backend/src/entities/RouteDeliveryEntity.ts | 4 +- .../components/routes/dnd-view-edit/board.tsx | 99 ++++++++++++------- 3 files changed, 73 insertions(+), 44 deletions(-) diff --git a/backend/src/controllers/routeDelivery.ts b/backend/src/controllers/routeDelivery.ts index 450c8ba2..1d99d258 100644 --- a/backend/src/controllers/routeDelivery.ts +++ b/backend/src/controllers/routeDelivery.ts @@ -50,7 +50,7 @@ export default class RouteDeliveryController { const routePosition = request.body.routeNumber == 0 ? 0 : await this.getNextRouteNumber(request.body.routeNumber) const route = await this.RouteDeliveryRepository.update( - { id: parseInt(request.params.id) }, + { id: request.params.id }, { routeNumber: request.body.routeNumber, routePosition: routePosition} ); @@ -58,7 +58,7 @@ export default class RouteDeliveryController { }; incrementRoutePosition = async (request: Request, response: Response) => { - const r = await this.getRouteFromId(parseInt(request.params.id)) + const r = await this.getRouteFromId(request.params.id) await this.RouteDeliveryRepository.decrement( { routeNumber: r.routeNumber, routePosition: r.routePosition + 1 }, @@ -67,7 +67,7 @@ export default class RouteDeliveryController { ); const route = await this.RouteDeliveryRepository.increment( - { id: parseInt(request.params.id) }, + { id: request.params.id }, 'routePosition', 1 ); @@ -76,7 +76,7 @@ export default class RouteDeliveryController { }; decrementRoutePosition = async (request: Request, response: Response) => { - const r = await this.getRouteFromId(parseInt(request.params.id)) + const r = await this.getRouteFromId(request.params.id) await this.RouteDeliveryRepository.increment( { routeNumber: r.routeNumber, routePosition: r.routePosition - 1 }, @@ -85,7 +85,7 @@ export default class RouteDeliveryController { ); const route = await this.RouteDeliveryRepository.decrement( - { id: parseInt(request.params.id) }, + { id: request.params.id }, 'routePosition', 1 ); @@ -95,7 +95,7 @@ export default class RouteDeliveryController { // Helper Functions - getRouteFromId = async (id: number) => { + getRouteFromId = async (id: string) => { const route = await this.RouteDeliveryRepository.findOne({ where: { id: id @@ -124,7 +124,7 @@ export default class RouteDeliveryController { // for each stop routes[column].map(async (stop, index) => { const updatedStop = await this.RouteDeliveryRepository.update( - { id: parseInt(stop.id) }, + { id: stop.id }, { routeNumber: parseInt(column), routePosition: index} ); }) diff --git a/backend/src/entities/RouteDeliveryEntity.ts b/backend/src/entities/RouteDeliveryEntity.ts index 4606ec6c..9dc0470d 100644 --- a/backend/src/entities/RouteDeliveryEntity.ts +++ b/backend/src/entities/RouteDeliveryEntity.ts @@ -4,8 +4,8 @@ import { ProgramType, MealType } from './types'; @Entity() export class RouteDeliveryEntity { - @PrimaryGeneratedColumn() - id: number; + @PrimaryGeneratedColumn("uuid") + id: string; @Column() routeNumber: number; diff --git a/frontend-admin/src/components/routes/dnd-view-edit/board.tsx b/frontend-admin/src/components/routes/dnd-view-edit/board.tsx index 5b0a08e4..a8a0e6ec 100644 --- a/frontend-admin/src/components/routes/dnd-view-edit/board.tsx +++ b/frontend-admin/src/components/routes/dnd-view-edit/board.tsx @@ -1,7 +1,7 @@ import React, {useEffect, useState} from "react"; import SortableRouteDetails from './route-details'; import { - closestCorners, + closestCenter, DndContext, KeyboardSensor, PointerSensor, @@ -86,6 +86,19 @@ export default function Board({boardAction, setBoardAction}: BoardProps) { return item.data.current.sortable.containerId } + 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() + } + const handleDragStart = (event: DragStartEvent) => { const { active } = event; setActiveId(active.id); @@ -94,24 +107,29 @@ export default function Board({boardAction, setBoardAction}: BoardProps) { const handleDragEnd = (event: DragEndEvent) => { const { active, over } = event; - const activeId = active.id; - const overId = over ? over.id : null; - const activeColumnId = findColumnIdFromActive(active); - const overColumnId = over ? findColumnIdFromOver(over) : ""; - - if (!activeColumnId || !overColumnId || activeColumnId !== overColumnId) { - return null; + const { id } = active; + const overId = over!.id! + + const activeContainer = findContainer(id); + const overContainer = findContainer(overId); + + if ( + !activeContainer || + !overContainer || + activeContainer !== overContainer + ) { + return; } - - const activeIndex = editRoutes![activeColumnId].findIndex((i) => i.id === activeId); - const overIndex = editRoutes![overColumnId].findIndex((i) => i.id === overId); - + + 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 === activeColumnId) { - data = arrayMove(editRoutes![overColumnId], activeIndex, overIndex); + if (column === overContainer) { + data = arrayMove(editRoutes![overContainer], activeIndex, overIndex); d[column] = data } else { d[column] = data @@ -121,42 +139,53 @@ export default function Board({boardAction, setBoardAction}: BoardProps) { return d }); } + + setActiveId(null); }; const handleDragOver = (event: DragOverEvent) => { const { active, over, delta } = event; - const activeId = active.id; - const overId = over ? over.id : null; - const activeColumnId = findColumnIdFromActive(active); - const overColumnId = over ? findColumnIdFromOver(over) : null; - - if (!activeColumnId || !overColumnId || activeColumnId === overColumnId) { - return null; + 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![activeColumnId]; - const overItems = editRoutes![overColumnId]; + const activeItems = editRoutes![activeContainer]; + const overItems = editRoutes![overContainer]; const activeIndex = activeItems.findIndex((i) => i.id === activeId); const overIndex = overItems.findIndex((i) => i.id === overId); - const newIndex = () => { - const putOnBelowLastItem = - overIndex === overItems.length - 1 && delta.y > 0; + 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; - return overIndex >= 0 ? overIndex + modifier : overItems.length + 1; - }; - + newIndex = overIndex >= 0 ? overIndex + modifier : overItems.length + 1; + } + const d: ResponseData = {} Object.entries(prevState!).map(([column, data]) => { - if (column === activeColumnId) { + if (column === activeContainer) { data = activeItems.filter((i) => i.id !== activeId); d[column] = data - } else if (column === overColumnId) { - data = [...overItems.slice(0, newIndex()), + } else if (column === overContainer) { + data = [...overItems.slice(0, newIndex), activeItems[activeIndex], - ...overItems.slice(newIndex(), overItems.length), + ...overItems.slice(newIndex, overItems.length), ]; d[column] = data } else { @@ -198,7 +227,7 @@ export default function Board({boardAction, setBoardAction}: BoardProps) { return ( Date: Thu, 21 Mar 2024 11:53:34 +0700 Subject: [PATCH 21/67] drag overlay for stop --- .../components/routes/dnd-view-edit/board.tsx | 19 ++++++++----------- .../routes/dnd-view-edit/stop-details.tsx | 4 ++-- 2 files changed, 10 insertions(+), 13 deletions(-) diff --git a/frontend-admin/src/components/routes/dnd-view-edit/board.tsx b/frontend-admin/src/components/routes/dnd-view-edit/board.tsx index a8a0e6ec..b36448aa 100644 --- a/frontend-admin/src/components/routes/dnd-view-edit/board.tsx +++ b/frontend-admin/src/components/routes/dnd-view-edit/board.tsx @@ -27,6 +27,7 @@ import { useQuery, } from '@tanstack/react-query' import {getRouteDeliveries} from 'src/api/route-deliveries' +import {StopDetails} from "./stop-details"; type BoardProps = { boardAction: BoardAction; @@ -76,16 +77,6 @@ export default function Board({boardAction, setBoardAction}: BoardProps) { }) ) - const findColumnIdFromActive = (item: Active): string => { - if (!item.data.current) return ""; - return item.data.current.sortable.containerId - } - - const findColumnIdFromOver = (item: Over): string => { - if (!item.data.current) return ""; - return item.data.current.sortable.containerId - } - function findContainer(id: UniqueIdentifier) { // Route Id if (Object.keys(editRoutes!).find((key) => key == id)) { @@ -99,6 +90,12 @@ export default function Board({boardAction, setBoardAction}: BoardProps) { 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); @@ -265,7 +262,7 @@ export default function Board({boardAction, setBoardAction}: BoardProps) { { - activeId ?
OVERLAY {activeId}
: null + activeId ? : null }
diff --git a/frontend-admin/src/components/routes/dnd-view-edit/stop-details.tsx b/frontend-admin/src/components/routes/dnd-view-edit/stop-details.tsx index bebc8c4d..89098b31 100644 --- a/frontend-admin/src/components/routes/dnd-view-edit/stop-details.tsx +++ b/frontend-admin/src/components/routes/dnd-view-edit/stop-details.tsx @@ -29,12 +29,12 @@ export default function SortableStopDetails({data, editEnabled}: SortableStopDet } type StopDetailsProps = { - id: number; + id: string; mealType: string; program: string }; -function StopDetails({ id, mealType, program }: StopDetailsProps) { +export function StopDetails({ id, mealType, program }: StopDetailsProps) { return ( From 152093e7b8192c9c902b585a746563b925b1053a Mon Sep 17 00:00:00 2001 From: Sophearah Suy-Puth Date: Thu, 21 Mar 2024 11:57:04 +0700 Subject: [PATCH 22/67] remove old implementations of route editor that dont work well --- .../src/components/routes/board/list.tsx | 63 ------ .../src/components/routes/board/transfer.tsx | 200 ------------------ .../routes/board/transfer/route-buttons.tsx | 57 ----- .../board/transfer/transfer-buttons.tsx | 32 --- .../src/components/routes/board/view.tsx | 19 -- .../src/components/routes/dnd/board.tsx | 181 ---------------- .../src/components/routes/dnd/container.tsx | 38 ---- .../src/components/routes/dnd/item.tsx | 47 ---- .../{dnd-view-edit => editor}/board.tsx | 0 .../route-details.tsx | 0 .../stop-details.tsx | 0 .../routes/{dnd-view-edit => editor}/types.ts | 2 +- frontend-admin/src/components/routes/page.tsx | 13 +- frontend-admin/src/components/tasks/page.tsx | 4 - 14 files changed, 3 insertions(+), 653 deletions(-) delete mode 100644 frontend-admin/src/components/routes/board/list.tsx delete mode 100644 frontend-admin/src/components/routes/board/transfer.tsx delete mode 100644 frontend-admin/src/components/routes/board/transfer/route-buttons.tsx delete mode 100644 frontend-admin/src/components/routes/board/transfer/transfer-buttons.tsx delete mode 100644 frontend-admin/src/components/routes/board/view.tsx delete mode 100644 frontend-admin/src/components/routes/dnd/board.tsx delete mode 100644 frontend-admin/src/components/routes/dnd/container.tsx delete mode 100644 frontend-admin/src/components/routes/dnd/item.tsx rename frontend-admin/src/components/routes/{dnd-view-edit => editor}/board.tsx (100%) rename frontend-admin/src/components/routes/{dnd-view-edit => editor}/route-details.tsx (100%) rename frontend-admin/src/components/routes/{dnd-view-edit => editor}/stop-details.tsx (100%) rename frontend-admin/src/components/routes/{dnd-view-edit => editor}/types.ts (93%) 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 4e7663ce..00000000 --- a/frontend-admin/src/components/routes/board/transfer.tsx +++ /dev/null @@ -1,200 +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 [disabledDecrementPosition, setDisabledDecrementPosition] = useState(true) - const [disabledIncrementPosition, setDisabledIncrementPosition] = 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() - setSelectedRouteDelivery(null) - - setDisabledTransferRight(true) - setDisabledTransferLeft(true) - } - - const handleDecrementPosition = async () => { - await decrementRoutePositionMutation.mutate() - setSelectedRouteDelivery(null) - - setDisabledTransferRight(true) - setDisabledTransferLeft(true) - } - - // 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) - - if (route.routePosition == 0){ - setDisabledDecrementPosition(true) - } else if (route.routePosition == transferRoutes.length - 1) { - setDisabledIncrementPosition(true) - } else { - setDisabledIncrementPosition(false) - setDisabledDecrementPosition(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/dnd/board.tsx b/frontend-admin/src/components/routes/dnd/board.tsx deleted file mode 100644 index 2f135e85..00000000 --- a/frontend-admin/src/components/routes/dnd/board.tsx +++ /dev/null @@ -1,181 +0,0 @@ -import React, { useState, useMemo, useEffect } from "react"; -import { - DndContext, - closestCorners, - KeyboardSensor, - PointerSensor, - useSensor, - useSensors, - DragEndEvent, -} from "@dnd-kit/core"; -import { - SortableContext, - } from "@dnd-kit/sortable"; -import { arrayMove, sortableKeyboardCoordinates } from "@dnd-kit/sortable"; -import type { - DragOverEvent, - DragStartEvent, - UniqueIdentifier -} from "@dnd-kit/core"; -import {Stack} from '@mui/material'; - -import { - useQuery, -} from '@tanstack/react-query' -import {getRouteDeliveriesSimple} from 'src/api/route-deliveries' - -import { Container } from "./container"; - -export default function App() { - const { isLoading, isError, data, error } = useQuery(['routeDeliveries'], () => getRouteDeliveriesSimple()) - - useEffect(() => { - if (data) { - setRouteItems(data!.data.routes); - try { - const numbers: [] = data!.data.routes.length > 0 ? data!.data.routes.map((route: any) => route.routeNumber) : [] - setColumnIds([...new Set(numbers)]); - } catch { - - } - } - }, [data]); - - // initalized as all routes from database - const [routeItems, setRouteItems] = useState([]); - const [activeRouteId, setActiveRouteId] = useState(null); - - // initialized as all routes from database list of ids - const [columnIds, setColumnIds] = useState([]); - const [activeColumnId, setActiveColumnId] = useState(null); - - const sensors = useSensors( - useSensor(PointerSensor), - useSensor(KeyboardSensor, { - coordinateGetter: sortableKeyboardCoordinates - }) - ); - - function handleDragStart(event: DragStartEvent) { - const { active } = event; - const { id } = active; - - setActiveRouteId(id); - } - - function handleDragEnd(event: DragEndEvent) { - setActiveColumnId(null); - setActiveRouteId(null); - - const { active, over } = event; - if (!over) return; - - const activeId = active.id; - const overId = over.id; - - if (activeId === overId) return; - - setColumnIds((columns) => { - const activeColumnIndex = columns.findIndex((col) => col === activeId); - - const overColumnIndex = columns.findIndex((col) => col === overId); - - return arrayMove(columns, activeColumnIndex, overColumnIndex); - }); - } - - - function handleDragOver(event: DragOverEvent) { - const { active, over } = event; - if (!over) return; - - const activeId = active.id; - const overId = over.id; - - if (activeId === overId) return; - - const isActiveATask = active.data.current?.type === "Task"; - const isOverATask = over.data.current?.type === "Task"; - - if (!isActiveATask) return; - - // Im dropping a Task over another Task - if (isActiveATask && isOverATask) { - setRouteItems((routeItems: any) => { - const activeIndex = routeItems.findIndex((r: any) => r.id === activeId); - const overIndex = routeItems.findIndex((r: any) => r.id === overId); - - if (routeItems[activeIndex].routeNumber != routeItems[overIndex].routeNumber) { - // Fix introduced after video recording - routeItems[activeIndex].routeNumber = routeItems[overIndex].routeNumber; - return arrayMove(routeItems, activeIndex, overIndex - 1); - } - - return arrayMove(routeItems, activeIndex, overIndex); - }); - } - - const isOverAColumn = over.data.current?.type === "Column"; - - // Im dropping a Task over a column - if (isActiveATask && isOverAColumn) { - setRouteItems((routeItems: any) => { - const activeIndex = routeItems.findIndex((r: any) => r.id === activeId); - - routeItems[activeIndex].routeNumber = overId; - console.log("DROPPING TASK OVER COLUMN", { activeIndex }); - return arrayMove(routeItems, activeIndex, activeIndex); - }); - } - } - - function createNewColumn() { - setColumnIds([...columnIds, columnIds.length + 1]); - } - - return ( - - - - {columnIds.map((col) => ( - routeItem.routeNumber === col) : []} - /> - ))} - - - - {/* - {columnIds.map((col) => ( - routeItem.routeNumber === col) : []} - /> - ))} - - */} - - - ); -} \ No newline at end of file diff --git a/frontend-admin/src/components/routes/dnd/container.tsx b/frontend-admin/src/components/routes/dnd/container.tsx deleted file mode 100644 index eb389f9d..00000000 --- a/frontend-admin/src/components/routes/dnd/container.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import React from "react"; -import { useDroppable } from "@dnd-kit/core"; -import { - SortableContext, - verticalListSortingStrategy -} from "@dnd-kit/sortable"; -import {ItemType, SortableItem} from './item'; -import {Box, Typography} from '@mui/material'; - -type ContainerProps = { - containerId: string; - items: Array; -}; - -export function Container(props: ContainerProps) { - const { containerId, items } = props; - - const { setNodeRef } = useDroppable({ - id: containerId - }); - - return ( - - - - {containerId} - - {items.map((item) => ( - - ))} - - - ); -} diff --git a/frontend-admin/src/components/routes/dnd/item.tsx b/frontend-admin/src/components/routes/dnd/item.tsx deleted file mode 100644 index bb0f263f..00000000 --- a/frontend-admin/src/components/routes/dnd/item.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import React from "react"; -import { useSortable } from "@dnd-kit/sortable"; -import {Box, Typography} from '@mui/material'; - -type SortableItemProps = ItemType; - -export function Item({ id, mealType, program }: ItemType) { - return ( - - {id} - {mealType} - {program} - - ); -} - -export function SortableItem(props: SortableItemProps) { - const { id, mealType, program } = props; - const { - attributes, - listeners, - setNodeRef, - transform, - transition - } = useSortable({ id }); - - const style = { - transform: transform - ? `translate3d(${transform.x}px, ${Math.round( - transform.y - )}px, 0) scaleX(${transform.scaleX})` - : "", - transition - }; - - return ( - - - - ); -} - -export type ItemType = { - id: string; - mealType: string; - program: string -}; \ No newline at end of file diff --git a/frontend-admin/src/components/routes/dnd-view-edit/board.tsx b/frontend-admin/src/components/routes/editor/board.tsx similarity index 100% rename from frontend-admin/src/components/routes/dnd-view-edit/board.tsx rename to frontend-admin/src/components/routes/editor/board.tsx diff --git a/frontend-admin/src/components/routes/dnd-view-edit/route-details.tsx b/frontend-admin/src/components/routes/editor/route-details.tsx similarity index 100% rename from frontend-admin/src/components/routes/dnd-view-edit/route-details.tsx rename to frontend-admin/src/components/routes/editor/route-details.tsx diff --git a/frontend-admin/src/components/routes/dnd-view-edit/stop-details.tsx b/frontend-admin/src/components/routes/editor/stop-details.tsx similarity index 100% rename from frontend-admin/src/components/routes/dnd-view-edit/stop-details.tsx rename to frontend-admin/src/components/routes/editor/stop-details.tsx diff --git a/frontend-admin/src/components/routes/dnd-view-edit/types.ts b/frontend-admin/src/components/routes/editor/types.ts similarity index 93% rename from frontend-admin/src/components/routes/dnd-view-edit/types.ts rename to frontend-admin/src/components/routes/editor/types.ts index 777ebfe3..0bd3caf1 100644 --- a/frontend-admin/src/components/routes/dnd-view-edit/types.ts +++ b/frontend-admin/src/components/routes/editor/types.ts @@ -3,7 +3,7 @@ export type ResponseData = { } export type RouteDelivery = { - id: number; + id: string; routeNumber: number; routePosition: number; mealType: string; diff --git a/frontend-admin/src/components/routes/page.tsx b/frontend-admin/src/components/routes/page.tsx index a4c5a5b5..c06f7b04 100644 --- a/frontend-admin/src/components/routes/page.tsx +++ b/frontend-admin/src/components/routes/page.tsx @@ -5,18 +5,9 @@ import { 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 {Box} from '@mui/material' -import Board from './dnd-view-edit/board'; -import { ResponseData } from './dnd-view-edit/types' - -/* -NOTES: - routes/board: annoying click to transfer - dnd: bad implementation of dnd-kit? - dnd-view-edit: good implementation of dnd-kit -*/ +import Board from './editor/board'; +import { ResponseData } from './editor/types' export enum BoardAction { VIEW = "view", diff --git a/frontend-admin/src/components/tasks/page.tsx b/frontend-admin/src/components/tasks/page.tsx index 1655ea9b..35e4b17a 100644 --- a/frontend-admin/src/components/tasks/page.tsx +++ b/frontend-admin/src/components/tasks/page.tsx @@ -11,10 +11,6 @@ import { taskColumns } from './columns' const TasksPage = () => { const { isLoading, isError, data, error } = useQuery(['tasks'], () => getTasks()) - if (data) { - console.log("task data ", data) - } - return ( }> From b35bcb18978ff87790156a360c6bfbf67f96353f Mon Sep 17 00:00:00 2001 From: Sophearah Suy-Puth Date: Thu, 21 Mar 2024 11:57:58 +0700 Subject: [PATCH 23/67] remove another unnecessary file --- .../src/components/routes/columns.tsx | 54 ------------------- 1 file changed, 54 deletions(-) delete mode 100644 frontend-admin/src/components/routes/columns.tsx 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, - }, -]; From 01fa7dfa4da352c2d5fdfce62aea9d4a767ab264 Mon Sep 17 00:00:00 2001 From: Sophearah Suy-Puth Date: Thu, 21 Mar 2024 13:42:42 +0700 Subject: [PATCH 24/67] moved common components into respective subcategories --- frontend-admin/src/components/clients/modals/create.tsx | 4 ++-- frontend-admin/src/components/clients/modals/edit.tsx | 2 +- frontend-admin/src/components/clients/page.tsx | 6 +++--- .../src/components/common/{ => layout}/base-page.tsx | 2 +- .../src/components/common/{ => layout}/page-actionbar.tsx | 0 .../src/components/common/{ => modal}/use-modal-state.ts | 0 .../common/{ => modal}/use-state-setup-handler.ts | 0 .../src/components/common/{ => modal}/validators.ts | 0 frontend-admin/src/components/routes/page.tsx | 4 ++-- frontend-admin/src/components/tasks/page.tsx | 2 +- frontend-admin/src/components/volunteers/modals/create.tsx | 6 ++---- frontend-admin/src/components/volunteers/modals/edit.tsx | 4 ++-- frontend-admin/src/components/volunteers/page.tsx | 6 +++--- 13 files changed, 17 insertions(+), 19 deletions(-) rename frontend-admin/src/components/common/{ => layout}/base-page.tsx (91%) rename frontend-admin/src/components/common/{ => layout}/page-actionbar.tsx (100%) rename frontend-admin/src/components/common/{ => modal}/use-modal-state.ts (100%) rename frontend-admin/src/components/common/{ => modal}/use-state-setup-handler.ts (100%) rename frontend-admin/src/components/common/{ => modal}/validators.ts (100%) diff --git a/frontend-admin/src/components/clients/modals/create.tsx b/frontend-admin/src/components/clients/modals/create.tsx index 16f7973f..f85ebd20 100644 --- a/frontend-admin/src/components/clients/modals/create.tsx +++ b/frontend-admin/src/components/clients/modals/create.tsx @@ -1,7 +1,7 @@ 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 { ModalActionBar } from "src/components/common/modal/actionbar"; diff --git a/frontend-admin/src/components/clients/modals/edit.tsx b/frontend-admin/src/components/clients/modals/edit.tsx index 948141c1..258accd0 100644 --- a/frontend-admin/src/components/clients/modals/edit.tsx +++ b/frontend-admin/src/components/clients/modals/edit.tsx @@ -2,7 +2,7 @@ import React, { useEffect } from "react"; import { EditClientState, useEditClientStore } from "../client.store"; import { getClient, editClient } from "src/api/clients"; import { BaseModal } from "src/components/common/modal/modal"; -import { useStateSetupHandler } from "src/components/common/use-state-setup-handler"; +import { useStateSetupHandler } from "src/components/common/modal/use-state-setup-handler"; 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"; diff --git a/frontend-admin/src/components/clients/page.tsx b/frontend-admin/src/components/clients/page.tsx index 5b722138..c72bca3c 100644 --- a/frontend-admin/src/components/clients/page.tsx +++ b/frontend-admin/src/components/clients/page.tsx @@ -5,11 +5,11 @@ import { getClients } 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"; 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/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/routes/page.tsx b/frontend-admin/src/components/routes/page.tsx index c06f7b04..060e7102 100644 --- a/frontend-admin/src/components/routes/page.tsx +++ b/frontend-admin/src/components/routes/page.tsx @@ -3,8 +3,8 @@ 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 {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' diff --git a/frontend-admin/src/components/tasks/page.tsx b/frontend-admin/src/components/tasks/page.tsx index 35e4b17a..9ead2e58 100644 --- a/frontend-admin/src/components/tasks/page.tsx +++ b/frontend-admin/src/components/tasks/page.tsx @@ -3,7 +3,7 @@ import { useQuery, } from '@tanstack/react-query' import {getTasks} from 'src/api/tasks' -import {BasePage} from 'src/components/common/base-page' +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' diff --git a/frontend-admin/src/components/volunteers/modals/create.tsx b/frontend-admin/src/components/volunteers/modals/create.tsx index ae155920..823b3db9 100644 --- a/frontend-admin/src/components/volunteers/modals/create.tsx +++ b/frontend-admin/src/components/volunteers/modals/create.tsx @@ -2,8 +2,8 @@ import React, { useEffect } from "react"; import { useMutation, useQueryClient } from "@tanstack/react-query"; import { createVolunteer } 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"; @@ -55,7 +55,6 @@ export const CreateModal = (props: { handleClose: any }) => { }; useEffect(() => { - console.log("useEffect"); const valid = isAllValid([ name, isValidEmail(email), @@ -63,7 +62,6 @@ export const CreateModal = (props: { handleClose: any }) => { isValidPhone(phone), dayjs(date).isValid(), ]); - console.log(valid); setValid(valid); }, [name, email, password, phone, date]); diff --git a/frontend-admin/src/components/volunteers/modals/edit.tsx b/frontend-admin/src/components/volunteers/modals/edit.tsx index a53e9807..067a695f 100644 --- a/frontend-admin/src/components/volunteers/modals/edit.tsx +++ b/frontend-admin/src/components/volunteers/modals/edit.tsx @@ -5,8 +5,8 @@ import { } from "src/components/volunteers/volunteer.store"; import { getVolunteer, editVolunteer } from "src/api/volunteers"; 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 { ModalActionBar } from "src/components/common/modal/actionbar"; import { ModalPhoneInput } from "src/components/common/modal/inputs/phone"; import { ModalTextInput } from "src/components/common/modal/inputs/text"; diff --git a/frontend-admin/src/components/volunteers/page.tsx b/frontend-admin/src/components/volunteers/page.tsx index 9d2b1c48..489d6619 100644 --- a/frontend-admin/src/components/volunteers/page.tsx +++ b/frontend-admin/src/components/volunteers/page.tsx @@ -5,7 +5,7 @@ import { GridRowId, GridActionsCellItem } from "@mui/x-data-grid"; import { useNavigate } from "react-router-dom"; import { volunteerColumns } from "./columns"; -import { useModalState } from "src/components/common/use-modal-state"; +import { useModalState } from "src/components/common/modal/use-modal-state"; import { CreateModal, EditModal } from "./modals"; import { ModalControl } from "src/components/common/modal/control"; @@ -16,8 +16,8 @@ import { } from "src/components/volunteers/volunteer.store"; import { Box } from "@mui/material"; import { DataGrid } from "@mui/x-data-grid"; -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"; const VolunteersPage = () => { const navigate = useNavigate(); From 1c6b9d56bb2a6cd15ba9e21dc147952e38cd5665 Mon Sep 17 00:00:00 2001 From: Sophearah Suy-Puth Date: Thu, 21 Mar 2024 13:51:32 +0700 Subject: [PATCH 25/67] basic stop detail design to be more useful to the user --- .../components/routes/editor/stop-details.tsx | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/frontend-admin/src/components/routes/editor/stop-details.tsx b/frontend-admin/src/components/routes/editor/stop-details.tsx index 89098b31..06a4f66f 100644 --- a/frontend-admin/src/components/routes/editor/stop-details.tsx +++ b/frontend-admin/src/components/routes/editor/stop-details.tsx @@ -19,12 +19,13 @@ export default function SortableStopDetails({data, editEnabled}: SortableStopDet }; return
- + {/* + /> */}
; } @@ -34,13 +35,16 @@ type StopDetailsProps = { program: string }; -export function StopDetails({ id, mealType, program }: StopDetailsProps) { +export function StopDetails( props: {stop: RouteDelivery}) { + const {stop} = props + return ( - {id} - {mealType} - {program} + {stop.client.name} + {stop.mealType} + {stop.program} + {stop.id.substring(0,4)} ); From 49ae0c81caadaa30569add9dd9bcc34ec620da04 Mon Sep 17 00:00:00 2001 From: Sophearah Suy-Puth Date: Thu, 21 Mar 2024 14:34:35 +0700 Subject: [PATCH 26/67] basic modal setup for creating tasks --- .../src/components/tasks/modals/create.tsx | 29 ++++++++++++++ .../src/components/tasks/modals/index.ts | 1 + frontend-admin/src/components/tasks/page.tsx | 39 ++++++++++++++++++- 3 files changed, 67 insertions(+), 2 deletions(-) create mode 100644 frontend-admin/src/components/tasks/modals/create.tsx create mode 100644 frontend-admin/src/components/tasks/modals/index.ts 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..d77f72fb --- /dev/null +++ b/frontend-admin/src/components/tasks/modals/create.tsx @@ -0,0 +1,29 @@ +import React, { useEffect } from "react"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { isAllValid, BaseModal } from "src/components/common/modal/modal"; +import { Select, MenuItem, Box, Stack, Typography } from "@mui/material"; + +export const CreateModal = (props: { handleClose: any }) => { + + const handleChange = () => { + console.log("changed to smth") + } + + return ( + + + + Route 1 + + + + + ); +}; 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 9ead2e58..aa5d7cf5 100644 --- a/frontend-admin/src/components/tasks/page.tsx +++ b/frontend-admin/src/components/tasks/page.tsx @@ -7,12 +7,47 @@ 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()) + const { + state: createModal, + handleOpen: handleOpenCreateModal, + handleClose: handleCloseCreateModal, + } = useModalState(); + const Header = () => { + return ( + + ); + }; + + const CreateModalControl = () => { + return ( + , + }} + /> + ); + }; + return ( - }> + }> + { ) -} +} export default TasksPage; \ No newline at end of file From 268c5b4f569f29e3e6020ec4fc5e2e0071565315 Mon Sep 17 00:00:00 2001 From: Sophearah Suy-Puth Date: Thu, 21 Mar 2024 15:05:53 +0700 Subject: [PATCH 27/67] temp comment out tests oop --- backend/tests/client/client.test.ts | 142 ++++++++++----------- backend/tests/volunteer/volunteers.test.ts | 132 +++++++++---------- 2 files changed, 137 insertions(+), 137 deletions(-) diff --git a/backend/tests/client/client.test.ts b/backend/tests/client/client.test.ts index 86abf78e..c9283277 100644 --- a/backend/tests/client/client.test.ts +++ b/backend/tests/client/client.test.ts @@ -47,77 +47,77 @@ describe('Client tests', () => { }); // 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 - } - ] - }); - }); + // 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 - } - }); - }); + // 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/volunteer/volunteers.test.ts b/backend/tests/volunteer/volunteers.test.ts index 76e148b9..cd22f59b 100644 --- a/backend/tests/volunteer/volunteers.test.ts +++ b/backend/tests/volunteer/volunteers.test.ts @@ -40,73 +40,73 @@ describe('Volunteers tests', () => { }); }); - 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 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 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'); From 8632e8c67477cb2484ac2ca6216aa27969c4afc8 Mon Sep 17 00:00:00 2001 From: Sophearah Suy-Puth Date: Thu, 21 Mar 2024 15:06:27 +0700 Subject: [PATCH 28/67] prettier --- backend/src/app.ts | 6 +- backend/src/controllers/clients.ts | 59 +++++++++++--------- backend/src/controllers/routeDelivery.ts | 41 +++++++------- backend/src/controllers/volunteers.ts | 8 ++- backend/src/entities/ClientEntity.ts | 2 +- backend/src/entities/RouteDeliveryEntity.ts | 2 +- backend/src/entities/types.ts | 2 +- backend/src/routes/routeDelivery.routes.ts | 5 +- backend/src/scripts/seeders/client.seeder.ts | 2 +- 9 files changed, 72 insertions(+), 55 deletions(-) diff --git a/backend/src/app.ts b/backend/src/app.ts index 3c2b0bb0..83f27b01 100644 --- a/backend/src/app.ts +++ b/backend/src/app.ts @@ -14,10 +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/clients.ts b/backend/src/controllers/clients.ts index 2dcbc4a3..7cd3e3da 100644 --- a/backend/src/controllers/clients.ts +++ b/backend/src/controllers/clients.ts @@ -60,23 +60,22 @@ export default class ClientController { getClient = async (request: Request, response: Response) => { const client = await this.ClientRepository.findOne({ where: { - id: parseInt(request.params.id), - softDelete: false - } + id: parseInt(request.params.id), + softDelete: false + } }); response.status(StatusCode.OK).json({ client: client }); }; editClient = async (request: Request, response: Response) => { - const originalClient = await this.ClientRepository.findOne({ - where: {id: parseInt(request.params.id)} - }) + where: { id: parseInt(request.params.id) } + }); + + const editedMealType = !(request.body.mealType == originalClient.mealType); + const editedSTS = !(request.body.sts == originalClient.sts); + const editedMAP = !(request.body.map == originalClient.map); - const editedMealType = !(request.body.mealType == originalClient.mealType) - const editedSTS = !(request.body.sts == originalClient.sts) - const editedMAP = !(request.body.map == originalClient.map) - const client = await this.ClientRepository.update( { id: parseInt(request.params.id) }, { @@ -91,8 +90,8 @@ export default class ClientController { ); const savedClient = await this.ClientRepository.findOne({ - where: {id: parseInt(request.params.id)} - }) + where: { id: parseInt(request.params.id) } + }); if (editedSTS) { if (request.body.sts) { @@ -107,10 +106,13 @@ export default class ClientController { } else { const stsRouteDelivery = await this.RouteDeliveryRepository.find({ relations: { client: true }, - where: {client: {id: parseInt(request.params.id)}, program: ProgramType.STS} - }) + where: { + client: { id: parseInt(request.params.id) }, + program: ProgramType.STS + } + }); - await this.RouteDeliveryRepository.remove(stsRouteDelivery) + await this.RouteDeliveryRepository.remove(stsRouteDelivery); } } @@ -127,24 +129,27 @@ export default class ClientController { } else { const mapRouteDelivery = await this.RouteDeliveryRepository.find({ relations: { client: true }, - where: {client: {id: parseInt(request.params.id)}, program: ProgramType.MAP} - }) + where: { + client: { id: parseInt(request.params.id) }, + program: ProgramType.MAP + } + }); - await this.RouteDeliveryRepository.remove(mapRouteDelivery) + await this.RouteDeliveryRepository.remove(mapRouteDelivery); } } - if (editedMealType){ + if (editedMealType) { const routeDeliveries = await this.RouteDeliveryRepository.find({ relations: { client: true }, - where: {client: {id: parseInt(request.params.id)}} - }) - - routeDeliveries.forEach(routeDelivery => { - routeDelivery.mealType = savedClient.mealType - }) - - this.RouteDeliveryRepository.save(routeDeliveries) + where: { client: { id: parseInt(request.params.id) } } + }); + + routeDeliveries.forEach((routeDelivery) => { + routeDelivery.mealType = savedClient.mealType; + }); + + this.RouteDeliveryRepository.save(routeDeliveries); } response.status(StatusCode.OK).json({ client }); diff --git a/backend/src/controllers/routeDelivery.ts b/backend/src/controllers/routeDelivery.ts index 1d99d258..12411ba8 100644 --- a/backend/src/controllers/routeDelivery.ts +++ b/backend/src/controllers/routeDelivery.ts @@ -47,19 +47,22 @@ export default class RouteDeliveryController { }; setRouteNumber = async (request: Request, response: Response) => { - const routePosition = request.body.routeNumber == 0 ? 0 : await this.getNextRouteNumber(request.body.routeNumber) + const routePosition = + request.body.routeNumber == 0 + ? 0 + : await this.getNextRouteNumber(request.body.routeNumber); const route = await this.RouteDeliveryRepository.update( { id: request.params.id }, - { routeNumber: request.body.routeNumber, routePosition: routePosition} + { routeNumber: request.body.routeNumber, routePosition: routePosition } ); response.status(StatusCode.OK).json({ route }); }; incrementRoutePosition = async (request: Request, response: Response) => { - const r = await this.getRouteFromId(request.params.id) - + const r = await this.getRouteFromId(request.params.id); + await this.RouteDeliveryRepository.decrement( { routeNumber: r.routeNumber, routePosition: r.routePosition + 1 }, 'routePosition', @@ -71,13 +74,13 @@ export default class RouteDeliveryController { 'routePosition', 1 ); - + response.status(StatusCode.OK).json({ route }); }; decrementRoutePosition = async (request: Request, response: Response) => { - const r = await this.getRouteFromId(request.params.id) - + const r = await this.getRouteFromId(request.params.id); + await this.RouteDeliveryRepository.increment( { routeNumber: r.routeNumber, routePosition: r.routePosition - 1 }, 'routePosition', @@ -89,7 +92,7 @@ export default class RouteDeliveryController { 'routePosition', 1 ); - + response.status(StatusCode.OK).json({ route }); }; @@ -102,8 +105,8 @@ export default class RouteDeliveryController { } }); - return route - } + return route; + }; // Get the next route number getNextRouteNumber = async (routeNumber) => { @@ -112,24 +115,24 @@ export default class RouteDeliveryController { routeNumber: routeNumber } }); - + return routes.length || 0; - } + }; saveAllRouteDeliveries = async (request: Request, response: Response) => { - const routes = request.body.routes - + const routes = request.body.routes; + // for each column Object.entries(routes).map(([column, data]) => { // for each stop routes[column].map(async (stop, index) => { const updatedStop = await this.RouteDeliveryRepository.update( { id: stop.id }, - { routeNumber: parseInt(column), routePosition: index} + { routeNumber: parseInt(column), routePosition: index } ); - }) - }) + }); + }); - response.status(StatusCode.OK).json({ routes: "fd" }); - } + response.status(StatusCode.OK).json({ routes: 'fd' }); + }; } diff --git a/backend/src/controllers/volunteers.ts b/backend/src/controllers/volunteers.ts index e43ba7a5..7142e88b 100644 --- a/backend/src/controllers/volunteers.ts +++ b/backend/src/controllers/volunteers.ts @@ -5,7 +5,7 @@ import { VolunteerEntity } from '../entities/VolunteerEntity'; import { StatusCode } from './statusCode'; import * as bcrypt from 'bcrypt'; import * as jwt from 'jsonwebtoken'; -import {getNeighbourhoodFromString} from '../entities/types'; +import { getNeighbourhoodFromString } from '../entities/types'; export default class VolunteerController { private VolunteerRepository = AppDataSource.getRepository(VolunteerEntity); @@ -51,7 +51,11 @@ export default class VolunteerController { startDate: request.body.date, profilePicture: '', availabilities: request.body.availabilities, - preferredNeighbourhoods: request.body.preferredNeighbourhoods ? request.body.preferredNeighbourhoods.map(neighborhood => getNeighbourhoodFromString(neighborhood)) : [] + preferredNeighbourhoods: request.body.preferredNeighbourhoods + ? request.body.preferredNeighbourhoods.map((neighborhood) => + getNeighbourhoodFromString(neighborhood) + ) + : [] }); await this.VolunteerRepository.save(volunteer); response.status(StatusCode.OK).json({ volunteer }); diff --git a/backend/src/entities/ClientEntity.ts b/backend/src/entities/ClientEntity.ts index ddc0b1e6..77b4c5b0 100644 --- a/backend/src/entities/ClientEntity.ts +++ b/backend/src/entities/ClientEntity.ts @@ -18,7 +18,7 @@ export class ClientEntity { @Column() phoneNumber: string; - + @Column() address: string; diff --git a/backend/src/entities/RouteDeliveryEntity.ts b/backend/src/entities/RouteDeliveryEntity.ts index 9dc0470d..db8159ce 100644 --- a/backend/src/entities/RouteDeliveryEntity.ts +++ b/backend/src/entities/RouteDeliveryEntity.ts @@ -4,7 +4,7 @@ import { ProgramType, MealType } from './types'; @Entity() export class RouteDeliveryEntity { - @PrimaryGeneratedColumn("uuid") + @PrimaryGeneratedColumn('uuid') id: string; @Column() diff --git a/backend/src/entities/types.ts b/backend/src/entities/types.ts index eec4d72b..602a073c 100644 --- a/backend/src/entities/types.ts +++ b/backend/src/entities/types.ts @@ -36,7 +36,7 @@ const neighbourhoodReverseMapping: { [key: string]: Neighbourhood } = { [Neighbourhood.TMR]: Neighbourhood.TMR, [Neighbourhood.VERDUN]: Neighbourhood.VERDUN, [Neighbourhood.VILLESTLAURENT]: Neighbourhood.VILLESTLAURENT, - [Neighbourhood.WESTISLAND]: Neighbourhood.WESTISLAND, + [Neighbourhood.WESTISLAND]: Neighbourhood.WESTISLAND }; export function getNeighbourhoodFromString(str: string): Neighbourhood { diff --git a/backend/src/routes/routeDelivery.routes.ts b/backend/src/routes/routeDelivery.routes.ts index 6c5ad5f5..70fc234e 100644 --- a/backend/src/routes/routeDelivery.routes.ts +++ b/backend/src/routes/routeDelivery.routes.ts @@ -8,7 +8,10 @@ const routeDeliveryController = new RouteDeliveryController(); router.get('/route_delivery/', routeDeliveryController.getRouteDeliveries); router.post('/route_delivery/', routeDeliveryController.saveAllRouteDeliveries); // TODO: review this? v -router.get('/route_delivery_simple/', routeDeliveryController.getRouteDeliveriesSimple); +router.get( + '/route_delivery_simple/', + routeDeliveryController.getRouteDeliveriesSimple +); router.put('/route_delivery/:id/set', routeDeliveryController.setRouteNumber); router.put( '/route_delivery/:id/increment', diff --git a/backend/src/scripts/seeders/client.seeder.ts b/backend/src/scripts/seeders/client.seeder.ts index c8d7cfd6..f4bde6f3 100644 --- a/backend/src/scripts/seeders/client.seeder.ts +++ b/backend/src/scripts/seeders/client.seeder.ts @@ -40,7 +40,7 @@ const generateClient = async () => { softDelete: false }; user.sts = faker.datatype.boolean(); - user.map = !user.sts ? true :faker.datatype.boolean(); + user.map = !user.sts ? true : faker.datatype.boolean(); return user; }; From 158a2fac6320f3383387962b0d16ad4808a3a30d Mon Sep 17 00:00:00 2001 From: Sophearah Suy-Puth Date: Thu, 21 Mar 2024 16:03:39 +0700 Subject: [PATCH 29/67] fix bug --- frontend-admin/src/components/routes/editor/board.tsx | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/frontend-admin/src/components/routes/editor/board.tsx b/frontend-admin/src/components/routes/editor/board.tsx index b36448aa..7f34eacf 100644 --- a/frontend-admin/src/components/routes/editor/board.tsx +++ b/frontend-admin/src/components/routes/editor/board.tsx @@ -105,7 +105,12 @@ export default function Board({boardAction, setBoardAction}: BoardProps) { const handleDragEnd = (event: DragEndEvent) => { const { active, over } = event; const { id } = active; - const overId = over!.id! + const overId = over?.id; + + if (overId == null) { + setActiveId(null); + return; + } const activeContainer = findContainer(id); const overContainer = findContainer(overId); @@ -262,7 +267,7 @@ export default function Board({boardAction, setBoardAction}: BoardProps) { { - activeId ? : null + activeId ? : null }
From be0575ff55987a9a68414f63a01075bb9f251bdb Mon Sep 17 00:00:00 2001 From: Sophearah Suy-Puth Date: Fri, 22 Mar 2024 14:49:51 +0700 Subject: [PATCH 30/67] separate match control to independent component, create task modal action bar --- .../src/components/tasks/modals/create.tsx | 75 +++++++++++++++---- 1 file changed, 59 insertions(+), 16 deletions(-) diff --git a/frontend-admin/src/components/tasks/modals/create.tsx b/frontend-admin/src/components/tasks/modals/create.tsx index d77f72fb..d430dc15 100644 --- a/frontend-admin/src/components/tasks/modals/create.tsx +++ b/frontend-admin/src/components/tasks/modals/create.tsx @@ -1,29 +1,72 @@ -import React, { useEffect } from "react"; +import React, { useEffect, useState } from "react"; import { useMutation, useQueryClient } from "@tanstack/react-query"; -import { isAllValid, BaseModal } from "src/components/common/modal/modal"; +import { BaseModal } from "src/components/common/modal/modal"; import { Select, MenuItem, Box, Stack, Typography } from "@mui/material"; +import { ModalActionBar } from "src/components/common/modal/actionbar"; + +const MatchControl = (props: {name: string, handleChange: () => void}) => { + return <> + + + Route {props.name} + + + + +} export const CreateModal = (props: { handleClose: any }) => { - - const handleChange = () => { - console.log("changed to smth") + const handleChange = (name? : string) => { + console.log("changed to smth ", name) } + const handleSave = () => { + props.handleClose(); + }; + + const handleCancel = () => { + props.handleClose(); + }; + return ( - - Route 1 - - + + + + + + + + + ); }; From c509e874aece3fabe245ac4d8d5461310c500dfa Mon Sep 17 00:00:00 2001 From: Sophearah Suy-Puth Date: Fri, 22 Mar 2024 16:00:31 +0700 Subject: [PATCH 31/67] 1) endpoint to get a list of available volunteers/list of routes for the task modal. 2) display routes to match, use available volunteers as selection options --- backend/src/controllers/tasks.ts | 62 +++++++++++++++++++ backend/src/routes/task.routes.ts | 2 + frontend-admin/src/api/tasks.ts | 8 +++ .../src/components/tasks/modals/create.tsx | 55 +++++++++++----- 4 files changed, 113 insertions(+), 14 deletions(-) diff --git a/backend/src/controllers/tasks.ts b/backend/src/controllers/tasks.ts index 5407aa7c..c84f4374 100644 --- a/backend/src/controllers/tasks.ts +++ b/backend/src/controllers/tasks.ts @@ -7,6 +7,22 @@ import { VolunteerEntity } from '../entities/VolunteerEntity'; import { StatusCode } from './statusCode'; import { RouteDeliveryEntity } from '../entities/RouteDeliveryEntity'; +// Get day of week to get notification by +const getLastMonday = () => { + const currentDate: Date = new Date(); + + const currentDay: number = currentDate.getDay(); + + const daysUntilMonday: number = currentDay === 0 ? 6 : currentDay - 1; + + const mostRecentMonday: Date = new Date(currentDate); + mostRecentMonday.setDate(currentDate.getDate() - daysUntilMonday); + + const formattedMostRecentMonday: string = mostRecentMonday.toISOString().split('T')[0]; + + return formattedMostRecentMonday +} + export default class TaskController { private TaskRepository = AppDataSource.getRepository(TaskEntity); private MealDeliveryRepository = @@ -156,4 +172,50 @@ export default class TaskController { }); response.status(StatusCode.OK).json({}); }; + + getTaskMatchData = async (request: Request, response: Response) => { + const volunteers = await this.VolunteerRepository.find({ + select: { + id: true, + name: true, + availabilities: true, + availabilitiesLastUpdated: true + }, + }); + + const comparisonDate = getLastMonday() + + const availableVolunteers = volunteers.filter((v) => { + if (v.availabilitiesLastUpdated >= new Date(comparisonDate)) { + return v + } + }) + + 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 (key != 0) { + 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({ availableVolunteers: availableVolunteers, routes: groups }); + }; } diff --git a/backend/src/routes/task.routes.ts b/backend/src/routes/task.routes.ts index e047ac1a..62914c69 100644 --- a/backend/src/routes/task.routes.ts +++ b/backend/src/routes/task.routes.ts @@ -12,3 +12,5 @@ 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-match', taskController.getTaskMatchData); + \ No newline at end of file diff --git a/frontend-admin/src/api/tasks.ts b/frontend-admin/src/api/tasks.ts index b8eaecf5..26b05888 100644 --- a/frontend-admin/src/api/tasks.ts +++ b/frontend-admin/src/api/tasks.ts @@ -17,3 +17,11 @@ export const createTask = async (data: any) => { }); return response } + +export const getTaskMatchData = async () => { + const response = await AxiosInstance({ + method: "get", + url: "/tasks-match/", + }); + return response +} \ No newline at end of file diff --git a/frontend-admin/src/components/tasks/modals/create.tsx b/frontend-admin/src/components/tasks/modals/create.tsx index d430dc15..97a09721 100644 --- a/frontend-admin/src/components/tasks/modals/create.tsx +++ b/frontend-admin/src/components/tasks/modals/create.tsx @@ -1,10 +1,16 @@ import React, { useEffect, useState } from "react"; -import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { BaseModal } from "src/components/common/modal/modal"; import { Select, MenuItem, Box, Stack, Typography } from "@mui/material"; import { ModalActionBar } from "src/components/common/modal/actionbar"; +import { getTaskMatchData } from "src/api/tasks"; + +const MatchControl = (props: { + name: string, + handleChange: () => void, + options: string[] +}) => { -const MatchControl = (props: {name: string, handleChange: () => void}) => { return <> void}) => { value={10} onChange={props.handleChange} > - Ten - Twenty - Thirty + {props.options && props.options.map((o) => + {o} + )} } export const CreateModal = (props: { handleClose: any }) => { + const queryClient = useQueryClient(); + const [availableVolunteers, setAvailableVolunteers] = useState([]) + const [currMatch, setCurrMatch] = useState([]); + + const { data } = useQuery({ + queryKey: ["task-match"], + queryFn: () => getTaskMatchData(), + }); + + useEffect(() => { + if (data != null) { + const matches = Object.entries(data.data.routes).map(([key, value]) => { + return { + routeName: key, + volunteer: null + } + }) + + setCurrMatch(matches) + setAvailableVolunteers(data.data.availableVolunteers) + } + }, [data]); + + const handleChange = (name? : string) => { - console.log("changed to smth ", name) + console.log(data) } const handleSave = () => { @@ -46,14 +76,11 @@ export const CreateModal = (props: { handleClose: any }) => { return ( - - - - - - - - + { + currMatch && currMatch.map((m: any) => + v.name)} /> + ) + } Date: Fri, 22 Mar 2024 16:35:33 +0700 Subject: [PATCH 32/67] local selection of task matching --- .../src/components/tasks/modals/create.tsx | 45 +++++++++++++------ 1 file changed, 32 insertions(+), 13 deletions(-) diff --git a/frontend-admin/src/components/tasks/modals/create.tsx b/frontend-admin/src/components/tasks/modals/create.tsx index 97a09721..b452cabd 100644 --- a/frontend-admin/src/components/tasks/modals/create.tsx +++ b/frontend-admin/src/components/tasks/modals/create.tsx @@ -1,14 +1,25 @@ 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 } from "@mui/material"; +import { Select, MenuItem, Box, Stack, Typography, SelectChangeEvent } from "@mui/material"; import { ModalActionBar } from "src/components/common/modal/actionbar"; import { getTaskMatchData } from "src/api/tasks"; +type VolunteerOption = { + id: string, + name: string +} + +type MatchData = { + routeName: string, + volunteerId: string +} + const MatchControl = (props: { name: string, - handleChange: () => void, - options: string[] + handleChange: (routeName: string, volunteerId: string) => void, + options: VolunteerOption[], + matchData: MatchData[] }) => { return <> @@ -25,11 +36,11 @@ const MatchControl = (props: { @@ -39,7 +50,7 @@ const MatchControl = (props: { export const CreateModal = (props: { handleClose: any }) => { const queryClient = useQueryClient(); const [availableVolunteers, setAvailableVolunteers] = useState([]) - const [currMatch, setCurrMatch] = useState([]); + const [currMatch, setCurrMatch] = useState([]); const { data } = useQuery({ queryKey: ["task-match"], @@ -51,18 +62,19 @@ export const CreateModal = (props: { handleClose: any }) => { const matches = Object.entries(data.data.routes).map(([key, value]) => { return { routeName: key, - volunteer: null - } + volunteerId: "" + } as MatchData }) setCurrMatch(matches) setAvailableVolunteers(data.data.availableVolunteers) } }, [data]); - - const handleChange = (name? : string) => { - console.log(data) + const setMatch = (routeName: string, volunteerId: string) => { + setCurrMatch((prevMatch) => + prevMatch.map(m => (m.routeName === routeName ? { ...m, volunteerId: volunteerId } : m)) + ) } const handleSave = () => { @@ -78,7 +90,14 @@ export const CreateModal = (props: { handleClose: any }) => { { currMatch && currMatch.map((m: any) => - v.name)} /> + { + return {id: v.id, name: v.name} as VolunteerOption + })} + matchData={currMatch} + /> ) } From 47c720565121b83e1a8c43a039fa11a7e56dcbba Mon Sep 17 00:00:00 2001 From: Sophearah Suy-Puth Date: Fri, 22 Mar 2024 17:06:59 +0700 Subject: [PATCH 33/67] endpoint to save tasks, button makes req to save --- backend/src/controllers/tasks.ts | 46 +++++++++++++++++++ backend/src/routes/task.routes.ts | 2 +- frontend-admin/src/api/tasks.ts | 9 ++++ .../src/components/tasks/modals/create.tsx | 13 +++++- 4 files changed, 68 insertions(+), 2 deletions(-) diff --git a/backend/src/controllers/tasks.ts b/backend/src/controllers/tasks.ts index c84f4374..49bcc2b5 100644 --- a/backend/src/controllers/tasks.ts +++ b/backend/src/controllers/tasks.ts @@ -218,4 +218,50 @@ export default class TaskController { response.status(StatusCode.OK).json({ availableVolunteers: availableVolunteers, routes: groups }); }; + + saveTaskMatch = async (request: Request, response: Response) => { + const taskMatches = request.body.matches + + for (let i = 0; i < taskMatches.length; i++) { + const match = taskMatches[i]; + + // CREATE TASK + const newTask = new TaskEntity(); + newTask.deliveries = []; + newTask.isCompleted = false; + newTask.volunteer = await this.VolunteerRepository.findOne({ + where: { id: match.volunteerId } + }); + + const savedTask = await this.TaskRepository.save(newTask); + + // CREATE MEAL DELIVERY OBJECTS + const routeObjs = await this.RouteDeliveryRepository.find({ + where: { + routeNumber: match.routeName + }, + relations: { + client: true + } + }) + + for (let i = 0; i < routeObjs.length; i++) { + const routeObj = routeObjs[i]; + const newMeal = new MealDeliveryEntity(); + newMeal.mealType = routeObj.mealType; + newMeal.task = savedTask; + newMeal.routePosition = routeObj.routePosition; + newMeal.program = routeObj.program; + const foundClient = await this.ClientRepository.findOne({ + where: { id: routeObj.client.id } + }); + newMeal.client = foundClient; + const savedDelivery = await this.MealDeliveryRepository.save(newMeal); + newTask.deliveries.push(savedDelivery) + } + + await this.TaskRepository.save(newTask); + } + response.status(StatusCode.OK).json({}); + } } diff --git a/backend/src/routes/task.routes.ts b/backend/src/routes/task.routes.ts index 62914c69..b52babe0 100644 --- a/backend/src/routes/task.routes.ts +++ b/backend/src/routes/task.routes.ts @@ -13,4 +13,4 @@ router.put('/tasks/:id', taskController.updateOrCreateTask); router.post('/tasks/', taskController.createTask); router.delete('/tasks/:id', taskController.deleteTask); router.get('/tasks-match', taskController.getTaskMatchData); - \ No newline at end of file +router.post('/tasks-match', taskController.saveTaskMatch); \ No newline at end of file diff --git a/frontend-admin/src/api/tasks.ts b/frontend-admin/src/api/tasks.ts index 26b05888..50afe073 100644 --- a/frontend-admin/src/api/tasks.ts +++ b/frontend-admin/src/api/tasks.ts @@ -24,4 +24,13 @@ export const getTaskMatchData = async () => { url: "/tasks-match/", }); return response +} + +export const saveTaskMatchData = async (matchData: any) => { + const response = await AxiosInstance({ + method: "post", + url: "/tasks-match/", + data: matchData + }); + return response } \ No newline at end of file diff --git a/frontend-admin/src/components/tasks/modals/create.tsx b/frontend-admin/src/components/tasks/modals/create.tsx index b452cabd..175f8d0d 100644 --- a/frontend-admin/src/components/tasks/modals/create.tsx +++ b/frontend-admin/src/components/tasks/modals/create.tsx @@ -3,7 +3,7 @@ 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 { getTaskMatchData } from "src/api/tasks"; +import { getTaskMatchData, saveTaskMatchData } from "src/api/tasks"; type VolunteerOption = { id: string, @@ -57,6 +57,13 @@ export const CreateModal = (props: { handleClose: any }) => { queryFn: () => getTaskMatchData(), }); + const mutation = useMutation(saveTaskMatchData, { + onSuccess: () => { + // Invalidate and refetch + queryClient.invalidateQueries("task-match"); + }, + }); + useEffect(() => { if (data != null) { const matches = Object.entries(data.data.routes).map(([key, value]) => { @@ -78,6 +85,10 @@ export const CreateModal = (props: { handleClose: any }) => { } const handleSave = () => { + mutation.mutate({ + matches: currMatch + }); + props.handleClose(); }; From 124ed0f76313eebb074d59d038cc6d87ba9aaf6c Mon Sep 17 00:00:00 2001 From: Sophearah Suy-Puth Date: Sun, 31 Mar 2024 14:08:25 +0700 Subject: [PATCH 34/67] volunteer endpoint tests, fe-admin api service class --- backend/src/controllers/volunteers.ts | 6 +- backend/tests/volunteer/volunteers.test.ts | 316 +++++++++++------- backend/tests/volunteer/volunteers.utils.ts | 12 + frontend-admin/src/api/volunteers.ts | 64 ++-- .../components/volunteers/modals/create.tsx | 4 +- .../src/components/volunteers/modals/edit.tsx | 6 +- .../src/components/volunteers/page.tsx | 4 +- 7 files changed, 249 insertions(+), 163 deletions(-) diff --git a/backend/src/controllers/volunteers.ts b/backend/src/controllers/volunteers.ts index 7142e88b..527be615 100644 --- a/backend/src/controllers/volunteers.ts +++ b/backend/src/controllers/volunteers.ts @@ -43,7 +43,7 @@ export default class VolunteerController { }; createVolunteer = async (request: Request, response: Response) => { - const volunteer = await this.VolunteerRepository.create({ + const volunteer = await this.VolunteerRepository.save({ name: request.body.name, password: request.body.password, email: request.body.email, @@ -55,9 +55,9 @@ export default class VolunteerController { ? request.body.preferredNeighbourhoods.map((neighborhood) => getNeighbourhoodFromString(neighborhood) ) - : [] + : [], + softDelete: false }); - await this.VolunteerRepository.save(volunteer); response.status(StatusCode.OK).json({ volunteer }); }; diff --git a/backend/tests/volunteer/volunteers.test.ts b/backend/tests/volunteer/volunteers.test.ts index cd22f59b..e1369b76 100644 --- a/backend/tests/volunteer/volunteers.test.ts +++ b/backend/tests/volunteer/volunteers.test.ts @@ -10,7 +10,7 @@ import { StatusCode } from '../../src/controllers/statusCode'; import { TaskEntity } from '../../src/entities/TaskEntity'; import TaskEntityHelper from '../task/task.utils'; -describe('Volunteers tests', () => { +describe('Volunteer API tests', () => { const VolunteerRepository = AppDataSource.getRepository(VolunteerEntity); const volunteerHelper = new VolunteerEntityHelper(VolunteerRepository); const TaskRepository = AppDataSource.getRepository(TaskEntity); @@ -32,81 +32,151 @@ describe('Volunteers tests', () => { 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: [] + describe('GET /api/volunteers', () => { + 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 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', + '[{"day":"monday","time":"14"}]', + [] + ); + const res = await request(app).get('/api/volunteers'); + expect(res.status).toBe(StatusCode.OK); + expect(res.body).toEqual({ + volunteers: [ + { + availabilities: '[{"day":"monday","time":"14"}]', + email: 'email1', + phoneNumber: '0123456789', + id: 1, + name: 'name1', + profilePicture: 'link to profile', + availabilitiesLastUpdated: lastUpdated.toISOString(), + startDate: date.toISOString(), + password: 'password1', + tasks: [], + token: null, + preferredNeighbourhoods: null, + softDelete: false + } + ] + }); + }); + }) - // 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 - // } - // }); - // }); + describe('GET /api/volunteer/:id', () => { + 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', + '[{"day":"monday","time":"14"}]', + [] + ); + const res = await request(app).get(`/api/volunteers/${volunteer.id}`); + expect(res.status).toBe(StatusCode.OK); + expect(res.body).toEqual({ + volunteer: { + availabilities: '[{"day":"monday","time":"14"}]', + email: 'email1', + phoneNumber: '0123456789', + id: 1, + name: 'name1', + profilePicture: 'link to profile', + availabilitiesLastUpdated: lastUpdated.toISOString(), + startDate: date.toISOString(), + password: 'password1', + tasks: [], + token: null, + preferredNeighbourhoods: null, + softDelete: false + } + }); + }); + }) + + describe('PUT /api/volunteers/:id/edit', () => { + it('should update 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', + '[{"day":"monday","time":"14"}]', + [] + ); + const res = await request(app).put(`/api/volunteers/${volunteer.id}/edit`).send({ + name: 'name2', + email: 'email1@gmail.com' + }); + expect(res.status).toBe(StatusCode.OK); + const updatedVolunteer = await volunteerHelper.getVolunteer(volunteer.id) + expect(updatedVolunteer).toEqual( + expect.objectContaining( + { + name: 'name2', + email: 'email1@gmail.com' + } + ) + ) + }); + }) + + describe('POST /api/volunteers', () => { + it('should create volunteer', async () => { + const date: Date = new Date('April 20, 2001 04:20:00'); + const res = await request(app).post(`/api/volunteers`).send({ + name: 'name', + password: 'password1', + email: 'email1', + phoneNumber: '0123456789', + date: date.toISOString(), + }); + expect(res.status).toBe(StatusCode.OK); + expect(res.body).toEqual({ + volunteer: { + availabilities: null, + email: 'email1', + phoneNumber: '0123456789', + id: 1, + name: 'name', + profilePicture: '', + availabilitiesLastUpdated: null, + startDate: date.toISOString(), + password: 'password1', + token: null, + preferredNeighbourhoods: [], + softDelete: false + } + }); + }) + }) // it('should return a task', async () => { // const date: Date = new Date('April 20, 2001 04:20:00'); @@ -208,57 +278,57 @@ describe('Volunteers tests', () => { // 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); + // 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 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 - } - ] - }); - }); + // 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({}); - }); + // 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 index 3658d912..ed302ca4 100644 --- a/backend/tests/volunteer/volunteers.utils.ts +++ b/backend/tests/volunteer/volunteers.utils.ts @@ -32,4 +32,16 @@ export default class VolunteerEntityHelper { newVolunteer.tasks = tasks; return await this.VolunteerRepository.save(newVolunteer); }; + + getVolunteer = async (volunteerId: number) => { + return await this.VolunteerRepository.findOne({ + where: { + id: volunteerId, + softDelete: false + }, + relations: { + tasks: true + } + }); + } } 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/volunteers/modals/create.tsx b/frontend-admin/src/components/volunteers/modals/create.tsx index 823b3db9..ffc09ed9 100644 --- a/frontend-admin/src/components/volunteers/modals/create.tsx +++ b/frontend-admin/src/components/volunteers/modals/create.tsx @@ -1,6 +1,6 @@ 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/modal/use-state-setup-handler"; import { isValidEmail, isValidPhone } from "src/components/common/modal/validators"; @@ -15,7 +15,7 @@ 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"); diff --git a/frontend-admin/src/components/volunteers/modals/edit.tsx b/frontend-admin/src/components/volunteers/modals/edit.tsx index 067a695f..09b4d73e 100644 --- a/frontend-admin/src/components/volunteers/modals/edit.tsx +++ b/frontend-admin/src/components/volunteers/modals/edit.tsx @@ -3,7 +3,7 @@ import { EditVolunteerState, useEditVolunteerStore, } from "src/components/volunteers/volunteer.store"; -import { getVolunteer, editVolunteer } from "src/api/volunteers"; +import VolunteerAPI from "src/api/volunteers"; import { isAllValid, BaseModal } from "src/components/common/modal/modal"; import { useStateSetupHandler } from "src/components/common/modal/use-state-setup-handler"; import { isValidEmail, isValidPhone } from "src/components/common/modal/validators"; @@ -24,7 +24,7 @@ export const EditModal = (props: { handleClose: any }) => { const [valid, setValid] = React.useState(false); const { data } = useQuery({ queryKey: ["volunteers", id], - queryFn: () => getVolunteer(id), + queryFn: () => VolunteerAPI.getVolunteer(id), }); const { @@ -60,7 +60,7 @@ export const EditModal = (props: { handleClose: any }) => { const queryClient = new QueryClient(); - const mutation = useMutation(editVolunteer, { + const mutation = useMutation(VolunteerAPI.editVolunteer, { onSuccess: () => { // Invalidate and refetch queryClient.invalidateQueries("volunteers"); diff --git a/frontend-admin/src/components/volunteers/page.tsx b/frontend-admin/src/components/volunteers/page.tsx index 489d6619..48bfd07a 100644 --- a/frontend-admin/src/components/volunteers/page.tsx +++ b/frontend-admin/src/components/volunteers/page.tsx @@ -9,7 +9,7 @@ import { useModalState } from "src/components/common/modal/use-modal-state"; import { CreateModal, EditModal } from "./modals"; import { ModalControl } from "src/components/common/modal/control"; -import { getVolunteers } from "src/api/volunteers"; +import VolunteerAPI from "src/api/volunteers"; import { EditVolunteerState, useEditVolunteerStore, @@ -22,7 +22,7 @@ import { ActionBar } from "src/components/common/layout/page-actionbar"; const VolunteersPage = () => { const navigate = useNavigate(); const { isLoading, isError, data, error } = useQuery(["volunteers"], () => - getVolunteers() + VolunteerAPI.getVolunteers() ); const { state: createModal, From 9c740a64d90a99d9168add03c27a3698895cfece Mon Sep 17 00:00:00 2001 From: Sophearah Suy-Puth Date: Sun, 31 Mar 2024 15:08:28 +0700 Subject: [PATCH 35/67] volunteer controller as static class --- backend/src/controllers/volunteers.ts | 18 +++++++++--------- backend/src/routes/volunteer.routes.ts | 18 +++++++++--------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/backend/src/controllers/volunteers.ts b/backend/src/controllers/volunteers.ts index 527be615..6ec47c72 100644 --- a/backend/src/controllers/volunteers.ts +++ b/backend/src/controllers/volunteers.ts @@ -8,9 +8,9 @@ import * as jwt from 'jsonwebtoken'; import { getNeighbourhoodFromString } from '../entities/types'; export default class VolunteerController { - private VolunteerRepository = AppDataSource.getRepository(VolunteerEntity); + private static VolunteerRepository = AppDataSource.getRepository(VolunteerEntity); - getVolunteers = async (request: Request, response: Response) => { + static getVolunteers = async (request: Request, response: Response) => { const volunteers = await this.VolunteerRepository.find({ relations: { tasks: true @@ -22,7 +22,7 @@ export default class VolunteerController { response.status(StatusCode.OK).json({ volunteers: volunteers }); }; - getVolunteer = async (request: Request, response: Response) => { + static getVolunteer = async (request: Request, response: Response) => { const volunteer = await this.VolunteerRepository.findOne({ where: { id: parseInt(request.params.id), @@ -35,14 +35,14 @@ export default class VolunteerController { response.status(StatusCode.OK).json({ volunteer: volunteer }); }; - removeVolunteer = async (request: Request, response: Response) => { + static removeVolunteer = async (request: Request, response: Response) => { await this.VolunteerRepository.delete({ id: parseInt(request.params.id) }); response.status(StatusCode.OK).json({}); }; - createVolunteer = async (request: Request, response: Response) => { + static createVolunteer = async (request: Request, response: Response) => { const volunteer = await this.VolunteerRepository.save({ name: request.body.name, password: request.body.password, @@ -61,7 +61,7 @@ export default class VolunteerController { response.status(StatusCode.OK).json({ volunteer }); }; - editVolunteer = async (request: Request, response: Response) => { + static editVolunteer = async (request: Request, response: Response) => { const volunteer = await this.VolunteerRepository.update( { id: parseInt(request.params.id) }, request.body @@ -69,7 +69,7 @@ export default class VolunteerController { response.status(StatusCode.OK).json({ volunteer }); }; - getVolunteerTasks = async (request: Request, response: Response) => { + static getVolunteerTasks = async (request: Request, response: Response) => { const volunteer = await this.VolunteerRepository.findOne({ where: { id: parseInt(request.params.id) }, relations: ['tasks', 'tasks.deliveries'] @@ -79,7 +79,7 @@ export default class VolunteerController { : response.status(StatusCode.OK).json({ tasks: volunteer.tasks }); }; - getVolunteerAvailabilities = async (request: Request, response: Response) => { + static getVolunteerAvailabilities = async (request: Request, response: Response) => { const volunteer = await this.VolunteerRepository.findOne({ where: { id: parseInt(request.params.id) }, relations: ['availabilities'] @@ -91,7 +91,7 @@ export default class VolunteerController { .json({ availabilities: volunteer.availabilities }); }; - login = async (req: Request, res: Response) => { + static 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); diff --git a/backend/src/routes/volunteer.routes.ts b/backend/src/routes/volunteer.routes.ts index b820b32e..5141257c 100644 --- a/backend/src/routes/volunteer.routes.ts +++ b/backend/src/routes/volunteer.routes.ts @@ -1,14 +1,14 @@ 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/edit', VolunteerController.editVolunteer); + +// TODO: review +router.get('/volunteers/:id/tasks', VolunteerController.getVolunteerTasks); +router.delete('/volunteers/:id', VolunteerController.removeVolunteer); +router.post('/volunteer/login', VolunteerController.login); From 44cd49b3a7c38a5acef39fa313ec5857aef54037 Mon Sep 17 00:00:00 2001 From: Sophearah Suy-Puth Date: Sun, 31 Mar 2024 15:13:43 +0700 Subject: [PATCH 36/67] remove example test files --- backend/tests/example.test.ts | 6 ------ backend/tests/example/example.ts | 3 --- 2 files changed, 9 deletions(-) delete mode 100644 backend/tests/example.test.ts delete mode 100644 backend/tests/example/example.ts 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; -} From 30cd693fa968f8ad42cb4e4448ccff82348b725f Mon Sep 17 00:00:00 2001 From: Sophearah Suy-Puth Date: Mon, 1 Apr 2024 15:05:18 +0700 Subject: [PATCH 37/67] client endpoint tests, fe-admin api service class --- backend/tests/client/client.test.ts | 220 +++++++++++------- backend/tests/client/client.utils.ts | 9 + frontend-admin/src/api/clients.ts | 70 +++--- .../src/components/clients/modals/create.tsx | 6 +- .../src/components/clients/modals/edit.tsx | 6 +- .../src/components/clients/page.tsx | 4 +- 6 files changed, 195 insertions(+), 120 deletions(-) diff --git a/backend/tests/client/client.test.ts b/backend/tests/client/client.test.ts index c9283277..c314fdad 100644 --- a/backend/tests/client/client.test.ts +++ b/backend/tests/client/client.test.ts @@ -36,88 +36,150 @@ describe('Client tests', () => { 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: [] + describe('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: [] + }); + }); + + 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, + softDelete: false + }, + { + id: 2, + name: 'Test Client2', + email: 'email2@email.com', + phoneNumber: '0987654321', + address: '1234 Test Address2', + mealType: MealType.NOMEAT, + sts: false, + map: true, + softDelete: false + } + ] + }); }); }); - // 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 - // } - // ] - // }); - // }); + describe('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, + softDelete: false + } + }); + }); + }) + + describe('PUT /api/clients/:id/edit', () => { + it('should update 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).put(`/api/clients/${client1.id}/edit`).send({ + name: 'name2', + email: 'email1@gmail.com' + }); + expect(res.status).toBe(StatusCode.OK); + const updatedClient = await clientHelper.getClient(client1.id) + expect(updatedClient).toEqual( + expect.objectContaining( + { + name: 'name2', + email: 'email1@gmail.com' + } + ) + ) + }); + }) - // 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 - // } - // }); - // }); + describe('POST /api/clients', () => { + it('should create client', async () => { + const res = await request(app).post(`/api/clients`).send({ + name: 'Test Client', + email: 'email@email.com', + phoneNumber: '1234567890', + address: '1234 Test Address', + mealType: MealType.NOFISH, + sts: true, + map: true, + }); + 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, + softDelete: false + } + }); + }) + }) }); diff --git a/backend/tests/client/client.utils.ts b/backend/tests/client/client.utils.ts index 0604dbf7..30878c8f 100644 --- a/backend/tests/client/client.utils.ts +++ b/backend/tests/client/client.utils.ts @@ -32,4 +32,13 @@ export default class ClientEntityHelper { newClient.map = typeof props.map !== 'undefined' ? props.map : true; return await this.ClientRepository.save(newClient); } + + getClient = async (clientId: number) => { + return await this.ClientRepository.findOne({ + where: { + id: clientId, + softDelete: false + } + }); + } } 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/components/clients/modals/create.tsx b/frontend-admin/src/components/clients/modals/create.tsx index f85ebd20..8287ebd8 100644 --- a/frontend-admin/src/components/clients/modals/create.tsx +++ b/frontend-admin/src/components/clients/modals/create.tsx @@ -3,7 +3,7 @@ import { isAllValid, BaseModal } from "src/components/common/modal/modal"; 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 c72bca3c..3b885cdb 100644 --- a/frontend-admin/src/components/clients/page.tsx +++ b/frontend-admin/src/components/clients/page.tsx @@ -1,7 +1,7 @@ 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"; @@ -15,7 +15,7 @@ import { Box } from "@mui/system"; const ClientsPage = () => { const { isLoading, isError, data, error } = useQuery(["clients"], () => - getClients() + ClientAPI.getClients() ); const { state: createModal, From 3eccf5f2588d505eb687267eadc3410bea437615 Mon Sep 17 00:00:00 2001 From: Sophearah Suy-Puth Date: Mon, 1 Apr 2024 15:34:14 +0700 Subject: [PATCH 38/67] setup route delivery test file, fe-admin api service class --- .../tests/routeDelivery/routeDelivery.test.ts | 35 +++++++++ .../routeDelivery/routeDelivery.utils.ts | 19 +++++ frontend-admin/src/api/route-deliveries.ts | 72 +++++-------------- .../src/components/routes/editor/board.tsx | 7 +- frontend-admin/src/components/routes/page.tsx | 4 +- 5 files changed, 78 insertions(+), 59 deletions(-) create mode 100644 backend/tests/routeDelivery/routeDelivery.test.ts create mode 100644 backend/tests/routeDelivery/routeDelivery.utils.ts diff --git a/backend/tests/routeDelivery/routeDelivery.test.ts b/backend/tests/routeDelivery/routeDelivery.test.ts new file mode 100644 index 00000000..d1509853 --- /dev/null +++ b/backend/tests/routeDelivery/routeDelivery.test.ts @@ -0,0 +1,35 @@ +import { describe, it } from '@jest/globals'; +import DataSourceHelper from '../data.utils'; +import app from '../../src/app'; +import * as request from 'supertest'; +import { StatusCode } from '../../src/controllers/statusCode'; + +describe('Route Delivery API 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('GET /api/route_delivery', () => { + it.skip('should return routes grouped', async () => { + // TODO: test here + }); + }) + + describe('POST /api/route_delivery', () => { + it.skip('should save routes in new group', async () => { + // TODO: test here + }); + }) +}) \ No newline at end of file diff --git a/backend/tests/routeDelivery/routeDelivery.utils.ts b/backend/tests/routeDelivery/routeDelivery.utils.ts new file mode 100644 index 00000000..fb68b3f8 --- /dev/null +++ b/backend/tests/routeDelivery/routeDelivery.utils.ts @@ -0,0 +1,19 @@ +import { RouteDeliveryEntity } from '../../src/entities/RouteDeliveryEntity'; +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 RouteDeliveryEntityHelper { + RouteDeliveryRepository: Repository; + + constructor(repository: Repository) { + this.RouteDeliveryRepository = repository; + } + + routeMealDelivery = async () => { + const newRouteDelivery = new RouteDeliveryEntity(); + // TODO: setup routes + return await this.RouteDeliveryRepository.save(newRouteDelivery); + }; +} diff --git a/frontend-admin/src/api/route-deliveries.ts b/frontend-admin/src/api/route-deliveries.ts index 8fe79024..13fa1312 100644 --- a/frontend-admin/src/api/route-deliveries.ts +++ b/frontend-admin/src/api/route-deliveries.ts @@ -1,58 +1,24 @@ import AxiosInstance from './axios'; -export const getRouteDeliveries = async () => { - const response = await AxiosInstance({ - method: "get", - url: "/route_delivery", - }); - - return response -} - -// TODO: review this -export const getRouteDeliveriesSimple = async () => { - const response = await AxiosInstance({ - method: "get", - url: "/route_delivery_simple", - }); - - 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 -} - -export const increaseRouteDeliveryPosition = async (id: number) => { - const response = await AxiosInstance({ - method: "put", - url: "/route_delivery/" + id + "/increment" - }); - - return response -} +class RouteDeliveryAPI { + static getRouteDeliveries = async () => { + const response = await AxiosInstance({ + method: "get", + url: "/route_delivery", + }); + + return response + } -export const decreaseRouteDeliveryPosition = async (id: number) => { - const response = await AxiosInstance({ - method: "put", - url: "/route_delivery/" + id + "/decrement" - }); - - return response + static saveAllRouteDeliveries = async (editRoutes: any) => { + const response = await AxiosInstance({ + method: "post", + url: "/route_delivery", + data: {routes: editRoutes} + }); + + return response + } } -export const saveAllRouteDeliveries = async (editRoutes: any) => { - const response = await AxiosInstance({ - method: "post", - url: "/route_delivery", - data: {routes: editRoutes} - }); - - return response -} +export default RouteDeliveryAPI \ No newline at end of file diff --git a/frontend-admin/src/components/routes/editor/board.tsx b/frontend-admin/src/components/routes/editor/board.tsx index 7f34eacf..4e5c9593 100644 --- a/frontend-admin/src/components/routes/editor/board.tsx +++ b/frontend-admin/src/components/routes/editor/board.tsx @@ -22,11 +22,10 @@ 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 {saveAllRouteDeliveries} from 'src/api/route-deliveries' import { useQuery, } from '@tanstack/react-query' -import {getRouteDeliveries} from 'src/api/route-deliveries' +import RouteDeliveryAPI from 'src/api/route-deliveries' import {StopDetails} from "./stop-details"; type BoardProps = { @@ -35,7 +34,7 @@ type BoardProps = { }; export default function Board({boardAction, setBoardAction}: BoardProps) { - const { isLoading, isError, data, error, refetch, isStale } = useQuery(['routeDeliveries'], () => getRouteDeliveries()) + const { isLoading, isError, data, error, refetch, isStale } = useQuery(['routeDeliveries'], () => RouteDeliveryAPI.getRouteDeliveries()) const [viewRoutes, setViewRoutes] = useState(null); const [editRoutes, setEditRoutes] = useState(null); @@ -46,7 +45,7 @@ export default function Board({boardAction, setBoardAction}: BoardProps) { const queryClient = useQueryClient(); const saveAllRouteDeliveriesMutation = useMutation({ - mutationFn: async (data: any) => await saveAllRouteDeliveries(data), + mutationFn: async (data: any) => await RouteDeliveryAPI.saveAllRouteDeliveries(data), onSuccess: () => { queryClient.invalidateQueries(['routeDeliveries']) refetch() diff --git a/frontend-admin/src/components/routes/page.tsx b/frontend-admin/src/components/routes/page.tsx index 060e7102..b52727dd 100644 --- a/frontend-admin/src/components/routes/page.tsx +++ b/frontend-admin/src/components/routes/page.tsx @@ -2,7 +2,7 @@ import React, {useEffect, useState} from 'react' import { useQuery, } from '@tanstack/react-query' -import {getRouteDeliveries} from 'src/api/route-deliveries' +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' @@ -17,7 +17,7 @@ export enum BoardAction { } const RoutesPage = () => { - const { isLoading, isError, data, error, refetch } = useQuery(['routeDeliveries'], () => getRouteDeliveries()) + const { isLoading, isError, data, error, refetch } = useQuery(['routeDeliveries'], () => RouteDeliveryAPI.getRouteDeliveries()) const [boardAction, setBoardAction] = useState(BoardAction.VIEW) const handleEnableEdit = () => { From ef33846b7daf27d0021531278846311bd28f9577 Mon Sep 17 00:00:00 2001 From: Sophearah Suy-Puth Date: Mon, 1 Apr 2024 15:56:20 +0700 Subject: [PATCH 39/67] setup task test file, fe-admin api service class --- backend/tests/task/tasks.test.ts | 118 ++++++++++-------- frontend-admin/src/api/tasks.ts | 57 ++++----- .../src/components/tasks/modals/create.tsx | 6 +- frontend-admin/src/components/tasks/page.tsx | 4 +- 4 files changed, 99 insertions(+), 86 deletions(-) diff --git a/backend/tests/task/tasks.test.ts b/backend/tests/task/tasks.test.ts index a4c1e86d..a171505b 100644 --- a/backend/tests/task/tasks.test.ts +++ b/backend/tests/task/tasks.test.ts @@ -34,53 +34,71 @@ describe('Tasks tests', () => { 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 + describe('GET /api/tasks', () => { + it.skip('should return all tasks', async () => { + // TODO: test here }); - }); + }) - 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 - } + describe('GET /api/tasks-match', () => { + it.skip('should return all route delivery and volunteer data for matching tasks', async () => { + // TODO: test here }); - }); + }) - 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 - } - ] + describe('POST /api/tasks-match', () => { + it.skip('should create tasks from match data', async () => { + // TODO: test here }); - }); + }) + + // 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 () => { @@ -100,12 +118,12 @@ describe('Tasks tests', () => { // }); // }); - 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({}); - }); + // 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/frontend-admin/src/api/tasks.ts b/frontend-admin/src/api/tasks.ts index 50afe073..0beca4c8 100644 --- a/frontend-admin/src/api/tasks.ts +++ b/frontend-admin/src/api/tasks.ts @@ -1,36 +1,31 @@ import AxiosInstance from './axios'; -export const getTasks = async () => { - const response = await AxiosInstance({ - method: "get", - url: "/tasks", - }); - - return response -} +class TaskAPI { + static getTasks = async () => { + const response = await AxiosInstance({ + method: "get", + url: "/tasks", + }); + + return response + } -export const createTask = async (data: any) => { - const response = await AxiosInstance({ - method: "post", - url: "/tasks", - data: data - }); - return response -} - -export const getTaskMatchData = async () => { - const response = await AxiosInstance({ - method: "get", - url: "/tasks-match/", - }); - return response + static getTaskMatchData = async () => { + const response = await AxiosInstance({ + method: "get", + url: "/tasks-match/", + }); + return response + } + + static saveTaskMatchData = async (matchData: any) => { + const response = await AxiosInstance({ + method: "post", + url: "/tasks-match/", + data: matchData + }); + return response + } } -export const saveTaskMatchData = async (matchData: any) => { - const response = await AxiosInstance({ - method: "post", - url: "/tasks-match/", - data: matchData - }); - return response -} \ No newline at end of file +export default TaskAPI; \ No newline at end of file diff --git a/frontend-admin/src/components/tasks/modals/create.tsx b/frontend-admin/src/components/tasks/modals/create.tsx index 175f8d0d..9e26b3b9 100644 --- a/frontend-admin/src/components/tasks/modals/create.tsx +++ b/frontend-admin/src/components/tasks/modals/create.tsx @@ -3,7 +3,7 @@ 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 { getTaskMatchData, saveTaskMatchData } from "src/api/tasks"; +import TaskAPI from "src/api/tasks"; type VolunteerOption = { id: string, @@ -54,10 +54,10 @@ export const CreateModal = (props: { handleClose: any }) => { const { data } = useQuery({ queryKey: ["task-match"], - queryFn: () => getTaskMatchData(), + queryFn: () => TaskAPI.getTaskMatchData(), }); - const mutation = useMutation(saveTaskMatchData, { + const mutation = useMutation(TaskAPI.saveTaskMatchData, { onSuccess: () => { // Invalidate and refetch queryClient.invalidateQueries("task-match"); diff --git a/frontend-admin/src/components/tasks/page.tsx b/frontend-admin/src/components/tasks/page.tsx index aa5d7cf5..4d36e80a 100644 --- a/frontend-admin/src/components/tasks/page.tsx +++ b/frontend-admin/src/components/tasks/page.tsx @@ -2,7 +2,7 @@ import React, {useState} from 'react' import { useQuery, } from '@tanstack/react-query' -import {getTasks} from 'src/api/tasks' +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' @@ -13,7 +13,7 @@ import { CreateModal } from "./modals"; import { ActionBar } from "src/components/common/layout/page-actionbar"; const TasksPage = () => { - const { isLoading, isError, data, error } = useQuery(['tasks'], () => getTasks()) + const { isLoading, isError, data, error } = useQuery(['tasks'], () => TaskAPI.getTasks()) const { state: createModal, handleOpen: handleOpenCreateModal, From b42a8ae8b44740d9730d862312f5f2b5f92f2077 Mon Sep 17 00:00:00 2001 From: Sophearah Suy-Puth Date: Mon, 1 Apr 2024 16:10:22 +0700 Subject: [PATCH 40/67] disable meal delivery tests --- .../tests/mealDelivery/mealDelivery.test.ts | 132 ------------------ .../tests/mealDelivery/mealDeliverytest.ts | 132 ++++++++++++++++++ 2 files changed, 132 insertions(+), 132 deletions(-) delete mode 100644 backend/tests/mealDelivery/mealDelivery.test.ts create mode 100644 backend/tests/mealDelivery/mealDeliverytest.ts 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/mealDeliverytest.ts b/backend/tests/mealDelivery/mealDeliverytest.ts new file mode 100644 index 00000000..0c9665eb --- /dev/null +++ b/backend/tests/mealDelivery/mealDeliverytest.ts @@ -0,0 +1,132 @@ +// 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({}); +// // }); +// }); From 0f308bed6967c203cf81638531802ec99c711940 Mon Sep 17 00:00:00 2001 From: Sophearah Suy-Puth Date: Mon, 1 Apr 2024 16:21:07 +0700 Subject: [PATCH 41/67] prettier format --- backend/src/controllers/tasks.ts | 34 +++++++++++++++------------ backend/src/controllers/volunteers.ts | 8 +++++-- backend/src/routes/task.routes.ts | 2 +- 3 files changed, 26 insertions(+), 18 deletions(-) diff --git a/backend/src/controllers/tasks.ts b/backend/src/controllers/tasks.ts index 49bcc2b5..8d588ccc 100644 --- a/backend/src/controllers/tasks.ts +++ b/backend/src/controllers/tasks.ts @@ -18,10 +18,12 @@ const getLastMonday = () => { const mostRecentMonday: Date = new Date(currentDate); mostRecentMonday.setDate(currentDate.getDate() - daysUntilMonday); - const formattedMostRecentMonday: string = mostRecentMonday.toISOString().split('T')[0]; + const formattedMostRecentMonday: string = mostRecentMonday + .toISOString() + .split('T')[0]; - return formattedMostRecentMonday -} + return formattedMostRecentMonday; +}; export default class TaskController { private TaskRepository = AppDataSource.getRepository(TaskEntity); @@ -180,16 +182,16 @@ export default class TaskController { name: true, availabilities: true, availabilitiesLastUpdated: true - }, + } }); - const comparisonDate = getLastMonday() + const comparisonDate = getLastMonday(); const availableVolunteers = volunteers.filter((v) => { if (v.availabilitiesLastUpdated >= new Date(comparisonDate)) { - return v + return v; } - }) + }); const routes = await this.RouteDeliveryRepository.find({ relations: { @@ -216,12 +218,14 @@ export default class TaskController { ); } - response.status(StatusCode.OK).json({ availableVolunteers: availableVolunteers, routes: groups }); + response + .status(StatusCode.OK) + .json({ availableVolunteers: availableVolunteers, routes: groups }); }; saveTaskMatch = async (request: Request, response: Response) => { - const taskMatches = request.body.matches - + const taskMatches = request.body.matches; + for (let i = 0; i < taskMatches.length; i++) { const match = taskMatches[i]; @@ -234,7 +238,7 @@ export default class TaskController { }); const savedTask = await this.TaskRepository.save(newTask); - + // CREATE MEAL DELIVERY OBJECTS const routeObjs = await this.RouteDeliveryRepository.find({ where: { @@ -243,7 +247,7 @@ export default class TaskController { relations: { client: true } - }) + }); for (let i = 0; i < routeObjs.length; i++) { const routeObj = routeObjs[i]; @@ -257,11 +261,11 @@ export default class TaskController { }); newMeal.client = foundClient; const savedDelivery = await this.MealDeliveryRepository.save(newMeal); - newTask.deliveries.push(savedDelivery) + newTask.deliveries.push(savedDelivery); } - + await this.TaskRepository.save(newTask); } response.status(StatusCode.OK).json({}); - } + }; } diff --git a/backend/src/controllers/volunteers.ts b/backend/src/controllers/volunteers.ts index 6ec47c72..e3ece26f 100644 --- a/backend/src/controllers/volunteers.ts +++ b/backend/src/controllers/volunteers.ts @@ -8,7 +8,8 @@ import * as jwt from 'jsonwebtoken'; import { getNeighbourhoodFromString } from '../entities/types'; export default class VolunteerController { - private static VolunteerRepository = AppDataSource.getRepository(VolunteerEntity); + private static VolunteerRepository = + AppDataSource.getRepository(VolunteerEntity); static getVolunteers = async (request: Request, response: Response) => { const volunteers = await this.VolunteerRepository.find({ @@ -79,7 +80,10 @@ export default class VolunteerController { : response.status(StatusCode.OK).json({ tasks: volunteer.tasks }); }; - static getVolunteerAvailabilities = async (request: Request, response: Response) => { + static getVolunteerAvailabilities = async ( + request: Request, + response: Response + ) => { const volunteer = await this.VolunteerRepository.findOne({ where: { id: parseInt(request.params.id) }, relations: ['availabilities'] diff --git a/backend/src/routes/task.routes.ts b/backend/src/routes/task.routes.ts index b52babe0..621852d2 100644 --- a/backend/src/routes/task.routes.ts +++ b/backend/src/routes/task.routes.ts @@ -13,4 +13,4 @@ router.put('/tasks/:id', taskController.updateOrCreateTask); router.post('/tasks/', taskController.createTask); router.delete('/tasks/:id', taskController.deleteTask); router.get('/tasks-match', taskController.getTaskMatchData); -router.post('/tasks-match', taskController.saveTaskMatch); \ No newline at end of file +router.post('/tasks-match', taskController.saveTaskMatch); From 2c1035ab9894e85b83e7702e1c47b8632dd92874 Mon Sep 17 00:00:00 2001 From: Sophearah Suy-Puth Date: Mon, 1 Apr 2024 16:51:09 +0700 Subject: [PATCH 42/67] auth api service class on fe-admin --- frontend-admin/src/api/auth.ts | 13 ++++++------- frontend-admin/src/components/auth/page.tsx | 4 ++-- 2 files changed, 8 insertions(+), 9 deletions(-) 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/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'), }) From d2e96d5f7925ad2c270a18c6c564e2596bbb652f Mon Sep 17 00:00:00 2001 From: Sophearah Suy-Puth Date: Sat, 11 May 2024 15:55:08 +0700 Subject: [PATCH 43/67] 1) client type definitions, 2) setup meal/program type enums under types, 3) service methods and tests for getActiveClients(), getAllClients(), getClient(), 4) client test utils, 5) deprecated old client tests --- backend/src/services/clients.ts | 32 ++++++ backend/src/types/clients.ts | 14 +++ backend/src/types/enums.ts | 12 +++ ...client.test.ts => deprecatedclienttest.ts} | 2 +- ...ient.utils.ts => deprecatedclientutils.ts} | 0 backend/tests/client/service.test.ts | 102 ++++++++++++++++++ backend/tests/client/utils.ts | 43 ++++++++ 7 files changed, 204 insertions(+), 1 deletion(-) create mode 100644 backend/src/services/clients.ts create mode 100644 backend/src/types/clients.ts create mode 100644 backend/src/types/enums.ts rename backend/tests/client/{client.test.ts => deprecatedclienttest.ts} (98%) rename backend/tests/client/{client.utils.ts => deprecatedclientutils.ts} (100%) create mode 100644 backend/tests/client/service.test.ts create mode 100644 backend/tests/client/utils.ts diff --git a/backend/src/services/clients.ts b/backend/src/services/clients.ts new file mode 100644 index 00000000..71fa61b1 --- /dev/null +++ b/backend/src/services/clients.ts @@ -0,0 +1,32 @@ +import { AppDataSource } from '../data-source'; +import { ClientEntity } from '../entities/ClientEntity'; +import { Client } from '../types/clients'; + +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 () => { + const clients = await this.ClientRepository.find({}); + return clients + } + + static getClient = async (id: number) => { + const client = await this.ClientRepository.findOne({ + where: { + id: id + } + }); + + return client + } +} diff --git a/backend/src/types/clients.ts b/backend/src/types/clients.ts new file mode 100644 index 00000000..8fc07d0a --- /dev/null +++ b/backend/src/types/clients.ts @@ -0,0 +1,14 @@ +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; +} + diff --git a/backend/src/types/enums.ts b/backend/src/types/enums.ts new file mode 100644 index 00000000..22c3ef26 --- /dev/null +++ b/backend/src/types/enums.ts @@ -0,0 +1,12 @@ +export enum ProgramType { + MAP = 'MAP', + STS = 'STS' +} + +export enum MealType { + VEGETARIAN = 'Vegetarian', + NOFISH = 'No Fish', + NOMEAT = 'No Meat', + REGULAR = 'Regular' +} + \ No newline at end of file diff --git a/backend/tests/client/client.test.ts b/backend/tests/client/deprecatedclienttest.ts similarity index 98% rename from backend/tests/client/client.test.ts rename to backend/tests/client/deprecatedclienttest.ts index c314fdad..00fb0b29 100644 --- a/backend/tests/client/client.test.ts +++ b/backend/tests/client/deprecatedclienttest.ts @@ -9,7 +9,7 @@ import { } from '@jest/globals'; import * as request from 'supertest'; import DataSourceHelper from '../data.utils'; -import ClientEntityHelper from './client.utils'; +import ClientEntityHelper from './deprecatedclientutils'; import { AppDataSource } from '../../src/data-source'; import { ClientEntity } from '../../src/entities/ClientEntity'; import app from '../../src/app'; diff --git a/backend/tests/client/client.utils.ts b/backend/tests/client/deprecatedclientutils.ts similarity index 100% rename from backend/tests/client/client.utils.ts rename to backend/tests/client/deprecatedclientutils.ts diff --git a/backend/tests/client/service.test.ts b/backend/tests/client/service.test.ts new file mode 100644 index 00000000..4cf6ef99 --- /dev/null +++ b/backend/tests/client/service.test.ts @@ -0,0 +1,102 @@ +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 } 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, + }); + }); + }) +}); diff --git a/backend/tests/client/utils.ts b/backend/tests/client/utils.ts new file mode 100644 index 00000000..f55d918f --- /dev/null +++ b/backend/tests/client/utils.ts @@ -0,0 +1,43 @@ +import { AppDataSource } from '../../src/data-source'; +import { ClientEntity } from '../../src/entities/ClientEntity'; +import { MealType } from '../../src/types/enums'; + +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 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; + return await this.ClientRepository.save(newClient); + } +} From cd10b3f5b5b428f18656f7a6ec84c4e0cebf8a23 Mon Sep 17 00:00:00 2001 From: Sophearah Suy-Puth Date: Sat, 11 May 2024 20:00:59 +0700 Subject: [PATCH 44/67] 1) service method/test createClient(), 2) service method createStopsFromClient(), 3) test util getStopsForClient() --- backend/src/services/clients.ts | 20 ++++++++++++- backend/src/services/stop.ts | 30 +++++++++++++++++++ backend/src/types/clients.ts | 1 + backend/tests/client/service.test.ts | 44 +++++++++++++++++++++++++++- backend/tests/client/utils.ts | 23 +++++++++++++++ 5 files changed, 116 insertions(+), 2 deletions(-) create mode 100644 backend/src/services/stop.ts diff --git a/backend/src/services/clients.ts b/backend/src/services/clients.ts index 71fa61b1..31aab6de 100644 --- a/backend/src/services/clients.ts +++ b/backend/src/services/clients.ts @@ -1,6 +1,7 @@ import { AppDataSource } from '../data-source'; import { ClientEntity } from '../entities/ClientEntity'; -import { Client } from '../types/clients'; +import { Client, CreateClientProps } from '../types/clients'; +import StopService from './stop'; export default class ClientService { static readonly ClientRepository = AppDataSource.getRepository(ClientEntity); @@ -29,4 +30,21 @@ export default class ClientService { return client } + + static createClient = async (props: CreateClientProps) => { + 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 + } } diff --git a/backend/src/services/stop.ts b/backend/src/services/stop.ts new file mode 100644 index 00000000..71a560f1 --- /dev/null +++ b/backend/src/services/stop.ts @@ -0,0 +1,30 @@ +import { AppDataSource } from '../data-source'; +import { Client } from "../types/clients" +import { RouteDeliveryEntity } from '../entities/RouteDeliveryEntity'; +import { ProgramType } from '../types/enums'; + +export default class StopService { + static readonly StopRepository = AppDataSource.getRepository(RouteDeliveryEntity); + + 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); + } + } + + private static createDefaultStopFromClient = async (client: Client) => { + const stop = new RouteDeliveryEntity(); + stop.routeNumber = 0; + stop.routePosition = 0; + stop.client = client; + stop.mealType = client.mealType; + return stop + } +} \ No newline at end of file diff --git a/backend/src/types/clients.ts b/backend/src/types/clients.ts index 8fc07d0a..c63be76f 100644 --- a/backend/src/types/clients.ts +++ b/backend/src/types/clients.ts @@ -12,3 +12,4 @@ export type Client = { softDelete: boolean; } +export type CreateClientProps = Omit \ No newline at end of file diff --git a/backend/tests/client/service.test.ts b/backend/tests/client/service.test.ts index 4cf6ef99..4fd0f967 100644 --- a/backend/tests/client/service.test.ts +++ b/backend/tests/client/service.test.ts @@ -10,7 +10,7 @@ import { import DataSourceHelper from '../data.utils'; import ClientService from '../../src/services/clients'; import ClientUtils from './utils'; -import { MealType } from '../../src/types/enums'; +import { MealType, ProgramType } from '../../src/types/enums'; describe('Client Service Unit Tests', () => { @@ -99,4 +99,46 @@ describe('Client Service Unit Tests', () => { }); }); }) + + 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 + }); + }); + }) }); diff --git a/backend/tests/client/utils.ts b/backend/tests/client/utils.ts index f55d918f..21c04171 100644 --- a/backend/tests/client/utils.ts +++ b/backend/tests/client/utils.ts @@ -1,6 +1,9 @@ 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 { RouteDeliveryEntity } from '../../src/entities/RouteDeliveryEntity'; +import { ProgramType } from '../../src/entities/types'; type CreateClientProps = { name?: string, @@ -15,6 +18,7 @@ type CreateClientProps = { export default class ClientUtils { static ClientRepository = AppDataSource.getRepository(ClientEntity); + static StopRepository = AppDataSource.getRepository(RouteDeliveryEntity) static createClient = async (props: Partial = {}) => { const mergedProps: CreateClientProps = { @@ -40,4 +44,23 @@ export default class ClientUtils { newClient.softDelete = mergedProps.softDelete; return await this.ClientRepository.save(newClient); } + + static getStopsForClient = async (client: Client) => { + const stops = await this.StopRepository.find({ + relations: { + client: true + }, + where: { + client: client + } + }) + + 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 + }; + } } From c7c2a4d8bad1c7bcbf251bc679b91f72ca19425e Mon Sep 17 00:00:00 2001 From: Sophearah Suy-Puth Date: Mon, 13 May 2024 14:09:41 +0700 Subject: [PATCH 45/67] 1) service method/test updateClient(), 2) service method updateStopsFromClient(), 3) update create client util to build the stops --- backend/src/services/clients.ts | 27 ++++- backend/src/services/stop.ts | 59 ++++++++++- backend/src/types/clients.ts | 3 +- backend/tests/client/service.test.ts | 143 ++++++++++++++++++++++++++- backend/tests/client/utils.ts | 27 ++++- 5 files changed, 252 insertions(+), 7 deletions(-) diff --git a/backend/src/services/clients.ts b/backend/src/services/clients.ts index 31aab6de..7e92d166 100644 --- a/backend/src/services/clients.ts +++ b/backend/src/services/clients.ts @@ -1,6 +1,6 @@ import { AppDataSource } from '../data-source'; import { ClientEntity } from '../entities/ClientEntity'; -import { Client, CreateClientProps } from '../types/clients'; +import { Client, CreateClientProps, UpdateClientProps } from '../types/clients'; import StopService from './stop'; export default class ClientService { @@ -47,4 +47,29 @@ export default class ClientService { return client } + + static updateClient = async (id: number, updateProps: UpdateClientProps) => { + 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/stop.ts b/backend/src/services/stop.ts index 71a560f1..742de2c7 100644 --- a/backend/src/services/stop.ts +++ b/backend/src/services/stop.ts @@ -1,5 +1,5 @@ import { AppDataSource } from '../data-source'; -import { Client } from "../types/clients" +import { Client, UpdateClientProps } from "../types/clients" import { RouteDeliveryEntity } from '../entities/RouteDeliveryEntity'; import { ProgramType } from '../types/enums'; @@ -19,6 +19,63 @@ export default class StopService { } } + 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) + } + } + private static createDefaultStopFromClient = async (client: Client) => { const stop = new RouteDeliveryEntity(); stop.routeNumber = 0; diff --git a/backend/src/types/clients.ts b/backend/src/types/clients.ts index c63be76f..fd08600b 100644 --- a/backend/src/types/clients.ts +++ b/backend/src/types/clients.ts @@ -12,4 +12,5 @@ export type Client = { softDelete: boolean; } -export type CreateClientProps = Omit \ No newline at end of file +export type CreateClientProps = Omit +export type UpdateClientProps = Partial \ No newline at end of file diff --git a/backend/tests/client/service.test.ts b/backend/tests/client/service.test.ts index 4fd0f967..e446d024 100644 --- a/backend/tests/client/service.test.ts +++ b/backend/tests/client/service.test.ts @@ -68,7 +68,6 @@ describe('Client Service Unit Tests', () => { }); }) - describe('getClient()', () => { it("should return no clients, when it doesn't exist", async () => { const client = await ClientService.getClient(1) @@ -86,7 +85,7 @@ describe('Client Service Unit Tests', () => { map: true, }); const client = await ClientService.getClient(1); - + expect(client).toMatchObject({ id: 1, name: 'Firstname Lastname', @@ -141,4 +140,144 @@ describe('Client Service Unit Tests', () => { }); }); }) + + 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 index 21c04171..7e6f3670 100644 --- a/backend/tests/client/utils.ts +++ b/backend/tests/client/utils.ts @@ -42,7 +42,30 @@ export default class ClientUtils { newClient.sts = mergedProps.sts; newClient.map = mergedProps.map; newClient.softDelete = mergedProps.softDelete; - return await this.ClientRepository.save(newClient); + + const savedClient = await this.ClientRepository.save(newClient); + + if (savedClient.sts) { + const stop = new RouteDeliveryEntity(); + 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 RouteDeliveryEntity(); + 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) => { @@ -51,7 +74,7 @@ export default class ClientUtils { client: true }, where: { - client: client + client: { id: client.id }, } }) From 6c9652473b1e8d6fa2cc0d279795e76ee35d962e Mon Sep 17 00:00:00 2001 From: Sophearah Suy-Puth Date: Mon, 13 May 2024 14:36:48 +0700 Subject: [PATCH 46/67] client controller --- backend/src/controllers/clients.ts | 187 ++++++---------------------- backend/src/routes/client.routes.ts | 2 +- 2 files changed, 36 insertions(+), 153 deletions(-) diff --git a/backend/src/controllers/clients.ts b/backend/src/controllers/clients.ts index 7cd3e3da..d519d8a3 100644 --- a/backend/src/controllers/clients.ts +++ b/backend/src/controllers/clients.ts @@ -1,157 +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({ - where: { - softDelete: false - } - }); - 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), - softDelete: false - } - }); - response.status(StatusCode.OK).json({ client: client }); - }; - - editClient = async (request: Request, response: Response) => { - const originalClient = await this.ClientRepository.findOne({ - where: { id: parseInt(request.params.id) } - }); - - const editedMealType = !(request.body.mealType == originalClient.mealType); - const editedSTS = !(request.body.sts == originalClient.sts); - const editedMAP = !(request.body.map == originalClient.map); - - 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 - } - ); - - const savedClient = await this.ClientRepository.findOne({ - where: { id: parseInt(request.params.id) } - }); - - if (editedSTS) { - if (request.body.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); - } else { - const stsRouteDelivery = await this.RouteDeliveryRepository.find({ - relations: { client: true }, - where: { - client: { id: parseInt(request.params.id) }, - program: ProgramType.STS - } - }); - - await this.RouteDeliveryRepository.remove(stsRouteDelivery); - } - } - - if (editedMAP) { - if (request.body.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); - } else { - const mapRouteDelivery = await this.RouteDeliveryRepository.find({ - relations: { client: true }, - where: { - client: { id: parseInt(request.params.id) }, - program: ProgramType.MAP - } - }); - - await this.RouteDeliveryRepository.remove(mapRouteDelivery); - } - } - - if (editedMealType) { - const routeDeliveries = await this.RouteDeliveryRepository.find({ - relations: { client: true }, - where: { client: { id: parseInt(request.params.id) } } - }); - - routeDeliveries.forEach((routeDelivery) => { - routeDelivery.mealType = savedClient.mealType; - }); - - this.RouteDeliveryRepository.save(routeDeliveries); - } - - response.status(StatusCode.OK).json({ client }); - }; + getClients = async (request: Request, response: Response) => { + const clients = ClientService.getAllClients() + response.status(StatusCode.OK).json({clients: clients}) + } + + createClient = async (request: Request, response: Response) => { + const props: CreateClientProps = request.body + const client = ClientService.createClient(props) + response.status(StatusCode.OK).json({client: client}) + } + + getClient = async (request: Request, response: Response) => { + const id = parseInt(request.params.id) + const client = ClientService.getClient(id) + + if (client == null) { + response.status(StatusCode.NOT_FOUND) + } else { + response.status(StatusCode.OK).json({client: client}) + } + } + + updateClient = async (request: Request, response: Response) => { + const id = parseInt(request.params.id) + const props: UpdateClientProps = request.body + const client = 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/routes/client.routes.ts b/backend/src/routes/client.routes.ts index 6eb3a8cf..29eb4148 100644 --- a/backend/src/routes/client.routes.ts +++ b/backend/src/routes/client.routes.ts @@ -7,5 +7,5 @@ const clientController = new ClientController(); router.get('/clients', clientController.getClients); router.get('/clients/:id', clientController.getClient); -router.put('/clients/:id/edit', clientController.editClient); +router.put('/clients/:id/edit', clientController.updateClient); router.post('/clients', clientController.createClient); From f6952c9095ca045edb36f42c0adce486f73a717a Mon Sep 17 00:00:00 2001 From: Sophearah Suy-Puth Date: Mon, 13 May 2024 14:37:56 +0700 Subject: [PATCH 47/67] remove controller tests (too detailed, focus on service level function tests --- backend/tests/client/deprecatedclienttest.ts | 185 ------------------ backend/tests/client/deprecatedclientutils.ts | 44 ----- 2 files changed, 229 deletions(-) delete mode 100644 backend/tests/client/deprecatedclienttest.ts delete mode 100644 backend/tests/client/deprecatedclientutils.ts diff --git a/backend/tests/client/deprecatedclienttest.ts b/backend/tests/client/deprecatedclienttest.ts deleted file mode 100644 index 00fb0b29..00000000 --- a/backend/tests/client/deprecatedclienttest.ts +++ /dev/null @@ -1,185 +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 './deprecatedclientutils'; -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 - describe('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: [] - }); - }); - - 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, - softDelete: false - }, - { - id: 2, - name: 'Test Client2', - email: 'email2@email.com', - phoneNumber: '0987654321', - address: '1234 Test Address2', - mealType: MealType.NOMEAT, - sts: false, - map: true, - softDelete: false - } - ] - }); - }); - }); - - describe('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, - softDelete: false - } - }); - }); - }) - - describe('PUT /api/clients/:id/edit', () => { - it('should update 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).put(`/api/clients/${client1.id}/edit`).send({ - name: 'name2', - email: 'email1@gmail.com' - }); - expect(res.status).toBe(StatusCode.OK); - const updatedClient = await clientHelper.getClient(client1.id) - expect(updatedClient).toEqual( - expect.objectContaining( - { - name: 'name2', - email: 'email1@gmail.com' - } - ) - ) - }); - }) - - describe('POST /api/clients', () => { - it('should create client', async () => { - const res = await request(app).post(`/api/clients`).send({ - name: 'Test Client', - email: 'email@email.com', - phoneNumber: '1234567890', - address: '1234 Test Address', - mealType: MealType.NOFISH, - sts: true, - map: true, - }); - 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, - softDelete: false - } - }); - }) - }) -}); diff --git a/backend/tests/client/deprecatedclientutils.ts b/backend/tests/client/deprecatedclientutils.ts deleted file mode 100644 index 30878c8f..00000000 --- a/backend/tests/client/deprecatedclientutils.ts +++ /dev/null @@ -1,44 +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); - } - - getClient = async (clientId: number) => { - return await this.ClientRepository.findOne({ - where: { - id: clientId, - softDelete: false - } - }); - } -} From 3c5b0c706a4703f8c9cb30f6b818df94e9e3894e Mon Sep 17 00:00:00 2001 From: Sophearah Suy-Puth Date: Mon, 13 May 2024 17:43:17 +0700 Subject: [PATCH 48/67] 1) service method/test getRoutes(), 2) test util for creating routes, 3) type definition file for stops --- backend/src/services/clients.ts | 6 +- backend/src/services/stop.ts | 26 +++++++ backend/src/types/stop.ts | 14 ++++ backend/tests/stops/service.test.ts | 117 ++++++++++++++++++++++++++++ backend/tests/stops/utils.ts | 55 +++++++++++++ 5 files changed, 215 insertions(+), 3 deletions(-) create mode 100644 backend/src/types/stop.ts create mode 100644 backend/tests/stops/service.test.ts create mode 100644 backend/tests/stops/utils.ts diff --git a/backend/src/services/clients.ts b/backend/src/services/clients.ts index 7e92d166..2ac50c11 100644 --- a/backend/src/services/clients.ts +++ b/backend/src/services/clients.ts @@ -16,7 +16,7 @@ export default class ClientService { return clients } - static getAllClients = async () => { + static getAllClients = async (): Promise => { const clients = await this.ClientRepository.find({}); return clients } @@ -31,7 +31,7 @@ export default class ClientService { return client } - static createClient = async (props: CreateClientProps) => { + static createClient = async (props: CreateClientProps): Promise => { const client = await this.ClientRepository.create({ name: props.name, email: props.email, @@ -48,7 +48,7 @@ export default class ClientService { return client } - static updateClient = async (id: number, updateProps: UpdateClientProps) => { + static updateClient = async (id: number, updateProps: UpdateClientProps): Promise => { const client = await this.ClientRepository.findOne({ where: { id: id diff --git a/backend/src/services/stop.ts b/backend/src/services/stop.ts index 742de2c7..65c31159 100644 --- a/backend/src/services/stop.ts +++ b/backend/src/services/stop.ts @@ -2,6 +2,7 @@ import { AppDataSource } from '../data-source'; import { Client, UpdateClientProps } from "../types/clients" import { RouteDeliveryEntity } from '../entities/RouteDeliveryEntity'; import { ProgramType } from '../types/enums'; +import {Routes} from '../types/stop'; export default class StopService { static readonly StopRepository = AppDataSource.getRepository(RouteDeliveryEntity); @@ -76,6 +77,31 @@ export default class StopService { } } + static getRoutes = async (): Promise => { + const stops = await this.StopRepository.find({ + relations: { + client: true + } + }) + + 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 + ); + } + + return routes + } + private static createDefaultStopFromClient = async (client: Client) => { const stop = new RouteDeliveryEntity(); stop.routeNumber = 0; diff --git a/backend/src/types/stop.ts b/backend/src/types/stop.ts new file mode 100644 index 00000000..2e08a625 --- /dev/null +++ b/backend/src/types/stop.ts @@ -0,0 +1,14 @@ +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[]; +} \ No newline at end of file diff --git a/backend/tests/stops/service.test.ts b/backend/tests/stops/service.test.ts new file mode 100644 index 00000000..8aa2c2bb --- /dev/null +++ b/backend/tests/stops/service.test.ts @@ -0,0 +1,117 @@ +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 + }); + }); + }); +}); diff --git a/backend/tests/stops/utils.ts b/backend/tests/stops/utils.ts new file mode 100644 index 00000000..a8fa17ad --- /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 { RouteDeliveryEntity } from '../../src/entities/RouteDeliveryEntity'; + +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(RouteDeliveryEntity) + + 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 RouteDeliveryEntity(); + 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); + } + } + } +} From c6a453a06a77db22dbfd70db097634d821355e2f Mon Sep 17 00:00:00 2001 From: Sophearah Suy-Puth Date: Mon, 13 May 2024 17:53:22 +0700 Subject: [PATCH 49/67] tab prettier config --- backend/.prettierrc | 1 + backend/src/ExampleEntity.ts | 12 +- backend/src/app.ts | 8 +- backend/src/controllers/admin.ts | 54 +- backend/src/controllers/authentication.ts | 31 +- backend/src/controllers/clients.ts | 60 +-- backend/src/controllers/mealDelivery.ts | 179 ++++--- backend/src/controllers/routeDelivery.ts | 259 ++++----- backend/src/controllers/statusCode.ts | 8 +- backend/src/controllers/tasks.ts | 498 +++++++++--------- backend/src/controllers/volunteers.ts | 193 +++---- backend/src/data-source.ts | 42 +- backend/src/entities/AdminEntity.ts | 8 +- backend/src/entities/ClientEntity.ts | 42 +- backend/src/entities/MealDeliveryEntity.ts | 44 +- backend/src/entities/RouteDeliveryEntity.ts | 24 +- backend/src/entities/TaskEntity.ts | 34 +- backend/src/entities/UserEntity.ts | 28 +- backend/src/entities/VolunteerEntity.ts | 104 ++-- backend/src/entities/types.ts | 58 +- backend/src/middlewares/authentication.ts | 14 +- backend/src/routes/mealDelivery.routes.ts | 8 +- backend/src/routes/routeDelivery.routes.ts | 12 +- backend/src/scripts/drop.ts | 8 +- backend/src/scripts/seed.ts | 24 +- backend/src/scripts/seeders/admin.seeder.ts | 32 +- backend/src/scripts/seeders/client.seeder.ts | 122 ++--- backend/src/scripts/seeders/task.seeder.ts | 119 +++-- backend/src/scripts/seeders/user.ts | 40 +- .../src/scripts/seeders/volunteer.seeder.ts | 114 ++-- backend/src/services/clients.ts | 45 +- backend/src/services/stop.ts | 68 ++- backend/src/types/clients.ts | 10 +- backend/src/types/enums.ts | 5 +- backend/src/types/stop.ts | 8 +- 35 files changed, 1173 insertions(+), 1143 deletions(-) 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/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 83f27b01..7648c726 100644 --- a/backend/src/app.ts +++ b/backend/src/app.ts @@ -15,10 +15,10 @@ 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({ - credentials: true, - origin: ['http://localhost:5173'] - }) + cors({ + credentials: true, + origin: ['http://localhost:5173'] + }) ); // Routes go here diff --git a/backend/src/controllers/admin.ts b/backend/src/controllers/admin.ts index b0558b05..4834d06f 100644 --- a/backend/src/controllers/admin.ts +++ b/backend/src/controllers/admin.ts @@ -9,34 +9,36 @@ require('dotenv').config(); const TOKEN_KEY = process.env.TOKEN_KEY; export default class AdminController { - private AdminRepository = AppDataSource.getRepository(AdminEntity); + 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'); - } + login = async (request: Request, response: Response) => { + console.log(request); + const { email, password } = request.body; + if (!(email && password)) { + console.log('missing params'); + } - const adminUser = await this.AdminRepository.findOne({ where: { email } }); + const adminUser = await this.AdminRepository.findOne({ + where: { email } + }); - // User not found - if (!adminUser) - return response - .status(StatusCode.NOT_FOUND) - .json({ message: 'User not found' }); + // User not found + if (!adminUser) + return 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' }); - } - }; + 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' }); + } + }; } diff --git a/backend/src/controllers/authentication.ts b/backend/src/controllers/authentication.ts index ed90eb4d..46ec98bb 100644 --- a/backend/src/controllers/authentication.ts +++ b/backend/src/controllers/authentication.ts @@ -6,21 +6,22 @@ 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); + 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 } - }); + 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' }); - }; + 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 d519d8a3..638aea6a 100644 --- a/backend/src/controllers/clients.ts +++ b/backend/src/controllers/clients.ts @@ -4,37 +4,37 @@ import ClientService from '../services/clients'; import { Client, CreateClientProps, UpdateClientProps } from '../types/clients'; export default class ClientController { - getClients = async (request: Request, response: Response) => { - const clients = ClientService.getAllClients() - response.status(StatusCode.OK).json({clients: clients}) - } + getClients = async (request: Request, response: Response) => { + const clients = ClientService.getAllClients(); + response.status(StatusCode.OK).json({ clients: clients }); + }; - createClient = async (request: Request, response: Response) => { - const props: CreateClientProps = request.body - const client = ClientService.createClient(props) - response.status(StatusCode.OK).json({client: client}) - } + createClient = async (request: Request, response: Response) => { + const props: CreateClientProps = request.body; + const client = ClientService.createClient(props); + response.status(StatusCode.OK).json({ client: client }); + }; - getClient = async (request: Request, response: Response) => { - const id = parseInt(request.params.id) - const client = ClientService.getClient(id) - - if (client == null) { - response.status(StatusCode.NOT_FOUND) - } else { - response.status(StatusCode.OK).json({client: client}) - } - } + getClient = async (request: Request, response: Response) => { + const id = parseInt(request.params.id); + const client = ClientService.getClient(id); - updateClient = async (request: Request, response: Response) => { - const id = parseInt(request.params.id) - const props: UpdateClientProps = request.body - const client = ClientService.updateClient(id, props) - - if (client == null) { - response.status(StatusCode.NOT_FOUND) - } else { - response.status(StatusCode.OK).json({client: client}) - } - } + if (client == null) { + response.status(StatusCode.NOT_FOUND); + } else { + response.status(StatusCode.OK).json({ client: client }); + } + }; + + updateClient = async (request: Request, response: Response) => { + const id = parseInt(request.params.id); + const props: UpdateClientProps = request.body; + const client = 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 index ec8bce7b..5cb90ca5 100644 --- a/backend/src/controllers/mealDelivery.ts +++ b/backend/src/controllers/mealDelivery.ts @@ -6,96 +6,103 @@ 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); + 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 }); - }; + 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 }); - }; + 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; + 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 + 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 }); } - }); - 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({}); - }; + 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 index 12411ba8..7f9445bf 100644 --- a/backend/src/controllers/routeDelivery.ts +++ b/backend/src/controllers/routeDelivery.ts @@ -5,134 +5,137 @@ import { TaskEntity } from '../entities/TaskEntity'; import { StatusCode } from './statusCode'; export default class RouteDeliveryController { - private RouteDeliveryRepository = - AppDataSource.getRepository(RouteDeliveryEntity); - - // TODO: review this? v - getRouteDeliveriesSimple = async (request: Request, response: Response) => { - const routes = await this.RouteDeliveryRepository.find({ - relations: { - client: true - } - }); - - response.status(StatusCode.OK).json({ routes: routes }); - }; - - 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 routePosition = - request.body.routeNumber == 0 - ? 0 - : await this.getNextRouteNumber(request.body.routeNumber); - - const route = await this.RouteDeliveryRepository.update( - { id: request.params.id }, - { routeNumber: request.body.routeNumber, routePosition: routePosition } - ); - - response.status(StatusCode.OK).json({ route }); - }; - - incrementRoutePosition = async (request: Request, response: Response) => { - const r = await this.getRouteFromId(request.params.id); - - await this.RouteDeliveryRepository.decrement( - { routeNumber: r.routeNumber, routePosition: r.routePosition + 1 }, - 'routePosition', - 1 - ); - - const route = await this.RouteDeliveryRepository.increment( - { id: request.params.id }, - 'routePosition', - 1 - ); - - response.status(StatusCode.OK).json({ route }); - }; - - decrementRoutePosition = async (request: Request, response: Response) => { - const r = await this.getRouteFromId(request.params.id); - - await this.RouteDeliveryRepository.increment( - { routeNumber: r.routeNumber, routePosition: r.routePosition - 1 }, - 'routePosition', - 1 - ); - - const route = await this.RouteDeliveryRepository.decrement( - { id: request.params.id }, - 'routePosition', - 1 - ); - - response.status(StatusCode.OK).json({ route }); - }; - - // Helper Functions - - getRouteFromId = async (id: string) => { - const route = await this.RouteDeliveryRepository.findOne({ - where: { - id: id - } - }); - - return route; - }; - - // Get the next route number - getNextRouteNumber = async (routeNumber) => { - const routes = await this.RouteDeliveryRepository.find({ - where: { - routeNumber: routeNumber - } - }); - - return routes.length || 0; - }; - - saveAllRouteDeliveries = async (request: Request, response: Response) => { - const routes = request.body.routes; - - // for each column - Object.entries(routes).map(([column, data]) => { - // for each stop - routes[column].map(async (stop, index) => { - const updatedStop = await this.RouteDeliveryRepository.update( - { id: stop.id }, - { routeNumber: parseInt(column), routePosition: index } + private RouteDeliveryRepository = + AppDataSource.getRepository(RouteDeliveryEntity); + + // TODO: review this? v + getRouteDeliveriesSimple = async (request: Request, response: Response) => { + const routes = await this.RouteDeliveryRepository.find({ + relations: { + client: true + } + }); + + response.status(StatusCode.OK).json({ routes: routes }); + }; + + 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 routePosition = + request.body.routeNumber == 0 + ? 0 + : await this.getNextRouteNumber(request.body.routeNumber); + + const route = await this.RouteDeliveryRepository.update( + { id: request.params.id }, + { + routeNumber: request.body.routeNumber, + routePosition: routePosition + } ); - }); - }); - response.status(StatusCode.OK).json({ routes: 'fd' }); - }; + response.status(StatusCode.OK).json({ route }); + }; + + incrementRoutePosition = async (request: Request, response: Response) => { + const r = await this.getRouteFromId(request.params.id); + + await this.RouteDeliveryRepository.decrement( + { routeNumber: r.routeNumber, routePosition: r.routePosition + 1 }, + 'routePosition', + 1 + ); + + const route = await this.RouteDeliveryRepository.increment( + { id: request.params.id }, + 'routePosition', + 1 + ); + + response.status(StatusCode.OK).json({ route }); + }; + + decrementRoutePosition = async (request: Request, response: Response) => { + const r = await this.getRouteFromId(request.params.id); + + await this.RouteDeliveryRepository.increment( + { routeNumber: r.routeNumber, routePosition: r.routePosition - 1 }, + 'routePosition', + 1 + ); + + const route = await this.RouteDeliveryRepository.decrement( + { id: request.params.id }, + 'routePosition', + 1 + ); + + response.status(StatusCode.OK).json({ route }); + }; + + // Helper Functions + + getRouteFromId = async (id: string) => { + const route = await this.RouteDeliveryRepository.findOne({ + where: { + id: id + } + }); + + return route; + }; + + // Get the next route number + getNextRouteNumber = async (routeNumber) => { + const routes = await this.RouteDeliveryRepository.find({ + where: { + routeNumber: routeNumber + } + }); + + return routes.length || 0; + }; + + saveAllRouteDeliveries = async (request: Request, response: Response) => { + const routes = request.body.routes; + + // for each column + Object.entries(routes).map(([column, data]) => { + // for each stop + routes[column].map(async (stop, index) => { + const updatedStop = await this.RouteDeliveryRepository.update( + { id: stop.id }, + { routeNumber: parseInt(column), routePosition: index } + ); + }); + }); + + response.status(StatusCode.OK).json({ routes: 'fd' }); + }; } 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/tasks.ts b/backend/src/controllers/tasks.ts index 8d588ccc..e91bacb2 100644 --- a/backend/src/controllers/tasks.ts +++ b/backend/src/controllers/tasks.ts @@ -9,263 +9,265 @@ import { RouteDeliveryEntity } from '../entities/RouteDeliveryEntity'; // Get day of week to get notification by const getLastMonday = () => { - const currentDate: Date = new Date(); + const currentDate: Date = new Date(); - const currentDay: number = currentDate.getDay(); + const currentDay: number = currentDate.getDay(); - const daysUntilMonday: number = currentDay === 0 ? 6 : currentDay - 1; + const daysUntilMonday: number = currentDay === 0 ? 6 : currentDay - 1; - const mostRecentMonday: Date = new Date(currentDate); - mostRecentMonday.setDate(currentDate.getDate() - daysUntilMonday); + const mostRecentMonday: Date = new Date(currentDate); + mostRecentMonday.setDate(currentDate.getDate() - daysUntilMonday); - const formattedMostRecentMonday: string = mostRecentMonday - .toISOString() - .split('T')[0]; + const formattedMostRecentMonday: string = mostRecentMonday + .toISOString() + .split('T')[0]; - return formattedMostRecentMonday; + return formattedMostRecentMonday; }; 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 + 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 }); } - }); - response.status(StatusCode.OK).json({ task: savedTask }); - } - }; - - deleteTask = async (request: Request, response: Response) => { - const taskDeleted = await this.TaskRepository.delete({ - id: parseInt(request.params.id) - }); - response.status(StatusCode.OK).json({}); - }; - - getTaskMatchData = async (request: Request, response: Response) => { - const volunteers = await this.VolunteerRepository.find({ - select: { - id: true, - name: true, - availabilities: true, - availabilitiesLastUpdated: true - } - }); - - const comparisonDate = getLastMonday(); - - const availableVolunteers = volunteers.filter((v) => { - if (v.availabilitiesLastUpdated >= new Date(comparisonDate)) { - return v; - } - }); - - 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 (key != 0) { - if (!groups[key]) { - groups[key] = []; + }; + + deleteTask = async (request: Request, response: Response) => { + const taskDeleted = await this.TaskRepository.delete({ + id: parseInt(request.params.id) + }); + response.status(StatusCode.OK).json({}); + }; + + getTaskMatchData = async (request: Request, response: Response) => { + const volunteers = await this.VolunteerRepository.find({ + select: { + id: true, + name: true, + availabilities: true, + availabilitiesLastUpdated: true + } + }); + + const comparisonDate = getLastMonday(); + + const availableVolunteers = volunteers.filter((v) => { + if (v.availabilitiesLastUpdated >= new Date(comparisonDate)) { + return v; + } + }); + + 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 (key != 0) { + 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 + ); } - 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({ availableVolunteers: availableVolunteers, routes: groups }); - }; - - saveTaskMatch = async (request: Request, response: Response) => { - const taskMatches = request.body.matches; - - for (let i = 0; i < taskMatches.length; i++) { - const match = taskMatches[i]; - - // CREATE TASK - const newTask = new TaskEntity(); - newTask.deliveries = []; - newTask.isCompleted = false; - newTask.volunteer = await this.VolunteerRepository.findOne({ - where: { id: match.volunteerId } - }); - - const savedTask = await this.TaskRepository.save(newTask); - - // CREATE MEAL DELIVERY OBJECTS - const routeObjs = await this.RouteDeliveryRepository.find({ - where: { - routeNumber: match.routeName - }, - relations: { - client: true + + response + .status(StatusCode.OK) + .json({ availableVolunteers: availableVolunteers, routes: groups }); + }; + + saveTaskMatch = async (request: Request, response: Response) => { + const taskMatches = request.body.matches; + + for (let i = 0; i < taskMatches.length; i++) { + const match = taskMatches[i]; + + // CREATE TASK + const newTask = new TaskEntity(); + newTask.deliveries = []; + newTask.isCompleted = false; + newTask.volunteer = await this.VolunteerRepository.findOne({ + where: { id: match.volunteerId } + }); + + const savedTask = await this.TaskRepository.save(newTask); + + // CREATE MEAL DELIVERY OBJECTS + const routeObjs = await this.RouteDeliveryRepository.find({ + where: { + routeNumber: match.routeName + }, + relations: { + client: true + } + }); + + for (let i = 0; i < routeObjs.length; i++) { + const routeObj = routeObjs[i]; + const newMeal = new MealDeliveryEntity(); + newMeal.mealType = routeObj.mealType; + newMeal.task = savedTask; + newMeal.routePosition = routeObj.routePosition; + newMeal.program = routeObj.program; + const foundClient = await this.ClientRepository.findOne({ + where: { id: routeObj.client.id } + }); + newMeal.client = foundClient; + const savedDelivery = await this.MealDeliveryRepository.save( + newMeal + ); + newTask.deliveries.push(savedDelivery); + } + + await this.TaskRepository.save(newTask); } - }); - - for (let i = 0; i < routeObjs.length; i++) { - const routeObj = routeObjs[i]; - const newMeal = new MealDeliveryEntity(); - newMeal.mealType = routeObj.mealType; - newMeal.task = savedTask; - newMeal.routePosition = routeObj.routePosition; - newMeal.program = routeObj.program; - const foundClient = await this.ClientRepository.findOne({ - where: { id: routeObj.client.id } - }); - newMeal.client = foundClient; - const savedDelivery = await this.MealDeliveryRepository.save(newMeal); - newTask.deliveries.push(savedDelivery); - } - - await this.TaskRepository.save(newTask); - } - response.status(StatusCode.OK).json({}); - }; + response.status(StatusCode.OK).json({}); + }; } diff --git a/backend/src/controllers/volunteers.ts b/backend/src/controllers/volunteers.ts index e3ece26f..8a9b49f6 100644 --- a/backend/src/controllers/volunteers.ts +++ b/backend/src/controllers/volunteers.ts @@ -8,109 +8,110 @@ import * as jwt from 'jsonwebtoken'; import { getNeighbourhoodFromString } from '../entities/types'; export default class VolunteerController { - private static VolunteerRepository = - AppDataSource.getRepository(VolunteerEntity); + private static VolunteerRepository = + AppDataSource.getRepository(VolunteerEntity); - static getVolunteers = async (request: Request, response: Response) => { - const volunteers = await this.VolunteerRepository.find({ - relations: { - tasks: true - }, - where: { - softDelete: false - } - }); - response.status(StatusCode.OK).json({ volunteers: volunteers }); - }; + static getVolunteers = async (request: Request, response: Response) => { + const volunteers = await this.VolunteerRepository.find({ + relations: { + tasks: true + }, + where: { + softDelete: false + } + }); + response.status(StatusCode.OK).json({ volunteers: volunteers }); + }; - static getVolunteer = async (request: Request, response: Response) => { - const volunteer = await this.VolunteerRepository.findOne({ - where: { - id: parseInt(request.params.id), - softDelete: false - }, - relations: { - tasks: true - } - }); - response.status(StatusCode.OK).json({ volunteer: volunteer }); - }; + static getVolunteer = async (request: Request, response: Response) => { + const volunteer = await this.VolunteerRepository.findOne({ + where: { + id: parseInt(request.params.id), + softDelete: false + }, + relations: { + tasks: true + } + }); + response.status(StatusCode.OK).json({ volunteer: volunteer }); + }; - static removeVolunteer = async (request: Request, response: Response) => { - await this.VolunteerRepository.delete({ - id: parseInt(request.params.id) - }); - response.status(StatusCode.OK).json({}); - }; + static removeVolunteer = async (request: Request, response: Response) => { + await this.VolunteerRepository.delete({ + id: parseInt(request.params.id) + }); + response.status(StatusCode.OK).json({}); + }; - static createVolunteer = async (request: Request, response: Response) => { - const volunteer = await this.VolunteerRepository.save({ - 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, - preferredNeighbourhoods: request.body.preferredNeighbourhoods - ? request.body.preferredNeighbourhoods.map((neighborhood) => - getNeighbourhoodFromString(neighborhood) - ) - : [], - softDelete: false - }); - response.status(StatusCode.OK).json({ volunteer }); - }; + static createVolunteer = async (request: Request, response: Response) => { + const volunteer = await this.VolunteerRepository.save({ + 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, + preferredNeighbourhoods: request.body.preferredNeighbourhoods + ? request.body.preferredNeighbourhoods.map((neighborhood) => + getNeighbourhoodFromString(neighborhood) + ) + : [], + softDelete: false + }); + response.status(StatusCode.OK).json({ volunteer }); + }; - static 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 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 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 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 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 }); - }; + static 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 }); + }; - static 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 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 } + }); - 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 && (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' }); + }; } diff --git a/backend/src/data-source.ts b/backend/src/data-source.ts index 85e9fa89..430272f7 100644 --- a/backend/src/data-source.ts +++ b/backend/src/data-source.ts @@ -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, + RouteDeliveryEntity + ], + 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 77b4c5b0..3064394c 100644 --- a/backend/src/entities/ClientEntity.ts +++ b/backend/src/entities/ClientEntity.ts @@ -4,33 +4,33 @@ import { MealType, Neighbourhood } from './types'; @Entity() export class ClientEntity { - @PrimaryGeneratedColumn() - id: number; + @PrimaryGeneratedColumn() + id: number; - @Column() - name: string; + @Column() + name: string; - @Column({ - unique: true, - nullable: false - }) - email: string; + @Column({ + unique: true, + nullable: false + }) + email: string; - @Column() - phoneNumber: string; + @Column() + phoneNumber: string; - @Column() - address: string; + @Column() + address: string; - @Column() - mealType: MealType; + @Column() + mealType: MealType; - @Column() - sts: boolean; + @Column() + sts: boolean; - @Column() - map: boolean; + @Column() + map: boolean; - @Column() - softDelete: boolean = false; + @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 index db8159ce..81213082 100644 --- a/backend/src/entities/RouteDeliveryEntity.ts +++ b/backend/src/entities/RouteDeliveryEntity.ts @@ -4,21 +4,21 @@ import { ProgramType, MealType } from './types'; @Entity() export class RouteDeliveryEntity { - @PrimaryGeneratedColumn('uuid') - id: string; + @PrimaryGeneratedColumn('uuid') + id: string; - @Column() - routeNumber: number; + @Column() + routeNumber: number; - @Column() - routePosition: number; + @Column() + routePosition: number; - @Column() - mealType: MealType; + @Column() + mealType: MealType; - @Column() - program: ProgramType; + @Column() + program: ProgramType; - @ManyToOne(() => ClientEntity) - client: ClientEntity; + @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..61088163 100644 --- a/backend/src/entities/UserEntity.ts +++ b/backend/src/entities/UserEntity.ts @@ -3,22 +3,22 @@ import { RouteDeliveryEntity } from './RouteDeliveryEntity'; @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 248b3031..0e65fe63 100644 --- a/backend/src/entities/VolunteerEntity.ts +++ b/backend/src/entities/VolunteerEntity.ts @@ -1,88 +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() + profilePicture: string; - @Column({ nullable: true }) - availabilities: string; + @Column({ nullable: true }) + availabilities: string; - @Column({ nullable: true }) - 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; + @Column() + softDelete: boolean = false; } diff --git a/backend/src/entities/types.ts b/backend/src/entities/types.ts index 602a073c..a211932d 100644 --- a/backend/src/entities/types.ts +++ b/backend/src/entities/types.ts @@ -1,44 +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 + [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]; + 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/mealDelivery.routes.ts b/backend/src/routes/mealDelivery.routes.ts index 5fbb7679..3c04c26e 100644 --- a/backend/src/routes/mealDelivery.routes.ts +++ b/backend/src/routes/mealDelivery.routes.ts @@ -9,11 +9,11 @@ const mealDeliveryController = new MealDeliveryController(); router.get('/meal_delivery/', mealDeliveryController.getMealDeliveries); router.get('/meal_delivery/:id', mealDeliveryController.getMealDelivery); router.put( - '/meal_delivery/:id', - mealDeliveryController.updateOrCreateMealDelivery + '/meal_delivery/:id', + mealDeliveryController.updateOrCreateMealDelivery ); router.put( - '/meal_delivery/', - mealDeliveryController.updateOrCreateMealDelivery + '/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 index 70fc234e..617ca24a 100644 --- a/backend/src/routes/routeDelivery.routes.ts +++ b/backend/src/routes/routeDelivery.routes.ts @@ -9,15 +9,15 @@ router.get('/route_delivery/', routeDeliveryController.getRouteDeliveries); router.post('/route_delivery/', routeDeliveryController.saveAllRouteDeliveries); // TODO: review this? v router.get( - '/route_delivery_simple/', - routeDeliveryController.getRouteDeliveriesSimple + '/route_delivery_simple/', + routeDeliveryController.getRouteDeliveriesSimple ); router.put('/route_delivery/:id/set', routeDeliveryController.setRouteNumber); router.put( - '/route_delivery/:id/increment', - routeDeliveryController.incrementRoutePosition + '/route_delivery/:id/increment', + routeDeliveryController.incrementRoutePosition ); router.put( - '/route_delivery/:id/decrement', - routeDeliveryController.decrementRoutePosition + '/route_delivery/:id/decrement', + routeDeliveryController.decrementRoutePosition ); diff --git a/backend/src/scripts/drop.ts b/backend/src/scripts/drop.ts index ae2cc0ee..7528d0c7 100644 --- a/backend/src/scripts/drop.ts +++ b/backend/src/scripts/drop.ts @@ -1,10 +1,10 @@ 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'); + console.log('Begin initalizing data source'); + 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..c175419a 100644 --- a/backend/src/scripts/seed.ts +++ b/backend/src/scripts/seed.ts @@ -7,19 +7,19 @@ import TaskSeeder from './seeders/task.seeder'; import { AppDataSource } from '../data-source'; export const seed = async () => { - console.log('Begin initalizing data source'); - await AppDataSource.initialize(); + 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 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'); }; seed(); diff --git a/backend/src/scripts/seeders/admin.seeder.ts b/backend/src/scripts/seeders/admin.seeder.ts index 540aa680..3267f759 100644 --- a/backend/src/scripts/seeders/admin.seeder.ts +++ b/backend/src/scripts/seeders/admin.seeder.ts @@ -7,26 +7,26 @@ import { generateStaffUser } from './user'; require('dotenv').config(); const DEFAULT_ADMIN = { - password: process.env.ADMIN_PASSWORD, - email: process.env.ADMIN_EMAIL, - jobTitle: 'Administrator' + 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; + 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); - } + 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 index f4bde6f3..7be30b07 100644 --- a/backend/src/scripts/seeders/client.seeder.ts +++ b/backend/src/scripts/seeders/client.seeder.ts @@ -8,80 +8,80 @@ import { ProgramType } from '../../entities/types'; import { generateStaffUser } from './user'; const generateRouteDelivery = async ( - program: ProgramType, - client: ClientEntity + 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 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 firstName = faker.name.firstName(); - const lastName = faker.name.lastName(); + 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(); + 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; + 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; + 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 repository = dataSource.getRepository(ClientEntity); - const clients = await generateClients(10); - await repository.insert(clients); + 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); + 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.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); - } - }); - } + 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 index 0bc88460..c8fbf6af 100644 --- a/backend/src/scripts/seeders/task.seeder.ts +++ b/backend/src/scripts/seeders/task.seeder.ts @@ -8,74 +8,77 @@ 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; - } + 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; + 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 + 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 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 clientRepository = dataSource.getRepository(ClientEntity); + const mealRepository = dataSource.getRepository(MealDeliveryEntity); - const clients = await clientRepository.find(); + 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); - } + 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; + 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); - }); - } + 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 index 202c1301..827707b1 100644 --- a/backend/src/scripts/seeders/user.ts +++ b/backend/src/scripts/seeders/user.ts @@ -5,33 +5,33 @@ 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(); + const firstName = first || faker.name.firstName(); + const lastName = last || faker.name.lastName(); - // random avalability - everyone will have 2 avalabilities + // 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() - }; + 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; + return user; }; export const generateStaffUser = async () => { - const firstName = faker.name.firstName(); - const lastName = faker.name.lastName(); + const firstName = faker.name.firstName(); + const lastName = faker.name.lastName(); - const user = generateUser(firstName, lastName) as any; + 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; + 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; + return user; }; diff --git a/backend/src/scripts/seeders/volunteer.seeder.ts b/backend/src/scripts/seeders/volunteer.seeder.ts index ff83d3d3..5279e575 100644 --- a/backend/src/scripts/seeders/volunteer.seeder.ts +++ b/backend/src/scripts/seeders/volunteer.seeder.ts @@ -1,11 +1,11 @@ import { Seeder, SeederFactoryManager } from 'typeorm-extension'; import { DataSource } from 'typeorm'; import { - VolunteerEntity, - indexedDayOfWeek, - indexedTimeSlots, - TimeSlots, - Availabilities + VolunteerEntity, + indexedDayOfWeek, + indexedTimeSlots, + TimeSlots, + Availabilities } from '../../entities/VolunteerEntity'; import { faker } from '@faker-js/faker'; import { generateTask } from './task.seeder'; @@ -13,75 +13,75 @@ import { generateStaffUser } from './user'; import * as bcrypt from 'bcryptjs'; const DEFAULT_VOLUNTEER = { - password: process.env.TEST_VOLUNTEER_PASSWORD, - email: process.env.TEST_VOLUNTEER_EMAIL + 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); - volunteer.softDelete = false; - return volunteer; + 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); + volunteer.softDelete = false; + 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']; - volunteer.softDelete = false; - return volunteer; + 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']; + volunteer.softDelete = false; + 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 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 volunteers: VolunteerEntity[] = []; + for (let i = 0; i < num; i++) { + const v = await generateVolunteer(); + volunteers.push(v); + } + return volunteers; }; const generateTasks = ( - dataSource: DataSource, - volunteers: VolunteerEntity[] + 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); - } - }); + 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); - } + 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/clients.ts b/backend/src/services/clients.ts index 2ac50c11..c9e9fbcb 100644 --- a/backend/src/services/clients.ts +++ b/backend/src/services/clients.ts @@ -4,7 +4,8 @@ import { Client, CreateClientProps, UpdateClientProps } from '../types/clients'; import StopService from './stop'; export default class ClientService { - static readonly ClientRepository = AppDataSource.getRepository(ClientEntity); + static readonly ClientRepository = + AppDataSource.getRepository(ClientEntity); static getActiveClients = async (): Promise => { const clients = await this.ClientRepository.find({ @@ -13,13 +14,13 @@ export default class ClientService { } }); - return clients - } + return clients; + }; static getAllClients = async (): Promise => { const clients = await this.ClientRepository.find({}); - return clients - } + return clients; + }; static getClient = async (id: number) => { const client = await this.ClientRepository.findOne({ @@ -28,10 +29,10 @@ export default class ClientService { } }); - return client - } + return client; + }; - static createClient = async (props: CreateClientProps): Promise => { + static createClient = async (props: CreateClientProps): Promise => { const client = await this.ClientRepository.create({ name: props.name, email: props.email, @@ -40,15 +41,18 @@ export default class ClientService { mealType: props.mealType, sts: props.sts, map: props.map - }) + }); - await this.ClientRepository.save(client) - await StopService.createStopsFromClient(client) + await this.ClientRepository.save(client); + await StopService.createStopsFromClient(client); - return client - } + return client; + }; - static updateClient = async (id: number, updateProps: UpdateClientProps): Promise => { + static updateClient = async ( + id: number, + updateProps: UpdateClientProps + ): Promise => { const client = await this.ClientRepository.findOne({ where: { id: id @@ -56,11 +60,8 @@ export default class ClientService { }); if (!client) return null; - - await this.ClientRepository.update( - { id: id }, - updateProps - ) + + await this.ClientRepository.update({ id: id }, updateProps); const updatedClient = await this.ClientRepository.findOne({ where: { @@ -68,8 +69,8 @@ export default class ClientService { } }); - await StopService.updateStopsFromClient(client, updatedClient) + await StopService.updateStopsFromClient(client, updatedClient); - return updatedClient - } + return updatedClient; + }; } diff --git a/backend/src/services/stop.ts b/backend/src/services/stop.ts index 65c31159..894b0bda 100644 --- a/backend/src/services/stop.ts +++ b/backend/src/services/stop.ts @@ -1,36 +1,44 @@ import { AppDataSource } from '../data-source'; -import { Client, UpdateClientProps } from "../types/clients" +import { Client, UpdateClientProps } from '../types/clients'; import { RouteDeliveryEntity } from '../entities/RouteDeliveryEntity'; import { ProgramType } from '../types/enums'; -import {Routes} from '../types/stop'; +import { Routes } from '../types/stop'; export default class StopService { - static readonly StopRepository = AppDataSource.getRepository(RouteDeliveryEntity); + static readonly StopRepository = + AppDataSource.getRepository(RouteDeliveryEntity); static createStopsFromClient = async (client: Client) => { if (client.sts) { - const stopSTS = await this.createDefaultStopFromClient(client) + const stopSTS = await this.createDefaultStopFromClient(client); stopSTS.program = ProgramType.STS; await this.StopRepository.save(stopSTS); } if (client.map) { - const stopMAP = await this.createDefaultStopFromClient(client) + 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); + 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) + const stopSTS = await this.createDefaultStopFromClient( + updatedClient + ); stopSTS.program = ProgramType.STS; await this.StopRepository.save(stopSTS); - // deleted STS + // deleted STS } else if (updatedSTS && !updatedClient.sts) { const stopSTS = await this.StopRepository.find({ relations: { client: true }, @@ -39,17 +47,19 @@ export default class StopService { program: ProgramType.STS } }); - + await this.StopRepository.remove(stopSTS); } // created MAP if (updatedMAP && updatedClient.map) { - const stopMAP = await this.createDefaultStopFromClient(updatedClient) + const stopMAP = await this.createDefaultStopFromClient( + updatedClient + ); stopMAP.program = ProgramType.STS; await this.StopRepository.save(stopMAP); - - // deleted MAP + + // deleted MAP } else if (updatedMAP && !updatedClient.map) { const stopMAP = await this.StopRepository.find({ relations: { client: true }, @@ -66,23 +76,23 @@ export default class StopService { if (updatedMealType) { const stops = await this.StopRepository.find({ relations: { client: true }, - where: {client: {id: prevClient.id}} - }) + where: { client: { id: prevClient.id } } + }); stops.forEach((stop) => { - stop.mealType = updatedClient.mealType - }) + stop.mealType = updatedClient.mealType; + }); - await this.StopRepository.save(stops) + await this.StopRepository.save(stops); } - } + }; static getRoutes = async (): Promise => { const stops = await this.StopRepository.find({ relations: { client: true } - }) + }); const routes = stops.reduce((routes, stop) => { const key = stop.routeNumber; @@ -91,7 +101,7 @@ export default class StopService { } routes[key].push(stop); return routes; - }, {}) + }, {}); for (const routeNumber in routes) { routes[routeNumber].sort( @@ -99,15 +109,15 @@ export default class StopService { ); } - return routes - } - + return routes; + }; + private static createDefaultStopFromClient = async (client: Client) => { const stop = new RouteDeliveryEntity(); stop.routeNumber = 0; stop.routePosition = 0; stop.client = client; stop.mealType = client.mealType; - return stop - } -} \ No newline at end of file + return stop; + }; +} diff --git a/backend/src/types/clients.ts b/backend/src/types/clients.ts index fd08600b..ccd12476 100644 --- a/backend/src/types/clients.ts +++ b/backend/src/types/clients.ts @@ -1,7 +1,7 @@ -import { MealType } from "./enums"; +import { MealType } from './enums'; export type Client = { - id: number + id: number; name: string; email: string; phoneNumber: string; @@ -10,7 +10,7 @@ export type Client = { sts: boolean; map: boolean; softDelete: boolean; -} +}; -export type CreateClientProps = Omit -export type UpdateClientProps = Partial \ No newline at end of file +export type CreateClientProps = Omit; +export type UpdateClientProps = Partial; diff --git a/backend/src/types/enums.ts b/backend/src/types/enums.ts index 22c3ef26..eeb4b70c 100644 --- a/backend/src/types/enums.ts +++ b/backend/src/types/enums.ts @@ -1,12 +1,11 @@ export enum ProgramType { MAP = 'MAP', STS = 'STS' -} - +} + export enum MealType { VEGETARIAN = 'Vegetarian', NOFISH = 'No Fish', NOMEAT = 'No Meat', REGULAR = 'Regular' } - \ No newline at end of file diff --git a/backend/src/types/stop.ts b/backend/src/types/stop.ts index 2e08a625..ade3ab69 100644 --- a/backend/src/types/stop.ts +++ b/backend/src/types/stop.ts @@ -1,5 +1,5 @@ -import { Client } from "./clients"; -import { MealType, ProgramType } from "./enums"; +import { Client } from './clients'; +import { MealType, ProgramType } from './enums'; export type Stop = { routeNumber: number; @@ -7,8 +7,8 @@ export type Stop = { client: Client; mealType: MealType; program: ProgramType; -} +}; export type Routes = { [key: number]: Stop[]; -} \ No newline at end of file +}; From be3747a3ae640f2dcf60b8ffb294d6d294ac4a7d Mon Sep 17 00:00:00 2001 From: Sophearah Suy-Puth Date: Wed, 15 May 2024 13:54:11 +0700 Subject: [PATCH 50/67] 1) routedelivery id is number, 2) remove methods to individually set route number, 3) service/test saveRoutes() --- backend/src/controllers/routeDelivery.ts | 63 ---------- backend/src/entities/RouteDeliveryEntity.ts | 4 +- backend/src/routes/routeDelivery.routes.ts | 9 -- backend/src/services/stop.ts | 36 +++++- backend/src/types/stop.ts | 4 + backend/tests/stops/service.test.ts | 128 ++++++++++++++++++++ 6 files changed, 164 insertions(+), 80 deletions(-) diff --git a/backend/src/controllers/routeDelivery.ts b/backend/src/controllers/routeDelivery.ts index 7f9445bf..943296a3 100644 --- a/backend/src/controllers/routeDelivery.ts +++ b/backend/src/controllers/routeDelivery.ts @@ -46,71 +46,8 @@ export default class RouteDeliveryController { response.status(StatusCode.OK).json({ routes: groups }); }; - setRouteNumber = async (request: Request, response: Response) => { - const routePosition = - request.body.routeNumber == 0 - ? 0 - : await this.getNextRouteNumber(request.body.routeNumber); - - const route = await this.RouteDeliveryRepository.update( - { id: request.params.id }, - { - routeNumber: request.body.routeNumber, - routePosition: routePosition - } - ); - - response.status(StatusCode.OK).json({ route }); - }; - - incrementRoutePosition = async (request: Request, response: Response) => { - const r = await this.getRouteFromId(request.params.id); - - await this.RouteDeliveryRepository.decrement( - { routeNumber: r.routeNumber, routePosition: r.routePosition + 1 }, - 'routePosition', - 1 - ); - - const route = await this.RouteDeliveryRepository.increment( - { id: request.params.id }, - 'routePosition', - 1 - ); - - response.status(StatusCode.OK).json({ route }); - }; - - decrementRoutePosition = async (request: Request, response: Response) => { - const r = await this.getRouteFromId(request.params.id); - - await this.RouteDeliveryRepository.increment( - { routeNumber: r.routeNumber, routePosition: r.routePosition - 1 }, - 'routePosition', - 1 - ); - - const route = await this.RouteDeliveryRepository.decrement( - { id: request.params.id }, - 'routePosition', - 1 - ); - - response.status(StatusCode.OK).json({ route }); - }; - // Helper Functions - getRouteFromId = async (id: string) => { - const route = await this.RouteDeliveryRepository.findOne({ - where: { - id: id - } - }); - - return route; - }; - // Get the next route number getNextRouteNumber = async (routeNumber) => { const routes = await this.RouteDeliveryRepository.find({ diff --git a/backend/src/entities/RouteDeliveryEntity.ts b/backend/src/entities/RouteDeliveryEntity.ts index 81213082..bb6a7793 100644 --- a/backend/src/entities/RouteDeliveryEntity.ts +++ b/backend/src/entities/RouteDeliveryEntity.ts @@ -4,8 +4,8 @@ import { ProgramType, MealType } from './types'; @Entity() export class RouteDeliveryEntity { - @PrimaryGeneratedColumn('uuid') - id: string; + @PrimaryGeneratedColumn() + id: number; @Column() routeNumber: number; diff --git a/backend/src/routes/routeDelivery.routes.ts b/backend/src/routes/routeDelivery.routes.ts index 617ca24a..ea9e4d94 100644 --- a/backend/src/routes/routeDelivery.routes.ts +++ b/backend/src/routes/routeDelivery.routes.ts @@ -12,12 +12,3 @@ router.get( '/route_delivery_simple/', routeDeliveryController.getRouteDeliveriesSimple ); -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/services/stop.ts b/backend/src/services/stop.ts index 894b0bda..bc6a9e55 100644 --- a/backend/src/services/stop.ts +++ b/backend/src/services/stop.ts @@ -2,7 +2,7 @@ import { AppDataSource } from '../data-source'; import { Client, UpdateClientProps } from '../types/clients'; import { RouteDeliveryEntity } from '../entities/RouteDeliveryEntity'; import { ProgramType } from '../types/enums'; -import { Routes } from '../types/stop'; +import { Routes, SaveRouteProps } from '../types/stop'; export default class StopService { static readonly StopRepository = @@ -88,11 +88,7 @@ export default class StopService { }; static getRoutes = async (): Promise => { - const stops = await this.StopRepository.find({ - relations: { - client: true - } - }); + const stops = await this.StopRepository.find({}); const routes = stops.reduce((routes, stop) => { const key = stop.routeNumber; @@ -109,9 +105,37 @@ export default class StopService { ); } + 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 + } + private static createDefaultStopFromClient = async (client: Client) => { const stop = new RouteDeliveryEntity(); stop.routeNumber = 0; diff --git a/backend/src/types/stop.ts b/backend/src/types/stop.ts index ade3ab69..00c42131 100644 --- a/backend/src/types/stop.ts +++ b/backend/src/types/stop.ts @@ -12,3 +12,7 @@ export type Stop = { export type Routes = { [key: number]: Stop[]; }; + +export type SaveRouteProps = { + [key: number]: number[]; +} \ No newline at end of file diff --git a/backend/tests/stops/service.test.ts b/backend/tests/stops/service.test.ts index 8aa2c2bb..a3db1c8c 100644 --- a/backend/tests/stops/service.test.ts +++ b/backend/tests/stops/service.test.ts @@ -113,5 +113,133 @@ describe('Stop Service Unit Tests', () => { 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(); + }) + }) }); From 22262de3d9b8d9ebbf0161a956fe3c1d630f926d Mon Sep 17 00:00:00 2001 From: Sophearah Suy-Puth Date: Wed, 15 May 2024 14:53:34 +0700 Subject: [PATCH 51/67] 1) rename routeDelivery to stop, 2) delete routeDelivery tests, 3) setup stop controller --- backend/src/controllers/routeDelivery.ts | 78 ------------------- backend/src/controllers/stops.ts | 20 +++++ backend/src/controllers/tasks.ts | 2 +- backend/src/data-source.ts | 4 +- .../{RouteDeliveryEntity.ts => StopEntity.ts} | 2 +- backend/src/entities/UserEntity.ts | 2 +- backend/src/routes/index.ts | 4 +- backend/src/routes/routeDelivery.routes.ts | 14 ---- backend/src/routes/stop.routes.ts | 9 +++ backend/src/scripts/seeders/client.seeder.ts | 2 +- backend/src/services/stop.ts | 4 +- backend/tests/client/utils.ts | 8 +- .../tests/routeDelivery/routeDelivery.test.ts | 35 --------- .../routeDelivery/routeDelivery.utils.ts | 19 ----- backend/tests/stops/utils.ts | 6 +- 15 files changed, 46 insertions(+), 163 deletions(-) delete mode 100644 backend/src/controllers/routeDelivery.ts create mode 100644 backend/src/controllers/stops.ts rename backend/src/entities/{RouteDeliveryEntity.ts => StopEntity.ts} (92%) delete mode 100644 backend/src/routes/routeDelivery.routes.ts create mode 100644 backend/src/routes/stop.routes.ts delete mode 100644 backend/tests/routeDelivery/routeDelivery.test.ts delete mode 100644 backend/tests/routeDelivery/routeDelivery.utils.ts diff --git a/backend/src/controllers/routeDelivery.ts b/backend/src/controllers/routeDelivery.ts deleted file mode 100644 index 943296a3..00000000 --- a/backend/src/controllers/routeDelivery.ts +++ /dev/null @@ -1,78 +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); - - // TODO: review this? v - getRouteDeliveriesSimple = async (request: Request, response: Response) => { - const routes = await this.RouteDeliveryRepository.find({ - relations: { - client: true - } - }); - - response.status(StatusCode.OK).json({ routes: routes }); - }; - - 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 }); - }; - - // Helper Functions - - // Get the next route number - getNextRouteNumber = async (routeNumber) => { - const routes = await this.RouteDeliveryRepository.find({ - where: { - routeNumber: routeNumber - } - }); - - return routes.length || 0; - }; - - saveAllRouteDeliveries = async (request: Request, response: Response) => { - const routes = request.body.routes; - - // for each column - Object.entries(routes).map(([column, data]) => { - // for each stop - routes[column].map(async (stop, index) => { - const updatedStop = await this.RouteDeliveryRepository.update( - { id: stop.id }, - { routeNumber: parseInt(column), routePosition: index } - ); - }); - }); - - response.status(StatusCode.OK).json({ routes: 'fd' }); - }; -} diff --git a/backend/src/controllers/stops.ts b/backend/src/controllers/stops.ts new file mode 100644 index 00000000..2ee9469d --- /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 { + getRoutes = async (request: Request, response: Response) => { + const routes = await StopService.getRoutes() + response.status(StatusCode.OK).json({ routes: routes }); + } + + 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 e91bacb2..7b2a7bb9 100644 --- a/backend/src/controllers/tasks.ts +++ b/backend/src/controllers/tasks.ts @@ -5,7 +5,7 @@ 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 { StopEntity as RouteDeliveryEntity } from '../entities/StopEntity'; // Get day of week to get notification by const getLastMonday = () => { diff --git a/backend/src/data-source.ts b/backend/src/data-source.ts index 430272f7..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'; @@ -28,7 +28,7 @@ export const AppDataSource = new DataSource({ UserEntity, VolunteerEntity, ClientEntity, - RouteDeliveryEntity + StopEntity ], migrations: [], subscribers: [], diff --git a/backend/src/entities/RouteDeliveryEntity.ts b/backend/src/entities/StopEntity.ts similarity index 92% rename from backend/src/entities/RouteDeliveryEntity.ts rename to backend/src/entities/StopEntity.ts index bb6a7793..d4bca64f 100644 --- a/backend/src/entities/RouteDeliveryEntity.ts +++ b/backend/src/entities/StopEntity.ts @@ -3,7 +3,7 @@ import { ClientEntity } from './ClientEntity'; import { ProgramType, MealType } from './types'; @Entity() -export class RouteDeliveryEntity { +export class StopEntity { @PrimaryGeneratedColumn() id: number; diff --git a/backend/src/entities/UserEntity.ts b/backend/src/entities/UserEntity.ts index 61088163..13ab6a7c 100644 --- a/backend/src/entities/UserEntity.ts +++ b/backend/src/entities/UserEntity.ts @@ -1,5 +1,5 @@ import { Column, OneToMany, Entity, PrimaryGeneratedColumn } from 'typeorm'; -import { RouteDeliveryEntity } from './RouteDeliveryEntity'; +import { StopEntity } from './StopEntity'; @Entity() export abstract class UserEntity { diff --git a/backend/src/routes/index.ts b/backend/src/routes/index.ts index 568f8552..e42f907b 100644 --- a/backend/src/routes/index.ts +++ b/backend/src/routes/index.ts @@ -1,7 +1,7 @@ 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'; @@ -14,7 +14,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); diff --git a/backend/src/routes/routeDelivery.routes.ts b/backend/src/routes/routeDelivery.routes.ts deleted file mode 100644 index ea9e4d94..00000000 --- a/backend/src/routes/routeDelivery.routes.ts +++ /dev/null @@ -1,14 +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.post('/route_delivery/', routeDeliveryController.saveAllRouteDeliveries); -// TODO: review this? v -router.get( - '/route_delivery_simple/', - routeDeliveryController.getRouteDeliveriesSimple -); diff --git a/backend/src/routes/stop.routes.ts b/backend/src/routes/stop.routes.ts new file mode 100644 index 00000000..9eb24c15 --- /dev/null +++ b/backend/src/routes/stop.routes.ts @@ -0,0 +1,9 @@ +import * as express from 'express'; +import StopController from '../controllers/stops'; + +// Create a router object +export const router = express.Router(); +const stopController = new StopController(); + +router.get('/stops/routes/', stopController.getRoutes); +router.post('/stops/routes/', stopController.saveRoutes); \ No newline at end of file diff --git a/backend/src/scripts/seeders/client.seeder.ts b/backend/src/scripts/seeders/client.seeder.ts index 7be30b07..27ece7d6 100644 --- a/backend/src/scripts/seeders/client.seeder.ts +++ b/backend/src/scripts/seeders/client.seeder.ts @@ -2,7 +2,7 @@ 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 { StopEntity as RouteDeliveryEntity } from '../../entities/StopEntity'; import { ProgramType } from '../../entities/types'; import { generateStaffUser } from './user'; diff --git a/backend/src/services/stop.ts b/backend/src/services/stop.ts index bc6a9e55..a2c97c95 100644 --- a/backend/src/services/stop.ts +++ b/backend/src/services/stop.ts @@ -1,12 +1,12 @@ import { AppDataSource } from '../data-source'; import { Client, UpdateClientProps } from '../types/clients'; -import { RouteDeliveryEntity } from '../entities/RouteDeliveryEntity'; +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(RouteDeliveryEntity); + AppDataSource.getRepository(StopEntity); static createStopsFromClient = async (client: Client) => { if (client.sts) { diff --git a/backend/tests/client/utils.ts b/backend/tests/client/utils.ts index 7e6f3670..f97c832d 100644 --- a/backend/tests/client/utils.ts +++ b/backend/tests/client/utils.ts @@ -2,7 +2,7 @@ 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 { RouteDeliveryEntity } from '../../src/entities/RouteDeliveryEntity'; +import { StopEntity } from '../../src/entities/StopEntity'; import { ProgramType } from '../../src/entities/types'; type CreateClientProps = { @@ -18,7 +18,7 @@ type CreateClientProps = { export default class ClientUtils { static ClientRepository = AppDataSource.getRepository(ClientEntity); - static StopRepository = AppDataSource.getRepository(RouteDeliveryEntity) + static StopRepository = AppDataSource.getRepository(StopEntity) static createClient = async (props: Partial = {}) => { const mergedProps: CreateClientProps = { @@ -46,7 +46,7 @@ export default class ClientUtils { const savedClient = await this.ClientRepository.save(newClient); if (savedClient.sts) { - const stop = new RouteDeliveryEntity(); + const stop = new StopEntity(); stop.routeNumber = 0; stop.routePosition = 0; stop.client = savedClient; @@ -56,7 +56,7 @@ export default class ClientUtils { } if (savedClient.map) { - const stop = new RouteDeliveryEntity(); + const stop = new StopEntity(); stop.routeNumber = 0; stop.routePosition = 0; stop.client = savedClient; diff --git a/backend/tests/routeDelivery/routeDelivery.test.ts b/backend/tests/routeDelivery/routeDelivery.test.ts deleted file mode 100644 index d1509853..00000000 --- a/backend/tests/routeDelivery/routeDelivery.test.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { describe, it } from '@jest/globals'; -import DataSourceHelper from '../data.utils'; -import app from '../../src/app'; -import * as request from 'supertest'; -import { StatusCode } from '../../src/controllers/statusCode'; - -describe('Route Delivery API 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('GET /api/route_delivery', () => { - it.skip('should return routes grouped', async () => { - // TODO: test here - }); - }) - - describe('POST /api/route_delivery', () => { - it.skip('should save routes in new group', async () => { - // TODO: test here - }); - }) -}) \ No newline at end of file diff --git a/backend/tests/routeDelivery/routeDelivery.utils.ts b/backend/tests/routeDelivery/routeDelivery.utils.ts deleted file mode 100644 index fb68b3f8..00000000 --- a/backend/tests/routeDelivery/routeDelivery.utils.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { RouteDeliveryEntity } from '../../src/entities/RouteDeliveryEntity'; -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 RouteDeliveryEntityHelper { - RouteDeliveryRepository: Repository; - - constructor(repository: Repository) { - this.RouteDeliveryRepository = repository; - } - - routeMealDelivery = async () => { - const newRouteDelivery = new RouteDeliveryEntity(); - // TODO: setup routes - return await this.RouteDeliveryRepository.save(newRouteDelivery); - }; -} diff --git a/backend/tests/stops/utils.ts b/backend/tests/stops/utils.ts index a8fa17ad..f7c1a4db 100644 --- a/backend/tests/stops/utils.ts +++ b/backend/tests/stops/utils.ts @@ -1,7 +1,7 @@ import { AppDataSource } from '../../src/data-source'; import { ClientEntity } from '../../src/entities/ClientEntity'; import { MealType, ProgramType } from '../../src/types/enums'; -import { RouteDeliveryEntity } from '../../src/entities/RouteDeliveryEntity'; +import { StopEntity } from '../../src/entities/StopEntity'; type CreateStopProps = { mealType: MealType, @@ -14,7 +14,7 @@ export type CreateRoutesProps = { export default class StopUtils { static ClientRepository = AppDataSource.getRepository(ClientEntity); - static StopRepository = AppDataSource.getRepository(RouteDeliveryEntity) + static StopRepository = AppDataSource.getRepository(StopEntity) static setupRoutes = async (props: CreateRoutesProps) => { for (const [column, data] of Object.entries(props)) { @@ -39,7 +39,7 @@ export default class StopUtils { const savedClient = await this.ClientRepository.save(newClient); - const stop = new RouteDeliveryEntity(); + const stop = new StopEntity(); stop.routeNumber = parseInt(column); stop.routePosition = 0; if (parseInt(column) != 0){ From db6c07f25a835a94cf4ceb1388ac035d5319e6fe Mon Sep 17 00:00:00 2001 From: Sophearah Suy-Puth Date: Wed, 15 May 2024 15:38:59 +0700 Subject: [PATCH 52/67] 1) service/test admin login(), 2) admin utils for testing, 3) admin controller setup --- backend/src/controllers/admin.ts | 46 ++++++++++-------------- backend/src/services/admin.ts | 37 +++++++++++++++++++ backend/src/services/stop.ts | 2 +- backend/tests/admin/service.test.ts | 56 +++++++++++++++++++++++++++++ backend/tests/admin/utils.ts | 35 ++++++++++++++++++ 5 files changed, 147 insertions(+), 29 deletions(-) create mode 100644 backend/src/services/admin.ts create mode 100644 backend/tests/admin/service.test.ts create mode 100644 backend/tests/admin/utils.ts diff --git a/backend/src/controllers/admin.ts b/backend/src/controllers/admin.ts index 4834d06f..35ec5591 100644 --- a/backend/src/controllers/admin.ts +++ b/backend/src/controllers/admin.ts @@ -1,44 +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'); + response + .status(StatusCode.BAD_REQUEST) + .json({ message: 'No email or password' }); } - const adminUser = await this.AdminRepository.findOne({ - where: { email } - }); - - // User not found - if (!adminUser) - return response + 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 + try { + const token = await AdminService.login(email, password) + response + .status(StatusCode.OK) + .json({ token: token}); + } catch { + response .status(StatusCode.UNAUTHORIZED) - .json({ message: 'Invalid Credentials' }); + .json({ message: 'Invalid credentials' }); } }; } 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/stop.ts b/backend/src/services/stop.ts index a2c97c95..9eadc6b0 100644 --- a/backend/src/services/stop.ts +++ b/backend/src/services/stop.ts @@ -137,7 +137,7 @@ export default class StopService { } private static createDefaultStopFromClient = async (client: Client) => { - const stop = new RouteDeliveryEntity(); + const stop = new StopEntity(); stop.routeNumber = 0; stop.routePosition = 0; stop.client = client; 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 + } +} From 75ba9e736d8f0ffae2f0a5e542502901f6c34c80 Mon Sep 17 00:00:00 2001 From: Sophearah Suy-Puth Date: Wed, 15 May 2024 16:10:48 +0700 Subject: [PATCH 53/67] 1) service/test loginVolunteer(), 2) setup volunteer utils createVolunteer() --- backend/src/entities/VolunteerEntity.ts | 2 +- backend/src/services/volunteer.ts | 25 +++++++++++ backend/tests/volunteer/service.test.ts | 58 +++++++++++++++++++++++++ backend/tests/volunteer/utils.ts | 37 ++++++++++++++++ 4 files changed, 121 insertions(+), 1 deletion(-) create mode 100644 backend/src/services/volunteer.ts create mode 100644 backend/tests/volunteer/service.test.ts create mode 100644 backend/tests/volunteer/utils.ts diff --git a/backend/src/entities/VolunteerEntity.ts b/backend/src/entities/VolunteerEntity.ts index 0e65fe63..ac8cb8cf 100644 --- a/backend/src/entities/VolunteerEntity.ts +++ b/backend/src/entities/VolunteerEntity.ts @@ -63,7 +63,7 @@ export class VolunteerEntity extends UserEntity { @Column() startDate: Date; - @Column() + @Column({ nullable: true }) profilePicture: string; @Column({ nullable: true }) diff --git a/backend/src/services/volunteer.ts b/backend/src/services/volunteer.ts new file mode 100644 index 00000000..62f6cbb3 --- /dev/null +++ b/backend/src/services/volunteer.ts @@ -0,0 +1,25 @@ +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; + +export default class VolunteerService { + static readonly VolunteerRepository = AppDataSource.getRepository(VolunteerEntity); + + 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'); + } + } +} \ No newline at end of file diff --git a/backend/tests/volunteer/service.test.ts b/backend/tests/volunteer/service.test.ts new file mode 100644 index 00000000..fde9a227 --- /dev/null +++ b/backend/tests/volunteer/service.test.ts @@ -0,0 +1,58 @@ +import { + jest, + describe, + it, + beforeAll, + afterAll, + afterEach, + expect +} from '@jest/globals'; +import DataSourceHelper from '../data.utils'; +import VolunteerService from '../../src/services/volunteer'; +import AdminUtils from './utils'; +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'); + }); + }); +}); diff --git a/backend/tests/volunteer/utils.ts b/backend/tests/volunteer/utils.ts new file mode 100644 index 00000000..87ffd277 --- /dev/null +++ b/backend/tests/volunteer/utils.ts @@ -0,0 +1,37 @@ +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, +} + +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(), + ...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; + + const savedVolunteer = await this.VolunteerRepository.save(newVolunteer); + + return savedVolunteer + } +} From 1b69db3a7134b05b4b2d177512c806f53c7435a5 Mon Sep 17 00:00:00 2001 From: Sophearah Suy-Puth Date: Wed, 15 May 2024 16:18:25 +0700 Subject: [PATCH 54/67] service/test getActiveVolunteers() and getAllVolunteers() --- backend/src/services/volunteer.ts | 15 ++++++++++ backend/tests/volunteer/service.test.ts | 39 ++++++++++++++++++++++++- backend/tests/volunteer/utils.ts | 3 ++ 3 files changed, 56 insertions(+), 1 deletion(-) diff --git a/backend/src/services/volunteer.ts b/backend/src/services/volunteer.ts index 62f6cbb3..3c03b27c 100644 --- a/backend/src/services/volunteer.ts +++ b/backend/src/services/volunteer.ts @@ -22,4 +22,19 @@ export default class VolunteerService { 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; + }; } \ No newline at end of file diff --git a/backend/tests/volunteer/service.test.ts b/backend/tests/volunteer/service.test.ts index fde9a227..53ab0cd6 100644 --- a/backend/tests/volunteer/service.test.ts +++ b/backend/tests/volunteer/service.test.ts @@ -9,7 +9,6 @@ import { } from '@jest/globals'; import DataSourceHelper from '../data.utils'; import VolunteerService from '../../src/services/volunteer'; -import AdminUtils from './utils'; import * as jwt from 'jsonwebtoken'; import VolunteerUtils from './utils'; require('dotenv').config(); @@ -55,4 +54,42 @@ describe('Volunteer Service Unit Tests', () => { 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]); + }); + }) }); diff --git a/backend/tests/volunteer/utils.ts b/backend/tests/volunteer/utils.ts index 87ffd277..85ce4649 100644 --- a/backend/tests/volunteer/utils.ts +++ b/backend/tests/volunteer/utils.ts @@ -8,6 +8,7 @@ type CreateVolunteerProps = { phoneNumber?: string, password?: string, startDate?: Date, + softDelete?: boolean } export default class VolunteerUtils { @@ -20,6 +21,7 @@ export default class VolunteerUtils { phoneNumber: '(514) 000 0000', password: 'password', startDate: new Date(), + softDelete: false, ...props }; @@ -29,6 +31,7 @@ export default class VolunteerUtils { 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); From fcf7a2963e2b0a2ddc8dfd148e0770294991e8a5 Mon Sep 17 00:00:00 2001 From: Sophearah Suy-Puth Date: Wed, 15 May 2024 16:21:35 +0700 Subject: [PATCH 55/67] service/test getVolunteer() --- backend/src/services/volunteer.ts | 10 ++++++++++ backend/tests/volunteer/service.test.ts | 23 +++++++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/backend/src/services/volunteer.ts b/backend/src/services/volunteer.ts index 3c03b27c..697875f7 100644 --- a/backend/src/services/volunteer.ts +++ b/backend/src/services/volunteer.ts @@ -37,4 +37,14 @@ export default class VolunteerService { const volunteers = await this.VolunteerRepository.find({}); return volunteers; }; + + static getVolunteer = async (id: number) => { + const volunteer = await this.VolunteerRepository.findOne({ + where: { + id: id + } + }); + + return volunteer; + }; } \ No newline at end of file diff --git a/backend/tests/volunteer/service.test.ts b/backend/tests/volunteer/service.test.ts index 53ab0cd6..c3e8d111 100644 --- a/backend/tests/volunteer/service.test.ts +++ b/backend/tests/volunteer/service.test.ts @@ -92,4 +92,27 @@ describe('Volunteer Service Unit Tests', () => { 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' + }); + }); + }) }); From ff3bf465e783cf6583767808a727dcdf7afce239 Mon Sep 17 00:00:00 2001 From: Sophearah Suy-Puth Date: Wed, 15 May 2024 17:18:13 +0700 Subject: [PATCH 56/67] service/test createVolunteer() and updateVolunteer(), drop volunteer endpoint tests --- backend/src/entities/VolunteerEntity.ts | 2 +- backend/src/services/volunteer.ts | 38 +++ backend/src/types/volunteer.ts | 13 + backend/tests/volunteer/service.test.ts | 63 ++++ backend/tests/volunteer/volunteers.test.ts | 334 -------------------- backend/tests/volunteer/volunteers.utils.ts | 47 --- 6 files changed, 115 insertions(+), 382 deletions(-) create mode 100644 backend/src/types/volunteer.ts delete mode 100644 backend/tests/volunteer/volunteers.test.ts delete mode 100644 backend/tests/volunteer/volunteers.utils.ts diff --git a/backend/src/entities/VolunteerEntity.ts b/backend/src/entities/VolunteerEntity.ts index ac8cb8cf..fa5d6d3d 100644 --- a/backend/src/entities/VolunteerEntity.ts +++ b/backend/src/entities/VolunteerEntity.ts @@ -66,7 +66,7 @@ export class VolunteerEntity extends UserEntity { @Column({ nullable: true }) profilePicture: string; - @Column({ nullable: true }) + @Column({nullable: true, type: 'simple-array'}) availabilities: string; @Column({ nullable: true }) diff --git a/backend/src/services/volunteer.ts b/backend/src/services/volunteer.ts index 697875f7..1b9d9477 100644 --- a/backend/src/services/volunteer.ts +++ b/backend/src/services/volunteer.ts @@ -4,6 +4,7 @@ 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); @@ -47,4 +48,41 @@ export default class VolunteerService { 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 updatedClient = await this.VolunteerRepository.findOne({ + where: { + id: id + } + }); + + return updatedClient; + }; } \ No newline at end of file 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/volunteer/service.test.ts b/backend/tests/volunteer/service.test.ts index c3e8d111..1eb7380b 100644 --- a/backend/tests/volunteer/service.test.ts +++ b/backend/tests/volunteer/service.test.ts @@ -115,4 +115,67 @@ describe('Volunteer Service Unit Tests', () => { }); }); }) + + 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/volunteers.test.ts b/backend/tests/volunteer/volunteers.test.ts deleted file mode 100644 index e1369b76..00000000 --- a/backend/tests/volunteer/volunteers.test.ts +++ /dev/null @@ -1,334 +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('Volunteer API 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(); - }); - - describe('GET /api/volunteers', () => { - 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', - '[{"day":"monday","time":"14"}]', - [] - ); - const res = await request(app).get('/api/volunteers'); - expect(res.status).toBe(StatusCode.OK); - expect(res.body).toEqual({ - volunteers: [ - { - availabilities: '[{"day":"monday","time":"14"}]', - email: 'email1', - phoneNumber: '0123456789', - id: 1, - name: 'name1', - profilePicture: 'link to profile', - availabilitiesLastUpdated: lastUpdated.toISOString(), - startDate: date.toISOString(), - password: 'password1', - tasks: [], - token: null, - preferredNeighbourhoods: null, - softDelete: false - } - ] - }); - }); - }) - - describe('GET /api/volunteer/:id', () => { - 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', - '[{"day":"monday","time":"14"}]', - [] - ); - const res = await request(app).get(`/api/volunteers/${volunteer.id}`); - expect(res.status).toBe(StatusCode.OK); - expect(res.body).toEqual({ - volunteer: { - availabilities: '[{"day":"monday","time":"14"}]', - email: 'email1', - phoneNumber: '0123456789', - id: 1, - name: 'name1', - profilePicture: 'link to profile', - availabilitiesLastUpdated: lastUpdated.toISOString(), - startDate: date.toISOString(), - password: 'password1', - tasks: [], - token: null, - preferredNeighbourhoods: null, - softDelete: false - } - }); - }); - }) - - describe('PUT /api/volunteers/:id/edit', () => { - it('should update 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', - '[{"day":"monday","time":"14"}]', - [] - ); - const res = await request(app).put(`/api/volunteers/${volunteer.id}/edit`).send({ - name: 'name2', - email: 'email1@gmail.com' - }); - expect(res.status).toBe(StatusCode.OK); - const updatedVolunteer = await volunteerHelper.getVolunteer(volunteer.id) - expect(updatedVolunteer).toEqual( - expect.objectContaining( - { - name: 'name2', - email: 'email1@gmail.com' - } - ) - ) - }); - }) - - describe('POST /api/volunteers', () => { - it('should create volunteer', async () => { - const date: Date = new Date('April 20, 2001 04:20:00'); - const res = await request(app).post(`/api/volunteers`).send({ - name: 'name', - password: 'password1', - email: 'email1', - phoneNumber: '0123456789', - date: date.toISOString(), - }); - expect(res.status).toBe(StatusCode.OK); - expect(res.body).toEqual({ - volunteer: { - availabilities: null, - email: 'email1', - phoneNumber: '0123456789', - id: 1, - name: 'name', - profilePicture: '', - availabilitiesLastUpdated: null, - startDate: date.toISOString(), - password: 'password1', - token: null, - preferredNeighbourhoods: [], - softDelete: false - } - }); - }) - }) - - // 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 ed302ca4..00000000 --- a/backend/tests/volunteer/volunteers.utils.ts +++ /dev/null @@ -1,47 +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); - }; - - getVolunteer = async (volunteerId: number) => { - return await this.VolunteerRepository.findOne({ - where: { - id: volunteerId, - softDelete: false - }, - relations: { - tasks: true - } - }); - } -} From 830b59e1bf4f1b90f04dd29564c5be002e75e393 Mon Sep 17 00:00:00 2001 From: Sophearah Suy-Puth Date: Wed, 15 May 2024 17:27:11 +0700 Subject: [PATCH 57/67] 1) remove auth route/controller, 2) setup volunteer controller --- backend/src/controllers/authentication.ts | 27 ---- backend/src/controllers/volunteers.ts | 146 +++++++------------- backend/src/routes/authentication.routes.ts | 10 -- backend/src/routes/index.ts | 2 - backend/src/routes/volunteer.routes.ts | 6 +- backend/src/services/volunteer.ts | 13 +- 6 files changed, 62 insertions(+), 142 deletions(-) delete mode 100644 backend/src/controllers/authentication.ts delete mode 100644 backend/src/routes/authentication.routes.ts diff --git a/backend/src/controllers/authentication.ts b/backend/src/controllers/authentication.ts deleted file mode 100644 index 46ec98bb..00000000 --- a/backend/src/controllers/authentication.ts +++ /dev/null @@ -1,27 +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/volunteers.ts b/backend/src/controllers/volunteers.ts index 8a9b49f6..61763ace 100644 --- a/backend/src/controllers/volunteers.ts +++ b/backend/src/controllers/volunteers.ts @@ -1,117 +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 { getNeighbourhoodFromString } from '../entities/types'; +import VolunteerService from '../services/volunteer' +import {CreateVolunteerProps, UpdateVolunteerProps} from '../types/volunteer' export default class VolunteerController { - private static VolunteerRepository = - AppDataSource.getRepository(VolunteerEntity); + static login = async (request: Request, response: Response) => { + const { email, password } = request.body; - static getVolunteers = async (request: Request, response: Response) => { - const volunteers = await this.VolunteerRepository.find({ - relations: { - tasks: true - }, - where: { - softDelete: false - } - }); - response.status(StatusCode.OK).json({ volunteers: volunteers }); - }; + if (!(email && password)) { + response + .status(StatusCode.BAD_REQUEST) + .json({ message: 'No email or password' }); + } - static getVolunteer = async (request: Request, response: Response) => { - const volunteer = await this.VolunteerRepository.findOne({ - where: { - id: parseInt(request.params.id), - softDelete: false - }, - 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' }); + } - static 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' }); + } }; - static createVolunteer = async (request: Request, response: Response) => { - const volunteer = await this.VolunteerRepository.save({ - 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, - preferredNeighbourhoods: request.body.preferredNeighbourhoods - ? request.body.preferredNeighbourhoods.map((neighborhood) => - getNeighbourhoodFromString(neighborhood) - ) - : [], - softDelete: false - }); - response.status(StatusCode.OK).json({ volunteer }); + static getVolunteers = async (request: Request, response: Response) => { + const volunteers = VolunteerService.getAllVolunteers(); + response.status(StatusCode.OK).json({ volunteers: volunteers }); }; - static 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 = VolunteerService.createVolunteer(props); + response.status(StatusCode.OK).json({ volunteer: volunteer }); }; - static 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 = VolunteerService.getVolunteer(id); - static 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 }); + } }; - static 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 = 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 }); + if (volunteer == null) { + response.status(StatusCode.NOT_FOUND); + } else { + response.status(StatusCode.OK).json({ volunteer: volunteer }); } - return res.status(400).json({ error: 'bad login informations' }); }; } 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/index.ts b/backend/src/routes/index.ts index e42f907b..060bce74 100644 --- a/backend/src/routes/index.ts +++ b/backend/src/routes/index.ts @@ -5,7 +5,6 @@ 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 @@ -17,5 +16,4 @@ api.use(mealDelivery); api.use(stop); api.use(volunteers); api.use(clients); -api.use(authentication); api.use(admin); diff --git a/backend/src/routes/volunteer.routes.ts b/backend/src/routes/volunteer.routes.ts index 5141257c..60d2de5d 100644 --- a/backend/src/routes/volunteer.routes.ts +++ b/backend/src/routes/volunteer.routes.ts @@ -6,9 +6,5 @@ export const router = express.Router(); router.get('/volunteers', VolunteerController.getVolunteers); router.get('/volunteers/:id', VolunteerController.getVolunteer); router.post('/volunteers', VolunteerController.createVolunteer); -router.put('/volunteers/:id/edit', VolunteerController.editVolunteer); - -// TODO: review -router.get('/volunteers/:id/tasks', VolunteerController.getVolunteerTasks); -router.delete('/volunteers/:id', VolunteerController.removeVolunteer); +router.put('/volunteers/:id', VolunteerController.updateVolunteer); router.post('/volunteer/login', VolunteerController.login); diff --git a/backend/src/services/volunteer.ts b/backend/src/services/volunteer.ts index 1b9d9477..2081b816 100644 --- a/backend/src/services/volunteer.ts +++ b/backend/src/services/volunteer.ts @@ -9,6 +9,18 @@ import {CreateVolunteerProps, UpdateVolunteerProps, Volunteer} from '../types/vo 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 } @@ -62,7 +74,6 @@ export default class VolunteerService { return volunteer; }; - static updateVolunteer = async ( id: number, updateProps: UpdateVolunteerProps From 31c498c89804420ee1a71446accaeec00379ffb3 Mon Sep 17 00:00:00 2001 From: Sophearah Suy-Puth Date: Thu, 16 May 2024 12:11:14 +0700 Subject: [PATCH 58/67] 1) service createMealsForTask(), 2) service getRouteStops(), 3) service getTasks(), getTasks(), createTask(), 4) test createTask(), 5) TaskUtils createVolunteer(), setupRoutes() --- backend/src/services/meal.ts | 30 ++++++++++ backend/src/services/stop.ts | 13 +++++ backend/src/services/task.ts | 41 +++++++++++++ backend/src/types/meal.ts | 13 +++++ backend/src/types/task.ts | 10 ++++ backend/tests/task/service.test.ts | 82 ++++++++++++++++++++++++++ backend/tests/task/utils.ts | 94 ++++++++++++++++++++++++++++++ 7 files changed, 283 insertions(+) create mode 100644 backend/src/services/meal.ts create mode 100644 backend/src/services/task.ts create mode 100644 backend/src/types/meal.ts create mode 100644 backend/src/types/task.ts create mode 100644 backend/tests/task/service.test.ts create mode 100644 backend/tests/task/utils.ts diff --git a/backend/src/services/meal.ts b/backend/src/services/meal.ts new file mode 100644 index 00000000..35dd218b --- /dev/null +++ b/backend/src/services/meal.ts @@ -0,0 +1,30 @@ +import { AppDataSource } from '../data-source'; +import { MealDeliveryEntity } from '../entities/MealDeliveryEntity'; +import StopService from './stop'; +import TaskService from './task'; + +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 + }; +} diff --git a/backend/src/services/stop.ts b/backend/src/services/stop.ts index 9eadc6b0..0bfc58f8 100644 --- a/backend/src/services/stop.ts +++ b/backend/src/services/stop.ts @@ -136,6 +136,19 @@ export default class StopService { 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; diff --git a/backend/src/services/task.ts b/backend/src/services/task.ts new file mode 100644 index 00000000..b7efe0d6 --- /dev/null +++ b/backend/src/services/task.ts @@ -0,0 +1,41 @@ +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 + }; +} diff --git a/backend/src/types/meal.ts b/backend/src/types/meal.ts new file mode 100644 index 00000000..7dc966c5 --- /dev/null +++ b/backend/src/types/meal.ts @@ -0,0 +1,13 @@ +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; +}; 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/tests/task/service.test.ts b/backend/tests/task/service.test.ts new file mode 100644 index 00000000..e9f61391 --- /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, volunteer: volunteer}, + {mealType: MealType.REGULAR, program: ProgramType.STS, volunteer: volunteer}, + {mealType: MealType.REGULAR, program: ProgramType.MAP, volunteer: volunteer}, + ] + }) + 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/utils.ts b/backend/tests/task/utils.ts new file mode 100644 index 00000000..cf2cb928 --- /dev/null +++ b/backend/tests/task/utils.ts @@ -0,0 +1,94 @@ +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, + volunteer: Volunteer +} + +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); + } + } + } + +} From 37acd598cb5e9c4000070fefc838133aa36847c7 Mon Sep 17 00:00:00 2001 From: Sophearah Suy-Puth Date: Thu, 16 May 2024 13:06:25 +0700 Subject: [PATCH 59/67] remove task test files that arent used --- backend/src/services/volunteer.ts | 4 +- backend/tests/task/task.utils.ts | 21 ----- backend/tests/task/tasks.test.ts | 129 ------------------------------ 3 files changed, 2 insertions(+), 152 deletions(-) delete mode 100644 backend/tests/task/task.utils.ts delete mode 100644 backend/tests/task/tasks.test.ts diff --git a/backend/src/services/volunteer.ts b/backend/src/services/volunteer.ts index 2081b816..952c5061 100644 --- a/backend/src/services/volunteer.ts +++ b/backend/src/services/volunteer.ts @@ -88,12 +88,12 @@ export default class VolunteerService { await this.VolunteerRepository.update({ id: id }, updateProps); - const updatedClient = await this.VolunteerRepository.findOne({ + const updatedVolunteer = await this.VolunteerRepository.findOne({ where: { id: id } }); - return updatedClient; + return updatedVolunteer; }; } \ No newline at end of file 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 a171505b..00000000 --- a/backend/tests/task/tasks.test.ts +++ /dev/null @@ -1,129 +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(); - }); - - describe('GET /api/tasks', () => { - it.skip('should return all tasks', async () => { - // TODO: test here - }); - }) - - describe('GET /api/tasks-match', () => { - it.skip('should return all route delivery and volunteer data for matching tasks', async () => { - // TODO: test here - }); - }) - - describe('POST /api/tasks-match', () => { - it.skip('should create tasks from match data', async () => { - // TODO: test here - }); - }) - - // 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({}); - // }); -}); From 4dad19f99801311641fc740f1c883c4be7b86590 Mon Sep 17 00:00:00 2001 From: Sophearah Suy-Puth Date: Sat, 18 May 2024 12:00:38 +0700 Subject: [PATCH 60/67] remove unnecessary prop for test task util --- backend/tests/task/service.test.ts | 6 +++--- backend/tests/task/utils.ts | 3 +-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/backend/tests/task/service.test.ts b/backend/tests/task/service.test.ts index e9f61391..24cb8665 100644 --- a/backend/tests/task/service.test.ts +++ b/backend/tests/task/service.test.ts @@ -36,9 +36,9 @@ describe('Task Service Unit Tests', () => { await TaskUtils.setupRoutes({ 0: [], 1: [ - {mealType: MealType.REGULAR, program: ProgramType.MAP, volunteer: volunteer}, - {mealType: MealType.REGULAR, program: ProgramType.STS, volunteer: volunteer}, - {mealType: MealType.REGULAR, program: ProgramType.MAP, volunteer: volunteer}, + {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) diff --git a/backend/tests/task/utils.ts b/backend/tests/task/utils.ts index cf2cb928..62dec506 100644 --- a/backend/tests/task/utils.ts +++ b/backend/tests/task/utils.ts @@ -17,8 +17,7 @@ type CreateVolunteerProps = { type CreateStopProps = { mealType: MealType, - program: ProgramType, - volunteer: Volunteer + program: ProgramType } type CreateRoutesProps = { From c348502b599870f7134d23eab12de21fc8f1294b Mon Sep 17 00:00:00 2001 From: Sophearah Suy-Puth Date: Sat, 18 May 2024 12:02:04 +0700 Subject: [PATCH 61/67] 1) service/test updateMeal(), 2) service updateMealInTask(), 3) meal test utils --- backend/src/services/meal.ts | 26 +++++++ backend/src/services/task.ts | 31 +++++++++ backend/src/types/meal.ts | 2 + backend/tests/meal/service.test.ts | 57 ++++++++++++++++ backend/tests/meal/utils.ts | 105 +++++++++++++++++++++++++++++ 5 files changed, 221 insertions(+) create mode 100644 backend/tests/meal/service.test.ts create mode 100644 backend/tests/meal/utils.ts diff --git a/backend/src/services/meal.ts b/backend/src/services/meal.ts index 35dd218b..adbc1e6b 100644 --- a/backend/src/services/meal.ts +++ b/backend/src/services/meal.ts @@ -2,6 +2,7 @@ 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 = @@ -27,4 +28,29 @@ export default class MealService { } 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/task.ts b/backend/src/services/task.ts index b7efe0d6..fb1f3558 100644 --- a/backend/src/services/task.ts +++ b/backend/src/services/task.ts @@ -38,4 +38,35 @@ export default class TaskService { 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/types/meal.ts b/backend/src/types/meal.ts index 7dc966c5..6c0e9bf0 100644 --- a/backend/src/types/meal.ts +++ b/backend/src/types/meal.ts @@ -11,3 +11,5 @@ export type Meal = { task: Task; client: Client; }; + +export type UpdateMealProps = Partial; 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 + } +} From edd3ff9ae20b0e3727bdcf8b4d75acd25f57f40b Mon Sep 17 00:00:00 2001 From: Sophearah Suy-Puth Date: Sat, 18 May 2024 12:02:28 +0700 Subject: [PATCH 62/67] remove old meal delivery tests --- .../tests/mealDelivery/mealDelivery.utils.ts | 31 ---- .../tests/mealDelivery/mealDeliverytest.ts | 132 ------------------ 2 files changed, 163 deletions(-) delete mode 100644 backend/tests/mealDelivery/mealDelivery.utils.ts delete mode 100644 backend/tests/mealDelivery/mealDeliverytest.ts 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/mealDelivery/mealDeliverytest.ts b/backend/tests/mealDelivery/mealDeliverytest.ts deleted file mode 100644 index 0c9665eb..00000000 --- a/backend/tests/mealDelivery/mealDeliverytest.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({}); -// // }); -// }); From e6b3eceb87f86b98772c67478530ed7c0adfd92e Mon Sep 17 00:00:00 2001 From: Sophearah Suy-Puth Date: Sat, 18 May 2024 12:46:43 +0700 Subject: [PATCH 63/67] 1) get rid of meal delivery controller/router, 2) setup task controller and router --- backend/src/controllers/mealDelivery.ts | 108 --------- backend/src/controllers/tasks.ts | 276 +--------------------- backend/src/routes/mealDelivery.routes.ts | 19 -- backend/src/routes/task.routes.ts | 12 +- 4 files changed, 12 insertions(+), 403 deletions(-) delete mode 100644 backend/src/controllers/mealDelivery.ts delete mode 100644 backend/src/routes/mealDelivery.routes.ts diff --git a/backend/src/controllers/mealDelivery.ts b/backend/src/controllers/mealDelivery.ts deleted file mode 100644 index 5cb90ca5..00000000 --- a/backend/src/controllers/mealDelivery.ts +++ /dev/null @@ -1,108 +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/tasks.ts b/backend/src/controllers/tasks.ts index 7b2a7bb9..297698f0 100644 --- a/backend/src/controllers/tasks.ts +++ b/backend/src/controllers/tasks.ts @@ -1,273 +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 { StopEntity as RouteDeliveryEntity } from '../entities/StopEntity'; - -// Get day of week to get notification by -const getLastMonday = () => { - const currentDate: Date = new Date(); - - const currentDay: number = currentDate.getDay(); - - const daysUntilMonday: number = currentDay === 0 ? 6 : currentDay - 1; - - const mostRecentMonday: Date = new Date(currentDate); - mostRecentMonday.setDate(currentDate.getDate() - daysUntilMonday); - - const formattedMostRecentMonday: string = mostRecentMonday - .toISOString() - .split('T')[0]; - - return formattedMostRecentMonday; -}; +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 } - }); - + static getTasks = (request: Request, response: Response) => { + const tasks = TaskService.getTasks(); + response.status(StatusCode.OK).json({ tasks: tasks }); + } + + static createTask = (request: Request, response: Response) => { + const {volunteerId, routeNumber} = request.body + const task = TaskService.createTask(volunteerId, routeNumber) 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 }); - } - }; - - deleteTask = async (request: Request, response: Response) => { - const taskDeleted = await this.TaskRepository.delete({ - id: parseInt(request.params.id) - }); - response.status(StatusCode.OK).json({}); - }; - - getTaskMatchData = async (request: Request, response: Response) => { - const volunteers = await this.VolunteerRepository.find({ - select: { - id: true, - name: true, - availabilities: true, - availabilitiesLastUpdated: true - } - }); - - const comparisonDate = getLastMonday(); - - const availableVolunteers = volunteers.filter((v) => { - if (v.availabilitiesLastUpdated >= new Date(comparisonDate)) { - return v; - } - }); - - 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 (key != 0) { - 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({ availableVolunteers: availableVolunteers, routes: groups }); - }; - - saveTaskMatch = async (request: Request, response: Response) => { - const taskMatches = request.body.matches; - - for (let i = 0; i < taskMatches.length; i++) { - const match = taskMatches[i]; - - // CREATE TASK - const newTask = new TaskEntity(); - newTask.deliveries = []; - newTask.isCompleted = false; - newTask.volunteer = await this.VolunteerRepository.findOne({ - where: { id: match.volunteerId } - }); - - const savedTask = await this.TaskRepository.save(newTask); - - // CREATE MEAL DELIVERY OBJECTS - const routeObjs = await this.RouteDeliveryRepository.find({ - where: { - routeNumber: match.routeName - }, - relations: { - client: true - } - }); - - for (let i = 0; i < routeObjs.length; i++) { - const routeObj = routeObjs[i]; - const newMeal = new MealDeliveryEntity(); - newMeal.mealType = routeObj.mealType; - newMeal.task = savedTask; - newMeal.routePosition = routeObj.routePosition; - newMeal.program = routeObj.program; - const foundClient = await this.ClientRepository.findOne({ - where: { id: routeObj.client.id } - }); - newMeal.client = foundClient; - const savedDelivery = await this.MealDeliveryRepository.save( - newMeal - ); - newTask.deliveries.push(savedDelivery); - } - - await this.TaskRepository.save(newTask); - } - response.status(StatusCode.OK).json({}); - }; + } } diff --git a/backend/src/routes/mealDelivery.routes.ts b/backend/src/routes/mealDelivery.routes.ts deleted file mode 100644 index 3c04c26e..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/task.routes.ts b/backend/src/routes/task.routes.ts index 621852d2..6bb36f23 100644 --- a/backend/src/routes/task.routes.ts +++ b/backend/src/routes/task.routes.ts @@ -3,14 +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-match', taskController.getTaskMatchData); -router.post('/tasks-match', taskController.saveTaskMatch); +router.get('/tasks', TaskController.getTasks); +router.post('/tasks', TaskController.getTasks); From 85548970d13c315d83b05fbd2d372505d3c04861 Mon Sep 17 00:00:00 2001 From: Sophearah Suy-Puth Date: Sat, 18 May 2024 12:48:36 +0700 Subject: [PATCH 64/67] controllers use static methods --- backend/src/controllers/admin.ts | 2 +- backend/src/controllers/clients.ts | 8 ++++---- backend/src/controllers/stops.ts | 4 ++-- backend/src/routes/admin.routes.ts | 3 +-- backend/src/routes/client.routes.ts | 9 ++++----- backend/src/routes/index.ts | 2 -- backend/src/routes/stop.routes.ts | 5 ++--- 7 files changed, 14 insertions(+), 19 deletions(-) diff --git a/backend/src/controllers/admin.ts b/backend/src/controllers/admin.ts index 35ec5591..67d7f812 100644 --- a/backend/src/controllers/admin.ts +++ b/backend/src/controllers/admin.ts @@ -3,7 +3,7 @@ import { StatusCode } from './statusCode'; import AdminService from '../services/admin'; export default class AdminController { - login = async (request: Request, response: Response) => { + static login = async (request: Request, response: Response) => { const { email, password } = request.body; if (!(email && password)) { diff --git a/backend/src/controllers/clients.ts b/backend/src/controllers/clients.ts index 638aea6a..a2ff4ada 100644 --- a/backend/src/controllers/clients.ts +++ b/backend/src/controllers/clients.ts @@ -4,18 +4,18 @@ import ClientService from '../services/clients'; import { Client, CreateClientProps, UpdateClientProps } from '../types/clients'; export default class ClientController { - getClients = async (request: Request, response: Response) => { + static getClients = async (request: Request, response: Response) => { const clients = ClientService.getAllClients(); response.status(StatusCode.OK).json({ clients: clients }); }; - createClient = async (request: Request, response: Response) => { + static createClient = async (request: Request, response: Response) => { const props: CreateClientProps = request.body; const client = ClientService.createClient(props); response.status(StatusCode.OK).json({ client: client }); }; - getClient = async (request: Request, response: Response) => { + static getClient = async (request: Request, response: Response) => { const id = parseInt(request.params.id); const client = ClientService.getClient(id); @@ -26,7 +26,7 @@ export default class ClientController { } }; - updateClient = async (request: Request, response: Response) => { + static updateClient = async (request: Request, response: Response) => { const id = parseInt(request.params.id); const props: UpdateClientProps = request.body; const client = ClientService.updateClient(id, props); diff --git a/backend/src/controllers/stops.ts b/backend/src/controllers/stops.ts index 2ee9469d..af81938a 100644 --- a/backend/src/controllers/stops.ts +++ b/backend/src/controllers/stops.ts @@ -4,12 +4,12 @@ import StopService from '../services/stop'; import { SaveRouteProps } from '../types/stop'; export default class StopController { - getRoutes = async (request: Request, response: Response) => { + static getRoutes = async (request: Request, response: Response) => { const routes = await StopService.getRoutes() response.status(StatusCode.OK).json({ routes: routes }); } - saveRoutes = async (request: Request, response: Response) => { + static saveRoutes = async (request: Request, response: Response) => { const props: SaveRouteProps = request.body; const routes = await StopService.saveRoutes(props) if (routes == null) { 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/client.routes.ts b/backend/src/routes/client.routes.ts index 29eb4148..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.updateClient); -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 060bce74..fba21730 100644 --- a/backend/src/routes/index.ts +++ b/backend/src/routes/index.ts @@ -1,6 +1,5 @@ import * as express from 'express'; import { router as tasks } from './task.routes'; -import { router as mealDelivery } from './mealDelivery.routes'; import { router as stop } from './stop.routes'; import { router as volunteers } from './volunteer.routes'; import { router as admin } from './admin.routes'; @@ -12,7 +11,6 @@ export const api = express.Router(); // Adds the routes from todo.route to this router api.use(tasks); -api.use(mealDelivery); api.use(stop); api.use(volunteers); api.use(clients); diff --git a/backend/src/routes/stop.routes.ts b/backend/src/routes/stop.routes.ts index 9eb24c15..10ab9fae 100644 --- a/backend/src/routes/stop.routes.ts +++ b/backend/src/routes/stop.routes.ts @@ -3,7 +3,6 @@ import StopController from '../controllers/stops'; // Create a router object export const router = express.Router(); -const stopController = new StopController(); -router.get('/stops/routes/', stopController.getRoutes); -router.post('/stops/routes/', stopController.saveRoutes); \ No newline at end of file +router.get('/stops/routes/', StopController.getRoutes); +router.post('/stops/routes/', StopController.saveRoutes); \ No newline at end of file From 821b3e059632482b15577f89649d6910b0b5948c Mon Sep 17 00:00:00 2001 From: Sophearah Suy-Puth Date: Mon, 20 May 2024 12:08:32 +0700 Subject: [PATCH 65/67] add await to the controller functions bc forgot --- backend/src/controllers/clients.ts | 8 ++++---- backend/src/controllers/tasks.ts | 8 ++++---- backend/src/controllers/volunteers.ts | 8 ++++---- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/backend/src/controllers/clients.ts b/backend/src/controllers/clients.ts index a2ff4ada..9e249597 100644 --- a/backend/src/controllers/clients.ts +++ b/backend/src/controllers/clients.ts @@ -5,19 +5,19 @@ import { Client, CreateClientProps, UpdateClientProps } from '../types/clients'; export default class ClientController { static getClients = async (request: Request, response: Response) => { - const clients = ClientService.getAllClients(); + 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 = ClientService.createClient(props); + 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 = ClientService.getClient(id); + const client = await ClientService.getClient(id); if (client == null) { response.status(StatusCode.NOT_FOUND); @@ -29,7 +29,7 @@ export default class ClientController { static updateClient = async (request: Request, response: Response) => { const id = parseInt(request.params.id); const props: UpdateClientProps = request.body; - const client = ClientService.updateClient(id, props); + const client = await ClientService.updateClient(id, props); if (client == null) { response.status(StatusCode.NOT_FOUND); diff --git a/backend/src/controllers/tasks.ts b/backend/src/controllers/tasks.ts index 297698f0..fd60cb15 100644 --- a/backend/src/controllers/tasks.ts +++ b/backend/src/controllers/tasks.ts @@ -4,14 +4,14 @@ import { StatusCode } from './statusCode'; import TaskService from '../services/task'; export default class TaskController { - static getTasks = (request: Request, response: Response) => { - const tasks = TaskService.getTasks(); + static getTasks = async (request: Request, response: Response) => { + const tasks = await TaskService.getTasks(); response.status(StatusCode.OK).json({ tasks: tasks }); } - static createTask = (request: Request, response: Response) => { + static createTask = async (request: Request, response: Response) => { const {volunteerId, routeNumber} = request.body - const task = TaskService.createTask(volunteerId, routeNumber) + 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 61763ace..fefe16b2 100644 --- a/backend/src/controllers/volunteers.ts +++ b/backend/src/controllers/volunteers.ts @@ -34,19 +34,19 @@ export default class VolunteerController { }; static getVolunteers = async (request: Request, response: Response) => { - const volunteers = VolunteerService.getAllVolunteers(); + const volunteers = await VolunteerService.getAllVolunteers(); response.status(StatusCode.OK).json({ volunteers: volunteers }); }; static createVolunteer = async (request: Request, response: Response) => { const props: CreateVolunteerProps = request.body; - const volunteer = VolunteerService.createVolunteer(props); + const volunteer = await VolunteerService.createVolunteer(props); response.status(StatusCode.OK).json({ volunteer: volunteer }); }; static getVolunteer = async (request: Request, response: Response) => { const id = parseInt(request.params.id); - const volunteer = VolunteerService.getVolunteer(id); + const volunteer = await VolunteerService.getVolunteer(id); if (volunteer == null) { response.status(StatusCode.NOT_FOUND); @@ -58,7 +58,7 @@ export default class VolunteerController { static updateVolunteer = async (request: Request, response: Response) => { const id = parseInt(request.params.id); const props: UpdateVolunteerProps = request.body; - const volunteer = VolunteerService.updateVolunteer(id, props); + const volunteer = await VolunteerService.updateVolunteer(id, props); if (volunteer == null) { response.status(StatusCode.NOT_FOUND); From 617de8d69cbd49d373749a646fc62a7173d159d5 Mon Sep 17 00:00:00 2001 From: Sophearah Suy-Puth Date: Mon, 20 May 2024 12:12:25 +0700 Subject: [PATCH 66/67] rewrite seed scripts using services, hide env file --- backend/.env | 7 -- backend/example.env | 10 +++ backend/src/scripts/drop.ts | 1 - backend/src/scripts/seed.ts | 16 +--- .../admin.seeder.ts => seed/admin.seed.ts} | 8 +- backend/src/scripts/seed/client.seed.ts | 55 ++++++++++++ .../{seeders/user.ts => seed/utils.ts} | 26 ++---- backend/src/scripts/seed/volunteer.seed.ts | 52 +++++++++++ backend/src/scripts/seeders/client.seeder.ts | 87 ------------------- backend/src/scripts/seeders/task.seeder.ts | 84 ------------------ .../src/scripts/seeders/volunteer.seeder.ts | 87 ------------------- 11 files changed, 132 insertions(+), 301 deletions(-) delete mode 100644 backend/.env create mode 100644 backend/example.env rename backend/src/scripts/{seeders/admin.seeder.ts => seed/admin.seed.ts} (85%) create mode 100644 backend/src/scripts/seed/client.seed.ts rename backend/src/scripts/{seeders/user.ts => seed/utils.ts} (58%) create mode 100644 backend/src/scripts/seed/volunteer.seed.ts delete mode 100644 backend/src/scripts/seeders/client.seeder.ts delete mode 100644 backend/src/scripts/seeders/task.seeder.ts delete mode 100644 backend/src/scripts/seeders/volunteer.seeder.ts 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/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/scripts/drop.ts b/backend/src/scripts/drop.ts index 7528d0c7..44aea456 100644 --- a/backend/src/scripts/drop.ts +++ b/backend/src/scripts/drop.ts @@ -1,7 +1,6 @@ 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'); diff --git a/backend/src/scripts/seed.ts b/backend/src/scripts/seed.ts index c175419a..5617ea4e 100644 --- a/backend/src/scripts/seed.ts +++ b/backend/src/scripts/seed.ts @@ -1,24 +1,16 @@ 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, AdminSeeder); 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'); }; diff --git a/backend/src/scripts/seeders/admin.seeder.ts b/backend/src/scripts/seed/admin.seed.ts similarity index 85% rename from backend/src/scripts/seeders/admin.seeder.ts rename to backend/src/scripts/seed/admin.seed.ts index 3267f759..925fd7b8 100644 --- a/backend/src/scripts/seeders/admin.seeder.ts +++ b/backend/src/scripts/seed/admin.seed.ts @@ -2,13 +2,13 @@ import { Seeder, SeederFactoryManager } from 'typeorm-extension'; import { DataSource } from 'typeorm'; import { AdminEntity } from '../../entities/AdminEntity'; import * as bcrypt from 'bcryptjs'; -import { generateStaffUser } from './user'; +import { generateStaffUser } from './utils'; require('dotenv').config(); const DEFAULT_ADMIN = { - password: process.env.ADMIN_PASSWORD, - email: process.env.ADMIN_EMAIL, + password: process.env.SEED_DEFAULT_ADMIN_EMAIL, + email: process.env.SEED_DEFAULT_ADMIN_PASSWORD, jobTitle: 'Administrator' }; @@ -29,4 +29,4 @@ export default class AdminSeeder implements Seeder { 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/seeders/user.ts b/backend/src/scripts/seed/utils.ts similarity index 58% rename from backend/src/scripts/seeders/user.ts rename to backend/src/scripts/seed/utils.ts index 827707b1..b925f9bc 100644 --- a/backend/src/scripts/seeders/user.ts +++ b/backend/src/scripts/seed/utils.ts @@ -1,37 +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; -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 +export const generateStaffUser = async () => { + const firstName = faker.name.firstName(); + const lastName = faker.name.lastName(); - const availability = - avalabilities[Math.floor(Math.random() * avalabilities.length)]; - const user = { + const user: any = { 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; + 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/client.seeder.ts b/backend/src/scripts/seeders/client.seeder.ts deleted file mode 100644 index 27ece7d6..00000000 --- a/backend/src/scripts/seeders/client.seeder.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { Seeder, SeederFactoryManager } from 'typeorm-extension'; -import { DataSource } from 'typeorm'; -import { faker } from '@faker-js/faker'; -import { ClientEntity } from '../../entities/ClientEntity'; -import { StopEntity as RouteDeliveryEntity } from '../../entities/StopEntity'; -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 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 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 c8fbf6af..00000000 --- a/backend/src/scripts/seeders/task.seeder.ts +++ /dev/null @@ -1,84 +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/volunteer.seeder.ts b/backend/src/scripts/seeders/volunteer.seeder.ts deleted file mode 100644 index 5279e575..00000000 --- a/backend/src/scripts/seeders/volunteer.seeder.ts +++ /dev/null @@ -1,87 +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); - volunteer.softDelete = false; - 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']; - volunteer.softDelete = false; - 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); - } -} From 6650dbfd683cf2ffef9e8eb7aa168af7bdf81180 Mon Sep 17 00:00:00 2001 From: Sophearah Suy-Puth Date: Mon, 20 May 2024 12:13:39 +0700 Subject: [PATCH 67/67] gitignore update to hide .env file --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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