diff --git a/.circleci/config.yml b/.circleci/config.yml index 717a8915d..36aa6b62a 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -226,6 +226,7 @@ workflows: branches: only: - dev + - hide_ba_details - deployQa: context: org-global diff --git a/src/apps/admin/src/AdminHomeRedirect.tsx b/src/apps/admin/src/AdminHomeRedirect.tsx index a96869288..9bf239061 100644 --- a/src/apps/admin/src/AdminHomeRedirect.tsx +++ b/src/apps/admin/src/AdminHomeRedirect.tsx @@ -5,7 +5,7 @@ import { reportsRootRoute } from '~/apps/reports' import { ProfileContextData, useProfileContext } from '~/libs/core' import { manageChallengeRouteId } from './config/routes.config' -import { isAdministrator } from './lib/utils' +import { isAdministrator } from './lib/utils/access' /** * Redirects authenticated admin-app users to the first route they can access. diff --git a/src/apps/admin/src/lib/components/common/Tab/config/system-admin-tabs-config.ts b/src/apps/admin/src/lib/components/common/Tab/config/system-admin-tabs-config.ts index 1154c6cbf..1aa72a376 100644 --- a/src/apps/admin/src/lib/components/common/Tab/config/system-admin-tabs-config.ts +++ b/src/apps/admin/src/lib/components/common/Tab/config/system-admin-tabs-config.ts @@ -1,7 +1,7 @@ import _ from 'lodash' import { TabsNavItem } from '~/libs/ui' -import { isAdministrator } from '~/apps/admin/src/lib/utils' +import { isAdministrator } from '~/apps/admin/src/lib/utils/access' import { billingAccountRouteId, defaultReviewersRouteId, diff --git a/src/apps/admin/src/lib/utils/index.ts b/src/apps/admin/src/lib/utils/index.ts index 5e827a00b..869914c86 100644 --- a/src/apps/admin/src/lib/utils/index.ts +++ b/src/apps/admin/src/lib/utils/index.ts @@ -6,3 +6,9 @@ export * from './number' export * from './string' export * from './others' export * from './access' +export { + administratorOnlyRoles, + adminReportsAccessRoles, + canAccessAdminReports, + isAdministrator, +} from './access' diff --git a/src/apps/copilots/src/services/copilot-opportunities.ts b/src/apps/copilots/src/services/copilot-opportunities.ts index 3446a9ef6..4437b2aaf 100644 --- a/src/apps/copilots/src/services/copilot-opportunities.ts +++ b/src/apps/copilots/src/services/copilot-opportunities.ts @@ -12,6 +12,11 @@ export const copilotBaseUrl = `${EnvironmentConfig.API.V6}/projects` const PAGE_SIZE = 20 +type CopilotApplicationApiResponse = Omit & { + id: number | string + userId: number | string +} + /** * Creates a CopilotOpportunity object by merging the provided data and its nested data, * setting specific properties, and formatting the createdAt date. @@ -27,6 +32,23 @@ function copilotOpportunityFactory(data: any): CopilotOpportunity { } } +/** + * Normalizes copilot application identifiers that the API may serialize as strings. + * + * The copilot opportunity UI joins applications to member records and the signed-in profile + * using numeric ids, so we coerce the API payload into the shape expected by the UI. + * + * @param data - Raw copilot application returned by the API. + * @returns A copilot application with numeric identifiers. + */ +function copilotApplicationFactory(data: CopilotApplicationApiResponse): CopilotApplication { + return { + ...data, + id: Number(data.id), + userId: Number(data.userId), + } +} + export interface CopilotOpportunitiesResponse { isValidating: boolean; data: CopilotOpportunity[]; @@ -144,8 +166,8 @@ export const useCopilotApplications = (opportunityId?: string): CopilotApplicati ? buildUrl(`${copilotBaseUrl}/copilots/opportunity/${opportunityId}/applications`) : undefined - const fetcher = (urlp: string): Promise => xhrGetAsync(urlp) - .then(data => data) + const fetcher = (urlp: string): Promise => xhrGetAsync(urlp) + .then(data => data.map(copilotApplicationFactory)) .catch(() => []) return useSWR(url, fetcher) diff --git a/src/apps/engagements/src/components/assignment-card/AssignmentCard.tsx b/src/apps/engagements/src/components/assignment-card/AssignmentCard.tsx index 517eeb818..196c59286 100644 --- a/src/apps/engagements/src/components/assignment-card/AssignmentCard.tsx +++ b/src/apps/engagements/src/components/assignment-card/AssignmentCard.tsx @@ -15,11 +15,11 @@ import { import type { Engagement, EngagementAssignment } from '../../lib/models' import { formatCurrencyAmount, - formatDate, - formatLocation, formatStandardHoursPerWeek, - truncateText, -} from '../../lib/utils' +} from '../../lib/utils/currency.utils' +import { formatDate } from '../../lib/utils/date.utils' +import { formatLocation } from '../../lib/utils/api.utils' +import { truncateText } from '../../lib/utils/application.utils' import { StatusBadge } from '../status-badge' import styles from './AssignmentCard.module.scss' diff --git a/src/apps/engagements/src/components/assignment-offer-modal/AssignmentOfferModal.tsx b/src/apps/engagements/src/components/assignment-offer-modal/AssignmentOfferModal.tsx index 8f5fbdc6a..b1c943639 100644 --- a/src/apps/engagements/src/components/assignment-offer-modal/AssignmentOfferModal.tsx +++ b/src/apps/engagements/src/components/assignment-offer-modal/AssignmentOfferModal.tsx @@ -5,9 +5,9 @@ import { BaseModal, Button } from '~/libs/ui' import type { Engagement, EngagementAssignment } from '../../lib/models' import { formatCurrencyAmount, - formatDate, formatStandardHoursPerWeek, -} from '../../lib/utils' +} from '../../lib/utils/currency.utils' +import { formatDate } from '../../lib/utils/date.utils' import styles from './AssignmentOfferModal.module.scss' diff --git a/src/apps/engagements/src/lib/utils/index.ts b/src/apps/engagements/src/lib/utils/index.ts index 8f5f0a9b8..2fc1ffd2b 100644 --- a/src/apps/engagements/src/lib/utils/index.ts +++ b/src/apps/engagements/src/lib/utils/index.ts @@ -3,3 +3,11 @@ export * from './application.utils' export * from './currency.utils' export * from './date.utils' export * from './terms.utils' +export { formatLocation } from './api.utils' +export { truncateText } from './application.utils' +export { + formatCurrencyAmount, + formatStandardHoursPerWeek, + normalizePositiveNumericValue, +} from './currency.utils' +export { formatDate } from './date.utils' diff --git a/src/apps/work/src/lib/components/PaymentFormModal/PaymentFormModal.spec.tsx b/src/apps/work/src/lib/components/PaymentFormModal/PaymentFormModal.spec.tsx index c58fb5c39..47ac68dc7 100644 --- a/src/apps/work/src/lib/components/PaymentFormModal/PaymentFormModal.spec.tsx +++ b/src/apps/work/src/lib/components/PaymentFormModal/PaymentFormModal.spec.tsx @@ -51,6 +51,10 @@ jest.mock('../../utils', () => ({ getAssignmentStandardHoursPerWeek: jest.fn(() => 40), })) +jest.mock('../../constants', () => ({ + BILLING_ACCOUNT_MEMBER_PAYMENT_DETAILS_ENABLED: false, +})) + describe('PaymentFormModal', () => { const member: Assignment = { agreementRate: '821.20', diff --git a/src/apps/work/src/lib/components/PaymentFormModal/PaymentFormModal.tsx b/src/apps/work/src/lib/components/PaymentFormModal/PaymentFormModal.tsx index b50141c4d..2cec47e65 100644 --- a/src/apps/work/src/lib/components/PaymentFormModal/PaymentFormModal.tsx +++ b/src/apps/work/src/lib/components/PaymentFormModal/PaymentFormModal.tsx @@ -17,6 +17,9 @@ import { Button, } from '~/libs/ui' +import { + BILLING_ACCOUNT_MEMBER_PAYMENT_DETAILS_ENABLED, +} from '../../constants' import { Assignment, } from '../../models' @@ -156,7 +159,9 @@ const PaymentFormModal: FC = ( [hoursWorked, ratePerHour], ) const challengeFee = useMemo( - () => calculatePaymentChallengeFee(amount, props.billingAccountMarkup), + () => (BILLING_ACCOUNT_MEMBER_PAYMENT_DETAILS_ENABLED + ? calculatePaymentChallengeFee(amount, props.billingAccountMarkup) + : undefined), [amount, props.billingAccountMarkup], ) const paymentTitle = useMemo( @@ -289,10 +294,14 @@ const PaymentFormModal: FC = ( Rate Per Week {formatCurrency(props.member?.agreementRate)} -
- Billing Account - {props.billingAccountId || 'Unavailable'} -
+ {BILLING_ACCOUNT_MEMBER_PAYMENT_DETAILS_ENABLED + ? ( +
+ Billing Account + {props.billingAccountId || 'Unavailable'} +
+ ) + : undefined}
diff --git a/src/apps/work/src/lib/components/PaymentHistoryModal/PaymentHistoryModal.spec.tsx b/src/apps/work/src/lib/components/PaymentHistoryModal/PaymentHistoryModal.spec.tsx index 622822f14..e4688e855 100644 --- a/src/apps/work/src/lib/components/PaymentHistoryModal/PaymentHistoryModal.spec.tsx +++ b/src/apps/work/src/lib/components/PaymentHistoryModal/PaymentHistoryModal.spec.tsx @@ -12,6 +12,10 @@ jest.mock('../../hooks', () => ({ useFetchAssignmentPayments: (...args: unknown[]): unknown => mockUseFetchAssignmentPayments(...args), })) +jest.mock('../../constants', () => ({ + BILLING_ACCOUNT_MEMBER_PAYMENT_DETAILS_ENABLED: false, +})) + jest.mock('~/libs/ui', () => ({ BaseModal: (props: { buttons?: JSX.Element @@ -92,9 +96,9 @@ describe('PaymentHistoryModal', () => { .toBeTruthy() expect(screen.getByText('payment.manager')) .toBeTruthy() - expect(screen.getByText('Fee:')) - .toBeTruthy() - expect(screen.getByText('$18.60')) - .toBeTruthy() + expect(screen.queryByText('Fee:')) + .toBeNull() + expect(screen.queryByText('$18.60')) + .toBeNull() }) }) diff --git a/src/apps/work/src/lib/components/PaymentHistoryModal/PaymentHistoryModal.tsx b/src/apps/work/src/lib/components/PaymentHistoryModal/PaymentHistoryModal.tsx index 406db4486..bb7b33318 100644 --- a/src/apps/work/src/lib/components/PaymentHistoryModal/PaymentHistoryModal.tsx +++ b/src/apps/work/src/lib/components/PaymentHistoryModal/PaymentHistoryModal.tsx @@ -5,6 +5,9 @@ import { Button, } from '~/libs/ui' +import { + BILLING_ACCOUNT_MEMBER_PAYMENT_DETAILS_ENABLED, +} from '../../constants' import { useFetchAssignmentPayments, } from '../../hooks' @@ -88,7 +91,9 @@ const PaymentHistoryModal: FC = (
    {paymentsResult.payments.map((payment, index) => { const paymentAmount = getPaymentAmount(payment) - const paymentChallengeFee = getPaymentChallengeFee(payment) + const paymentChallengeFee = BILLING_ACCOUNT_MEMBER_PAYMENT_DETAILS_ENABLED + ? getPaymentChallengeFee(payment) + : undefined const paymentStatus = getPaymentStatus(payment) const paymentHoursWorked = getPaymentHoursWorked(payment) const paymentRemarks = getPaymentRemarks(payment) diff --git a/src/apps/work/src/lib/components/ProjectBillingAccountExpiredNotice/ProjectBillingAccountExpiredNotice.spec.tsx b/src/apps/work/src/lib/components/ProjectBillingAccountExpiredNotice/ProjectBillingAccountExpiredNotice.spec.tsx index da541ea10..57bb79a3f 100644 --- a/src/apps/work/src/lib/components/ProjectBillingAccountExpiredNotice/ProjectBillingAccountExpiredNotice.spec.tsx +++ b/src/apps/work/src/lib/components/ProjectBillingAccountExpiredNotice/ProjectBillingAccountExpiredNotice.spec.tsx @@ -1,6 +1,5 @@ /* eslint-disable import/no-extraneous-dependencies, ordered-imports/ordered-imports */ import { - fireEvent, render, screen, } from '@testing-library/react' @@ -35,6 +34,11 @@ jest.mock('../BillingAccountLineItemsModal', () => ({ ), })) +jest.mock('../../constants', () => ({ + BILLING_ACCOUNT_BUDGET_DISPLAY_ENABLED: false, + BILLING_ACCOUNT_DETAILS_MODAL_ENABLED: false, +})) + jest.mock('~/libs/ui', () => ({ IconOutline: { InformationCircleIcon: (): JSX.Element => info, @@ -119,6 +123,17 @@ describe('ProjectBillingAccountExpiredNotice', () => { }) }) + it('hides billing account budget and line-item details while billing details are disabled', () => { + render( + + + , + ) it('keeps billing account details and line items available when remaining funds are insufficient', () => { renderNotice() @@ -128,21 +143,18 @@ describe('ProjectBillingAccountExpiredNotice', () => { .toBeTruthy() expect(screen.getByText(/80001063/)) .toBeTruthy() - expect(screen.getByText('$1,025 / $1,000 spent')) - .toBeTruthy() - expect(screen.getByText(/The billing account for this project has insufficient remaining funds,/)) - .toBeTruthy() - expect(screen.getByRole('link', { name: 'click here to update' }) - .getAttribute('href')) - .toBe('/projects/project-1/edit') - - fireEvent.click(screen.getByRole('button', { + expect(screen.queryByText('$1,025 / $1,000 spent')) + .toBeNull() + expect(screen.queryByText(/The billing account for this project has insufficient remaining funds,/)) + .toBeNull() + expect(screen.queryByRole('link', { name: 'click here to update' })) + .toBeNull() + expect(screen.queryByRole('button', { name: 'View billing account details', })) - - expect(screen.getByRole('dialog') - .textContent) - .toContain('Billing account details for 80001063') + .toBeNull() + expect(screen.queryByRole('dialog')) + .toBeNull() }) it('shows member payments remaining instead of spent and total budget for copilots', () => { diff --git a/src/apps/work/src/lib/components/ProjectBillingAccountExpiredNotice/ProjectBillingAccountExpiredNotice.tsx b/src/apps/work/src/lib/components/ProjectBillingAccountExpiredNotice/ProjectBillingAccountExpiredNotice.tsx index 95e67aa53..3a5df4ca6 100644 --- a/src/apps/work/src/lib/components/ProjectBillingAccountExpiredNotice/ProjectBillingAccountExpiredNotice.tsx +++ b/src/apps/work/src/lib/components/ProjectBillingAccountExpiredNotice/ProjectBillingAccountExpiredNotice.tsx @@ -10,6 +10,9 @@ import { Link } from 'react-router-dom' import { IconOutline } from '~/libs/ui' import { + BILLING_ACCOUNT_BUDGET_DISPLAY_ENABLED, + BILLING_ACCOUNT_DETAILS_MODAL_ENABLED, +} from '../../constants' WorkAppContext, } from '../../contexts/WorkAppContext' import { @@ -22,6 +25,9 @@ import type { UseFetchBillingAccountsResult, UseFetchProjectBillingAccountResult, } from '../../hooks' +import type { + BillingAccountDetails, +} from '../../services' import type { WorkAppContextModel } from '../../models' import type { BillingAccountBudgetInfo, @@ -45,6 +51,30 @@ interface ProjectBillingAccountExpiredNoticeProps { projectId: string } +type BudgetStatus = 'healthy' | 'warning' | 'critical' +type BillingAccountIssue = ReturnType + +interface BillingBudgetInfo { + spent: number + status: BudgetStatus + totalBudget: number +} + +interface BillingAccountDetailsContentProps { + billingAccountId: string + billingAccountName: string | undefined + budgetInfo: BillingBudgetInfo | undefined + onOpenModal: () => void +} + +interface RenderBillingAccountContentParams { + billingAccountDetailsContent: JSX.Element | undefined + billingAccountModal: JSX.Element | undefined + canManageProject: boolean + projectId: string + visibleBillingAccountIssue: BillingAccountIssue +} + function normalizeOptionalString(value: unknown): string | undefined { if (value === undefined || value === null) { return undefined @@ -171,6 +201,168 @@ function renderBudgetDisplayContent( ) } +/** + * Hides budget-derived billing account notices while budget display is disabled. + * + * @param billingAccountIssue The billing account issue resolved for the project. + * @returns The issue to display, or `undefined` when the temporary hide applies. + */ +function getVisibleBillingAccountIssue( + billingAccountIssue: BillingAccountIssue, +): BillingAccountIssue { + const isInsufficientFundsNoticeHidden = !BILLING_ACCOUNT_BUDGET_DISPLAY_ENABLED + && billingAccountIssue === 'insufficient-funds' + + return isInsufficientFundsNoticeHidden + ? undefined + : billingAccountIssue +} + +/** + * Builds the optional spent/total budget display model from fetched billing details. + * + * @param billingAccountDetails Billing account details returned by the work app hook. + * @returns Spent, total, and status information, or `undefined` while hidden or unavailable. + */ +function getBillingAccountBudgetInfo( + billingAccountDetails: BillingAccountDetails | undefined, +): BillingBudgetInfo | undefined { + if (!BILLING_ACCOUNT_BUDGET_DISPLAY_ENABLED || !billingAccountDetails) { + return undefined + } + + const totalBudget = Number(billingAccountDetails.budget) || 0 + const remaining = Number(billingAccountDetails.totalBudgetRemaining) || 0 + const status = getBudgetStatus(remaining, totalBudget) + + return { + spent: Math.max(totalBudget - remaining, 0), + status, + totalBudget, + } +} + +/** + * Renders the visible billing account label plus optional budget and details controls. + * + * @param props Billing account label, optional budget data, and modal open handler. + * @returns The billing account details row for project pages. + */ +const BillingAccountDetailsContent: FC = ( + props: BillingAccountDetailsContentProps, +) => { + const budgetStatusClass = props.budgetInfo + ? styles[`budget${props.budgetInfo.status.charAt(0) + .toUpperCase()}${props.budgetInfo.status.slice(1)}`] + : '' + + return ( +
    + + Billing account: + {' '} + {props.billingAccountName || 'Unknown'} + {' '} + / + {' '} + {props.billingAccountId} + + {props.budgetInfo + ? ( + + {formatCurrency(props.budgetInfo.spent)} + {' / '} + {formatCurrency(props.budgetInfo.totalBudget)} + {' spent'} + + ) + : undefined} + {BILLING_ACCOUNT_DETAILS_MODAL_ENABLED + ? ( + + ) + : undefined} +
    + ) +} + +/** + * Renders the temporarily enabled/disabled line-item modal. + * + * @param billingAccountDetails Billing account detail payload, if loaded. + * @param isModalOpen Whether the details modal has been requested. + * @param onClose Close handler passed to the modal. + * @returns The line-item modal, or `undefined` when the feature is hidden. + */ +function renderBillingAccountModal( + billingAccountDetails: BillingAccountDetails | undefined, + isModalOpen: boolean, + onClose: () => void, +): JSX.Element | undefined { + if (!BILLING_ACCOUNT_DETAILS_MODAL_ENABLED || !isModalOpen || !billingAccountDetails) { + return undefined + } + + return ( + + ) +} + +/** + * Renders the project billing-account issue notice when one should remain visible. + * + * @param params Project billing display state and rendered child content. + * @returns The notice stack, normal details content, or an empty fragment. + */ +function renderBillingAccountContent(params: RenderBillingAccountContentParams): JSX.Element { + if (params.visibleBillingAccountIssue) { + const noticeMessage = getProjectBillingAccountNoticeMessage(params.visibleBillingAccountIssue) + const managedNoticeMessage = `${noticeMessage.slice(0, -1)}, ` + + return ( +
    + {params.billingAccountDetailsContent} +
    + {params.canManageProject + ? ( + <> + {managedNoticeMessage} + + click here to update + + + ) + : ( + {noticeMessage} + )} +
    + {params.billingAccountModal} +
    + ) + } + + if (!params.billingAccountDetailsContent) { + return <> + } + + return ( + <> + {params.billingAccountDetailsContent} + {params.billingAccountModal} + + ) +} + export const ProjectBillingAccountExpiredNotice: FC = ( props: ProjectBillingAccountExpiredNoticeProps, ) => { @@ -193,6 +385,7 @@ export const ProjectBillingAccountExpiredNotice: FC getBillingAccountBudgetInfo(billingAccountDetailsData), + [billingAccountDetailsData], + ) const billingAccountIssue = getProjectBillingAccountChallengeIssue(billingAccount) const standardBudgetInfo = useMemo(() => { @@ -251,6 +452,13 @@ export const ProjectBillingAccountExpiredNotice: FC ) : undefined - - if (billingAccountIssue) { - const noticeMessage = getProjectBillingAccountNoticeMessage(billingAccountIssue) - const managedNoticeMessage = `${noticeMessage.slice(0, -1)}, ` - - return ( -
    - {billingAccountDetailsContent} -
    - {props.canManageProject - ? ( - <> - {managedNoticeMessage} - - click here to update - - - ) - : ( - {noticeMessage} - )} -
    - {billingAccountModal} -
    - ) - } - - if (!normalizedBillingAccountId) { - return <> - } - - return ( - <> - {billingAccountDetailsContent} - {billingAccountModal} - + const billingAccountModal = renderBillingAccountModal( + billingAccountDetailsData, + isModalOpen, + handleCloseModal, ) + + return renderBillingAccountContent({ + billingAccountDetailsContent, + billingAccountModal, + canManageProject: props.canManageProject, + projectId: props.projectId, + visibleBillingAccountIssue, + }) } export default ProjectBillingAccountExpiredNotice diff --git a/src/apps/work/src/lib/components/ProjectsTable/ProjectsTable.spec.tsx b/src/apps/work/src/lib/components/ProjectsTable/ProjectsTable.spec.tsx index b70fe0854..9036e0435 100644 --- a/src/apps/work/src/lib/components/ProjectsTable/ProjectsTable.spec.tsx +++ b/src/apps/work/src/lib/components/ProjectsTable/ProjectsTable.spec.tsx @@ -1,7 +1,6 @@ /* eslint-disable import/no-extraneous-dependencies, ordered-imports/ordered-imports */ import type { ReactNode } from 'react' import { - fireEvent, render, screen, } from '@testing-library/react' @@ -38,6 +37,8 @@ jest.mock('../BillingAccountLineItemsModal', () => ({ })) jest.mock('../../constants', () => ({ + BILLING_ACCOUNT_BUDGET_DISPLAY_ENABLED: false, + BILLING_ACCOUNT_DETAILS_MODAL_ENABLED: false, PROJECT_STATUS: { DRAFT: 'draft', }, @@ -168,7 +169,7 @@ describe('ProjectsTable', () => { .toBe('/projects/100440/challenges') }) - it('shows project billing account spent totals and opens the line-item modal', () => { + it('hides billing account spent totals and line-item details while billing details are disabled', () => { mockedUseFetchBillingAccounts.mockReturnValue({ billingAccounts: [ { @@ -198,16 +199,14 @@ describe('ProjectsTable', () => { expect(screen.getAllByText('Access BA / 80001063').length) .toBeGreaterThan(0) - expect(screen.getAllByText('$350 / $1,000 spent').length) - .toBeGreaterThan(0) - - fireEvent.click(screen.getAllByRole('button', { + expect(screen.queryByText('$350 / $1,000 spent')) + .toBeNull() + expect(screen.queryByRole('button', { name: 'View billing account details', - })[0]) - - expect(screen.getByRole('dialog') - .textContent) - .toContain('Billing account details for 80001063') + })) + .toBeNull() + expect(screen.queryByRole('dialog')) + .toBeNull() }) it('shows member payments remaining for copilot project rows', () => { diff --git a/src/apps/work/src/lib/components/ProjectsTable/ProjectsTable.tsx b/src/apps/work/src/lib/components/ProjectsTable/ProjectsTable.tsx index 9d8359088..b94c1a149 100644 --- a/src/apps/work/src/lib/components/ProjectsTable/ProjectsTable.tsx +++ b/src/apps/work/src/lib/components/ProjectsTable/ProjectsTable.tsx @@ -15,7 +15,11 @@ import { TableColumn, } from '~/libs/ui' -import { PROJECT_STATUS } from '../../constants' +import { + BILLING_ACCOUNT_BUDGET_DISPLAY_ENABLED, + BILLING_ACCOUNT_DETAILS_MODAL_ENABLED, + PROJECT_STATUS, +} from '../../constants' import { WorkAppContext } from '../../contexts/WorkAppContext' import { useFetchBillingAccountDetails, @@ -251,8 +255,7 @@ interface ProjectBillingAccountCellProps { * modal only after the details button is opened. * * @param props Project row and matching billing-account summary from the list API. - * @returns Billing-account label, role-specific budget badge, and optional - * line-item modal. + * @returns Billing-account label, with budget and line-item details shown only when enabled. */ const ProjectBillingAccountCell: FC = ( props: ProjectBillingAccountCellProps, @@ -261,6 +264,14 @@ const ProjectBillingAccountCell: FC = ( const normalizedBillingAccountId = normalizeOptionalString(props.project.billingAccountId) || normalizeOptionalString(props.billingAccount?.id) const billingAccountDetailsResult: UseFetchBillingAccountDetailsResult = useFetchBillingAccountDetails( + BILLING_ACCOUNT_DETAILS_MODAL_ENABLED && isModalOpen + ? normalizedBillingAccountId + : undefined, + ) + const billingAccountDetails = billingAccountDetailsResult.billingAccountDetails + const budgetInfo = BILLING_ACCOUNT_BUDGET_DISPLAY_ENABLED + ? getBillingAccountBudgetInfo(props.billingAccount) + : undefined isModalOpen && props.showPaymentDetails ? normalizedBillingAccountId : undefined, @@ -294,6 +305,9 @@ const ProjectBillingAccountCell: FC = ( const handleCloseModal = useCallback((): void => { setIsModalOpen(false) }, []) + const shouldShowBillingAccountModal = BILLING_ACCOUNT_DETAILS_MODAL_ENABLED + && isModalOpen + && !!billingAccountDetails return (
    @@ -307,7 +321,7 @@ const ProjectBillingAccountCell: FC = ( ) : undefined} - {normalizedBillingAccountId && props.showPaymentDetails + {BILLING_ACCOUNT_DETAILS_MODAL_ENABLED && normalizedBillingAccountId && props.showPaymentDetails ? ( ) : undefined} - {isModalOpen && billingAccountDetailsResult.billingAccountDetails + {shouldShowBillingAccountModal && billingAccountDetails ? (