Skip to content
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
Original file line number Diff line number Diff line change
Expand Up @@ -62,28 +62,11 @@ const BudgetDetailApprovedRequestHeader = () => {
const BudgetDetailApprovedRequest = ({ enterpriseId }) => {
const { isLoading, bnrRequests, fetchApprovedRequests } = useBnrSubsidyRequests({ enterpriseId });

// Transform the data to match the expected table format
// Generate status counts from the actual results data
const statusCounts = {};
(bnrRequests.results || []).forEach((request) => {
const { lastActionStatus } = request;
if (lastActionStatus) {
statusCounts[lastActionStatus] = (statusCounts[lastActionStatus] || 0) + 1;
}
});

const requestStatusCounts = Object.entries(statusCounts).map(
([lastActionStatus, count]) => ({
lastActionStatus,
count,
}),
);

const approvedRequests = {
count: bnrRequests.itemCount || 0,
numPages: bnrRequests.pageCount || 1,
results: bnrRequests.results || [],
requestStatusCounts,
requestStatusCounts: bnrRequests.learnerRequestStateCounts || [],
};

return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ import RequestAmountTableCell from './RequestAmountTableCell';
import RequestRecentActionTableCell from './RequestRecentActionTableCell';
import ApprovedRequestActionsTableCell from './ApprovedRequestActionsTableCell';
import ApprovedRequestsTableRefreshAction from './ApprovedRequestsTableRefreshAction';
import { DEFAULT_PAGE, PAGE_SIZE, REQUEST_STATUSES } from './data';
import { DEFAULT_PAGE, PAGE_SIZE } from './data';
import { transformLearnerRequestStateCounts } from './data/utils';

const FilterStatus = (rest) => (
<DataTable.FilterStatus showFilteredFields={false} {...rest} />
Expand All @@ -29,18 +30,7 @@ const BudgetDetailApprovedRequestTable = ({
fetchTableData,
}) => {
const intl = useIntl();
const statusFilterChoices = tableData.requestStatusCounts
? tableData.requestStatusCounts
.filter(({ lastActionStatus }) => {
const displayName = REQUEST_STATUSES[lastActionStatus];
return !!displayName;
})
.map(({ lastActionStatus, count }) => ({
name: REQUEST_STATUSES[lastActionStatus],
number: count,
value: lastActionStatus,
}))
: [];
const statusFilterChoices = transformLearnerRequestStateCounts(tableData.requestStatusCounts);

const approvedRequestsTableData = (() => ({
tableActions: [
Expand All @@ -51,8 +41,7 @@ const BudgetDetailApprovedRequestTable = ({

return (
<DataTable
// Temporarily disabling sorting for release
isSortable={false}
isSortable
manualSortBy
isPaginated
manualPagination
Expand Down Expand Up @@ -93,13 +82,12 @@ const BudgetDetailApprovedRequestTable = ({
description:
'Column header for the status column in the approved requests table',
}),
accessor: 'lastActionStatus',
accessor: 'learnerRequestState',
Cell: RequestStatusTableCell,
Filter: CheckboxFilter,
filter: 'includesValue',
filterChoices: statusFilterChoices,
// Temporarily disabling filters for release
disableFilters: true,
disableFilters: false,
},
{
Header: intl.formatMessage({
Expand Down Expand Up @@ -157,7 +145,7 @@ BudgetDetailApprovedRequestTable.propTypes = {
results: PropTypes.arrayOf(PropTypes.shape()),
requestStatusCounts: PropTypes.arrayOf(
PropTypes.shape({
requestStatus: PropTypes.string.isRequired,
learnerRequestState: PropTypes.string.isRequired,
count: PropTypes.number.isRequired,
}),
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ const BudgetDetailRequestsTabContent = ({ enterpriseId }) => {
const {
isLoading,
bnrRequests,
requestsOverview,
fetchBnrRequests,
refreshRequests,
} = useBnrSubsidyRequests({ enterpriseId });
Expand All @@ -35,7 +34,9 @@ const BudgetDetailRequestsTabContent = ({ enterpriseId }) => {
itemCount={bnrRequests.itemCount}
data={bnrRequests.results}
fetchData={fetchBnrRequests}
requestStatusFilterChoices={requestsOverview}
tableData={{
requestStatusCounts: bnrRequests.learnerRequestStateCounts || [],
}}
onApprove={(row) => {
setSelectedRequest(row);
setIsApproveModalOpen(true);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,45 +1,45 @@
import React from 'react';
import PropTypes from 'prop-types';
import { REQUEST_RECENT_ACTIONS } from './data';

const RequestRecentActionTableCell = ({ row }) => {
const { original } = row;
const {
requestDate,
requestStatus,
latestAction,
lastActionDate,
} = original;

const formatRequest = () => {
const hasRemindedAction = latestAction && latestAction.recentAction === REQUEST_RECENT_ACTIONS.reminded;
// Using reminded action if the latest action is 'reminded' else fall back to requestStatus
const status = hasRemindedAction ? latestAction.recentAction : requestStatus;
const formattedActionType = `${status.charAt(0).toUpperCase()}${status.slice(1)}`;

const formattedActionTimestamp = hasRemindedAction ? lastActionDate : requestDate;

return { formattedActionType, formattedActionTimestamp };
const formatRecentActionDisplay = () => {
// Check if latestAction exists and has recentAction property
if (!latestAction || !latestAction.recentAction) {
return {
actionType: 'No action',
timestamp: lastActionDate || 'N/A',
};
}

const actionType = latestAction.recentAction;
return {
actionType: `${REQUEST_RECENT_ACTIONS[actionType].charAt(0).toUpperCase()}${REQUEST_RECENT_ACTIONS[actionType].slice(1)}`,
timestamp: lastActionDate,
};
};

const { formattedActionType, formattedActionTimestamp } = formatRequest();
const { actionType, timestamp } = formatRecentActionDisplay();

return (
<span>
{formattedActionType}: {formattedActionTimestamp}
{actionType}: {timestamp}
</span>
);
};

RequestRecentActionTableCell.propTypes = {
row: PropTypes.shape({
original: PropTypes.shape({
requestDate: PropTypes.string.isRequired,
requestStatus: PropTypes.string.isRequired,
latestAction: PropTypes.shape({
recentAction: PropTypes.string.isRequired,
}).isRequired,
lastActionDate: PropTypes.string.isRequired,
recentAction: PropTypes.string,
}),
lastActionDate: PropTypes.string,
}).isRequired,
}).isRequired,
};
Expand Down
82 changes: 47 additions & 35 deletions src/components/learner-credit-management/RequestStatusTableCell.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,28 +7,24 @@ import FailedCancellation from './request-status-chips/FailedCancellation';
import FailedRedemption from './request-status-chips/FailedRedemption';
import { capitalizeFirstLetter } from '../../utils';
import {
REQUEST_ERROR_STATES, REQUEST_RECENT_ACTIONS, useBudgetId, useSubsidyAccessPolicy,
REQUEST_ERROR_STATES, useBudgetId, useSubsidyAccessPolicy,
LEARNER_CREDIT_REQUEST_STATES, LEARNER_CREDIT_REQUEST_STATE_LABELS,
} from './data';

const RequestStatusTableCell = ({ enterpriseId, row }) => {
const { original } = row;
const {
learnerEmail,
lastActionStatus,
email: learnerEmail,
learnerRequestState,
lastActionErrorReason,
requestStatus,
} = original;

// Use lastActionStatus if available, otherwise fall back to requestStatus
// There is a lot of complexity around status field inconsistencies across different data sources,
// but these inconsistencies can only be resolved by API improvements.
const { subsidyAccessPolicyId } = useBudgetId();
const { data: subsidyAccessPolicy } = useSubsidyAccessPolicy(
subsidyAccessPolicyId,
);
const sharedTrackEventMetadata = {
learnerEmail,
requestStatus,
learnerRequestState,
subsidyAccessPolicy,
};

Expand All @@ -39,50 +35,66 @@ const RequestStatusTableCell = ({ enterpriseId, row }) => {
});
};

// TODO: Consolidate status handling in future API improvements
// 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 === REQUEST_ERROR_STATES.failed_cancellation) {
return (
<FailedCancellation
learnerEmail={learnerEmail}
trackEvent={sendGenericTrackEvent}
/>
const sendErrorStateTrackEvent = (eventName, eventMetadata = {}) => {
const errorReasonMetadata = {
erroredAction: {
errorReason: lastActionErrorReason || null,
},
};
const errorStateMetadata = {
...sharedTrackEventMetadata,
...errorReasonMetadata,
...eventMetadata,
};
sendEnterpriseTrackEvent(
enterpriseId,
eventName,
errorStateMetadata,
);
};

// Learner request state is not available, so don't display anything.
if (!learnerRequestState) {
return null;
}

if (lastActionErrorReason === REQUEST_ERROR_STATES.failed_redemption) {
// Handle specific learner request states with appropriate chips
if (learnerRequestState === LEARNER_CREDIT_REQUEST_STATES.waiting) {
return (
<FailedRedemption trackEvent={sendGenericTrackEvent} />
<WaitingForLearner learnerEmail={learnerEmail} trackEvent={sendGenericTrackEvent} />
);
}

if (lastActionStatus === REQUEST_RECENT_ACTIONS.reminded || requestStatus === REQUEST_RECENT_ACTIONS.approved) {
if (learnerRequestState === LEARNER_CREDIT_REQUEST_STATES.failed) {
// Determine which failure chip to display based on the error reason
if (lastActionErrorReason === REQUEST_ERROR_STATES.failed_cancellation) {
return <FailedCancellation trackEvent={sendErrorStateTrackEvent} />;
}
if (lastActionErrorReason === REQUEST_ERROR_STATES.failed_redemption) {
return <FailedRedemption trackEvent={sendErrorStateTrackEvent} />;
}
// For other failure cases, display a generic failed chip
return (
<WaitingForLearner
learnerEmail={learnerEmail}
trackEvent={sendGenericTrackEvent}
/>
<Chip variant="dark">
{LEARNER_CREDIT_REQUEST_STATE_LABELS.failed}
</Chip>
);
}

return (
<Chip>
{`${capitalizeFirstLetter(requestStatus)}`}
</Chip>
);
// For all other states, display the appropriate label
const displayLabel = LEARNER_CREDIT_REQUEST_STATE_LABELS[learnerRequestState]
|| capitalizeFirstLetter(learnerRequestState);

return <Chip>{displayLabel}</Chip>;
};

RequestStatusTableCell.propTypes = {
enterpriseId: PropTypes.string.isRequired,
row: PropTypes.shape({
original: PropTypes.shape({
requestStatus: PropTypes.string,
learnerEmail: PropTypes.string,
lastActionStatus: PropTypes.string,
email: PropTypes.string,
learnerRequestState: PropTypes.string,
lastActionErrorReason: PropTypes.string,
recentAction: PropTypes.string,
}).isRequired,
}).isRequired,
};
Expand Down
38 changes: 25 additions & 13 deletions src/components/learner-credit-management/data/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -180,19 +180,7 @@ export const REQUEST_RECENT_ACTIONS = {
expired: 'expired',
reversed: 'reversed',
reminded: 'reminded',
};

export const REQUEST_STATUSES = {
requested: 'Requested',
pending: 'Pending',
approved: 'Waiting For Learner',
declined: 'Declined',
error: 'Errored',
accepted: 'Redeemed By Learner',
cancelled: 'Cancelled',
expired: 'Expired',
reversed: 'Refunded',
reminded: 'Waiting For Learner',
failed: 'failed',
};

export const REQUEST_ERROR_REASON = {
Expand All @@ -210,3 +198,27 @@ export const REQUEST_ERROR_STATES = {
failed_redemption: 'failed_redemption',
failed_reversal: 'failed_reversal',
};

// Learner credit request state constants based on the API payload
export const LEARNER_CREDIT_REQUEST_STATES = {
requested: 'requested',
waiting: 'waiting',
failed: 'failed',
notifying: 'notifying',
accepted: 'accepted',
cancelled: 'cancelled',
expired: 'expired',
reversed: 'reversed',
};

// Human-readable labels for learner credit request states
export const LEARNER_CREDIT_REQUEST_STATE_LABELS = {
[LEARNER_CREDIT_REQUEST_STATES.requested]: 'Requested',
[LEARNER_CREDIT_REQUEST_STATES.waiting]: 'Waiting For Learner',
[LEARNER_CREDIT_REQUEST_STATES.failed]: 'Failed',
[LEARNER_CREDIT_REQUEST_STATES.notifying]: 'Notifying',
[LEARNER_CREDIT_REQUEST_STATES.accepted]: 'Redeemed By Learner',
[LEARNER_CREDIT_REQUEST_STATES.cancelled]: 'Cancelled',
[LEARNER_CREDIT_REQUEST_STATES.expired]: 'Expired',
[LEARNER_CREDIT_REQUEST_STATES.reversed]: 'Refunded',
};
Loading