diff --git a/apps/backend/src/foodManufacturers/manufacturers.controller.spec.ts b/apps/backend/src/foodManufacturers/manufacturers.controller.spec.ts index 6fbb043d6..d9403a753 100644 --- a/apps/backend/src/foodManufacturers/manufacturers.controller.spec.ts +++ b/apps/backend/src/foodManufacturers/manufacturers.controller.spec.ts @@ -10,6 +10,7 @@ import { Donation } from '../donations/donations.entity'; import { UpdateFoodManufacturerApplicationDto } from './dtos/update-manufacturer-application.dto'; import { NotFoundException } from '@nestjs/common'; import { AuthenticatedRequest } from '../auth/authenticated-request'; +import { Role } from '../users/types'; import { DonationDetailsDto, DonationReminderDto, @@ -78,6 +79,37 @@ describe('FoodManufacturersController', () => { }); }); + describe('GET /approved', () => { + it('should return approved food manufacturers', async () => { + const mockManufacturers: Partial[] = [ + { + foodManufacturerId: 1, + foodManufacturerName: 'Good Foods Inc', + status: ApplicationStatus.APPROVED, + }, + { + foodManufacturerId: 2, + foodManufacturerName: 'Healthy Eats LLC', + status: ApplicationStatus.APPROVED, + }, + ]; + + mockManufacturersService.getApprovedManufacturers.mockResolvedValue( + mockManufacturers as FoodManufacturer[], + ); + + const result = await controller.getApprovedManufacturers(); + + expect(result).toEqual(mockManufacturers); + expect(result).toHaveLength(2); + expect(result[0].foodManufacturerId).toBe(1); + expect(result[1].foodManufacturerId).toBe(2); + expect( + mockManufacturersService.getApprovedManufacturers, + ).toHaveBeenCalled(); + }); + }); + describe('GET /:id', () => { it('should return a food manufacturer by id', async () => { mockManufacturersService.findOne.mockResolvedValue( @@ -224,7 +256,7 @@ describe('FoodManufacturersController', () => { }); describe('PATCH /:manufacturerId/application', () => { - const req = { user: { id: 1 } }; + const req = { user: { id: 1, role: Role.FOODMANUFACTURER } }; it('should update a food manufacturer application', async () => { const manufacturerId = 1; @@ -251,7 +283,12 @@ describe('FoodManufacturersController', () => { expect(result).toEqual(mockManufacturer1); expect( mockManufacturersService.updateFoodManufacturerApplication, - ).toHaveBeenCalledWith(manufacturerId, mockUpdateData, 1); + ).toHaveBeenCalledWith( + manufacturerId, + mockUpdateData, + 1, + Role.FOODMANUFACTURER, + ); }); it('should throw error if manufacturer does not exist', async () => { @@ -272,7 +309,31 @@ describe('FoodManufacturersController', () => { ).rejects.toThrow(); expect( mockManufacturersService.updateFoodManufacturerApplication, - ).toHaveBeenCalledWith(999, mockUpdateData, 1); + ).toHaveBeenCalledWith(999, mockUpdateData, 1, Role.FOODMANUFACTURER); + }); + + it('should allow admin to update any food manufacturer application', async () => { + const adminReq = { user: { id: 2, role: Role.ADMIN } }; + const manufacturerId = 1; + + const mockUpdateData: UpdateFoodManufacturerApplicationDto = { + secondaryContactFirstName: 'Admin Updated', + }; + + mockManufacturersService.updateFoodManufacturerApplication.mockResolvedValue( + mockManufacturer1 as FoodManufacturer, + ); + + const result = await controller.updateFoodManufacturerApplication( + adminReq as AuthenticatedRequest, + manufacturerId, + mockUpdateData, + ); + + expect(result).toEqual(mockManufacturer1); + expect( + mockManufacturersService.updateFoodManufacturerApplication, + ).toHaveBeenCalledWith(manufacturerId, mockUpdateData, 2, Role.ADMIN); }); }); diff --git a/apps/backend/src/foodManufacturers/manufacturers.controller.ts b/apps/backend/src/foodManufacturers/manufacturers.controller.ts index 5f2a7eafc..bca2bfe70 100644 --- a/apps/backend/src/foodManufacturers/manufacturers.controller.ts +++ b/apps/backend/src/foodManufacturers/manufacturers.controller.ts @@ -50,6 +50,12 @@ export class FoodManufacturersController { return this.foodManufacturersService.getPendingManufacturers(); } + @Roles(Role.ADMIN) + @Get('/approved') + async getApprovedManufacturers(): Promise { + return this.foodManufacturersService.getApprovedManufacturers(); + } + @Roles(Role.FOODMANUFACTURER) @Get('/my-id') async getCurrentUserFoodManufacturerId( @@ -216,7 +222,7 @@ export class FoodManufacturersController { ); } - @Roles(Role.FOODMANUFACTURER) + @Roles(Role.FOODMANUFACTURER, Role.ADMIN) @CheckOwnership({ idParam: 'foodManufacturerId', resolver: resolveFoodManufacturerAuthorizedUserIds, @@ -232,6 +238,7 @@ export class FoodManufacturersController { foodManufacturerId, foodManufacturerData, req.user.id, + req.user.role, ); } diff --git a/apps/backend/src/foodManufacturers/manufacturers.service.spec.ts b/apps/backend/src/foodManufacturers/manufacturers.service.spec.ts index 0c2ea0314..2dc3e5e99 100644 --- a/apps/backend/src/foodManufacturers/manufacturers.service.spec.ts +++ b/apps/backend/src/foodManufacturers/manufacturers.service.spec.ts @@ -28,6 +28,7 @@ import { FoodType } from '../donationItems/types'; import { DonationService } from '../donations/donations.service'; import { PantriesService } from '../pantries/pantries.service'; import { Pantry } from '../pantries/pantries.entity'; +import { Role } from '../users/types'; import { Allocation } from '../allocations/allocations.entity'; import { RecurrenceEnum } from '../donations/types'; import { UpdateFoodManufacturerApplicationDto } from './dtos/update-manufacturer-application.dto'; @@ -174,6 +175,24 @@ describe('FoodManufacturersService', () => { }); }); + describe('getApprovedManufacturers', () => { + it('returns manufacturers with approved status', async () => { + const approved = await service.getApprovedManufacturers(); + expect(approved.length).toBeGreaterThan(0); + expect( + approved.every((m) => m.status === ApplicationStatus.APPROVED), + ).toBe(true); + }); + + it('returns empty array when no approved manufacturers exist', async () => { + await testDataSource.query( + `UPDATE food_manufacturers SET status = 'pending' WHERE status = 'approved'`, + ); + const approved = await service.getApprovedManufacturers(); + expect(approved).toEqual([]); + }); + }); + describe('approve', () => { it('approves a pending manufacturer', async () => { const pending = await service.getPendingManufacturers(); @@ -962,5 +981,45 @@ describe('FoodManufacturersService', () => { ), ); }); + + it('allows admin to update any approved manufacturer (bypassing ownership check)', async () => { + const dto: UpdateFoodManufacturerApplicationDto = { + secondaryContactFirstName: 'AdminUpdated', + foodManufacturerName: 'Admin Edited Foods', + }; + + // User 999 is not the representative of manufacturer 1, but as admin should be able to edit + const adminUserId = 999; + const updated = await service.updateFoodManufacturerApplication( + 1, + dto, + adminUserId, + Role.ADMIN, + ); + + expect(updated.secondaryContactFirstName).toBe('AdminUpdated'); + expect(updated.foodManufacturerName).toBe('Admin Edited Foods'); + }); + + it('still throws ForbiddenException for non-admin non-owner users', async () => { + const dto: UpdateFoodManufacturerApplicationDto = { + foodManufacturerName: 'Should not work', + }; + + const invalidUserId = 999; + + await expect( + service.updateFoodManufacturerApplication( + 1, + dto, + invalidUserId, + Role.FOODMANUFACTURER, + ), + ).rejects.toThrow( + new ForbiddenException( + `User ${invalidUserId} is not allowed to edit application for Food Manufacturer 1`, + ), + ); + }); }); }); diff --git a/apps/backend/src/foodManufacturers/manufacturers.service.ts b/apps/backend/src/foodManufacturers/manufacturers.service.ts index 845be4f8a..aba6fa089 100644 --- a/apps/backend/src/foodManufacturers/manufacturers.service.ts +++ b/apps/backend/src/foodManufacturers/manufacturers.service.ts @@ -240,6 +240,13 @@ export class FoodManufacturersService { }); } + async getApprovedManufacturers(): Promise { + return this.repo.find({ + where: { status: ApplicationStatus.APPROVED }, + relations: ['foodManufacturerRepresentative'], + }); + } + async addFoodManufacturer( foodManufacturerData: FoodManufacturerApplicationDto, ) { @@ -320,6 +327,7 @@ export class FoodManufacturersService { manufacturerId: number, foodManufacturerData: UpdateFoodManufacturerApplicationDto, currentUserId: number, + currentUserRole?: Role, ): Promise { validateId(manufacturerId, 'Food Manufacturer'); validateId(currentUserId, 'User'); @@ -335,7 +343,10 @@ export class FoodManufacturersService { ); } - if (manufacturer.foodManufacturerRepresentative.id !== currentUserId) { + if ( + currentUserRole !== Role.ADMIN && + manufacturer.foodManufacturerRepresentative.id !== currentUserId + ) { throw new ForbiddenException( `User ${currentUserId} is not allowed to edit application for Food Manufacturer ${manufacturerId}`, ); diff --git a/apps/frontend/src/api/apiClient.ts b/apps/frontend/src/api/apiClient.ts index 2f09a76c8..aef05f5e0 100644 --- a/apps/frontend/src/api/apiClient.ts +++ b/apps/frontend/src/api/apiClient.ts @@ -156,6 +156,12 @@ export class ApiClient { .then((response) => response.data); } + public async getApprovedFoodManufacturers(): Promise { + return this.axiosInstance + .get('/api/manufacturers/approved') + .then((response) => response.data); + } + public async getFoodManufacturer( manufacturerId: number, ): Promise { diff --git a/apps/frontend/src/app.tsx b/apps/frontend/src/app.tsx index 614df457a..063cffa7d 100644 --- a/apps/frontend/src/app.tsx +++ b/apps/frontend/src/app.tsx @@ -32,6 +32,7 @@ import AdminDonationStats from '@containers/adminDonationStats'; import ProfilePage from '@containers/profilePage'; import VolunteerOrderManagement from '@containers/volunteerOrderManagement'; import AdminPantryManagement from '@containers/adminPantryManagement'; +import AdminFoodManufacturerManagement from '@containers/adminFoodManufacturerManagement'; import AdminRequestManagement from '@containers/adminRequestManagement'; import PantryDashboard from '@containers/pantryDashboard'; import VolunteerDashboard from '@containers/volunteerDashboard'; @@ -161,6 +162,14 @@ const router = createBrowserRouter([ ), }, + { + path: ROUTES.FOOD_MANUFACTURER_MANAGEMENT_DETAILS, + element: ( + + + + ), + }, { path: ROUTES.ADMIN_DONATION, element: ( @@ -257,6 +266,14 @@ const router = createBrowserRouter([ ), }, + { + path: ROUTES.FOOD_MANUFACTURER_MANAGEMENT, + element: ( + + + + ), + }, ], }, ]); diff --git a/apps/frontend/src/components/Navbar.tsx b/apps/frontend/src/components/Navbar.tsx index dc3eb3101..1c0f964c8 100644 --- a/apps/frontend/src/components/Navbar.tsx +++ b/apps/frontend/src/components/Navbar.tsx @@ -54,6 +54,10 @@ const ROLE_NAV_SECTIONS: Record = { type: 'group', label: 'Manufacturers', children: [ + { + label: 'Food Manufacturer Management', + to: ROUTES.FOOD_MANUFACTURER_MANAGEMENT, + }, { label: 'Donation Management', to: ROUTES.ADMIN_DONATION }, { label: 'Application Review', to: ROUTES.APPROVE_FOOD_MANUFACTURERS }, { label: 'Donation Statistics', to: ROUTES.ADMIN_DONATION_STATS }, diff --git a/apps/frontend/src/containers/adminFoodManufacturerManagement.tsx b/apps/frontend/src/containers/adminFoodManufacturerManagement.tsx new file mode 100644 index 000000000..3ec7b2333 --- /dev/null +++ b/apps/frontend/src/containers/adminFoodManufacturerManagement.tsx @@ -0,0 +1,351 @@ +import { useEffect, useState } from 'react'; +import { + Table, + Text, + Flex, + Input, + VStack, + Box, + Pagination, + ButtonGroup, + IconButton, + Link, + Button, + Checkbox, + Badge, +} from '@chakra-ui/react'; +import { ChevronRight, ChevronLeft, Funnel, Search } from 'lucide-react'; +import { AlertStatus, FoodManufacturer } from '../types/types'; +import { DonateWastedFood } from '../types/manufacturerEnums'; +import ApiClient from '@api/apiClient'; +import { FloatingAlert } from '@components/floatingAlert'; +import { useAlert } from '../hooks/alert'; +import { useNavigate } from 'react-router-dom'; +import { ROUTES } from '../routes'; + +const AdminFoodManufacturerManagement: React.FC = () => { + const navigate = useNavigate(); + + const [currentPage, setCurrentPage] = useState(1); + const [foodManufacturers, setFoodManufacturers] = useState< + FoodManufacturer[] + >([]); + + const [searchFM, setSearchFM] = useState(''); + const [selectedFMs, setSelectedFMs] = useState([]); + + const [alertState, setAlertMessage] = useAlert(); + const [isFilterOpen, setIsFilterOpen] = useState(false); + + const pageSize = 10; + + const fetchFoodManufacturers = async () => { + try { + const approved = await ApiClient.getApprovedFoodManufacturers(); + setFoodManufacturers(approved); + } catch { + setAlertMessage('Error fetching food manufacturers', AlertStatus.ERROR); + } + }; + + useEffect(() => { + fetchFoodManufacturers(); + }, [setAlertMessage]); + + useEffect(() => { + setCurrentPage(1); + }, [selectedFMs]); + + const fmOptions = [ + ...new Set(foodManufacturers.map((fm) => fm.foodManufacturerName)), + ].sort((a, b) => a.localeCompare(b)); + + const handleFilterChange = (fm: string, checked: boolean) => { + if (checked) { + setSelectedFMs([...selectedFMs, fm]); + } else { + setSelectedFMs(selectedFMs.filter((f) => f !== fm)); + } + }; + + const filteredFMs = foodManufacturers.filter((fm) => { + const matchesFilter = + selectedFMs.length === 0 || selectedFMs.includes(fm.foodManufacturerName); + return matchesFilter; + }); + + const paginatedFMs = filteredFMs.slice( + (currentPage - 1) * pageSize, + currentPage * pageSize, + ); + + const textHeaderStyles = { + color: 'neutral.800', + textStyle: 'p2', + fontWeight: '600', + fontFamily: 'inter', + }; + + return ( + + + Manufacturer Management + + {alertState && ( + + )} + + + + + + {isFilterOpen && ( + <> + setIsFilterOpen(false)} + zIndex={10} + /> + + + + setSearchFM(e.target.value)} + fontSize="sm" + pl="30px" + border="none" + bg="transparent" + _focus={{ + boxShadow: 'none', + border: 'none', + outline: 'none', + }} + /> + + + {fmOptions + .filter((fm) => + fm.toLowerCase().includes(searchFM.toLowerCase()), + ) + .map((fm) => ( + + handleFilterChange(fm, e.checked) + } + color="gray.dark" + size="md" + > + + + {fm} + + ))} + + + + )} + + + + + + + Food Manufacturer + + + Food Rescue + + + Action + + + + + {paginatedFMs?.map((fm) => ( + + + + navigate( + ROUTES.FOOD_MANUFACTURER_MANAGEMENT_DETAILS.replace( + ':foodManufacturerId', + fm.foodManufacturerId.toString(), + ), + ) + } + > + {fm.foodManufacturerName} + + + + + {fm.donateWastedFood === DonateWastedFood.ALWAYS + ? 'Yes' + : fm.donateWastedFood === DonateWastedFood.SOMETIMES + ? 'Sometimes' + : 'No'} + + + + navigate(ROUTES.ADMIN_DONATION)} + > + View Donations + + + + ))} + + + + setCurrentPage(page)} + > + + + + setCurrentPage((prev) => Math.max(prev - 1, 1)) + } + > + + + + + ( + setCurrentPage(page.value)} + > + {page.value} + + )} + /> + + + + setCurrentPage((prev) => + Math.min( + prev + 1, + Math.ceil(filteredFMs.length / pageSize), + ), + ) + } + > + + + + + + + + + ); +}; + +export default AdminFoodManufacturerManagement; diff --git a/apps/frontend/src/containers/foodManufacturerApplicationDetails.tsx b/apps/frontend/src/containers/foodManufacturerApplicationDetails.tsx index 180684da4..7a28808ef 100644 --- a/apps/frontend/src/containers/foodManufacturerApplicationDetails.tsx +++ b/apps/frontend/src/containers/foodManufacturerApplicationDetails.tsx @@ -1,5 +1,5 @@ import React, { useCallback, useEffect, useState } from 'react'; -import { useParams, useNavigate, Link } from 'react-router-dom'; +import { useParams, useNavigate, useMatch, Link } from 'react-router-dom'; import { Box, Grid, @@ -19,9 +19,10 @@ import { } from '../types/types'; import { formatDate, formatPhone } from '@utils/utils'; import { TagGroup } from '@components/forms/tagGroup'; -import { TriangleAlert } from 'lucide-react'; +import { Pencil, TriangleAlert } from 'lucide-react'; import { AxiosError } from 'axios'; import { FloatingAlert } from '@components/floatingAlert'; +import EditableFMApplication from '@components/forms/editableFMApplication'; import { useAlert } from '../hooks/alert'; import ConfirmFoodManufacturerDecisionModal from '@components/forms/confirmFoodManufacturerDecisionModal'; import { ROUTES } from '../routes'; @@ -90,13 +91,21 @@ const EmptyState: React.FC = ({ }; const FoodManufacturerApplicationDetails: React.FC = () => { - const { applicationId } = useParams<{ applicationId: string }>(); + const { applicationId, foodManufacturerId } = useParams<{ + applicationId?: string; + foodManufacturerId?: string; + }>(); + const id = applicationId ?? foodManufacturerId; + const isApplicationMode = useMatch( + ROUTES.FOOD_MANUFACTURER_APPLICATION_DETAILS, + ); const navigate = useNavigate(); const [application, setApplication] = useState(null); const [loading, setLoading] = useState(true); const [alertState, setAlertMessage] = useAlert(); const [showApproveModal, setShowApproveModal] = useState(false); const [showDenyModal, setShowDenyModal] = useState(false); + const [isEditing, setIsEditing] = useState(false); const fieldContentStyles = { textStyle: 'p2', @@ -123,15 +132,13 @@ const FoodManufacturerApplicationDetails: React.FC = () => { const fetchApplicationDetails = useCallback(async () => { try { setLoading(true); - if (!applicationId) { + if (!id) { setAlertMessage('Application ID not provided.', AlertStatus.ERROR); return; - } else if (isNaN(parseInt(applicationId, 10))) { + } else if (isNaN(parseInt(id, 10))) { setAlertMessage('Application ID is not a number.', AlertStatus.ERROR); } - const data = await ApiClient.getFoodManufacturer( - parseInt(applicationId, 10), - ); + const data = await ApiClient.getFoodManufacturer(parseInt(id, 10)); if (!data) { setAlertMessage('Application not found.', AlertStatus.ERROR); } @@ -148,7 +155,7 @@ const FoodManufacturerApplicationDetails: React.FC = () => { } finally { setLoading(false); } - }, [applicationId]); + }, [id]); useEffect(() => { fetchApplicationDetails(); @@ -224,7 +231,9 @@ const FoodManufacturerApplicationDetails: React.FC = () => { - Application Details + {isApplicationMode + ? 'Application Details' + : 'Food Manufacturer Details'} {alertState && ( @@ -247,181 +256,228 @@ const FoodManufacturerApplicationDetails: React.FC = () => { {/* Header */} - - Application #{application.foodManufacturerId} - - - {application.foodManufacturerName} - - - Applied {formatDate(application.dateApplied)} - - - - {/* Point of Contact Information */} - - - Point of Contact Information - - - - Primary Representative - - {rep.firstName} {rep.lastName} + {isApplicationMode ? ( + <> + + Application #{application.foodManufacturerId} + + + {application.foodManufacturerName} + + + Applied {formatDate(application.dateApplied)} - {formatPhone(rep.phone)} - {rep.email} - + + ) : ( + + + {application.foodManufacturerName} + + setIsEditing(true)} + > + + + {isEditing ? 'Editing' : 'Edit'} + + + + )} + - {hasSecondaryContact && ( - - Secondary Contact - {(application.secondaryContactFirstName || - application.secondaryContactLastName) && ( - - {application.secondaryContactFirstName}{' '} - {application.secondaryContactLastName} - - )} - {application.secondaryContactPhone && ( + {!isApplicationMode && isEditing ? ( + { + setIsEditing(editing); + if (!editing) { + fetchApplicationDetails(); + } + }} + foodManufacturerId={application.foodManufacturerId} + /> + ) : ( + <> + {/* Point of Contact Information */} + + + Point of Contact Information + + + + Primary Representative - {formatPhone(application.secondaryContactPhone)} + {rep.firstName} {rep.lastName} - )} - {application.secondaryContactEmail && ( - {application.secondaryContactEmail} + {formatPhone(rep.phone)} - )} - - )} - - + {rep.email} + - {/* Food Manufacturer Details */} - - - Food Manufacturer Details - - - - - Name - - {application.foodManufacturerName} - - - - Website - - {application.foodManufacturerWebsite} - - - - - - Unlisted Product Allergens - {application.unlistedProductAllergens.length > 0 ? ( - - ) : ( - None - )} + {hasSecondaryContact && ( + + Secondary Contact + {(application.secondaryContactFirstName || + application.secondaryContactLastName) && ( + + {application.secondaryContactFirstName}{' '} + {application.secondaryContactLastName} + + )} + {application.secondaryContactPhone && ( + + {formatPhone(application.secondaryContactPhone)} + + )} + {application.secondaryContactEmail && ( + + {application.secondaryContactEmail} + + )} + + )} + + {/* Food Manufacturer Details */} - - Allergens Facility is Free Of - - {application.facilityFreeAllergens.length > 0 ? ( - - ) : ( - None - )} - + + Food Manufacturer Details + + + + + Name + + {application.foodManufacturerName} + + + + Website + + {application.foodManufacturerWebsite} + + + - - - - Products are Gluten-Free? - - - {application.productsGlutenFree ? 'Yes' : 'No'} - - - - In-Kind Donations? - - {application.inKindDonations ? 'Yes' : 'No'} - - - - Donate Wasted Food? - - {application.donateWastedFood} - - - + + + Unlisted Product Allergens + + {application.unlistedProductAllergens.length > 0 ? ( + + ) : ( + None + )} + - - - Sustainable Products Explanation - - - {application.productsSustainableExplanation || '-'} - - + + + Allergens Facility is Free Of + + {application.facilityFreeAllergens.length > 0 ? ( + + ) : ( + None + )} + - - Additional Comments - - {application.additionalComments || '-'} - + + + + Products are Gluten-Free? + + + {application.productsGlutenFree ? 'Yes' : 'No'} + + + + In-Kind Donations? + + {application.inKindDonations ? 'Yes' : 'No'} + + + + Donate Wasted Food? + + {application.donateWastedFood} + + + + + + + Sustainable Products Explanation + + + {application.productsSustainableExplanation || '-'} + + + + + Additional Comments + + {application.additionalComments || '-'} + + + - - + + )} - - - + {isApplicationMode && ( + + + - setShowApproveModal(false)} - onConfirm={handleApprove} - decision="approve" - foodManufacturerName={application.foodManufacturerName} - dateApplied={formatDate(application.dateApplied)} - /> + setShowApproveModal(false)} + onConfirm={handleApprove} + decision="approve" + foodManufacturerName={application.foodManufacturerName} + dateApplied={formatDate(application.dateApplied)} + /> - setShowDenyModal(false)} - onConfirm={handleDeny} - decision="deny" - foodManufacturerName={application.foodManufacturerName} - dateApplied={formatDate(application.dateApplied)} - /> - + setShowDenyModal(false)} + onConfirm={handleDeny} + decision="deny" + foodManufacturerName={application.foodManufacturerName} + dateApplied={formatDate(application.dateApplied)} + /> + + )} diff --git a/apps/frontend/src/routes.ts b/apps/frontend/src/routes.ts index ac42813db..25a92402c 100644 --- a/apps/frontend/src/routes.ts +++ b/apps/frontend/src/routes.ts @@ -16,11 +16,14 @@ export const ROUTES = { PANTRY_MANAGEMENT_DETAILS: '/pantry-details/pantry/:pantryId', FOOD_MANUFACTURER_APPLICATION_DETAILS: '/food-manufacturer-application-details/:applicationId', + FOOD_MANUFACTURER_MANAGEMENT_DETAILS: + '/food-manufacturer-details/manufacturer/:foodManufacturerId', APPROVE_PANTRIES: '/approve-pantries', APPROVE_FOOD_MANUFACTURERS: '/approve-food-manufacturers', VOLUNTEER_MANAGEMENT: '/volunteer-management', PANTRY_MANAGEMENT: '/pantry-management', + FOOD_MANUFACTURER_MANAGEMENT: '/food-manufacturer-management', ADMIN_ORDER_MANAGEMENT: '/admin-order-management', ADMIN_DONATION: '/admin-donation', ADMIN_DONATION_STATS: '/admin-donation-stats',