@@ -3,6 +3,7 @@ import type { ChangeEvent, DragEvent, KeyboardEvent, MouseEvent } from 'react'
33import { MarkGithubIcon , GraphIcon , PeopleIcon , CopilotIcon , TableIcon , OrganizationIcon , DatabaseIcon , InfoIcon , QuestionIcon , CreditCardIcon } from '@primer/octicons-react'
44
55import { NewVersionBanner , UploadPage } from './components'
6+ import { SeatCountConfirmation } from './components/SeatCountConfirmation'
67import { UsersView } from './views/UsersView'
78import type { SeatOverrides } from './views/UsersView'
89import { UserDetailsView } from './views/UserDetailsView'
@@ -67,6 +68,9 @@ function App() {
6768 const [ budgetSimulation , setBudgetSimulation ] = useState < BudgetSimulationResult | null > ( null )
6869 const [ budgetSimulationError , setBudgetSimulationError ] = useState < string | null > ( null )
6970 const [ isApplyingBudgetSimulation , setIsApplyingBudgetSimulation ] = useState ( false )
71+ const [ seatConfirmationPending , setSeatConfirmationPending ] = useState ( false )
72+ const [ seatConfirmationError , setSeatConfirmationError ] = useState < string | null > ( null )
73+ const [ isApplyingSeatConfirmation , setIsApplyingSeatConfirmation ] = useState ( false )
7074 const fileInputRef = useRef < HTMLInputElement | null > ( null )
7175 const currentFileRef = useRef < File | null > ( null )
7276 const latestRunIdRef = useRef ( 0 )
@@ -146,7 +150,6 @@ function App() {
146150 }
147151 } , [ ] )
148152
149-
150153 const getDefaultSeatCounts = useCallback ( ( ) => {
151154 const summary = calculateLicenseSummary ( userUsage ?. users ?? [ ] )
152155 return {
@@ -179,6 +182,9 @@ function App() {
179182 setProgress ( 0 )
180183 setRowsProcessed ( 0 )
181184 setSeatOverrides ( { } )
185+ setSeatConfirmationPending ( false )
186+ setSeatConfirmationError ( null )
187+ setIsApplyingSeatConfirmation ( false )
182188 setBudgetValues ( EMPTY_BUDGET_VALUES )
183189 setBudgetSimulation ( null )
184190 setBudgetSimulationError ( null )
@@ -203,6 +209,11 @@ function App() {
203209 setProgress ( 100 )
204210 applyProcessedData ( nextData )
205211 setBudgetValues ( getDefaultBudgetValues ( nextData . userUsage . users ) )
212+ setSeatConfirmationError ( null )
213+ const processedUsers = nextData . userUsage . users
214+ const hasOrgContext = processedUsers . some ( ( user ) => user . organizations . length > 0 || user . costCenters . length > 0 )
215+ const processedPlanScope = inferReportPlanScope ( processedUsers . length , hasOrgContext )
216+ setSeatConfirmationPending ( processedPlanScope === 'organization' )
206217 setStatus ( 'done' )
207218 } catch ( err ) {
208219 if ( runId !== latestRunIdRef . current ) return
@@ -260,30 +271,64 @@ function App() {
260271 setIsApplyingBudgetSimulation ( false )
261272 } , [ ] )
262273
263- const handleSeatOverridesChange = useCallback ( async ( overrides : SeatOverrides ) => {
274+ const handleSeatOverridesChange = useCallback ( async (
275+ overrides : SeatOverrides ,
276+ onError ?: ( message : string ) => void ,
277+ ) : Promise < boolean > => {
264278 const file = currentFileRef . current
265- if ( ! file ) return
279+ if ( ! file ) return false
266280
267281 const runId = ++ latestRunIdRef . current
268282 latestSimulationIdRef . current += 1
269283 const resolvedOverrides = resolveIncludedCreditOverrides ( overrides )
270- setError ( null )
271284 setBudgetSimulation ( null )
272285 setBudgetSimulationError ( null )
273286 setIsApplyingBudgetSimulation ( false )
274287
275288 try {
276289 const nextData = await buildReportData ( file , resolvedOverrides )
277- if ( runId !== latestRunIdRef . current ) return
290+ if ( runId !== latestRunIdRef . current ) return false
278291
279292 applyProcessedData ( nextData )
280293 setSeatOverrides ( compactSeatOverrides ( resolvedOverrides ) )
294+ if ( ! onError ) {
295+ setError ( null )
296+ }
297+ return true
281298 } catch ( err ) {
282- if ( runId !== latestRunIdRef . current ) return
283- setError ( err instanceof Error ? err . message : 'Failed to recalculate usage-based billing.' )
299+ if ( runId !== latestRunIdRef . current ) return false
300+ const message = err instanceof Error ? err . message : 'Failed to recalculate usage-based billing.'
301+ if ( onError ) {
302+ onError ( message )
303+ } else {
304+ setError ( message )
305+ }
306+ return false
284307 }
285308 } , [ applyProcessedData , buildReportData , compactSeatOverrides , resolveIncludedCreditOverrides ] )
286309
310+ const handleSeatConfirmationApply = useCallback ( async ( counts : { business : number ; enterprise : number } ) => {
311+ setIsApplyingSeatConfirmation ( true )
312+ setSeatConfirmationError ( null )
313+ try {
314+ const { business : defaultBusiness , enterprise : defaultEnterprise } = getDefaultSeatCounts ( )
315+ if ( counts . business === defaultBusiness && counts . enterprise === defaultEnterprise ) {
316+ setSeatConfirmationPending ( false )
317+ return
318+ }
319+
320+ const success = await handleSeatOverridesChange (
321+ { business : counts . business , enterprise : counts . enterprise } ,
322+ setSeatConfirmationError ,
323+ )
324+ if ( success ) {
325+ setSeatConfirmationPending ( false )
326+ }
327+ } finally {
328+ setIsApplyingSeatConfirmation ( false )
329+ }
330+ } , [ getDefaultSeatCounts , handleSeatOverridesChange ] )
331+
287332 const handleApplyBudgetSimulation = useCallback ( async ( ) => {
288333 const file = currentFileRef . current
289334 if ( ! file ) return
@@ -432,6 +477,7 @@ function App() {
432477 }
433478
434479 const hasReport = status === 'done' && fileName !== null
480+ const showSeatConfirmation = hasReport && seatConfirmationPending
435481 const rangeStart = reportContext ?. startDate ?? null
436482 const rangeEnd = reportContext ?. endDate ?? null
437483 const reportUsers = userUsage ?. users ?? [ ]
@@ -533,6 +579,16 @@ function App() {
533579 </ header >
534580
535581 { hasReport ? (
582+ showSeatConfirmation ? (
583+ < SeatCountConfirmation
584+ fileName = { fileName }
585+ defaultBusinessSeats = { defaultBusinessSeats }
586+ defaultEnterpriseSeats = { defaultEnterpriseSeats }
587+ error = { seatConfirmationError }
588+ isApplying = { isApplyingSeatConfirmation }
589+ onConfirm = { ( counts ) => { void handleSeatConfirmationApply ( counts ) } }
590+ />
591+ ) : (
536592 < >
537593 < nav className = "bg-bg-default border-b border-border-default px-6 py-3 flex justify-between items-center gap-4 flex-wrap max-sm:px-4 max-sm:flex-col max-sm:items-start max-sm:gap-3" >
538594 < div className = "flex items-center gap-2 flex-wrap text-sm text-fg-default max-sm:flex-col max-sm:items-start max-sm:gap-1" >
@@ -675,6 +731,7 @@ function App() {
675731 licenseSeatCounts = { licenseSeatCounts }
676732 reportPlanScope = { reportPlanScope }
677733 upgradeRecommendation = { individualUpgradeRecommendation }
734+ onAdjustSeatCounts = { reportPlanScope === 'organization' && ! isIndividualReport ? ( ) => setActiveView ( 'users' ) : undefined }
678735 />
679736 ) : visibleActiveView === 'models' ? (
680737 modelUsage && modelUsage . models . length > 0 ? (
@@ -771,6 +828,7 @@ function App() {
771828 </ main >
772829 </ div >
773830 </ >
831+ )
774832 ) : (
775833 < UploadPage
776834 dragActive = { dragActive }
0 commit comments