Skip to content
3 changes: 2 additions & 1 deletion apps/backend/src/foodRequests/request.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import {
export class RequestsController {
constructor(private requestsService: RequestsService) {}

@Roles(Role.PANTRY, Role.ADMIN)
@Roles(Role.PANTRY, Role.ADMIN, Role.VOLUNTEER)
@Get('/:requestId')
async getRequest(
@Param('requestId', ParseIntPipe) requestId: number,
Expand All @@ -41,6 +41,7 @@ export class RequestsController {
return this.requestsService.find(pantryId);
}

@Roles(Role.VOLUNTEER, Role.PANTRY, Role.ADMIN)
@Get('/:requestId/order-details')
async getAllOrderDetailsFromRequest(
@Param('requestId', ParseIntPipe) requestId: number,
Expand Down
3 changes: 2 additions & 1 deletion apps/backend/src/foodRequests/request.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ describe('RequestsService', () => {
});

describe('find', () => {
it('should return all food requests for a specific pantry', async () => {
it('should return all food requests for a specific pantry with pantry details', async () => {
const pantryId = 1;
const result = await service.find(pantryId);

Expand All @@ -206,6 +206,7 @@ describe('RequestsService', () => {
result.forEach((request) => {
expect(request.orders).toBeDefined();
});
expect(result.every((r) => r.pantry)).toBeDefined();
});

it('should return empty array for pantry with no requests', async () => {
Expand Down
2 changes: 1 addition & 1 deletion apps/backend/src/foodRequests/request.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,7 @@ export class RequestsService {

return await this.repo.find({
where: { pantryId },
relations: ['orders'],
relations: ['orders', 'pantry'],
});
}

Expand Down
26 changes: 26 additions & 0 deletions apps/backend/src/volunteers/volunteers.controller.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import { Test, TestingModule } from '@nestjs/testing';
import { mock } from 'jest-mock-extended';
import { Pantry } from '../pantries/pantries.entity';
import { VolunteersService } from './volunteers.service';
import { FoodRequest } from '../foodRequests/request.entity';
import { AuthenticatedRequest } from '../auth/authenticated-request';

const mockVolunteersService = mock<VolunteersService>();

Expand Down Expand Up @@ -160,4 +162,28 @@ describe('VolunteersController', () => {
).toHaveBeenCalledWith(3, pantryIds);
});
});

describe('GET /me/assigned-requests', () => {
it('returns assigned requests when req.currentUser is present', async () => {
const req: AuthenticatedRequest = {
user: { id: 1 },
} as AuthenticatedRequest;
const foodRequests: Partial<FoodRequest>[] = [
{ requestId: 10 },
{ requestId: 5 },
];
mockVolunteersService.findRequestsByVolunteer.mockResolvedValueOnce(
foodRequests as FoodRequest[],
);

const result = await controller.getAssignedRequests(
req as AuthenticatedRequest,
);

expect(result).toEqual(foodRequests);
expect(
mockVolunteersService.findRequestsByVolunteer,
).toHaveBeenCalledWith(1);
});
});
});
13 changes: 13 additions & 0 deletions apps/backend/src/volunteers/volunteers.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,16 @@ import {
ParseIntPipe,
Post,
Body,
Req,
} from '@nestjs/common';
import { User } from '../users/users.entity';
import { Pantry } from '../pantries/pantries.entity';
import { VolunteersService } from './volunteers.service';
import { Role } from '../users/types';
import { Roles } from '../auth/roles.decorator';
import { Assignments } from './types';
import { FoodRequest } from '../foodRequests/request.entity';
import { AuthenticatedRequest } from '../auth/authenticated-request';

@Controller('volunteers')
export class VolunteersController {
Expand Down Expand Up @@ -42,4 +45,14 @@ export class VolunteersController {
): Promise<User> {
return this.volunteersService.assignPantriesToVolunteer(id, pantryIds);
}

@Roles(Role.VOLUNTEER)
@Get('/me/assigned-requests')
async getAssignedRequests(
@Req() req: AuthenticatedRequest,
): Promise<FoodRequest[]> {
const currentUser = req.user;

return this.volunteersService.findRequestsByVolunteer(currentUser.id);
}
}
2 changes: 2 additions & 0 deletions apps/backend/src/volunteers/volunteers.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@ import { AuthModule } from '../auth/auth.module';
import { VolunteersController } from './volunteers.controller';
import { VolunteersService } from './volunteers.service';
import { UsersModule } from '../users/users.module';
import { RequestsModule } from '../foodRequests/request.module';

@Module({
imports: [
TypeOrmModule.forFeature([User]),
UsersModule,
forwardRef(() => PantriesModule),
forwardRef(() => AuthModule),
RequestsModule,
],
controllers: [VolunteersController],
providers: [VolunteersService],
Expand Down
67 changes: 67 additions & 0 deletions apps/backend/src/volunteers/volunteers.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -245,4 +245,71 @@ describe('VolunteersService', () => {
expect(pantryIds).toEqual([2, 3]);
});
});

describe('findRequestsByVolunteer', () => {
it('returned requests include pantry info', async () => {
const requests = await service.findRequestsByVolunteer(7);
requests.forEach((request) => {
expect(request.pantry).toBeDefined();
expect(request.pantry).toHaveProperty('pantryName');
});
});

it('returns requests only from assigned pantries', async () => {
const volunteerId = 6;

const assignedPantries = await service.getVolunteerPantries(volunteerId);
const assignedPantryIds = assignedPantries.map((p) => p.pantryId);

const requests = await service.findRequestsByVolunteer(volunteerId);
requests.forEach((request) => {
expect(assignedPantryIds).toContain(request.pantryId);
});
});

it('returns empty array when volunteer has no assigned pantries', async () => {
const volunteerId = await testDataSource
.query(
`
INSERT INTO users (first_name, last_name, email, phone, role)
VALUES ('Test', 'Volunteer', 'test@volunteer.com', '537-280-1238', 'volunteer')
RETURNING user_id
`,
)
.then((rows) => rows[0].user_id);

const result = await service.findRequestsByVolunteer(volunteerId);
expect(result).toEqual([]);
});

it('returns empty array when assigned pantries have no requests', async () => {
const volunteerId = 8;

const assignedPantries = await service.getVolunteerPantries(volunteerId);
const assignedPantryIds = assignedPantries.map((p) => p.pantryId);
await testDataSource.query(
`DELETE FROM allocations
WHERE order_id IN (
SELECT o.order_id FROM orders o
JOIN food_requests fr ON o.request_id = fr.request_id
WHERE fr.pantry_id = ANY($1)
)`,
[assignedPantryIds],
);
await testDataSource.query(
`DELETE FROM orders
WHERE request_id IN (
SELECT request_id FROM food_requests WHERE pantry_id = ANY($1)
)`,
[assignedPantryIds],
);
await testDataSource.query(
`DELETE FROM food_requests WHERE pantry_id = ANY($1)`,
[assignedPantryIds],
);

const requests = await service.findRequestsByVolunteer(volunteerId);
expect(requests).toEqual([]);
});
});
});
16 changes: 16 additions & 0 deletions apps/backend/src/volunteers/volunteers.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import { Pantry } from '../pantries/pantries.entity';
import { PantriesService } from '../pantries/pantries.service';
import { UsersService } from '../users/users.service';
import { Assignments } from './types';
import { FoodRequest } from '../foodRequests/request.entity';
import { RequestsService } from '../foodRequests/request.service';

@Injectable()
export class VolunteersService {
Expand All @@ -16,6 +18,7 @@ export class VolunteersService {
private repo: Repository<User>,
private usersService: UsersService,
private pantriesService: PantriesService,
private requestsService: RequestsService,
) {}

async findOne(id: number): Promise<User> {
Expand Down Expand Up @@ -73,4 +76,17 @@ export class VolunteersService {
volunteer.pantries = [...existingPantries, ...newPantries];
return this.repo.save(volunteer);
}

async findRequestsByVolunteer(volunteerId: number): Promise<FoodRequest[]> {
validateId(volunteerId, 'Volunteer');

const pantries = await this.getVolunteerPantries(volunteerId);
const pantryIds = pantries.map((p) => p.pantryId);

const requestArrays = await Promise.all(
pantryIds.map((id) => this.requestsService.find(id)),
);

return requestArrays.flat();
}
}
7 changes: 6 additions & 1 deletion apps/frontend/src/api/apiClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,9 @@ import {
OrderSummary,
UserDto,
OrderDetails,
Assignments,
FoodRequestSummaryDto,
PantryWithUser,
Assignments,
} from 'types/types';

const defaultBaseUrl =
Expand Down Expand Up @@ -294,6 +294,11 @@ export class ApiClient {
return data as FoodRequest[];
}

public async getVolunteerAssignedRequests(): Promise<FoodRequest[]> {
const data = await this.get(`/api/volunteers/me/assigned-requests`);
return data as FoodRequest[];
}

public async confirmDelivery(
requestId: number,
data: FormData,
Expand Down
9 changes: 9 additions & 0 deletions apps/frontend/src/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import { Authenticator } from '@aws-amplify/ui-react';
import FoodManufacturerApplication from '@containers/foodManufacturerApplication';
import { submitManufacturerApplicationForm } from '@components/forms/manufacturerApplicationForm';
import AssignedPantries from '@containers/volunteerAssignedPantries';
import VolunteerRequestManagement from '@containers/volunteerRequestManagement';

Amplify.configure(CognitoAuthConfig);

Expand Down Expand Up @@ -198,6 +199,14 @@ const router = createBrowserRouter([
</ProtectedRoute>
),
},
{
path: '/volunteer-request-management',
element: (
<ProtectedRoute>
<VolunteerRequestManagement />
</ProtectedRoute>
),
},
],
},
]);
Expand Down
23 changes: 12 additions & 11 deletions apps/frontend/src/containers/adminDonation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
Checkbox,
VStack,
ButtonGroup,
Link,
} from '@chakra-ui/react';
import { Donation } from 'types/types';
import DonationDetailsModal from '@components/forms/donationDetailsModal';
Expand Down Expand Up @@ -231,20 +232,13 @@ const AdminDonation: React.FC = () => {
borderRightColor="neutral.100"
py={0}
>
<Button
variant="plain"
textDecoration="underline"
<Link
textDecorationColor="black"
variant="underline"
onClick={() => setSelectedDonation(donation)}
>
{donation.donationId}
</Button>
{selectedDonation && (
<DonationDetailsModal
donation={selectedDonation}
isOpen={selectedDonation !== null}
onClose={() => setSelectedDonation(null)}
/>
)}
</Link>
</Table.Cell>
<Table.Cell
textStyle="p2"
Expand All @@ -258,6 +252,13 @@ const AdminDonation: React.FC = () => {
</Table.Cell>
</Table.Row>
))}
{selectedDonation && (
<DonationDetailsModal
donation={selectedDonation}
isOpen={selectedDonation !== null}
onClose={() => setSelectedDonation(null)}
/>
)}
</Table.Body>
</Table.Root>

Expand Down
29 changes: 15 additions & 14 deletions apps/frontend/src/containers/adminOrderManagement.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
ButtonGroup,
Checkbox,
Input,
Link,
} from '@chakra-ui/react';
import {
ArrowDownUp,
Expand Down Expand Up @@ -625,21 +626,13 @@ const OrderStatusSection: React.FC<OrderStatusSectionProps> = ({
borderRight="1px solid"
borderRightColor="neutral.100"
>
<Button
variant="plain"
fontWeight="400"
textDecoration="underline"
<Link
textDecorationColor="black"
variant="underline"
onClick={() => onOrderSelect(order.orderId)}
>
{order.orderId}
</Button>
{selectedOrderId === order.orderId && (
<OrderDetailsModal
orderId={order.orderId}
isOpen={true}
onClose={() => onOrderSelect(null)}
/>
)}
</Link>
</Table.Cell>
<Table.Cell
{...tableCellStyles}
Expand All @@ -652,8 +645,9 @@ const OrderStatusSection: React.FC<OrderStatusSectionProps> = ({
color={colors[1]}
display="inline-block"
fontWeight="500"
my={2}
py={1}
fontSize="12px"
my={3}
py={0.5}
px={3}
>
{capitalize(order.status)}
Expand Down Expand Up @@ -719,6 +713,13 @@ const OrderStatusSection: React.FC<OrderStatusSectionProps> = ({
</Table.Row>
);
})}
{selectedOrderId && (
<OrderDetailsModal
orderId={selectedOrderId}
isOpen={true}
onClose={() => onOrderSelect(null)}
/>
)}
</Table.Body>
</Table.Root>

Expand Down
Loading
Loading