diff --git a/apps/backend/src/config/typeorm.ts b/apps/backend/src/config/typeorm.ts index ac3fb2e3..b905a030 100644 --- a/apps/backend/src/config/typeorm.ts +++ b/apps/backend/src/config/typeorm.ts @@ -16,6 +16,8 @@ import { UpdateFoodRequests1744051370129 } from '../migrations/1744051370129-upd import { UpdateRequestTable1741571847063 } from '../migrations/1741571847063-updateRequestTable'; import { RemoveOrderIdFromRequests1744133526650 } from '../migrations/1744133526650-removeOrderIdFromRequests'; import { AddOrders1739496585940 } from '../migrations/1739496585940-addOrders'; +import { AddManufacturerDetails1743518493960 } from '../migrations/1743518493960-AddManufacturerDetails'; +import { AddManufacturerDonationFrequency1743623272909 } from '../migrations/1743623272909-AddManufacturerDonationFrequency'; import { UpdatePantriesTable1742739750279 } from '../migrations/1742739750279-updatePantriesTable'; const config = { @@ -42,6 +44,8 @@ const config = { UpdatePantriesTable1739056029076, AssignmentsPantryIdNotUnique1758384669652, AddOrders1739496585940, + AddManufacturerDetails1743518493960, + AddManufacturerDonationFrequency1743623272909, UpdateOrdersTable1740367964915, UpdateRequestTable1741571847063, UpdateFoodRequests1744051370129, diff --git a/apps/backend/src/donations/donations.controller.ts b/apps/backend/src/donations/donations.controller.ts index 69ad0300..d3936f96 100644 --- a/apps/backend/src/donations/donations.controller.ts +++ b/apps/backend/src/donations/donations.controller.ts @@ -33,6 +33,18 @@ export class DonationsController { return this.donationService.findOne(donationId); } + @Get('/get-all-donations') + async getAllDonations(): Promise { + return this.donationService.getAll(); + } + + @Get('/getManufacturerDonationCount/:manufacturerId') + async getManufacturerDonationCount( + @Param('manufacturerId', ParseIntPipe) manufacturerId: number, + ): Promise { + return this.donationService.getManufacturerDonationCount(manufacturerId); + } + @Post('/create') @ApiBody({ description: 'Details for creating a donation', diff --git a/apps/backend/src/donations/donations.service.ts b/apps/backend/src/donations/donations.service.ts index 0f037fed..2442f88a 100644 --- a/apps/backend/src/donations/donations.service.ts +++ b/apps/backend/src/donations/donations.service.ts @@ -36,6 +36,13 @@ export class DonationService { return this.repo.count(); } + async getManufacturerDonationCount(manufacturerId: number) { + const count = await this.repo.count({ + where: { foodManufacturerId: manufacturerId }, + }); + return count; + } + async create( foodManufacturerId: number, dateDonated: Date, diff --git a/apps/backend/src/foodManufacturers/manufacturer.controller.ts b/apps/backend/src/foodManufacturers/manufacturer.controller.ts new file mode 100644 index 00000000..406186de --- /dev/null +++ b/apps/backend/src/foodManufacturers/manufacturer.controller.ts @@ -0,0 +1,26 @@ +import { Controller, Get, Param, ParseIntPipe, Patch } from '@nestjs/common'; +import { FoodManufacturer } from './manufacturer.entity'; +import { ManufacturerService } from './manufacturer.service'; + +@Controller('manufacturer') +export class ManufacturerController { + constructor(private manufacturerService: ManufacturerService) {} + + @Get('/getDetails/:manufacturerId') + async getManufacturerDetails( + @Param('manufacturerId', ParseIntPipe) manufacturerId: number, + ): Promise { + return this.manufacturerService.getDetails(manufacturerId); + } + + @Patch('/updateFrequency/:manufacturerId/:frequency') + async updateManufacturerFrequency( + @Param('manufacturerId') manufacturerId: number, + @Param('frequency') frequency: string, + ): Promise { + return this.manufacturerService.updateManufacturerFrequency( + manufacturerId, + frequency, + ); + } +} diff --git a/apps/backend/src/foodManufacturers/manufacturer.entity.ts b/apps/backend/src/foodManufacturers/manufacturer.entity.ts index 895b84b8..bf3e615c 100644 --- a/apps/backend/src/foodManufacturers/manufacturer.entity.ts +++ b/apps/backend/src/foodManufacturers/manufacturer.entity.ts @@ -24,6 +24,23 @@ export class FoodManufacturer { }) foodManufacturerRepresentative: User; + @Column({ type: 'varchar', length: 255, nullable: true }) + industry: string; + + @Column({ type: 'varchar', length: 255, nullable: true }) + email: string; + + @Column({ type: 'varchar', length: 255, nullable: true }) + phone: string; + + @Column({ type: 'varchar', length: 255, nullable: true }) + address: string; + + @Column({ type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' }) + signupDate: Date; + + @Column({ type: 'varchar', length: 255, nullable: true }) + donationFrequency: string; @OneToMany(() => Donation, (donation) => donation.foodManufacturer) donations: Donation[]; } diff --git a/apps/backend/src/foodManufacturers/manufacturer.module.ts b/apps/backend/src/foodManufacturers/manufacturer.module.ts index 2ba2b117..21fef323 100644 --- a/apps/backend/src/foodManufacturers/manufacturer.module.ts +++ b/apps/backend/src/foodManufacturers/manufacturer.module.ts @@ -1,8 +1,14 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { FoodManufacturer } from './manufacturer.entity'; +import { ManufacturerController } from './manufacturer.controller'; +import { ManufacturerService } from './manufacturer.service'; +import { JwtStrategy } from '../auth/jwt.strategy'; +import { AuthService } from '../auth/auth.service'; @Module({ imports: [TypeOrmModule.forFeature([FoodManufacturer])], + controllers: [ManufacturerController], + providers: [ManufacturerService, AuthService, JwtStrategy], }) export class ManufacturerModule {} diff --git a/apps/backend/src/foodManufacturers/manufacturer.service.ts b/apps/backend/src/foodManufacturers/manufacturer.service.ts new file mode 100644 index 00000000..2f7abbcd --- /dev/null +++ b/apps/backend/src/foodManufacturers/manufacturer.service.ts @@ -0,0 +1,50 @@ +import { Injectable, NotFoundException } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { FoodManufacturer } from './manufacturer.entity'; + +@Injectable() +export class ManufacturerService { + constructor( + @InjectRepository(FoodManufacturer) + private repo: Repository, + ) {} + + async updateManufacturerFrequency( + manufacturerId: number, + donationFrequency: string, + ): Promise { + const manufacturer = await this.repo.findOne({ + where: { foodManufacturerId: manufacturerId }, + }); + if (!manufacturer) { + return null; + } + manufacturer.donationFrequency = donationFrequency; + return this.repo.save(manufacturer); + } + + async getDetails(manufacturerId: number): Promise { + if (!manufacturerId || manufacturerId < 1) { + throw new NotFoundException('Invalid manufacturer ID'); + } + return await this.repo.findOne({ + where: { foodManufacturerId: manufacturerId }, + relations: ['foodManufacturerRepresentative'], + select: { + foodManufacturerId: true, + foodManufacturerName: true, + industry: true, + email: true, + phone: true, + address: true, + signupDate: true, + donationFrequency: true, + foodManufacturerRepresentative: { + firstName: true, + lastName: true, + }, + }, + }); + } +} diff --git a/apps/backend/src/migrations/1743518493960-AddManufacturerDetails.ts b/apps/backend/src/migrations/1743518493960-AddManufacturerDetails.ts new file mode 100644 index 00000000..4168c0ac --- /dev/null +++ b/apps/backend/src/migrations/1743518493960-AddManufacturerDetails.ts @@ -0,0 +1,25 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddManufacturerDetails1743518493960 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + ALTER TABLE food_manufacturers + ADD COLUMN industry VARCHAR(255), + ADD COLUMN email VARCHAR(255), + ADD COLUMN phone VARCHAR(255), + ADD COLUMN address VARCHAR(255), + ADD COLUMN signup_date TIMESTAMP NOT NULL DEFAULT NOW(); + `); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + ALTER TABLE food_manufacturers + DROP COLUMN industry, + DROP COLUMN email, + DROP COLUMN phone, + DROP COLUMN address, + DROP COLUMN signup_date; + `); + } +} diff --git a/apps/backend/src/migrations/1743623272909-AddManufacturerDonationFrequency.ts b/apps/backend/src/migrations/1743623272909-AddManufacturerDonationFrequency.ts new file mode 100644 index 00000000..c32f5cbf --- /dev/null +++ b/apps/backend/src/migrations/1743623272909-AddManufacturerDonationFrequency.ts @@ -0,0 +1,19 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddManufacturerDonationFrequency1743623272909 + implements MigrationInterface +{ + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + ALTER TABLE food_manufacturers + ADD COLUMN donation_frequency VARCHAR(255); + `); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + ALTER TABLE food_manufacturers + DROP COLUMN donation_frequency; + `); + } +} diff --git a/apps/frontend/src/api/apiClient.ts b/apps/frontend/src/api/apiClient.ts index 9c9f6770..d6891c65 100644 --- a/apps/frontend/src/api/apiClient.ts +++ b/apps/frontend/src/api/apiClient.ts @@ -1,12 +1,13 @@ import axios, { type AxiosInstance, AxiosResponse } from 'axios'; import { + Donation, + DonationItem, User, Pantry, + ManufacturerDetails, Order, FoodRequest, FoodManufacturer, - DonationItem, - Donation, Allocation, PantryApplicationDto, VolunteerPantryAssignment, @@ -31,6 +32,51 @@ export class ApiClient { return this.axiosInstance.get(path).then((response) => response.data); } + public async getRepresentativeUser(userId: number): Promise { + return this.axiosInstance + .get(`/api/users/${userId}`) + .then((response) => response.data); + } + + public async getAllPendingPantries(): Promise { + return this.axiosInstance + .get('/api/pantries/pending') + .then((response) => response.data); + } + + public async getManufacturerDetails( + manufacturerId: number, + ): Promise { + return this.get( + `/api/manufacturer/getDetails/${manufacturerId}`, + ) as Promise; + } + + public async updatePantry( + pantryId: number, + decision: 'approve' | 'deny', + ): Promise { + await this.axiosInstance.post(`/api/pantries/${decision}/${pantryId}`, { + pantryId, + }); + } + + public async getPantry(pantryId: number): Promise { + return this.get(`/api/pantries/${pantryId}`) as Promise; + } + + public async getManufacturerDonationCount( + manufacturerId: number, + ): Promise { + return this.get( + `/api/donations/getManufacturerDonationCount/${manufacturerId}`, + ) as Promise; + } + + public async getPantrySSFRep(pantryId: number): Promise { + return this.get(`/api/pantries/${pantryId}/ssf-contact`) as Promise; + } + private async post(path: string, body: unknown): Promise { return this.axiosInstance .post(path, body) @@ -76,6 +122,17 @@ export class ApiClient { ) as Promise; } + public async updateDonationFrequency( + manufacturerId: number, + frequency: string, + body?: unknown, + ): Promise { + return this.patch( + `/api/manufacturer/updateFrequency/${manufacturerId}/${frequency}`, + body, + ) as Promise; + } + public async updateDonationItemQuantity( itemId: number, body?: unknown, diff --git a/apps/frontend/src/app.tsx b/apps/frontend/src/app.tsx index ee203fc8..3ce54975 100644 --- a/apps/frontend/src/app.tsx +++ b/apps/frontend/src/app.tsx @@ -19,6 +19,7 @@ import ApprovePantries from '@containers/approvePantries'; import VolunteerManagement from '@containers/volunteerManagement'; import FoodManufacturerOrderDashboard from '@containers/foodManufacturerOrderDashboard'; import DonationManagement from '@containers/donationManagement'; +import FoodManufacturerDashboard from '@containers/foodManufacturerDashboard'; import Homepage from '@containers/homepage'; const router = createBrowserRouter([ @@ -84,6 +85,10 @@ const router = createBrowserRouter([ path: '/approve-pantries', element: , }, + { + path: '/food-manufacturer-dashboard/:manufacturerId', + element: , + }, { path: '/volunteer-management', element: , diff --git a/apps/frontend/src/containers/foodManufacturerDashboard.tsx b/apps/frontend/src/containers/foodManufacturerDashboard.tsx new file mode 100644 index 00000000..786c53fb --- /dev/null +++ b/apps/frontend/src/containers/foodManufacturerDashboard.tsx @@ -0,0 +1,315 @@ +import { + Menu, + Button, + MenuButton, + MenuList, + MenuItem, + Link, + Image, + Card, + CardHeader, + CardBody, + Heading, + Text, + Box, + Select, + Stack, + HStack, + VStack, +} from '@chakra-ui/react'; +import { HamburgerIcon } from '@chakra-ui/icons'; +import React, { useEffect, useState } from 'react'; +import { useParams } from 'react-router-dom'; +import ApiClient from '@api/apiClient'; +import { ManufacturerDetails } from 'types/types'; + +const FoodManufacturerDashboard: React.FC = () => { + const { manufacturerId } = useParams<{ manufacturerId: string }>(); + const [manufacturerDetails, setManufacturerDetails] = + useState(); + const [currentSelectedFrequency, setCurrentSelectedFrequency] = + useState(); + const [manufacturerDonations, setManufacturerDonations] = useState(); + const [notFound, setNotFound] = useState(); + + useEffect(() => { + if (!manufacturerId) { + console.error('Error: manufacturerId is undefined'); + return; + } + + const fetchDetails = async () => { + try { + const response = await ApiClient.getManufacturerDetails( + parseInt(manufacturerId, 10), + ); + + if (!response) { + setNotFound(true); + return; + } + + if (response?.signupDate) { + response.signupDate = new Date(response.signupDate); + } + setManufacturerDetails(response); + setCurrentSelectedFrequency(response.donationFrequency); + + const numberDonations = await ApiClient.getManufacturerDonationCount( + parseInt(manufacturerId, 10), + ); + setManufacturerDonations(numberDonations); + } catch (error) { + console.error('Error fetching manufacturer details: ', error); + setNotFound(true); + } + }; + + fetchDetails(); + }, [manufacturerId]); + + const handleUpdate = async () => { + try { + if (manufacturerId && currentSelectedFrequency) { + await ApiClient.updateDonationFrequency( + parseInt(manufacturerId, 10), + currentSelectedFrequency, + ); + + setManufacturerDetails((prev) => + prev + ? { ...prev, donationFrequency: currentSelectedFrequency } + : prev, + ); + + alert('update frequency successful'); + } + } catch (error) { + console.error('Error updating manufacturer frequency: ', error); + } + }; + + const handleFrequencyChange = ( + event: React.ChangeEvent, + ) => { + setCurrentSelectedFrequency('' + event.target.value); + }; + + const HamburgerMenu = () => { + return ( + + + + + + + Profile + + + Donation Management + + + Orders + + + Donation Statistics + + + Sign Out + + + + ); + }; + + const ManufacturerCard = () => { + return ( + + + + Welcome to the Food Manufacturer Admin Dashboard + {manufacturerDetails?.foodManufacturerName && + ` - ${manufacturerDetails.foodManufacturerName}`} + + + + {manufacturerDetails && ( + + + + + + + )} + + ); + }; + + const ManufacturerDetailsBox = () => { + return ( + + + About Manufacturer {manufacturerDetails?.foodManufacturerName} + +
+ + + + + Assigned SSF Contact:{' '} + {manufacturerDetails?.foodManufacturerRepresentative.firstName}{' '} + {manufacturerDetails?.foodManufacturerRepresentative.lastName} + + + + + Pantry Partner since:{' '} + {manufacturerDetails?.signupDate.getFullYear().toString()} + + + + + + + Total Donations: {manufacturerDonations} + + + + Manufacturer Industry: {manufacturerDetails?.industry} + + + + + + + Email Address: {manufacturerDetails?.email} + + + Phone Number: {manufacturerDetails?.phone} + + + + + + + Address for Food Shipments: {manufacturerDetails?.address} + + + + +
+ ); + }; + + const UpdateFrequencyBox = () => { + return ( + + Update Frequency of Donations +
+

+ {' '} + Current Frequency:{' '} + {manufacturerDetails?.donationFrequency ?? 'not set'}{' '} +

+ + + New Frequency: + + + + +
+ ); + }; + + if (notFound) { + return ( + + + Manufacturer not found + + The manufacturer you are looking for does not exist. + + ); + } + + return ( + <> + + + + + Icon + + + + ); +}; + +export default FoodManufacturerDashboard; diff --git a/apps/frontend/src/types/types.ts b/apps/frontend/src/types/types.ts index 75cd3702..25de9dae 100644 --- a/apps/frontend/src/types/types.ts +++ b/apps/frontend/src/types/types.ts @@ -158,6 +158,21 @@ export interface Allocation { status: string; } +export interface ManufacturerDetails { + foodManufacturerId: number; + foodManufacturerName: string; + industry: string; + email: string; + phone: string; + address: string; + signupDate: Date; + donationFrequency: string; + foodManufacturerRepresentative: { + firstName: string; + lastName: string; + } +} + export enum VolunteerType { LEAD_VOLUNTEER = 'lead_volunteer', STANDARD_VOLUNTEER = 'standard_volunteer',