05/05 Update the API to follow-up and resinspection
This commit is contained in:
@@ -157,6 +157,7 @@ actor APIClient {
|
|||||||
]
|
]
|
||||||
if let score = inspection.overallScore { body["overall_score"] = score }
|
if let score = inspection.overallScore { body["overall_score"] = score }
|
||||||
if let areaId = inspection.areaServerId { body["area_id"] = areaId }
|
if let areaId = inspection.areaServerId { body["area_id"] = areaId }
|
||||||
|
if let parentId = inspection.parentServerId { body["parent_inspection_id"] = parentId }
|
||||||
if !inspection.inspectorNotes.isEmpty { body["notes"] = inspection.inspectorNotes }
|
if !inspection.inspectorNotes.isEmpty { body["notes"] = inspection.inspectorNotes }
|
||||||
|
|
||||||
let fmt = ISO8601DateFormatter()
|
let fmt = ISO8601DateFormatter()
|
||||||
|
|||||||
@@ -284,6 +284,10 @@ struct APIInspectionSummary: Decodable, Identifiable, Sendable {
|
|||||||
let inspectionDate: String?
|
let inspectionDate: String?
|
||||||
let completedAt: String?
|
let completedAt: String?
|
||||||
let mobileLocalId: String?
|
let mobileLocalId: String?
|
||||||
|
// ── Follow-up / re-inspection ─────────────────────────────────────────
|
||||||
|
let followUpRequired: Bool
|
||||||
|
let followUpNote: String?
|
||||||
|
let parentInspectionId: Int?
|
||||||
|
|
||||||
var inspectionDateParsed: Date? {
|
var inspectionDateParsed: Date? {
|
||||||
guard let str = inspectionDate else { return nil }
|
guard let str = inspectionDate else { return nil }
|
||||||
@@ -304,11 +308,15 @@ struct APIInspectionSummary: Decodable, Identifiable, Sendable {
|
|||||||
inspectionDate = try? c.decode(String.self, forKey: .inspectionDate)
|
inspectionDate = try? c.decode(String.self, forKey: .inspectionDate)
|
||||||
completedAt = try? c.decode(String.self, forKey: .completedAt)
|
completedAt = try? c.decode(String.self, forKey: .completedAt)
|
||||||
mobileLocalId = try? c.decode(String.self, forKey: .mobileLocalId)
|
mobileLocalId = try? c.decode(String.self, forKey: .mobileLocalId)
|
||||||
|
followUpRequired = (try? c.decode(Bool.self, forKey: .followUpRequired)) ?? false
|
||||||
|
followUpNote = try? c.decode(String.self, forKey: .followUpNote)
|
||||||
|
parentInspectionId = try? c.decode(Int.self, forKey: .parentInspectionId)
|
||||||
}
|
}
|
||||||
private enum CodingKeys: String, CodingKey {
|
private enum CodingKeys: String, CodingKey {
|
||||||
case id, templateId, templateName, facilityId, facilityName
|
case id, templateId, templateName, facilityId, facilityName
|
||||||
case areaId, areaName, status, overallScore
|
case areaId, areaName, status, overallScore
|
||||||
case inspectionDate, completedAt, mobileLocalId
|
case inspectionDate, completedAt, mobileLocalId
|
||||||
|
case followUpRequired, followUpNote, parentInspectionId
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -39,6 +39,18 @@ final class LocalInspection {
|
|||||||
var syncErrorMessage: String?
|
var syncErrorMessage: String?
|
||||||
var syncRetryCount: Int
|
var syncRetryCount: Int
|
||||||
|
|
||||||
|
// ── Follow-up / re-inspection (populated from server after sync) ───────
|
||||||
|
/// Set by director/admin on the web app; signals this inspection needs a follow-up.
|
||||||
|
@Attribute var followUpRequired: Bool = false
|
||||||
|
/// Optional note explaining what the follow-up should address.
|
||||||
|
var followUpNote: String?
|
||||||
|
/// Server ID of the parent inspection this record is a re-inspection of.
|
||||||
|
var parentServerId: Int?
|
||||||
|
/// Local UUID of the parent LocalInspection — set at creation, always available
|
||||||
|
/// regardless of whether the parent has synced. Used to clear the parent's
|
||||||
|
/// followUpRequired badge without relying on parentServerId being non-nil.
|
||||||
|
var parentLocalId: String?
|
||||||
|
|
||||||
// ── Relationships ──────────────────────────────────────────────────────
|
// ── Relationships ──────────────────────────────────────────────────────
|
||||||
@Relationship(deleteRule: .cascade) var pendingPhotos: [PendingPhoto]
|
@Relationship(deleteRule: .cascade) var pendingPhotos: [PendingPhoto]
|
||||||
@Relationship(deleteRule: .cascade) var localIssues: [LocalIssue]
|
@Relationship(deleteRule: .cascade) var localIssues: [LocalIssue]
|
||||||
@@ -66,6 +78,10 @@ final class LocalInspection {
|
|||||||
self.syncStatus = "pending"
|
self.syncStatus = "pending"
|
||||||
self.syncErrorMessage = nil
|
self.syncErrorMessage = nil
|
||||||
self.syncRetryCount = 0
|
self.syncRetryCount = 0
|
||||||
|
self.followUpRequired = false
|
||||||
|
self.followUpNote = nil
|
||||||
|
self.parentServerId = nil
|
||||||
|
self.parentLocalId = nil
|
||||||
self.pendingPhotos = []
|
self.pendingPhotos = []
|
||||||
self.localIssues = []
|
self.localIssues = []
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -134,6 +134,19 @@ class SyncManager: ObservableObject {
|
|||||||
inspection.serverId = inspectionId
|
inspection.serverId = inspectionId
|
||||||
inspection.syncStatus = "synced"
|
inspection.syncStatus = "synced"
|
||||||
inspection.status = "synced"
|
inspection.status = "synced"
|
||||||
|
|
||||||
|
// ── Clear follow-up flag on parent ────────────────────────
|
||||||
|
// Use parentLocalId (always set at creation) rather than
|
||||||
|
// parentServerId (nil until parent syncs) so the badge clears
|
||||||
|
// regardless of whether the parent has been synced yet.
|
||||||
|
if let parentLocalId = inspection.parentLocalId {
|
||||||
|
let allInspections = try? context.fetch(FetchDescriptor<LocalInspection>())
|
||||||
|
if let parent = allInspections?.first(where: { $0.localId == parentLocalId }) {
|
||||||
|
parent.followUpRequired = false
|
||||||
|
parent.followUpNote = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
try? context.save()
|
try? context.save()
|
||||||
|
|
||||||
} catch {
|
} catch {
|
||||||
|
|||||||
@@ -294,6 +294,19 @@ struct InspectionRowView: View {
|
|||||||
.foregroundStyle(score >= 80 ? .green : score >= 60 ? .orange : .red)
|
.foregroundStyle(score >= 80 ? .green : score >= 60 ? .orange : .red)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// ── Follow-up badge ────────────────────────────────────────────
|
||||||
|
if inspection.followUpRequired {
|
||||||
|
HStack(spacing: 4) {
|
||||||
|
Image(systemName: "exclamationmark.arrow.circlepath")
|
||||||
|
.font(.caption2)
|
||||||
|
Text("Follow-up Required")
|
||||||
|
.font(.caption2.bold())
|
||||||
|
}
|
||||||
|
.padding(.horizontal, 8).padding(.vertical, 3)
|
||||||
|
.background(Color.orange.opacity(0.15))
|
||||||
|
.foregroundStyle(.orange)
|
||||||
|
.clipShape(Capsule())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.padding(.vertical, 4)
|
.padding(.vertical, 4)
|
||||||
}
|
}
|
||||||
@@ -337,6 +350,8 @@ struct CompletedInspectionView: View {
|
|||||||
let inspection: LocalInspection
|
let inspection: LocalInspection
|
||||||
@Environment(\.modelContext) private var context
|
@Environment(\.modelContext) private var context
|
||||||
|
|
||||||
|
@State private var showReInspect = false
|
||||||
|
|
||||||
private var templateName: String {
|
private var templateName: String {
|
||||||
let id = inspection.templateServerId
|
let id = inspection.templateServerId
|
||||||
return (try? context.fetch(
|
return (try? context.fetch(
|
||||||
@@ -347,6 +362,52 @@ struct CompletedInspectionView: View {
|
|||||||
var body: some View {
|
var body: some View {
|
||||||
ScrollView {
|
ScrollView {
|
||||||
VStack(alignment: .leading, spacing: 16) {
|
VStack(alignment: .leading, spacing: 16) {
|
||||||
|
|
||||||
|
// ── Follow-up required banner ──────────────────────────────
|
||||||
|
if inspection.followUpRequired {
|
||||||
|
HStack(alignment: .top, spacing: 12) {
|
||||||
|
Image(systemName: "exclamationmark.arrow.circlepath")
|
||||||
|
.foregroundStyle(.orange)
|
||||||
|
.font(.title3)
|
||||||
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
|
Text("Follow-up Inspection Required")
|
||||||
|
.font(.callout.bold())
|
||||||
|
.foregroundStyle(.orange)
|
||||||
|
if let note = inspection.followUpNote, !note.isEmpty {
|
||||||
|
Text(note)
|
||||||
|
.font(.callout)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
|
Button {
|
||||||
|
showReInspect = true
|
||||||
|
} label: {
|
||||||
|
Label("Start Re-inspection", systemImage: "arrow.uturn.right.circle.fill")
|
||||||
|
.font(.callout.bold())
|
||||||
|
}
|
||||||
|
.buttonStyle(.borderedProminent)
|
||||||
|
.tint(.orange)
|
||||||
|
.padding(.top, 4)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(14)
|
||||||
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
|
.background(Color.orange.opacity(0.1))
|
||||||
|
.clipShape(RoundedRectangle(cornerRadius: 12))
|
||||||
|
.padding(.horizontal)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Is a re-inspection — parent link ───────────────────────
|
||||||
|
if let parentId = inspection.parentServerId {
|
||||||
|
HStack(spacing: 10) {
|
||||||
|
Image(systemName: "arrow.uturn.right.circle")
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
Text("Re-inspection of inspection #\(parentId)")
|
||||||
|
.font(.callout)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
|
.padding(.horizontal)
|
||||||
|
}
|
||||||
|
|
||||||
GroupBox {
|
GroupBox {
|
||||||
VStack(alignment: .leading, spacing: 8) {
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
if let score = inspection.overallScore {
|
if let score = inspection.overallScore {
|
||||||
@@ -404,6 +465,14 @@ struct CompletedInspectionView: View {
|
|||||||
}
|
}
|
||||||
.navigationTitle(templateName)
|
.navigationTitle(templateName)
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
|
.sheet(isPresented: $showReInspect) {
|
||||||
|
StartInspectionView(
|
||||||
|
preFillTemplateId: inspection.templateServerId,
|
||||||
|
preFillFacilityId: inspection.facilityServerId,
|
||||||
|
parentServerId: inspection.serverId,
|
||||||
|
parentLocalId: inspection.localId
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -347,6 +347,13 @@ struct ExecuteInspectionView: View {
|
|||||||
inspection.status = "completed"
|
inspection.status = "completed"
|
||||||
inspection.completedAt = Date()
|
inspection.completedAt = Date()
|
||||||
inspection.syncStatus = "pending"
|
inspection.syncStatus = "pending"
|
||||||
|
|
||||||
|
// ── Clear follow-up flag on parent immediately ────────────────────
|
||||||
|
// Do this at submit time rather than relying solely on SyncManager,
|
||||||
|
// so the badge disappears the moment the inspector taps Submit —
|
||||||
|
// regardless of connectivity or sync timing.
|
||||||
|
clearParentFollowUpFlag()
|
||||||
|
|
||||||
try? context.save()
|
try? context.save()
|
||||||
|
|
||||||
isSubmitting = false
|
isSubmitting = false
|
||||||
@@ -366,6 +373,49 @@ struct ExecuteInspectionView: View {
|
|||||||
dismiss()
|
dismiss()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Find the parent LocalInspection and clear its followUpRequired flag.
|
||||||
|
/// Tries parentLocalId first (set for new re-inspections), then falls back
|
||||||
|
/// to parentServerId (set after parent has synced), then as a last resort
|
||||||
|
/// matches by template+facility for re-inspections created before these
|
||||||
|
/// fields were added (parentLocalId=nil, parentServerId=nil).
|
||||||
|
private func clearParentFollowUpFlag() {
|
||||||
|
var parent: LocalInspection?
|
||||||
|
|
||||||
|
// Primary: match by the parent's localId UUID (always available if set)
|
||||||
|
if let lid = inspection.parentLocalId {
|
||||||
|
parent = try? context.fetch(
|
||||||
|
FetchDescriptor<LocalInspection>(predicate: #Predicate { $0.localId == lid })
|
||||||
|
).first
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback 1: match by server ID (available once parent has synced)
|
||||||
|
if parent == nil, let sid = inspection.parentServerId {
|
||||||
|
parent = try? context.fetch(FetchDescriptor<LocalInspection>())
|
||||||
|
.first(where: { $0.serverId == sid })
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback 2: for stale re-inspections created before parentLocalId existed,
|
||||||
|
// find any LocalInspection with the same template+facility that has
|
||||||
|
// followUpRequired=true and is not this inspection itself.
|
||||||
|
if parent == nil {
|
||||||
|
let tid = inspection.templateServerId
|
||||||
|
let fid = inspection.facilityServerId
|
||||||
|
let selfId = inspection.localId
|
||||||
|
parent = try? context.fetch(FetchDescriptor<LocalInspection>())
|
||||||
|
.first(where: {
|
||||||
|
$0.templateServerId == tid &&
|
||||||
|
$0.facilityServerId == fid &&
|
||||||
|
$0.followUpRequired == true &&
|
||||||
|
$0.localId != selfId
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if let parent {
|
||||||
|
parent.followUpRequired = false
|
||||||
|
parent.followUpNote = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ── Photo Handling ────────────────────────────────────────────────────
|
// ── Photo Handling ────────────────────────────────────────────────────
|
||||||
|
|
||||||
private func handlePhotoSelected(localPath: String, field: [String: Any]) {
|
private func handlePhotoSelected(localPath: String, field: [String: Any]) {
|
||||||
|
|||||||
@@ -23,6 +23,16 @@ struct StartInspectionView: View {
|
|||||||
|
|
||||||
@EnvironmentObject private var auth: AuthManager
|
@EnvironmentObject private var auth: AuthManager
|
||||||
|
|
||||||
|
// ── Pre-fill for re-inspections ───────────────────────────────────────
|
||||||
|
/// When launching from a "Start Re-inspection" button, these are set so
|
||||||
|
/// the form opens with the parent's template and facility pre-selected.
|
||||||
|
var preFillTemplateId: Int? = nil
|
||||||
|
var preFillFacilityId: Int? = nil
|
||||||
|
var parentServerId: Int? = nil
|
||||||
|
/// Local UUID of the parent — always available, used by SyncManager to
|
||||||
|
/// clear the parent's followUpRequired badge after the re-inspection syncs.
|
||||||
|
var parentLocalId: String? = nil
|
||||||
|
|
||||||
private var selectedFacility: LocalFacility? {
|
private var selectedFacility: LocalFacility? {
|
||||||
facilities.first { $0.serverId == selectedFacilityId }
|
facilities.first { $0.serverId == selectedFacilityId }
|
||||||
}
|
}
|
||||||
@@ -38,6 +48,24 @@ struct StartInspectionView: View {
|
|||||||
var body: some View {
|
var body: some View {
|
||||||
NavigationStack {
|
NavigationStack {
|
||||||
Form {
|
Form {
|
||||||
|
// ── Re-inspection notice ───────────────────────────────────
|
||||||
|
if parentServerId != nil {
|
||||||
|
Section {
|
||||||
|
HStack(spacing: 10) {
|
||||||
|
Image(systemName: "arrow.uturn.right.circle.fill")
|
||||||
|
.foregroundStyle(.orange)
|
||||||
|
VStack(alignment: .leading, spacing: 2) {
|
||||||
|
Text("Re-inspection")
|
||||||
|
.font(.callout.bold())
|
||||||
|
Text("This will be linked to inspection #\(parentServerId!).")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.vertical, 4)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ── Template picker ────────────────────────────────────────
|
// ── Template picker ────────────────────────────────────────
|
||||||
Section("Inspection Template") {
|
Section("Inspection Template") {
|
||||||
if templates.isEmpty {
|
if templates.isEmpty {
|
||||||
@@ -104,7 +132,12 @@ struct StartInspectionView: View {
|
|||||||
} label: {
|
} label: {
|
||||||
HStack {
|
HStack {
|
||||||
Spacer()
|
Spacer()
|
||||||
Label("Start Inspection", systemImage: "play.circle.fill")
|
Label(
|
||||||
|
parentServerId != nil ? "Start Re-inspection" : "Start Inspection",
|
||||||
|
systemImage: parentServerId != nil
|
||||||
|
? "arrow.uturn.right.circle.fill"
|
||||||
|
: "play.circle.fill"
|
||||||
|
)
|
||||||
.font(.headline)
|
.font(.headline)
|
||||||
Spacer()
|
Spacer()
|
||||||
}
|
}
|
||||||
@@ -112,7 +145,7 @@ struct StartInspectionView: View {
|
|||||||
.disabled(!canStart)
|
.disabled(!canStart)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.navigationTitle("New Inspection")
|
.navigationTitle(parentServerId != nil ? "Re-inspection" : "New Inspection")
|
||||||
.navigationBarTitleDisplayMode(.large)
|
.navigationBarTitleDisplayMode(.large)
|
||||||
.toolbar {
|
.toolbar {
|
||||||
ToolbarItem(placement: .cancellationAction) {
|
ToolbarItem(placement: .cancellationAction) {
|
||||||
@@ -124,6 +157,11 @@ struct StartInspectionView: View {
|
|||||||
ExecuteInspectionView(inspection: inspection)
|
ExecuteInspectionView(inspection: inspection)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.onAppear {
|
||||||
|
// Apply pre-fill from re-inspection launch
|
||||||
|
if let tid = preFillTemplateId { selectedTemplateId = tid }
|
||||||
|
if let fid = preFillFacilityId { selectedFacilityId = fid }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -138,6 +176,50 @@ struct StartInspectionView: View {
|
|||||||
areaServerId: selectedAreaId,
|
areaServerId: selectedAreaId,
|
||||||
inspectorUserId: auth.currentUserId
|
inspectorUserId: auth.currentUserId
|
||||||
)
|
)
|
||||||
|
// Link to parent if this is a re-inspection
|
||||||
|
inspection.parentServerId = parentServerId
|
||||||
|
inspection.parentLocalId = parentLocalId
|
||||||
|
|
||||||
|
// ── Pre-fill from parent (mirrors web app behaviour) ───────────────
|
||||||
|
// Copy non-scoring field values from the parent inspection so the
|
||||||
|
// inspector doesn't re-enter static data. Scoring fields (rating,
|
||||||
|
// pass_fail) and media fields (image, signature) are always left blank
|
||||||
|
// so every scoreable item must be re-evaluated fresh.
|
||||||
|
if let parentId = parentServerId {
|
||||||
|
let allInspections = try? context.fetch(FetchDescriptor<LocalInspection>())
|
||||||
|
if let parent = allInspections?.first(where: { $0.serverId == parentId }),
|
||||||
|
!parent.formData.isEmpty {
|
||||||
|
|
||||||
|
// Fetch the template schema to identify field types
|
||||||
|
let tid = templateId
|
||||||
|
let schema = (try? context.fetch(
|
||||||
|
FetchDescriptor<LocalTemplate>(predicate: #Predicate { $0.serverId == tid })
|
||||||
|
).first?.formSchema) ?? []
|
||||||
|
|
||||||
|
// Build the set of field IDs that must NOT be carried over
|
||||||
|
let excludeTypes: Set<String> = ["rating", "pass_fail", "image", "signature"]
|
||||||
|
var excludeIds = Set<String>()
|
||||||
|
for field in schema {
|
||||||
|
if let type_ = field["type"] as? String, excludeTypes.contains(type_),
|
||||||
|
let id = field["id"] {
|
||||||
|
excludeIds.insert("\(id)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy all parent values except excluded fields
|
||||||
|
let parentData = parent.formData
|
||||||
|
var prefilled: [String: Any] = [:]
|
||||||
|
for (key, value) in parentData {
|
||||||
|
if !excludeIds.contains(key) {
|
||||||
|
prefilled[key] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !prefilled.isEmpty {
|
||||||
|
inspection.formData = prefilled
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
context.insert(inspection)
|
context.insert(inspection)
|
||||||
try? context.save()
|
try? context.save()
|
||||||
|
|
||||||
|
|||||||
@@ -182,6 +182,19 @@ struct HistoryRowView: View {
|
|||||||
.clipShape(Capsule())
|
.clipShape(Capsule())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// ── Follow-up badge ────────────────────────────────────────────
|
||||||
|
if inspection.followUpRequired {
|
||||||
|
HStack(spacing: 4) {
|
||||||
|
Image(systemName: "exclamationmark.arrow.circlepath")
|
||||||
|
.font(.caption2)
|
||||||
|
Text("Follow-up Required")
|
||||||
|
.font(.caption2.bold())
|
||||||
|
}
|
||||||
|
.padding(.horizontal, 8).padding(.vertical, 3)
|
||||||
|
.background(Color.orange.opacity(0.15))
|
||||||
|
.foregroundStyle(.orange)
|
||||||
|
.clipShape(Capsule())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.padding(.vertical, 4)
|
.padding(.vertical, 4)
|
||||||
}
|
}
|
||||||
@@ -198,6 +211,7 @@ struct HistoryDetailView: View {
|
|||||||
let inspection: APIInspectionSummary
|
let inspection: APIInspectionSummary
|
||||||
|
|
||||||
@Environment(\.modelContext) private var context
|
@Environment(\.modelContext) private var context
|
||||||
|
@State private var showReInspect = false
|
||||||
|
|
||||||
// Look up the local copy by mobileLocalId — present only for this-device submissions
|
// Look up the local copy by mobileLocalId — present only for this-device submissions
|
||||||
private var localCopy: LocalInspection? {
|
private var localCopy: LocalInspection? {
|
||||||
@@ -230,6 +244,46 @@ struct HistoryDetailView: View {
|
|||||||
ScrollView {
|
ScrollView {
|
||||||
VStack(alignment: .leading, spacing: 16) {
|
VStack(alignment: .leading, spacing: 16) {
|
||||||
|
|
||||||
|
// ── Follow-up required banner ──────────────────────────────
|
||||||
|
if inspection.followUpRequired {
|
||||||
|
HStack(alignment: .top, spacing: 12) {
|
||||||
|
Image(systemName: "exclamationmark.arrow.circlepath")
|
||||||
|
.foregroundStyle(.orange)
|
||||||
|
.font(.title3)
|
||||||
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
|
Text("Follow-up Inspection Required")
|
||||||
|
.font(.callout.bold())
|
||||||
|
.foregroundStyle(.orange)
|
||||||
|
if let note = inspection.followUpNote, !note.isEmpty {
|
||||||
|
Text(note)
|
||||||
|
.font(.callout)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
|
Button {
|
||||||
|
showReInspect = true
|
||||||
|
} label: {
|
||||||
|
Label("Start Re-inspection", systemImage: "arrow.uturn.right.circle.fill")
|
||||||
|
.font(.callout.bold())
|
||||||
|
}
|
||||||
|
.buttonStyle(.borderedProminent)
|
||||||
|
.tint(.orange)
|
||||||
|
.padding(.top, 4)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(14)
|
||||||
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
|
.background(Color.orange.opacity(0.1))
|
||||||
|
.clipShape(RoundedRectangle(cornerRadius: 12))
|
||||||
|
.padding(.horizontal, 24)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Is a re-inspection — parent link ───────────────────────
|
||||||
|
if let parentId = inspection.parentInspectionId {
|
||||||
|
infoRow(icon: "arrow.uturn.right.circle",
|
||||||
|
text: "Re-inspection of inspection #\(parentId)")
|
||||||
|
.padding(.horizontal, 24)
|
||||||
|
}
|
||||||
|
|
||||||
// ── Summary card ───────────────────────────────────────────
|
// ── Summary card ───────────────────────────────────────────
|
||||||
summaryCard
|
summaryCard
|
||||||
|
|
||||||
@@ -266,6 +320,37 @@ struct HistoryDetailView: View {
|
|||||||
.background(Color(.systemBackground))
|
.background(Color(.systemBackground))
|
||||||
.navigationTitle(inspection.templateName)
|
.navigationTitle(inspection.templateName)
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
|
.onAppear {
|
||||||
|
syncFollowUpToLocalCopy()
|
||||||
|
}
|
||||||
|
.sheet(isPresented: $showReInspect) {
|
||||||
|
StartInspectionView(
|
||||||
|
preFillTemplateId: inspection.templateId,
|
||||||
|
preFillFacilityId: inspection.facilityId,
|
||||||
|
parentServerId: inspection.id,
|
||||||
|
parentLocalId: inspection.mobileLocalId
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Write the server's follow-up fields back onto the local SwiftData copy
|
||||||
|
/// so that MyInspectionsView and CompletedInspectionView reflect the latest state.
|
||||||
|
private func syncFollowUpToLocalCopy() {
|
||||||
|
guard let copy = localCopy else { return }
|
||||||
|
var changed = false
|
||||||
|
if copy.followUpRequired != inspection.followUpRequired {
|
||||||
|
copy.followUpRequired = inspection.followUpRequired
|
||||||
|
changed = true
|
||||||
|
}
|
||||||
|
if copy.followUpNote != inspection.followUpNote {
|
||||||
|
copy.followUpNote = inspection.followUpNote
|
||||||
|
changed = true
|
||||||
|
}
|
||||||
|
if copy.parentServerId != inspection.parentInspectionId {
|
||||||
|
copy.parentServerId = inspection.parentInspectionId
|
||||||
|
changed = true
|
||||||
|
}
|
||||||
|
if changed { try? context.save() }
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Summary card ───────────────────────────────────────────────────────
|
// ── Summary card ───────────────────────────────────────────────────────
|
||||||
|
|||||||
Reference in New Issue
Block a user