@@ -24,48 +24,104 @@ struct ClaudeUsageReportView: View {
2424 // Debounced state for progress updates
2525 @DebouncedState ( duration: . milliseconds( 300 ) ) private var debouncedFilesProcessed = 0
2626 @DebouncedState ( duration: . milliseconds( 300 ) ) private var debouncedLoadingMessage = " "
27+
28+ // Debounced state for table data to prevent excessive updates
29+ @DebouncedState ( duration: . milliseconds( 500 ) ) private var debouncedDailyUsage : [ Date : [ ClaudeLogEntry ] ] = [ : ]
30+ @DebouncedState ( duration: . milliseconds( 500 ) ) private var debouncedDataVersion = 0
31+ @State var cachedSummaries : [ DailyUsageSummary ] = [ ]
32+ @State var cachedProjectSummaries : [ ProjectUsageSummary ] = [ ]
33+ @State var availableProjects : [ String ] = [ ]
2734
2835 @State var animationTrigger = false
2936
3037 // MARK: - View
3138
3239 var body : some View {
40+ mainContent
41+ . frame ( maxWidth: . infinity, maxHeight: . infinity)
42+ . onAppear {
43+ handleOnAppear ( )
44+ }
45+ . toolbar {
46+ ToolbarItemGroup ( placement: . automatic) {
47+ toolbarAutomaticItems
48+ }
49+ ToolbarItemGroup ( placement: . primaryAction) {
50+ toolbarPrimaryItems
51+ }
52+ }
53+ }
54+
55+ @ViewBuilder
56+ private var mainContent : some View {
57+ baseContent
58+ . onChange ( of: dataLoader. filesProcessed) { _, newValue in
59+ debouncedFilesProcessed = newValue
60+ }
61+ . onChange ( of: dataLoader. loadingMessage) { _, newValue in
62+ debouncedLoadingMessage = newValue
63+ }
64+ . onChange ( of: dataLoader. dailyUsage. count) { _, _ in
65+ // When data changes, update debounced data and increment version
66+ debouncedDailyUsage = dataLoader. dailyUsage
67+ debouncedDataVersion += 1
68+ }
69+ . onChange ( of: debouncedDataVersion) { _, _ in
70+ updateCachedSummaries ( )
71+ }
72+ . onChange ( of: sortOrder) { _, _ in
73+ cachedSummaries = cachedSummaries. sorted ( using: sortOrder)
74+ }
75+ . onChange ( of: projectSortOrder) { _, _ in
76+ cachedProjectSummaries = cachedProjectSummaries. sorted ( using: projectSortOrder)
77+ }
78+ . onChange ( of: selectedProject) { _, _ in
79+ updateCachedSummaries ( )
80+ }
81+ . onChange ( of: dateRangeStart) { _, _ in
82+ if viewMode == . project {
83+ cachedProjectSummaries = computeProjectSummaries ( )
84+ }
85+ }
86+ . onChange ( of: dateRangeEnd) { _, _ in
87+ if viewMode == . project {
88+ cachedProjectSummaries = computeProjectSummaries ( )
89+ }
90+ }
91+ . onChange ( of: selectedCostStrategy) { _, _ in
92+ updateCachedSummaries ( )
93+ }
94+ . onChange ( of: viewMode) { _, _ in
95+ updateCachedSummaries ( )
96+ }
97+ }
98+
99+ @ViewBuilder
100+ private var baseContent : some View {
33101 Group {
34102 if let error = dataLoader. errorMessage {
35103 errorView ( error: error)
36104 } else {
37- VStack ( spacing: 0 ) {
38- if dataLoader. isLoading, dataLoader. dailyUsage. isEmpty {
39- // Initial loading state
40- initialLoadingView
41- } else {
42- headerSection
43- Divider ( )
44- if dataLoader. isLoading {
45- progressSection
46- Divider ( )
47- }
48- contentSection
49- }
50- }
105+ dataContentView
51106 }
52107 }
53- . frame ( maxWidth: . infinity, maxHeight: . infinity)
54- . onChange ( of: dataLoader. filesProcessed) { _, newValue in
55- debouncedFilesProcessed = newValue
56- }
57- . onChange ( of: dataLoader. loadingMessage) { _, newValue in
58- debouncedLoadingMessage = newValue
59- }
60- . onAppear {
61- handleOnAppear ( )
62- }
63- . toolbar {
64- ToolbarItemGroup ( placement: . automatic) {
65- toolbarAutomaticItems
66- }
67- ToolbarItemGroup ( placement: . primaryAction) {
68- toolbarPrimaryItems
108+ }
109+
110+
111+ @ViewBuilder
112+ private var dataContentView : some View {
113+ VStack ( spacing: 0 ) {
114+ if dataLoader. isLoading, dataLoader. dailyUsage. isEmpty {
115+ // Initial loading state
116+ initialLoadingView
117+ } else {
118+ headerSection
119+ Divider ( )
120+ if dataLoader. isLoading {
121+ progressSection
122+ Divider ( )
123+ }
124+ contentSection
69125 }
70126 }
71127 }
@@ -136,14 +192,86 @@ struct ClaudeUsageReportView: View {
136192 private func handleOnAppear( ) {
137193 // Initialize cost strategy from settings
138194 selectedCostStrategy = settingsManager. displaySettingsManager. costCalculationStrategy
139- refreshData ( )
195+
196+ // Load data WITHOUT forcing refresh - use cache if available
197+ dataLoader. loadData ( forceRefresh: false )
140198
141199 // Start animation
142200 animationTrigger = true
143201
144202 // Initialize debounced values with current dataLoader values
145203 debouncedFilesProcessed = dataLoader. filesProcessed
146204 debouncedLoadingMessage = dataLoader. loadingMessage
205+ debouncedDailyUsage = dataLoader. dailyUsage
206+ debouncedDataVersion = 1 // Trigger initial update
207+
208+ // Initialize cached summaries
209+ updateCachedSummaries ( )
210+ }
211+
212+ private func updateCachedSummaries( ) {
213+ // Update daily summaries
214+ cachedSummaries = computeSummaries ( )
215+
216+ // Update project summaries
217+ cachedProjectSummaries = computeProjectSummaries ( )
218+
219+ // Update available projects from debounced data
220+ let projects = debouncedDailyUsage. values
221+ . flatMap ( \. self)
222+ . compactMap ( \. projectName)
223+ availableProjects = Array ( Set ( projects) ) . sorted ( )
224+ }
225+
226+ private func computeSummaries( ) -> [ DailyUsageSummary ] {
227+ let filtered = getFilteredDailyUsage ( )
228+ let sortedDays = filtered. keys. sorted ( by: > )
229+
230+ let summaries : [ DailyUsageSummary ] = sortedDays. compactMap { date -> DailyUsageSummary ? in
231+ guard let entries = filtered [ date] else { return nil }
232+ return DailyUsageSummary ( date: date, entries: entries, costStrategy: selectedCostStrategy)
233+ }
234+
235+ return summaries. sorted ( using: sortOrder)
236+ }
237+
238+ private func computeProjectSummaries( ) -> [ ProjectUsageSummary ] {
239+ // Filter entries by date range
240+ let calendar = Calendar . current
241+ let startOfDay = calendar. startOfDay ( for: dateRangeStart)
242+ let endOfDay = calendar. startOfDay ( for: dateRangeEnd) . addingTimeInterval ( 24 * 60 * 60 )
243+
244+ let filteredEntries = debouncedDailyUsage. flatMap { date, entries -> [ ClaudeLogEntry ] in
245+ guard date >= startOfDay, date < endOfDay else { return [ ] }
246+ return entries
247+ }
248+
249+ // Group by project
250+ let entriesByProject = Dictionary ( grouping: filteredEntries) { entry in
251+ entry. projectName ?? " Unknown "
252+ }
253+
254+ // Create summaries
255+ let summaries = entriesByProject. map { projectName, entries in
256+ ProjectUsageSummary ( projectName: projectName, entries: entries, costStrategy: selectedCostStrategy)
257+ }
258+
259+ return summaries. sorted ( using: projectSortOrder)
260+ }
261+
262+ private func getFilteredDailyUsage( ) -> [ Date : [ ClaudeLogEntry ] ] {
263+ guard selectedProject != " All Projects " else {
264+ return debouncedDailyUsage
265+ }
266+
267+ var filtered : [ Date : [ ClaudeLogEntry ] ] = [ : ]
268+ for (date, entries) in debouncedDailyUsage {
269+ let projectEntries = entries. filter { $0. projectName == selectedProject }
270+ if !projectEntries. isEmpty {
271+ filtered [ date] = projectEntries
272+ }
273+ }
274+ return filtered
147275 }
148276}
149277
0 commit comments