05/15 Update: show issue's images, push notifications
This commit is contained in:
@@ -219,6 +219,36 @@ actor APIClient {
|
||||
return result.status
|
||||
}
|
||||
|
||||
// ── Notification polling (Phase C) ────────────────────────────────────
|
||||
|
||||
/// Fetch notifications, optionally scoped to those created after `since`.
|
||||
func fetchNotifications(since: Date? = nil) async throws -> [APINotification] {
|
||||
var ep = "/api/v1/notifications"
|
||||
if let since {
|
||||
let fmt = DateFormatter()
|
||||
fmt.dateFormat = "yyyy-MM-dd'T'HH:mm:ss"
|
||||
ep += "?since=\(fmt.string(from: since))"
|
||||
}
|
||||
let result: APINotificationsResponseData = try await request(ep)
|
||||
return result.notifications
|
||||
}
|
||||
|
||||
/// Mark the given notification IDs as read on the server.
|
||||
func markNotificationsRead(ids: [Int]) async throws {
|
||||
guard !ids.isEmpty else { return }
|
||||
let _: APIMarkReadResponseData = try await request(
|
||||
"/api/v1/notifications/mark-read",
|
||||
method: "PATCH",
|
||||
body: ["ids": ids]
|
||||
)
|
||||
}
|
||||
|
||||
/// Fetch issues assigned to the current user from the server.
|
||||
func fetchAssignedIssues() async throws -> [APIAssignedIssue] {
|
||||
let result: APIAssignedIssuesResponseData = try await request("/api/v1/issues")
|
||||
return result.issues
|
||||
}
|
||||
|
||||
// ── Token Refresh ─────────────────────────────────────────────────────
|
||||
|
||||
private func refreshAccessToken() async -> Bool {
|
||||
|
||||
@@ -378,3 +378,101 @@ struct APIIssueStatusUpdate: Decodable, Sendable {
|
||||
}
|
||||
private enum CodingKeys: String, CodingKey { case issueId, status }
|
||||
}
|
||||
|
||||
// ── Notification polling (Phase C) ────────────────────────────────────────────
|
||||
|
||||
struct APINotification: Decodable, Identifiable, Sendable {
|
||||
let id: Int
|
||||
let title: String
|
||||
let body: String
|
||||
let eventType: String?
|
||||
let issueId: Int?
|
||||
let createdAt: String
|
||||
|
||||
nonisolated init(from decoder: any Decoder) throws {
|
||||
let c = try decoder.container(keyedBy: CodingKeys.self)
|
||||
id = try c.decode(Int.self, forKey: .id)
|
||||
title = try c.decode(String.self, forKey: .title)
|
||||
body = try c.decode(String.self, forKey: .body)
|
||||
eventType = try? c.decode(String.self, forKey: .eventType)
|
||||
issueId = try? c.decode(Int.self, forKey: .issueId)
|
||||
createdAt = try c.decode(String.self, forKey: .createdAt)
|
||||
}
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case id, title, body, eventType, issueId, createdAt
|
||||
}
|
||||
}
|
||||
|
||||
struct APINotificationsResponseData: Decodable, Sendable {
|
||||
let notifications: [APINotification]
|
||||
let count: Int
|
||||
|
||||
nonisolated init(from decoder: any Decoder) throws {
|
||||
let c = try decoder.container(keyedBy: CodingKeys.self)
|
||||
notifications = try c.decode([APINotification].self, forKey: .notifications)
|
||||
count = try c.decode(Int.self, forKey: .count)
|
||||
}
|
||||
private enum CodingKeys: String, CodingKey { case notifications, count }
|
||||
}
|
||||
|
||||
struct APIMarkReadResponseData: Decodable, Sendable {
|
||||
let marked: Int
|
||||
|
||||
nonisolated init(from decoder: any Decoder) throws {
|
||||
let c = try decoder.container(keyedBy: CodingKeys.self)
|
||||
marked = try c.decode(Int.self, forKey: .marked)
|
||||
}
|
||||
private enum CodingKeys: String, CodingKey { case marked }
|
||||
}
|
||||
|
||||
// ── Assigned issues list (Phase C) ────────────────────────────────────────────
|
||||
|
||||
struct APIAssignedIssue: Decodable, Identifiable, Sendable {
|
||||
let id: Int
|
||||
let status: String
|
||||
let severity: String
|
||||
let description: String
|
||||
let assignedTo: Int?
|
||||
let facilityId: Int?
|
||||
let facilityName: String?
|
||||
let reportedAt: String?
|
||||
let mobileLocalId: String?
|
||||
let photoPath: String? // primary issue photo (relative server path)
|
||||
let resultPhotos: [String] // resolution photos (relative server paths)
|
||||
|
||||
nonisolated init(from decoder: any Decoder) throws {
|
||||
let c = try decoder.container(keyedBy: CodingKeys.self)
|
||||
id = try c.decode(Int.self, forKey: .id)
|
||||
status = try c.decode(String.self, forKey: .status)
|
||||
severity = try c.decode(String.self, forKey: .severity)
|
||||
description = try c.decode(String.self, forKey: .description)
|
||||
assignedTo = try? c.decode(Int.self, forKey: .assignedTo)
|
||||
facilityId = try? c.decode(Int.self, forKey: .facilityId)
|
||||
facilityName = try? c.decode(String.self, forKey: .facilityName)
|
||||
reportedAt = try? c.decode(String.self, forKey: .reportedAt)
|
||||
mobileLocalId = try? c.decode(String.self, forKey: .mobileLocalId)
|
||||
photoPath = try? c.decode(String.self, forKey: .photoPath)
|
||||
resultPhotos = (try? c.decode([String].self, forKey: .resultPhotos)) ?? []
|
||||
}
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case id, status, severity, description, assignedTo
|
||||
case facilityId, facilityName, reportedAt, mobileLocalId
|
||||
case photoPath, resultPhotos
|
||||
}
|
||||
}
|
||||
|
||||
struct APIAssignedIssuesResponseData: Decodable, Sendable {
|
||||
let issues: [APIAssignedIssue]
|
||||
let total: Int
|
||||
let limit: Int
|
||||
let offset: Int
|
||||
|
||||
nonisolated init(from decoder: any Decoder) throws {
|
||||
let c = try decoder.container(keyedBy: CodingKeys.self)
|
||||
issues = try c.decode([APIAssignedIssue].self, forKey: .issues)
|
||||
total = try c.decode(Int.self, forKey: .total)
|
||||
limit = try c.decode(Int.self, forKey: .limit)
|
||||
offset = try c.decode(Int.self, forKey: .offset)
|
||||
}
|
||||
private enum CodingKeys: String, CodingKey { case issues, total, limit, offset }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user