Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
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
18 changes: 18 additions & 0 deletions apps/frontend/src/api/apiClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ import {
DonationDetails,
VolunteerOrder,
VolunteerAction,
ApprovedPantryResponse,
UpdatePantryVolunteersDto,
FoodRequestWithoutRelations,
PendingApplication,
} from 'types/types';
Expand Down Expand Up @@ -164,6 +166,12 @@ export class ApiClient {
.then((response) => response.data);
}

public async getApprovedPantries(): Promise<ApprovedPantryResponse[]> {
return this.axiosInstance
.get(`/api/pantries/approved`)
.then((response) => response.data);
}

public async getPantryFromOrder(orderId: number): Promise<Pantry | null> {
return this.axiosInstance
.get(`/api/orders/${orderId}/pantry`)
Expand Down Expand Up @@ -414,6 +422,16 @@ export class ApiClient {
});
}

public async updatePantryVolunteers(
pantryId: number,
body: UpdatePantryVolunteersDto,
): Promise<void> {
await this.axiosInstance.patch(
`/api/pantries/${pantryId}/volunteers`,
body,
);
}

public async updateFoodManufacturer(
manufacturerId: number,
decision: 'approve' | 'deny',
Expand Down
17 changes: 17 additions & 0 deletions apps/frontend/src/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import VolunteerRequestManagement from '@containers/volunteerRequestManagement';
import AdminDonationStats from '@containers/adminDonationStats';
import ProfilePage from '@containers/profilePage';
import VolunteerOrderManagement from '@containers/volunteerOrderManagement';
import AdminPantryManagement from '@containers/adminPantryManagement';
import AdminRequestManagement from '@containers/adminRequestManagement';
import AdminDashboard from '@containers/adminDashboard';

Expand Down Expand Up @@ -117,6 +118,14 @@ const router = createBrowserRouter([
</ProtectedRoute>
),
},
{
path: ROUTES.PANTRY_MANAGEMENT_DETAILS,
element: (
<ProtectedRoute>
<PantryApplicationDetails />
</ProtectedRoute>
),
},
{
path: ROUTES.FOOD_MANUFACTURER_APPLICATION_DETAILS,
element: (
Expand Down Expand Up @@ -213,6 +222,14 @@ const router = createBrowserRouter([
</ProtectedRoute>
),
},
{
path: ROUTES.PANTRY_MANAGEMENT,
element: (
<ProtectedRoute>
<AdminPantryManagement />
</ProtectedRoute>
),
},
],
},
]);
Expand Down
282 changes: 282 additions & 0 deletions apps/frontend/src/components/forms/assignVolunteersModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,282 @@
import ApiClient from '@api/apiClient';
import {
Box,
Button,
Checkbox,
CloseButton,
Dialog,
Flex,
Input,
InputGroup,
Text,
VStack,
} from '@chakra-ui/react';
import { useAlert } from '../../hooks/alert';
import { useEffect, useState } from 'react';
import { ApprovedPantryResponse, Assignments } from 'types/types';
import { SearchIcon } from 'lucide-react';
import { getInitials, USER_ICON_COLORS } from '@utils/utils';
Comment thread
Juwang110 marked this conversation as resolved.
import { FloatingAlert } from '@components/floatingAlert';
import { useModalBodyCleanup } from '../../hooks/modalBodyCleanup';

interface AssignVolunteersModalProps {
pantry: ApprovedPantryResponse;
onSuccess: () => void;
onClose: () => void;
isOpen: boolean;
}

type VolunteerDisplay = {
Comment thread
Juwang110 marked this conversation as resolved.
userId: number;
firstName: string;
lastName: string;
};

