diff --git a/web-ui/src/components/certifications/EarnedCertificationsTable.css b/web-ui/src/components/certifications/EarnedCertificationsTable.css index 9013e52810..bc320a8f4e 100644 --- a/web-ui/src/components/certifications/EarnedCertificationsTable.css +++ b/web-ui/src/components/certifications/EarnedCertificationsTable.css @@ -1,6 +1,4 @@ #earned-certifications-table { - display: flex; - justify-content: center; .MuiCardHeader-root { padding-bottom: 0; @@ -13,6 +11,7 @@ table { margin: 0 auto; + width: 100%; border-collapse: collapse; img { @@ -43,6 +42,10 @@ tr:nth-child(odd) { background-color: var(--checkins-palette-background-default); } + + .actions-th { + width: 6rem; + } } .earned-certifications-dialog { diff --git a/web-ui/src/components/certifications/EarnedCertificationsTable.jsx b/web-ui/src/components/certifications/EarnedCertificationsTable.jsx index dc5feeb87f..a13b67e8ad 100644 --- a/web-ui/src/components/certifications/EarnedCertificationsTable.jsx +++ b/web-ui/src/components/certifications/EarnedCertificationsTable.jsx @@ -10,6 +10,7 @@ import { } from '@mui/icons-material'; import { Autocomplete, + Avatar, Button, Card, CardContent, @@ -370,7 +371,7 @@ const EarnedCertificationsTable = ({ () => ( } + avatar={} title="Earned Certifications" titleTypographyProps={{ variant: 'h5', component: 'h2' }} /> @@ -389,7 +390,7 @@ const EarnedCertificationsTable = ({ {sortIndicator(column)} ))} - Actions + Actions {earnedCertifications.map(earnedCertificationRow)} diff --git a/web-ui/src/components/dialogs/OrganizationDialog.jsx b/web-ui/src/components/dialogs/OrganizationDialog.jsx index 67a977b1cf..b227144f28 100644 --- a/web-ui/src/components/dialogs/OrganizationDialog.jsx +++ b/web-ui/src/components/dialogs/OrganizationDialog.jsx @@ -2,17 +2,25 @@ import React from 'react'; import { Dialog, DialogTitle, DialogContent, DialogActions, Button, TextField } from '@mui/material'; import PropTypes from 'prop-types'; -const OrganizationDialog = ({ open, onClose, onSave, organization, setOrganization }) => { +const OrganizationDialog = ({ + open, + onClose, + onSave, + organization, + setOrganization +}) => { return ( Create New Organization + {/* This section no longer includes the option to select an existing organization */} setOrganization({ ...organization, name: e.target.value })} + required /> setOrganization({ ...organization, description: e.target.value })} + required /> - + ); diff --git a/web-ui/src/components/profile/Profile.jsx b/web-ui/src/components/profile/Profile.jsx index 9a3e03a3b0..3bad89d0e7 100644 --- a/web-ui/src/components/profile/Profile.jsx +++ b/web-ui/src/components/profile/Profile.jsx @@ -1,11 +1,10 @@ import React, { useContext, useEffect, useState } from 'react'; import { styled } from '@mui/material/styles'; -import { Avatar, Typography, Button, Dialog, DialogTitle, DialogContent, DialogActions, TextField } from '@mui/material'; +import { Avatar, Typography } from '@mui/material'; import { AppContext } from '../../context/AppContext'; import { selectProfileMap } from '../../context/selectors'; import { getAvatarURL } from '../../api/api.js'; import { getMember } from '../../api/member'; -import { saveNewOrganization, saveNewEvent } from '../../api/volunteer'; // Importing the functions from volunteer.js const PREFIX = 'Profile'; @@ -53,22 +52,19 @@ const Root = styled('div')(() => ({ } })); -const Profile = ({ memberId, pdlId, checkinPdlId, showButtons = true }) => { // Add showButtons prop with default as true +const Profile = ({ memberId, pdlId, checkinPdlId }) => { const { state } = useContext(AppContext); const { csrf } = state; const userProfile = selectProfileMap(state)[memberId]; - const { workEmail, name, title, location, supervisorid } = userProfile ? userProfile : {}; + const { workEmail, name, title, location, supervisorid } = userProfile + ? userProfile + : {}; const [pdl, setPDL] = useState(''); const [checkinPdl, setCheckinPdl] = useState(''); const [supervisor, setSupervisor] = useState(''); - const [organizationDialogOpen, setOrganizationDialogOpen] = useState(false); - const [eventDialogOpen, setEventDialogOpen] = useState(false); - const [newOrganization, setNewOrganization] = useState({ name: '', description: '', website: '' }); - const [newEvent, setNewEvent] = useState({ relationshipId: '', eventDate: '', hours: 0, notes: '' }); - const areSamePdls = checkinPdl && pdl && checkinPdl === pdl; // Get PDL's name @@ -76,7 +72,8 @@ const Profile = ({ memberId, pdlId, checkinPdlId, showButtons = true }) => { // async function getPDLName() { if (pdlId) { let res = await getMember(pdlId, csrf); - let pdlProfile = res.payload.data && !res.error ? res.payload.data : undefined; + let pdlProfile = + res.payload.data && !res.error ? res.payload.data : undefined; setPDL(pdlProfile ? pdlProfile.name : ''); } } @@ -90,7 +87,8 @@ const Profile = ({ memberId, pdlId, checkinPdlId, showButtons = true }) => { // async function getCheckinPDLName() { if (checkinPdlId) { let res = await getMember(checkinPdlId, csrf); - let checkinPdlProfile = res.payload.data && !res.error ? res.payload.data : undefined; + let checkinPdlProfile = + res.payload.data && !res.error ? res.payload.data : undefined; setCheckinPdl(checkinPdlProfile ? checkinPdlProfile.name : ''); } } @@ -104,7 +102,8 @@ const Profile = ({ memberId, pdlId, checkinPdlId, showButtons = true }) => { // async function getSupervisorName() { if (supervisorid) { let res = await getMember(supervisorid, csrf); - let supervisorProfile = res.payload.data && !res.error ? res.payload.data : undefined; + let supervisorProfile = + res.payload.data && !res.error ? res.payload.data : undefined; setSupervisor(supervisorProfile ? supervisorProfile.name : ''); } } @@ -113,24 +112,6 @@ const Profile = ({ memberId, pdlId, checkinPdlId, showButtons = true }) => { // } }, [csrf, supervisorid]); - const handleSaveNewOrganization = async () => { - const result = await saveNewOrganization(csrf, newOrganization); // Use the imported API call - if (result.error) { - // Handle error - return; - } - setOrganizationDialogOpen(false); - }; - - const handleSaveNewEvent = async () => { - const result = await saveNewEvent(csrf, newEvent); // Use the imported API call - if (result.error) { - // Handle error - return; - } - setEventDialogOpen(false); - }; - return ( { // - + {workEmail}
@@ -167,100 +152,14 @@ const Profile = ({ memberId, pdlId, checkinPdlId, showButtons = true }) => { //
Current PDL: {pdl}
- {checkinPdl && !areSamePdls && `PDL @ Time of Check-In: ${checkinPdl}`} + {checkinPdl && + !areSamePdls && + `PDL @ Time of Check-In: ${checkinPdl}`}
- - {/* Conditionally render the buttons based on showButtons prop */} - {showButtons && ( - <> - - - - - )} - - {/* Organization Creation Dialog */} - setOrganizationDialogOpen(false)}> - Create New Organization - - setNewOrganization({ ...newOrganization, name: e.target.value })} - /> - setNewOrganization({ ...newOrganization, description: e.target.value })} - /> - setNewOrganization({ ...newOrganization, website: e.target.value })} - /> - - - - - - - - {/* Event Creation Dialog */} - setEventDialogOpen(false)}> - Create New Event - - setNewEvent({ ...newEvent, eventDate: e.target.value })} - /> - setNewEvent({ ...newEvent, hours: e.target.value })} - /> - setNewEvent({ ...newEvent, notes: e.target.value })} - /> - - - - - -
); }; -export default Profile; \ No newline at end of file +export default Profile; diff --git a/web-ui/src/components/profile/__snapshots__/Profile.test.jsx.snap b/web-ui/src/components/profile/__snapshots__/Profile.test.jsx.snap index 408fbc0530..e523ca3728 100644 --- a/web-ui/src/components/profile/__snapshots__/Profile.test.jsx.snap +++ b/web-ui/src/components/profile/__snapshots__/Profile.test.jsx.snap @@ -56,30 +56,6 @@ exports[`renders correctly 1`] = ` Current PDL:

- - diff --git a/web-ui/src/components/skills/SkillSection.css b/web-ui/src/components/skills/SkillSection.css index 0440c89ce3..f39d5845f1 100644 --- a/web-ui/src/components/skills/SkillSection.css +++ b/web-ui/src/components/skills/SkillSection.css @@ -79,7 +79,6 @@ flex-direction: row; flex-wrap: wrap; align-items: flex-end; - padding: 0 16px 16px 16px; justify-content: space-between; } diff --git a/web-ui/src/components/skills/SkillSection.jsx b/web-ui/src/components/skills/SkillSection.jsx index 1c722f9766..c83bda5f45 100644 --- a/web-ui/src/components/skills/SkillSection.jsx +++ b/web-ui/src/components/skills/SkillSection.jsx @@ -23,9 +23,11 @@ import { getSkill, createSkill } from '../../api/skill.js'; import SkillSlider from './SkillSlider'; import { + Avatar, Button, Card, CardActions, + CardContent, Dialog, DialogActions, DialogContent, @@ -280,9 +282,10 @@ const SkillSection = ({ userId }) => { +
- + Skills @@ -315,6 +318,7 @@ const SkillSection = ({ userId }) => { ); })} +
{ const [organizationDialogOpen, setOrganizationDialogOpen] = useState(false); const [organizations, setOrganizations] = useState([]); const [selectedOrganization, setSelectedOrganization] = useState(null); + const [newOrganization, setNewOrganization] = useState({ name: '', description: '', website: '' }); // Add new state for new organization const [sortAscending, setSortAscending] = useState(true); const [sortColumn, setSortColumn] = useState('Name'); const { state } = useContext(AppContext); const csrf = selectCsrfToken(state); + // Load organizations from the server const loadOrganizations = useCallback(async () => { const res = await resolve({ method: 'GET', @@ -65,21 +64,25 @@ const Organizations = ({ onlyMe = false }) => { setOrganizations([...organizations]); }, [sortAscending, sortColumn]); + // Add organization handler const addOrganization = useCallback(() => { - setSelectedOrganization({ name: '', description: '', website: '' }); + setNewOrganization({ name: '', description: '', website: '' }); // Reset the new organization form setOrganizationDialogOpen(true); }, []); + // Cancel organization creation/edit const cancelOrganization = useCallback(() => { setSelectedOrganization(null); setOrganizationDialogOpen(false); }, []); + // Confirm delete const confirmDelete = useCallback(organization => { setSelectedOrganization(organization); setConfirmDeleteOpen(true); }, []); + // Delete organization handler const deleteOrganization = useCallback(async organization => { organization.active = false; const res = await resolve({ @@ -97,18 +100,50 @@ const Organizations = ({ onlyMe = false }) => { setOrganizations(orgs => orgs.filter(org => org.id !== organization.id)); }, []); + // Edit organization handler const editOrganization = useCallback(org => { setSelectedOrganization(org); setOrganizationDialogOpen(true); }, []); + // Handle form submission for adding or editing an organization + const saveOrganization = useCallback(async () => { + const { id, name, description, website } = selectedOrganization || newOrganization; // Use selected or new organization + const url = id ? `${organizationBaseUrl}/${id}` : organizationBaseUrl; + + const res = await resolve({ + method: id ? 'PUT' : 'POST', + url, + headers: { + 'X-CSRF-Header': csrf, + Accept: 'application/json', + 'Content-Type': 'application/json;charset=UTF-8' + }, + data: { name, description, website } + }); + + if (res.error) return; + + const newOrg = res.payload.data; + + if (!id) { + setOrganizations(prevOrgs => [...prevOrgs, newOrg]); // Add new organization to the list + } else { + const index = organizations.findIndex(org => org.id === id); + organizations[index] = newOrg; // Update the edited organization + setOrganizations([...organizations]); + } + setOrganizationDialogOpen(false); // Close dialog after saving + }, [selectedOrganization, newOrganization, csrf]); + + // Render each organization row const organizationRow = useCallback( organization => ( {organization.name} {organization.description} - + website @@ -118,7 +153,6 @@ const Organizations = ({ onlyMe = false }) => { editOrganization(organization)} - style={{ color: 'black' }} // Default for light mode > @@ -127,7 +161,6 @@ const Organizations = ({ onlyMe = false }) => { confirmDelete(organization)} - style={{ color: 'black' }} // Default for light mode > @@ -136,168 +169,49 @@ const Organizations = ({ onlyMe = false }) => { )} ), - [] - ); - - const organizationDialog = useCallback( - () => ( - - - {selectedOrganization?.id ? 'Edit' : 'Add'} Organization - - - - setSelectedOrganization({ - ...selectedOrganization, - name: e.target.value - }) - } - value={selectedOrganization?.name ?? ''} - /> - - setSelectedOrganization({ - ...selectedOrganization, - description: e.target.value - }) - } - value={selectedOrganization?.description ?? ''} - /> - - setSelectedOrganization({ - ...selectedOrganization, - website: e.target.value - }) - } - value={selectedOrganization?.website ?? ''} - /> - - - - - - - ), - [organizationDialogOpen, selectedOrganization] + [editOrganization, confirmDelete, onlyMe] ); + // Organization table rendering const organizationsTable = useCallback( () => ( - - } - title="Organizations" - titleTypographyProps={{ variant: 'h5', component: 'h2' }} - /> - -
- - - - {sortableTableColumns.map(column => ( - - ))} - - {!onlyMe && ( - - )} - - - {organizations.map(organizationRow)} -
sortTable(column)} - style={{ cursor: 'pointer' }} - > - {column} - {sortIndicator(column)} - Website - Actions -
- - - -
- {onlyMe && ( -

- The administrator will edit and delete organizations to ensure accuracy. -

- )} -
-
+
+ + + + {sortableTableColumns.map(column => ( + + ))} + + {!onlyMe && ( + + )} + + + {organizations.map(organizationRow)} +
sortTable(column)} + style={{ cursor: 'pointer' }} + > + {column} + {sortIndicator(column)} + Website + Actions +
+ + {console.log("Add Organization button rendered")} + + +
), - [organizations, sortAscending, sortColumn] - ); - - const organizationValue = useCallback( - organization => { - switch (sortColumn) { - case 'Name': - return organization.name; - case 'Description': - return organization.description; - case 'Website': - return organization.website || ''; - } - }, - [sortColumn] + [organizations, sortAscending, sortColumn, organizationRow] ); - const saveOrganization = useCallback(async () => { - const { id, name, description, website } = selectedOrganization; - const url = id ? `${organizationBaseUrl}/${id}` : organizationBaseUrl; - - const res = await resolve({ - method: id ? 'PUT' : 'POST', - url, - headers: { - 'X-CSRF-Header': csrf, - Accept: 'application/json', - 'Content-Type': 'application/json;charset=UTF-8' - }, - data: { name, description, website } - }); - - if (res.error) return; - - const newOrg = res.payload.data; - - // Add the organization to both global and user's list - if (!id) { - organizations.push(newOrg); - } else { - const index = organizations.findIndex(org => org.id === id); - organizations[index] = newOrg; - } - setOrganizations([...organizations]); - setOrganizationDialogOpen(false); - }, [selectedOrganization]); - + // Sort organizations const sortOrganizations = useCallback( orgs => { orgs.sort((org1, org2) => { @@ -312,7 +226,7 @@ const Organizations = ({ onlyMe = false }) => { const sortIndicator = useCallback( column => { if (column !== sortColumn) return ''; - return ' ' + (sortAscending ? '🔼' : '🔽'); + return sortAscending ? '🔼' : '🔽'; }, [sortAscending, sortColumn] ); @@ -329,11 +243,78 @@ const Organizations = ({ onlyMe = false }) => { [sortAscending, sortColumn] ); + // Value sorting helper + const organizationValue = useCallback( + organization => { + switch (sortColumn) { + case 'Name': + return organization.name; + case 'Description': + return organization.description; + case 'Website': + return organization.website || ''; + default: + return ''; + } + }, + [sortColumn] + ); + return (
{organizationsTable()} - - {organizationDialog()} + {/* Dialog for adding/editing an organization */} + + + {selectedOrganization?.id ? 'Edit Organization' : 'Add Organization'} + + + + selectedOrganization + ? setSelectedOrganization({ ...selectedOrganization, name: e.target.value }) + : setNewOrganization({ ...newOrganization, name: e.target.value }) + } + value={selectedOrganization?.name ?? newOrganization.name} + /> + + selectedOrganization + ? setSelectedOrganization({ ...selectedOrganization, description: e.target.value }) + : setNewOrganization({ ...newOrganization, description: e.target.value }) + } + value={selectedOrganization?.description ?? newOrganization.description} + /> + + selectedOrganization + ? setSelectedOrganization({ ...selectedOrganization, website: e.target.value }) + : setNewOrganization({ ...newOrganization, website: e.target.value }) + } + value={selectedOrganization?.website ?? newOrganization.website} + /> + + + + + + { Organizations.propTypes = propTypes; -export default Organizations; \ No newline at end of file +export default Organizations; diff --git a/web-ui/src/components/volunteer/VolunteerEvents.jsx b/web-ui/src/components/volunteer/VolunteerEvents.jsx index 9722595a11..f0fb174e87 100644 --- a/web-ui/src/components/volunteer/VolunteerEvents.jsx +++ b/web-ui/src/components/volunteer/VolunteerEvents.jsx @@ -1,13 +1,10 @@ import PropTypes from 'prop-types'; import React, { useCallback, useContext, useEffect, useState } from 'react'; -import { AddCircleOutline, Delete, Edit, Event } from '@mui/icons-material'; +import { AddCircleOutline, Delete, Edit } from '@mui/icons-material'; import { Autocomplete, Button, - Card, - CardContent, - CardHeader, Dialog, DialogActions, DialogContent, @@ -21,11 +18,7 @@ import { resolve } from '../../api/api.js'; import DatePickerField from '../date-picker-field/DatePickerField'; import ConfirmationDialog from '../dialogs/ConfirmationDialog'; import { AppContext } from '../../context/AppContext'; -import { - selectCsrfToken, - selectCurrentUser, - selectProfileMap -} from '../../context/selectors'; +import { selectCsrfToken, selectCurrentUser, selectProfileMap } from '../../context/selectors'; import { formatDate } from '../../helpers/datetime'; const eventBaseUrl = '/services/volunteer/event'; @@ -73,9 +66,7 @@ const VolunteerEvents = ({ forceUpdate = () => {}, onlyMe = false }) => { // Only keep the events for my relationships. events = events.filter(e => Boolean(relationshipMap[e.relationshipId])); } - events.sort((event1, event2) => - event1.eventDate.localeCompare(event2.eventDate) - ); + events.sort((event1, event2) => event1.eventDate.localeCompare(event2.eventDate)); setEvents(events); }, [csrf, relationshipMap]); @@ -92,9 +83,7 @@ const VolunteerEvents = ({ forceUpdate = () => {}, onlyMe = false }) => { if (res.error) return; const organizations = res.payload.data; - setOrganizationMap( - organizations.reduce((acc, org) => ({ ...acc, [org.id]: org }), {}) - ); + setOrganizationMap(organizations.reduce((acc, org) => ({ ...acc, [org.id]: org }), {})); }, [csrf]); const loadRelationships = useCallback(async () => { @@ -119,9 +108,7 @@ const VolunteerEvents = ({ forceUpdate = () => {}, onlyMe = false }) => { return member1.name.localeCompare(member2.name); }); setRelationships(relationships); - setRelationshipMap( - relationships.reduce((acc, rel) => ({ ...acc, [rel.id]: rel }), {}) - ); + setRelationshipMap(relationships.reduce((acc, rel) => ({ ...acc, [rel.id]: rel }), {})); }, [csrf, onlyMe, profileMap]); useEffect(() => { @@ -183,11 +170,7 @@ const VolunteerEvents = ({ forceUpdate = () => {}, onlyMe = false }) => { const eventDialog = useCallback( () => ( - + {selectedEvent?.id ? 'Edit' : 'Add'} Event {}, onlyMe = false }) => { }} options={relationships} renderInput={params => ( - + )} - value={ - selectedEvent?.relationshipId - ? relationshipMap[selectedEvent.relationshipId] - : null - } + value={selectedEvent?.relationshipId ? relationshipMap[selectedEvent.relationshipId] : null} /> {}, onlyMe = false }) => { /> - setSelectedEvent({ ...selectedEvent, notes: e.target.value }) - } + onChange={e => setSelectedEvent({ ...selectedEvent, notes: e.target.value })} value={selectedEvent?.notes ?? ''} /> @@ -270,10 +243,7 @@ const VolunteerEvents = ({ forceUpdate = () => {}, onlyMe = false }) => { - confirmDelete(event)} - > + confirmDelete(event)}> @@ -290,58 +260,29 @@ const VolunteerEvents = ({ forceUpdate = () => {}, onlyMe = false }) => { if (isEmpty(relationshipMap)) return null; return ( - - } - title="Events" - titleTypographyProps={{ variant: 'h5', component: 'h2' }} - /> - -
- - - - {sortableTableColumns.map(column => ( - - ))} - - - - {events.map(eventRow)} -
sortTable(column)} - style={{ cursor: 'pointer' }} - > - {column} - {sortIndicator(column)} - - Actions -
- - - -
- {onlyMe && ( -

- Create events for your organizations. -

- )} -
-
+
+ + + + {sortableTableColumns.map(column => ( + + ))} + + + + {events.map(eventRow)} +
sortTable(column)} style={{ cursor: 'pointer' }}> + {column} + {sortIndicator(column)} + + Actions +
+ + + +
); - }, [ - events, - organizationMap, - profileMap, - relationshipMap, - sortAscending, - sortColumn - ]); + }, [events, organizationMap, profileMap, relationshipMap, sortAscending, sortColumn]); const getDate = dateString => { if (!dateString) return null; diff --git a/web-ui/src/components/volunteer/VolunteerRelationships.jsx b/web-ui/src/components/volunteer/VolunteerRelationships.jsx index e074b8ff12..00b8272a0f 100644 --- a/web-ui/src/components/volunteer/VolunteerRelationships.jsx +++ b/web-ui/src/components/volunteer/VolunteerRelationships.jsx @@ -1,13 +1,10 @@ import PropTypes from 'prop-types'; import React, { useCallback, useContext, useEffect, useState } from 'react'; -import { AddCircleOutline, Delete, Edit, Handshake } from '@mui/icons-material'; +import { AddCircleOutline, Delete, Edit } from '@mui/icons-material'; import { Autocomplete, Button, - Card, - CardContent, - CardHeader, IconButton, TextField, Tooltip, @@ -18,16 +15,11 @@ import { } from '@mui/material'; import { resolve } from '../../api/api'; -import { createNewOrganization } from '../../api/volunteer'; // Importing the new API function import DatePickerField from '../date-picker-field/DatePickerField'; import ConfirmationDialog from '../dialogs/ConfirmationDialog'; -import OrganizationDialog from '../dialogs/OrganizationDialog'; // Importing the new reusable component +import OrganizationDialog from '../dialogs/OrganizationDialog'; import { AppContext } from '../../context/AppContext'; -import { - selectCsrfToken, - selectCurrentUser, - selectProfileMap -} from '../../context/selectors'; +import { selectCsrfToken, selectCurrentUser, selectProfileMap } from '../../context/selectors'; import { formatDate } from '../../helpers/datetime'; const relationshipBaseUrl = '/services/volunteer/relationship'; @@ -39,7 +31,7 @@ const VolunteerRelationships = ({ forceUpdate = () => {}, onlyMe = false }) => { const [organizationMap, setOrganizationMap] = useState({}); const [organizations, setOrganizations] = useState([]); const [relationshipDialogOpen, setRelationshipDialogOpen] = useState(false); - const [organizationDialogOpen, setOrganizationDialogOpen] = useState(false); // New dialog for adding an organization + const [organizationDialogOpen, setOrganizationDialogOpen] = useState(false); const [newOrganization, setNewOrganization] = useState({ name: '', description: '', website: '' }); const [relationshipMap, setRelationshipMap] = useState({}); const [relationships, setRelationships] = useState([]); @@ -51,12 +43,13 @@ const VolunteerRelationships = ({ forceUpdate = () => {}, onlyMe = false }) => { const csrf = selectCsrfToken(state); const currentUser = selectCurrentUser(state); const profileMap = selectProfileMap(state); - const profiles = Object.values(profileMap).filter(profile => profile && profile.name); // Filter out undefined profiles + const profiles = Object.values(profileMap).filter(profile => profile && profile.name); profiles.sort((a, b) => a.name.localeCompare(b.name)); const sortableTableColumns = ['Organization', 'Start Date', 'End Date']; if (!onlyMe) sortableTableColumns.unshift('Member'); + // Fetch organizations const loadOrganizations = useCallback(async () => { const res = await resolve({ method: 'GET', @@ -75,8 +68,9 @@ const VolunteerRelationships = ({ forceUpdate = () => {}, onlyMe = false }) => { setOrganizationMap( organizations.reduce((acc, org) => ({ ...acc, [org.id]: org }), {}) ); - }, []); + }, [csrf]); + // Fetch relationships const loadRelationships = useCallback(async () => { let url = relationshipBaseUrl; if (onlyMe) url += '?memberId=' + currentUser.id; @@ -98,36 +92,39 @@ const VolunteerRelationships = ({ forceUpdate = () => {}, onlyMe = false }) => { return (member1?.name || '').localeCompare(member2?.name || ''); }); setRelationships(relationships); - }, [organizationMap]); + }, [currentUser.id, onlyMe, profileMap]); useEffect(() => { loadOrganizations(); - }, []); + }, [loadOrganizations]); useEffect(() => { if (Object.keys(organizationMap).length > 0) loadRelationships(); - }, [organizationMap]); + }, [organizationMap, loadRelationships]); - useEffect(() => { - sortRelationships(relationships); - setRelationships([...relationships]); - }, [sortAscending, sortColumn]); + const refreshRelationships = async () => { + await loadRelationships(); + }; const addRelationship = useCallback(() => { setSelectedRelationship({ memberId: onlyMe ? currentUser.id : '', organizationId: '', - startDate: '', - endDate: '' + startDate: null, // Ensure this is a valid date object + endDate: null // Ensure this is a valid date object }); setRelationshipDialogOpen(true); - }, []); + }, [currentUser.id, onlyMe]); const cancelRelationship = useCallback(() => { setSelectedRelationship(null); setRelationshipDialogOpen(false); }, []); + const cancelOrganizationCreation = useCallback(() => { + setOrganizationDialogOpen(false); + }, []); + const confirmDelete = useCallback(relationship => { setSelectedRelationship(relationship); setConfirmDeleteOpen(true); @@ -135,7 +132,6 @@ const VolunteerRelationships = ({ forceUpdate = () => {}, onlyMe = false }) => { const deleteRelationship = useCallback(async relationship => { if (!relationship) return; - relationship.active = false; const res = await resolve({ method: 'PUT', url: relationshipBaseUrl + '/' + relationship.id, @@ -144,306 +140,103 @@ const VolunteerRelationships = ({ forceUpdate = () => {}, onlyMe = false }) => { Accept: 'application/json', 'Content-Type': 'application/json;charset=UTF-8' }, - data: relationship + data: { ...relationship, active: false } }); if (res.error) return; - setRelationships(orgs => orgs.filter(org => org.id !== relationship.id)); - }, []); - - const editRelationship = useCallback( - relationship => { - setSelectedRelationship(relationship); - setRelationshipDialogOpen(true); - }, - [relationshipMap] - ); + // Refresh the relationships list after deletion + await refreshRelationships(); + setConfirmDeleteOpen(false); + }, [csrf, refreshRelationships]); - const getDate = dateString => { - if (!dateString) return null; - const [year, month, day] = dateString.split('-'); - return new Date(Number(year), Number(month) - 1, Number(day)); - }; + const saveRelationship = useCallback(async () => { + const { id, organizationId, startDate, endDate } = selectedRelationship; - const handleCreateNewOrganization = async () => { - if (!newOrganization.name || !newOrganization.description) { - console.error('Organization name and description are required.'); + if (!organizationId || !startDate || !endDate) { + console.error('Organization, start date, or end date missing'); return; } - const res = await createNewOrganization(csrf, newOrganization); // Call the API function + const formattedStartDate = formatDate(new Date(startDate)); + const formattedEndDate = formatDate(new Date(endDate)); + const res = await resolve({ + method: id ? 'PUT' : 'POST', + url: id ? `${relationshipBaseUrl}/${id}` : relationshipBaseUrl, + headers: { + 'X-CSRF-Header': csrf, + Accept: 'application/json', + 'Content-Type': 'application/json;charset=UTF-8' + }, + data: { ...selectedRelationship, startDate: formattedStartDate, endDate: formattedEndDate } + }); if (res.error) { - console.error('Error saving new organization', res.error); + console.error("Error saving relationship", res.error); return; } - const newOrg = res.payload.data; - - if (newOrg && newOrg.name) { - setOrganizations([...organizations, newOrg]); // Add new organization to the list - setOrganizationMap({ ...organizationMap, [newOrg.id]: newOrg }); - setSelectedRelationship({ - ...selectedRelationship, - organizationId: newOrg.id - }); - setOrganizationDialogOpen(false); // Close the organization creation dialog - } else { - console.error('Invalid organization data received'); - } - }; - - const relationshipRow = useCallback( - relationship => { - const org = organizationMap[relationship.organizationId]; - if (!org) { - console.error('Organization not found for relationship:', relationship); - return null; - } - return ( - - {!onlyMe && {profileMap[relationship.memberId]?.name ?? ''}} - {org.name ?? 'N/A'} {/* Defensive check */} - {relationship.startDate} - {relationship.endDate} - - - editRelationship(relationship)} - > - - - - - confirmDelete(relationship)} - > - - - - - - ); - }, - [organizationMap, profileMap, editRelationship, confirmDelete, onlyMe] - ); - - const relationshipDialog = useCallback( - () => ( - - - {selectedRelationship?.id ? 'Edit' : 'Add'} Relationship - - - {!onlyMe && ( - profile?.name ?? ''} - isOptionEqualToValue={(option, value) => option.id === value.id} - onChange={(event, profile) => { - setSelectedRelationship({ - ...selectedRelationship, - memberId: profile.id - }); - }} - options={profiles} - renderInput={params => ( - - )} - value={ - selectedRelationship?.memberId - ? profileMap[selectedRelationship.memberId] - : null - } - /> - )} - organization?.name ?? ''} - isOptionEqualToValue={(option, value) => option.id === value.id} - onChange={(event, organization) => { - if (organization && organization.inputValue === 'Add new organization') { - setOrganizationDialogOpen(true); // Open the dialog to create a new organization - } else { - setSelectedRelationship({ - ...selectedRelationship, - organizationId: organization.id - }); - } - }} - options={[...organizations, { name: 'Add new organization', inputValue: 'Add new organization' }]} - renderInput={params => ( - - )} - value={ - selectedRelationship?.organizationId - ? organizationMap[selectedRelationship.organizationId] - : null - } - /> - { - const startDate = formatDate(date); - let { endDate } = selectedRelationship; - if (endDate && startDate > endDate) endDate = startDate; - setSelectedRelationship({ - ...selectedRelationship, - startDate, - endDate - }); - }} - /> - { - const endDate = date ? formatDate(date) : ''; - let { startDate } = selectedRelationship; - if (endDate && (!startDate || endDate < startDate)) { - startDate = endDate; - } - setSelectedRelationship({ - ...selectedRelationship, - startDate, - endDate - }); - }} - /> - - - - - - - ), - [relationshipDialogOpen, selectedRelationship, onlyMe, profiles, profileMap, organizationMap, organizations] - ); + await refreshRelationships(); // Refresh the list after saving + setSelectedRelationship(null); + setRelationshipDialogOpen(false); + }, [selectedRelationship, csrf, refreshRelationships]); - const relationshipsTable = useCallback( - () => ( - - } - title={onlyMe ? 'Organizations' : 'Relationships'} - titleTypographyProps={{ variant: 'h5', component: 'h2' }} - /> - -
- - - - {sortableTableColumns.map(column => ( - - ))} - - - - {relationships.map(relationshipRow)} -
sortTable(column)} - style={{ cursor: 'pointer' }} - > - {column} - {sortIndicator(column)} - - Actions -
- - - -
-
-
- ), - [relationships, sortAscending, sortColumn, relationshipRow] - ); + const openCreateOrganizationDialog = useCallback(() => { + setNewOrganization({ name: '', description: '', website: '' }); + setOrganizationDialogOpen(true); + }, []); - const relationshipValue = useCallback( - relationship => { - switch (sortColumn) { - case 'Member': - return profileMap[relationship.memberId]?.name ?? ''; - case 'Organization': - return organizationMap[relationship.organizationId]?.name ?? ''; - case 'Start Date': - return relationship.startDate || ''; - case 'End Date': - return relationship.endDate || ''; - } - }, - [relationshipMap, sortColumn] - ); + const saveOrganizationAndRelationship = useCallback(async () => { + const { name, description, website } = newOrganization; + if (!name || !description) { + console.error('Missing organization name or description'); + return; + } - const saveRelationship = useCallback(async () => { - const { id } = selectedRelationship; const res = await resolve({ - method: id ? 'PUT' : 'POST', - url: id ? `${relationshipBaseUrl}/${id}` : relationshipBaseUrl, + method: 'POST', + url: '/services/volunteer/organization', headers: { 'X-CSRF-Header': csrf, Accept: 'application/json', 'Content-Type': 'application/json;charset=UTF-8' }, - data: selectedRelationship + data: { name, description, website } }); if (res.error) { - console.error("Error saving relationship", res.error); + console.error('Error creating organization', res.error); return; } - const newRel = res.payload.data; + const createdOrg = res.payload.data; - if (id) { - const index = relationships.findIndex(rel => rel.id === id); - relationships[index] = newRel; - } else { - relationships.push(newRel); - } - sortRelationships(relationships); - setRelationships(relationships); + // Update the organization map with the new organization + setOrganizationMap(prev => ({ ...prev, [createdOrg.id]: createdOrg })); - setSelectedRelationship(null); - setRelationshipDialogOpen(false); - }, [selectedRelationship]); + // Set the relationship to reference the newly created organization by its name + setSelectedRelationship({ + ...selectedRelationship, + organizationId: createdOrg.id + }); + + setOrganizationDialogOpen(false); + setRelationshipDialogOpen(true); + }, [csrf, newOrganization, selectedRelationship]); const sortRelationships = useCallback( orgs => { orgs.sort((org1, org2) => { - const v1 = relationshipValue(org1); - const v2 = relationshipValue(org2); + const v1 = profileMap[org1.memberId]?.name ?? ''; + const v2 = profileMap[org2.memberId]?.name ?? ''; return sortAscending ? v1.localeCompare(v2) : v2.localeCompare(v1); }); }, - [sortAscending, sortColumn] + [sortAscending, profileMap] ); const sortIndicator = useCallback( column => { if (column !== sortColumn) return ''; - return ' ' + (sortAscending ? '🔼' : '🔽'); + return sortAscending ? '🔼' : '🔽'; }, [sortAscending, sortColumn] ); @@ -460,21 +253,120 @@ const VolunteerRelationships = ({ forceUpdate = () => {}, onlyMe = false }) => { [sortAscending, sortColumn] ); - const validRelationship = useCallback(() => { - const rel = selectedRelationship; - return rel?.memberId && rel?.organizationId; - }); - return (
- {relationshipsTable()} - - {relationshipDialog()} + {/* Table for showing relationships */} +
+ + + + {sortableTableColumns.map(column => ( + + ))} + + + + + {relationships.map(relationship => ( + + {!onlyMe && ( + + )} + + + + + + ))} + +
sortTable(column)} + style={{ cursor: 'pointer' }} + > + {column} + {sortIndicator(column)} + + Actions +
{profileMap[relationship.memberId]?.name ?? ''}{organizationMap[relationship.organizationId]?.name ?? 'N/A'}{relationship.startDate}{relationship.endDate} + + { + setSelectedRelationship(relationship); + setRelationshipDialogOpen(true); + }} + > + + + + + confirmDelete(relationship)} + > + + + +
+ + + +
+ + {/* Message below the table */} +

The administrator may edit organizations to ensure accuracy.

+ + {/* Dialog for creating/editing a relationship */} + + + {selectedRelationship?.id ? 'Edit Relationship' : 'Add Relationship'} + + + (organizationMap[option]?.name || option)} + options={['Create a New Organization', ...Object.keys(organizationMap)]} + onChange={(event, value) => { + if (value === 'Create a New Organization') { + setRelationshipDialogOpen(false); // Close the relationship dialog + openCreateOrganizationDialog(); // Open the organization creation dialog + } else { + setSelectedRelationship({ ...selectedRelationship, organizationId: value }); + } + }} + renderInput={(params) => ( + + )} + value={selectedRelationship?.organizationId || ''} + /> + setSelectedRelationship({ ...selectedRelationship, startDate: date })} + /> + setSelectedRelationship({ ...selectedRelationship, endDate: date })} + /> + + + + + + + {/* Dialog for creating a new organization */} setOrganizationDialogOpen(false)} - onSave={handleCreateNewOrganization} + onClose={cancelOrganizationCreation} + onSave={saveOrganizationAndRelationship} organization={newOrganization} setOrganization={setNewOrganization} /> diff --git a/web-ui/src/components/volunteer/VolunteerTables.css b/web-ui/src/components/volunteer/VolunteerTables.css index 51b52086a8..10dbe9a62b 100644 --- a/web-ui/src/components/volunteer/VolunteerTables.css +++ b/web-ui/src/components/volunteer/VolunteerTables.css @@ -15,11 +15,16 @@ width: 100%; } +.volunteer-activities-card { + width: 100%; + margin: 1rem auto; + box-shadow: var(--checkins-palette-shadow); +} + .volunteer-tables { - display: flex; - flex-direction: column; - align-items: center; - position: relative; + .volunteer-relationships { + width: 100%; + } .manage-btn { position: absolute; @@ -38,6 +43,7 @@ table { border-collapse: collapse; + width: 100%; img { max-height: 5rem; diff --git a/web-ui/src/components/volunteer/VolunteerTables.jsx b/web-ui/src/components/volunteer/VolunteerTables.jsx index 89a671e817..d1073c1530 100644 --- a/web-ui/src/components/volunteer/VolunteerTables.jsx +++ b/web-ui/src/components/volunteer/VolunteerTables.jsx @@ -1,10 +1,12 @@ import PropTypes from 'prop-types'; import React, { useReducer, useState } from 'react'; -import { Box, Tab, Tabs } from '@mui/material'; +import { Box, Tab, Tabs, Typography, Card, CardHeader, CardContent, Avatar } from '@mui/material'; -import Organizations from './Organizations'; import VolunteerEvents from './VolunteerEvents'; import VolunteerRelationships from './VolunteerRelationships'; +import GroupIcon from '@mui/icons-material/Group'; +import EventIcon from '@mui/icons-material/Event'; +import HandshakeIcon from '@mui/icons-material/Handshake'; // Adding Handshake Icon import './VolunteerTables.css'; @@ -34,67 +36,68 @@ TabPanel.displayName = 'TabPanel'; const propTypes = { onlyMe: PropTypes.bool }; + const VolunteerReportPage = ({ onlyMe = false }) => { const [n, forceUpdate] = useReducer(n => n + 1, 0); const [tabIndex, setTabIndex] = useState(0); return ( -
- setTabIndex(index)} - textColor="inherit" - value={tabIndex} - variant="fullWidth" - > - {/* Add sx prop to style each Tab */} - - - - - - - - - - - - - -
+ + + setTabIndex(index)} + textColor="inherit" + value={tabIndex} + variant="fullWidth" + > + + + + + Volunteer Orgs + + } + {...a11yProps(0)} + sx={{ + minWidth: '150px', + whiteSpace: 'nowrap' + }} + /> + + + + + Events + + } + {...a11yProps(1)} + sx={{ + minWidth: '150px', + whiteSpace: 'nowrap' + }} + /> + + + + + + + + + ); }; diff --git a/web-ui/src/pages/ProfilePage.jsx b/web-ui/src/pages/ProfilePage.jsx index 986364e0c7..7adee7e709 100644 --- a/web-ui/src/pages/ProfilePage.jsx +++ b/web-ui/src/pages/ProfilePage.jsx @@ -19,7 +19,7 @@ import ProgressBar from '../components/contribution_hours/ProgressBar'; import VolunteerTables from '../components/volunteer/VolunteerTables'; import { Info } from '@mui/icons-material'; -import { Card, CardContent, CardHeader, Chip, TextField } from '@mui/material'; +import { Card, CardContent, CardHeader, Chip, TextField, Avatar } from '@mui/material'; import GroupIcon from '@mui/icons-material/Group'; import Autocomplete from '@mui/material/Autocomplete'; @@ -149,7 +149,7 @@ const ProfilePage = () => {
} + avatar={} title="Bio" titleTypographyProps={{ variant: 'h5', component: 'h2' }} /> @@ -171,7 +171,7 @@ const ProfilePage = () => { {myHours && ( } + avatar={} subheader={`As Of: ${new Date( myHours?.asOfDate ).toLocaleDateString()}`} @@ -189,8 +189,8 @@ const ProfilePage = () => {
} - title="Guilds" + avatar={} + title="Guilds & Communities" titleTypographyProps={{ variant: 'h5', component: 'h2' }} /> @@ -218,7 +218,7 @@ const ProfilePage = () => {
} + avatar={} title="Teams" titleTypographyProps={{ variant: 'h5', component: 'h1' }} /> diff --git a/web-ui/src/pages/__snapshots__/CheckinsPage.test.jsx.snap b/web-ui/src/pages/__snapshots__/CheckinsPage.test.jsx.snap index 6baa57d4ed..ba49726a5f 100644 --- a/web-ui/src/pages/__snapshots__/CheckinsPage.test.jsx.snap +++ b/web-ui/src/pages/__snapshots__/CheckinsPage.test.jsx.snap @@ -101,8 +101,8 @@ exports[`renders correctly 1`] = ` @@ -114,7 +114,7 @@ exports[`renders correctly 1`] = ` aria-label="Choose date, selected date is Oct 21, 2020" autocomplete="off" class="MuiInputBase-input MuiOutlinedInput-input MuiInputBase-inputAdornedEnd Mui-readOnly MuiInputBase-readOnly css-nxo287-MuiInputBase-input-MuiOutlinedInput-input" - id=":r3:" + id=":r1:" inputmode="text" placeholder="MMMM DD, YYYY @hh:mm aa" readonly=""