From 5c99423be4578ef262c3646b1ea4c670d34d8cc8 Mon Sep 17 00:00:00 2001 From: Cyril Nxumalo Date: Tue, 22 Jul 2025 07:48:01 +0200 Subject: [PATCH] feat: support new latest_action API structure with nested field filtering MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove getLastActionStatus function, use API values directly - Fix errorReason field access after camelCase transformation - Add nested field filtering/sorting (action_status, action_error_reason) - Implement configuration-driven filtering for maintainability - Update status mappings (reminded vs waiting_for_learner) - Fix BnrRequestStatusCell error display (failed_approval → "Failed: Approval") - Add React.memo optimizations and fix deprecation warnings - Update tests for new API structure Maintains backward compatibility while enabling enhanced filtering capabilities. --- .../ApprovedRequestActionsTableCell.jsx | 2 +- .../BudgetDetailApprovedRequestTable.jsx | 19 +++---- .../CancelApprovedRequestModal.jsx | 2 +- .../RequestStatusTableCell.jsx | 5 +- .../tests/useBnrSubsidyRequests.test.jsx | 28 +++++++--- .../data/hooks/useBnrSubsidyRequests.js | 55 +++++++++++++------ .../requests-tab/BnrRequestStatusCell.jsx | 23 +++++++- .../tests/BudgetDetailPage.test.jsx | 4 +- 8 files changed, 95 insertions(+), 43 deletions(-) diff --git a/src/components/learner-credit-management/ApprovedRequestActionsTableCell.jsx b/src/components/learner-credit-management/ApprovedRequestActionsTableCell.jsx index 2787ce52e2..8301d87c55 100644 --- a/src/components/learner-credit-management/ApprovedRequestActionsTableCell.jsx +++ b/src/components/learner-credit-management/ApprovedRequestActionsTableCell.jsx @@ -10,7 +10,7 @@ const ApprovedRequestActionsTableCell = ({ row }) => { // Check if the cancel and remind button should be shown for this row const shouldShowShowActionButtons = ( - (original.lastActionStatus === 'waiting_for_learner' || original.requestStatus === 'approved') + (original.lastActionStatus === 'reminded' || original.requestStatus === 'approved') ); // Don't render dropdown if no actions are available diff --git a/src/components/learner-credit-management/BudgetDetailApprovedRequestTable.jsx b/src/components/learner-credit-management/BudgetDetailApprovedRequestTable.jsx index ed07d43e52..2648d62c42 100644 --- a/src/components/learner-credit-management/BudgetDetailApprovedRequestTable.jsx +++ b/src/components/learner-credit-management/BudgetDetailApprovedRequestTable.jsx @@ -17,19 +17,14 @@ const FilterStatus = (rest) => ( ); const getRequestStatusDisplayName = (status) => { - if (status === 'waiting_for_learner') { - return 'Waiting for learner'; - } + const statusDisplayMap = { + reminded: 'Waiting for learner', + approved: 'Approved', + pending: 'Pending', + refunded: 'Refunded', + }; - if (status === 'refunded') { - return 'Refunded'; - } - - if (status === 'failed_cancellation') { - return 'Failed cancellation'; - } - - return status + return statusDisplayMap[status] || status .split('_') .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) .join(' '); diff --git a/src/components/learner-credit-management/CancelApprovedRequestModal.jsx b/src/components/learner-credit-management/CancelApprovedRequestModal.jsx index f95a300a4f..a76b8cdd15 100644 --- a/src/components/learner-credit-management/CancelApprovedRequestModal.jsx +++ b/src/components/learner-credit-management/CancelApprovedRequestModal.jsx @@ -62,7 +62,7 @@ const CancelApprovedRequestModal = ({

diff --git a/src/components/learner-credit-management/RequestStatusTableCell.jsx b/src/components/learner-credit-management/RequestStatusTableCell.jsx index a821cd401b..ec9097c50b 100644 --- a/src/components/learner-credit-management/RequestStatusTableCell.jsx +++ b/src/components/learner-credit-management/RequestStatusTableCell.jsx @@ -40,7 +40,8 @@ const RequestStatusTableCell = ({ enterpriseId, row }) => { // Currently we check both `lastActionErrorReason` and `lastActionStatus` which creates // confusion since status information comes from two different sources. The API should // be updated to return a single, unified status field to simplify this logic. - if (lastActionErrorReason === 'Failed: Cancellation') { + + if (lastActionErrorReason === 'failed_cancellation') { return ( { ); } - if (lastActionStatus === 'waiting_for_learner' || requestStatus === 'approved') { + if (lastActionStatus === 'reminded' || requestStatus === 'approved') { return ( { { page: 1, page_size: 25, - search: 'test@example.com', state: 'approved,pending', + search: 'test@example.com', }, ); }); @@ -528,10 +528,24 @@ describe('applyFiltersToOptions', () => { }); }); - it('should not apply status filter when array is empty', () => { + it('should apply nested field filters correctly', () => { + const options = {}; + applyFiltersToOptions([ + { id: 'lastActionStatus', value: ['reminded', 'approved'] }, + { id: 'lastActionErrorReason', value: ['failed_cancellation'] }, + ], options); + expect(options).toEqual({ + action_status: 'reminded,approved', + action_error_reason: 'failed_cancellation', + }); + }); + + it('should not apply filters when values are empty', () => { const options = {}; applyFiltersToOptions([ { id: 'requestStatus', value: [] }, + { id: 'requestDetails', value: '' }, + { id: 'lastActionStatus', value: [] }, ], options); expect(options).toEqual({}); }); diff --git a/src/components/learner-credit-management/data/hooks/useBnrSubsidyRequests.js b/src/components/learner-credit-management/data/hooks/useBnrSubsidyRequests.js index e441f8ffb6..ab99fd9593 100644 --- a/src/components/learner-credit-management/data/hooks/useBnrSubsidyRequests.js +++ b/src/components/learner-credit-management/data/hooks/useBnrSubsidyRequests.js @@ -21,6 +21,9 @@ const API_FIELDS_BY_TABLE_COLUMN_ACCESSOR = { amount: 'amount', requestDate: 'requestDate', requestStatus: 'requestStatus', + lastActionStatus: 'latest_action__status', + lastActionErrorReason: 'latest_action__error_reason', + recentAction: 'latest_action__recent_action', }; export const applySortByToOptions = (sortBy, options) => { @@ -43,27 +46,47 @@ export const applySortByToOptions = (sortBy, options) => { } }; +// Configuration mapping for filter field transformations +const FILTER_FIELD_CONFIG = { + requestDetails: { + apiParam: 'search', + transform: (value) => value, + }, + requestStatus: { + apiParam: 'state', + transform: (value) => (Array.isArray(value) ? value.join(',') : value), + }, + lastActionStatus: { + apiParam: 'action_status', + transform: (value) => (Array.isArray(value) ? value.join(',') : value), + }, + lastActionErrorReason: { + apiParam: 'action_error_reason', + transform: (value) => (Array.isArray(value) ? value.join(',') : value), + }, +}; + export const applyFiltersToOptions = (filters, options) => { if (!filters || filters.length === 0) { return; } - const emailSearchQuery = filters.find(filter => filter.id === 'requestDetails')?.value; - const statusFilter = filters.find(filter => filter.id === 'requestStatus')?.value; - if (emailSearchQuery) { - Object.assign(options, { - search: emailSearchQuery, - }); - } + filters.forEach(filter => { + const config = FILTER_FIELD_CONFIG[filter.id]; - if (statusFilter && statusFilter.length > 0) { - Object.assign(options, { - state: statusFilter.join(','), - }); - } -}; + if (config && filter.value) { + if (Array.isArray(filter.value) && filter.value.length === 0) { + return; + } + if (typeof filter.value === 'string' && !filter.value.trim()) { + return; + } -const getLastActionStatus = (latestAction) => latestAction?.status?.toLowerCase().replace(/\s+/g, '_'); + const transformedValue = config.transform(filter.value); + Object.assign(options, { [config.apiParam]: transformedValue }); + } + }); +}; // Transform API response data to match DataTable the requirements const transformApiDataToTableData = (apiResults) => apiResults.map((item) => { @@ -85,8 +108,8 @@ const transformApiDataToTableData = (apiResults) => apiResults.map((item) => { amount: item?.coursePrice || 0, requestDate, requestStatus: item?.state, - lastActionStatus: getLastActionStatus(item?.latestAction), - lastActionErrorReason: item?.latestAction?.errorReason, + lastActionStatus: item?.latestAction?.status, // Direct assignment, no transformation + lastActionErrorReason: item?.latestAction?.errorReason, // Use error_reason field lastActionDate, latestAction: item?.latestAction, }; diff --git a/src/components/learner-credit-management/requests-tab/BnrRequestStatusCell.jsx b/src/components/learner-credit-management/requests-tab/BnrRequestStatusCell.jsx index 1e18afc367..81c1c8a37b 100644 --- a/src/components/learner-credit-management/requests-tab/BnrRequestStatusCell.jsx +++ b/src/components/learner-credit-management/requests-tab/BnrRequestStatusCell.jsx @@ -11,6 +11,24 @@ const BnrRequestStatusCell = ({ row }) => { const recentAction = latestAction?.recentAction; const [target, setTarget] = useState(null); const [isModalOpen, setIsModalOpen] = useState(false); + + // Format error reasons for display + const formatErrorReason = (reason) => { + if (!reason) { + return null; + } + + const errorReasonMap = { + failed_approval: 'Failed: Approval', + failed_cancellation: 'Failed: Cancellation', + failed_system: 'Failed: System Error', + failed_payment: 'Failed: Payment', + failed_enrollment: 'Failed: Enrollment', + }; + + return errorReasonMap[reason] + || reason.split('_').map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(': '); + }; const getStatusConfig = useMemo(() => { const statusConfigs = { requested: { @@ -44,7 +62,8 @@ const BnrRequestStatusCell = ({ row }) => { }; // Determine what to display in the chip - const displayText = errorReason || getStatusConfig.label; + const formattedErrorReason = formatErrorReason(errorReason); + const displayText = formattedErrorReason || getStatusConfig.label; const displayIcon = errorReason ? Error : getStatusConfig.icon; const displayVariant = errorReason ? 'dark' : ''; const isClickable = !!errorReason; @@ -66,7 +85,7 @@ const BnrRequestStatusCell = ({ row }) => { {errorReason && ( ', () => { const mockRequestsWithDifferentStatuses = [ { ...mockApprovedRequest, - lastActionStatus: 'waiting_for_learner', + lastActionStatus: 'reminded', }, { ...createMockApprovedRequest(), lastActionStatus: 'refunded', - lastActionErrorReason: 'Failed: Cancellation', + lastActionErrorReason: 'failed_cancellation', }, { ...createMockApprovedRequest(),