11import { useCallback , useRef , useState } from 'react'
22import type { ChangeEvent , DragEvent , KeyboardEvent , MouseEvent } from 'react'
3- import { MarkGithubIcon , GraphIcon , PeopleIcon , CopilotIcon , TableIcon , OrganizationIcon , DatabaseIcon , InfoIcon , QuestionIcon } from '@primer/octicons-react'
3+ import { MarkGithubIcon , GraphIcon , PeopleIcon , CopilotIcon , TableIcon , OrganizationIcon , DatabaseIcon , InfoIcon , QuestionIcon , CreditCardIcon } from '@primer/octicons-react'
44
55import { UploadPage } from './components'
66import { UsersView } from './views/UsersView'
@@ -13,6 +13,7 @@ import { ReportGuideView } from './views/ReportGuideView'
1313import { FaqView } from './views/FaqView'
1414import { ProductsView } from './views/ProductsView'
1515import { OverviewView } from './views/OverviewView'
16+ import { CostManagementView , type BudgetField , type BudgetValues } from './views/CostManagementView'
1617import { QuickStatsAggregator , type QuickStatsResult } from './pipeline/aggregators/quickStatsAggregator'
1718import { ReportContextAggregator , type ReportContextResult } from './pipeline/aggregators/reportContextAggregator'
1819import { DailyUsageAggregator , type DailyUsageData } from './pipeline/aggregators/dailyUsageAggregator'
@@ -22,10 +23,21 @@ import { CostCenterAggregator, type CostCenterResult } from './pipeline/aggregat
2223import { OrganizationAggregator , type OrganizationResult } from './pipeline/aggregators/organizationAggregator'
2324import { UserUsageAggregator , type UserUsageResult } from './pipeline/aggregators/userUsageAggregator'
2425import { calculateLicenseSummary , inferReportPlanScope , type AicEntitlementOverrides } from './pipeline/aicEntitlements'
26+ import { PRODUCT_BUDGET_COPILOT , PRODUCT_BUDGET_COPILOT_CODE_REVIEW , PRODUCT_BUDGET_SPARK } from './pipeline/productClassification'
2527import { runPipeline } from './pipeline/runPipeline'
28+ import { runBudgetSimulation , type BudgetSimulationResult } from './utils/enterpriseBudgetSimulation'
2629import { calculateCostOptimizationOpportunity } from './utils/costOptimization'
2730
2831type Status = 'idle' | 'processing' | 'done'
32+ type ActiveView = 'overview' | 'users' | 'userDetails' | 'costCenters' | 'orgs' | 'models' | 'products' | 'costManagement' | 'guide' | 'faq'
33+
34+ const EMPTY_BUDGET_VALUES : BudgetValues = {
35+ user : '' ,
36+ enterprise : '' ,
37+ productCodeReview : '' ,
38+ productSpark : '' ,
39+ productCopilot : '' ,
40+ }
2941
3042function App ( ) {
3143 const [ status , setStatus ] = useState < Status > ( 'idle' )
@@ -35,7 +47,7 @@ function App() {
3547 const [ fileName , setFileName ] = useState < string | null > ( null )
3648 const [ dragActive , setDragActive ] = useState ( false )
3749 const [ dailyUsageData , setDailyUsageData ] = useState < DailyUsageData [ ] > ( [ ] )
38- const [ activeView , setActiveView ] = useState < 'overview' | 'users' | 'userDetails' | 'costCenters' | 'orgs' | 'models' | 'products' | 'guide' | 'faq' > ( 'overview' )
50+ const [ activeView , setActiveView ] = useState < ActiveView > ( 'overview' )
3951 const [ userUsage , setUserUsage ] = useState < UserUsageResult | null > ( null )
4052 const [ modelUsage , setModelUsage ] = useState < ModelUsageResult | null > ( null )
4153 const [ productUsage , setProductUsage ] = useState < ProductUsageResult | null > ( null )
@@ -45,9 +57,14 @@ function App() {
4557 const [ progress , setProgress ] = useState ( 0 )
4658 const [ rowsProcessed , setRowsProcessed ] = useState ( 0 )
4759 const [ seatOverrides , setSeatOverrides ] = useState < SeatOverrides > ( { } )
60+ const [ budgetValues , setBudgetValues ] = useState < BudgetValues > ( EMPTY_BUDGET_VALUES )
61+ const [ budgetSimulation , setBudgetSimulation ] = useState < BudgetSimulationResult | null > ( null )
62+ const [ budgetSimulationError , setBudgetSimulationError ] = useState < string | null > ( null )
63+ const [ isApplyingBudgetSimulation , setIsApplyingBudgetSimulation ] = useState ( false )
4864 const fileInputRef = useRef < HTMLInputElement | null > ( null )
4965 const currentFileRef = useRef < File | null > ( null )
5066 const latestRunIdRef = useRef ( 0 )
67+ const latestSimulationIdRef = useRef ( 0 )
5168
5269 const applyProcessedData = useCallback ( ( {
5370 quickStats,
@@ -144,6 +161,7 @@ function App() {
144161 const handleProcess = useCallback ( async ( file : File ) => {
145162 currentFileRef . current = file
146163 const runId = ++ latestRunIdRef . current
164+ latestSimulationIdRef . current += 1
147165 setStatus ( 'processing' )
148166 setError ( null )
149167 setQuickStats ( null )
@@ -160,6 +178,10 @@ function App() {
160178 setProgress ( 0 )
161179 setRowsProcessed ( 0 )
162180 setSeatOverrides ( { } )
181+ setBudgetValues ( EMPTY_BUDGET_VALUES )
182+ setBudgetSimulation ( null )
183+ setBudgetSimulationError ( null )
184+ setIsApplyingBudgetSimulation ( false )
163185
164186 try {
165187 const nextData = await buildReportData ( file , { } , ( progressInfo ) => {
@@ -218,13 +240,28 @@ function App() {
218240 return compactOverrides
219241 } , [ getDefaultSeatCounts ] )
220242
243+ const handleBudgetValueChange = useCallback ( ( field : BudgetField , value : string ) => {
244+ latestSimulationIdRef . current += 1
245+ setBudgetValues ( ( current ) => ( {
246+ ...current ,
247+ [ field ] : value ,
248+ } ) )
249+ setBudgetSimulation ( null )
250+ setBudgetSimulationError ( null )
251+ setIsApplyingBudgetSimulation ( false )
252+ } , [ ] )
253+
221254 const handleSeatOverridesChange = useCallback ( async ( overrides : SeatOverrides ) => {
222255 const file = currentFileRef . current
223256 if ( ! file ) return
224257
225258 const runId = ++ latestRunIdRef . current
259+ latestSimulationIdRef . current += 1
226260 const resolvedOverrides = resolveEntitlementOverrides ( overrides )
227261 setError ( null )
262+ setBudgetSimulation ( null )
263+ setBudgetSimulationError ( null )
264+ setIsApplyingBudgetSimulation ( false )
228265
229266 try {
230267 const nextData = await buildReportData ( file , resolvedOverrides )
@@ -238,6 +275,80 @@ function App() {
238275 }
239276 } , [ applyProcessedData , buildReportData , compactSeatOverrides , resolveEntitlementOverrides ] )
240277
278+ const handleApplyBudgetSimulation = useCallback ( async ( ) => {
279+ const file = currentFileRef . current
280+ if ( ! file ) return
281+
282+ const parsedEnterpriseBudget = budgetValues . enterprise . trim ( ) === '' ? undefined : Number ( budgetValues . enterprise )
283+ const parsedUserBudget = budgetValues . user . trim ( ) === '' ? undefined : Number ( budgetValues . user )
284+ const parsedProductCodeReviewBudget = budgetValues . productCodeReview . trim ( ) === '' ? undefined : Number ( budgetValues . productCodeReview )
285+ const parsedProductSparkBudget = budgetValues . productSpark . trim ( ) === '' ? undefined : Number ( budgetValues . productSpark )
286+ const parsedProductCopilotBudget = budgetValues . productCopilot . trim ( ) === '' ? undefined : Number ( budgetValues . productCopilot )
287+
288+ if (
289+ parsedEnterpriseBudget === undefined
290+ && parsedUserBudget === undefined
291+ && parsedProductCodeReviewBudget === undefined
292+ && parsedProductSparkBudget === undefined
293+ && parsedProductCopilotBudget === undefined
294+ ) {
295+ setBudgetSimulation ( null )
296+ setBudgetSimulationError ( 'Enter a user-level, account-level, or product-level budget in USD before running the simulation.' )
297+ return
298+ }
299+
300+ if (
301+ ( parsedEnterpriseBudget !== undefined && ! Number . isFinite ( parsedEnterpriseBudget ) )
302+ || ( parsedUserBudget !== undefined && ! Number . isFinite ( parsedUserBudget ) )
303+ || ( parsedProductCodeReviewBudget !== undefined && ! Number . isFinite ( parsedProductCodeReviewBudget ) )
304+ || ( parsedProductSparkBudget !== undefined && ! Number . isFinite ( parsedProductSparkBudget ) )
305+ || ( parsedProductCopilotBudget !== undefined && ! Number . isFinite ( parsedProductCopilotBudget ) )
306+ ) {
307+ setBudgetSimulation ( null )
308+ setBudgetSimulationError ( 'Enter valid USD budget values before running the simulation.' )
309+ return
310+ }
311+
312+ const simulationId = ++ latestSimulationIdRef . current
313+ setBudgetSimulationError ( null )
314+ setIsApplyingBudgetSimulation ( true )
315+
316+ try {
317+ const result = await runBudgetSimulation (
318+ file ,
319+ {
320+ enterpriseBudgetUsd : parsedEnterpriseBudget ,
321+ userBudgetUsd : parsedUserBudget ,
322+ productBudgetsUsd : {
323+ [ PRODUCT_BUDGET_COPILOT_CODE_REVIEW ] : parsedProductCodeReviewBudget ,
324+ [ PRODUCT_BUDGET_SPARK ] : parsedProductSparkBudget ,
325+ [ PRODUCT_BUDGET_COPILOT ] : parsedProductCopilotBudget ,
326+ } ,
327+ } ,
328+ resolveEntitlementOverrides ( seatOverrides ) ,
329+ )
330+
331+ if ( simulationId !== latestSimulationIdRef . current ) return
332+ setBudgetSimulation ( result )
333+ } catch ( err ) {
334+ if ( simulationId !== latestSimulationIdRef . current ) return
335+ setBudgetSimulation ( null )
336+ setBudgetSimulationError ( err instanceof Error ? err . message : 'Failed to run the budget simulation.' )
337+ } finally {
338+ if ( simulationId === latestSimulationIdRef . current ) {
339+ setIsApplyingBudgetSimulation ( false )
340+ }
341+ }
342+ } , [
343+ budgetValues . enterprise ,
344+ budgetValues . productCodeReview ,
345+ budgetValues . productCopilot ,
346+ budgetValues . productSpark ,
347+ budgetValues . user ,
348+ resolveEntitlementOverrides ,
349+ seatOverrides ,
350+ ] )
351+
241352 const preventDefault = ( event : DragEvent < HTMLDivElement > ) => {
242353 event . preventDefault ( )
243354 event . stopPropagation ( )
@@ -306,7 +417,22 @@ function App() {
306417 setActiveView ( 'users' )
307418 }
308419
309- const overviewAicNetAmount = dailyUsageData . reduce ( ( sum , day ) => sum + day . aicNetAmount , 0 )
420+ const overviewTotals = dailyUsageData . reduce (
421+ ( totals , day ) => {
422+ totals . requests += day . requests
423+ totals . grossAmount += day . grossAmount
424+ totals . discountAmount += day . discountAmount
425+ totals . netAmount += day . netAmount
426+ totals . aicQuantity += day . aicQuantity
427+ totals . aicGrossAmount += day . aicGrossAmount
428+ totals . aicNetAmount += day . aicNetAmount
429+ return totals
430+ } ,
431+ { requests : 0 , grossAmount : 0 , discountAmount : 0 , netAmount : 0 , aicQuantity : 0 , aicGrossAmount : 0 , aicNetAmount : 0 } ,
432+ )
433+ const overviewPruNetAmount = overviewTotals . netAmount
434+ const overviewAicNetAmount = overviewTotals . aicNetAmount
435+ const overviewAicDiscountAmount = Math . max ( overviewTotals . aicGrossAmount - overviewAicNetAmount , 0 )
310436 const resolvedSeatOverrides = resolveEntitlementOverrides ( seatOverrides )
311437 const costOptimizationOpportunity = calculateCostOptimizationOpportunity ( {
312438 aicNetAmount : overviewAicNetAmount ,
@@ -434,6 +560,15 @@ function App() {
434560 </ button >
435561 ) }
436562
563+ < button
564+ type = "button"
565+ className = { `${ sidebarItemBase } ${ activeView === 'costManagement' ? sidebarActive : sidebarInactive } ` }
566+ onClick = { ( ) => setActiveView ( 'costManagement' ) }
567+ >
568+ < CreditCardIcon size = { 18 } className = "shrink-0" aria-hidden />
569+ < span className = "whitespace-nowrap overflow-hidden text-ellipsis max-sm:sr-only" > Cost Management</ span >
570+ </ button >
571+
437572 < hr className = "border-0 border-t border-border-default my-[6px]" />
438573
439574 < button
@@ -506,13 +641,33 @@ function App() {
506641 < div className = { viewContentClasses } >
507642 < CostCentersView data = { costCenters ?? { costCenters : [ ] } } rangeStart = { rangeStart } />
508643 </ div >
509- ) : activeView === 'products' ? (
510- < div className = { viewContentClasses } >
511- < ProductsView data = { productUsage ?? { products : [ ] } } />
512- </ div >
513- ) : activeView === 'guide' ? (
514- < div className = { viewContentClasses } >
515- < ReportGuideView />
644+ ) : activeView === 'products' ? (
645+ < div className = { viewContentClasses } >
646+ < ProductsView data = { productUsage ?? { products : [ ] } } />
647+ </ div >
648+ ) : activeView === 'costManagement' ? (
649+ < div className = { viewContentClasses } >
650+ < CostManagementView
651+ budgetValues = { budgetValues }
652+ currentPruBill = { overviewPruNetAmount }
653+ currentPruGrossAmount = { overviewTotals . grossAmount }
654+ currentPruDiscountAmount = { overviewTotals . discountAmount }
655+ currentPruQuantity = { overviewTotals . requests }
656+ currentAicBill = { overviewAicNetAmount }
657+ currentAicGrossAmount = { overviewTotals . aicGrossAmount }
658+ currentAicDiscountAmount = { overviewAicDiscountAmount }
659+ currentAicQuantity = { overviewTotals . aicQuantity }
660+ dailyUsageData = { dailyUsageData }
661+ budgetSimulation = { budgetSimulation }
662+ budgetSimulationError = { budgetSimulationError }
663+ isApplyingBudgetSimulation = { isApplyingBudgetSimulation }
664+ onBudgetValueChange = { handleBudgetValueChange }
665+ onApplyBudgetSimulation = { handleApplyBudgetSimulation }
666+ />
667+ </ div >
668+ ) : activeView === 'guide' ? (
669+ < div className = { viewContentClasses } >
670+ < ReportGuideView />
516671 </ div >
517672 ) : activeView === 'faq' ? (
518673 < div className = { viewContentClasses } >
0 commit comments