Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions apps/backend/src/volunteers/volunteers.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,14 @@ import {
import { User } from '../users/user.entity';
import { Pantry } from '../pantries/pantries.entity';
import { VolunteersService } from './volunteers.service';
import { Role } from '../users/types';
import { Roles } from '../auth/roles.decorator';

@Controller('volunteers')
export class VolunteersController {
constructor(private volunteersService: VolunteersService) {}

@Roles(Role.VOLUNTEER, Role.ADMIN)
@Get('/')
async getAllVolunteers(): Promise<
(Omit<User, 'pantries'> & { pantryIds: number[] })[]
Expand Down
5 changes: 5 additions & 0 deletions apps/frontend/src/api/apiClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
OrderSummary,
UserDto,
OrderDetails,
Assignments,
} from 'types/types';

const defaultBaseUrl =
Expand Down Expand Up @@ -301,6 +302,10 @@ export class ApiClient {
const data = await this.get('/api/pantries/my-id');
return data as number;
}

public async getAllVolunteers(): Promise<Assignments[]> {
return this.get('/api/volunteers') as Promise<Assignments[]>;
}
}

export default new ApiClient();
21 changes: 21 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 Unauthorized from '@containers/unauthorized';
import { Authenticator } from '@aws-amplify/ui-react';
import FoodManufacturerApplication from '@containers/foodManufacturerApplication';
import { submitManufacturerApplicationForm } from '@components/forms/manufacturerApplicationForm';
import AssignedPantries from '@containers/volunteerAssignedPantries';

Amplify.configure(CognitoAuthConfig);

