void} props.onApproveExtraWorkingDays function called when
+ * user approves adding extra working days
* @param {(v: number) => void} props.onWorkingDaysChange function called when
* working days change
* @param {() => void} props.onWorkingDaysUpdateHintTimeout function called when
@@ -26,30 +29,47 @@ import styles from "./styles.module.scss";
* @returns {JSX.Element}
*/
const PeriodWorkingDays = ({
- bookingStart,
- bookingEnd,
className,
controlName,
- data: { daysPaid, daysWorked, daysWorkedMax, daysWorkedIsUpdated },
+ data: {
+ daysPaid,
+ daysWorked,
+ daysWorkedAllowExtra,
+ daysWorkedIsUpdated,
+ daysWorkedMax,
+ },
isDisabled,
+ onApproveExtraWorkingDays,
onWorkingDaysChange,
onWorkingDaysUpdateHintTimeout,
updateHintTimeout = 2000,
}) => {
+ const [isModalOpen, setIsModalOpen] = useState(false);
+
const isBtnMinusDisabled =
daysWorked === 0 || (daysWorked > 0 && daysWorked <= daysPaid);
- const isBtnPlusDisabled = daysWorked < 5 && daysWorked >= daysWorkedMax;
+ const isBtnPlusDisabled = daysWorked >= DAYS_WORKED_HARD_LIMIT;
+
const decreaseDaysWorkedMessage = useMemo(
() => `Cannot decrease "Working Days" below the number of days already
paid for: ${daysPaid}`,
[daysPaid]
);
const increaseDaysWorkedMessage = useMemo(
- () => `Cannot increase "Working Days" because the Resource Booking period
- is between ${formatDate(bookingStart)} and ${formatDate(bookingEnd)}`,
- [bookingStart, bookingEnd]
+ () => `Maximum working days allowed is ${DAYS_WORKED_HARD_LIMIT}`,
+ []
);
+ const onApprove = useCallback(() => {
+ onApproveExtraWorkingDays();
+ setIsModalOpen(false);
+ }, [onApproveExtraWorkingDays]);
+
+ const onDismiss = () => {
+ onWorkingDaysChange(daysWorked - 1);
+ setIsModalOpen(false);
+ };
+
return (
@@ -111,12 +131,25 @@ const PeriodWorkingDays = ({
onClick={(event) => {
event.stopPropagation();
if (!isDisabled) {
- onWorkingDaysChange(Math.min(daysWorked + 1, daysWorkedMax));
+ onWorkingDaysChange(daysWorked + 1);
+ if (daysWorked + 1 > daysWorkedMax && !daysWorkedAllowExtra) {
+ setIsModalOpen(true);
+ }
}
}}
/>
+ {`The Resource Booking has only ${daysWorkedMax} real working days
+ on this week. Are you sure you would like to increase the number of
+ Working Days to more?`}
);
};
@@ -129,10 +162,12 @@ PeriodWorkingDays.propTypes = {
data: PT.shape({
daysPaid: PT.number.isRequired,
daysWorked: PT.number.isRequired,
+ daysWorkedAllowExtra: PT.bool.isRequired,
daysWorkedMax: PT.number.isRequired,
daysWorkedIsUpdated: PT.bool.isRequired,
}).isRequired,
isDisabled: PT.bool.isRequired,
+ onApproveExtraWorkingDays: PT.func.isRequired,
onWorkingDaysChange: PT.func.isRequired,
onWorkingDaysUpdateHintTimeout: PT.func.isRequired,
updateHintTimeout: PT.number,
diff --git a/src/routes/WorkPeriods/components/PeriodWorkingDays/styles.module.scss b/src/routes/WorkPeriods/components/PeriodWorkingDays/styles.module.scss
index b27f3bd..fc23bec 100644
--- a/src/routes/WorkPeriods/components/PeriodWorkingDays/styles.module.scss
+++ b/src/routes/WorkPeriods/components/PeriodWorkingDays/styles.module.scss
@@ -32,9 +32,10 @@ input.input {
flex: 1 1 0;
margin: 0;
border: none !important;
- padding: 3px 0;
+ padding: 2px 0 3px;
height: 28px;
- line-height: 22px;
+ font-size: 14px;
+ line-height: normal;
background: #fff;
outline: none !important;
box-shadow: none !important;
diff --git a/src/routes/WorkPeriods/components/PeriodsHistoryItem/index.jsx b/src/routes/WorkPeriods/components/PeriodsHistoryItem/index.jsx
index 235047a..52951fc 100644
--- a/src/routes/WorkPeriods/components/PeriodsHistoryItem/index.jsx
+++ b/src/routes/WorkPeriods/components/PeriodsHistoryItem/index.jsx
@@ -10,6 +10,7 @@ import PeriodWorkingDays from "../PeriodWorkingDays";
import { PAYMENT_STATUS } from "constants/workPeriods";
import {
setDetailsWorkingDays,
+ toggleWorkingDaysAllowExtra,
toggleWorkingDaysUpdated,
} from "store/actions/workPeriods";
import { updateWorkPeriodWorkingDays } from "store/thunks/workPeriods";
@@ -41,6 +42,10 @@ const PeriodsHistoryItem = ({
const daysWorked = data.daysWorked;
const isCurrent = item.start.isSame(currentStartDate, "date");
+ const onApproveExtraWorkingDays = useCallback(() => {
+ dispatch(toggleWorkingDaysAllowExtra(item.id, true));
+ }, [dispatch, item.id]);
+
const onWorkingDaysChange = useCallback(
(daysWorked) => {
dispatch(setDetailsWorkingDays(item.id, daysWorked));
@@ -90,9 +95,10 @@ const PeriodsHistoryItem = ({
)}
@@ -110,6 +116,7 @@ const PeriodsHistoryItem = ({
controlName={`wp_det_wd_${item.id}`}
data={data}
isDisabled={isDisabled}
+ onApproveExtraWorkingDays={onApproveExtraWorkingDays}
onWorkingDaysChange={onWorkingDaysChange}
onWorkingDaysUpdateHintTimeout={onWorkingDaysUpdateHintTimeout}
updateHintTimeout={2000}
diff --git a/src/services/workPeriods.js b/src/services/workPeriods.js
index 50a2cf8..18e9816 100644
--- a/src/services/workPeriods.js
+++ b/src/services/workPeriods.js
@@ -162,6 +162,44 @@ export const cancelWorkPeriodPayment = (paymentId) => {
.then(extractResponseData);
};
+/**
+ * Sends request to modify data for specific payment.
+ *
+ * @param {string} paymentId payment id
+ * @param {Object} payment payment data
+ * @returns {Promise}
+ */
+export const patchWorkPeriodPayment = (paymentId, payment) => {
+ return axios
+ .patch(`${PAYMENTS_API_URL}/${paymentId}`, payment)
+ .then(extractResponseData);
+};
+
+/**
+ * Sends request to modify payments' data for specific working period.
+ *
+ * @param {Array} payments
+ * @returns {Promise}
+ */
+export const patchWorkPeriodPayments = (payments) => {
+ return axios
+ .patch(`${PAYMENTS_API_URL}/bulk`, payments)
+ .then(extractResponseData);
+};
+
+/**
+ * Sends request to queue payment for specific working period.
+ *
+ * @param {Object} payment payment object
+ * @param {string} payment.workPeriodId working period id
+ * @param {number} payment.days number of days we're paying for
+ * @param {number|string} payment.amount the amount of payment
+ * @returns {Promise}
+ */
+export const postWorkPeriodPayment = (payment) => {
+ return axios.post(PAYMENTS_API_URL, payment).then(extractResponseData);
+};
+
/**
* Sends request to queue payments for specific working periods and amounts
* inside the provided array.
@@ -170,7 +208,9 @@ export const cancelWorkPeriodPayment = (paymentId) => {
* @returns {Promise}
*/
export const postWorkPeriodsPayments = (payments) => {
- return axios.post(`${PAYMENTS_API_URL}/bulk`, payments).then(extractResponseData);
+ return axios
+ .post(`${PAYMENTS_API_URL}/bulk`, payments)
+ .then(extractResponseData);
};
/**
diff --git a/src/store/actionTypes/workPeriods.js b/src/store/actionTypes/workPeriods.js
index ad0d5e9..26fb49d 100644
--- a/src/store/actionTypes/workPeriods.js
+++ b/src/store/actionTypes/workPeriods.js
@@ -24,6 +24,7 @@ export const WP_SET_ALERT_OPTION = "WP_SET_ALERT_OPTION";
export const WP_SET_PERIOD_DATA_PENDING = "WP_SET_PERIOD_DATA_PENDING";
export const WP_SET_PERIOD_DATA_SUCCESS = "WP_SET_PERIOD_DATA_SUCCESS";
export const WP_SET_PERIOD_DATA_ERROR = "WP_SET_PERIOD_DATA_ERROR";
+export const WP_SET_PERIOD_PAYMENTS = "WP_SET_PERIOD_PAYMENTS";
export const WP_SET_SORT_BY = "WP_SET_SORT_BY";
export const WP_SET_SORT_ORDER = "WP_SET_SORT_ORDER";
export const WP_SET_SORTING = "WP_SET_SORTING";
@@ -37,5 +38,6 @@ 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_EXTRA = "WP_TOGGLE_WORKING_DAYS_EXTRA";
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 46251b9..223cc24 100644
--- a/src/store/actions/workPeriods.js
+++ b/src/store/actions/workPeriods.js
@@ -268,7 +268,7 @@ export const setWorkPeriodsPaymentStatuses = (paymentStatuses) => ({
/**
* Creates an action denoting the changing of alert option
*
- * @param {Object} paymentStatuses object with working periods' payment statuses
+ * @param {Object} option object with working periods' payment statuses
* @returns {Object}
*/
export const setAlertOption = (option) => ({
@@ -315,6 +315,11 @@ export const setWorkPeriodPaymentData = (paymentData) => ({
payload: paymentData,
});
+export const setWorkPeriodPayments = (periodId, payments) => ({
+ type: ACTION_TYPE.WP_SET_PERIOD_PAYMENTS,
+ payload: { periodId, payments },
+});
+
/**
* Creates an action to change working days for specific working period.
*
@@ -399,6 +404,19 @@ export const toggleWorkPeriodsProcessingPayments = (on = null) => ({
payload: on,
});
+/**
+ * Creates an action denoting the toggling of the flag allowing extra working
+ * days.
+ *
+ * @param {string} periodId working period id
+ * @param {?boolean} on whether to toggle daysWorkedAllowExtra on or off
+ * @returns {Object}
+ */
+export const toggleWorkingDaysAllowExtra = (periodId, on = null) => ({
+ type: ACTION_TYPE.WP_TOGGLE_WORKING_DAYS_EXTRA,
+ payload: { periodId, on },
+});
+
/**
* Creates an action denoting the change of working-days-updated flag for
* working period with the specified id.
diff --git a/src/store/reducers/workPeriods.js b/src/store/reducers/workPeriods.js
index 254d775..b4312e4 100644
--- a/src/store/reducers/workPeriods.js
+++ b/src/store/reducers/workPeriods.js
@@ -12,6 +12,7 @@ import {
URL_QUERY_PARAM_MAP,
REASON_DISABLED,
ALERT,
+ DAYS_WORKED_HARD_LIMIT,
} from "constants/workPeriods";
import {
filterPeriodsByStartDate,
@@ -135,12 +136,14 @@ const actionHandlers = {
for (let period of periods) {
periodsById[period.id] = true;
let periodData = initPeriodData(period);
- periodData.daysWorkedMax = computeDaysWorkedMax(
+ let daysWorkedMax = computeDaysWorkedMax(
period.bookingStart,
period.bookingEnd,
periodStart,
periodEnd
);
+ periodData.daysWorkedAllowExtra = periodData.daysWorked > daysWorkedMax;
+ periodData.daysWorkedMax = daysWorkedMax;
periodsData[period.id] = periodData;
let reasonsDisabled = findReasonsDisabled(period, dateRange);
if (reasonsDisabled) {
@@ -244,12 +247,14 @@ const actionHandlers = {
const periodsData = state.periodsData[0];
for (let period of details.periods) {
let periodData = initPeriodData(period);
- periodData.daysWorkedMax = computeDaysWorkedMax(
+ let daysWorkedMax = computeDaysWorkedMax(
bookingStart,
bookingEnd,
period.start,
period.end
);
+ periodData.daysWorkedAllowExtra = periodData.daysWorked > daysWorkedMax;
+ periodData.daysWorkedMax = daysWorkedMax;
periodsData[period.id] = periodData;
delete period.data;
}
@@ -443,12 +448,19 @@ const actionHandlers = {
}
daysWorked = Math.min(
Math.max(daysWorked, periodData.daysPaid),
- periodData.daysWorkedMax
+ DAYS_WORKED_HARD_LIMIT
);
if (daysWorked === periodData.daysWorked) {
return state;
}
- periodsData[periodId] = { ...periodData, daysWorked };
+ periodsData[periodId] = {
+ ...periodData,
+ daysWorked,
+ daysWorkedAllowExtra:
+ daysWorked <= periodData.daysWorkedMax
+ ? false
+ : periodData.daysWorkedAllowExtra,
+ };
state = {
...state,
periodsData: [periodsData],
@@ -637,6 +649,21 @@ const actionHandlers = {
periodsData: [periodsData],
};
},
+ [ACTION_TYPE.WP_SET_PERIOD_PAYMENTS]: (state, { periodId, payments }) => {
+ const periodsData = state.periodsData[0];
+ const periodData = periodsData[periodId];
+ if (!periodData) {
+ return state;
+ }
+ periodsData[periodId] = {
+ ...periodData,
+ payments,
+ };
+ return {
+ ...state,
+ periodsData: [periodsData],
+ };
+ },
[ACTION_TYPE.WP_SET_PAYMENT_DATA]: (state, paymentData) => {
const periodId = paymentData.workPeriodId;
const periodsData = state.periodsData[0];
@@ -674,12 +701,19 @@ const actionHandlers = {
}
daysWorked = Math.min(
Math.max(daysWorked, periodData.daysPaid),
- periodData.daysWorkedMax
+ DAYS_WORKED_HARD_LIMIT
);
if (daysWorked === periodData.daysWorked) {
return state;
}
- periodsData[periodId] = { ...periodData, daysWorked };
+ periodsData[periodId] = {
+ ...periodData,
+ daysWorked,
+ daysWorkedAllowExtra:
+ daysWorked <= periodData.daysWorkedMax
+ ? false
+ : periodData.daysWorkedAllowExtra,
+ };
return updateStateAfterWorkingDaysChange(periodId, {
...state,
periodsData: [periodsData],
@@ -813,6 +847,22 @@ const actionHandlers = {
isSelectedPeriodsVisible,
};
},
+ [ACTION_TYPE.WP_TOGGLE_WORKING_DAYS_EXTRA]: (state, { periodId, on }) => {
+ const periodsData = state.periodsData[0];
+ const periodData = periodsData[periodId];
+ on = on === null ? !periodData.daysWorkedAllowExtra : on;
+ if (!periodData || periodData.daysWorkedAllowExtra === on) {
+ return state;
+ }
+ periodsData[periodId] = {
+ ...periodData,
+ daysWorkedAllowExtra: on,
+ };
+ return {
+ ...state,
+ periodsData: [periodsData],
+ };
+ },
[ACTION_TYPE.WP_TOGGLE_WORKING_DAYS_UPDATED]: (state, { periodId, on }) => {
const periodsData = state.periodsData[0];
const periodData = periodsData[periodId];
diff --git a/src/store/thunks/workPeriods.js b/src/store/thunks/workPeriods.js
index 8b65138..4f848d2 100644
--- a/src/store/thunks/workPeriods.js
+++ b/src/store/thunks/workPeriods.js
@@ -4,14 +4,16 @@ import * as actions from "store/actions/workPeriods";
import * as selectors from "store/selectors/workPeriods";
import * as services from "services/workPeriods";
import {
- SORT_BY_MAP,
+ API_CHALLENGE_PAYMENT_STATUS,
+ API_FIELDS_QUERY,
API_SORT_BY,
DATE_FORMAT_API,
PAYMENT_STATUS_MAP,
- API_FIELDS_QUERY,
- API_CHALLENGE_PAYMENT_STATUS,
+ SERVER_DATA_UPDATE_DELAY,
+ SORT_BY_MAP,
} from "constants/workPeriods";
import {
+ delay,
extractResponseData,
extractResponsePagination,
replaceItems,
@@ -20,6 +22,7 @@ import {
makeUrlQuery,
normalizeBillingAccounts,
normalizeDetailsPeriodItems,
+ normalizePaymentData,
normalizePeriodData,
normalizePeriodItems,
} from "utils/workPeriods";
@@ -33,43 +36,143 @@ import {
import { RESOURCE_BOOKING_STATUS, WORK_PERIODS_PATH } from "constants/index.js";
import { currencyFormatter } from "utils/formatters";
-export const loadWorkPeriodAfterPaymentCancel =
- (periodId, paymentId) => async (dispatch, getState) => {
+/**
+ * A thunk that adds working period's payment and reloads working period data
+ * after some delay.
+ *
+ * @param {string} workPeriodId working period id
+ * @param {Object} data payment data
+ * @param {string|number} data.amount payment amount
+ * @param {number} data.days number of days for payment
+ * @param {string} [data.workPeriodId] working period id
+ * @param {number} [periodUpdateDelay] update delay for period data
+ * @returns {function}
+ */
+export const addWorkPeriodPayment =
+ (workPeriodId, data, periodUpdateDelay = SERVER_DATA_UPDATE_DELAY) =>
+ async (dispatch) => {
+ let errorMessage = null;
+ try {
+ let paymentData = await services.postWorkPeriodPayment({
+ ...data,
+ workPeriodId,
+ });
+ if ("error" in paymentData) {
+ errorMessage = paymentData.error.message;
+ }
+ } catch (error) {
+ errorMessage = error.toString();
+ }
+ if (errorMessage) {
+ makeToast(errorMessage);
+ return false;
+ }
+ [, errorMessage] = await dispatch(
+ loadWorkPeriodData(workPeriodId, periodUpdateDelay)
+ );
+ if (errorMessage) {
+ makeToast(
+ "Additional payment scheduled for resource " +
+ "but working period data was not reloaded.\n" +
+ errorMessage,
+ "warning"
+ );
+ } else {
+ makeToast("Additional payment scheduled for resource", "success");
+ }
+ return true;
+ };
+
+/**
+ * A thunk that cancels specific working period payment, reloads WP data
+ * and updates store's state after certain delay.
+ *
+ * @param {string} periodId working period id
+ * @param {string} paymentId working period's payment id
+ * @param {number} [periodUpdateDelay] update delay for period data
+ * @returns {function}
+ */
+export const cancelWorkPeriodPayment =
+ (periodId, paymentId, periodUpdateDelay = SERVER_DATA_UPDATE_DELAY) =>
+ async (dispatch) => {
+ let paymentData = null;
+ let errorMessage = null;
+ try {
+ paymentData = await services.cancelWorkPeriodPayment(paymentId);
+ paymentData = normalizePaymentData(paymentData);
+ } catch (error) {
+ errorMessage = error.toString();
+ }
+ if (errorMessage) {
+ makeToast(errorMessage);
+ return false;
+ }
+ dispatch(actions.setWorkPeriodPaymentData(paymentData));
+ let periodData;
+ [periodData, errorMessage] = await dispatch(
+ loadWorkPeriodData(periodId, periodUpdateDelay)
+ );
+ if (errorMessage) {
+ makeToast(
+ `Payment ${paymentData.amount} was marked as "cancelled" ` +
+ "but working period data wos not reloaded.\n" +
+ errorMessage,
+ "warning"
+ );
+ } else if (periodData) {
+ let userHandle = periodData.userHandle;
+ let amount = null;
+ for (let payment of periodData.payments) {
+ if (payment.id === paymentId) {
+ amount = currencyFormatter.format(payment.amount);
+ break;
+ }
+ }
+ makeToast(
+ `Payment ${amount} for ${userHandle} was marked as "cancelled"`,
+ "success"
+ );
+ }
+ return true;
+ };
+
+/**
+ * A thunk that loads specific working period data and updates store's state.
+ *
+ * @param {string} periodId working period id
+ * @param {number} [updateDelay] update delay in milliseconds
+ * @returns {function}
+ */
+export const loadWorkPeriodData =
+ (periodId, updateDelay = 0) =>
+ async (dispatch, getState) => {
+ if (updateDelay > 0) {
+ await delay(updateDelay);
+ }
let [periodsData] = selectors.getWorkPeriodsData(getState());
periodsData[periodId]?.cancelSource?.cancel();
const [promise, source] = services.fetchWorkPeriod(periodId);
dispatch(actions.setWorkPeriodDataPending(periodId, source));
- let periodData = null;
let userHandle = null;
+ let periodData = null;
let errorMessage = null;
try {
const data = await promise;
- periodData = normalizePeriodData(data);
userHandle = data.userHandle;
+ periodData = normalizePeriodData(data);
} catch (error) {
if (!axios.isCancel(error)) {
errorMessage = error.toString();
}
}
if (periodData) {
- let amount = null;
- for (let payment of periodData.payments) {
- if (payment.id === paymentId) {
- amount = currencyFormatter.format(payment.amount);
- break;
- }
- }
dispatch(actions.setWorkPeriodDataSuccess(periodId, periodData));
- makeToast(
- `Payment ${amount} for ${userHandle} was marked as "cancelled"`,
- "success"
- );
+ return [{ ...periodData, userHandle }, null];
} else if (errorMessage) {
dispatch(actions.setWorkPeriodDataError(periodId, errorMessage));
- makeToast(
- `Failed to load data for working period ${periodId}.\n` + errorMessage
- );
+ return [null, errorMessage];
}
+ return [null, null];
};
/**
@@ -238,6 +341,130 @@ export const toggleWorkPeriodDetails =
}
};
+/**
+ * A thunk that updates working period's payment and reloads working period data
+ * after some delay.
+ *
+ * @param {string} periodId working period id
+ * @param {string} paymentId working period payment id
+ * @param {Object} data payment data
+ * @param {string|number} data.amount payment amount
+ * @param {number} [data.days] number of days for payment
+ * @param {number} [periodUpdateDelay] update delay for period data
+ * @returns {function}
+ */
+export const updateWorkPeriodPayment =
+ (periodId, paymentId, data, periodUpdateDelay = SERVER_DATA_UPDATE_DELAY) =>
+ async (dispatch) => {
+ let paymentData = null;
+ let errorMessage = null;
+ try {
+ paymentData = await services.patchWorkPeriodPayment(paymentId, data);
+ paymentData = normalizePaymentData(paymentData);
+ } catch (error) {
+ errorMessage = error.toString();
+ }
+ if (errorMessage) {
+ makeToast(errorMessage);
+ return false;
+ }
+ dispatch(actions.setWorkPeriodPaymentData(paymentData));
+ [, errorMessage] = await dispatch(
+ loadWorkPeriodData(periodId, periodUpdateDelay)
+ );
+ if (errorMessage) {
+ makeToast(
+ "Payment was successfully updated " +
+ "but working period data was not reloaded.\n" +
+ errorMessage,
+ "warning"
+ );
+ } else {
+ makeToast("Payment was successfully updated", "success");
+ }
+ return true;
+ };
+
+/**
+ * A thunk that updates the billing accounts for all the payments from the
+ * specific working period.
+ *
+ * @param {string} periodId working period id
+ * @param {number} billingAccountId desired billing account id
+ * @returns {function}
+ */
+export const updatePaymentsBillingAccount =
+ (periodId, billingAccountId) => async (dispatch, getState) => {
+ let [periodsData] = selectors.getWorkPeriodsData(getState());
+ let periodData = periodsData[periodId];
+ if (!periodData) {
+ return true; // no period to update
+ }
+ let paymentsToUpdate = [];
+ for (let payment of periodData.payments) {
+ if (payment.billingAccountId !== billingAccountId) {
+ paymentsToUpdate.push({ id: payment.id, billingAccountId });
+ }
+ }
+ if (!paymentsToUpdate.length) {
+ makeToast(
+ "All payments have desired billing account. Nothing to update.",
+ "success"
+ );
+ return true;
+ }
+ let paymentsData = null;
+ let errorMessage = null;
+ try {
+ paymentsData = await services.patchWorkPeriodPayments(paymentsToUpdate);
+ } catch (error) {
+ errorMessage = error.toString();
+ }
+ if (errorMessage) {
+ makeToast(errorMessage);
+ return false;
+ }
+ let paymentsNotUpdated = [];
+ let paymentsUpdated = new Map();
+ for (let payment of paymentsData) {
+ if ("error" in payment || payment.billingAccountId !== billingAccountId) {
+ paymentsNotUpdated.push(payment);
+ } else {
+ paymentsUpdated.set(payment.id, payment);
+ }
+ }
+ periodData = periodsData[periodId];
+ if (!periodData) {
+ return true; // no period to update
+ }
+ if (paymentsUpdated.size) {
+ let payments = [];
+ let paymentsOld = periodData.payments;
+ for (let i = 0, len = paymentsOld.length; i < len; i++) {
+ let paymentOld = paymentsOld[i];
+ if (paymentsUpdated.has(paymentOld.id)) {
+ // We update only billingAccountId because other payment properties
+ // may have been updated on the server and as a result the UI state
+ // may become inconsistent, i.e. WP properties like status and
+ // total paid may become inconsisten with payments' properties.
+ payments.push({ ...paymentOld, billingAccountId });
+ } else {
+ payments.push(paymentOld);
+ }
+ }
+ dispatch(actions.setWorkPeriodPayments(periodId, payments));
+ }
+ if (paymentsNotUpdated.length) {
+ makeToast("Could not update billing account for some payments.");
+ return false;
+ }
+ makeToast(
+ "Billing account was successfully updated for all the payments.",
+ "success"
+ );
+ return true;
+ };
+
/**
*
* @param {string} rbId
@@ -423,3 +650,25 @@ const processPaymentsSpecific = async (dispatch, getState) => {
makeToast(errorMessage);
}
};
+
+/**
+ * Sends request to process the payment and shows success or error toastrs.
+ *
+ * @param {string} workPeriodId working period id
+ * @param {number} amount the amount of payment
+ * @returns (function)
+ */
+export const processAdditionalPayment = (workPeriodId, amount) => async () => {
+ const promise = services.postWorkPeriodPayment({
+ workPeriodId,
+ days: 0,
+ amount,
+ });
+ try {
+ await promise;
+ } catch (error) {
+ makeToast(error.toString());
+ throw error;
+ }
+ makeToast("Additional payment scheduled for resource", "success");
+};
diff --git a/src/styles/toastr.scss b/src/styles/toastr.scss
index c707bbb..3589b5c 100644
--- a/src/styles/toastr.scss
+++ b/src/styles/toastr.scss
@@ -2,44 +2,45 @@
@import "variables";
.redux-toastr {
- position: absolute;
+ flex: 0 0 auto;
+ z-index: 10000;
+ position: sticky;
left: 0;
- top: 0;
+ top: -4px;
right: 0;
margin: 0;
border: none;
padding: 0;
- height: auto;
+ width: 100%;
+ height: 0;
background: transparent;
+ overflow: visible;
@include desktop {
- left: $sidebar-width;
+ padding-left: $sidebar-width;
}
> div {
- position: absolute;
- left: 10px;
- top: 24px;
- right: 10px;
+ position: relative;
margin: 0;
border: none;
padding: 0;
height: auto;
background: transparent;
+ }
+
+ .top-right {
+ z-index: 1000;
+ position: absolute;
+ left: 10px;
+ top: 24px;
+ right: 10px;
@include desktop {
left: 22px;
top: 24px;
right: 14px;
}
- }
-
- .top-right {
- z-index: 1000;
- position: absolute;
- left: 0;
- top: 0;
- right: 0;
> div {
margin-top: 10px;
diff --git a/src/styles/variables/_colors.scss b/src/styles/variables/_colors.scss
index 1abfa76..b213dff 100644
--- a/src/styles/variables/_colors.scss
+++ b/src/styles/variables/_colors.scss
@@ -18,10 +18,10 @@ $control-disabled-border-color: lighten(
$color: $control-border-color,
$amount: 5%,
);
-$control-disabled-bg-color: lighten(
- $color: $control-border-color,
- $amount: 10%,
-);
+$control-disabled-bg-color: #ddd; // lighten(
+// $color: $control-border-color,
+// $amount: 10%,
+// );
$control-disabled-text-color: lighten(
$color: $text-color,
$amount: 10%,
diff --git a/src/utils/formatters.js b/src/utils/formatters.js
index 5dca94a..955cd91 100644
--- a/src/utils/formatters.js
+++ b/src/utils/formatters.js
@@ -1,6 +1,10 @@
import moment from "moment";
import isNumber from "lodash/isNumber";
-import { DATE_FORMAT_UI, PAYMENT_STATUS_LABELS } from "constants/workPeriods";
+import {
+ DATETIME_FORMAT_UI,
+ DATE_FORMAT_UI,
+ PAYMENT_STATUS_LABELS,
+} from "constants/workPeriods";
import {
PLATFORM_WEBSITE_URL,
TAAS_BASE_PATH,
@@ -29,6 +33,16 @@ export function formatDate(date) {
return date ? moment(date).format(DATE_FORMAT_UI) : "-";
}
+/**
+ * Formats the provided time in UTC-0 as time in local timezone.
+ *
+ * @param {number} dateTime number of milliseconds since UTC epoch
+ * @returns {string}
+ */
+export function formatDateTimeAsLocal(dateTime) {
+ return moment(dateTime).format(DATETIME_FORMAT_UI);
+}
+
/**
* Returns a string denoting whether the specified start date corresponds to the
* current period or future period.
diff --git a/src/utils/hooks.js b/src/utils/hooks.js
index ba6a1d2..0ccfc4c 100644
--- a/src/utils/hooks.js
+++ b/src/utils/hooks.js
@@ -14,7 +14,10 @@ export const useClickOutside = (element, listener, deps) => {
let onClick = null;
if (element && listener) {
onClick = (event) => {
- if (!element.contains(event.target)) {
+ if (
+ document.contains(event.target) &&
+ !element.contains(event.target)
+ ) {
listener();
}
};
@@ -47,3 +50,19 @@ export const useUpdateEffect = (effect, deps) => {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, deps);
};
+
+/**
+ * A hook that returns previously saved value before component updated.
+ *
+ * @param {*} value value to save
+ * @returns {*}
+ */
+export const usePrevious = (value) => {
+ const ref = useRef();
+
+ useEffect(() => {
+ ref.current = value;
+ });
+
+ return ref.current;
+};
diff --git a/src/utils/misc.js b/src/utils/misc.js
index 9db3dca..d421899 100644
--- a/src/utils/misc.js
+++ b/src/utils/misc.js
@@ -28,7 +28,7 @@ export function filterPeriodsByStartDate(periods, startDate) {
* Returns the option which matches the provided value or null.
*
* @param {{ value: string, label: string }[]} options options object
- * @param {string} value value to search for
+ * @param {any} value value to search for
* @returns {?{ value: string, label: string }}
*/
export function getOptionByValue(options, value) {
@@ -141,6 +141,17 @@ export const buildRequestQuery = (params) => {
return queryParams.join("&");
};
+/**
+ * Function that returns a promise which resolves after the provided delay.
+ *
+ * @param {number} ms number of milliseconds
+ * @returns {Promise}
+ */
+export const delay = (ms) =>
+ new Promise((resolve) => {
+ setTimeout(resolve, ms);
+ });
+
export const extractResponsePagination = ({ headers }) => ({
totalCount: +headers["x-total"] || 0,
pageCount: +headers["x-total-pages"] || 0,
@@ -222,3 +233,15 @@ export const hoursToHumanReadableTime = (timeHrs) => {
}
return timeStr;
};
+
+/**
+ * Checks if the provided value is a valid payment amount. It can be a number
+ * or a string that can be converted to number.
+ *
+ * @param {any} value payment amount
+ * @returns {boolean}
+ */
+export function validateAmount(value) {
+ let amount = +value;
+ return !isNaN(amount) && amount > 0 && amount < 1e5 && !value.endsWith(".");
+}
diff --git a/src/utils/workPeriods.js b/src/utils/workPeriods.js
index 8a4d091..bc1e3dc 100644
--- a/src/utils/workPeriods.js
+++ b/src/utils/workPeriods.js
@@ -3,7 +3,6 @@ import {
ALERT,
API_CHALLENGE_PAYMENT_STATUS_MAP,
API_PAYMENT_STATUS_MAP,
- DATE_FORMAT_API,
DATE_FORMAT_ISO,
PAYMENT_STATUS,
REASON_DISABLED,
@@ -127,7 +126,7 @@ export function makeUrlQuery(state) {
const { pageNumber, pageSize } = pagination;
const { criteria, order } = sorting;
const params = {
- startDate: dateRange[0].format(DATE_FORMAT_API),
+ startDate: dateRange[0].format(DATE_FORMAT_ISO),
paymentStatuses: Object.keys(paymentStatuses).join(",").toLowerCase(),
alertOptions: Object.keys(alertOptions).join(",").toLowerCase(),
onlyFailedPayments: onlyFailedPayments ? "y" : "",
@@ -169,6 +168,8 @@ export function normalizePeriodItems(items) {
bookingEnd: item.endDate
? moment(item.endDate).format(DATE_FORMAT_ISO)
: "",
+ start: workPeriod.startDate,
+ end: workPeriod.endDate,
weeklyRate: item.memberRate,
data: normalizePeriodData(workPeriod),
});
@@ -214,22 +215,50 @@ export function normalizePeriodData(period) {
paymentStatus: normalizePaymentStatus(period.paymentStatus),
paymentTotal: +period.paymentTotal || 0,
};
- let payments = period.payments;
- if (payments) {
- let lastFailedPayment = null;
- for (let payment of payments) {
- payment.createdAt = moment(payment.createdAt).valueOf();
- payment.status = normalizeChallengePaymentStatus(payment.status);
- if (payment.status === PAYMENT_STATUS.FAILED) {
- lastFailedPayment = payment;
- }
+ if (period.payments) {
+ normalizePeriodPayments(period.payments, data);
+ }
+ return data;
+}
+
+/**
+ * Normalizes working period payment data object by mutating it.
+ *
+ * @param {Object} payment working period payment data object
+ * @returns {Object} working period payment data object
+ */
+export function normalizePaymentData(payment) {
+ payment.createdAt = moment.utc(payment.createdAt).valueOf();
+ payment.status = normalizeChallengePaymentStatus(payment.status);
+ return payment;
+}
+
+/**
+ * Normalizes working period payments.
+ *
+ * @param {Array} payments array of payment data
+ * @param {Object} [data] period data object to populate
+ * @returns {Array} array with normalized payments data
+ */
+export function normalizePeriodPayments(payments, data) {
+ let lastFailedPayment = null;
+ for (let payment of payments) {
+ payment.createdAt = moment.utc(payment.createdAt).valueOf();
+ payment.status = normalizeChallengePaymentStatus(payment.status);
+ if (payment.status === PAYMENT_STATUS.FAILED) {
+ lastFailedPayment = payment;
}
+ }
+ payments.sort(sortPaymentsByCreatedAt);
+ if (data) {
data.paymentErrorLast = lastFailedPayment?.statusDetails;
- data.payments = payments.sort(
- (paymentA, paymentB) => paymentA.createdAt - paymentB.createdAt
- );
+ data.payments = payments;
}
- return data;
+ return payments;
+}
+
+function sortPaymentsByCreatedAt(paymentA, paymentB) {
+ return paymentA.createdAt - paymentB.createdAt;
}
export function normalizeChallengePaymentStatus(paymentStatus) {
|