From a41685a2d6e476d659d93318bf815c6437dcc06d Mon Sep 17 00:00:00 2001 From: Justin Gasper Date: Thu, 30 Apr 2026 09:46:11 +1000 Subject: [PATCH] Fix merge issue --- ...rojectBillingAccountExpiredNotice.spec.tsx | 24 +- .../ProjectBillingAccountExpiredNotice.tsx | 120 ++------- .../ProjectsTable/ProjectsTable.tsx | 233 +++++++++++++----- 3 files changed, 215 insertions(+), 162 deletions(-) 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 57bb79a3f..9d0d65f46 100644 --- a/src/apps/work/src/lib/components/ProjectBillingAccountExpiredNotice/ProjectBillingAccountExpiredNotice.spec.tsx +++ b/src/apps/work/src/lib/components/ProjectBillingAccountExpiredNotice/ProjectBillingAccountExpiredNotice.spec.tsx @@ -124,16 +124,20 @@ describe('ProjectBillingAccountExpiredNotice', () => { }) it('hides billing account budget and line-item details while billing details are disabled', () => { - render( - - - , - ) + renderNotice() + + expect(screen.getByText(/Billing account:/)) + .toBeTruthy() + expect(screen.queryByText('$1,025 / $1,000 spent')) + .toBeNull() + expect(screen.queryByRole('button', { + name: 'View billing account details', + })) + .toBeNull() + expect(screen.queryByRole('dialog')) + .toBeNull() + }) + it('keeps billing account details and line items available when remaining funds are insufficient', () => { renderNotice() diff --git a/src/apps/work/src/lib/components/ProjectBillingAccountExpiredNotice/ProjectBillingAccountExpiredNotice.tsx b/src/apps/work/src/lib/components/ProjectBillingAccountExpiredNotice/ProjectBillingAccountExpiredNotice.tsx index 3a5df4ca6..eaa72dd62 100644 --- a/src/apps/work/src/lib/components/ProjectBillingAccountExpiredNotice/ProjectBillingAccountExpiredNotice.tsx +++ b/src/apps/work/src/lib/components/ProjectBillingAccountExpiredNotice/ProjectBillingAccountExpiredNotice.tsx @@ -13,6 +13,7 @@ import { BILLING_ACCOUNT_BUDGET_DISPLAY_ENABLED, BILLING_ACCOUNT_DETAILS_MODAL_ENABLED, } from '../../constants' +import { WorkAppContext, } from '../../contexts/WorkAppContext' import { @@ -51,20 +52,15 @@ 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 + budgetDisplayContent: JSX.Element | undefined + budgetInfo: BillingAccountBudgetInfo | undefined onOpenModal: () => void + showDetailsButton: boolean } interface RenderBillingAccountContentParams { @@ -218,30 +214,6 @@ function getVisibleBillingAccountIssue( : 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. * @@ -251,10 +223,7 @@ function getBillingAccountBudgetInfo( const BillingAccountDetailsContent: FC = ( props: BillingAccountDetailsContentProps, ) => { - const budgetStatusClass = props.budgetInfo - ? styles[`budget${props.budgetInfo.status.charAt(0) - .toUpperCase()}${props.budgetInfo.status.slice(1)}`] - : '' + const budgetStatusClass = getBudgetStatusClass(props.budgetInfo) return (
@@ -270,14 +239,11 @@ const BillingAccountDetailsContent: FC = ( {props.budgetInfo ? ( - {formatCurrency(props.budgetInfo.spent)} - {' / '} - {formatCurrency(props.budgetInfo.totalBudget)} - {' spent'} + {props.budgetDisplayContent} ) : undefined} - {BILLING_ACCOUNT_DETAILS_MODAL_ENABLED + {props.showDetailsButton ? ( - - )} -
- ) - : undefined - const billingAccountModal = isModalOpen && billingAccountDetailsData - ? ( - ) : undefined @@ -509,6 +441,8 @@ export const ProjectBillingAccountExpiredNotice: FC void + projectId: Project['id'] + showMemberPaymentsRemaining: boolean +} + +/** + * Resolves whether the billing-account details hook should fetch modal data. + * + * @param isModalOpen Whether the row details modal has been opened. + * @param showPaymentDetails Whether the current user may see payment details. + * @returns `true` when the modal feature is enabled and data should be fetched. + */ +function canFetchProjectBillingAccountDetails( + isModalOpen: boolean, + showPaymentDetails: boolean, +): boolean { + return BILLING_ACCOUNT_DETAILS_MODAL_ENABLED && isModalOpen && showPaymentDetails +} + +/** + * Selects the visible budget state for one project billing-account row. + * + * @param billingAccount Billing-account summary attached to the project row. + * @param showPaymentDetails Whether the current user may see payment details. + * @param showMemberPaymentsRemaining Whether the current user needs the copilot-safe member payment view. + * @returns Budget data to render, with copilot budget data included when needed. + */ +function getProjectBillingBudgetDisplayState( + billingAccount: BillingAccount | undefined, + showPaymentDetails: boolean, + showMemberPaymentsRemaining: boolean, +): ProjectBillingBudgetDisplayState { + if (showMemberPaymentsRemaining) { + const copilotBudgetInfo = getCopilotMemberPaymentsBudgetInfo(billingAccount) + + return { + budgetInfo: copilotBudgetInfo, + copilotBudgetInfo, + } + } + + if (!BILLING_ACCOUNT_BUDGET_DISPLAY_ENABLED || !showPaymentDetails) { + return { + budgetInfo: undefined, + copilotBudgetInfo: undefined, + } + } + + return { + budgetInfo: getBillingAccountBudgetInfo(billingAccount), + copilotBudgetInfo: undefined, + } +} + +/** + * Renders the optional billing-account budget badge for one project row. + * + * @param budgetState Budget data selected for the current user. + * @param showMemberPaymentsRemaining Whether the badge should render copilot-safe copy. + * @returns A budget badge element, or `undefined` when no budget should be shown. + */ +function renderProjectBillingAccountBudget( + budgetState: ProjectBillingBudgetDisplayState, + showMemberPaymentsRemaining: boolean, +): JSX.Element | undefined { + if (!budgetState.budgetInfo) { + return undefined + } + + const budgetStatusClass = getBudgetStatusClass( + budgetState.budgetInfo, + showMemberPaymentsRemaining, + ) + const budgetDisplayClass = budgetStatusClass + ? `${styles.budgetDisplay} ${budgetStatusClass}` + : styles.budgetDisplay + + return ( + + {renderBudgetDisplayContent( + budgetState.budgetInfo, + budgetState.copilotBudgetInfo, + showMemberPaymentsRemaining, + )} + + ) +} + +/** + * Renders the billing-account line-item details button when the feature is enabled. + * + * @param billingAccountId Normalized billing-account id for the current row. + * @param showPaymentDetails Whether the current user may open payment details. + * @param onOpen Open handler for the row modal. + * @returns The details button, or `undefined` when unavailable. + */ +function renderProjectBillingAccountDetailsButton( + billingAccountId: string | undefined, + showPaymentDetails: boolean, + onOpen: () => void, +): JSX.Element | undefined { + if (!BILLING_ACCOUNT_DETAILS_MODAL_ENABLED || !billingAccountId || !showPaymentDetails) { + return undefined + } + + return ( + + ) +} + +/** + * Renders the row-level billing-account line items modal after data is loaded. + * + * @param params Modal visibility, project context, and loaded billing-account details. + * @returns The modal element, or `undefined` while hidden or unloaded. + */ +function renderProjectBillingAccountModal( + params: RenderProjectBillingAccountModalParams, +): JSX.Element | undefined { + if ( + !BILLING_ACCOUNT_DETAILS_MODAL_ENABLED + || !params.isModalOpen + || !params.billingAccountDetails + ) { + return undefined + } + + return ( + + ) +} + /** * Renders a project billing-account summary and lazily loads the line-item * modal only after the details button is opened. @@ -264,37 +417,17 @@ 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 + canFetchProjectBillingAccountDetails(isModalOpen, props.showPaymentDetails) ? normalizedBillingAccountId : undefined, ) - const standardBudgetInfo = props.showPaymentDetails - ? getBillingAccountBudgetInfo(props.billingAccount) - : undefined - const copilotBudgetInfo = props.showMemberPaymentsRemaining - ? getCopilotMemberPaymentsBudgetInfo(props.billingAccount) - : undefined - const budgetInfo = props.showMemberPaymentsRemaining - ? copilotBudgetInfo - : standardBudgetInfo - const budgetStatusClass = getBudgetStatusClass( - budgetInfo, + const budgetDisplayState = getProjectBillingBudgetDisplayState( + props.billingAccount, + props.showPaymentDetails, props.showMemberPaymentsRemaining, ) - const budgetDisplayClass = budgetStatusClass - ? `${styles.budgetDisplay} ${budgetStatusClass}` - : styles.budgetDisplay - const budgetDisplayContent = renderBudgetDisplayContent( - budgetInfo, - copilotBudgetInfo, + const billingAccountBudget = renderProjectBillingAccountBudget( + budgetDisplayState, props.showMemberPaymentsRemaining, ) @@ -305,45 +438,27 @@ const ProjectBillingAccountCell: FC = ( const handleCloseModal = useCallback((): void => { setIsModalOpen(false) }, []) - const shouldShowBillingAccountModal = BILLING_ACCOUNT_DETAILS_MODAL_ENABLED - && isModalOpen - && !!billingAccountDetails + const billingAccountDetailsButton = renderProjectBillingAccountDetailsButton( + normalizedBillingAccountId, + props.showPaymentDetails, + handleOpenModal, + ) + const billingAccountModal = renderProjectBillingAccountModal({ + billingAccountDetails: billingAccountDetailsResult.billingAccountDetails, + isModalOpen, + onClose: handleCloseModal, + projectId: props.project.id, + showMemberPaymentsRemaining: props.showMemberPaymentsRemaining, + }) return (
{getBillingAccountDisplay(props.project, props.billingAccount)} - {budgetInfo - ? ( - - {budgetDisplayContent} - - ) - : undefined} - {BILLING_ACCOUNT_DETAILS_MODAL_ENABLED && normalizedBillingAccountId && props.showPaymentDetails - ? ( - - ) - : undefined} - {shouldShowBillingAccountModal && billingAccountDetails - ? ( - - ) - : undefined} + {billingAccountBudget} + {billingAccountDetailsButton} + {billingAccountModal}
) }