diff --git a/apps/backend/src/foodManufacturers/manufacturers.controller.spec.ts b/apps/backend/src/foodManufacturers/manufacturers.controller.spec.ts index 204d32bd3..bc86024a5 100644 --- a/apps/backend/src/foodManufacturers/manufacturers.controller.spec.ts +++ b/apps/backend/src/foodManufacturers/manufacturers.controller.spec.ts @@ -246,4 +246,23 @@ describe('FoodManufacturersController', () => { expect(mockManufacturersService.deny).toHaveBeenCalledWith(1); }); }); + + describe('getCurrentUserFoodManufacturerId', () => { + it('returns foodManufacturerId for authenticated user', async () => { + const req = { user: { id: 1 } }; + const manufacturer: Partial = { + foodManufacturerId: 10, + }; + mockManufacturersService.findByUserId.mockResolvedValueOnce( + manufacturer as FoodManufacturer, + ); + + const result = await controller.getCurrentUserFoodManufacturerId( + req as AuthenticatedRequest, + ); + + expect(result).toEqual(10); + expect(mockManufacturersService.findByUserId).toHaveBeenCalledWith(1); + }); + }); }); 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.spec.ts b/apps/backend/src/foodManufacturers/manufacturers.service.spec.ts index b972887ea..cd823bae3 100644 --- a/apps/backend/src/foodManufacturers/manufacturers.service.spec.ts +++ b/apps/backend/src/foodManufacturers/manufacturers.service.spec.ts @@ -528,6 +528,21 @@ describe('FoodManufacturersService', () => { }); }); + describe('findByUserId', () => { + it('findByUserId success', async () => { + const manufacturer = await service.findOne(1); + const userId = manufacturer.foodManufacturerRepresentative.id; + const result = await service.findByUserId(userId); + expect(result.foodManufacturerId).toBe(1); + }); + + it('findByUserId with non-existent user throws NotFoundException', async () => { + await expect(service.findByUserId(9999)).rejects.toThrow( + new NotFoundException('Food Manufacturer for User 9999 not found'), + ); + }); + }); + describe('getStats', () => { it('returns proper stats for manufacturer', async () => { const manufacturerId = 1; 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 ? ( +