Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions src/apps/sales/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
100 changes: 100 additions & 0 deletions src/apps/sales/src/SalesDashboardsPage.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(<SalesDashboardsPage portal='sales' />)
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<typeof within> => 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: [
Expand Down
38 changes: 31 additions & 7 deletions src/apps/sales/src/SalesDashboardsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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 (
<SalesCustomerTable
Expand All @@ -111,9 +116,11 @@ const CustomerDashboard: FC<{
<SourceContent source={props.source} title={props.title}>
{props.source.data?.notices.map(notice => <p key={notice} role='status'>{notice}</p>)}
<p>
{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.'}
</p>
</SourceContent>
</article>
Expand All @@ -139,6 +146,7 @@ const SalesDashboardsPage: FC<{ portal?: SalesDashboardPortal }> = props => {
const [taasMeasure, setTaasMeasure] = useState('Amount')
const [period, setPeriod] = useState<SalesDashboardPeriod>('Monthly')
const [accountCategory, setAccountCategory] = useState<SalesAccountCategory | ''>('')
const [offeringType, setOfferingType] = useState<SalesRevenueOfferingType | ''>('')
const bookings = useSalesDashboard('bookings', portal, dateBasis, refreshKey, wonGroup)
const pipeline = useSalesDashboard('pipeline', portal, dateBasis, refreshKey, pipelineGroup)
const revenue = useSalesDashboard('revenue', portal, dateBasis, refreshKey, revenueGroup)
Expand Down Expand Up @@ -264,6 +272,21 @@ const SalesDashboardsPage: FC<{ portal?: SalesDashboardPortal }> = props => {
<section aria-labelledby='monthly-customer' className={styles.section}>
<h2 id='monthly-customer'>Customer</h2>
<div className={styles.filters}>
<label>
Revenue offering type
<select
value={offeringType}
onChange={event => setOfferingType(event.target.value as SalesRevenueOfferingType | '')}
>
<option value=''>ALL</option>
{[
'AI Training', 'Crowd Powered Outcomes', 'Crowd Powered Innovation',
'Flexible Talent', 'TopGear', 'TaaS Ops',
].map(offering => (
<option key={offering} value={offering}>{offering}</option>
))}
</select>
</label>
<label>
Revenue account category
<select
Expand All @@ -283,6 +306,7 @@ const SalesDashboardsPage: FC<{ portal?: SalesDashboardPortal }> = props => {
title='Revenue Customer-Wise'
period={period}
accountCategory={accountCategory || undefined}
offeringType={offeringType || undefined}
/>
<CustomerDashboard
source={payments}
Expand Down
15 changes: 11 additions & 4 deletions src/apps/sales/src/sales-dashboard.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ export type SalesDashboardSection =
export type SalesDashboardPortal = 'reports' | 'sales'
export type SalesDashboardPeriod = 'Monthly' | 'Quarterly'
export type SalesAccountCategory = 'External Revenue' | 'Through Wipro' | 'Wipro Internal'
export type SalesRevenueOfferingType =
'AI Training' | 'Crowd Powered Outcomes' | 'Crowd Powered Innovation' | 'Flexible Talent' | 'TopGear' | 'TaaS Ops'

/** Dimension whose categories become chart series and monthly table columns. */
export type SalesDashboardGroupBy = 'offeringType' | 'dealCategory' | 'customerType' | 'stage' | 'fyClosure'
Expand All @@ -26,14 +28,19 @@ export interface MonthlySalesChart {
annual?: Record<string, number | null>
}

export interface SalesDashboardResponse {
export interface SalesCustomerRevenue {
/** Every customer from the revenue report, without grouping smaller customers into Other. */
customerRevenue?: MonthlySalesChart
customerRevenueByAccountCategory?: Partial<Record<SalesAccountCategory, MonthlySalesChart>>
}

export interface SalesDashboardResponse extends SalesCustomerRevenue {
charts: MonthlySalesChart[]
notices: string[]
/** Every customer before top-five/Other grouping, used for the monthly payments matrix. */
customerPayments?: MonthlySalesChart
/** Every customer from the revenue report, without grouping smaller customers into Other. */
customerRevenue?: MonthlySalesChart
customerRevenueByAccountCategory?: Partial<Record<SalesAccountCategory, MonthlySalesChart>>
/** Offering-scoped matrices, with account-category intersections calculated from revenue detail rows. */
customerRevenueByOfferingType?: Partial<Record<SalesRevenueOfferingType, SalesCustomerRevenue>>
}

/**
Expand Down
Loading