From 6c6a4536f3fce809fe5f179ecb08689d90007840 Mon Sep 17 00:00:00 2001 From: Dalton Burkhart Date: Thu, 9 Apr 2026 01:05:48 -0400 Subject: [PATCH 01/16] Implementation for both pages --- .../manufacturers.controller.ts | 11 + .../manufacturers.service.ts | 15 + .../dtos/update-pantry-application.dto.ts | 14 + apps/frontend/src/api/apiClient.ts | 25 + .../forms/editableFMApplication.tsx | 818 ++++++++++++ .../forms/editablePantryApplication.tsx | 1102 +++++++++++++++++ .../forms/pantryApplicationForm.tsx | 6 +- .../components/forms/profileAccountInfo.tsx | 43 +- .../src/components/forms/tagGroup.tsx | 3 +- apps/frontend/src/types/types.ts | 54 + 10 files changed, 2079 insertions(+), 12 deletions(-) create mode 100644 apps/frontend/src/components/forms/editableFMApplication.tsx create mode 100644 apps/frontend/src/components/forms/editablePantryApplication.tsx diff --git a/apps/backend/src/foodManufacturers/manufacturers.controller.ts b/apps/backend/src/foodManufacturers/manufacturers.controller.ts index 198fcf412..8f2b352f3 100644 --- a/apps/backend/src/foodManufacturers/manufacturers.controller.ts +++ b/apps/backend/src/foodManufacturers/manufacturers.controller.ts @@ -30,6 +30,17 @@ export class FoodManufacturersController { return this.foodManufacturersService.getPendingManufacturers(); } + @Roles(Role.FOODMANUFACTURER) + @Get('/my-id') + async getCurrentUserFoodManufacturerId( + @Req() req: AuthenticatedRequest, + ): Promise { + const manufacturer = await this.foodManufacturersService.findByUserId( + req.user.id, + ); + return manufacturer.foodManufacturerId; + } + @Get('/:foodManufacturerId') async getFoodManufacturer( @Param('foodManufacturerId', ParseIntPipe) foodManufacturerId: number, diff --git a/apps/backend/src/foodManufacturers/manufacturers.service.ts b/apps/backend/src/foodManufacturers/manufacturers.service.ts index ed93e6866..0c4c4b5b5 100644 --- a/apps/backend/src/foodManufacturers/manufacturers.service.ts +++ b/apps/backend/src/foodManufacturers/manufacturers.service.ts @@ -57,6 +57,21 @@ export class FoodManufacturersService { return foodManufacturer; } + async findByUserId(userId: number): Promise { + validateId(userId, 'User'); + + const manufacturer = await this.repo.findOne({ + where: { foodManufacturerRepresentative: { id: userId } }, + }); + + if (!manufacturer) { + throw new NotFoundException( + `Food Manufacturer for User ${userId} not found`, + ); + } + return manufacturer; + } + async getFMDonations( foodManufacturerId: number, currentUserId: number, diff --git a/apps/backend/src/pantries/dtos/update-pantry-application.dto.ts b/apps/backend/src/pantries/dtos/update-pantry-application.dto.ts index efb1d9f6b..5dceb2a47 100644 --- a/apps/backend/src/pantries/dtos/update-pantry-application.dto.ts +++ b/apps/backend/src/pantries/dtos/update-pantry-application.dto.ts @@ -132,6 +132,15 @@ export class UpdatePantryApplicationDto { @MaxLength(255, { each: true }) restrictions?: string[]; + @IsBoolean() + @IsOptional() + acceptFoodDeliveries?: boolean; + + @IsOptional() + @IsString() + @IsNotEmpty() + deliveryWindowInstructions?: string; + @IsEnum(RefrigeratedDonation) @IsOptional() refrigeratedDonation?: RefrigeratedDonation; @@ -140,6 +149,11 @@ export class UpdatePantryApplicationDto { @IsOptional() reserveFoodForAllergic?: ReserveFoodForAllergic; + @IsOptional() + @IsString() + @IsNotEmpty() + reservationExplanation?: string | null; + @IsBoolean() @IsOptional() dedicatedAllergyFriendly?: boolean; diff --git a/apps/frontend/src/api/apiClient.ts b/apps/frontend/src/api/apiClient.ts index 127d77c1c..011e71ca3 100644 --- a/apps/frontend/src/api/apiClient.ts +++ b/apps/frontend/src/api/apiClient.ts @@ -29,6 +29,8 @@ import { PantryWithUser, Assignments, UpdateProfileFields, + UpdatePantryApplicationDto, + UpdateFoodManufacturerApplicationDto, } from 'types/types'; const defaultBaseUrl = @@ -330,6 +332,15 @@ export class ApiClient { }); } + public async updatePantryApplication( + pantryId: number, + data: UpdatePantryApplicationDto, + ): Promise { + return this.axiosInstance + .patch(`/api/pantries/${pantryId}/update`, data) + .then((response) => response.data); + } + public async updatePantry( pantryId: number, decision: 'approve' | 'deny', @@ -366,6 +377,20 @@ export class ApiClient { return data as number; } + public async getCurrentUserFoodManufacturerId(): Promise { + const data = await this.get('/api/manufacturers/my-id'); + return data as number; + } + + public async updateFoodManufacturerApplicationData( + manufacturerId: number, + data: UpdateFoodManufacturerApplicationDto, + ): Promise { + return this.axiosInstance + .patch(`/api/manufacturers/${manufacturerId}/application`, data) + .then((response) => response.data); + } + public async getMe(): Promise { const data = await this.get('/api/users/me'); return data as User; diff --git a/apps/frontend/src/components/forms/editableFMApplication.tsx b/apps/frontend/src/components/forms/editableFMApplication.tsx new file mode 100644 index 000000000..178495f41 --- /dev/null +++ b/apps/frontend/src/components/forms/editableFMApplication.tsx @@ -0,0 +1,818 @@ +import React, { useCallback, useEffect, useState } from 'react'; +import { + Box, + Grid, + Text, + HStack, + VStack, + Center, + Input, + Button, + Textarea, + RadioGroup, + Stack, + NativeSelect, + NativeSelectIndicator, + Menu, +} from '@chakra-ui/react'; +import { ChevronDownIcon } from 'lucide-react'; +import ApiClient from '@api/apiClient'; +import { + FoodManufacturer, + UpdateFoodManufacturerApplicationDto, +} from '../../types/types'; +import { + Allergen, + DonateWastedFood, + ManufacturerAttribute, +} from '../../types/manufacturerEnums'; +import { formatPhone } from '@utils/utils'; +import { TagGroup } from '@components/forms/tagGroup'; +import { USPhoneInput } from '@components/forms/usPhoneInput'; + +// --------------------------------------------------------------------------- +// Style constants +// --------------------------------------------------------------------------- + +const fieldHeaderStyles = { + fontSize: '14px', + color: 'neutral.800' as const, + fontWeight: 600, + mb: 1, +}; + +const fieldContentStyles = { + fontSize: '14px', + color: 'neutral.800' as const, +}; + +const sectionLabelStyles = { + fontSize: '16px', + fontWeight: 600, + fontFamily: 'inter', + color: 'neutral.800' as const, + mb: 8, +}; + +const inputStyles = { + borderColor: 'neutral.100' as const, + color: 'neutral.600' as const, + size: 'sm' as const, +}; + +const allergenOptions = Object.values(Allergen); +const donateWastedFoodOptions = Object.values(DonateWastedFood); +const manufacturerAttributeOptions = Object.values(ManufacturerAttribute); + +// --------------------------------------------------------------------------- +// Read-only sub-components +// --------------------------------------------------------------------------- + +interface SectionProps { + title: string; + children: React.ReactNode; +} +const Section: React.FC = ({ title, children }) => ( + + {title} + {children} + +); + +interface FieldProps { + label: string; + value?: string | null; + fallback?: string; +} +const Field: React.FC = ({ label, value, fallback = '-' }) => ( + + {label} + {value || fallback} + +); + +// Edit Mode Subcomponents +interface EditFieldProps { + label: string; + name: string; + value: string; + onChange: (v: string) => void; + textarea?: boolean; + helperText?: string; + required?: boolean; +} +const EditField: React.FC = ({ + label, + name, + value, + onChange, + textarea, + helperText, + required, +}) => ( + + + {label} + {required && ( + + * + + )} + + {textarea ? ( +