Skip to content

Commit

Permalink
wip: retry mecanism
Browse files Browse the repository at this point in the history
  • Loading branch information
r0xsh committed Dec 9, 2024
1 parent 9232e77 commit 4287d1e
Show file tree
Hide file tree
Showing 4 changed files with 199 additions and 29 deletions.
46 changes: 45 additions & 1 deletion src/navigation/task/Complete.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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 <Center>
<AlertDialog isOpen={isOpen} onClose={onClose}>
<AlertDialog.Content>
<AlertDialog.CloseButton />
<AlertDialog.Header>Task Confirmation</AlertDialog.Header>
<AlertDialog.Body>{taskConfirmation.description}
</AlertDialog.Body>
<AlertDialog.Footer>
<Button.Group space={2}>
<Button variant="unstyled" colorScheme="coolGray" onPress={onClose}>
Cancel
</Button>
<Button colorScheme="danger" onPress={onConfirm}>
Complete previous task
</Button>
</Button.Group>
</AlertDialog.Footer>
</AlertDialog.Content>
</AlertDialog>
</Center>;
};

const CompleteTask = ({
httpClient,
signatures,
pictures,
deleteSignatureAt,
deletePictureAt,
taskConfirmation,
resolveTaskConfirmation
}) => {

const { t } = useTranslation();
Expand Down Expand Up @@ -457,6 +498,7 @@ const CompleteTask = ({
>
<VStack flex={1} w="100%">
<MultipleTasksLabel tasks={ tasks } />
{taskConfirmation && <TaskConfirmationModal taskConfirmation={taskConfirmation} resolveTaskConfirmation={resolveTaskConfirmation}/>}
<TouchableWithoutFeedback
// We need to disable TouchableWithoutFeedback when keyboard is not visible,
// otherwise the ScrollView for proofs of delivery is not scrollable
Expand Down Expand Up @@ -685,13 +727,15 @@ function mapStateToProps(state) {
httpClient: state.app.httpClient,
signatures: selectSignatures(state),
pictures: selectPictures(state),
taskConfirmation: selectTaskConfirmation(state)
};
}

function mapDispatchToProps(dispatch) {
return {
deleteSignatureAt: index => dispatch(deleteSignatureAt(index)),
deletePictureAt: index => dispatch(deletePictureAt(index)),
resolveTaskConfirmation: confirmed => dispatch(resolveTaskConfirmation(confirmed)),
};
}

Expand Down
167 changes: 139 additions & 28 deletions src/redux/Courier/taskActions.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down Expand Up @@ -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());

Expand Down
14 changes: 14 additions & 0 deletions src/redux/Courier/taskEntityReducer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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) {
Expand Down
1 change: 1 addition & 0 deletions src/redux/Courier/taskSelectors.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 */

Expand Down

0 comments on commit 4287d1e

Please sign in to comment.