diff --git a/src/constants/workPeriods.js b/src/constants/workPeriods.js index 3caa7e6..0a41526 100644 --- a/src/constants/workPeriods.js +++ b/src/constants/workPeriods.js @@ -34,7 +34,9 @@ export const REQUIRED_FIELDS = [ "workPeriods.startDate", "workPeriods.endDate", "workPeriods.paymentStatus", + "workPeriods.paymentTotal", "workPeriods.daysWorked", + "workPeriods.daysPaid", ]; // Valid parameter names for requests. @@ -58,6 +60,7 @@ export const SORT_BY_MAP = { [SORT_BY.END_DATE]: API_SORT_BY.END_DATE, [SORT_BY.WEEKLY_RATE]: API_SORT_BY.WEEKLY_RATE, [SORT_BY.PAYMENT_STATUS]: API_SORT_BY.PAYMENT_STATUS, + [SORT_BY.PAYMENT_TOTAL]: API_SORT_BY.PAYMENT_TOTAL, [SORT_BY.WORKING_DAYS]: API_SORT_BY.WORKING_DAYS, }; diff --git a/src/constants/workPeriods/apiSortBy.js b/src/constants/workPeriods/apiSortBy.js index c1bfc14..4f5de41 100644 --- a/src/constants/workPeriods/apiSortBy.js +++ b/src/constants/workPeriods/apiSortBy.js @@ -3,4 +3,5 @@ export const START_DATE = "startDate"; export const END_DATE = "endDate"; export const WEEKLY_RATE = "memberRate"; export const PAYMENT_STATUS = "workPeriods.paymentStatus"; +export const PAYMENT_TOTAL = "workPeriods.paymentTotal"; export const WORKING_DAYS = "workPeriods.daysWorked"; diff --git a/src/constants/workPeriods/sortBy.js b/src/constants/workPeriods/sortBy.js index 4c20856..cafb587 100644 --- a/src/constants/workPeriods/sortBy.js +++ b/src/constants/workPeriods/sortBy.js @@ -5,4 +5,5 @@ export const END_DATE = "END_DATE"; export const ALERT = "ALERT"; export const WEEKLY_RATE = "WEEKLY_RATE"; export const PAYMENT_STATUS = "STATUS"; +export const PAYMENT_TOTAL = "TOTAL_PAYMENT"; export const WORKING_DAYS = "WORKING_DAYS"; diff --git a/src/routes/WorkPeriods/components/PaymentStatus/styles.module.scss b/src/routes/WorkPeriods/components/PaymentStatus/styles.module.scss index 162480c..0fcef2e 100644 --- a/src/routes/WorkPeriods/components/PaymentStatus/styles.module.scss +++ b/src/routes/WorkPeriods/components/PaymentStatus/styles.module.scss @@ -1,4 +1,5 @@ @import "styles/mixins"; +@import "styles/variables"; .container { display: inline-block; @@ -25,7 +26,10 @@ background: #9d41c9; } -.cancelled, +.cancelled { + background: #999; +} + .failed { background: #da0000; } @@ -37,4 +41,5 @@ line-height: 20px; letter-spacing: normal; background: transparent; + color: $text-color; } diff --git a/src/routes/WorkPeriods/components/PeriodDetails/index.jsx b/src/routes/WorkPeriods/components/PeriodDetails/index.jsx index a5a86b8..687e5d5 100644 --- a/src/routes/WorkPeriods/components/PeriodDetails/index.jsx +++ b/src/routes/WorkPeriods/components/PeriodDetails/index.jsx @@ -124,7 +124,7 @@ const PeriodDetails = ({ className, details, isDisabled, isFailed }) => { - +
History @@ -146,7 +146,6 @@ const PeriodDetails = ({ className, details, isDisabled, isFailed }) => {
diff --git a/src/routes/WorkPeriods/components/PeriodItem/index.jsx b/src/routes/WorkPeriods/components/PeriodItem/index.jsx index ae5cf84..134798f 100644 --- a/src/routes/WorkPeriods/components/PeriodItem/index.jsx +++ b/src/routes/WorkPeriods/components/PeriodItem/index.jsx @@ -17,7 +17,11 @@ import { updateWorkPeriodWorkingDays, } from "store/thunks/workPeriods"; import { useUpdateEffect } from "utils/hooks"; -import { formatUserHandleLink, formatWeeklyRate } from "utils/formatters"; +import { + currencyFormatter, + formatUserHandleLink, + formatWeeklyRate, +} from "utils/formatters"; import { stopPropagation } from "utils/misc"; import styles from "./styles.module.scss"; @@ -29,6 +33,7 @@ import styles from "./styles.module.scss"; * @param {boolean} [props.isFailed] whether the item should be highlighted as failed * @param {boolean} props.isSelected whether the item is selected * @param {Object} props.item object describing a working period + * @param {Object} props.data changeable working period data such as working days * @param {Object} [props.details] object with working period details * @returns {JSX.Element} */ @@ -37,6 +42,7 @@ const PeriodItem = ({ isFailed = false, isSelected, item, + data, details, }) => { const dispatch = useDispatch(); @@ -53,16 +59,16 @@ const PeriodItem = ({ }, [dispatch, item]); const onWorkingDaysChange = useCallback( - (workingDays) => { - dispatch(setWorkPeriodWorkingDays({ periodId: item.id, workingDays })); + (daysWorked) => { + dispatch(setWorkPeriodWorkingDays(item.id, daysWorked)); }, [dispatch, item.id] ); const updateWorkingDays = useCallback( debounce( - (workingDays) => { - dispatch(updateWorkPeriodWorkingDays(item.id, workingDays)); + (daysWorked) => { + dispatch(updateWorkPeriodWorkingDays(item.id, daysWorked)); }, 300, { leading: false } @@ -72,8 +78,8 @@ const PeriodItem = ({ // Update working days on server if working days change. useUpdateEffect(() => { - updateWorkingDays(item.workingDays); - }, [item.workingDays]); + updateWorkingDays(data.daysWorked); + }, [data.daysWorked]); return ( <> @@ -115,18 +121,24 @@ const PeriodItem = ({ {formatWeeklyRate(item.weeklyRate)} + + + {currencyFormatter.format(data.paymentTotal)} + + ({data.daysPaid}) + - + - + @@ -155,9 +167,13 @@ PeriodItem.propTypes = { startDate: PT.string.isRequired, endDate: PT.string.isRequired, weeklyRate: PT.number, + }).isRequired, + data: PT.shape({ + daysWorked: PT.number.isRequired, + daysPaid: PT.number.isRequired, paymentStatus: PT.string.isRequired, - workingDays: PT.number.isRequired, - }), + paymentTotal: PT.number.isRequired, + }).isRequired, details: PT.shape({ periodId: PT.string.isRequired, rbId: PT.string.isRequired, diff --git a/src/routes/WorkPeriods/components/PeriodItem/styles.module.scss b/src/routes/WorkPeriods/components/PeriodItem/styles.module.scss index bd8766b..6c24821 100644 --- a/src/routes/WorkPeriods/components/PeriodItem/styles.module.scss +++ b/src/routes/WorkPeriods/components/PeriodItem/styles.module.scss @@ -25,7 +25,7 @@ padding-left: 14px; } - &.workingDays { + &.daysWorked { border-right: 1px solid #d6d6d6; padding-top: 4px; padding-right: 9px; @@ -77,10 +77,24 @@ td.weeklyRate { } } -td.workingDays { +td.paymentTotal { + white-space: nowrap; +} + +.paymentTotalSum { + display: inline-block; + width: 70px; + text-align: right; +} + +.daysPaid { + color: #aaa; +} + +td.daysWorked { padding: 5px 10px; } -.workingDaysControl { +.daysWorkedControl { width: 100px; } diff --git a/src/routes/WorkPeriods/components/PeriodList/index.jsx b/src/routes/WorkPeriods/components/PeriodList/index.jsx index 06ed6b5..6394127 100644 --- a/src/routes/WorkPeriods/components/PeriodList/index.jsx +++ b/src/routes/WorkPeriods/components/PeriodList/index.jsx @@ -7,6 +7,7 @@ import PeriodItem from "../PeriodItem"; import PeriodListHead from "../PeriodListHead"; import { getWorkPeriods, + getWorkPeriodsData, getWorkPeriodsDetails, getWorkPeriodsFailed, getWorkPeriodsIsProcessingPayments, @@ -23,6 +24,7 @@ import styles from "./styles.module.scss"; */ const PeriodList = ({ className }) => { const periods = useSelector(getWorkPeriods); + const [periodsData] = useSelector(getWorkPeriodsData); const periodsDetails = useSelector(getWorkPeriodsDetails); const periodsFailed = useSelector(getWorkPeriodsFailed); const periodsSelected = useSelector(getWorkPeriodsSelected); @@ -37,7 +39,7 @@ const PeriodList = ({ className }) => { - + {periods.map((period) => ( { isFailed={period.id in periodsFailed} isSelected={period.id in periodsSelected} item={period} + data={periodsData[period.id]} details={periodsDetails[period.id]} /> ))} diff --git a/src/routes/WorkPeriods/components/PeriodListHead/index.jsx b/src/routes/WorkPeriods/components/PeriodListHead/index.jsx index 175618b..8238ad9 100644 --- a/src/routes/WorkPeriods/components/PeriodListHead/index.jsx +++ b/src/routes/WorkPeriods/components/PeriodListHead/index.jsx @@ -74,6 +74,7 @@ const HEAD_CELLS = [ { label: "Start Date", id: SORT_BY.START_DATE }, { label: "End Date", id: SORT_BY.END_DATE }, { label: "Weekly Rate", id: SORT_BY.WEEKLY_RATE }, + { label: "Total Paid", id: SORT_BY.PAYMENT_TOTAL }, { label: "Status", id: SORT_BY.PAYMENT_STATUS }, { label: "Working Days", id: SORT_BY.WORKING_DAYS }, ]; diff --git a/src/routes/WorkPeriods/components/PeriodWeekPicker/index.jsx b/src/routes/WorkPeriods/components/PeriodWeekPicker/index.jsx index 09dbec4..212bea8 100644 --- a/src/routes/WorkPeriods/components/PeriodWeekPicker/index.jsx +++ b/src/routes/WorkPeriods/components/PeriodWeekPicker/index.jsx @@ -1,10 +1,12 @@ import React, { useCallback } from "react"; import { useSelector, useDispatch } from "react-redux"; import PT from "prop-types"; +import cn from "classnames"; import moment from "moment"; import WeekPicker from "components/WeekPicker"; import { getWorkPeriodsDateRange } from "store/selectors/workPeriods"; import { setWorkPeriodsDateRange } from "store/actions/workPeriods"; +import styles from "./styles.module.scss"; /** * Displays working periods' week picker. @@ -34,7 +36,7 @@ const PeriodWeekPicker = ({ className }) => { return ( { +const PeriodsHistory = ({ className, isDisabled, periods }) => { + const [periodsData] = useSelector(getWorkPeriodsData); const [startDate] = useSelector(getWorkPeriodsDateRange); return (
@@ -21,9 +25,9 @@ const PeriodsHistory = ({ className, isDisabled, periodId, periods }) => { {periods.map((period) => ( ))} @@ -36,7 +40,6 @@ const PeriodsHistory = ({ className, isDisabled, periodId, periods }) => { PeriodsHistory.propTypes = { className: PT.string, isDisabled: PT.bool.isRequired, - periodId: PT.string.isRequired, periods: PT.arrayOf(PT.object), }; diff --git a/src/routes/WorkPeriods/components/PeriodsHistoryItem/index.jsx b/src/routes/WorkPeriods/components/PeriodsHistoryItem/index.jsx index 5bbfabc..fd61966 100644 --- a/src/routes/WorkPeriods/components/PeriodsHistoryItem/index.jsx +++ b/src/routes/WorkPeriods/components/PeriodsHistoryItem/index.jsx @@ -3,19 +3,16 @@ import { useDispatch } from "react-redux"; import PT from "prop-types"; import cn from "classnames"; import debounce from "lodash/debounce"; +import moment from "moment"; import IntegerField from "components/IntegerField"; import PaymentStatus from "../PaymentStatus"; +import PeriodsHistoryPaymentTotal from "../PeriodsHistoryPaymentTotal"; import { PAYMENT_STATUS } from "constants/workPeriods"; import { setDetailsWorkingDays } from "store/actions/workPeriods"; import { updateWorkPeriodWorkingDays } from "store/thunks/workPeriods"; import { useUpdateEffect } from "utils/hooks"; -import { - formatDateLabel, - formatDateRange, - formatWeeklyRate, -} from "utils/formatters"; +import { formatDateLabel, formatDateRange } from "utils/formatters"; import styles from "./styles.module.scss"; -import PeriodsHistoryWeeklyRate from "../PeriodsHistoryWeeklyRate"; /** * Displays working period row in history table in details view. @@ -23,28 +20,24 @@ import PeriodsHistoryWeeklyRate from "../PeriodsHistoryWeeklyRate"; * @param {Object} props component properties * @returns {JSX.Element} */ -const PeriodsHistoryItem = ({ - periodId, - isDisabled, - item, - currentStartDate, -}) => { +const PeriodsHistoryItem = ({ isDisabled, item, data, currentStartDate }) => { const dispatch = useDispatch(); const dateLabel = formatDateLabel(item.startDate, currentStartDate); - const workingDays = item.workingDays; + const daysWorked = data.daysWorked; + const isCurrent = moment(item.startDate).isSame(currentStartDate, "date"); const onWorkingDaysChange = useCallback( - (workingDays) => { - dispatch(setDetailsWorkingDays(periodId, item.id, workingDays)); + (daysWorked) => { + dispatch(setDetailsWorkingDays(item.id, daysWorked)); }, - [dispatch, periodId, item.id] + [dispatch, item.id] ); const updateWorkingDays = useCallback( debounce( - (workingDays) => { - dispatch(updateWorkPeriodWorkingDays(item.id, workingDays)); + (daysWorked) => { + dispatch(updateWorkPeriodWorkingDays(item.id, daysWorked)); }, 300, { leading: false } @@ -54,8 +47,10 @@ const PeriodsHistoryItem = ({ // Update working days on server if working days change. useUpdateEffect(() => { - updateWorkingDays(item.workingDays); - }, [item.workingDays]); + if (!isCurrent) { + updateWorkingDays(data.daysWorked); + } + }, [data.daysWorked, isCurrent]); return ( {dateLabel} - - + - + - - {item.paymentStatus === PAYMENT_STATUS.PAID ? ( - `${workingDays} ${workingDays === 1 ? "Day" : "Days"}` + + {data.paymentStatus === PAYMENT_STATUS.PAID ? ( + `${daysWorked} ${daysWorked === 1 ? "Day" : "Days"}` ) : ( )} @@ -97,16 +93,19 @@ const PeriodsHistoryItem = ({ }; PeriodsHistoryItem.propTypes = { - periodId: PT.string.isRequired, isDisabled: PT.bool.isRequired, item: PT.shape({ id: PT.string.isRequired, startDate: PT.oneOfType([PT.string, PT.number]).isRequired, endDate: PT.oneOfType([PT.string, PT.number]).isRequired, - paymentStatus: PT.string.isRequired, payments: PT.array, weeklyRate: PT.number, - workingDays: PT.number.isRequired, + }).isRequired, + data: PT.shape({ + daysWorked: PT.number.isRequired, + daysPaid: PT.number.isRequired, + paymentStatus: PT.string.isRequired, + paymentTotal: PT.number.isRequired, }).isRequired, currentStartDate: PT.oneOfType([PT.string, PT.number, PT.object]).isRequired, }; diff --git a/src/routes/WorkPeriods/components/PeriodsHistoryItem/styles.module.scss b/src/routes/WorkPeriods/components/PeriodsHistoryItem/styles.module.scss index 27d50dc..c91363d 100644 --- a/src/routes/WorkPeriods/components/PeriodsHistoryItem/styles.module.scss +++ b/src/routes/WorkPeriods/components/PeriodsHistoryItem/styles.module.scss @@ -27,20 +27,20 @@ } } -.weeklyRate { +.paymentTotal { padding: 6px 12px; line-height: 26px; } -.weeklyRateContainer { +.paymentTotalContainer { position: relative; } -.workingDays { +.daysWorked { padding: 4px 10px; } -.workingDaysControl { +.daysWorkedControl { display: block; width: 100px; } diff --git a/src/routes/WorkPeriods/components/PeriodsHistoryWeeklyRate/index.jsx b/src/routes/WorkPeriods/components/PeriodsHistoryPaymentTotal/index.jsx similarity index 74% rename from src/routes/WorkPeriods/components/PeriodsHistoryWeeklyRate/index.jsx rename to src/routes/WorkPeriods/components/PeriodsHistoryPaymentTotal/index.jsx index 8383ecc..fdc8cea 100644 --- a/src/routes/WorkPeriods/components/PeriodsHistoryWeeklyRate/index.jsx +++ b/src/routes/WorkPeriods/components/PeriodsHistoryPaymentTotal/index.jsx @@ -3,11 +3,17 @@ import React, { useCallback, useRef, useState } from "react"; import { usePopper } from "react-popper"; import PT from "prop-types"; import cn from "classnames"; -import ChallengePopup from "../PaymentsPopup"; -import compStyles from "./styles.module.scss"; +import PaymentsPopup from "../PaymentsPopup"; import { useClickOutside } from "utils/hooks"; +import { currencyFormatter } from "utils/formatters"; +import compStyles from "./styles.module.scss"; -const PeriodsHistoryWeeklyRate = ({ className, payments, weeklyRate }) => { +const PeriodsHistoryPaymentTotal = ({ + className, + payments, + paymentTotal, + daysPaid, +}) => { const [isShowPopup, setIsShowPopup] = useState(false); const containerRef = useRef(); @@ -37,15 +43,18 @@ const PeriodsHistoryWeeklyRate = ({ className, payments, weeklyRate }) => { return (
- - {weeklyRate} - + + {currencyFormatter.format(paymentTotal)} + + ({daysPaid}) +
{hasPayments && isShowPopup && (
{ style={styles.popper} {...attributes.popper} > - +
{ ); }; -PeriodsHistoryWeeklyRate.propTypes = { +PeriodsHistoryPaymentTotal.propTypes = { className: PT.string, payments: PT.array, - weeklyRate: PT.string.isRequired, + paymentTotal: PT.number.isRequired, + daysPaid: PT.number.isRequired, }; function negate(value) { return !value; } -export default PeriodsHistoryWeeklyRate; +export default PeriodsHistoryPaymentTotal; diff --git a/src/routes/WorkPeriods/components/PeriodsHistoryWeeklyRate/styles.module.scss b/src/routes/WorkPeriods/components/PeriodsHistoryPaymentTotal/styles.module.scss similarity index 69% rename from src/routes/WorkPeriods/components/PeriodsHistoryWeeklyRate/styles.module.scss rename to src/routes/WorkPeriods/components/PeriodsHistoryPaymentTotal/styles.module.scss index 8ab6350..60b6009 100644 --- a/src/routes/WorkPeriods/components/PeriodsHistoryWeeklyRate/styles.module.scss +++ b/src/routes/WorkPeriods/components/PeriodsHistoryPaymentTotal/styles.module.scss @@ -6,12 +6,20 @@ } } -.weeklyRateValue { +.paymentTotal { + white-space: nowrap; +} + +.paymentTotalSum { display: inline-block; width: 70px; text-align: right; } +.daysPaid { + color: #aaa; +} + .hasPayments { cursor: pointer; } diff --git a/src/services/workPeriods.js b/src/services/workPeriods.js index 8928618..7880e4a 100644 --- a/src/services/workPeriods.js +++ b/src/services/workPeriods.js @@ -105,10 +105,20 @@ export const fetchResourceBookings = (params) => { * * @param {string} periodId working period id * @param {number} daysWorked new number of working days - * @returns {Promise} + * @returns {[Promise, Object]} */ export const patchWorkPeriodWorkingDays = (periodId, daysWorked) => { - return axios.patch(`${WORK_PERIODS_API_URL}/${periodId}`, { daysWorked }); + const source = CancelToken.source(); + return [ + axios + .patch( + `${WORK_PERIODS_API_URL}/${periodId}`, + { daysWorked }, + { cancelToken: source.token } + ) + .then(extractResponseData), + source, + ]; }; /** diff --git a/src/store/actionTypes/workPeriods.js b/src/store/actionTypes/workPeriods.js index 23196b9..85eb490 100644 --- a/src/store/actionTypes/workPeriods.js +++ b/src/store/actionTypes/workPeriods.js @@ -19,6 +19,9 @@ export const WP_SET_DETAILS_HIDE_PAST_PERIODS = "WP_SET_DETAILS_HIDE_PAST_PERIODS"; export const WP_SET_PAGE_NUMBER = "WP_SET_PAGE_NUMBER"; export const WP_SET_PAGE_SIZE = "WP_SET_PAGE_SIZE"; +export const WP_SET_DATA_PENDING = "WP_SET_DATA_PENDING"; +export const WP_SET_DATA_SUCCESS = "WP_SET_DATA_SUCCESS"; +export const WP_SET_DATA_ERROR = "WP_SET_DATA_ERROR"; export const WP_SET_DATE_RANGE = "WP_SET_DATE_RANGE"; export const WP_SET_SORT_BY = "WP_SET_SORT_BY"; export const WP_SET_SORT_ORDER = "WP_SET_SORT_ORDER"; diff --git a/src/store/actions/workPeriods.js b/src/store/actions/workPeriods.js index d93bff5..592cd1c 100644 --- a/src/store/actions/workPeriods.js +++ b/src/store/actions/workPeriods.js @@ -159,18 +159,13 @@ export const setBillingAccount = (periodId, accountId) => ({ * Creates an action denoting the change of working period's working days in * details view. * - * @param {string} parentPeriodId parent working period id * @param {string} periodId working period id - * @param {number} workingDays number of working days + * @param {number} daysWorked number of working days * @returns {Object} */ -export const setDetailsWorkingDays = ( - parentPeriodId, - periodId, - workingDays -) => ({ +export const setDetailsWorkingDays = (periodId, daysWorked) => ({ type: ACTION_TYPE.WP_SET_DETAILS_WORKING_DAYS, - payload: { parentPeriodId, periodId, workingDays }, + payload: { periodId, daysWorked }, }); /** @@ -314,14 +309,35 @@ export const setWorkPeriodsUserHandle = (handle) => ({ /** * Creates an action to change working days for specific working period. * - * @param {Object} payload object containing period id and days number - * @param {string|number} payload.periodId period id - * @param {number} payload.workingDays number of working days + * @param {string|number} periodId period id + * @param {number} daysWorked number of working days * @returns {Object} */ -export const setWorkPeriodWorkingDays = (payload) => ({ +export const setWorkPeriodWorkingDays = (periodId, daysWorked) => ({ type: ACTION_TYPE.WP_SET_WORKING_DAYS, - payload, + payload: { periodId, daysWorked }, +}); + +/** + * Creates an action denoting the update of working period's changeable data. + * + * @param {Object} periodId working period id + * @param {Object} cancelSource axios cancel token source + * @returns {Object} + */ +export const setWorkPeriodDataPending = (periodId, cancelSource) => ({ + type: ACTION_TYPE.WP_SET_DATA_PENDING, + payload: { periodId, cancelSource }, +}); + +export const setWorkPeriodDataSuccess = (periodId, data) => ({ + type: ACTION_TYPE.WP_SET_DATA_SUCCESS, + payload: { periodId, data }, +}); + +export const setWorkPeriodDataError = (periodId, message) => ({ + type: ACTION_TYPE.WP_SET_DATA_ERROR, + payload: { periodId, message }, }); /** diff --git a/src/store/reducers/workPeriods.js b/src/store/reducers/workPeriods.js index 110d2d8..0ea2522 100644 --- a/src/store/reducers/workPeriods.js +++ b/src/store/reducers/workPeriods.js @@ -17,6 +17,8 @@ import { } from "utils/misc"; import { createAssignedBillingAccountOption } from "utils/workPeriods"; +const cancelSourceDummy = { cancel: () => {} }; + const initPagination = () => ({ totalCount: 0, pageCount: 0, @@ -34,8 +36,6 @@ const initFilters = () => ({ userHandle: "", }); -const cancelSourceDummy = { cancel: () => {} }; - const initPeriodDetails = ( periodId, rbId, @@ -65,6 +65,7 @@ const initialState = { error: null, cancelSource: cancelSourceDummy, periods: [], + periodsData: [{}], periodsDetails: {}, periodsFailed: {}, periodsSelected: {}, @@ -95,6 +96,7 @@ const actionHandlers = { cancelSource, error: null, periods: [], + periodsData: [{}], periodsDetails: {}, periodsFailed: {}, periodsSelected: {}, @@ -115,11 +117,18 @@ const actionHandlers = { oldPagination.pageCount !== pageCount ? { ...oldPagination, totalCount, pageCount } : oldPagination; + const periodsData = {}; + for (let period of periods) { + period.data.cancelSource = null; + periodsData[period.id] = period.data; + delete period.data; + } return { ...state, cancelSource: null, error: null, periods, + periodsData: [periodsData], pagination, }; }, @@ -194,6 +203,12 @@ const actionHandlers = { // This branch should not be reachable but just in case. return state; } + const periodsData = state.periodsData[0]; + for (let period of details.periods) { + period.data.cancelSource = null; + periodsData[period.id] = period.data; + delete period.data; + } periodDetails = { ...periodDetails, periods: details.periods, @@ -210,6 +225,7 @@ const actionHandlers = { periodsDetails[periodId] = periodDetails; return { ...state, + periodsData: [periodsData], periodsDetails, }; }, @@ -376,36 +392,18 @@ const actionHandlers = { }, [ACTION_TYPE.WP_SET_DETAILS_WORKING_DAYS]: ( state, - { parentPeriodId, periodId, workingDays } + { periodId, daysWorked } ) => { - const periodsDetails = { ...state.periodsDetails }; - let periodDetails = periodsDetails[parentPeriodId]; - if (!periodDetails) { + const periodsData = state.periodsData[0]; + let periodData = periodsData[periodId]; + daysWorked = Math.min(Math.max(daysWorked, periodData.daysPaid), 5); + if (daysWorked === periodData.daysWorked) { return state; } - workingDays = Math.min(Math.max(workingDays, 0), 5); - const periods = []; - for (let period of periodDetails.periods) { - if (period.id === periodId) { - period = { ...period, workingDays }; - } - periods.push(period); - } - const periodsVisible = []; - for (let period of periodDetails.periodsVisible) { - if (period.id === periodId) { - period = { ...period, workingDays }; - } - periodsVisible.push(period); - } - periodsDetails[parentPeriodId] = { - ...periodDetails, - periods, - periodsVisible, - }; + periodsData[periodId] = { ...periodData, daysWorked }; return { ...state, - periodsDetails, + periodsData: [periodsData], }; }, [ACTION_TYPE.WP_RESET_FILTERS]: (state) => ({ @@ -516,22 +514,66 @@ const actionHandlers = { }, }; }, - [ACTION_TYPE.WP_SET_WORKING_DAYS]: (state, { periodId, workingDays }) => { - const oldPeriods = state.periods; - const periods = []; - for (let i = 0, len = oldPeriods.length; i < len; i++) { - let period = oldPeriods[i]; - if (period.id === periodId) { - period = { - ...period, - workingDays: Math.min(Math.max(workingDays, 0), 5), - }; - } - periods.push(period); + [ACTION_TYPE.WP_SET_DATA_PENDING]: (state, { periodId, cancelSource }) => { + const periodsData = state.periodsData[0]; + const periodData = periodsData[periodId]; + if (!periodData) { + return state; } + periodsData[periodId] = { + ...periodData, + cancelSource, + }; return { ...state, - periods, + periodsData: [periodsData], + }; + }, + [ACTION_TYPE.WP_SET_DATA_SUCCESS]: (state, { periodId, data }) => { + const periodsData = state.periodsData[0]; + const periodData = periodsData[periodId]; + if (!periodData) { + return state; + } + periodsData[periodId] = { + ...periodData, + ...data, + cancelSource: null, + }; + return { + ...state, + periodsData: [periodsData], + }; + }, + [ACTION_TYPE.WP_SET_DATA_ERROR]: (state, { periodId }) => { + const periodsData = state.periodsData[0]; + const periodData = periodsData[periodId]; + if (!periodData) { + return state; + } + periodsData[periodId] = { + ...periodData, + cancelSource: null, + }; + return { + ...state, + periodsData: [periodsData], + }; + }, + [ACTION_TYPE.WP_SET_WORKING_DAYS]: (state, { periodId, daysWorked }) => { + const periodsData = state.periodsData[0]; + const periodData = periodsData[periodId]; + if (!periodData) { + return state; + } + daysWorked = Math.min(Math.max(daysWorked, periodData.daysPaid), 5); + if (daysWorked === periodData.daysWorked) { + return state; + } + periodsData[periodId] = { ...periodData, daysWorked }; + return { + ...state, + periodsData: [periodsData], }; }, [ACTION_TYPE.WP_TOGGLE_PERIOD]: (state, periodId) => { diff --git a/src/store/selectors/workPeriods.js b/src/store/selectors/workPeriods.js index d0ad0ab..060d93f 100644 --- a/src/store/selectors/workPeriods.js +++ b/src/store/selectors/workPeriods.js @@ -64,6 +64,8 @@ export const getWorkPeriodsPageSize = (state) => export const getWorkPeriodsCount = (state) => state.workPeriods.periods.length; +export const getWorkPeriodsData = (state) => state.workPeriods.periodsData; + export const getWorkPeriodsTotalCount = (state) => state.workPeriods.pagination.totalCount; diff --git a/src/store/thunks/workPeriods.js b/src/store/thunks/workPeriods.js index e04818a..785f807 100644 --- a/src/store/thunks/workPeriods.js +++ b/src/store/thunks/workPeriods.js @@ -19,6 +19,7 @@ import { import { normalizeBillingAccounts, normalizeDetailsPeriodItems, + normalizePeriodData, normalizePeriodItems, } from "utils/workPeriods"; import { makeToast } from "components/ToastrMessage"; @@ -217,20 +218,49 @@ export const updateWorkPeriodBillingAccount = }; /** + * Sends an update request to the server to update the number of working + * period's working days. The working period is also updated with the data + * from response. * - * @param {string} periodId - * @param {number} workingDays + * @param {string} periodId working period id + * @param {number} daysWorked working period's working days * @returns {function} */ export const updateWorkPeriodWorkingDays = - (periodId, workingDays) => async () => { + (periodId, daysWorked) => async (dispatch, getState) => { + let [periodsData] = selectors.getWorkPeriodsData(getState()); + periodsData[periodId]?.cancelSource?.cancel(); + const [promise, source] = services.patchWorkPeriodWorkingDays( + periodId, + daysWorked + ); + dispatch(actions.setWorkPeriodDataPending(periodId, source)); + let periodData = null; + let errorMessage = null; try { - await services.patchWorkPeriodWorkingDays(periodId, workingDays); + const data = await promise; + periodData = normalizePeriodData(data); } catch (error) { - makeToast( - `Failed to update working days for working period ${periodId}.\n` + - error.toString() - ); + if (!axios.isCancel(error)) { + errorMessage = error.toString(); + makeToast( + `Failed to update working days for working period ${periodId}.\n` + + errorMessage + ); + } + } + [periodsData] = selectors.getWorkPeriodsData(getState()); + const currentDaysWorked = periodsData[periodId]?.daysWorked; + // If periodData is null it means the request was cancelled right before + // another request was sent and so we don't need to update the state. + // If periodData's daysWorked is not equal to the current daysWorked + // it means that the state was changed while the data was in transit + // and there will be a new request at the end of which the period's data + // will be updated so again we don't need to update the state. + if (periodData && periodData.daysWorked === currentDaysWorked) { + dispatch(actions.setWorkPeriodDataSuccess(periodId, periodData)); + } else if (errorMessage) { + dispatch(actions.setWorkPeriodDataError(periodId, errorMessage)); } }; diff --git a/src/utils/workPeriods.js b/src/utils/workPeriods.js index 8ed10a2..6e05f70 100644 --- a/src/utils/workPeriods.js +++ b/src/utils/workPeriods.js @@ -11,7 +11,6 @@ export function normalizePeriodItems(items) { for (let item of items) { const workPeriod = item.workPeriods?.[0] || empty; const billingAccountId = item.billingAccountId; - const daysWorked = workPeriod.daysWorked; periods.push({ id: workPeriod.id || item.id, rbId: item.id, @@ -25,13 +24,32 @@ export function normalizePeriodItems(items) { : "", endDate: item.endDate ? moment(item.endDate).format(DATE_FORMAT_UI) : "", weeklyRate: item.memberRate, - paymentStatus: normalizePaymentStatus(workPeriod.paymentStatus), - workingDays: daysWorked === null ? 5 : +daysWorked || 0, + data: normalizePeriodData(workPeriod), }); } return periods; } +/** + * Normalizes specific working period data (daysWorked, daysPaid, + * paymentStatus, paymentTotal). + * + * @param {Object} period + * @param {number} period.daysWorked + * @param {number} period.daysPaid + * @param {string} period.paymentStatus + * @param {number} period.paymentTotal + * @returns {Object} + */ +export function normalizePeriodData(period) { + return { + daysWorked: period.daysWorked === null ? 5 : +period.daysWorked || 0, + daysPaid: +period.daysPaid || 0, + paymentStatus: normalizePaymentStatus(period.paymentStatus), + paymentTotal: +period.paymentTotal || 0, + }; +} + /** * Creates options to be used in dropdown selecting working period's * billing account. @@ -67,15 +85,13 @@ export function createAssignedBillingAccountOption(accountId) { export function normalizeDetailsPeriodItems(items) { const periods = []; for (let item of items) { - const daysWorked = item.daysWorked; periods.push({ id: item.id, startDate: item.startDate ? moment(item.startDate).valueOf() : 0, endDate: item.endDate ? moment(item.endDate).valueOf() : 0, - paymentStatus: normalizePaymentStatus(item.paymentStatus), payments: item.payments || [], weeklyRate: item.memberRate, - workingDays: daysWorked === null ? 5 : +daysWorked || 0, + data: normalizePeriodData(item), }); } periods.sort(sortByStartDate);