05/27 Update functionalities
This commit is contained in:
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user