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
105 changes: 105 additions & 0 deletions Sources/ViewModels/TranscriptionHistoryViewModel.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import Foundation

/// ViewModel for `TranscriptionHistoryView`. Owns the paged record data, the
/// load/pagination state, and all `DataManager` interactions so the view never
/// touches the store directly (audit item A2).
///
/// `dataManager` is injected via the initializer with a `DataManager.shared`
/// default, mirroring `DashboardHomeView`'s constructor injection so tests can
/// substitute a `MockDataManager`.
@MainActor
@Observable
final class TranscriptionHistoryViewModel {
// MARK: - Data + Load State

private(set) var records: [TranscriptionRecord] = []
private(set) var page: Int = 0
private(set) var hasMore: Bool = true
private(set) var isLoading: Bool = false
private(set) var hasLoadedOnce: Bool = false

var showError = false
var errorMessage = ""

// MARK: - Dependencies

private let dataManager: DataManagerProtocol
private let pageSize: Int

// MARK: - Initialization

init(dataManager: DataManagerProtocol = DataManager.shared, pageSize: Int = 50) {
self.dataManager = dataManager
self.pageSize = pageSize
}

// MARK: - Paginated Loading

/// Loads the next page of records (or the first page when `reset` is true).
/// `search` is the raw search text from the view; it is trimmed here and
/// treated as "no filter" when empty.
func loadRecords(reset: Bool = false, search: String = "") async {
guard !isLoading else { return }
isLoading = true
Comment on lines +42 to +43
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Reset search requests can be dropped while loading, causing stale results.

At Line 42, guard !isLoading else { return } discards any reset: true call fired during an in-flight fetch. With rapid search updates, the latest query may never load.

💡 Suggested fix (queue latest reset and replay after current load)
 final class TranscriptionHistoryViewModel {
+    private var pendingResetSearch: String?
@@
     func loadRecords(reset: Bool = false, search: String = "") async {
-        guard !isLoading else { return }
+        if isLoading {
+            if reset { pendingResetSearch = search }
+            return
+        }
         isLoading = true
         defer {
             isLoading = false
             hasLoadedOnce = true
         }
@@
         do {
@@
         } catch {
             errorMessage = "Failed to load transcription history: \(error.localizedDescription)"
             showError = true
             hasMore = false
         }
+
+        if let queuedSearch = pendingResetSearch {
+            pendingResetSearch = nil
+            Task { [weak self] in
+                await self?.loadRecords(reset: true, search: queuedSearch)
+            }
+        }
     }
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Sources/ViewModels/TranscriptionHistoryViewModel.swift` around lines 42 - 43,
The guard that returns when isLoading is true discards incoming reset requests;
instead add a queued-reset mechanism on TranscriptionHistoryViewModel: introduce
a property like pendingReset (and optionally pendingQuery or pendingParameters)
and, in the method that currently contains "guard !isLoading else { return }",
if isLoading is true set pendingReset = true (and store the latest query params)
and return without dropping the request; after the current fetch completes
(where you set isLoading = false) check pendingReset and if set clear it and
replay the load call with reset: true (using the stored params if applicable) so
the latest reset is executed when the in-flight load finishes.

defer {
isLoading = false
hasLoadedOnce = true
}

if reset {
page = 0
hasMore = true
}

let trimmed = search.trimmingCharacters(in: .whitespacesAndNewlines)
let searchTerm: String? = trimmed.isEmpty ? nil : trimmed

do {
let offset = page * pageSize
let batch = try await dataManager.fetchRecords(
limit: pageSize,
offset: offset,
search: searchTerm
)

if reset {
records = batch
} else {
records.append(contentsOf: batch)
}

hasMore = batch.count == pageSize
page += 1
} catch {
errorMessage = "Failed to load transcription history: \(error.localizedDescription)"
showError = true
hasMore = false
}
}

// MARK: - Mutations

/// Deletes a single record and reloads the first page on success.
func deleteRecord(_ record: TranscriptionRecord, search: String = "") async {
do {
try await dataManager.deleteRecord(record)
await loadRecords(reset: true, search: search)
} catch {
errorMessage = "Failed to delete record: \(error.localizedDescription)"
showError = true
}
}

/// Deletes every record and reloads the first page.
func clearAllRecords(search: String = "") async {
isLoading = true
do {
try await dataManager.deleteAllRecords()
} catch {
errorMessage = "Failed to clear all records: \(error.localizedDescription)"
showError = true
}
isLoading = false
await loadRecords(reset: true, search: search)
}
}
265 changes: 0 additions & 265 deletions Sources/Views/Dashboard/UsageDashboardView.swift

This file was deleted.

Loading
Loading