diff --git a/apps/frontend/src/api/apiClient.ts b/apps/frontend/src/api/apiClient.ts index 74d2c812..bdd49bbd 100644 --- a/apps/frontend/src/api/apiClient.ts +++ b/apps/frontend/src/api/apiClient.ts @@ -11,6 +11,7 @@ import { CreateFoodRequestBody, Pantry, PantryApplicationDto, + UserDto, } from 'types/types'; const defaultBaseUrl = @@ -96,6 +97,10 @@ export class ApiClient { .then((response) => response.data); } + public async postUser(data: UserDto): Promise { + return this.axiosInstance.post(`/api/users`, data); + } + public async getPantrySSFRep(pantryId: number): Promise { return this.get(`/api/pantries/${pantryId}/ssf-contact`) as Promise; } diff --git a/apps/frontend/src/components/forms/addNewVolunteerModal.tsx b/apps/frontend/src/components/forms/addNewVolunteerModal.tsx new file mode 100644 index 00000000..9ca2221e --- /dev/null +++ b/apps/frontend/src/components/forms/addNewVolunteerModal.tsx @@ -0,0 +1,146 @@ +import { + Dialog, + Button, + Text, + Flex, + Field, + Input, + CloseButton, + Box +} from '@chakra-ui/react'; +import { useState } from 'react'; +import { Role, UserDto } from "../../types/types"; +import ApiClient from '@api/apiClient'; +import { USPhoneInput } from './usPhoneInput'; +import { PlusIcon } from 'lucide-react'; + +interface NewVolunteerModalProps { + onSubmitSuccess?: () => void; + onSubmitFail?: () => void; +} + +const NewVolunteerModal: React.FC = ({ onSubmitSuccess, onSubmitFail }) => { + const [firstName, setFirstName] = useState(""); + const [lastName, setLastName] = useState(""); + const [email, setEmail] = useState(""); + const [phone, setPhone] = useState(""); + + const [isOpen, setIsOpen] = useState(false); + + const [error, setError] = useState(""); + + const handleSubmit = async () => { + console.log("RAW phone value:", phone); + if (!firstName || !lastName || !email || !phone || phone === "+1") { + setError("Please fill in all fields. *"); + return; + } + + setError(""); + + const newVolunteer: UserDto = { + firstName, + lastName, + email, + phone, + role: Role.STANDARD_VOLUNTEER + }; + + try { + await ApiClient.postUser(newVolunteer); + if (onSubmitSuccess) onSubmitSuccess(); + handleClear(); + } catch (error: any) { + const message = error.response?.data?.message; + const hasEmailError = Array.isArray(message) && message.some((msg: any) => typeof msg === "string" && (msg.toLowerCase().includes("email"))); + const hasPhoneError = Array.isArray(message) && message.some((msg: any) => typeof msg === "string" && (msg.toLowerCase().includes("phone"))); + + if (hasEmailError) { + setError("Please specify a valid email. *") + } else if (hasPhoneError) { + setError("Please specify a valid phone number. *") + } else { + if (onSubmitFail) onSubmitFail(); + handleClear(); + } + } + } + + const handleClear = () => { + setFirstName(""); + setLastName(""); + setEmail(""); + setPhone(""); + setError(""); + setIsOpen(false); + }; + + return ( + + + + + + + + + + Add New Volunteer + + setIsOpen(false)} size="md" position="absolute" top={3} right={3}/> + + + + Complete all information in the form to register a new volunteer. + + + + First Name + setFirstName(e.target.value)}/> + + + Last Name + setLastName(e.target.value)}/> + + + + Email + setEmail(e.target.value)}/> + + + Phone Number + + + {error && ( + + {error} + + )} + + + + + + + + + ); +}; + + + + + + +export default NewVolunteerModal; \ No newline at end of file diff --git a/apps/frontend/src/containers/volunteerManagement.tsx b/apps/frontend/src/containers/volunteerManagement.tsx index b2c9a43f..c227b567 100644 --- a/apps/frontend/src/containers/volunteerManagement.tsx +++ b/apps/frontend/src/containers/volunteerManagement.tsx @@ -1,38 +1,40 @@ import { useEffect, useState } from 'react'; import { Table, - TableCaption, Text, - Center, - Button, Flex, Input, - Menu, - Checkbox, VStack, Box, - Portal, - NativeSelect, - NativeSelectIndicator, + InputGroup, + Pagination, + ButtonGroup, + IconButton, + Alert, + Link, } from '@chakra-ui/react'; -import { VolunteerType } from '../types/types'; -import { Link } from 'react-router-dom'; -import { ChevronDownIcon } from 'lucide-react'; +import { SearchIcon, ChevronRight, ChevronLeft } from 'lucide-react'; import { User } from '../types/types'; import ApiClient from '@api/apiClient'; +import NewVolunteerModal from '@components/forms/addNewVolunteerModal'; const VolunteerManagement: React.FC = () => { + const [currentPage, setCurrentPage] = useState(1); const [volunteers, setVolunteers] = useState([]); - const [changedVolunteers, setChangedVolunteers] = useState([]); const [searchName, setSearchName] = useState(''); - const [checkedTypes, setCheckedTypes] = useState([]); + + const [alertMessage, setAlertMessage] = useState(''); + const [submitSuccess, setSubmitSuccess] = useState(false); + + const pageSize = 8; + + const USER_ICON_COLORS = ['#F89E19', '#CC3538', '#2795A5', '#2B4E60']; useEffect(() => { const fetchVolunteers = async () => { try { const allVolunteers = await ApiClient.getVolunteers(); setVolunteers(allVolunteers); - setChangedVolunteers(allVolunteers); } catch (error) { alert('Error fetching volunteers'); console.error('Error fetching volunteers: ', error); @@ -40,44 +42,21 @@ const VolunteerManagement: React.FC = () => { }; fetchVolunteers(); - }, []); + }, [alertMessage]); - const filteredVolunteers = changedVolunteers.filter((a) => { + useEffect(() => { + setCurrentPage(1); + }, [searchName]); + + const filteredVolunteers = volunteers.filter((a) => { const fullName = `${a.firstName} ${a.lastName}`.toLowerCase(); - return ( - fullName.includes(searchName.toLowerCase()) && - (checkedTypes.includes(a.role.toUpperCase()) || checkedTypes.length === 0) - ); + return (fullName.includes(searchName.toLowerCase())); }); - const volunteerTypeDropdown = ({ - volunteerType, - volunteerId, - }: { - volunteerType: VolunteerType; - volunteerId: number; - }) => { - return ( - - - handleVolunteerTypeChange( - e.target.value as VolunteerType, - volunteerId, - ) - } - > - {Object.entries(DISPLAY_VOLUNTEER_TYPES).map(([key, label]) => ( - - ))} - - - - ); - }; + const paginatedVolunteers = filteredVolunteers.slice( + (currentPage - 1) * pageSize, + currentPage * pageSize + ); const handleSearchNameChange = ( event: React.ChangeEvent, @@ -85,157 +64,127 @@ const VolunteerManagement: React.FC = () => { setSearchName(event.target.value); }; - const handleVolunteerFilterChange = (type: string, checked: boolean) => { - if (checked) { - setCheckedTypes([...checkedTypes, type.toUpperCase()]); - } else { - setCheckedTypes( - checkedTypes.filter( - (checkedType) => checkedType !== type.toUpperCase(), - ), - ); - } - }; - - const handleReset = () => { - setSearchName(''); - setCheckedTypes([]); - - setChangedVolunteers(volunteers); - }; - - const handleSaveChanges = async () => { - try { - await Promise.all( - changedVolunteers.map((volunteer) => - ApiClient.updateUserVolunteerRole(volunteer.id, { - role: String(volunteer.role), - }), - ), - ); - setVolunteers(changedVolunteers); - alert('successful save!'); - } catch (error) { - alert('Error updating volunteer type'); - console.error('Error updating volunteer type: ', error); - } - }; - - const handleVolunteerTypeChange = ( - type: VolunteerType, - volunteerId: number, - ) => { - setChangedVolunteers((prev) => - prev.map((a) => (a.id === volunteerId ? { ...a, role: type } : a)), - ); - }; - - const DISPLAY_VOLUNTEER_TYPES: Record = { - LEAD_VOLUNTEER: 'Lead Volunteer', - STANDARD_VOLUNTEER: 'Standard Volunteer', - }; - return ( -
- Pantry Volunteer Management + + Volunteer Management + {alertMessage && ( + + + {alertMessage} + + )} - - - - - - - - - - {Object.values(VolunteerType).map((volunteerType) => ( - - - handleVolunteerFilterChange(volunteerType, e.checked) - } - > - - - - {DISPLAY_VOLUNTEER_TYPES[ - volunteerType.toUpperCase() - ] || volunteerType} - - - - ))} - - - - + + + } maxW={300}> + + + { + setAlertMessage("Volunteer added."); + setSubmitSuccess(true); + setTimeout(() => setAlertMessage(""), 3000); + }} onSubmitFail={() => { + setAlertMessage("Volunteer could not be added."); + setSubmitSuccess(false); + setTimeout(() => setAlertMessage(""), 3000); + }} + /> + - - - - - - - - + - Volunteer Name - Email - Phone - Type - Assigned Pantries - + Volunteer + Email + Actions + - - {filteredVolunteers?.map((volunteer) => ( + + {paginatedVolunteers?.map((volunteer) => ( - {volunteer.firstName} {volunteer.lastName} + + + {volunteer.firstName + .charAt(0) + .toUpperCase()} + {volunteer.lastName + .charAt(0) + .toUpperCase()} + + {volunteer.firstName} {volunteer.lastName} + - {volunteer.email} - {volunteer.phone} - {volunteerTypeDropdown({ - volunteerType: - VolunteerType[ - volunteer.role.toUpperCase() as keyof typeof VolunteerType - ], - volunteerId: volunteer.id, - })} + {volunteer.email} - - + + + View Assigned Pantries + ))} + + setCurrentPage(page)}> + + + setCurrentPage((prev) => Math.max(prev - 1, 1))}> + + + + + ( + setCurrentPage(page.value)} + > + {page.value} + + )} + /> + + + setCurrentPage((prev) => Math.min(prev + 1, Math.ceil(filteredVolunteers.length / pageSize)))}> + + + + + + -
+ ); }; diff --git a/apps/frontend/src/types/types.ts b/apps/frontend/src/types/types.ts index cb901272..237f09e8 100644 --- a/apps/frontend/src/types/types.ts +++ b/apps/frontend/src/types/types.ts @@ -115,6 +115,14 @@ export interface User { phone: string; } +export interface UserDto { + email: string, + firstName: string, + lastName: string, + phone: string, + role: Role, +} + export interface FoodRequest { requestId: number; requestedAt: string;