From c4d42f6e84074fc3e153ab19e0e491d97d5be786 Mon Sep 17 00:00:00 2001 From: Justin Gasper Date: Wed, 11 Nov 2026 08:40:47 +1100 Subject: [PATCH] feat: add customer revenue offering filter (PM-6740) --- src/apps/sales/README.md | 14 +++ .../sales/src/SalesDashboardsPage.spec.tsx | 100 ++++++++++++++++++ src/apps/sales/src/SalesDashboardsPage.tsx | 38 +++++-- src/apps/sales/src/sales-dashboard.service.ts | 15 ++- 4 files changed, 156 insertions(+), 11 deletions(-) diff --git a/src/apps/sales/README.md b/src/apps/sales/README.md index b9c450a5a..65e4099a6 100644 --- a/src/apps/sales/README.md +++ b/src/apps/sales/README.md @@ -369,6 +369,20 @@ App Enhancement(s) 5 to Flexible Talent; populated values remain unchanged. A pe another report without column settings. See that repository's `SALES.md` for source definitions, settings and API parameters. +### PM-6740 customer revenue by offering + +The Customer section's **Revenue offering type** selector offers ALL, AI Training, +Crowd Powered Outcomes, Crowd Powered Innovation, Flexible Talent, TopGear and TaaS +Ops. It combines with **Revenue account category** to select the corresponding +`customerRevenueByOfferingType` matrix and optional account-category intersection. +The API aggregates only matching revenue detail rows, including the two existing +blank-offering corrections; TopGear includes TopGear Ops. ALL retains unknown +and other offerings. Filters affect only Revenue Customer-Wise, require no new +requests, and remain selected through refresh and Monthly/Quarterly changes. +Unsupported source columns show an unavailable message; valid empty selections +show no customer data. Deploy the reports-api-v6 change before the UI to enable +these optional datasets. + ### PM-6667 dashboard changes The Reports API removes Total Opportunity Size, returns Won - SOW Signed bookings diff --git a/src/apps/sales/src/SalesDashboardsPage.spec.tsx b/src/apps/sales/src/SalesDashboardsPage.spec.tsx index 773074312..4c535645c 100644 --- a/src/apps/sales/src/SalesDashboardsPage.spec.tsx +++ b/src/apps/sales/src/SalesDashboardsPage.spec.tsx @@ -845,6 +845,106 @@ it('filters customer revenue by account category without changing revenue charts .toHaveBeenCalledTimes(7) }) +it('combines customer revenue filters and preserves selections across refresh', async () => { + const ai: MonthlySalesChart = { + ...customerPayments, + months: [ + { month: '2026-04-01', values: { anz: 30, metro: 10, rebel: 20 } }, + { month: '2026-05-01', values: { anz: 0, metro: -2, rebel: 0 } }, + ], + } + const external: MonthlySalesChart = { + ...ai, + months: [ + { month: '2026-04-01', values: { metro: 5 } }, + { month: '2026-05-01', values: { metro: -2 } }, + ], + series: [ai.series[0]], + } + fetchDashboard.mockImplementation(async section => ({ + charts: [], + customerPayments, + notices: [], + ...(section === 'revenue' ? { + customerRevenue: customerPayments, + customerRevenueByAccountCategory: { 'External Revenue': customerPayments }, + customerRevenueByOfferingType: { + 'AI Training': { + customerRevenue: ai, + customerRevenueByAccountCategory: { 'External Revenue': external }, + }, + 'TaaS Ops': { + customerRevenue: { ...ai, series: [] }, + }, + }, + } : {}), + })) + render() + await screen.findByRole('table', { name: 'Revenue Customer-Wise (USD)' }) + const offering = screen.getByRole('combobox', { name: 'Revenue offering type' }) + const account = screen.getByRole('combobox', { name: 'Revenue account category' }) + const table = (): ReturnType => within(screen.getByRole('table', { + name: 'Revenue Customer-Wise (USD)', + })) + expect(within(offering) + .getAllByRole('option') + .map(option => option.textContent)) + .toEqual(['ALL', 'AI Training', 'Crowd Powered Outcomes', 'Crowd Powered Innovation', + 'Flexible Talent', 'TopGear', 'TaaS Ops']) + expect(offering) + .toHaveValue('') + fireEvent.change(offering, { target: { value: 'AI Training' } }) + expect(table() + .getAllByRole('rowheader')) + .toHaveLength(4) + expect(table() + .getByRole('row', { name: /^Total/ })) + .toHaveTextContent('Total$58.00$60.00-$2.00') + fireEvent.change(account, { target: { value: 'External Revenue' } }) + expect(table() + .getAllByRole('rowheader')) + .toHaveLength(2) + expect(table() + .getByRole('row', { name: /^Metro/ })) + .toHaveTextContent('Metro$3.00$5.00-$2.00') + fireEvent.click(screen.getByRole('button', { name: 'Quarterly' })) + expect(table() + .getByRole('row', { name: /^Total/ })) + .toHaveTextContent('Total$3.00$3.00') + expect(within(screen.getByRole('table', { name: 'Goods Sold Customer-Wise (USD)' })) + .getByRole('row', { name: /^Metro/ })) + .toHaveTextContent('Metro$8,500.00$8,500.00') + expect(fetchDashboard) + .toHaveBeenCalledTimes(7) + fireEvent.click(screen.getByRole('button', { name: 'Refresh data' })) + await screen.findByRole('table', { name: 'Revenue Customer-Wise (USD)' }) + expect(offering) + .toHaveValue('AI Training') + expect(account) + .toHaveValue('External Revenue') + expect(table() + .getByRole('row', { name: /^Total/ })) + .toHaveTextContent('Total$3.00$3.00') + fireEvent.change(offering, { target: { value: '' } }) + expect(table() + .getAllByRole('rowheader')) + .toHaveLength(4) + fireEvent.change(account, { target: { value: '' } }) + fireEvent.change(offering, { target: { value: 'TaaS Ops' } }) + expect(screen.getByText('No customer data in this fiscal year.')) + .toBeInTheDocument() + fireEvent.change(offering, { target: { value: 'TopGear' } }) + expect(screen.getByText('Revenue offering type details are unavailable for this source.')) + .toBeInTheDocument() + expect(screen.queryByRole('table', { name: 'Revenue Customer-Wise (USD)' })).not.toBeInTheDocument() + fireEvent.change(offering, { target: { value: '' } }) + expect(table() + .getAllByRole('rowheader')) + .toHaveLength(4) + expect(fetchDashboard) + .toHaveBeenCalledTimes(14) +}) + it('headlines quarterly data using the current fiscal quarter and the previous quarter comparison', () => { const chart = { ...response.charts[0], months: [ diff --git a/src/apps/sales/src/SalesDashboardsPage.tsx b/src/apps/sales/src/SalesDashboardsPage.tsx index 375fe5aeb..ee5f9210e 100644 --- a/src/apps/sales/src/SalesDashboardsPage.tsx +++ b/src/apps/sales/src/SalesDashboardsPage.tsx @@ -6,9 +6,11 @@ import { Button, LoadingSpinner, PageTitle } from '~/libs/ui' import { SalesAccountCategory, + SalesCustomerRevenue, SalesDashboardGroupBy, SalesDashboardPeriod, SalesDashboardPortal, + SalesRevenueOfferingType, } from './sales-dashboard.service' import { salesFiscalYearLabel, salesFiscalYearPeriod } from './sales-fiscal-year' import { SalesDashboardSource, useSalesDashboard } from './useSalesDashboard' @@ -89,11 +91,14 @@ const MeasureToggle: FC<{ */ const CustomerDashboard: FC<{ source: SalesDashboardSource; field: 'customerRevenue' | 'customerPayments'; title: string - period: SalesDashboardPeriod; accountCategory?: SalesAccountCategory + period: SalesDashboardPeriod; accountCategory?: SalesAccountCategory; offeringType?: SalesRevenueOfferingType }> = props => { - const data = props.accountCategory - ? props.source.data?.customerRevenueByAccountCategory?.[props.accountCategory] - : props.source.data?.[props.field] + const revenueScope: SalesCustomerRevenue | undefined = props.offeringType + ? props.source.data?.customerRevenueByOfferingType?.[props.offeringType] + : props.source.data + const data = props.field === 'customerPayments' ? props.source.data?.customerPayments + : props.accountCategory ? revenueScope?.customerRevenueByAccountCategory?.[props.accountCategory] + : revenueScope?.customerRevenue if (data) { return ( {props.source.data?.notices.map(notice =>

{notice}

)}

- {props.accountCategory - ? 'Revenue account category details are unavailable for this source.' - : 'Customer details are unavailable for this source.'} + {props.offeringType && !revenueScope + ? 'Revenue offering type details are unavailable for this source.' + : props.accountCategory + ? 'Revenue account category details are unavailable for this source.' + : 'Customer details are unavailable for this source.'}

@@ -139,6 +146,7 @@ const SalesDashboardsPage: FC<{ portal?: SalesDashboardPortal }> = props => { const [taasMeasure, setTaasMeasure] = useState('Amount') const [period, setPeriod] = useState('Monthly') const [accountCategory, setAccountCategory] = useState('') + const [offeringType, setOfferingType] = useState('') const bookings = useSalesDashboard('bookings', portal, dateBasis, refreshKey, wonGroup) const pipeline = useSalesDashboard('pipeline', portal, dateBasis, refreshKey, pipelineGroup) const revenue = useSalesDashboard('revenue', portal, dateBasis, refreshKey, revenueGroup) @@ -264,6 +272,21 @@ const SalesDashboardsPage: FC<{ portal?: SalesDashboardPortal }> = props => {

Customer

+