// 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. // // Follows the same pattern as LocalFacility / LocalArea: a `.unique` serverId // WITHOUT an inline default (a default on the unique key breaks @Model's // PersistentModel conformance) and a full init(from:)/update(from:) pair. import Foundation import SwiftData @Model final class LocalScheduledInspection { /// Server ID of the ScheduledInspection row — stable unique identity. @Attribute(.unique) var serverId: Int var facilityServerId: Int var facilityName: String var templateServerId: Int var templateName: String var inspectorId: Int? var frequency: String // once | daily | weekly | monthly var frequencyLabel: String /// Raw server date string "YYYY-MM-DD" — sortable (ISO strings sort /// chronologically) and the source for the parsed `nextDue`. var dueDateString: String var isOverdue: Bool var notes: String? /// Set on device the moment an inspection fulfilling this schedule is /// submitted, so the SCHEDULED lists hide the row immediately — online or /// offline — without waiting for the round trip. /// /// Purely a display flag. The schedule lifecycle stays server-driven: the /// next successful pull calls `update(from:)`, which clears this and writes /// the authoritative `next_due_date`. If the submission never lands, the /// server still reports the schedule as due and the row simply comes back — /// self-healing. /// /// This exists instead of deleting the row. A recurring schedule is /// returned by the server *again* after it rolls forward, so deleting meant /// delete-then-reinsert against an `@Attribute(.unique)` key on every /// completion, and the reinserted row did not reliably carry the new due /// date. Flagging keeps `update(from:)` — the path that has always worked — /// as the only way a cached row's dates ever change. /// /// Non-optional with an inline default, so SwiftData migrates lightweight /// (CLAUDE.md rule 12): existing rows read as `false`, no app reinstall. var fulfilledLocally: Bool = false /// Manager-authored instructions for this occurrence, normalised. /// /// The wire field, the server model attribute and the DB column are all /// still `notes` — only the user-facing wording changed to "Instructions" /// (web form label + both iPad surfaces). Renaming the stored property would /// break `init(from:)`/`update(from:)` against `APIScheduledInspection.notes` /// for no benefit, so the rename lives here. /// /// Returns nil for nil, empty, or whitespace-only text so callers can guard /// with a single `if let` instead of repeating the emptiness check. /// Computed, not stored — no SwiftData schema change. var instructions: String? { guard let t = notes?.trimmingCharacters(in: .whitespacesAndNewlines), !t.isEmpty else { return nil } return t } /// Last time this row was refreshed from the server pull. var updatedAt: Date /// Parsed due date for display. Computed properties are not persisted by /// SwiftData; sort on `dueDateString` (not this) in @Query. var nextDue: Date? { Self.dateOnlyFormatter.date(from: dueDateString) } private static let dateOnlyFormatter: DateFormatter = { let f = DateFormatter() f.locale = Locale(identifier: "en_US_POSIX") f.dateFormat = "yyyy-MM-dd" return f }() init(from api: APIScheduledInspection) { self.serverId = api.id self.facilityServerId = api.facilityId self.facilityName = api.facilityName ?? "" self.templateServerId = api.templateId self.templateName = api.templateName ?? "" self.inspectorId = api.inspectorId self.frequency = api.frequency self.frequencyLabel = api.frequencyLabel ?? "" self.dueDateString = api.nextDueDate ?? "" self.isOverdue = api.isOverdue self.notes = api.notes self.fulfilledLocally = false self.updatedAt = Date() } func update(from api: APIScheduledInspection) { self.facilityServerId = api.facilityId self.facilityName = api.facilityName ?? "" self.templateServerId = api.templateId self.templateName = api.templateName ?? "" self.inspectorId = api.inspectorId self.frequency = api.frequency self.frequencyLabel = api.frequencyLabel ?? "" self.dueDateString = api.nextDueDate ?? "" self.isOverdue = api.isOverdue self.notes = api.notes // The server is authoritative. Being returned by the pull at all means // this schedule is live again — for a recurring one, on its NEXT // occurrence — so any local "just did it" flag is stale by definition. self.fulfilledLocally = false self.updatedAt = Date() } }