Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
2ac780b
frontend for pantry management
Juwang110 Apr 23, 2026
73f1e0d
assign volunteers modal
Juwang110 Apr 23, 2026
51b7eeb
Merge branch 'main' into jw/ssf-194-admin-pantry-management-frontend
Juwang110 Apr 23, 2026
1da1fcc
format issues
Juwang110 Apr 23, 2026
f2a01bb
fetch pantries bug fix
Juwang110 Apr 23, 2026
f283a84
comments
Juwang110 Apr 24, 2026
036f7d6
Merge branch 'main' into jw/ssf-194-admin-pantry-management-frontend
Juwang110 Apr 24, 2026
7305e66
comments
Juwang110 Apr 25, 2026
6a63067
Merge branch 'jw/ssf-194-admin-pantry-management-frontend' of https:/…
Juwang110 Apr 25, 2026
36b11dd
Merge branch 'main' into jw/ssf-194-admin-pantry-management-frontend
Juwang110 Apr 25, 2026
a3cb81c
comments
Juwang110 Apr 25, 2026
568554c
comments
Juwang110 Apr 27, 2026
49d6efc
Merge branch 'main' into jw/ssf-194-admin-pantry-management-frontend
Juwang110 Apr 27, 2026
138bb93
Merge branch 'main' into jw/ssf-194-admin-pantry-management-frontend
Juwang110 Apr 28, 2026
a5ac4cf
Merge branch 'main' into jw/ssf-194-admin-pantry-management-frontend
Juwang110 May 1, 2026
fec3e1b
comments
Juwang110 May 2, 2026
d7707c1
Merge branch 'main' into jw/ssf-194-admin-pantry-management-frontend
Juwang110 May 7, 2026
ee79b4c
Merge branch 'main' into jw/ssf-194-admin-pantry-management-frontend
Juwang110 May 9, 2026
c5734f8
Merge branch 'main' into jw/ssf-194-admin-pantry-management-frontend
Juwang110 May 11, 2026
178377d
comment
Juwang110 May 11, 2026
f8f91fe
comment
Juwang110 May 12, 2026
c409313
Merge branch 'main' into jw/ssf-194-admin-pantry-management-frontend
Juwang110 May 15, 2026
0ad59ed
comments
Juwang110 May 15, 2026
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
19 changes: 19 additions & 0 deletions apps/frontend/src/api/apiClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ import {
DonationDetails,
VolunteerOrder,
VolunteerAction,
ApprovedPantryResponse,
UpdatePantryVolunteersDto,
FoodRequestWithoutRelations,
BulkUpdateTrackingCostDto,
UpdateDonationItemDetailsDto,
PendingApplication,
Expand Down Expand Up @@ -161,6 +164,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 @@ -407,6 +416,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
4 changes: 2 additions & 2 deletions apps/frontend/src/components/dashboardCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
getInitials,
ORDER_STATUS_COLORS,
DONATION_STATUS_COLORS,
ASSIGNEE_COLORS,
USER_ICON_COLORS,
} from '@utils/utils';
import { OrderAssignee, OrderStatus, DonationStatus } from '../types/types';

Expand Down Expand Up @@ -192,7 +192,7 @@ const DashboardCard: React.FC<DashboardCardProps> = ({
w="30px"
h="30px"
borderRadius="full"
bg={ASSIGNEE_COLORS[assignee.id % ASSIGNEE_COLORS.length]}
bg={USER_ICON_COLORS[assignee.id % USER_ICON_COLORS.length]}
color="white"
display="flex"
alignItems="center"
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;
Loading
Loading