130 lines
5.5 KiB
Swift
130 lines
5.5 KiB
Swift
// 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()
|
|
}
|
|
}
|