diff --git a/src/assets/images/icon-checkmark-circled.svg b/src/assets/images/icon-checkmark-circled.svg new file mode 100644 index 0000000..c551790 --- /dev/null +++ b/src/assets/images/icon-checkmark-circled.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/components/Content/styles.module.scss b/src/components/Content/styles.module.scss index 01ca520..dcb5511 100644 --- a/src/components/Content/styles.module.scss +++ b/src/components/Content/styles.module.scss @@ -4,7 +4,8 @@ padding: 0 10px; @include desktop { - flex: 1 1 auto; + flex: 1 1 0; padding: 0 35px; + min-width: 0; } } diff --git a/src/components/Icons/CheckmarkCircled/index.jsx b/src/components/Icons/CheckmarkCircled/index.jsx new file mode 100644 index 0000000..4b9fcdb --- /dev/null +++ b/src/components/Icons/CheckmarkCircled/index.jsx @@ -0,0 +1,61 @@ +import React, { useEffect, useState } from "react"; +import PT from "prop-types"; +import cn from "classnames"; +import Icon from "../../../assets/images/icon-checkmark-circled.svg"; +import styles from "./styles.module.scss"; + +/** + * Displays an animated checkmark inside circle. After the specified timeout + * the checkmark is faded out and after fade transition ends the onTimeout + * is called. + * + * @param {Object} props component properties + * @param {string} [props.className] class name to be added to root element + * @param {() => void} props.onTimeout + * @param {number} props.timeout timeout milliseconds + * @returns {JSX.Element} + */ +const CheckmarkCircled = ({ className, onTimeout, timeout = 2000 }) => { + const [isAnimated, setIsAnimated] = useState(false); + const [isTimedOut, setIsTimedOut] = useState(false); + + useEffect(() => { + setIsAnimated(true); + }, []); + + useEffect(() => { + setIsTimedOut(false); + let timeoutId = setTimeout(() => { + timeoutId = 0; + setIsTimedOut(true); + }, Math.max(timeout, /* total CSS animation duration */ 1200)); + return () => { + if (timeoutId) { + clearTimeout(timeoutId); + } + }; + }, [timeout]); + + return ( + + + + ); +}; + +CheckmarkCircled.propTypes = { + className: PT.string, + onTimeout: PT.func.isRequired, + timeout: PT.number, +}; + +export default CheckmarkCircled; diff --git a/src/components/Icons/CheckmarkCircled/styles.module.scss b/src/components/Icons/CheckmarkCircled/styles.module.scss new file mode 100644 index 0000000..108fac3 --- /dev/null +++ b/src/components/Icons/CheckmarkCircled/styles.module.scss @@ -0,0 +1,79 @@ +@import "styles/variables"; + +.container { + display: inline-block; + width: 30px; + height: 30px; + opacity: 1; + transition: opacity 0.2s ease; +} + +.checkmark { + display: block; + width: auto; + height: 100%; + border-radius: 999px; + stroke-width: 2; + stroke: $primary-color; + stroke-miterlimit: 10; + box-shadow: inset 0px 0px 0px $primary-color; + animation-play-state: paused; + animation: /*checkmark-circled-fill 0.4s ease-in-out 0.4s forwards,*/ checkmark-circled-scale + 0.3s ease-in-out 0.9s both; + + :global(.checkmark__circle) { + stroke-dasharray: 166; + stroke-dashoffset: 166; + stroke-width: 2; + stroke-miterlimit: 10; + stroke: $primary-color; + fill: rgba(255, 255, 255, 0); + animation-play-state: paused; + animation: checkmark-circled-stroke 0.6s cubic-bezier(0.65, 0, 0.45, 1) + forwards; + } + + :global(.checkmark__check) { + transform-origin: 50% 50%; + stroke-dasharray: 48; + stroke-dashoffset: 48; + animation-play-state: paused; + animation: checkmark-circled-stroke 0.3s cubic-bezier(0.65, 0, 0.45, 1) 0.8s + forwards; + } +} + +.animated { + animation-play-state: running; + + :global(.checkmark__circle), + :global(.checkmark__check) { + animation-play-state: running; + } +} + +.fadeOut { + opacity: 0; +} + +@keyframes checkmark-circled-stroke { + 100% { + stroke-dashoffset: 0; + } +} + +@keyframes checkmark-circled-scale { + 0%, + 100% { + transform: none; + } + 50% { + transform: scale3d(1.1, 1.1, 1); + } +} + +@keyframes checkmark-circled-fill { + 100% { + box-shadow: inset 0px 0px 0px 10px $primary-color; + } +} diff --git a/src/components/Popup/index.jsx b/src/components/Popup/index.jsx index dd22d5b..b5be2db 100644 --- a/src/components/Popup/index.jsx +++ b/src/components/Popup/index.jsx @@ -35,12 +35,16 @@ const Popup = ({ return (
{children} -
+
); }; diff --git a/src/components/Popup/styles.module.scss b/src/components/Popup/styles.module.scss index 6a800ea..72f1e2b 100644 --- a/src/components/Popup/styles.module.scss +++ b/src/components/Popup/styles.module.scss @@ -7,7 +7,7 @@ background: #fff; box-shadow: $popover-box-shadow; - :global(.popup-arrow) { + .popupArrow { display: none; } } diff --git a/src/components/SelectField/styles.module.scss b/src/components/SelectField/styles.module.scss index 3d578f5..9b693e3 100644 --- a/src/components/SelectField/styles.module.scss +++ b/src/components/SelectField/styles.module.scss @@ -104,14 +104,16 @@ .medium { :global(.custom__value-container) { - margin-top: 4px; + margin-top: 2px; + margin-bottom: 2px; padding: 6px 15px; } } .small { :global(.custom__value-container) { - margin-top: 2px; + margin-top: 1px; + margin-bottom: 1px; padding: 2px 7px 2px 13px; } diff --git a/src/decls/svg.d.ts b/src/decls/svg.d.ts index 9348424..a180524 100644 --- a/src/decls/svg.d.ts +++ b/src/decls/svg.d.ts @@ -1,4 +1,6 @@ -declare module '*.svg' { - const value: string; +declare module "*.svg" { + const value: import("react").FunctionComponent< + React.SVGAttributes + >; export default value; } diff --git a/src/routes/WorkPeriods/components/PaymentErrorDetails/styles.module.scss b/src/routes/WorkPeriods/components/PaymentErrorDetails/styles.module.scss index a1f4bf1..bbbed10 100644 --- a/src/routes/WorkPeriods/components/PaymentErrorDetails/styles.module.scss +++ b/src/routes/WorkPeriods/components/PaymentErrorDetails/styles.module.scss @@ -2,7 +2,6 @@ .container { display: block; - max-width: 480px; text-align: left; } diff --git a/src/routes/WorkPeriods/components/PeriodDetails/index.jsx b/src/routes/WorkPeriods/components/PeriodDetails/index.jsx index e34978b..cc5bc1a 100644 --- a/src/routes/WorkPeriods/components/PeriodDetails/index.jsx +++ b/src/routes/WorkPeriods/components/PeriodDetails/index.jsx @@ -104,12 +104,12 @@ const PeriodDetails = ({ className, details, isDisabled, isFailed }) => {
-
+
Billing Account
{ + dispatch(toggleWorkingDaysUpdated(item.id, false)); + }, [dispatch, item.id]); + const onWorkingDaysChange = useCallback( (daysWorked) => { dispatch(setWorkPeriodWorkingDays(item.id, daysWorked)); @@ -141,19 +146,19 @@ const PeriodItem = ({ - {details && ( td { - padding-left: 17px; - padding-right: 17px; + padding-left: 15px; + padding-right: 15px; background: #fff; } @@ -40,8 +40,22 @@ tr.container { } } +.periodDetails { + + .container.hasDetails { + > td { + &.toggle { + padding-top: 12px; + } + + &.daysWorked { + padding-top: 5px; + } + } + } +} + td.toggle { - padding: 12px 20px 12px 15px; + padding: 12px 18px 12px 15px; line-height: 15px; } @@ -67,6 +81,8 @@ td.teamName { td.startDate, td.endDate { + padding-left: 10px; + padding-right: 10px; white-space: nowrap; } @@ -90,7 +106,3 @@ td.paymentTotal { td.daysWorked { padding: 5px 10px; } - -.daysWorkedControl { - width: 100px; -} diff --git a/src/routes/WorkPeriods/components/PeriodList/index.jsx b/src/routes/WorkPeriods/components/PeriodList/index.jsx index 6394127..8533de4 100644 --- a/src/routes/WorkPeriods/components/PeriodList/index.jsx +++ b/src/routes/WorkPeriods/components/PeriodList/index.jsx @@ -32,7 +32,13 @@ const PeriodList = ({ className }) => { return ( -
+
diff --git a/src/routes/WorkPeriods/components/PeriodList/styles.module.scss b/src/routes/WorkPeriods/components/PeriodList/styles.module.scss index 9bb7222..2e8d7aa 100644 --- a/src/routes/WorkPeriods/components/PeriodList/styles.module.scss +++ b/src/routes/WorkPeriods/components/PeriodList/styles.module.scss @@ -3,6 +3,13 @@ .container { position: relative; padding: 0 20px 0 15px; + width: 100%; + overflow-x: auto; + overflow-y: visible; + + &.hasItems { + min-height: 348px; + } } .table { diff --git a/src/routes/WorkPeriods/components/PeriodListHead/index.jsx b/src/routes/WorkPeriods/components/PeriodListHead/index.jsx index 936765a..12b987e 100644 --- a/src/routes/WorkPeriods/components/PeriodListHead/index.jsx +++ b/src/routes/WorkPeriods/components/PeriodListHead/index.jsx @@ -53,7 +53,7 @@ const PeriodListHead = () => { {HEAD_CELLS.map(({ id, className, label, disableSort }) => (
-
+
{label} {!disableSort && ( { const HEAD_CELLS = [ { label: "Topcoder Handle", id: SORT_BY.USER_HANDLE }, { label: "Team Name", id: SORT_BY.TEAM_NAME, disableSort: true }, - { label: "Start Date", id: SORT_BY.START_DATE }, - { label: "End Date", id: SORT_BY.END_DATE }, + { label: "Start Date", id: SORT_BY.START_DATE, className: "startDate" }, + { label: "End Date", id: SORT_BY.END_DATE, className: "endDate" }, { label: "Weekly Rate", id: SORT_BY.WEEKLY_RATE, className: "weeklyRate" }, { label: "Total Paid", id: SORT_BY.PAYMENT_TOTAL, className: "totalPaid" }, { label: "Status", id: SORT_BY.PAYMENT_STATUS }, diff --git a/src/routes/WorkPeriods/components/PeriodListHead/styles.module.scss b/src/routes/WorkPeriods/components/PeriodListHead/styles.module.scss index 68decd6..88f3f15 100644 --- a/src/routes/WorkPeriods/components/PeriodListHead/styles.module.scss +++ b/src/routes/WorkPeriods/components/PeriodListHead/styles.module.scss @@ -1,7 +1,7 @@ @import "styles/mixins"; .container { - th { + > th { text-align: left; background: #f4f4f4; @@ -34,7 +34,7 @@ &:last-child { .colHead { - padding: 12px 10px; + padding: 12px 10px 12px 50px; &::before { right: -20px; @@ -43,8 +43,14 @@ } } - :global(.weeklyRate), - :global(.totalPaid) { + .startDate, + .endDate { + padding-left: 10px; + padding-right: 10px; + } + + .weeklyRate, + .totalPaid { justify-content: flex-end; } } @@ -54,7 +60,7 @@ display: flex; justify-content: flex-start; align-items: center; - padding: 12px 17px; + padding: 12px 15px; height: 40px; } diff --git a/src/routes/WorkPeriods/components/PeriodWorkingDays/index.jsx b/src/routes/WorkPeriods/components/PeriodWorkingDays/index.jsx new file mode 100644 index 0000000..5c221f7 --- /dev/null +++ b/src/routes/WorkPeriods/components/PeriodWorkingDays/index.jsx @@ -0,0 +1,69 @@ +import React from "react"; +import PT from "prop-types"; +import cn from "classnames"; +import IntegerField from "components/IntegerField"; +import IconCheckmarkCircled from "components/Icons/CheckmarkCircled"; +import styles from "./styles.module.scss"; + +/** + * Displays working days input field with an icon hinting about the update. + * + * @param {Object} props component properties + * @param {string} [props.className] class name to be added to root element + * @param {string} props.controlName working days input control name + * @param {Object} props.data working period data object + * @param {boolean} props.isDisabled whether the input field should be disabled + * @param {(v: number) => void} props.onWorkingDaysChange function called when + * working days change + * @param {() => void} props.onWorkingDaysUpdateHintTimeout function called when + * update hint icon has finished its animation + * @param {number} [props.updateHintTimeout] timeout in milliseconds for update + * hint icon + * @returns {JSX.Element} + */ +const PeriodWorkingDays = ({ + className, + controlName, + data, + isDisabled, + onWorkingDaysChange, + onWorkingDaysUpdateHintTimeout, + updateHintTimeout = 2000, +}) => ( +
+ + {data.daysWorkedIsUpdated && ( + + )} + + +
+); + +PeriodWorkingDays.propTypes = { + className: PT.string, + controlName: PT.string.isRequired, + data: PT.shape({ + daysPaid: PT.number.isRequired, + daysWorked: PT.number.isRequired, + daysWorkedIsUpdated: PT.bool.isRequired, + }).isRequired, + isDisabled: PT.bool.isRequired, + onWorkingDaysChange: PT.func.isRequired, + onWorkingDaysUpdateHintTimeout: PT.func.isRequired, + updateHintTimeout: PT.number, +}; + +export default PeriodWorkingDays; diff --git a/src/routes/WorkPeriods/components/PeriodWorkingDays/styles.module.scss b/src/routes/WorkPeriods/components/PeriodWorkingDays/styles.module.scss new file mode 100644 index 0000000..bca1443 --- /dev/null +++ b/src/routes/WorkPeriods/components/PeriodWorkingDays/styles.module.scss @@ -0,0 +1,21 @@ +.container { + display: flex; + align-items: baseline; +} + +.iconPlaceholder { + align-self: center; + margin-right: 10px; + width: 30px; + height: 30px; +} + +.checkmarkIcon { + display: block; + width: 100%; + height: 100%; +} + +.daysWorkedControl { + width: 100px; +} diff --git a/src/routes/WorkPeriods/components/PeriodsHistoryItem/index.jsx b/src/routes/WorkPeriods/components/PeriodsHistoryItem/index.jsx index f38e4dc..c8ae8b7 100644 --- a/src/routes/WorkPeriods/components/PeriodsHistoryItem/index.jsx +++ b/src/routes/WorkPeriods/components/PeriodsHistoryItem/index.jsx @@ -4,12 +4,15 @@ import PT from "prop-types"; import cn from "classnames"; import debounce from "lodash/debounce"; import moment from "moment"; -import IntegerField from "components/IntegerField"; import PaymentError from "../PaymentError"; import PaymentStatus from "../PaymentStatus"; import PaymentTotal from "../PaymentTotal"; +import PeriodWorkingDays from "../PeriodWorkingDays"; import { PAYMENT_STATUS } from "constants/workPeriods"; -import { setDetailsWorkingDays } from "store/actions/workPeriods"; +import { + setDetailsWorkingDays, + toggleWorkingDaysUpdated, +} from "store/actions/workPeriods"; import { updateWorkPeriodWorkingDays } from "store/thunks/workPeriods"; import { useUpdateEffect } from "utils/hooks"; import { formatDateLabel, formatDateRange } from "utils/formatters"; @@ -35,6 +38,10 @@ const PeriodsHistoryItem = ({ isDisabled, item, data, currentStartDate }) => { [dispatch, item.id] ); + const onWorkingDaysUpdateHintTimeout = useCallback(() => { + dispatch(toggleWorkingDaysUpdated(item.id, false)); + }, [dispatch, item.id]); + const updateWorkingDays = useCallback( debounce( (daysWorked) => { @@ -85,14 +92,13 @@ const PeriodsHistoryItem = ({ isDisabled, item, data, currentStartDate }) => { {data.paymentStatus === PAYMENT_STATUS.COMPLETED ? ( `${daysWorked} ${daysWorked === 1 ? "Day" : "Days"}` ) : ( - )} diff --git a/src/routes/WorkPeriods/components/ToastPaymentsError/index.jsx b/src/routes/WorkPeriods/components/ToastPaymentsError/index.jsx index fd12b97..4812271 100644 --- a/src/routes/WorkPeriods/components/ToastPaymentsError/index.jsx +++ b/src/routes/WorkPeriods/components/ToastPaymentsError/index.jsx @@ -1,6 +1,7 @@ import React from "react"; import PT from "prop-types"; import ToastMessage from "components/ToastrMessage"; +import { formatPlural } from "utils/formatters"; /** * Displays a toastr message with info about the number of resources payments @@ -12,7 +13,7 @@ import ToastMessage from "components/ToastrMessage"; const ToastPaymentsError = ({ resourceCount, remove }) => { return ( - Failed to schedule payments for {resourceCount} resources + Failed to schedule payment for {formatPlural(resourceCount, "resource")} ); }; diff --git a/src/routes/WorkPeriods/components/ToastPaymentsProcessing/index.jsx b/src/routes/WorkPeriods/components/ToastPaymentsProcessing/index.jsx index 76357d0..a6216cf 100644 --- a/src/routes/WorkPeriods/components/ToastPaymentsProcessing/index.jsx +++ b/src/routes/WorkPeriods/components/ToastPaymentsProcessing/index.jsx @@ -1,6 +1,7 @@ import React from "react"; import PT from "prop-types"; import ToastMessage from "components/ToastrMessage"; +import { formatPlural } from "utils/formatters"; import styles from "./styles.module.scss"; /** @@ -14,7 +15,7 @@ const ToastPaymentsProcessing = ({ resourceCount, remove }) => { return ( - Payment in progress for {resourceCount} resources + Payment in progress for {formatPlural(resourceCount, "resource")} ); }; diff --git a/src/routes/WorkPeriods/components/ToastPaymentsSuccess/index.jsx b/src/routes/WorkPeriods/components/ToastPaymentsSuccess/index.jsx index bba3215..fe7ac0b 100644 --- a/src/routes/WorkPeriods/components/ToastPaymentsSuccess/index.jsx +++ b/src/routes/WorkPeriods/components/ToastPaymentsSuccess/index.jsx @@ -1,6 +1,7 @@ import React from "react"; import PT from "prop-types"; import ToastMessage from "components/ToastrMessage"; +import { formatPlural } from "utils/formatters"; /** * Displays a toastr message with info about the number of resources payments @@ -12,7 +13,7 @@ import ToastMessage from "components/ToastrMessage"; const ToastPaymentsSuccess = ({ resourceCount, remove }) => { return ( - Payment scheduled for {resourceCount} resources + Payment scheduled for {formatPlural(resourceCount, "resource")} ); }; diff --git a/src/routes/WorkPeriods/components/ToastPaymentsWarning/index.jsx b/src/routes/WorkPeriods/components/ToastPaymentsWarning/index.jsx index 43d4456..edf1307 100644 --- a/src/routes/WorkPeriods/components/ToastPaymentsWarning/index.jsx +++ b/src/routes/WorkPeriods/components/ToastPaymentsWarning/index.jsx @@ -1,6 +1,7 @@ import React from "react"; import PT from "prop-types"; import ToastMessage from "components/ToastrMessage"; +import { formatPlural } from "utils/formatters"; import styles from "./styles.module.scss"; /** @@ -28,12 +29,14 @@ const ToastPaymentsWarning = ({
- Payment scheduled for {resourcesSucceededCount} resources + Payment scheduled for{" "} + {formatPlural(resourcesSucceededCount, "resource")}
- Failed to schedule payment for {resourcesFailedCount} resources + Failed to schedule payment for{" "} + {formatPlural(resourcesFailedCount, "resource")} {resourcesFailed && resourcesFailed.length ? ":" : ""}
{resourcesFailed && resourcesFailed.length && ( diff --git a/src/routes/WorkPeriods/index.jsx b/src/routes/WorkPeriods/index.jsx index 3e9649f..25d6a96 100644 --- a/src/routes/WorkPeriods/index.jsx +++ b/src/routes/WorkPeriods/index.jsx @@ -20,28 +20,25 @@ import styles from "./styles.module.scss"; * @returns {JSX.Element} */ const WorkPeriods = () => ( - + - +
- +
diff --git a/src/routes/WorkPeriods/styles.module.scss b/src/routes/WorkPeriods/styles.module.scss index 84a9490..ef2c3b6 100644 --- a/src/routes/WorkPeriods/styles.module.scss +++ b/src/routes/WorkPeriods/styles.module.scss @@ -1,31 +1,57 @@ -.container { -} - -.periodsBlock { -} +@import "styles/mixins"; .periodsHeader { display: flex; + flex-wrap: wrap; justify-content: space-between; align-items: center; - padding: 13px 13px 13px 32px; -} + padding: 13px 13px 13px 29px; -.periodsFooter { - display: flex; - justify-content: flex-end; - align-items: center; - padding: 13px 13px 13px 32px; + @include desktop { + flex-wrap: nowrap; + } } .periodCount { + margin-right: 20px; + margin-bottom: 13px; white-space: nowrap; + + @include tablet { + margin-bottom: 0; + } + + @include desktop { + margin-right: 40px; + } } .periodWeekPicker { - margin-left: 40px; + margin-bottom: 13px; + + @include tablet { + margin-bottom: 0; + } +} + +.periodsPaginationTop { + width: 100%; + + @include tablet { + margin-left: 20px; + width: auto; + min-width: 382px; + } + + @include desktop { + margin-left: 40px; + min-width: 505px; + } } -.periodsPagination { - margin-left: 40px; +.periodsFooter { + display: flex; + justify-content: flex-end; + align-items: center; + padding: 13px 13px 13px 32px; } diff --git a/src/store/actionTypes/workPeriods.js b/src/store/actionTypes/workPeriods.js index de5fdf2..30ddbd2 100644 --- a/src/store/actionTypes/workPeriods.js +++ b/src/store/actionTypes/workPeriods.js @@ -34,4 +34,5 @@ export const WP_TOGGLE_PERIOD = "WP_TOGGLE_PERIOD"; export const WP_TOGGLE_PERIODS_ALL = "WP_TOGGLE_PERIODS_ALL"; export const WP_TOGGLE_PERIODS_VISIBLE = "WP_TOGGLE_PERIODS_VISIBLE"; export const WP_TOGGLE_PROCESSING_PAYMENTS = "WP_TOGGLE_PROCESSING_PAYMENTS"; +export const WP_TOGGLE_WORKING_DAYS_UPDATED = "WP_TOGGLE_WORKING_DAYS_UPDATED"; export const WP_UPDATE_STATE_FROM_QUERY = "WP_UPDATE_STATE_FROM_QUERY"; diff --git a/src/store/actions/workPeriods.js b/src/store/actions/workPeriods.js index e1e72a4..c4ef757 100644 --- a/src/store/actions/workPeriods.js +++ b/src/store/actions/workPeriods.js @@ -384,11 +384,24 @@ export const toggleWorkingPeriodsVisible = (on = null) => ({ * @param {?boolean} on whether to turn processing-payments state on or off * @returns {Object} */ -export const toggleWorkPeriodsProcessingPeyments = (on = null) => ({ +export const toggleWorkPeriodsProcessingPayments = (on = null) => ({ type: ACTION_TYPE.WP_TOGGLE_PROCESSING_PAYMENTS, payload: on, }); +/** + * Creates an action denoting the change of working-days-updated flag for + * working period with the specified id. + * + * @param {string} periodId working period id + * @param {boolean} on whether to toggle working-days-updated flag on or off. + * @returns {Object} + */ +export const toggleWorkingDaysUpdated = (periodId, on) => ({ + type: ACTION_TYPE.WP_TOGGLE_WORKING_DAYS_UPDATED, + payload: { periodId, on }, +}); + /** * Creates an action denoting an update of working periods state slice using * the provided query. diff --git a/src/store/reducers/workPeriods.js b/src/store/reducers/workPeriods.js index e34ddeb..4aaa439 100644 --- a/src/store/reducers/workPeriods.js +++ b/src/store/reducers/workPeriods.js @@ -38,6 +38,14 @@ const initFilters = () => ({ userHandle: "", }); +const initPeriodData = (period) => { + const data = period.data; + data.cancelSource = null; + data.daysWorkedIsUpdated = false; + delete period.data; + return data; +}; + const initPeriodDetails = ( periodId, rbId, @@ -114,9 +122,7 @@ const actionHandlers = { : oldPagination; const periodsData = {}; for (let period of periods) { - period.data.cancelSource = null; - periodsData[period.id] = period.data; - delete period.data; + periodsData[period.id] = initPeriodData(period); } return { ...state, @@ -200,9 +206,7 @@ const actionHandlers = { } const periodsData = state.periodsData[0]; for (let period of details.periods) { - period.data.cancelSource = null; - periodsData[period.id] = period.data; - delete period.data; + periodsData[period.id] = initPeriodData(period); } periodDetails = { ...periodDetails, @@ -545,6 +549,7 @@ const actionHandlers = { periodsData[periodId] = { ...periodData, cancelSource, + daysWorkedIsUpdated: false, }; return { ...state, @@ -561,6 +566,7 @@ const actionHandlers = { ...periodData, ...data, cancelSource: null, + daysWorkedIsUpdated: true, }; return { ...state, @@ -576,6 +582,7 @@ const actionHandlers = { periodsData[periodId] = { ...periodData, cancelSource: null, + daysWorkedIsUpdated: false, }; return { ...state, @@ -680,6 +687,21 @@ const actionHandlers = { isSelectedPeriodsVisible, }; }, + [ACTION_TYPE.WP_TOGGLE_WORKING_DAYS_UPDATED]: (state, { periodId, on }) => { + const periodsData = state.periodsData[0]; + const periodData = periodsData[periodId]; + if (!periodData || periodData.daysWorkedIsUpdated === on) { + return state; + } + periodsData[periodId] = { + ...periodData, + daysWorkedIsUpdated: on, + }; + return { + ...state, + periodsData: [periodsData], + }; + }, [ACTION_TYPE.WP_TOGGLE_PROCESSING_PAYMENTS]: (state, on) => { let periodsFailed = state.periodsFailed; let isProcessingPayments = on === null ? !state.isProcessingPayments : on; diff --git a/src/store/thunks/workPeriods.js b/src/store/thunks/workPeriods.js index a406918..358c716 100644 --- a/src/store/thunks/workPeriods.js +++ b/src/store/thunks/workPeriods.js @@ -293,16 +293,26 @@ export const updateWorkPeriodWorkingDays = * @param {function} getState function returning redux store root state */ export const processPayments = async (dispatch, getState) => { - dispatch(actions.toggleWorkPeriodsProcessingPeyments(true)); const state = getState(); + const isProcessing = selectors.getWorkPeriodsIsProcessingPayments(state); + if (isProcessing) { + return; + } + dispatch(actions.toggleWorkPeriodsProcessingPayments(true)); const isSelectedAll = selectors.getWorkPeriodsIsSelectedAll(state); const { pageSize, totalCount } = selectors.getWorkPeriodsPagination(state); - if (isSelectedAll && totalCount > pageSize) { - processPaymentsAll(dispatch, getState); - } else { - processPaymentsSpecific(dispatch, getState); + const promise = + isSelectedAll && totalCount > pageSize + ? processPaymentsAll(dispatch, getState) + : processPaymentsSpecific(dispatch, getState); + // The promise returned by processPaymentsAll or processPaymentsSpecific + // should never be rejected but adding try-catch block just in case. + try { + await promise; + } catch (error) { + console.error(error); } - dispatch(actions.toggleWorkPeriodsProcessingPeyments(false)); + dispatch(actions.toggleWorkPeriodsProcessingPayments(false)); }; const processPaymentsAll = async (dispatch, getState) => { diff --git a/src/utils/formatters.js b/src/utils/formatters.js index d3c0a17..de354bb 100644 --- a/src/utils/formatters.js +++ b/src/utils/formatters.js @@ -71,6 +71,18 @@ export function formatPaymentStatus(status) { return paymentStatus; } +/** + * Creates the string with the number of items and the word describing the item + * possibly in plural form. + * + * @param {number} count number of items + * @param {string} baseWord word describing the item + * @returns {string} + */ +export function formatPlural(count, baseWord) { + return `${count} ${baseWord}${count > 1 ? "s" : ""}`; +} + /** * Formats user handle link. *