From 4287d1ed075a4752f4fb07a75938a385aaffb068 Mon Sep 17 00:00:00 2001 From: Antoine Bagnaud Date: Mon, 9 Dec 2024 19:23:04 +0100 Subject: [PATCH] wip: retry mecanism --- src/navigation/task/Complete.js | 46 ++++++- src/redux/Courier/taskActions.js | 167 ++++++++++++++++++++----- src/redux/Courier/taskEntityReducer.js | 14 +++ src/redux/Courier/taskSelectors.js | 1 + 4 files changed, 199 insertions(+), 29 deletions(-) diff --git a/src/navigation/task/Complete.js b/src/navigation/task/Complete.js index 3c691326c..d02bba80e 100644 --- a/src/navigation/task/Complete.js +++ b/src/navigation/task/Complete.js @@ -13,6 +13,8 @@ import { Text, TextArea, VStack, + Center, + AlertDialog } from 'native-base'; import React, { useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; @@ -49,7 +51,8 @@ import { } from '../../redux/Courier'; import { greenColor, yellowColor } from '../../styles/common'; import { doneIconName, incidentIconName } from './styles/common'; -import { reportIncident } from '../../redux/Courier/taskActions'; +import { reportIncident, resolveTaskConfirmation } from '../../redux/Courier/taskActions'; +import { selectTaskConfirmation } from '../../redux/Courier/taskSelectors'; const DELETE_ICON_SIZE = 32; const CONTENT_PADDING = 20; @@ -368,12 +371,50 @@ const FailureReasonForm = ({ data, onChange }) => { ) } +const TaskConfirmationModal = ({taskConfirmation, resolveTaskConfirmation}) => { + const [isOpen, setIsOpen] = React.useState(true); + + if (!taskConfirmation) return null + const onClose = () => { + taskConfirmation.onResolve(false) + resolveTaskConfirmation(false) + } + + const onConfirm = () => { + taskConfirmation.onResolve(true) + resolveTaskConfirmation(true) + } + + return
+ + + + Task Confirmation + {taskConfirmation.description} + + + + + + + + + +
; +}; + const CompleteTask = ({ httpClient, signatures, pictures, deleteSignatureAt, deletePictureAt, + taskConfirmation, + resolveTaskConfirmation }) => { const { t } = useTranslation(); @@ -457,6 +498,7 @@ const CompleteTask = ({ > + {taskConfirmation && } dispatch(deleteSignatureAt(index)), deletePictureAt: index => dispatch(deletePictureAt(index)), + resolveTaskConfirmation: confirmed => dispatch(resolveTaskConfirmation(confirmed)), }; } diff --git a/src/redux/Courier/taskActions.js b/src/redux/Courier/taskActions.js index 8ec88348a..d997bf011 100644 --- a/src/redux/Courier/taskActions.js +++ b/src/redux/Courier/taskActions.js @@ -49,6 +49,10 @@ export const SET_SIGNATURE_SCREEN_FIRST = 'SET_SIGNATURE_SCREEN_FIRST'; export const CHANGE_DATE = 'CHANGE_DATE'; +export const TASK_CONFIRMATION_REQUIRED = 'TASK_CONFIRMATION_REQUIRED'; +export const TASK_CONFIRMATION_RESOLVED = 'TASK_CONFIRMATION_RESOLVED'; + + /* * Action Creators */ @@ -348,45 +352,152 @@ export function markTaskFailed( }; } +/** + * Creates a task queue processor with the given context + * @returns {Function} - Configured task queue processor + */ +function createTaskProcessor(context) { + const { dispatch, httpClient, onRequireConfirmation } = context; + + async function processTaskQueue(queue = [], { onSuccess } = {}) { + if (queue.length === 0) { + if (typeof onSuccess === 'function') { + setTimeout(() => onSuccess(), 100); + } + return Promise.resolve(); + } + + const [current, ...remainingTasks] = queue; + const { task, data = {}, uploadTasks = [] } = current; + + try { + // Instead of using validateStatus option, we'll catch and handle the error directly + const response = await httpClient.put(task['@id'] + '/done', data) + .catch(error => { + // Return the error response instead of throwing + if (error.response) { + return error.response; + } + throw error; + }); + + // Check if it's a 409 response + if (response.status === 409) { + const { data: responseData } = response; + + //TODO: Transform this into translator + if (responseData?.required_action === 'validate_previous_task') { + const shouldValidate = await onRequireConfirmation({ + type: 'validate_previous_task', + taskId: responseData.previous_task, + title: 'Previous Task Incomplete', + message: 'A prerequisite task needs to be completed', + description: `Task #${responseData.previous_task} must be completed first. Would you like to complete it now?`, + confirmLabel: 'Complete Previous Task' + }); + + if (!shouldValidate) { + throw new Error(responseData.error); + } + + return processTaskQueue([ + { + task: { '@id': `/api/tasks/${responseData.previous_task}` }, + data: {} + }, + { + task, + data, + uploadTasks + }, + ...remainingTasks + ], { onSuccess }); + } + + throw new Error(responseData.error || 'Conflict error occurred'); + } + + // Handle other error status codes + if (response.status >= 400) { + throw new Error(response.data?.error || `Request failed with status ${response.status}`); + } + + //TODO: Check if the pictures are well uploaded + + // Execute upload tasks after successful task completion + if (uploadTasks.length > 0) { + await httpClient.execUploadTask(uploadTasks); + } + + return processTaskQueue(remainingTasks, { onSuccess }); + + } catch (error) { + dispatch(markTaskDoneFailure(error)); + setTimeout(() => showAlert(error.message), 100); + return Promise.reject(error); + } + } + + return processTaskQueue; +} + +// actions.js export function markTaskDone(task, notes = '', onSuccess, contactName = '') { - return function (dispatch, getState) { + return async function(dispatch, getState) { dispatch(markTaskDoneRequest(task)); + const httpClient = selectHttpClient(getState()); + const data = _.isEmpty(contactName) ? { notes } : { notes, contactName }; + + try { + // First handle image uploads + const uploadTasks = await uploadEntityImages(task, '/api/task_images', getState()); + + console.log(uploadTasks) + const processTaskQueue = createTaskProcessor({ + dispatch, + httpClient, + onRequireConfirmation: async (confirmationData) => { + return new Promise((resolve) => { + dispatch({ + type: TASK_CONFIRMATION_REQUIRED, + payload: { + ...confirmationData, + onResolve: resolve + } + }); + }); + } + }); - let payload = { - notes, - }; + await processTaskQueue([ + { + task, + data, + uploadTasks + } + ], { onSuccess }); - if (!_.isEmpty(contactName)) { - payload = { - ...payload, - contactName, - }; + dispatch(clearFiles()); + dispatch(markTaskDoneSuccess(task)); + + } catch (error) { + console.log(error) + dispatch(markTaskDoneFailure(error)); + setTimeout(() => showAlert(error.message), 100); } + }; +} - // Make sure to return a promise for testing - return uploadEntityImages(task, '/api/task_images', getState()) - .then(uploadTasks => { - return httpClient - .put(task['@id'] + '/done', payload) - .then(savedTask => { - httpClient.execUploadTask(uploadTasks); - dispatch(clearFiles()); - dispatch(markTaskDoneSuccess(savedTask)); - if (typeof onSuccess === 'function') { - setTimeout(() => onSuccess(), 100); - } - }); - }) - .catch(e => { - dispatch(markTaskDoneFailure(e)); - setTimeout(() => showAlert(e), 100); - }); +export function resolveTaskConfirmation(confirmed) { + return { + type: TASK_CONFIRMATION_RESOLVED, + payload: confirmed }; } export function markTasksDone(tasks, notes = '', onSuccess, contactName = '') { - return function (dispatch, getState) { + return async function (dispatch, getState) { dispatch(markTasksDoneRequest()); const httpClient = selectHttpClient(getState()); diff --git a/src/redux/Courier/taskEntityReducer.js b/src/redux/Courier/taskEntityReducer.js index 9d1b2a2ac..7896a0204 100644 --- a/src/redux/Courier/taskEntityReducer.js +++ b/src/redux/Courier/taskEntityReducer.js @@ -32,6 +32,8 @@ import { REPORT_INCIDENT_REQUEST, REPORT_INCIDENT_SUCCESS, REPORT_INCIDENT_FAILURE, + TASK_CONFIRMATION_REQUIRED, + TASK_CONFIRMATION_RESOLVED } from './taskActions'; import { apiSlice } from '../api/slice' @@ -70,6 +72,7 @@ const tasksEntityInitialState = { username: null, pictures: [], // Array of base64 encoded pictures signatures: [], // Array of base64 encoded signatures + taskConfirmation: null, }; function replaceItem(state, payload) { @@ -260,6 +263,17 @@ export const tasksEntityReducer = ( ...state, items: {}, }; + + case TASK_CONFIRMATION_REQUIRED: + return { + ...state, + taskConfirmation: action.payload + }; + case TASK_CONFIRMATION_RESOLVED: + return { + ...state, + taskConfirmation: null + }; } switch (true) { diff --git a/src/redux/Courier/taskSelectors.js b/src/redux/Courier/taskSelectors.js index ecae87484..17b8bc11a 100644 --- a/src/redux/Courier/taskSelectors.js +++ b/src/redux/Courier/taskSelectors.js @@ -26,6 +26,7 @@ export const selectSignatureScreenFirst = state => state.ui.tasks.signatureScreenFirst; export const selectSignatures = state => state.entities.tasks.signatures; export const selectPictures = state => state.entities.tasks.pictures; +export const selectTaskConfirmation = state => state.entities.tasks.taskConfirmation; /* Compound Selectors */