From c4c5ebaefa8663968434cd02ea21ff41e764561b Mon Sep 17 00:00:00 2001 From: Hamzah Ullah Date: Tue, 4 Mar 2025 11:33:15 -0500 Subject: [PATCH] feat: datatable POC --- src/components/Admin/index.jsx | 38 +- .../Datatable/hooks/useDatatable.jsx | 150 +++++ .../EnrollmentsTable/EnrollmentsTable.jsx | 191 ++++++ .../AssignMoreCoursesEmptyStateMinimal.jsx | 3 +- .../AssignmentAmountTableCell.jsx | 5 +- .../BudgetDetail.jsx | 2 +- .../BudgetDetailPageOverviewUtilization.jsx | 7 +- .../SpendTableAmountContents.jsx | 2 +- .../SubBudgetCardUtilization.jsx | 4 +- .../AssignmentModalContent.jsx | 8 +- .../AssignmentModalSummary.jsx | 2 +- .../AssignmentModalSummaryLearnerList.jsx | 3 +- .../cards/BaseCourseCard.jsx | 3 +- .../NotEnoughBalanceAlertModal.jsx | 5 +- .../cards/data/useCourseCardMetadata.jsx | 3 +- .../cards/data/utils.ts | 3 +- .../cards/tests/CourseCard.test.jsx | 6 +- .../learner-credit-management/data/utils.js | 10 - .../tests/BudgetCard.test.jsx | 9 +- .../tests/BudgetDetailPage.test.jsx | 2 +- src/config/index.js | 2 +- src/data/actions/table.js | 6 +- src/data/services/EnterpriseDataApiService.js | 2 +- src/data/services/POCHelper.js | 576 ++++++++++++++++++ src/index.jsx | 7 +- src/utils.js | 26 +- 26 files changed, 997 insertions(+), 78 deletions(-) create mode 100644 src/components/Datatable/hooks/useDatatable.jsx create mode 100644 src/components/EnrollmentsTable/EnrollmentsTable.jsx create mode 100644 src/data/services/POCHelper.js diff --git a/src/components/Admin/index.jsx b/src/components/Admin/index.jsx index bac0fb1585..2e71f74947 100644 --- a/src/components/Admin/index.jsx +++ b/src/components/Admin/index.jsx @@ -34,6 +34,7 @@ import AIAnalyticsSummary from './AIAnalyticsSummary'; import AIAnalyticsSummarySkeleton from './AIAnalyticsSummarySkeleton'; import BudgetExpiryAlertAndModal from '../BudgetExpiryAlertAndModal'; import ModuleActivityReport from './tabs/ModuleActivityReport'; +import EnrollmentsTablePOC from '../EnrollmentsTable/EnrollmentsTable'; class Admin extends React.Component { constructor() { @@ -531,28 +532,27 @@ class Admin extends React.Component { >
- {!error && !loading && !this.hasEmptyData() && ( - <> -
-
- {this.renderDownloadButton()} -
-
- {this.displaySearchBar() && ( - this.props.searchEnrollmentsList()} - tableData={this.getTableData() ? this.getTableData().results : []} - budgets={budgets} - groups={groups} - enterpriseId={enterpriseId} - /> - )} - + {/* {!error && !loading && !this.hasEmptyData() && ( */} +
+
+ {this.renderDownloadButton()} +
+
+ {this.displaySearchBar() && ( + this.props.searchEnrollmentsList()} + tableData={this.getTableData() ? this.getTableData().results : []} + budgets={budgets} + groups={groups} + enterpriseId={enterpriseId} + /> )} + {/* )} */} {csvErrorMessage && this.renderCsvErrorMessage(csvErrorMessage)}
- {enterpriseId && tableMetadata.component} + + {/* {enterpriseId && tableMetadata.component} */}
diff --git a/src/components/Datatable/hooks/useDatatable.jsx b/src/components/Datatable/hooks/useDatatable.jsx new file mode 100644 index 0000000000..6af2488ce7 --- /dev/null +++ b/src/components/Datatable/hooks/useDatatable.jsx @@ -0,0 +1,150 @@ +import { useMemo, useState } from 'react'; +import { logError } from '@edx/frontend-platform/logging'; +import { v4 as uuidv4 } from 'uuid'; +import snakeCase from 'lodash/snakeCase'; +import { useLocation, useNavigate } from 'react-router-dom'; +import isArray from 'lodash/isArray'; +import { isObject } from 'lodash'; + +// We should attempt to include sane defaults for null values +const DEFAULT_PAGE_SIZE = 10; +const DEFAULT_PAGE_INDEX = 0; +const shortIdentifier = uuidv4().slice(0, 8); + +// Handle filter and sorting +const handleFilters = (id, filters, customFilters = {}) => { + const filterObj = {}; + // custom filter from external search input + for (const key in customFilters) { + if (customFilters[key]) { + filterObj[`${id}_${snakeCase(key)}`] = customFilters[key]; + } + } + + // default filter + filters.forEach((filter) => { + filterObj[`${id}${filter}`] = snakeCase(filter); + }); + return filterObj; +}; + +const handleSortBy = (id, sortBy) => { + const sortByObj = { }; + sortBy.forEach((sortByItem) => { + sortByObj[`${id}_ordering`] = sortByItem.desc ? `-${snakeCase(sortByItem.id)}` : `${snakeCase(sortByItem.id)}`; + }); + return sortByObj; +}; + +// A wrapper to the fetchData call to include any necessary calls required for any datatable +const fetchDataWrapper = async (args) => { + try { + const sortByObj = handleSortBy(args.dataTableId, args.sortBy); + const filtersObj = handleFilters(args.dataTableId, args.filters, args.customFilters); + const newSearchParams = { ...filtersObj, ...sortByObj }; + const updatedArgs = { ...args, searchParams: newSearchParams }; + const resolvedData = await args.fetchData(updatedArgs); + return resolvedData; + } catch (error) { + logError(error); + } +}; + +const useDatatable = (args) => { + const [data, setData] = useState([]); + const [originalResponse, setOriginalResponse] = useState(null); + const navigate = useNavigate(); + const location = useLocation(); + + // Instantiate with values from args or sane defaults + const datatableProps = useMemo(() => ({ + id: args.tableId || shortIdentifier, + initialState: args.initialState || {}, + itemCount: args.itemCount || 0, + pageCount: args.pageCount || 1, + columns: args.columns || [], + fetchData: args.fetchData || null, + data: args.data || [], + isPaginated: args.isPaginated || false, + isSortable: args.isSortable || false, + isFilterable: args.isFilterable || false, + }), [args]); + const initialState = { + pageSize: args.initialState.pageSize || DEFAULT_PAGE_SIZE, + pageIndex: args.initialState.pageIndex || DEFAULT_PAGE_INDEX, + filters: args.initialState.filters || [], + sortBy: args.initialState.sortBy || [], + }; + + // Initial State + datatableProps.initialState = args.initialState; + // columns + datatableProps.columns = args.columns || []; + + // manual pagination + if (args.manualPagination) { + datatableProps.manualPagination = true; + datatableProps.isPaginated = true; + datatableProps.initialState.pageSize = initialState.pageSize; + datatableProps.initialState.pageIndex = initialState.pageIndex; + } + // manual sort + if (args.manualSortBy) { + datatableProps.manualSortBy = true; + datatableProps.isSortable = true; + } + + // manual filters + if (args.manualFilters) { + datatableProps.manualFilters = true; + datatableProps.isFilterable = true; + } + + // Datatable data and fetch data + if (args.fetchData) { + datatableProps.fetchData = async (fetchDataArgs) => { + try { + const response = await fetchDataWrapper({ + ...fetchDataArgs, + fetchData: args.fetchData, + dataTableId: datatableProps.id, + customFilters: args.customFilters || {}, + navigate, + location, + }); + if (response) { + // TODO: Update conditionals + if (isArray(response) && data.length === 0) { + setData(response); + } else if (isObject(response) && originalResponse === null) { + setOriginalResponse(response); + const resultsData = args.dataAccessor ? response[args.dataAccessor] : response; + setData(resultsData); + } + return response; + } + return []; + } catch (error) { + logError(error); + } + return []; + }; + // Data attribute + datatableProps.data = data[args.dataAccessor] || data || args.data || []; + + // item and page count + if (datatableProps.isPaginated && originalResponse) { + if (originalResponse.count) { + datatableProps.itemCount = originalResponse.count; + } + if (originalResponse.numPages) { + datatableProps.pageCount = originalResponse.numPages; + } else if (datatableProps.itemCount) { + datatableProps.pageCount = Math.ceil(datatableProps.itemCount / datatableProps.data.length); + } + } + } + return useMemo(() => datatableProps, [datatableProps]); +}; + +export default useDatatable; diff --git a/src/components/EnrollmentsTable/EnrollmentsTable.jsx b/src/components/EnrollmentsTable/EnrollmentsTable.jsx new file mode 100644 index 0000000000..3fe5bbb963 --- /dev/null +++ b/src/components/EnrollmentsTable/EnrollmentsTable.jsx @@ -0,0 +1,191 @@ +import { DataTable, TextFilter } from '@openedx/paragon'; +import { useIntl } from '@edx/frontend-platform/i18n'; +import { useCallback, useEffect, useState } from 'react'; +import { connect } from 'react-redux'; +import { camelCaseObject } from '@edx/frontend-platform'; +import { logError } from '@edx/frontend-platform/logging'; +import { useLocation, useNavigate } from 'react-router-dom'; +import { + formatPercentage, + formatPrice, + i18nFormatPassedTimestamp, + i18nFormatProgressStatus, + i18nFormatTimestamp, + updateUrl, +} from '../../utils'; +import useDatatable from '../Datatable/hooks/useDatatable'; +import { analyticsEnrollmentsResponsePage1 } from '../../data/services/POCHelper'; + +const retrieveData = async (datatableProps, setDataset) => { + try { + const results = await datatableProps.data; + setDataset(results); + return results; + } catch (error) { + logError(error); + } +}; + +const EnrollmentsTablePOC = (props) => { + const [dataset, setDataset] = useState([]); + const intl = useIntl(); + const navigate = useNavigate(); + const location = useLocation(); + const datasetFn = useCallback(async (args) => { + // Goal is to keep the URL updated with the filter and sortby metadata keyed to the datatable id + if (args.searchParams) { + updateUrl(navigate, location.pathname, args.searchParams); + } + if (!args) { + const results = await camelCaseObject(analyticsEnrollmentsResponsePage1()); + return results; + } + let page = analyticsEnrollmentsResponsePage1(); + if (args?.pageIndex === 1) { + page = await analyticsEnrollmentsResponsePage1().next(); + } + return camelCaseObject(page); + }, []); + const datatableProps = useDatatable({ + tableId: 'enrollments', + initialState: { + pageSize: 50, + pageIndex: 0, + sortBy: [ + { id: 'lastActivityDate', desc: true }, + ], + filters: [], + }, + manualPagination: true, + manualSortBy: true, + manualFilters: true, + fetchData: datasetFn, + customFilters: props.searchParams, + dataAccessor: 'results', + columns: [ + { + Header: intl.formatMessage({ + id: 'adminPortal.enrollmentsTable.user_email', + defaultMessage: 'Email', + }), + accessor: 'userEmail', + }, + { + Header: intl.formatMessage({ + id: 'adminPortal.enrollmentsTable.user_first_name', + defaultMessage: 'First Name', + description: 'Title for the first name column in the enrollments table', + }), + accessor: 'userFirstName', + disableFilters: true, + }, + { + Header: intl.formatMessage({ + id: 'adminPortal.enrollmentsTable.user_last_name', + defaultMessage: 'Last Name', + description: 'Title for the last name column in the enrollments table', + }), + accessor: 'userLastName', + disableFilters: true, + }, + { + Header: intl.formatMessage({ + id: 'adminPortal.enrollmentsTable.courseTitle', + defaultMessage: 'Course Title', + }), + accessor: 'courseTitle', + }, + { + Header: intl.formatMessage({ + id: 'adminPortal.enrollmentsTable.courseListPrice', + defaultMessage: 'Course Price', + }), + accessor: 'courseListPrice', + Cell: ({ row }) => formatPrice(row.values.courseListPrice), + disableFilters: true, + }, + { + Header: intl.formatMessage({ + id: 'adminPortal.enrollmentsTable.courseStartDate', + defaultMessage: 'Start Date', + }), + accessor: 'courseStartDate', + Cell: ({ row }) => i18nFormatTimestamp({ intl, timestamp: row.values.courseStartDate }), + }, + { + Header: intl.formatMessage({ + id: 'adminPortal.enrollmentsTable.courseEndDate', + defaultMessage: 'End Date', + }), + accessor: 'courseEndDate', + Cell: ({ row }) => i18nFormatTimestamp({ intl, timestamp: row.values.courseEndDate }), + disableFilters: true, + }, + { + Header: intl.formatMessage({ + id: 'adminPortal.enrollmentsTable.passedDate', + defaultMessage: 'Passed Date', + }), + accessor: 'passedDate', + Cell: ({ row }) => i18nFormatPassedTimestamp({ intl, timestamp: row.values.passedDate }), + disableFilters: true, + }, + { + Header: intl.formatMessage({ + id: 'adminPortal.enrollmentsTable.currentGrade', + defaultMessage: 'Current Grade', + }), + accessor: 'currentGrade', + Cell: ({ row }) => formatPercentage({ decimal: row.values.currentGrade }), + disableFilters: true, + }, + { + Header: intl.formatMessage({ + id: 'adminPortal.enrollmentsTable.progressStatus', + defaultMessage: 'Progress Status', + }), + accessor: 'progressStatus', + Cell: ({ row }) => i18nFormatProgressStatus({ intl, progressStatus: row.values.progressStatus }), + disableFilters: true, + }, + { + Header: intl.formatMessage({ + id: 'adminPortal.enrollmentsTable.lastActivityDate', + defaultMessage: 'Last Activity Date', + }), + accessor: 'lastActivityDate', + Cell: ({ row }) => i18nFormatTimestamp({ intl, timestamp: row.values.lastActivityDate }), + disableFilters: true, + }, + ], + }); + + useEffect(() => { + retrieveData(datatableProps, setDataset); + }, [datatableProps, datasetFn, setDataset]); + + return ( +
+ +
+ + ); +}; + +const mapStateToProps = ((state, ownProps) => { + const tableState = state.table[ownProps.id] || {}; + return { + enterpriseId: state.portalConfiguration.enterpriseId, + data: tableState.data && tableState.data.results, + currentPage: tableState.data && tableState.data.current_page, + pageCount: tableState.data && tableState.data.num_pages, + }; +}); + +export default connect( + mapStateToProps, +)(EnrollmentsTablePOC); diff --git a/src/components/learner-credit-management/AssignMoreCoursesEmptyStateMinimal.jsx b/src/components/learner-credit-management/AssignMoreCoursesEmptyStateMinimal.jsx index 56bab10bcc..6d35e400f1 100644 --- a/src/components/learner-credit-management/AssignMoreCoursesEmptyStateMinimal.jsx +++ b/src/components/learner-credit-management/AssignMoreCoursesEmptyStateMinimal.jsx @@ -4,9 +4,10 @@ import { Button, Card } from '@openedx/paragon'; import { FormattedMessage } from '@edx/frontend-platform/i18n'; import { - formatDate, formatPrice, useBudgetId, usePathToCatalogTab, useSubsidyAccessPolicy, + formatDate, useBudgetId, usePathToCatalogTab, useSubsidyAccessPolicy, } from './data'; import nameYourLearner from './assets/reading.svg'; +import { formatPrice } from '../../utils'; const AssignMoreCoursesEmptyStateMinimal = () => { const { subsidyAccessPolicyId } = useBudgetId(); diff --git a/src/components/learner-credit-management/AssignmentAmountTableCell.jsx b/src/components/learner-credit-management/AssignmentAmountTableCell.jsx index fd0ec267b8..590c48f467 100644 --- a/src/components/learner-credit-management/AssignmentAmountTableCell.jsx +++ b/src/components/learner-credit-management/AssignmentAmountTableCell.jsx @@ -1,10 +1,9 @@ import React from 'react'; import PropTypes from 'prop-types'; import { useIntl } from '@edx/frontend-platform/i18n'; -import { - formatPrice, getBudgetStatus, useBudgetId, useSubsidyAccessPolicy, -} from './data'; +import { getBudgetStatus, useBudgetId, useSubsidyAccessPolicy } from './data'; import { BUDGET_STATUSES } from '../EnterpriseApp/data/constants'; +import { formatPrice } from '../../utils'; const AssignmentAmountTableCell = ({ row }) => { const intl = useIntl(); diff --git a/src/components/learner-credit-management/BudgetDetail.jsx b/src/components/learner-credit-management/BudgetDetail.jsx index d3e29ffa90..6fdec47d85 100644 --- a/src/components/learner-credit-management/BudgetDetail.jsx +++ b/src/components/learner-credit-management/BudgetDetail.jsx @@ -2,8 +2,8 @@ import React from 'react'; import PropTypes from 'prop-types'; import { ProgressBar, Stack } from '@openedx/paragon'; -import { formatPrice } from './data'; import { BUDGET_STATUSES } from '../EnterpriseApp/data/constants'; +import { formatPrice } from '../../utils'; const BudgetDetail = ({ available, utilized, limit, status, diff --git a/src/components/learner-credit-management/BudgetDetailPageOverviewUtilization.jsx b/src/components/learner-credit-management/BudgetDetailPageOverviewUtilization.jsx index a1c5f511f7..3ab30f6857 100644 --- a/src/components/learner-credit-management/BudgetDetailPageOverviewUtilization.jsx +++ b/src/components/learner-credit-management/BudgetDetailPageOverviewUtilization.jsx @@ -2,15 +2,16 @@ import React from 'react'; import PropTypes from 'prop-types'; import { connect } from 'react-redux'; import { - Stack, Collapsible, Row, Col, Button, + Button, Col, Collapsible, Row, Stack, } from '@openedx/paragon'; import { ArrowDownward } from '@openedx/paragon/icons'; import { sendEnterpriseTrackEvent } from '@edx/frontend-enterprise-utils'; -import { generatePath, useParams, Link } from 'react-router-dom'; +import { generatePath, Link, useParams } from 'react-router-dom'; import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n'; -import { formatPrice, LEARNER_CREDIT_ROUTE } from './data'; +import { LEARNER_CREDIT_ROUTE } from './data'; import EVENT_NAMES from '../../eventTracking'; +import { formatPrice } from '../../utils'; const BudgetDetailPageOverviewUtilization = ({ budgetId, diff --git a/src/components/learner-credit-management/SpendTableAmountContents.jsx b/src/components/learner-credit-management/SpendTableAmountContents.jsx index c598f5e81e..fce51b2384 100644 --- a/src/components/learner-credit-management/SpendTableAmountContents.jsx +++ b/src/components/learner-credit-management/SpendTableAmountContents.jsx @@ -2,7 +2,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import { Stack } from '@openedx/paragon'; -import { formatPrice } from './data'; +import { formatPrice } from '../../utils'; const SpendTableAmountContents = ({ row }) => { const formattedContentPrice = formatPrice(row.original.courseListPrice); diff --git a/src/components/learner-credit-management/SubBudgetCardUtilization.jsx b/src/components/learner-credit-management/SubBudgetCardUtilization.jsx index 73b9624652..f745be0b4c 100644 --- a/src/components/learner-credit-management/SubBudgetCardUtilization.jsx +++ b/src/components/learner-credit-management/SubBudgetCardUtilization.jsx @@ -1,9 +1,9 @@ import PropTypes from 'prop-types'; -import { Col, Skeleton, Card } from '@openedx/paragon'; +import { Card, Col, Skeleton } from '@openedx/paragon'; import { FormattedMessage } from '@edx/frontend-platform/i18n'; import classNames from 'classnames'; -import { formatPrice } from './data'; import { BUDGET_STATUSES } from '../EnterpriseApp/data/constants'; +import { formatPrice } from '../../utils'; const SubBudgetCardUtilization = ({ isAssignable, diff --git a/src/components/learner-credit-management/assignment-modal/AssignmentModalContent.jsx b/src/components/learner-credit-management/assignment-modal/AssignmentModalContent.jsx index 9440be580a..b9491b53f0 100644 --- a/src/components/learner-credit-management/assignment-modal/AssignmentModalContent.jsx +++ b/src/components/learner-credit-management/assignment-modal/AssignmentModalContent.jsx @@ -11,18 +11,14 @@ import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n'; import { connect } from 'react-redux'; import BaseCourseCard from '../cards/BaseCourseCard'; -import { - formatPrice, - useBudgetId, - useSubsidyAccessPolicy, - useGroupDropdownToggle, -} from '../data'; +import { useBudgetId, useGroupDropdownToggle, useSubsidyAccessPolicy } from '../data'; import AssignmentModalSummary from './AssignmentModalSummary'; import { EMAIL_ADDRESSES_INPUT_VALUE_DEBOUNCE_DELAY, isAssignEmailAddressesInputValueValid } from '../cards/data'; import AssignmentAllocationHelpCollapsibles from './AssignmentAllocationHelpCollapsibles'; import EVENT_NAMES from '../../../eventTracking'; import FlexGroupDropdown from '../FlexGroupDropdown'; import { GROUP_DROPDOWN_TEXT } from '../../PeopleManagement/constants'; +import { formatPrice } from '../../../utils'; const AssignmentModalContent = ({ enterpriseId, diff --git a/src/components/learner-credit-management/assignment-modal/AssignmentModalSummary.jsx b/src/components/learner-credit-management/assignment-modal/AssignmentModalSummary.jsx index 94c96be134..c68f9eaec7 100644 --- a/src/components/learner-credit-management/assignment-modal/AssignmentModalSummary.jsx +++ b/src/components/learner-credit-management/assignment-modal/AssignmentModalSummary.jsx @@ -5,10 +5,10 @@ import { Card, Icon, Stack } from '@openedx/paragon'; import { Error } from '@openedx/paragon/icons'; import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n'; -import { formatPrice } from '../data'; import AssignmentModalSummaryEmptyState from './AssignmentModalSummaryEmptyState'; import AssignmentModalSummaryLearnerList from './AssignmentModalSummaryLearnerList'; import AssignmentModalSummaryErrorState from './AssignmentModalSummaryErrorState'; +import { formatPrice } from '../../../utils'; const AssignmentModalSummaryContents = ({ hasLearnerEmails, diff --git a/src/components/learner-credit-management/assignment-modal/AssignmentModalSummaryLearnerList.jsx b/src/components/learner-credit-management/assignment-modal/AssignmentModalSummaryLearnerList.jsx index 72aed84360..dfb80fa6b3 100644 --- a/src/components/learner-credit-management/assignment-modal/AssignmentModalSummaryLearnerList.jsx +++ b/src/components/learner-credit-management/assignment-modal/AssignmentModalSummaryLearnerList.jsx @@ -6,7 +6,8 @@ import { Person } from '@openedx/paragon/icons'; import { useIntl } from '@edx/frontend-platform/i18n'; import { hasLearnerEmailsSummaryListTruncation, MAX_INITIAL_LEARNER_EMAILS_DISPLAYED_COUNT } from '../cards/data'; -import { formatPrice } from '../data'; + +import { formatPrice } from '../../../utils'; const AssignmentModalSummaryLearnerList = ({ courseRun, diff --git a/src/components/learner-credit-management/cards/BaseCourseCard.jsx b/src/components/learner-credit-management/cards/BaseCourseCard.jsx index 6286e7dd33..b4f069aee3 100644 --- a/src/components/learner-credit-management/cards/BaseCourseCard.jsx +++ b/src/components/learner-credit-management/cards/BaseCourseCard.jsx @@ -9,7 +9,8 @@ import { camelCaseObject } from '@edx/frontend-platform/utils'; import { FormattedMessage } from '@edx/frontend-platform/i18n'; import { useCourseCardMetadata } from './data'; import AssignmentModalImportantDates from '../assignment-modal/AssignmentModalmportantDates'; -import { formatPrice } from '../data'; + +import { formatPrice } from '../../../utils'; const BaseCourseCard = ({ original, diff --git a/src/components/learner-credit-management/cards/assignment-allocation-status-modals/NotEnoughBalanceAlertModal.jsx b/src/components/learner-credit-management/cards/assignment-allocation-status-modals/NotEnoughBalanceAlertModal.jsx index f0dacb4091..d44bf36d3b 100644 --- a/src/components/learner-credit-management/cards/assignment-allocation-status-modals/NotEnoughBalanceAlertModal.jsx +++ b/src/components/learner-credit-management/cards/assignment-allocation-status-modals/NotEnoughBalanceAlertModal.jsx @@ -1,10 +1,11 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { AlertModal, ActionRow, Button } from '@openedx/paragon'; +import { ActionRow, AlertModal, Button } from '@openedx/paragon'; import { Error } from '@openedx/paragon/icons'; import { commonErrorAlertModalPropTypes, getBudgetDisplayName } from '../data'; -import { formatPrice, useBudgetId, useSubsidyAccessPolicy } from '../../data'; +import { useBudgetId, useSubsidyAccessPolicy } from '../../data'; +import { formatPrice } from '../../../../utils'; const NotEnoughBalanceAlertModal = ({ isErrorModalOpen, diff --git a/src/components/learner-credit-management/cards/data/useCourseCardMetadata.jsx b/src/components/learner-credit-management/cards/data/useCourseCardMetadata.jsx index e9c3c288ee..c50d176047 100644 --- a/src/components/learner-credit-management/cards/data/useCourseCardMetadata.jsx +++ b/src/components/learner-credit-management/cards/data/useCourseCardMetadata.jsx @@ -6,14 +6,13 @@ import { defineMessages, useIntl } from '@edx/frontend-platform/i18n'; import { EMPTY_CONTENT_PRICE_VALUE, EXEC_ED_COURSE_TYPE, - formatPrice, getAssignableCourseRuns, getEnrollmentDeadline, useBudgetId, useCatalogContainsContentItemsMultipleQueries, useSubsidyAccessPolicy, } from '../../data'; -import { pluralText } from '../../../../utils'; +import { formatPrice, pluralText } from '../../../../utils'; import { ENTERPRISE_RESTRICTION_TYPE } from '../../data/constants'; const messages = defineMessages({ diff --git a/src/components/learner-credit-management/cards/data/utils.ts b/src/components/learner-credit-management/cards/data/utils.ts index 9088602275..1d5783dd25 100644 --- a/src/components/learner-credit-management/cards/data/utils.ts +++ b/src/components/learner-credit-management/cards/data/utils.ts @@ -1,8 +1,7 @@ import isEmail from 'validator/lib/isEmail'; import { MAX_EMAIL_ENTRY_LIMIT, MAX_INITIAL_LEARNER_EMAILS_DISPLAYED_COUNT } from './constants'; -import { formatPrice } from '../../data'; -import { makePlural, removeStringsFromList } from '../../../../utils'; +import { formatPrice, makePlural, removeStringsFromList } from '../../../../utils'; /** * Transforms and formats a policy's display name for rendering within the assignment modal's allocation alert modals. diff --git a/src/components/learner-credit-management/cards/tests/CourseCard.test.jsx b/src/components/learner-credit-management/cards/tests/CourseCard.test.jsx index f5e9f14aa4..4fd538939e 100644 --- a/src/components/learner-credit-management/cards/tests/CourseCard.test.jsx +++ b/src/components/learner-credit-management/cards/tests/CourseCard.test.jsx @@ -14,14 +14,13 @@ import dayjs from 'dayjs'; import CourseCard from '../CourseCard'; import { DATETIME_FORMAT, - formatPrice, getNormalizedEnrollByDate, learnerCreditManagementQueryKeys, SHORT_MONTH_DATE_FORMAT, useBudgetId, - useSubsidyAccessPolicy, - useEnterpriseFlexGroups, useCatalogContainsContentItemsMultipleQueries, + useEnterpriseFlexGroups, + useSubsidyAccessPolicy, } from '../../data'; import { getButtonElement, queryClient } from '../../../test/testUtils'; @@ -30,6 +29,7 @@ import { BudgetDetailPageContext } from '../../BudgetDetailPageWrapper'; import { EMAIL_ADDRESSES_INPUT_VALUE_DEBOUNCE_DELAY } from '../data'; import { getGroupMemberEmails } from '../../data/hooks/useEnterpriseFlexGroups'; import { ENTERPRISE_RESTRICTION_TYPE } from '../../data/constants'; +import { formatPrice } from '../../../../utils'; jest.mock('@edx/frontend-platform', () => ({ getConfig: jest.fn(() => ({})), diff --git a/src/components/learner-credit-management/data/utils.js b/src/components/learner-credit-management/data/utils.js index 2ce61d2115..a5e068981b 100644 --- a/src/components/learner-credit-management/data/utils.js +++ b/src/components/learner-credit-management/data/utils.js @@ -223,16 +223,6 @@ export const getBudgetStatus = ({ }; }; -export const formatPrice = (price, options = {}) => { - const USDollar = new Intl.NumberFormat('en-US', { - style: 'currency', - currency: 'USD', - minimumFractionDigits: 0, - ...options, - }); - return USDollar.format(Math.abs(price)); -}; - /** * Orders a list of budgets based on their status, end date, and name. * Active budgets come first, followed by scheduled budgets, and then expired budgets. diff --git a/src/components/learner-credit-management/tests/BudgetCard.test.jsx b/src/components/learner-credit-management/tests/BudgetCard.test.jsx index 793cc72aea..98b087fa1a 100644 --- a/src/components/learner-credit-management/tests/BudgetCard.test.jsx +++ b/src/components/learner-credit-management/tests/BudgetCard.test.jsx @@ -4,20 +4,17 @@ import { Provider } from 'react-redux'; import thunk from 'redux-thunk'; import configureMockStore from 'redux-mock-store'; import dayjs from 'dayjs'; -import { - screen, - render, - within, -} from '@testing-library/react'; +import { render, screen, within } from '@testing-library/react'; import '@testing-library/jest-dom/extend-expect'; import { IntlProvider } from '@edx/frontend-platform/i18n'; import { QueryClientProvider } from '@tanstack/react-query'; import BudgetCard from '../BudgetCard'; -import { formatPrice, useSubsidySummaryAnalyticsApi, useBudgetRedemptions } from '../data'; +import { useBudgetRedemptions, useSubsidySummaryAnalyticsApi } from '../data'; import { BUDGET_STATUSES, BUDGET_TYPES } from '../../EnterpriseApp/data/constants'; import { EnterpriseSubsidiesContext } from '../../EnterpriseSubsidiesContext'; import { queryClient } from '../../test/testUtils'; +import { formatPrice } from '../../../utils'; jest.mock('../../EnterpriseSubsidiesContext/data/hooks', () => ({ ...jest.requireActual('../../EnterpriseSubsidiesContext/data/hooks'), diff --git a/src/components/learner-credit-management/tests/BudgetDetailPage.test.jsx b/src/components/learner-credit-management/tests/BudgetDetailPage.test.jsx index be436e2d0c..67056f3d6d 100644 --- a/src/components/learner-credit-management/tests/BudgetDetailPage.test.jsx +++ b/src/components/learner-credit-management/tests/BudgetDetailPage.test.jsx @@ -19,7 +19,6 @@ import BudgetDetailPage from '../BudgetDetailPage'; import { DEFAULT_PAGE, formatDate, - formatPrice, PAGE_SIZE, useBudgetContentAssignments, useBudgetDetailActivityOverview, @@ -47,6 +46,7 @@ import { mockSubsidySummary, } from '../data/tests/constants'; import { getButtonElement, queryClient } from '../../test/testUtils'; +import { formatPrice } from '../../../utils'; jest.mock('@edx/frontend-enterprise-utils', () => ({ ...jest.requireActual('@edx/frontend-enterprise-utils'), diff --git a/src/config/index.js b/src/config/index.js index 38606e319b..172a65ea57 100644 --- a/src/config/index.js +++ b/src/config/index.js @@ -43,7 +43,7 @@ const features = { CODE_MANAGEMENT: process.env.FEATURE_CODE_MANAGEMENT || hasFeatureFlagEnabled('CODE_MANAGEMENT'), REPORTING_CONFIGURATIONS: process.env.FEATURE_REPORTING_CONFIGURATIONS || hasFeatureFlagEnabled('REPORTING_CONFIGURATIONS'), ANALYTICS: process.env.FEATURE_ANALYTICS || hasFeatureFlagEnabled('ANALYTICS'), - ANALYTICS_SUPPORTED: process.env.ANALYTICS_SUPPORTED || hasFeatureFlagEnabled('ANALYTICS_SUPPORTED'), + ANALYTICS_SUPPORTED: true || hasFeatureFlagEnabled('ANALYTICS_SUPPORTED'), SAML_CONFIGURATION: process.env.FEATURE_SAML_CONFIGURATION || hasFeatureFlagEnabled('SAML_CONFIGURATION'), SUPPORT: process.env.FEATURE_SUPPORT || hasFeatureFlagEnabled('SUPPORT'), EXTERNAL_LMS_CONFIGURATION: process.env.FEATURE_EXTERNAL_LMS_CONFIGURATION || hasFeatureFlagEnabled('EXTERNAL_LMS_CONFIGURATION'), diff --git a/src/data/actions/table.js b/src/data/actions/table.js index 1ad8517a17..a160f26652 100644 --- a/src/data/actions/table.js +++ b/src/data/actions/table.js @@ -2,13 +2,13 @@ import { logError } from '@edx/frontend-platform/logging'; import { getPageOptionsFromUrl } from '../../utils'; import { + CLEAR_TABLE, + PAGINATION_FAILURE, PAGINATION_REQUEST, PAGINATION_SUCCESS, - PAGINATION_FAILURE, + SORT_FAILURE, SORT_REQUEST, SORT_SUCCESS, - SORT_FAILURE, - CLEAR_TABLE, } from '../constants/table'; const paginationRequest = (tableId, options) => ({ diff --git a/src/data/services/EnterpriseDataApiService.js b/src/data/services/EnterpriseDataApiService.js index 66005ba410..d4efec5315 100644 --- a/src/data/services/EnterpriseDataApiService.js +++ b/src/data/services/EnterpriseDataApiService.js @@ -1,5 +1,5 @@ import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; -import { snakeCaseObject, camelCaseObject } from '@edx/frontend-platform/utils'; +import { camelCaseObject, snakeCaseObject } from '@edx/frontend-platform/utils'; import omitBy from 'lodash/omitBy'; import { isFalsy } from '../../utils'; diff --git a/src/data/services/POCHelper.js b/src/data/services/POCHelper.js new file mode 100644 index 0000000000..1029946454 --- /dev/null +++ b/src/data/services/POCHelper.js @@ -0,0 +1,576 @@ +export function analyticsEnrollmentsResponsePage1() { + return { + // eslint-disable-next-line @typescript-eslint/no-use-before-define + next: analyticsEnrollmentsResponsePage2, + previous: null, + count: 10, + num_pages: 2, + current_page: 1, + start: 0, + results: [ + { + + is_consent_granted: true, + paid_by: 'Test', + user_current_enrollment_mode: 'verified', + enrollment_date: '2025-02-19', + unenrollment_date: null, + unenrollment_end_within_date: '2025-03-05', + is_refunded: false, + seat_delivery_method: 'Learner Credit', + + offer_name: 'Test - Year 2 LC All edX / Exec Ed - 2024/2025', + offer_type: null, + coupon_code: null, + coupon_name: null, + + course_list_price: 249.0, + amount_learner_paid: 0.0, + course_key: 'USMx+PMCOMM04', + courserun_key: 'course-v1:USMx+PMCOMM04+2T2024', + course_title: 'Designing Project Information Hubs for Program and Project Performance', + course_pacing_type: 'self_paced', + course_start_date: '2024-07-01', + course_end_date: '2026-07-01', + course_duration_weeks: 4, + course_max_effort: 5, + course_min_effort: 3, + course_primary_program: 'Program Management and the Art of Communication', + primary_program_type: 'Professional Certificate', + course_primary_subject: 'business-management', + has_passed: false, + last_activity_date: '2025-02-24', + progress_status: 'In Progress', + passed_date: null, + current_grade: 0.0, + letter_grade: null, + + user_email: 'test_cust_6@Testrein.com', + user_account_creation_date: '2025-02-19T14:44:37Z', + user_country_code: 'BR', + + user_first_name: '', + user_last_name: '', + enterprise_name: 'Test', + + enterprise_sso_uid: 'fake_sso', + created: '2025-02-25T03:27:50Z', + course_api_url: '/enterprise/v1/enterprise-catalogs/test-uuid/courses/course-v1:USMx+PMCOMM04+2T2024', + total_learning_time_hours: 0.13, + is_subsidy: false, + course_product_line: 'OCM', + + enterprise_group_name: null, + enterprise_group_uuid: null, + }, + { + + is_consent_granted: true, + paid_by: 'Test', + user_current_enrollment_mode: 'verified', + enrollment_date: '2025-01-27', + unenrollment_date: null, + unenrollment_end_within_date: '2025-02-10', + is_refunded: false, + seat_delivery_method: 'Learner Credit', + + offer_name: 'Test - Year 2 LC All edX / Exec Ed - 2024/2025', + offer_type: null, + coupon_code: null, + coupon_name: null, + + course_list_price: 299.0, + amount_learner_paid: 0.0, + course_key: 'HarvardX+CS50P', + courserun_key: 'course-v1:HarvardX+CS50P+Python', + course_title: "CS50's Introduction to Programming with Python", + course_pacing_type: 'self_paced', + course_start_date: '2022-04-01', + course_end_date: '2025-12-31', + course_duration_weeks: 10, + course_max_effort: 9, + course_min_effort: 3, + course_primary_program: 'Computer Science for Python Programming', + primary_program_type: 'Professional Certificate', + course_primary_subject: 'computer-science', + has_passed: false, + last_activity_date: '2025-02-24', + progress_status: 'In Progress', + passed_date: null, + current_grade: 0.0, + letter_grade: null, + + user_email: 'test_cust_8@Test.com', + user_account_creation_date: '2021-02-02T09:21:26Z', + user_country_code: 'NO', + + user_first_name: 'Greatness', + user_last_name: 'Unknown', + enterprise_name: 'Test', + + enterprise_sso_uid: 'fake_sso', + created: '2025-02-25T03:27:50Z', + course_api_url: '/enterprise/v1/enterprise-catalogs/test-uuid/courses/course-v1:HarvardX+CS50P+Python', + total_learning_time_hours: 12.92, + is_subsidy: false, + course_product_line: 'OCM', + + enterprise_group_name: null, + enterprise_group_uuid: null, + }, + { + + is_consent_granted: true, + paid_by: 'Test', + user_current_enrollment_mode: 'verified', + enrollment_date: '2025-02-21', + unenrollment_date: null, + unenrollment_end_within_date: '2025-03-07', + is_refunded: false, + seat_delivery_method: 'Learner Credit', + + offer_name: 'Test - Year 2 LC All edX / Exec Ed - 2024/2025', + offer_type: null, + coupon_code: null, + coupon_name: null, + + course_list_price: 189.0, + amount_learner_paid: 0.0, + course_key: 'UBCx+Excel1.1x', + courserun_key: 'course-v1:UBCx+Excel1.1x+1T2021', + course_title: 'Excel for Everyone: Core Foundations', + course_pacing_type: 'self_paced', + course_start_date: '2021-04-06', + course_end_date: '2025-12-31', + course_duration_weeks: 6, + course_max_effort: 6, + course_min_effort: 4, + course_primary_program: 'Excel for Everyone', + primary_program_type: 'Professional Certificate', + course_primary_subject: 'data-analysis-statistics', + has_passed: false, + last_activity_date: '2025-02-24', + progress_status: 'In Progress', + passed_date: null, + current_grade: 0.25, + letter_grade: null, + + user_email: 'test_cust_9@Test.com', + user_account_creation_date: '2025-02-19T11:43:30Z', + user_country_code: 'BR', + + user_first_name: '', + user_last_name: '', + enterprise_name: 'Test', + + enterprise_sso_uid: 'fake_sso', + created: '2025-02-25T03:27:50Z', + course_api_url: '/enterprise/v1/enterprise-catalogs/test-uuid/courses/course-v1:UBCx+Excel1.1x+1T2021', + total_learning_time_hours: 1.21, + is_subsidy: false, + course_product_line: 'OCM', + + enterprise_group_name: null, + enterprise_group_uuid: null, + }, + { + + is_consent_granted: true, + paid_by: 'Test', + user_current_enrollment_mode: 'verified', + enrollment_date: '2025-01-08', + unenrollment_date: null, + unenrollment_end_within_date: '2025-01-22', + is_refunded: false, + seat_delivery_method: 'Learner Credit', + + offer_name: 'Test - Year 2 LC All edX / Exec Ed - 2024/2025', + offer_type: null, + coupon_code: null, + coupon_name: null, + + course_list_price: 249.0, + amount_learner_paid: 0.0, + course_key: 'DavidsonX+DavidsonX_D008', + courserun_key: 'course-v1:DavidsonX+DavidsonX_D008+2T2023', + course_title: 'Excel for Beginners', + course_pacing_type: 'self_paced', + course_start_date: '2023-08-01', + course_end_date: '2025-08-31', + course_duration_weeks: 4, + course_max_effort: 5, + course_min_effort: 3, + course_primary_program: 'Data Analysis Basics with Excel and R', + primary_program_type: 'Professional Certificate', + course_primary_subject: 'data-analysis-statistics', + has_passed: false, + last_activity_date: '2025-02-24', + progress_status: 'In Progress', + passed_date: null, + current_grade: 0.01, + letter_grade: null, + + user_email: 'test_cust_10@Test.com', + user_account_creation_date: '2025-01-08T16:31:57Z', + user_country_code: 'US', + + user_first_name: '', + user_last_name: '', + enterprise_name: 'Test', + + enterprise_sso_uid: 'fake_sso', + created: '2025-02-25T03:27:50Z', + course_api_url: '/enterprise/v1/enterprise-catalogs/test-uuid/courses/course-v1:DavidsonX+DavidsonX_D008+2T2023', + total_learning_time_hours: 1.13, + is_subsidy: false, + course_product_line: 'OCM', + + enterprise_group_name: null, + enterprise_group_uuid: null, + }, + { + + is_consent_granted: true, + paid_by: 'Test', + user_current_enrollment_mode: 'verified', + enrollment_date: '2025-01-08', + unenrollment_date: null, + unenrollment_end_within_date: '2025-01-22', + is_refunded: false, + seat_delivery_method: 'Learner Credit', + + offer_name: 'Test - Year 2 LC All edX / Exec Ed - 2024/2025', + offer_type: null, + coupon_code: null, + coupon_name: null, + + course_list_price: 249.0, + amount_learner_paid: 0.0, + course_key: 'DavidsonX+DavidsonX_D008', + courserun_key: 'course-v1:DavidsonX+DavidsonX_D008+2T2023', + course_title: 'Excel for Beginners', + course_pacing_type: 'self_paced', + course_start_date: '2023-08-01', + course_end_date: '2025-08-31', + course_duration_weeks: 4, + course_max_effort: 5, + course_min_effort: 3, + course_primary_program: 'Data Analysis Basics with Excel and R', + primary_program_type: 'Professional Certificate', + course_primary_subject: 'data-analysis-statistics', + has_passed: true, + last_activity_date: '2025-02-24', + progress_status: 'Passed', + passed_date: '2025-02-17', + current_grade: 0.9, + letter_grade: 'A', + + user_email: 'test_cust_11@Test.com', + user_account_creation_date: '2025-01-08T15:09:46Z', + user_country_code: 'US', + + user_first_name: '', + user_last_name: '', + enterprise_name: 'Test', + + enterprise_sso_uid: 'fake_sso', + created: '2025-02-25T03:27:50Z', + course_api_url: '/enterprise/v1/enterprise-catalogs/test-uuid/courses/course-v1:DavidsonX+DavidsonX_D008+2T2023', + total_learning_time_hours: 14.14, + is_subsidy: false, + course_product_line: 'OCM', + + enterprise_group_name: null, + enterprise_group_uuid: null, + }, + ], + }; +} + +export function analyticsEnrollmentsResponsePage2() { + return { + next: null, + previous: analyticsEnrollmentsResponsePage1, + count: 10, + num_pages: 2, + current_page: 2, + start: 10, + results: [ + { + + is_consent_granted: true, + paid_by: 'Test', + user_current_enrollment_mode: 'verified', + enrollment_date: '2025-02-14', + unenrollment_date: null, + unenrollment_end_within_date: '2025-02-28', + is_refunded: false, + seat_delivery_method: 'Learner Credit', + + offer_name: 'Test - Year 2 LC All edX / Exec Ed - 2024/2025', + offer_type: null, + coupon_code: null, + coupon_name: null, + + course_list_price: 219.0, + amount_learner_paid: 0.0, + course_key: 'HarvardX+happy', + courserun_key: 'course-v1:HarvardX+happy+1T2024', + course_title: 'Managing Happiness', + course_pacing_type: 'self_paced', + course_start_date: '2024-03-27', + course_end_date: '2025-03-26', + course_duration_weeks: 6, + course_max_effort: 3, + course_min_effort: 2, + course_primary_program: 'Happiness in Leadership: Driving Team Success', + primary_program_type: 'Professional Certificate', + course_primary_subject: 'social-sciences', + has_passed: false, + last_activity_date: '2025-02-14', + progress_status: 'In Progress', + passed_date: null, + current_grade: 0.0, + letter_grade: null, + + user_email: 'test_cust_1@Test.com', + user_account_creation_date: '2023-12-04T07:24:07Z', + user_country_code: 'NO', + + user_first_name: '', + user_last_name: '', + enterprise_name: 'Test', + + enterprise_sso_uid: 'fake_sso', + created: '2025-02-26T03:22:05Z', + course_api_url: '/enterprise/v1/enterprise-catalogs/test-uuid/courses/course-v1:HarvardX+happy+1T2024', + total_learning_time_hours: 0.15, + is_subsidy: false, + course_product_line: 'OCM', + + enterprise_group_name: null, + enterprise_group_uuid: null, + }, + { + + is_consent_granted: true, + paid_by: 'Test', + user_current_enrollment_mode: 'verified', + enrollment_date: '2025-02-12', + unenrollment_date: null, + unenrollment_end_within_date: '2025-02-26', + is_refunded: false, + seat_delivery_method: 'Learner Credit', + + offer_name: 'Test - Year 2 LC All edX / Exec Ed - 2024/2025', + offer_type: null, + coupon_code: null, + coupon_name: null, + + course_list_price: 99.0, + amount_learner_paid: 0.0, + course_key: 'IBM+DA0100', + courserun_key: 'course-v1:IBM+DA0100+1T2021', + course_title: 'Data Analytics Basics for Everyone', + course_pacing_type: 'self_paced', + course_start_date: '2021-02-15', + course_end_date: '2025-06-30', + course_duration_weeks: 5, + course_max_effort: 3, + course_min_effort: 2, + course_primary_program: 'Data Analysis and Visualization Fundamentals', + primary_program_type: 'Professional Certificate', + course_primary_subject: 'data-analysis-statistics', + has_passed: true, + last_activity_date: '2025-02-14', + progress_status: 'Passed', + passed_date: '2025-02-14', + current_grade: 0.83, + letter_grade: 'Pass', + + user_email: 'test_cust_2n@Test.com', + user_account_creation_date: '2025-02-12T07:37:24Z', + user_country_code: 'GB', + + user_first_name: '', + user_last_name: '', + enterprise_name: 'Test', + + enterprise_sso_uid: 'fake_sso', + created: '2025-02-26T03:22:05Z', + course_api_url: '/enterprise/v1/enterprise-catalogs/test-uuid/courses/course-v1:IBM+DA0100+1T2021', + total_learning_time_hours: 1.81, + is_subsidy: false, + course_product_line: 'OCM', + + enterprise_group_name: null, + enterprise_group_uuid: null, + }, + { + + is_consent_granted: true, + paid_by: 'Audit', + user_current_enrollment_mode: 'audit', + enrollment_date: '2025-02-14', + unenrollment_date: null, + unenrollment_end_within_date: '2025-02-28', + is_refunded: false, + seat_delivery_method: 'Other', + + offer_name: null, + offer_type: null, + coupon_code: null, + coupon_name: null, + + course_list_price: null, + amount_learner_paid: 0.0, + course_key: 'edX+DemoX.1', + courserun_key: 'course-v1:edX+DemoX.1+2T2019', + course_title: 'DemoX', + course_pacing_type: 'self_paced', + course_start_date: '2019-08-30', + course_end_date: '2025-03-31', + course_duration_weeks: 1, + course_max_effort: 2, + course_min_effort: 1, + course_primary_program: 'Non-Program', + primary_program_type: null, + course_primary_subject: 'computer-science', + has_passed: false, + last_activity_date: '2025-02-14', + progress_status: 'In Progress', + passed_date: null, + current_grade: 0.0, + letter_grade: null, + + user_email: 'test_cust_3@Test.com', + user_account_creation_date: '2023-12-04T07:24:07Z', + user_country_code: 'NO', + + user_first_name: '', + user_last_name: '', + enterprise_name: 'Test', + + enterprise_sso_uid: 'fake_sso', + created: '2025-02-26T03:22:05Z', + course_api_url: '/enterprise/v1/enterprise-catalogs/test-uuid/courses/course-v1:edX+DemoX.1+2T2019', + total_learning_time_hours: 0.08, + is_subsidy: false, + course_product_line: 'OCM', + + enterprise_group_name: null, + enterprise_group_uuid: null, + }, + { + + is_consent_granted: true, + paid_by: 'Test', + user_current_enrollment_mode: 'verified', + enrollment_date: '2025-02-14', + unenrollment_date: null, + unenrollment_end_within_date: '2025-02-28', + is_refunded: false, + seat_delivery_method: 'Learner Credit', + + offer_name: 'Test - Year 2 LC All edX / Exec Ed - 2024/2025', + offer_type: null, + coupon_code: null, + coupon_name: null, + + course_list_price: 25.0, + amount_learner_paid: 0.0, + course_key: 'IMFx+VITARA-ERM', + courserun_key: 'course-v1:IMFx+VITARA-ERM+2T2024', + course_title: 'VITARA-Enterprise Risk Management', + course_pacing_type: 'self_paced', + course_start_date: '2024-06-01', + course_end_date: '2025-04-15', + course_duration_weeks: 1, + course_max_effort: 6, + course_min_effort: 3, + course_primary_program: 'Non-Program', + primary_program_type: null, + course_primary_subject: 'economics-finance', + has_passed: false, + last_activity_date: '2025-02-14', + progress_status: 'In Progress', + passed_date: null, + current_grade: 0.0, + letter_grade: null, + + user_email: 'test_cust_4@Test.com', + user_account_creation_date: '2023-12-04T07:24:07Z', + user_country_code: 'NO', + + user_first_name: '', + user_last_name: '', + enterprise_name: 'Test', + + enterprise_sso_uid: 'fake_sso', + created: '2025-02-26T03:22:05Z', + course_api_url: '/enterprise/v1/enterprise-catalogs/test-uuid/courses/course-v1:IMFx+VITARA-ERM+2T2024', + total_learning_time_hours: 0.3, + is_subsidy: false, + course_product_line: 'OCM', + + enterprise_group_name: null, + enterprise_group_uuid: null, + }, + { + + is_consent_granted: true, + paid_by: 'Test', + user_current_enrollment_mode: 'verified', + enrollment_date: '2025-02-13', + unenrollment_date: null, + unenrollment_end_within_date: '2025-02-27', + is_refunded: false, + seat_delivery_method: 'Learner Credit', + + offer_name: 'Test - Year 2 LC All edX / Exec Ed - 2024/2025', + offer_type: null, + coupon_code: null, + coupon_name: null, + + course_list_price: 199.0, + amount_learner_paid: 0.0, + course_key: 'TUMx+QPLS1x', + courserun_key: 'course-v1:TUMx+QPLS1x+2T2021', + course_title: 'Six Sigma: Define and Measure', + course_pacing_type: 'self_paced', + course_start_date: '2021-05-01', + course_end_date: '2027-12-31', + course_duration_weeks: 8, + course_max_effort: 4, + course_min_effort: 3, + course_primary_program: 'Lean Six Sigma Yellow Belt: Quantitative Tools for Quality and Productivity', + primary_program_type: 'Professional Certificate', + course_primary_subject: 'business-management', + has_passed: false, + last_activity_date: '2025-02-13', + progress_status: 'In Progress', + passed_date: null, + current_grade: 0.0, + letter_grade: null, + + user_email: 'test_cust_5@Test.com', + user_account_creation_date: '2025-01-22T20:36:42Z', + user_country_code: 'US', + + user_first_name: '', + user_last_name: '', + enterprise_name: 'Test', + + enterprise_sso_uid: 'fake_sso', + created: '2025-02-26T03:22:05Z', + course_api_url: '/enterprise/v1/enterprise-catalogs/test-uuid/courses/course-v1:TUMx+QPLS1x+2T2021', + total_learning_time_hours: 0.22, + is_subsidy: false, + course_product_line: 'OCM', + + enterprise_group_name: null, + enterprise_group_uuid: null, + }, + ], + }; +} diff --git a/src/index.jsx b/src/index.jsx index 7881842184..83339a982a 100644 --- a/src/index.jsx +++ b/src/index.jsx @@ -4,11 +4,7 @@ import 'regenerator-runtime/runtime'; import React from 'react'; import ReactDOM from 'react-dom'; import { - initialize, - APP_INIT_ERROR, - APP_READY, - subscribe, - mergeConfig, + APP_INIT_ERROR, APP_READY, initialize, mergeConfig, subscribe, } from '@edx/frontend-platform'; import { ErrorPage } from '@edx/frontend-platform/react'; import { hasFeatureFlagEnabled } from '@edx/frontend-enterprise-utils'; @@ -50,3 +46,4 @@ initialize({ requireAuthenticatedUser: false, hydrateAuthenticatedUser: true, }); +export { formatPrice } from './utils'; diff --git a/src/utils.js b/src/utils.js index 690c0cb326..f29439dcc2 100644 --- a/src/utils.js +++ b/src/utils.js @@ -16,9 +16,18 @@ import { snakeCaseObject } from '@edx/frontend-platform/utils'; import { features } from './config'; import { - BLACKBOARD_TYPE, CANVAS_TYPE, CORNERSTONE_TYPE, DEGREED2_TYPE, - HELP_CENTER_BLACKBOARD, HELP_CENTER_CANVAS, HELP_CENTER_CORNERSTONE, - HELP_CENTER_DEGREED, HELP_CENTER_MOODLE, HELP_CENTER_SAP, MOODLE_TYPE, SAP_TYPE, + BLACKBOARD_TYPE, + CANVAS_TYPE, + CORNERSTONE_TYPE, + DEGREED2_TYPE, + HELP_CENTER_BLACKBOARD, + HELP_CENTER_CANVAS, + HELP_CENTER_CORNERSTONE, + HELP_CENTER_DEGREED, + HELP_CENTER_MOODLE, + HELP_CENTER_SAP, + MOODLE_TYPE, + SAP_TYPE, } from './components/settings/data/constants'; import BlackboardIcon from './icons/Blackboard.svg'; import CanvasIcon from './icons/Canvas.svg'; @@ -675,6 +684,16 @@ function removeStringsFromList(list, stringsToRemove) { return list.filter((item) => !removalSet.has(item)); } +const formatPrice = (price, options = {}) => { + const USDollar = new Intl.NumberFormat('en-US', { + style: 'currency', + currency: 'USD', + minimumFractionDigits: 0, + ...options, + }); + return USDollar.format(Math.abs(price)); +}; + export { camelCaseDict, camelCaseDictArray, @@ -726,4 +745,5 @@ export { downloadCsv, splitAndTrim, removeStringsFromList, + formatPrice, };