From 8b4efb7e6355ede456ae8f038fd3bf831059dd0e Mon Sep 17 00:00:00 2001 From: dalemcgrew Date: Sun, 23 Feb 2025 18:39:52 -0800 Subject: [PATCH] Added support for PersonAway initial create. Still need to work on updating and list retrieve. --- package-lock.json | 126 ++++++++ package.json | 3 + .../components/Person/EditPersonAwayForm.jsx | 274 ++++++++++++++++++ src/js/components/Person/EditPersonForm.jsx | 42 +-- .../Person/PersonProfileDrawerMainContent.jsx | 28 ++ .../Task/EditTaskDefinitionForm.jsx | 4 +- src/js/controllers/PersonController.js | 45 +++ src/js/models/PersonModel.jsx | 4 + src/js/react-query/mutations.jsx | 10 + 9 files changed, 513 insertions(+), 23 deletions(-) create mode 100644 src/js/components/Person/EditPersonAwayForm.jsx diff --git a/package-lock.json b/package-lock.json index f41d9df..41013ec 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,11 +9,13 @@ "version": "1.0.0", "license": "MIT", "dependencies": { + "@date-io/dayjs": "^3.2.0", "@mui/icons-material": "^5.16.0", "@mui/lab": "^5.0.0-alpha.96", "@mui/material": "^5.16.0", "@mui/styled-engine-sc": "^6.3.0", "@mui/styles": "^5.16.0", + "@mui/x-date-pickers": "^7.27.0", "@openreplay/tracker": "^8.1.1", "@tanstack/react-query": "^5.64.1", "@tanstack/react-query-devtools": "^5.62.11", @@ -23,6 +25,7 @@ "d3-geo": "^2.0.1", "d3-selection": "^2.0.0", "d3-zoom": "^3.0.0", + "dayjs": "^1.11.13", "depcheck": "^1.4.7", "flux": "^4.0.4", "fs-extra": "^10.0.0", @@ -2036,6 +2039,29 @@ "node": ">=18" } }, + "node_modules/@date-io/core": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@date-io/core/-/core-3.2.0.tgz", + "integrity": "sha512-hqwXvY8/YBsT9RwQITG868ZNb1MVFFkF7W1Ecv4P472j/ZWa7EFcgSmxy8PUElNVZfvhdvfv+a8j6NWJqOX5mA==", + "license": "MIT" + }, + "node_modules/@date-io/dayjs": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@date-io/dayjs/-/dayjs-3.2.0.tgz", + "integrity": "sha512-+3LV+3N+cpQbEtmrFo8odg07k02AFY7diHgbi2EKYYANOOCPkDYUjDr2ENiHuYNidTs3tZwzDKckZoVNN4NXxg==", + "license": "MIT", + "dependencies": { + "@date-io/core": "^3.2.0" + }, + "peerDependencies": { + "dayjs": "^1.8.17" + }, + "peerDependenciesMeta": { + "dayjs": { + "optional": true + } + } + }, "node_modules/@discoveryjs/json-ext": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", @@ -3683,6 +3709,100 @@ "node": ">=6" } }, + "node_modules/@mui/x-date-pickers": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@mui/x-date-pickers/-/x-date-pickers-7.27.0.tgz", + "integrity": "sha512-wSx8JGk4WQ2hTObfQITc+zlmUKNleQYoH1hGocaQlpWpo1HhauDtcQfX6sDN0J0dPT2eeyxDWGj4uJmiSfQKcw==", + "dependencies": { + "@babel/runtime": "^7.25.7", + "@mui/utils": "^5.16.6 || ^6.0.0", + "@mui/x-internals": "7.26.0", + "@types/react-transition-group": "^4.4.11", + "clsx": "^2.1.1", + "prop-types": "^15.8.1", + "react-transition-group": "^4.4.5" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.9.0", + "@emotion/styled": "^11.8.1", + "@mui/material": "^5.15.14 || ^6.0.0", + "@mui/system": "^5.15.14 || ^6.0.0", + "date-fns": "^2.25.0 || ^3.2.0 || ^4.0.0", + "date-fns-jalali": "^2.13.0-0 || ^3.2.0-0 || ^4.0.0-0", + "dayjs": "^1.10.7", + "luxon": "^3.0.2", + "moment": "^2.29.4", + "moment-hijri": "^2.1.2 || ^3.0.0", + "moment-jalaali": "^0.7.4 || ^0.8.0 || ^0.9.0 || ^0.10.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + }, + "date-fns": { + "optional": true + }, + "date-fns-jalali": { + "optional": true + }, + "dayjs": { + "optional": true + }, + "luxon": { + "optional": true + }, + "moment": { + "optional": true + }, + "moment-hijri": { + "optional": true + }, + "moment-jalaali": { + "optional": true + } + } + }, + "node_modules/@mui/x-date-pickers/node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/@mui/x-internals": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@mui/x-internals/-/x-internals-7.26.0.tgz", + "integrity": "sha512-VxTCYQcZ02d3190pdvys2TDg9pgbvewAVakEopiOgReKAUhLdRlgGJHcOA/eAuGLyK1YIo26A6Ow6ZKlSRLwMg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.25.7", + "@mui/utils": "^5.16.6 || ^6.0.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/@nicolo-ribaudo/eslint-scope-5-internals": { "version": "5.1.1-v1", "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz", @@ -8015,6 +8135,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/dayjs": { + "version": "1.11.13", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz", + "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==", + "license": "MIT" + }, "node_modules/debounce": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.1.tgz", diff --git a/package.json b/package.json index f31c9b6..7e99054 100644 --- a/package.json +++ b/package.json @@ -85,11 +85,13 @@ "webpack-shell-plugin-next": "^2.3.1" }, "dependencies": { + "@date-io/dayjs": "^3.2.0", "@mui/icons-material": "^5.16.0", "@mui/lab": "^5.0.0-alpha.96", "@mui/material": "^5.16.0", "@mui/styled-engine-sc": "^6.3.0", "@mui/styles": "^5.16.0", + "@mui/x-date-pickers": "^7.27.0", "@openreplay/tracker": "^8.1.1", "@tanstack/react-query": "^5.64.1", "@tanstack/react-query-devtools": "^5.62.11", @@ -99,6 +101,7 @@ "d3-geo": "^2.0.1", "d3-selection": "^2.0.0", "d3-zoom": "^3.0.0", + "dayjs": "^1.11.13", "depcheck": "^1.4.7", "flux": "^4.0.4", "fs-extra": "^10.0.0", diff --git a/src/js/components/Person/EditPersonAwayForm.jsx b/src/js/components/Person/EditPersonAwayForm.jsx new file mode 100644 index 0000000..f46b1d4 --- /dev/null +++ b/src/js/components/Person/EditPersonAwayForm.jsx @@ -0,0 +1,274 @@ +import { Button, FormControl, FormControlLabel, FormLabel, Radio, RadioGroup, TextField } from '@mui/material'; +import { DatePicker } from '@mui/x-date-pickers/DatePicker'; +import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; +import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; +import dayjs from 'dayjs'; +import { withStyles } from '@mui/styles'; +import PropTypes from 'prop-types'; +import React, { useEffect, useRef, useState } from 'react'; +import styled from 'styled-components'; +import { renderLog } from '../../common/utils/logging'; +import { getPersonAwayParamsToSave, getPersonAwayLabel, getPersonAwayReason } from '../../controllers/PersonController'; +import { PERSON_AWAY_REASONS, PERSON_AWAY_REASONS_WITH_HR } from '../../models/PersonModel'; +import makeRequestParams from '../../react-query/makeRequestParams'; +import { usePersonAwaySaveMutation } from '../../react-query/mutations'; +import { SpanWithLinkStyle } from '../Style/linkStyles'; + +// const PERSON_AWAY_FIELDS_ACCEPTED = [ +// 'awayDescription', +// 'awayDescriptionForTeamLeads', +// 'dateEnd', +// 'dateEndEstimated', +// 'dateStart', +// 'dateSubmitted', +// ]; + +const EditPersonAwayForm = ({ classes, personId }) => { + renderLog('EditPersonAwayForm'); + const { mutate: personAwaySave } = usePersonAwaySaveMutation(); + + const [awayDescriptionForTeamLeadsValue, setAwayDescriptionForTeamLeadsValue] = useState(''); + const [awayDescriptionValue, setAwayDescriptionValue] = useState(''); + const [awayReasonRadioValue, setAwayReasonRadioValue] = useState('isVacation'); + const [personAway] = useState({}); + const [dateEnd, setDateEnd] = useState(dayjs().add(1, 'week')); + const [dateEndEstimated, setDateEndEstimated] = useState(dayjs().add(1, 'week')); + const [saveButtonActive, setSaveButtonActive] = useState(true); + const [showAwayDescriptionForTeamLeads, setShowAwayDescriptionForTeamLeads] = useState(false); + const [dateStart, setDateStart] = useState(dayjs()); + + const awayReasonInputRef = useRef(''); + const awayDescriptionForTeamLeadsInputRef = useRef(''); + const awayDescriptionInputRef = useRef(''); + const dateEndInputRef = useRef(''); + const dateEndEstimatedInputRef = useRef(''); + const dateStartInputRef = useRef(''); + + useEffect(() => { + if (personAway) { + setAwayDescriptionForTeamLeadsValue(personAway.awayDescriptionForTeamLeads); + setAwayDescriptionValue(personAway.awayDescription); + setAwayReasonRadioValue(getPersonAwayReason(personAway)); + } else { + setAwayDescriptionForTeamLeadsValue(''); + setAwayDescriptionValue(''); + setAwayReasonRadioValue('isVacation'); + } + }, [personAway]); + + const savePersonAway = () => { + const plainParams = { + personAwayId: (personAway && personAway.id >= 0) ? personAway.id : -1, + personId, + }; + // Need to break up awayReasonRadioValue into "isX" fields and send them as separate params + const updatedPersonAwayIsValues = getPersonAwayParamsToSave(awayReasonRadioValue); + const params = { + ...updatedPersonAwayIsValues, + awayDescription: awayDescriptionInputRef.current.value, + awayDescriptionForTeamLeads: awayDescriptionForTeamLeadsInputRef.current.value, + dateEnd: dateEnd.format('YYYY-MM-DD'), + dateEndEstimated: dateEndEstimated.format('YYYY-MM-DD'), + dateStart: dateStart.format('YYYY-MM-DD'), + }; + console.log('savePersonAway params:', params); + const requestParams = makeRequestParams(plainParams, params); + personAwaySave(requestParams); + // console.log('saveQuestionnaire requestParams:', requestParams); + setSaveButtonActive(false); + }; + + const updateSaveButton = () => { + if (awayDescriptionInputRef.current.value && awayDescriptionInputRef.current.value.length) { + if (!saveButtonActive) { + setSaveButtonActive(true); + } + } + }; + + const handleRadioChange = (event) => { + setAwayReasonRadioValue(event.target.value); + if (!saveButtonActive) { + setSaveButtonActive(true); + } + }; + + return ( + + + Why I'm Away + + {PERSON_AWAY_REASONS_WITH_HR.map((reason) => ( + } + key={reason} + label={getPersonAwayLabel(reason)} + value={reason} + /> + ))} + + + + updateSaveButton()} + placeholder="Hi team, I'm going to be away..." + rows={4} + variant="outlined" + /> +
+ {showAwayDescriptionForTeamLeads ? ( + setShowAwayDescriptionForTeamLeads(false)}>Hide explanation for team lead(s) + ) : ( + setShowAwayDescriptionForTeamLeads(true)}>Add explanation for team lead(s) only + )} +
+ updateSaveButton()} + placeholder="I'm going to be away for 2 weeks, starting next week..." + rows={3} + sx={!showAwayDescriptionForTeamLeads && { + position: 'absolute', + left: '-9999px', + width: '1px', + height: '1px', + overflow: 'hidden', + }} + variant="outlined" + /> +
+ + + + + { + setDateStart(newValue); + updateSaveButton(); + }} + renderInput={() => ( + + )} + /> + + + { + setDateEndEstimated(newValue); + updateSaveButton(); + }} + renderInput={() => ( + + )} + /> + + + { + setDateEnd(newValue); + updateSaveButton(); + }} + renderInput={() => ( + + )} + /> + + + + + + + +
+ ); +}; +EditPersonAwayForm.propTypes = { + classes: PropTypes.object.isRequired, + personId: PropTypes.number.isRequired, +}; + +const styles = (theme) => ({ + checkboxRoot: { + paddingTop: 0, + paddingLeft: '9px', + paddingBottom: 0, + }, + checkboxLabel: { + marginTop: 2, + }, + dateFormControl: { + marginTop: 20, + }, + formControl: { + width: '100%', + }, + savePersonAwayButton: { + marginTop: 20, + width: 300, + [theme.breakpoints.down('md')]: { + width: '100%', + }, + }, +}); + +const DateWrapper = styled('div')` + margin-right: 4; +`; + +const DateOptionsWrapper = styled('div')` + display: flex; + align-items: center; + justify-content: space-between; +`; + +const EditPersonAwayFormWrapper = styled('div')` +`; + +export default withStyles(styles)(EditPersonAwayForm); diff --git a/src/js/components/Person/EditPersonForm.jsx b/src/js/components/Person/EditPersonForm.jsx index 12892f4..d969ed4 100644 --- a/src/js/components/Person/EditPersonForm.jsx +++ b/src/js/components/Person/EditPersonForm.jsx @@ -21,22 +21,22 @@ const EditPersonForm = ({ classes }) => { // const [initialPerson] = useState(useGetPersonById(getAppContextValue('personDrawersPersonId'))); const [activePerson, setActivePerson] = useState({ ...initialPerson }); - const emailPersonal = useRef(''); - const firstName = useRef(''); - const firstNamePreferred = useRef(''); - const jobTitle = useRef(''); - const lastName = useRef(''); - const location = useRef(''); - const stateCode = useRef(''); + const emailPersonalInputRef = useRef(''); + const firstNameInputRef = useRef(''); + const firstNamePreferredInputRef = useRef(''); + const jobTitleInputRef = useRef(''); + const lastNameInputRef = useRef(''); + const locationInputRef = useRef(''); + const stateCodeInputRef = useRef(''); const savePerson = () => { - activePerson.emailPersonal = emailPersonal.current.value; - activePerson.firstName = firstName.current.value; - activePerson.firstNamePreferred = firstNamePreferred.current.value; - activePerson.jobTitle = jobTitle.current.value; - activePerson.lastName = lastName.current.value; - activePerson.location = location.current.value; - activePerson.stateCode = stateCode.current.value; + activePerson.emailPersonal = emailPersonalInputRef.current.value; + activePerson.firstName = firstNameInputRef.current.value; + activePerson.firstNamePreferred = firstNamePreferredInputRef.current.value; + activePerson.jobTitle = jobTitleInputRef.current.value; + activePerson.lastName = lastNameInputRef.current.value; + activePerson.location = locationInputRef.current.value; + activePerson.stateCode = stateCodeInputRef.current.value; setActivePerson(activePerson); // console.log('savePerson data:', JSON.stringify(activePerson)); @@ -64,7 +64,7 @@ const EditPersonForm = ({ classes }) => { autoFocus defaultValue={activePerson.firstName || ''} id="firstNameToBeSaved" - inputRef={firstName} + inputRef={firstNameInputRef} label="First (Legal) Name" margin="dense" name="firstName" @@ -75,7 +75,7 @@ const EditPersonForm = ({ classes }) => { { { id="emailPersonalToBeSaved" label="Email Address, Personal" name="emailPersonal" - inputRef={emailPersonal} + inputRef={emailPersonalInputRef} margin="dense" variant="outlined" onChange={() => setSaveButtonActive(true)} @@ -108,7 +108,7 @@ const EditPersonForm = ({ classes }) => { { { { renderLog('PersonProfileDrawerMainContent'); const { getAppContextValue } = useConnectAppContext(); + const [personId] = useState(getAppContextValue('personDrawersPersonId')); + const [showPersonAway, setShowPersonAway] = useState(false); return ( + + + My availability + + {' '} + ( + {showPersonAway ? ( + setShowPersonAway(false)}>hide + ) : ( + setShowPersonAway(true)}>show + )} + ) + + {showPersonAway && ( + + )} ); }; @@ -22,4 +42,12 @@ const PersonProfileDrawerMainContent = () => { const PersonProfileDrawerMainContentWrapper = styled('div')` `; +const PersonAwayTitle = styled('span')` + font-weight: bold; +`; + +const PersonAwayTitleAndToggle = styled('div')` + margin-top: 12px; +`; + export default PersonProfileDrawerMainContent; diff --git a/src/js/components/Task/EditTaskDefinitionForm.jsx b/src/js/components/Task/EditTaskDefinitionForm.jsx index 4087afe..5e57feb 100644 --- a/src/js/components/Task/EditTaskDefinitionForm.jsx +++ b/src/js/components/Task/EditTaskDefinitionForm.jsx @@ -22,7 +22,7 @@ import { useTaskDefinitionSaveMutation } from '../../react-query/mutations'; const EditTaskDefinitionForm = ({ classes }) => { renderLog('EditTaskDefinitionForm'); // Set LOG_RENDER_EVENTS to log all renders const { getAppContextValue, setAppContextValue } = useConnectAppContext(); - const { mutate } = useTaskDefinitionSaveMutation(); + const { mutate: taskDefinitionSave } = useTaskDefinitionSaveMutation(); const [taskGroup] = useState(getAppContextValue('editTaskDefinitionDrawerTaskGroup')); const [taskDefinition] = useState(getAppContextValue('editTaskDefinitionDrawerTaskDefinition')); @@ -61,7 +61,7 @@ const EditTaskDefinitionForm = ({ classes }) => { taskInstructions: taskInstFldRef.current.value, taskActionUrl: taskUrlFldRef.current.value, }); - mutate(requestParams); + taskDefinitionSave(requestParams); setSaveButtonActive(false); setAppContextValue('editTaskDefinitionDrawerOpen', false); setAppContextValue('editTaskDefinitionDrawerTaskDefinition', undefined); diff --git a/src/js/controllers/PersonController.js b/src/js/controllers/PersonController.js index e564487..772e017 100644 --- a/src/js/controllers/PersonController.js +++ b/src/js/controllers/PersonController.js @@ -1,5 +1,50 @@ // PersonController.js // Functions for manipulating data related to the person table. +import { PERSON_AWAY_REASONS } from '../models/PersonModel'; +import webAppConfig from '../config'; + +export const getPersonAwayReason = (personAway) => { + let personAwayReasonFound = 'isVacation'; // Default to vacation if none of the other reasons are found + PERSON_AWAY_REASONS.forEach((personAwayReason) => { + if (personAway[personAwayReason] === true) { + personAwayReasonFound = personAwayReason; + } + }); + return personAwayReasonFound; +}; + +export const getPersonAwayParamsToSave = (incomingPersonAwayReason) => { + const paramsToReturn = {}; + PERSON_AWAY_REASONS.forEach((personAwayReason) => { + paramsToReturn[personAwayReason] = personAwayReason === incomingPersonAwayReason; + }); + return paramsToReturn; +}; + +export const getPersonAwayLabel = (personAwayReason) => { + switch (personAwayReason) { + case 'isLeaveOfAbsence': + return 'Leave of Absence'; + case 'isMedicalLeave': + return 'Medical Leave'; + case 'isNonResponsive': + return 'Has stopped responding to management contact'; + case 'isNotAttending': + return 'Around, but cannot attend meetings'; + case 'isResigned': + if (webAppConfig.ORGANIZATION_NAME) { + return `Resigning from ${webAppConfig.ORGANIZATION_NAME}`; + } else { + return 'Resigned'; + } + case 'isVacation': + return 'Vacation'; + case 'isWorkTrip': + return 'Work Trip'; + default: + return personAwayReason; + } +}; export const searchWordFoundInOnePerson = (searchWord, person) => { const fieldsToSearch = [ diff --git a/src/js/models/PersonModel.jsx b/src/js/models/PersonModel.jsx index 19a7e50..179407f 100644 --- a/src/js/models/PersonModel.jsx +++ b/src/js/models/PersonModel.jsx @@ -6,6 +6,10 @@ import { useMutation, useQueryClient } from '@tanstack/react-query'; import { useConnectAppContext } from '../contexts/ConnectAppContext'; import weConnectQueryFn, { METHOD } from '../react-query/WeConnectQuery'; +// If you make any changes to the following, also update controllers/PersonController.js getPersonAwayLabel +export const PERSON_AWAY_REASONS = ['isLeaveOfAbsence', 'isMedicalLeave', 'isNotAttending', 'isResigned', 'isVacation', 'isWorkTrip']; +export const PERSON_AWAY_REASONS_WITH_HR = [...PERSON_AWAY_REASONS, 'isNonResponsive']; + export function capturePersonRetrieveData (incomingResults = {}, apiDataCache = {}, dispatch) { const { data, isSuccess } = incomingResults; const allPeopleCache = apiDataCache.allPeopleCache || {}; diff --git a/src/js/react-query/mutations.jsx b/src/js/react-query/mutations.jsx index 227de9c..7510586 100644 --- a/src/js/react-query/mutations.jsx +++ b/src/js/react-query/mutations.jsx @@ -79,6 +79,15 @@ const useGroupSaveMutation = () => { }); }; +const usePersonAwaySaveMutation = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: (params) => weConnectQueryFn('person-away-save', params, METHOD.GET), + onError: (error) => console.log('error in usePersonAwaySaveMutation: ', error), + onSuccess: () => queryClient.invalidateQueries('person-away-list-retrieve'), + }); +}; + // Moved to /models/PersonModel.jsx with a non-conflicting function name const usePersonSaveMutation = () => { const queryClient = useQueryClient(); @@ -121,6 +130,7 @@ const useGetAuthMutation = () => { export { useRemoveTeamMutation, useRemoveTeamMemberMutation, useAddPersonToTeamMutation, useQuestionnaireSaveMutation, useTaskDefinitionSaveMutation, useGroupSaveMutation, + usePersonAwaySaveMutation, useQuestionSaveMutation, usePersonSaveMutation, useSaveTaskMutation, useAnswerListSaveMutation, useLogoutMutation, useGetAuthMutation };