Expand Down Expand Up @@ -202,6 +203,26 @@ const router = createBrowserRouter([
path: '/confirm-delivery',
action: submitDeliveryConfirmationFormModal,
},
{
path: '/approve-pantries',
element: <ApprovePantries />,
},
{
path: '/admin-donation',
element: <AdminDonation />,
},
{
path: '/volunteer-management',
element: <VolunteerManagement />,
},
{
path: '/volunteer-assigned-pantries',
element: (
<ProtectedRoute>
<AssignedPantries />
</ProtectedRoute>
),
},
],
},
]);
Expand Down
15 changes: 15 additions & 0 deletions apps/frontend/src/containers/homepage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,21 @@ const Homepage: React.FC = () => {
</List.Root>
</Box>

<Box w="full">
<Heading as="h3" size="md" mb={3} textAlign="center">
Volunteer View
</Heading>
<List.Root unstyled gap={2}>
<ListItem textAlign="center">
<Link asChild color="teal.500">
<RouterLink to="/volunteer-assigned-pantries">
Assigned Pantries
</RouterLink>
</Link>
</ListItem>
</List.Root>
</Box>

<Box w="full">
<Heading as="h3" size="md" mb={3} textAlign="center">
Admin View
Expand Down
303 changes: 303 additions & 0 deletions apps/frontend/src/containers/volunteerAssignedPantries.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,303 @@
import React, { useState, useEffect, useMemo } from 'react';
import { Funnel } from 'lucide-react';
import {
Box,
Button,
Table,
Heading,
VStack,
Text,
RadioGroup,
Spinner,
Center,
} from '@chakra-ui/react';
import ApiClient from '@api/apiClient';
import { Pantry } from 'types/types';
import { RefrigeratedDonation } from '../types/pantryEnums';
import { Assignments } from './../types/types';
import { useNavigate } from 'react-router-dom';
import { FloatingAlert } from '@components/floatingAlert';

const AssignedPantries: React.FC = () => {
const navigator = useNavigate();
const [assignments, setAssignments] = useState<Assignments[]>([]);
const [pantryDetails, setPantryDetails] = useState<Map<number, Pantry>>(
new Map(),
);
const [isFilterOpen, setIsFilterOpen] = useState(false);
const [filterRefrigeratorFriendly, setFilterRefrigeratorFriendly] =
useState<string>('all');
const [alertMessage, setAlertMessage] = useState<string>('');
const [isLoading, setIsLoading] = useState(true);

const isRefrigeratorFriendly = (pantryId: number): boolean => {
const pantry = pantryDetails.get(pantryId);
if (!pantry) return false;
return (
pantry.refrigeratedDonation === RefrigeratedDonation.YES ||
pantry.refrigeratedDonation === RefrigeratedDonation.SOMETIMES
);
};

useEffect(() => {
const fetchAssignments = async () => {
try {
const data = await ApiClient.getAllVolunteers();
setAssignments(data);

const detailsMap = new Map<number, Pantry>();
const allPantryIds = [...new Set(data.flatMap((a) => a.pantryIds))];
await Promise.all(
allPantryIds.map(async (id) => {
try {
const pantry = await ApiClient.getPantry(id);
detailsMap.set(id, pantry);
} catch (error) {
console.error(`Error fetching pantry ${id}:`, error);
}
}),
);
setPantryDetails(detailsMap);
} catch (error) {
console.error('Error fetching assignments:', error);
setAlertMessage('Error fetching assigned pantries');
} finally {
setIsLoading(false);
}
};

fetchAssignments();
}, []);

const filteredAssignments = useMemo(() => {
if (filterRefrigeratorFriendly === 'all') return assignments;

const target = filterRefrigeratorFriendly === 'friendly';
return assignments
.map((a) => ({
...a,
pantryIds: a.pantryIds.filter((id) => {
const pantry = pantryDetails.get(id);
if (!pantry) return false;
return isRefrigeratorFriendly(pantry.pantryId) === target;
}),
}))
.filter((a) => a.pantryIds.length > 0);
}, [filterRefrigeratorFriendly, assignments, pantryDetails]);

const getRefrigeratorFriendlyText = (pantryId: number): string => {
return isRefrigeratorFriendly(pantryId)
? 'Refrigerator-Friendly'
: 'Not Refrigerator-Friendly';
};

const tableHeaderStyles = {
borderBottom: '1px solid',
borderColor: 'neutral.100',
color: 'neutral.800',
fontFamily: 'inter',
fontWeight: '600',
fontSize: 'sm',
py: 3,
px: 4,
};

return (
<Box p={12}>
{alertMessage && (
<FloatingAlert message={alertMessage} status="info" timeout={6000} />
)}

<Heading textStyle="h1" color="gray.light" mb={6}>
Assigned Pantries
</Heading>

{isLoading ? (
<Center mt={12}>
<Spinner size="lg" />
</Center>
) : (
<>
{/* Filter Button */}
<Box display="flex" gap={2} mb={6}>
<Box position="relative">
<Button
onClick={() => setIsFilterOpen(!isFilterOpen)}
variant="outline"
color="neutral.800"
border="1px solid"
borderColor="neutral.200"
size="sm"
p={3}
fontFamily="ibm"
fontWeight="semibold"
>
<Funnel />
Filter
</Button>

{isFilterOpen && (
<>
<Box
position="fixed"
top={0}
left={0}
right={0}
bottom={0}
onClick={() => setIsFilterOpen(false)}
zIndex={10}
/>
<Box
position="absolute"
top="100%"
left={0}
mt={2}
bg="white"
border="1px solid"
borderColor="gray.200"
borderRadius="md"
boxShadow="lg"
p={4}
minW="275px"
zIndex={20}
>
<RadioGroup.Root
value={filterRefrigeratorFriendly}
onValueChange={(e: { value: string }) =>
setFilterRefrigeratorFriendly(e.value)
}
>
<VStack align="stretch" gap={2}>
<RadioGroup.Item value="all">
<RadioGroup.ItemHiddenInput />
<RadioGroup.ItemIndicator />
<RadioGroup.ItemText fontSize="sm">
Show All
</RadioGroup.ItemText>
</RadioGroup.Item>
<RadioGroup.Item value="friendly">
<RadioGroup.ItemHiddenInput />
<RadioGroup.ItemIndicator />
<RadioGroup.ItemText fontSize="sm">
Refrigerator-Friendly Only
</RadioGroup.ItemText>
</RadioGroup.Item>
<RadioGroup.Item value="not-friendly">
<RadioGroup.ItemHiddenInput />
<RadioGroup.ItemIndicator />
<RadioGroup.ItemText fontSize="sm">
Not Refrigerator-Friendly Only
</RadioGroup.ItemText>
</RadioGroup.Item>
</VStack>
</RadioGroup.Root>
</Box>
</>
)}
</Box>
</Box>

{/* Pantries Table */}
<Table.Root variant="line">
<Table.Header>
<Table.Row>
<Table.ColumnHeader
{...tableHeaderStyles}
borderRight="1px solid"
borderRightColor="neutral.100"
width="40%"
>
Pantry
</Table.ColumnHeader>
<Table.ColumnHeader
{...tableHeaderStyles}
width="35%"
textAlign="right"
>
Refrigerator-Friendly
</Table.ColumnHeader>
<Table.ColumnHeader
{...tableHeaderStyles}
textAlign="right"
width="25%"
>
Action
</Table.ColumnHeader>
</Table.Row>
</Table.Header>
<Table.Body>
{filteredAssignments.flatMap((assignment) =>
assignment.pantryIds.map((pantryId) => {
const pantry = pantryDetails.get(pantryId);
const friendly = isRefrigeratorFriendly(pantryId);
return (
<Table.Row
key={`${assignment.id}-${pantryId}`}
_hover={{ bg: 'gray.50' }}
>
{/* Pantry Name */}
<Table.Cell
borderRight="1px solid"
borderRightColor="neutral.100"
px={4}
py={3}
>
<Text
as="span"
textStyle="p2"
fontFamily="inter"
textDecoration="underline"
cursor="pointer"
color="gray.800"
>
{pantry?.pantryName}
</Text>
</Table.Cell>

{/* Refrigerator-Friendly Badge */}
<Table.Cell px={4} py={3} textAlign="right">
<Box
bg={friendly ? 'neutral.100' : 'neutral.200'}
px={3}
py={1}
borderRadius="md"
display="inline-block"
fontSize="sm"
fontFamily="inter"
color="neutral.800"
>
{getRefrigeratorFriendlyText(pantryId)}
</Box>
</Table.Cell>

{/* Action */}
<Table.Cell px={4} py={3} textAlign="right">
<Button
variant="plain"
textDecoration="underline"
color="neutral.700"
textStyle="p2"
onClick={() => navigator(`/`)}
fontFamily="inter"
textAlign="right"
fontSize="sm"
p={0}
height="auto"
minW="auto"
>
View Orders
</Button>
</Table.Cell>
</Table.Row>
);
}),
)}
</Table.Body>
</Table.Root>
</>
)}
</Box>
);
};

export default AssignedPantries;
Loading