Aug 27 - Fixed inspection lost photos recovery
This commit is contained in:
@@ -0,0 +1,102 @@
|
||||
// Models/LocalNotification.swift
|
||||
// ------------------------------
|
||||
// SwiftData model backing the in-app notification inbox.
|
||||
//
|
||||
// WHY THIS IS A LOCAL STORE, unlike every other server-backed list in the app.
|
||||
// `GET /api/v1/notifications` is a POLLER, not an inbox: it filters to
|
||||
// `is_read = False` and never sends the flag at all
|
||||
// (app/api/notifications.py :: list_notifications). So the instant a
|
||||
// notification is marked read the server stops returning it — there is no
|
||||
// response the iPad could render as "read", and before this every row in the
|
||||
// list was unread by definition, which is why they all looked identical.
|
||||
// Keeping our own copy is the only way to show read and unread side by side.
|
||||
//
|
||||
// The web already has a real inbox (routes/notifications.py :: index, with an
|
||||
// all / unread / read filter and paging). If a mobile equivalent is ever added,
|
||||
// this model should become a cache of it rather than the source of truth — see
|
||||
// rule 92.
|
||||
//
|
||||
// Scope: notifications are per-user, so this is purged on an identity change
|
||||
// like everything else (rule 88).
|
||||
|
||||
import Foundation
|
||||
import SwiftData
|
||||
|
||||
@Model
|
||||
final class LocalNotification {
|
||||
|
||||
/// Server notification id — stable identity, and the value
|
||||
/// `PATCH /api/v1/notifications/mark-read` takes.
|
||||
/// No inline default: a `.unique` key must not carry one (rule 63).
|
||||
@Attribute(.unique) var serverId: Int
|
||||
|
||||
var title: String
|
||||
var body: String
|
||||
|
||||
/// e.g. `issue_assigned`, `sla_alert`, `scheduled_inspection`. Nil for rows
|
||||
/// created before the server's phase17 migration added the column.
|
||||
var eventType: String?
|
||||
|
||||
/// Set when the notification refers to an issue — drives "View Issue".
|
||||
var issueId: Int?
|
||||
|
||||
/// Server `created_at`, parsed. Falls back to receipt time when the string
|
||||
/// cannot be parsed so ordering never collapses to a single instant.
|
||||
var createdAt: Date
|
||||
|
||||
/// When THIS device first saw it. Distinct from `createdAt`: a notification
|
||||
/// raised while the iPad was offline arrives late but keeps its real time.
|
||||
var receivedAt: Date
|
||||
|
||||
// ── Read state ────────────────────────────────────────────────────────
|
||||
// Set optimistically on tap / Mark All so the UI responds offline, then
|
||||
// pushed to the server. Read state is shared with the web (rule 92).
|
||||
|
||||
var isRead: Bool = false
|
||||
var readAt: Date?
|
||||
|
||||
/// True while this row's read state has not yet reached the server.
|
||||
/// Drained by `SyncManager.pushNotificationReadState()`.
|
||||
///
|
||||
/// Non-optional with an inline default so SwiftData migrates lightweight
|
||||
/// (rule 8).
|
||||
var readSyncPending: Bool = false
|
||||
|
||||
init(from api: APINotification) {
|
||||
self.serverId = api.id
|
||||
self.title = api.title
|
||||
self.body = api.body
|
||||
self.eventType = api.eventType
|
||||
self.issueId = api.issueId
|
||||
self.createdAt = SyncManager.isoFormatter.date(from: api.createdAt) ?? Date()
|
||||
self.receivedAt = Date()
|
||||
self.isRead = false
|
||||
self.readAt = nil
|
||||
self.readSyncPending = false
|
||||
}
|
||||
|
||||
/// Refresh the mutable text from a later poll.
|
||||
///
|
||||
/// Deliberately does NOT touch `isRead`. The endpoint only ever returns
|
||||
/// UNREAD rows, so being returned again carries no information about read
|
||||
/// state — it usually just means our mark-read has not been pushed yet.
|
||||
/// Clobbering it here would make a notification the inspector just opened
|
||||
/// pop straight back to unread.
|
||||
func update(from api: APINotification) {
|
||||
self.title = api.title
|
||||
self.body = api.body
|
||||
self.eventType = api.eventType
|
||||
self.issueId = api.issueId
|
||||
if let parsed = SyncManager.isoFormatter.date(from: api.createdAt) {
|
||||
self.createdAt = parsed
|
||||
}
|
||||
}
|
||||
|
||||
/// Mark read locally and queue the server push. Idempotent.
|
||||
func markRead() {
|
||||
guard !isRead else { return }
|
||||
isRead = true
|
||||
readAt = Date()
|
||||
readSyncPending = true
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user