Jul 13 - Update codes to catch up with the web app updates: scheduled inspection and issue's handler
This commit is contained in:
@@ -0,0 +1,228 @@
|
||||
// Models/LocalIssue.swift
|
||||
// -----------------------
|
||||
// SwiftData model for issues flagged during an offline inspection.
|
||||
|
||||
import Foundation
|
||||
import SwiftData
|
||||
|
||||
@Model
|
||||
final class LocalIssue {
|
||||
|
||||
@Attribute(.unique) var localId: String
|
||||
var serverId: Int?
|
||||
|
||||
var inspectionLocalId: String // references LocalInspection.localId
|
||||
var facilityServerId: Int // facility this issue belongs to
|
||||
/// Server ID of the area this issue was flagged in. Set when flagged during
|
||||
/// an inspection that has an area selected. Nil for standalone issues.
|
||||
var areaServerId: Int?
|
||||
var severity: String // "low" | "medium" | "high" | "critical"
|
||||
var issueDescription: String
|
||||
var issueStatus: String = "open" // server status: "open" | "in_progress" | "resolved" | "pending_verification"
|
||||
/// JSON-encoded array of absolute local file paths, e.g. ["/var/.../photo1.jpg", ...]
|
||||
var photoLocalPathsJSON: String = "[]"
|
||||
/// JSON-encoded array of server paths after upload, e.g. ["uploads/issue_photos/abc.jpg", ...]
|
||||
var photoServerPathsJSON: String = "[]"
|
||||
/// JSON-encoded array of resolution photo server paths (issue_result_photos bucket).
|
||||
/// Mirrors Issue.result_photos on the server — shown under "Resolution Details".
|
||||
var resultPhotoServerPathsJSON: String = "[]"
|
||||
|
||||
// Shared coders — JSONDecoder/Encoder init is expensive (parses locale and
|
||||
// calendar info). Allocating them inside computed property getters means
|
||||
// a new instance per access; on a list showing 50 issues each with two
|
||||
// JSON-backed arrays that's 200 allocations per render pass. Static
|
||||
// instances are created once and reused for the lifetime of the app.
|
||||
private static let jsonDecoder = JSONDecoder()
|
||||
private static let jsonEncoder = JSONEncoder()
|
||||
|
||||
// Lightweight decode cache — avoids re-parsing identical JSON strings.
|
||||
// SwiftData may call the getter multiple times per render pass (once for
|
||||
// isEmpty, once for count, once for ForEach). Caching the last-decoded
|
||||
// value by JSON string identity means the JSON parse only happens when
|
||||
// the underlying data actually changes.
|
||||
// @Transient tells SwiftData not to persist these — they're in-memory only.
|
||||
// Lightweight decode cache — split into key+value pairs because SwiftData's
|
||||
// @Transient macro does not support tuple types. Two separate @Transient
|
||||
// properties per cache entry achieve the same result with no schema impact.
|
||||
@Transient private var _cachedLocalKey: String = ""
|
||||
@Transient private var _cachedLocalValue: [String] = []
|
||||
@Transient private var _cachedServerKey: String = ""
|
||||
@Transient private var _cachedServerValue: [String] = []
|
||||
@Transient private var _cachedResultKey: String = ""
|
||||
@Transient private var _cachedResultValue: [String] = []
|
||||
|
||||
/// Decoded local photo paths (up to 5)
|
||||
var photoLocalPaths: [String] {
|
||||
get {
|
||||
if _cachedLocalKey == photoLocalPathsJSON, !_cachedLocalKey.isEmpty {
|
||||
return _cachedLocalValue
|
||||
}
|
||||
let decoded = (try? Self.jsonDecoder.decode([String].self,
|
||||
from: Data(photoLocalPathsJSON.utf8))) ?? []
|
||||
_cachedLocalKey = photoLocalPathsJSON
|
||||
_cachedLocalValue = decoded
|
||||
return decoded
|
||||
}
|
||||
set {
|
||||
let encoded = (try? String(data: Self.jsonEncoder.encode(newValue),
|
||||
encoding: .utf8)) ?? "[]"
|
||||
photoLocalPathsJSON = encoded
|
||||
_cachedLocalKey = encoded
|
||||
_cachedLocalValue = newValue
|
||||
}
|
||||
}
|
||||
|
||||
/// Decoded server photo paths
|
||||
var photoServerPaths: [String] {
|
||||
get {
|
||||
if _cachedServerKey == photoServerPathsJSON, !_cachedServerKey.isEmpty {
|
||||
return _cachedServerValue
|
||||
}
|
||||
let decoded = (try? Self.jsonDecoder.decode([String].self,
|
||||
from: Data(photoServerPathsJSON.utf8))) ?? []
|
||||
_cachedServerKey = photoServerPathsJSON
|
||||
_cachedServerValue = decoded
|
||||
return decoded
|
||||
}
|
||||
set {
|
||||
let encoded = (try? String(data: Self.jsonEncoder.encode(newValue),
|
||||
encoding: .utf8)) ?? "[]"
|
||||
photoServerPathsJSON = encoded
|
||||
_cachedServerKey = encoded
|
||||
_cachedServerValue = newValue
|
||||
}
|
||||
}
|
||||
|
||||
/// Decoded resolution photo server paths (issue_result_photos bucket).
|
||||
/// Shown under "Resolution Details" — mirrors Issue.result_photos on the web.
|
||||
var resultPhotoServerPaths: [String] {
|
||||
get {
|
||||
if _cachedResultKey == resultPhotoServerPathsJSON, !_cachedResultKey.isEmpty {
|
||||
return _cachedResultValue
|
||||
}
|
||||
let decoded = (try? Self.jsonDecoder.decode([String].self,
|
||||
from: Data(resultPhotoServerPathsJSON.utf8))) ?? []
|
||||
_cachedResultKey = resultPhotoServerPathsJSON
|
||||
_cachedResultValue = decoded
|
||||
return decoded
|
||||
}
|
||||
set {
|
||||
let encoded = (try? String(data: Self.jsonEncoder.encode(newValue),
|
||||
encoding: .utf8)) ?? "[]"
|
||||
resultPhotoServerPathsJSON = encoded
|
||||
_cachedResultKey = encoded
|
||||
_cachedResultValue = newValue
|
||||
}
|
||||
}
|
||||
|
||||
var createdAt: Date
|
||||
var syncStatus: String // "pending" | "synced" | "failed"
|
||||
var syncRetryCount: Int
|
||||
var syncErrorMessage: String?
|
||||
|
||||
// ── Phase A additions — persisted from server response ────────────────
|
||||
// All new String?/Date? fields default to nil; SwiftData lightweight migration
|
||||
// supports nil-default optional properties without a migration plan.
|
||||
|
||||
/// Facility display name cached from the server response. Used when the
|
||||
/// local facility reference cache has been cleared (Settings → Clear Cache).
|
||||
var facilityNameCache: String?
|
||||
|
||||
/// Server-side reported_at timestamp. More accurate than createdAt for
|
||||
/// server-pulled issues because createdAt falls back to device time when
|
||||
/// the issue was created offline.
|
||||
var serverReportedAt: Date?
|
||||
|
||||
/// Resolution notes added by web staff after fixing the issue.
|
||||
var resultNotes: String?
|
||||
|
||||
/// Timestamp when a director/admin verified the fix.
|
||||
var verifiedAt: Date?
|
||||
|
||||
/// Note left by the verifier.
|
||||
var verificationNote: String?
|
||||
|
||||
/// Display name of the user who originally reported this issue.
|
||||
var reportedByName: String?
|
||||
|
||||
/// Name of the area this issue was flagged in (e.g. "Main Lobby").
|
||||
/// Set from server response; nil for standalone issues without area context.
|
||||
var areaNameCache: String?
|
||||
|
||||
/// Display name of the user currently assigned to this issue.
|
||||
/// Nil when unassigned. Updated on every pullAssignedIssues().
|
||||
var assignedToName: String?
|
||||
|
||||
// ── Handler ("Handled By", phase35) ───────────────────────────────────
|
||||
// Who resolves the issue: internal (our staff) | facility (facility's own
|
||||
// staff) | vendor (external contractor). Synced from the server; the
|
||||
// inspector may also set it from Issue Detail. nil-default optionals →
|
||||
// SwiftData lightweight migration safe.
|
||||
var handlerType: String? // "internal" | "facility" | "vendor"
|
||||
var handlerLabel: String? // human-readable label from server
|
||||
var facilityHandlerName: String?
|
||||
var facilityHandlerContact: String?
|
||||
var facilityHandlerNotes: String?
|
||||
var vendorName: String?
|
||||
var vendorContact: String?
|
||||
var vendorNotes: String?
|
||||
|
||||
// Explicit inverse declared so SwiftData has an unambiguous relationship
|
||||
// graph at schema-build time. Without it the relationship is implicit,
|
||||
// which can cause migration warnings or incorrect cascade behaviour on some
|
||||
// SwiftData versions. The deleteRule is .nullify (default) — deleting the
|
||||
// parent inspection cascades via LocalInspection.localIssues; this side
|
||||
// only nullifies the back-pointer.
|
||||
@Relationship(deleteRule: .nullify, inverse: \LocalInspection.localIssues)
|
||||
var inspection: LocalInspection?
|
||||
|
||||
init(
|
||||
inspectionLocalId: String,
|
||||
facilityServerId: Int,
|
||||
severity: String,
|
||||
description: String
|
||||
) {
|
||||
self.localId = UUID().uuidString
|
||||
self.serverId = nil
|
||||
self.inspectionLocalId = inspectionLocalId
|
||||
self.facilityServerId = facilityServerId
|
||||
self.areaServerId = nil
|
||||
self.severity = severity
|
||||
self.issueDescription = description
|
||||
self.issueStatus = "open"
|
||||
self.photoLocalPathsJSON = "[]"
|
||||
self.photoServerPathsJSON = "[]"
|
||||
self.resultPhotoServerPathsJSON = "[]"
|
||||
self.createdAt = Date()
|
||||
self.syncStatus = "pending"
|
||||
self.syncRetryCount = 0
|
||||
self.syncErrorMessage = nil
|
||||
// Phase A fields — nil by default
|
||||
self.facilityNameCache = nil
|
||||
self.serverReportedAt = nil
|
||||
self.resultNotes = nil
|
||||
self.verifiedAt = nil
|
||||
self.verificationNote = nil
|
||||
self.reportedByName = nil
|
||||
self.areaNameCache = nil
|
||||
self.assignedToName = nil
|
||||
// Handler fields — nil by default
|
||||
self.handlerType = nil
|
||||
self.handlerLabel = nil
|
||||
self.facilityHandlerName = nil
|
||||
self.facilityHandlerContact = nil
|
||||
self.facilityHandlerNotes = nil
|
||||
self.vendorName = nil
|
||||
self.vendorContact = nil
|
||||
self.vendorNotes = nil
|
||||
}
|
||||
|
||||
var severityColor: String {
|
||||
switch severity {
|
||||
case "critical": return "red"
|
||||
case "high": return "orange"
|
||||
case "medium": return "yellow"
|
||||
default: return "blue"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
// Models/LocalScheduledInspection.swift
|
||||
// --------------------------------------
|
||||
// SwiftData model for planned/recurring inspection assignments (phase36).
|
||||
//
|
||||
// Read-only reference data pulled from the server (GET /api/v1/scheduled-inspections)
|
||||
// and refreshed by SyncManager.pullScheduledInspections() — never created or
|
||||
// mutated on device. Surfaced in the "Scheduled" section on the Dashboard and
|
||||
// My Inspections. Tapping "Start" opens the normal new-inspection flow with the
|
||||
// facility + template preselected; the schedule lifecycle (fulfil / roll-forward)
|
||||
// stays server-driven.
|
||||
//
|
||||
// All non-optional stored properties carry explicit inline defaults so SwiftData
|
||||
// lightweight migration can add the new table without a migration plan.
|
||||
|
||||
import Foundation
|
||||
import SwiftData
|
||||
|
||||
@Model
|
||||
final class LocalScheduledInspection {
|
||||
|
||||
/// Server ID of the ScheduledInspection row — stable unique identity.
|
||||
@Attribute(.unique) var serverId: Int = 0
|
||||
|
||||
var facilityServerId: Int = 0
|
||||
var facilityName: String = ""
|
||||
var templateServerId: Int = 0
|
||||
var templateName: String = ""
|
||||
var inspectorId: Int? = nil
|
||||
|
||||
var frequency: String = "once" // once | daily | weekly | monthly
|
||||
var frequencyLabel: String = "" // human-readable label from server
|
||||
|
||||
/// Raw ISO date string "YYYY-MM-DD" from the server (display fallback).
|
||||
var dueDateString: String = ""
|
||||
/// Parsed due date — used for @Query sorting. Nil if the string was absent.
|
||||
var nextDue: Date? = nil
|
||||
|
||||
var isOverdue: Bool = false
|
||||
var notes: String? = nil
|
||||
|
||||
/// Last time this row was refreshed from the server pull.
|
||||
var updatedAt: Date = Date()
|
||||
|
||||
init(serverId: Int) {
|
||||
self.serverId = serverId
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user