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 (
);
};
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.
*
|