05/27 Update functionalities

This commit is contained in:
Nguyen Ngo
2026-05-27 17:26:52 -04:00
parent ea7e523f25
commit 7d05c3862d
7 changed files with 800 additions and 27 deletions
+67 -2
View File
@@ -21,6 +21,14 @@ class SyncManager: ObservableObject {
@Published var lastSyncAt: Date?
@Published var syncError: String?
@Published var pendingCount = 0
/// Dashboard KPI stats fetched from the server. Nil until first successful fetch.
@Published var dashboardStats: APIDashboardStats?
/// Count of notifications received since last resetNotificationPoller().
/// Incremented on each poll that returns new items; reset to 0 on logout.
@Published var unreadNotificationCount = 0
/// The most recent batch of notifications (up to 50) for the in-app inbox.
/// Replaced entirely on each successful poll; empty until first fetch.
@Published var recentNotifications: [APINotification] = []
// Dependencies
@@ -104,10 +112,17 @@ class SyncManager: ObservableObject {
/// Called on logout so the next login starts a clean fetch.
func resetNotificationPoller() {
lastNotificationFetch = nil
lastNotificationFetch = nil
unreadNotificationCount = 0
recentNotifications = []
stopPollTask()
}
/// Call when the user opens the NotificationsView to clear the badge.
func markNotificationsViewed() {
unreadNotificationCount = 0
}
// Notification polling
func pollNotifications() async {
@@ -121,6 +136,10 @@ class SyncManager: ObservableObject {
deliverLocalNotification(n)
}
// Update in-app inbox state
recentNotifications = notifications + recentNotifications.prefix(50 - notifications.count)
unreadNotificationCount += notifications.count
// Update the cursor to the newest notification's timestamp
let dates = notifications.compactMap { Self.isoFormatter.date(from: $0.createdAt) }
if let newest = dates.max() {
@@ -183,6 +202,9 @@ class SyncManager: ObservableObject {
// and follow-up requests as soon as the app goes online.
await pollNotifications()
// Fetch dashboard KPIs best-effort, non-fatal on failure.
await fetchDashboardStats()
updatePendingCount(context: context)
lastSyncAt = Date()
}
@@ -470,6 +492,21 @@ class SyncManager: ObservableObject {
existing.severity = api.severity
existing.issueDescription = api.description
if let fid = api.facilityId { existing.facilityServerId = fid }
// Cache facility name so IssueDetailView works when local
// facility reference cache has been cleared (Settings Clear Cache).
if let fn = api.facilityName, !fn.isEmpty {
existing.facilityNameCache = fn
}
// Phase A resolution details from web staff
existing.resultNotes = api.resultNotes
existing.verificationNote = api.verificationNote
existing.reportedByName = api.reportedByName
existing.areaNameCache = api.areaName
existing.assignedToName = api.assignedToName
if let vts = api.verifiedAt,
let date = Self.isoFormatter.date(from: vts) {
existing.verifiedAt = date
}
// Refresh photos in case they were added after first pull
// photoServerPaths = evidence photos only (photo_path + mobile_photo_paths).
// result_photos are resolution photos shown separately on the web,
@@ -489,6 +526,18 @@ class SyncManager: ObservableObject {
local.serverId = api.id
local.issueStatus = api.status
local.syncStatus = "synced" // never re-submit
// Cache facility name for offline display
local.facilityNameCache = api.facilityName
// Phase A resolution details from web staff
local.resultNotes = api.resultNotes
local.verificationNote = api.verificationNote
local.reportedByName = api.reportedByName
local.areaNameCache = api.areaName
local.assignedToName = api.assignedToName
if let vts = api.verifiedAt,
let date = Self.isoFormatter.date(from: vts) {
local.verifiedAt = date
}
// Store server photos so IssueDetailView can show them
// photoServerPaths = evidence photos only (photo_path + mobile_photo_paths).
var serverPaths: [String] = []
@@ -497,7 +546,8 @@ class SyncManager: ObservableObject {
local.photoServerPaths = serverPaths
if let ts = api.reportedAt,
let date = Self.isoFormatter.date(from: ts) {
local.createdAt = date
local.createdAt = date
local.serverReportedAt = date // accurate server timestamp
}
context.insert(local)
}
@@ -528,4 +578,19 @@ class SyncManager: ObservableObject {
// Non-fatal IssuesListView still shows device-created issues
}
}
// Dashboard Stats
// Best-effort fetch a network failure silently leaves dashboardStats nil
// so the UI falls back to a placeholder card. Never blocks the sync pipeline.
func fetchDashboardStats() async {
guard isOnline, AuthManager.shared.isAuthenticated else { return }
do {
dashboardStats = try await APIClient.shared.fetchDashboardStats()
} catch APIError.notAuthenticated {
// Let AuthManager handle session expiry
} catch {
// Non-fatal stale stats stay visible until next successful fetch
}
}
}