summaryrefslogtreecommitdiff
path: root/Hutch/Views/Tickets/TrackerListViewModel.swift
diff options
context:
space:
mode:
authorChristian Cleberg <[email protected]>2026-03-17 23:19:43 -0500
committerChristian Cleberg <[email protected]>2026-03-17 23:19:43 -0500
commit32ad6cab8d58d99ebd8a28e8fa6e6f4e587cb1e5 (patch)
treeee36d421d704e508d5617d803b4c8cbdb4804fe1 /Hutch/Views/Tickets/TrackerListViewModel.swift
parent8f2057c53e9009c2529c9c4849c914666c0e4b40 (diff)
downloadhutch-32ad6cab8d58d99ebd8a28e8fa6e6f4e587cb1e5.tar.gz
hutch-32ad6cab8d58d99ebd8a28e8fa6e6f4e587cb1e5.tar.bz2
hutch-32ad6cab8d58d99ebd8a28e8fa6e6f4e587cb1e5.zip
v1.0
Diffstat (limited to 'Hutch/Views/Tickets/TrackerListViewModel.swift')
-rw-r--r--Hutch/Views/Tickets/TrackerListViewModel.swift166
1 files changed, 166 insertions, 0 deletions
diff --git a/Hutch/Views/Tickets/TrackerListViewModel.swift b/Hutch/Views/Tickets/TrackerListViewModel.swift
new file mode 100644
index 0000000..9704071
--- /dev/null
+++ b/Hutch/Views/Tickets/TrackerListViewModel.swift
@@ -0,0 +1,166 @@
+import Foundation
+
+// MARK: - Response types (file-private to avoid @MainActor Decodable issues)
+
+private struct TrackersResponse: Decodable, Sendable {
+ let trackers: TrackersPage
+}
+
+private struct TrackersPage: Decodable, Sendable {
+ let results: [TrackerSummary]
+ let cursor: String?
+}
+
+// MARK: - View Model
+
+@Observable
+@MainActor
+final class TrackerListViewModel {
+
+ private(set) var trackers: [TrackerSummary] = []
+ private(set) var isLoading = false
+ private(set) var isLoadingMore = false
+ private(set) var isCreatingTracker = false
+ var error: String?
+
+ private var cursor: String?
+ private var hasMore = true
+ private let client: SRHTClient
+
+ init(client: SRHTClient) {
+ self.client = client
+ }
+
+ // MARK: - Query
+
+ private static let query = """
+ query trackers($cursor: Cursor) {
+ trackers(cursor: $cursor) {
+ results {
+ id
+ rid
+ name
+ description
+ visibility
+ updated
+ owner { canonicalName }
+ }
+ cursor
+ }
+ }
+ """
+
+ private static let createTrackerMutation = """
+ mutation createTracker($name: String!, $visibility: Visibility!, $description: String) {
+ createTracker(name: $name, visibility: $visibility, description: $description) {
+ id
+ rid
+ name
+ description
+ visibility
+ updated
+ owner { canonicalName }
+ }
+ }
+ """
+
+ // MARK: - Public API
+
+ func loadTrackers() async {
+ isLoading = true
+ error = nil
+ cursor = nil
+ hasMore = true
+
+ do {
+ let page = try await fetchPage(cursor: nil)
+ trackers = page.results
+ cursor = page.cursor
+ hasMore = page.cursor != nil
+ } catch {
+ self.error = error.localizedDescription
+ }
+
+ isLoading = false
+ }
+
+ func loadMoreIfNeeded(currentItem: TrackerSummary) async {
+ guard let last = trackers.last,
+ last.id == currentItem.id,
+ hasMore,
+ !isLoadingMore else {
+ return
+ }
+
+ isLoadingMore = true
+
+ do {
+ let page = try await fetchPage(cursor: cursor)
+ trackers.append(contentsOf: page.results)
+ cursor = page.cursor
+ hasMore = page.cursor != nil
+ } catch {
+ self.error = error.localizedDescription
+ }
+
+ isLoadingMore = false
+ }
+
+ func createTracker(name: String, description: String, visibility: Visibility) async -> TrackerSummary? {
+ guard !isCreatingTracker else { return nil }
+
+ let trimmedName = name.trimmingCharacters(in: .whitespacesAndNewlines)
+ guard !trimmedName.isEmpty else {
+ error = "Enter a tracker name."
+ return nil
+ }
+
+ isCreatingTracker = true
+ error = nil
+ defer { isCreatingTracker = false }
+
+ var variables: [String: any Sendable] = [
+ "name": trimmedName,
+ "visibility": visibility.rawValue
+ ]
+ let trimmedDescription = description.trimmingCharacters(in: .whitespacesAndNewlines)
+ if !trimmedDescription.isEmpty {
+ variables["description"] = trimmedDescription
+ }
+
+ do {
+ let result = try await client.execute(
+ service: .todo,
+ query: Self.createTrackerMutation,
+ variables: variables,
+ responseType: CreateTrackerResponse.self
+ )
+ let tracker = result.createTracker
+ trackers.insert(tracker, at: 0)
+ return tracker
+ } catch {
+ self.error = "Couldn’t create the tracker. \(error.localizedDescription)"
+ return nil
+ }
+ }
+
+ // MARK: - Private
+
+ private func fetchPage(cursor: String?) async throws -> TrackersPage {
+ var variables: [String: any Sendable] = [:]
+ if let cursor {
+ variables["cursor"] = cursor
+ }
+ let result = try await client.execute(
+ service: .todo,
+ query: Self.query,
+ variables: variables.isEmpty ? nil : variables,
+ responseType: TrackersResponse.self
+ )
+ return result.trackers
+ }
+
+ private struct CreateTrackerResponse: Decodable, Sendable {
+ let createTracker: TrackerSummary
+ }
+}