11import { FC , useEffect , useState , useCallback } from "react" ;
22import { Modal } from "../ReusableComponents/Modal" ;
33import { ActiveTabs , Tabs } from "../ReusableComponents/ActiveTabs" ;
4- import { getAllUserMtdCosts , getBillingGroupsCosts } from "@/services/mtdCostService" ;
4+ import { getAllUserMtdCosts , getBillingGroupsCosts , getAllUserMtdCostsRecursive , AutoLoadProgress } from "@/services/mtdCostService" ;
55import { LoadingIcon } from "../Loader/LoadingIcon" ;
66import { IconRefresh , IconDownload , IconUsers , IconBuilding , IconLink , IconAlertTriangle , IconInfoCircle , IconKey , IconUserCog , IconBolt } from "@tabler/icons-react" ;
77import { InfoBox } from "../ReusableComponents/InfoBox" ;
@@ -92,11 +92,23 @@ export const UserCostsModal: FC<Props> = ({ open, onClose }) => {
9292 const [ userCosts , setUserCosts ] = useState < UserMtdData [ ] > ( [ ] ) ;
9393 const [ userLoading , setUserLoading ] = useState ( false ) ;
9494 const [ userError , setUserError ] = useState < string | null > ( null ) ;
95- const [ limit , setLimit ] = useState ( 50 ) ;
96- const [ responseData , setResponseData ] = useState < UserCostsResponse | null > ( null ) ;
9795 const [ expandedUser , setExpandedUser ] = useState < string | null > ( null ) ;
98- const [ lastEvaluatedKey , setLastEvaluatedKey ] = useState < any > ( null ) ;
99- const [ loadingMore , setLoadingMore ] = useState ( false ) ;
96+
97+ // Auto-load state
98+ const [ autoLoadState , setAutoLoadState ] = useState < {
99+ status : 'idle' | 'loading' | 'completed' | 'error' | 'aborted' ;
100+ loadedCount : number ;
101+ currentTotalCost : number ;
102+ batchNumber : number ;
103+ hasMore : boolean ;
104+ } > ( {
105+ status : 'idle' ,
106+ loadedCount : 0 ,
107+ currentTotalCost : 0 ,
108+ batchNumber : 0 ,
109+ hasMore : false
110+ } ) ;
111+ const [ abortController , setAbortController ] = useState < AbortController | null > ( null ) ;
100112
101113 // Search state
102114 const [ userSearchTerm , setUserSearchTerm ] = useState < string > ( '' ) ;
@@ -109,45 +121,56 @@ export const UserCostsModal: FC<Props> = ({ open, onClose }) => {
109121 const [ groupsError , setGroupsError ] = useState < string | null > ( null ) ;
110122 const [ expandedGroupMembers , setExpandedGroupMembers ] = useState < string | null > ( null ) ;
111123
112- // Fetch All Users MTD costs
113- const fetchMTDCosts = useCallback ( async ( appendData = false , nextKey : any = null ) => {
114- if ( appendData ) {
115- setLoadingMore ( true ) ;
116- } else {
117- setUserLoading ( true ) ;
118- setUserCosts ( [ ] ) ;
119- setLastEvaluatedKey ( null ) ;
120- }
124+ // Auto-load All Users MTD costs with progressive rendering
125+ const autoLoadAllUsers = useCallback ( async ( ) => {
126+ const controller = new AbortController ( ) ;
127+ setAbortController ( controller ) ;
128+ setUserLoading ( true ) ;
121129 setUserError ( null ) ;
122-
130+ setUserCosts ( [ ] ) ;
131+ setAutoLoadState ( {
132+ status : 'loading' ,
133+ loadedCount : 0 ,
134+ currentTotalCost : 0 ,
135+ batchNumber : 0 ,
136+ hasMore : true
137+ } ) ;
138+
139+ const handleProgress = ( progress : AutoLoadProgress ) => {
140+ setUserCosts ( progress . users ) ;
141+ setAutoLoadState ( {
142+ status : progress . isComplete ? 'completed' : 'loading' ,
143+ loadedCount : progress . loadedCount ,
144+ currentTotalCost : progress . currentTotalCost ,
145+ batchNumber : progress . batchNumber ,
146+ hasMore : progress . hasMore
147+ } ) ;
148+ } ;
149+
123150 try {
124- const result = await getAllUserMtdCosts ( limit , nextKey ) ;
125- console . log ( "result" , result . data ) ;
126- if ( ! result . success || ! result . data ) {
151+ const result = await getAllUserMtdCostsRecursive (
152+ handleProgress ,
153+ controller . signal ,
154+ 100
155+ ) ;
156+
157+ if ( ! result . success ) {
127158 setUserError ( result . message || 'Failed to fetch MTD costs' ) ;
128- return ;
129- }
130- // The result should already be decoded by doRequestOp
131- let data = result . data ;
132-
133- // Handle the response structure from your API
134- if ( data && data . users && Array . isArray ( data . users ) ) {
135- if ( appendData ) {
136- setUserCosts ( prev => [ ...prev , ...data . users ] ) ;
137- } else {
138- setUserCosts ( data . users ) ;
139- }
140- setResponseData ( data ) ;
141- setLastEvaluatedKey ( data . lastEvaluatedKey ) ;
159+ setAutoLoadState ( prev => ( { ...prev , status : 'error' } ) ) ;
160+ } else if ( result . data ?. aborted ) {
161+ setAutoLoadState ( prev => ( { ...prev , status : 'aborted' } ) ) ;
162+ } else {
163+ setAutoLoadState ( prev => ( { ...prev , status : 'completed' } ) ) ;
142164 }
143165 } catch ( err ) {
144166 setUserError ( 'An error occurred while fetching MTD costs' ) ;
145167 console . error ( 'Error fetching MTD costs:' , err ) ;
168+ setAutoLoadState ( prev => ( { ...prev , status : 'error' } ) ) ;
146169 } finally {
147170 setUserLoading ( false ) ;
148- setLoadingMore ( false ) ;
171+ setAbortController ( null ) ;
149172 }
150- } , [ limit ] ) ;
173+ } , [ ] ) ;
151174
152175 // Fetch Billing Groups costs
153176 const fetchBillingGroupsCosts = async ( ) => {
@@ -177,13 +200,21 @@ export const UserCostsModal: FC<Props> = ({ open, onClose }) => {
177200
178201 useEffect ( ( ) => {
179202 if ( open ) {
180- fetchMTDCosts ( ) ;
203+ autoLoadAllUsers ( ) ;
181204 fetchBillingGroupsCosts ( ) ;
182205 }
183- } , [ open , fetchMTDCosts ] ) ;
206+ return ( ) => {
207+ if ( abortController ) {
208+ abortController . abort ( ) ;
209+ }
210+ } ;
211+ } , [ open ] ) ;
184212
185- const handleLimitChange = ( newLimit : number ) => {
186- setLimit ( newLimit ) ;
213+ const handleStopLoading = ( ) => {
214+ if ( abortController ) {
215+ abortController . abort ( ) ;
216+ setAutoLoadState ( prev => ( { ...prev , status : 'aborted' , hasMore : false } ) ) ;
217+ }
187218 } ;
188219
189220
@@ -310,18 +341,12 @@ export const UserCostsModal: FC<Props> = ({ open, onClose }) => {
310341
311342 const handleRefresh = ( ) => {
312343 if ( activeTab === 0 ) {
313- fetchMTDCosts ( ) ;
344+ autoLoadAllUsers ( ) ;
314345 } else {
315346 fetchBillingGroupsCosts ( ) ;
316347 }
317348 } ;
318349
319- const handleLoadMore = ( ) => {
320- if ( lastEvaluatedKey && responseData ?. hasMore && ! loadingMore ) {
321- fetchMTDCosts ( true , lastEvaluatedKey ) ;
322- }
323- } ;
324-
325350 const toggleUserExpansion = ( email : string ) => {
326351 setExpandedUser ( expandedUser === email ? null : email ) ;
327352 } ;
@@ -645,24 +670,73 @@ export const UserCostsModal: FC<Props> = ({ open, onClose }) => {
645670 </ div >
646671
647672
673+ { /* Auto-Loading Banner */ }
674+ { autoLoadState . status === 'loading' && (
675+ < div className = "mb-4 mx-2 bg-gradient-to-r from-blue-50 to-purple-50 dark:from-blue-900/20 dark:to-purple-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-4 shadow-sm" >
676+ < div className = "flex items-center justify-between" >
677+ < div className = "flex items-center space-x-3" >
678+ < LoadingIcon style = { { width : '20px' , height : '20px' } } />
679+ < div >
680+ < div className = "flex items-center space-x-2" >
681+ < span className = "text-sm font-semibold text-blue-900 dark:text-blue-200" >
682+ Loading users...
683+ </ span >
684+ < span className = "text-sm text-blue-700 dark:text-blue-300" >
685+ { autoLoadState . loadedCount . toLocaleString ( ) } loaded
686+ </ span >
687+ < span className = "text-blue-400 dark:text-blue-500" > •</ span >
688+ < span className = "text-sm font-medium text-green-700 dark:text-green-300" >
689+ { formatCurrency ( autoLoadState . currentTotalCost ) } so far
690+ </ span >
691+ </ div >
692+ < div className = "text-xs text-blue-600 dark:text-blue-400 mt-1" >
693+ Batch { autoLoadState . batchNumber } • Fetching all users automatically...
694+ </ div >
695+ </ div >
696+ </ div >
697+ < button
698+ onClick = { handleStopLoading }
699+ className = "px-3 py-1.5 text-sm bg-red-100 hover:bg-red-200 dark:bg-red-900/30 dark:hover:bg-red-900/50 text-red-700 dark:text-red-300 rounded-md transition-colors"
700+ >
701+ Stop Loading
702+ </ button >
703+ </ div >
704+ < div className = "mt-3 w-full bg-blue-200 dark:bg-blue-900/40 rounded-full h-2 overflow-hidden" >
705+ < div
706+ className = "h-full bg-blue-600 dark:bg-blue-400 transition-all duration-300 ease-out animate-pulse"
707+ style = { { width : '100%' } }
708+ > </ div >
709+ </ div >
710+ </ div >
711+ ) }
712+
713+ { /* Completion Banner */ }
714+ { autoLoadState . status === 'completed' && (
715+ < div className = "mb-4 mx-2 bg-gradient-to-r from-green-50 to-emerald-50 dark:from-green-900/20 dark:to-emerald-900/20 border border-green-200 dark:border-green-800 rounded-lg p-3 shadow-sm" >
716+ < div className = "flex items-center space-x-2" >
717+ < span className = "text-green-600 dark:text-green-400 text-lg" > ✓</ span >
718+ < span className = "text-sm font-medium text-green-900 dark:text-green-200" >
719+ Loaded all { autoLoadState . loadedCount . toLocaleString ( ) } users • Total: { formatCurrency ( autoLoadState . currentTotalCost ) }
720+ </ span >
721+ </ div >
722+ </ div >
723+ ) }
724+
725+ { /* Aborted Banner */ }
726+ { autoLoadState . status === 'aborted' && (
727+ < div className = "mb-4 mx-2 bg-gradient-to-r from-yellow-50 to-orange-50 dark:from-yellow-900/20 dark:to-orange-900/20 border border-yellow-200 dark:border-yellow-800 rounded-lg p-3 shadow-sm" >
728+ < div className = "flex items-center space-x-2" >
729+ < span className = "text-yellow-600 dark:text-yellow-400 text-lg" > ⏸</ span >
730+ < span className = "text-sm font-medium text-yellow-900 dark:text-yellow-200" >
731+ Stopped loading • Showing { autoLoadState . loadedCount . toLocaleString ( ) } users • Partial total: { formatCurrency ( autoLoadState . currentTotalCost ) }
732+ </ span >
733+ </ div >
734+ </ div >
735+ ) }
736+
648737 { /* Controls */ }
649738 < div className = "mb-6 flex items-center justify-between px-2" >
650739 < div className = "flex items-center space-x-4" >
651- < label htmlFor = "limit-select" className = "text-sm font-medium text-gray-700 dark:text-gray-300" >
652- Show:
653- </ label >
654- < select
655- id = "limit-select"
656- value = { limit }
657- onChange = { ( e ) => handleLimitChange ( Number ( e . target . value ) ) }
658- className = "px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500"
659- >
660- { MTD_USAGE_LIMITS . map ( ( limitOption ) => (
661- < option key = { limitOption } value = { limitOption } >
662- { limitOption } users
663- </ option >
664- ) ) }
665- </ select >
666740 { /* Search Bar - only show if there are multiple users */ }
667741 { userCosts . length > 1 && (
668742 < div className = "px-2" >
@@ -710,27 +784,31 @@ export const UserCostsModal: FC<Props> = ({ open, onClose }) => {
710784 </ div >
711785 ) }
712786
713- { /* Loading State */ }
714- { userLoading && ! userError && (
787+ { /* Initial Loading State - only show when no data yet */ }
788+ { userLoading && userCosts . length === 0 && ! userError && (
715789 < div className = "flex items-center justify-center py-12" >
716790 < div className = "flex items-center space-x-2" >
717791 < LoadingIcon style = { { width : '24px' , height : '24px' } } />
718- < span className = "text-lg text-gray-700 dark:text-gray-300" > Loading user costs data...</ span >
792+ < span className = "text-lg text-gray-700 dark:text-gray-300" > Initializing data load ...</ span >
719793 </ div >
720794 </ div >
721795 ) }
722796
723- { /* Data Table */ }
724- { ! userLoading && ! userError && (
797+ { /* Data Table - Show even while loading if we have data */ }
798+ { userCosts . length > 0 && ! userError && (
725799 < div className = "flex-1 overflow-hidden" >
726800 < div className = "bg-white dark:bg-gray-800 rounded-lg shadow overflow-hidden mx-2 h-full flex flex-col" >
727801 < div className = "px-6 py-4 border-b border-gray-200 dark:border-gray-700" >
728802 < h2 className = "text-lg font-semibold text-gray-900 dark:text-white" >
729803 Month to Date Cost by User
730804 { userSearchTerm ? (
731805 < span > ({ filteredUsers . length } of { userCosts . length } users)</ span >
806+ ) : autoLoadState . status === 'loading' ? (
807+ < span > ({ userCosts . length . toLocaleString ( ) } loaded, loading more...)</ span >
808+ ) : autoLoadState . status === 'aborted' ? (
809+ < span > ({ userCosts . length . toLocaleString ( ) } partial)</ span >
732810 ) : (
733- < span > ({ userCosts . length } { responseData ?. hasMore ? `of ${ responseData ?. count || 'many' } ` : ' total' } )</ span >
811+ < span > ({ userCosts . length . toLocaleString ( ) } total)</ span >
734812 ) }
735813 </ h2 >
736814 </ div >
@@ -856,23 +934,17 @@ export const UserCostsModal: FC<Props> = ({ open, onClose }) => {
856934 </ div >
857935 ) }
858936
859- { /* Load More Button */ }
860- { responseData ?. hasMore && (
861- < div className = "px-6 py-4 border-t border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-900" >
862- < button
863- onClick = { handleLoadMore }
864- disabled = { loadingMore }
865- className = "w-full flex items-center justify-center space-x-2 px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
866- >
867- { loadingMore ? (
868- < >
869- < LoadingIcon style = { { width : '16px' , height : '16px' } } />
870- < span > Loading more...</ span >
871- </ >
872- ) : (
873- < span > Load More Users</ span >
874- ) }
875- </ button >
937+ { /* Skeleton Loaders - Show while loading more batches */ }
938+ { autoLoadState . status === 'loading' && autoLoadState . loadedCount > 0 && (
939+ < div className = "px-6 py-4 border-t border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-900 space-y-2" >
940+ { [ 1 , 2 , 3 ] . map ( ( i ) => (
941+ < div key = { i } className = "animate-pulse flex space-x-4" >
942+ < div className = "flex-1 space-y-2 py-1" >
943+ < div className = "h-4 bg-gray-300 dark:bg-gray-600 rounded w-3/4" > </ div >
944+ < div className = "h-3 bg-gray-300 dark:bg-gray-600 rounded w-1/2" > </ div >
945+ </ div >
946+ </ div >
947+ ) ) }
876948 </ div >
877949 ) }
878950 </ div >
0 commit comments