Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add more action on expense workflow #888

Merged
merged 2 commits into from
Feb 17, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions changelogs/unreleased/86726.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"title": "Expense: add cancel and return to draft status actions",
"type": "feat",
"packages": "hr"
}
26 changes: 24 additions & 2 deletions packages/apps/hr/src/api/expense-api.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,10 @@
*/

import {
createStandardSearch,
getSearchCriterias,
createStandardFetch,
createStandardSearch,
getActionApi,
getSearchCriterias,
getTypes,
} from '@axelor/aos-mobile-core';

Expand Down Expand Up @@ -202,3 +202,25 @@ export async function deleteExpense({expenseId}) {
},
});
}

export async function cancelExpense({expenseId, version}) {
return getActionApi().send({
url: `ws/aos/expense/cancel/${expenseId}`,
method: 'put',
body: {
version,
},
description: 'cancel expense',
});
}

export async function returnToDraftStatusExpense({expenseId, version}) {
return getActionApi().send({
url: `ws/aos/expense/draft/${expenseId}`,
method: 'put',
body: {
version,
},
description: 'return expense to draft status',
});
}
2 changes: 2 additions & 0 deletions packages/apps/hr/src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,13 @@ export {searchCurrencies as searchCurrenciesApi} from './currency-api';
export {getDistance as getDistanceApi} from './distance-api';
export {searchManagedEmployee as searchManagedEmployeeApi} from './employee-api';
export {
cancelExpense as cancelExpenseApi,
createExpense as createExpenseApi,
deleteExpense as deleteExpenseApi,
getExpense,
quickCreateExpense as quickCreateExpenseApi,
refuseExpense as refuseExpenseApi,
returnToDraftStatusExpense as returnToDraftStatusExpenseApi,
searchExpenseDraft as searchExpenseDraftApi,
searchExpenseToValidate as searchExpenseToValidateApi,
searchMyExpense as searchMyExpenseApi,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,25 @@ import {
} from '@axelor/aos-mobile-core';
import {Button, useThemeColor} from '@axelor/aos-mobile-ui';
import {
cancelExpense,
deleteExpense,
returnToDraftStatusExpense,
sendExpense,
validateExpense,
} from '../../../features/expenseSlice';
import {ExpenseRefusalPopup} from '../../templates';

const ExpenseDetailsValidationButton = ({expense, mode, isManualCreation}) => {
interface ExpenseDetailsValidationButtonProps {
expense: any;
mode: string;
isManualCreation?: boolean;
}

const ExpenseDetailsValidationButton = ({
expense,
mode,
isManualCreation = false,
}: ExpenseDetailsValidationButtonProps) => {
const navigation = useNavigation();
const Colors = useThemeColor();
const I18n = useTranslator();
Expand All @@ -50,7 +62,7 @@ const ExpenseDetailsValidationButton = ({expense, mode, isManualCreation}) => {

const sendExpenseAPI = useCallback(() => {
dispatch(
sendExpense({
(sendExpense as any)({
expenseId: expense.id,
version: expense.version,
onExpense: true,
Expand All @@ -60,7 +72,7 @@ const ExpenseDetailsValidationButton = ({expense, mode, isManualCreation}) => {

const validateExpenseAPI = useCallback(() => {
dispatch(
validateExpense({
(validateExpense as any)({
expenseId: expense.id,
version: expense.version,
onExpense: true,
Expand All @@ -70,10 +82,43 @@ const ExpenseDetailsValidationButton = ({expense, mode, isManualCreation}) => {
}, [dispatch, mode, expense]);

const deleteExpenseAPI = useCallback(() => {
dispatch(deleteExpense({expenseId: expense.id, userId: user.id}));
dispatch((deleteExpense as any)({expenseId: expense.id, userId: user.id}));
navigation.pop();
}, [dispatch, expense.id, navigation, user.id]);

const cancelExpenseAPI = useCallback(() => {
dispatch(
(cancelExpense as any)({
expenseId: expense.id,
version: expense.version,
mode,
user,
}),
);
}, [dispatch, expense, mode, user]);

const returnToDraftStatusExpenseAPI = useCallback(() => {
dispatch(
(returnToDraftStatusExpense as any)({
expenseId: expense.id,
version: expense.version,
user,
}),
);
}, [dispatch, expense, user]);

const renderCancelButton = width => {
return (
<Button
title={I18n.t('Base_Cancel')}
onPress={cancelExpenseAPI}
width={width}
color={Colors.errorColor}
iconName="x-lg"
/>
);
};

if (readonly) {
return null;
}
Expand All @@ -90,41 +135,67 @@ const ExpenseDetailsValidationButton = ({expense, mode, isManualCreation}) => {
iconName={isManualCreation ? 'x-lg' : 'trash3-fill'}
/>
)}
{(!canDelete || !isManualCreation) && renderCancelButton('45%')}
<Button
title={I18n.t('Hr_Send')}
onPress={sendExpenseAPI}
width="45%"
width={canDelete && !isManualCreation ? '94%' : '45%'}
iconName="send-fill"
/>
</View>
);
}

if (expense.statusSelect === Expense?.statusSelect.WaitingValidation) {
return (
<View style={styles.buttonContainer}>
{(user?.employee?.hrManager ||
expense.employee?.managerUser?.id === user.id) && (
<>
<Button
title={I18n.t('Hr_Refuse')}
onPress={() => setRefusalPopupIsOpen(true)}
color={Colors.errorColor}
width="45%"
iconName="ban"
/>
<ExpenseRefusalPopup
isOpen={refusalPopupIsOpen}
expense={expense}
expenseMode={mode}
onCancel={() => setRefusalPopupIsOpen(false)}
/>
<Button
title={I18n.t('Hr_Validate')}
onPress={validateExpenseAPI}
width="45%"
iconName="check-lg"
/>
</>
)}
{renderCancelButton('94%')}
</View>
);
}

if (expense.statusSelect === Expense?.statusSelect.Validate) {
return (
<View style={styles.buttonContainer}>{renderCancelButton('94%')}</View>
);
}

if (
(user?.employee?.hrManager ||
expense.employee?.managerUser?.id === user.id) &&
expense.statusSelect === Expense?.statusSelect.WaitingValidation
expense.statusSelect === Expense?.statusSelect.Refused ||
expense.statusSelect === Expense?.statusSelect.Canceled
) {
return (
<View style={styles.buttonContainer}>
<Button
title={I18n.t('Hr_Refuse')}
onPress={() => setRefusalPopupIsOpen(true)}
color={Colors.errorColor}
width="45%"
iconName="x-lg"
/>
<ExpenseRefusalPopup
isOpen={refusalPopupIsOpen}
expense={expense}
expenseMode={mode}
onCancel={() => setRefusalPopupIsOpen(false)}
/>
<Button
title={I18n.t('Hr_Validate')}
onPress={validateExpenseAPI}
width="45%"
iconName="check-lg"
title={I18n.t('Hr_ReturnToDraftStatus')}
onPress={returnToDraftStatusExpenseAPI}
width="94%"
color={Colors.primaryColor}
iconName="reply-fill"
/>
</View>
);
Expand All @@ -135,6 +206,7 @@ const ExpenseDetailsValidationButton = ({expense, mode, isManualCreation}) => {

const styles = StyleSheet.create({
buttonContainer: {
flexWrap: 'wrap',
flexDirection: 'row',
justifyContent: 'space-evenly',
},
Expand Down
2 changes: 2 additions & 0 deletions packages/apps/hr/src/features/asyncFunctions-index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,13 @@ export {
updateExpenseLine,
} from './expenseLineSlice';
export {
cancelExpense,
createExpense,
deleteExpense,
fetchExpenseById,
quickCreateExpense,
refuseExpense,
returnToDraftStatusExpense,
searchExpenseDraft,
searchExpenseToValidate,
searchMyExpense,
Expand Down
40 changes: 39 additions & 1 deletion packages/apps/hr/src/features/expenseSlice.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,17 @@ import {
updateAgendaItems,
} from '@axelor/aos-mobile-core';
import {
cancelExpense as _cancelExpense,
createExpense as _createExpense,
deleteExpense as _deleteExpense,
getExpense,
quickCreateExpense as _quickCreateExpense,
refuseExpense as _refuseExpense,
sendExpense as _sendExpense,
returnToDraftStatusExpense as _returnToDraftStatusExpense,
searchExpenseDraft as _searchExpenseDraft,
searchExpenseToValidate as _searchExpenseToValidate,
searchMyExpense as _searchMyExpense,
sendExpense as _sendExpense,
updateExpense as _updateExpense,
validateExpense as _validateExpense,
} from '../api/expense-api';
Expand Down Expand Up @@ -272,6 +274,42 @@ export const deleteExpense = createAsyncThunk(
},
);

export const cancelExpense = createAsyncThunk(
'hr_expense/cancelExpense',
async function (data, {getState, dispatch}) {
return handlerApiCall({
fetchFunction: _cancelExpense,
data,
action: 'Hr_SliceAction_CancelExpense',
getState,
responseOptions: {isArrayResponse: false},
}).then(() => {
dispatch(fetchExpenseById({ExpenseId: data.expenseId}));
if (data.mode === Expense.mode.validation) {
dispatch(searchExpenseToValidate({user: data.user}));
} else {
dispatch(searchMyExpense({userId: data.user?.id}));
}
});
},
);

export const returnToDraftStatusExpense = createAsyncThunk(
'hr_expense/returnToDraftStatusExpense',
async function (data, {getState, dispatch}) {
return handlerApiCall({
fetchFunction: _returnToDraftStatusExpense,
data,
action: 'Hr_SliceAction_ReturnToDraftStatusExpense',
getState,
responseOptions: {isArrayResponse: false},
}).then(() => {
dispatch(fetchExpenseById({ExpenseId: data.expenseId}));
dispatch(searchMyExpense({userId: data.user?.id}));
});
},
);

const initialState = {
loading: true,
expenseDraftList: [],
Expand Down
5 changes: 4 additions & 1 deletion packages/apps/hr/src/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@
"Hr_AvailableQty": "Available quantity",
"Hr_MissingQuantity": "Missing quantity",
"Hr_ExceedingQuantity": "Exceeding quantity",
"Hr_ReturnToDraftStatus": "Return to draft status",
"Hr_SliceAction_FetchExpenseLines": "fetch expense lines",
"Hr_SliceAction_SearchDraftExpense": "fetch draft expenses",
"Hr_SliceAction_FetchExpense": "fetch expenses",
Expand Down Expand Up @@ -160,5 +161,7 @@
"Hr_SliceAction_CancelLeave": "cancel leave",
"Hr_SliceAction_RejectLeave": "reject leave",
"Hr_SliceAction_DeleteLeave": "delete leave",
"Hr_SliceAction_CreateLeaveRequest": "create leave requests"
"Hr_SliceAction_CreateLeaveRequest": "create leave requests",
"Hr_SliceAction_CancelExpense": "cancel expense",
"Hr_SliceAction_ReturnToDraftStatusExpense": "return expense to draft status"
}
5 changes: 4 additions & 1 deletion packages/apps/hr/src/i18n/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@
"Hr_AvailableQty": "Quantité disponible",
"Hr_MissingQuantity": "Quantité manquante",
"Hr_ExceedingQuantity": "Quantité en trop",
"Hr_ReturnToDraftStatus": "Retourner à l'état brouillon",
"Hr_SliceAction_FetchExpenseLines": "récupération des lignes de frais",
"Hr_SliceAction_SearchDraftExpense": "récupération des notes de frais brouillon",
"Hr_SliceAction_FetchExpense": "récupération des notes de frais",
Expand Down Expand Up @@ -160,5 +161,7 @@
"Hr_SliceAction_CancelLeave": "annulation de la demande de congé",
"Hr_SliceAction_RejectLeave": "rejet de la demande de congé",
"Hr_SliceAction_DeleteLeave": "suppression de la demande de congé",
"Hr_SliceAction_CreateLeaveRequest": "création des demandes de congés"
"Hr_SliceAction_CreateLeaveRequest": "création des demandes de congés",
"Hr_SliceAction_CancelExpense": "annulation de la note de frais",
"Hr_SliceAction_ReturnToDraftStatusExpense": "retour à l'état brouillon de la note de frais"
}