const AssignVolunteersModal: React.FC<AssignVolunteersModalProps> = ({
pantry,
onSuccess,
onClose,
isOpen,
}) => {
useModalBodyCleanup();
const [alertState, setAlertMessage] = useAlert();

const [volunteers, setVolunteers] = useState<VolunteerDisplay[]>([]);

const [selectedIds, setSelectedIds] = useState<Set<number>>(new Set());

const [searchName, setSearchName] = useState<string>('');

const handleSearchNameChange = (
event: React.ChangeEvent<HTMLInputElement>,
) => {
setSearchName(event.target.value);
};

useEffect(() => {
if (!isOpen) return;
const fetchVolunteers = async () => {
Comment thread
Juwang110 marked this conversation as resolved.
try {
const allVolunteers: Assignments[] = await ApiClient.getVolunteers();

const assignedIds = new Set(pantry.volunteers.map((v) => v.userId));

const normalized: VolunteerDisplay[] = allVolunteers.map((v) => ({
userId: v.id,
firstName: v.firstName,
lastName: v.lastName,
}));

setVolunteers(normalized);
setSelectedIds(new Set(assignedIds));
} catch {
setAlertMessage('Error fetching volunteers');
}
};

fetchVolunteers();
}, [pantry, setAlertMessage, isOpen]);

const filteredVolunteers = volunteers.filter((v) => {
const fullName = `${v.firstName} ${v.lastName}`.toLowerCase();
return fullName.includes(searchName.toLowerCase());
});

const handleToggle = (userId: number, checked: boolean) => {
setSelectedIds((prev) => {
const next = new Set(prev);
if (checked) next.add(userId);
else next.delete(userId);
return next;
});
};

const handleSave = async () => {
try {
const originalIds = new Set(pantry.volunteers.map((v) => v.userId));

const addVolunteerIds = [...selectedIds].filter(
(id) => !originalIds.has(id),
);
const removeVolunteerIds = [...originalIds].filter(
(id) => !selectedIds.has(id),
);

if (addVolunteerIds.length > 0 || removeVolunteerIds.length > 0) {
await ApiClient.updatePantryVolunteers(pantry.pantryId, {
addVolunteerIds,
removeVolunteerIds,
});
}

onSuccess();
onClose();
} catch {
setAlertMessage('Error saving volunteer assignments');
}
};

return (
<Dialog.Root
size="md"
open={isOpen}
onOpenChange={(e: { open: boolean }) => {
if (!e.open) onClose();
}}
closeOnInteractOutside
>
{alertState && (
<FloatingAlert
key={alertState.id}
message={alertState.message}
status="error"
timeout={6000}
/>
)}
<Dialog.Backdrop />
<Dialog.Positioner>
<Dialog.Content>
<Dialog.CloseTrigger asChild>
Comment thread
Juwang110 marked this conversation as resolved.
<CloseButton
color="var(--chakra-colors-neutral-700)"
size="md"
mt={3}
/>
</Dialog.CloseTrigger>

<Dialog.Header pb={0}>
<Dialog.Title
Comment thread
Juwang110 marked this conversation as resolved.
fontSize="18px"
fontFamily="inter"
fontWeight={600}
color="black"
mt={3}
>
Assign Volunteers
</Dialog.Title>
</Dialog.Header>
<Dialog.Body pb={6}>
<VStack align="stretch" gap={4}>
<Text textStyle="p2" color="gray.dark">
{pantry.pantryName}
</Text>
<VStack align="stretch" gap={8} mt={6}>
<InputGroup
startElement={
<Box>
<SearchIcon
color="var(--chakra-colors-neutral-600)"
size={13}
strokeWidth={3}
/>
</Box>
}
px={3}
>
<Input
placeholder="Search"
value={searchName}
borderColor="neutral.100"
ps="8"
onChange={handleSearchNameChange}
color="neutral.600"
textStyle="p2"
_focusVisible={{ boxShadow: 'none', outline: 'none' }}
/>
</InputGroup>
<Box maxH="300px" overflowY="auto" px={3}>
Comment thread
Juwang110 marked this conversation as resolved.
<VStack align="stretch" gap={0}>
{filteredVolunteers.map((volunteer) => (
<Flex
key={volunteer.userId}
align="center"
justify="space-between"
borderBottom="1px solid"
borderColor="neutral.100"
>
<Flex align="center" gap={3} py={2}>
<Box
borderRadius="full"
bg={
USER_ICON_COLORS[
volunteer.userId % USER_ICON_COLORS.length
]
}
width="33px"
height="33px"
display="flex"
alignItems="center"
justifyContent="center"
color="white"
fontSize="12px"
flexShrink={0}
>
{getInitials(
volunteer.firstName,
volunteer.lastName,
)}
</Box>

<Text color="neutral.700" textStyle="p2">
{volunteer.firstName} {volunteer.lastName}
</Text>
</Flex>

<Box
borderLeft="1px solid"
borderColor="neutral.100"
pl={4}
alignSelf="stretch"
display="flex"
alignItems="center"
>
<Checkbox.Root
checked={selectedIds.has(volunteer.userId)}
onCheckedChange={(e: { checked: boolean }) =>
handleToggle(volunteer.userId, e.checked)
}
size="md"
>
<Checkbox.HiddenInput />
<Checkbox.Control
borderRadius="2px"
borderColor="neutral.100"
/>
</Checkbox.Root>
</Box>
</Flex>
))}

{filteredVolunteers.length === 0 && (
<Text
color="neutral.500"
fontSize="14px"
textAlign="center"
py={4}
>
No volunteers found
</Text>
)}
</VStack>
</Box>
<Box w="100%" display="flex" justifyContent="flex-end">
<Button
bg="blue.core"
color="white"
Comment thread
Juwang110 marked this conversation as resolved.
fontWeight={600}
onClick={handleSave}
px={10}
>
Save Changes
</Button>
</Box>
</VStack>
</VStack>
</Dialog.Body>
</Dialog.Content>
</Dialog.Positioner>
</Dialog.Root>
);
};

export default AssignVolunteersModal;
2 changes: 1 addition & 1 deletion apps/frontend/src/containers/adminDashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ const AdminDashboard: React.FC = () => {
onLinkClick={() => {
navigate(
application.type === 'pantry'
? `/pantry-application-details/${application.id}`
? `/pantry-details/pantry/${application.id}`
: `/food-manufacturer-application-details/${application.id}`,
);
}}
Expand Down
Loading
Loading