05/11 Implement contract selection when starting an inspection

This commit is contained in:
Nguyen Ngo
2026-05-11 15:54:49 -04:00
parent 606eda9521
commit c0b47ec132
@@ -1,8 +1,9 @@
// Views/Inspection/StartInspectionView.swift
// Views/Dashboard/StartInspectionView.swift
// ------------------------------------------
// Screen where the inspector chooses a template, facility, and optional area
// before starting a new inspection. Creates the LocalInspection record
// immediately so the form can be resumed if the app is backgrounded.
// Screen where the inspector chooses a template, contract, facility,
// and optional area before starting a new inspection.
// Creates the LocalInspection record immediately so the form can be
// resumed if the app is backgrounded.
import SwiftUI
import SwiftData
@@ -16,6 +17,7 @@ struct StartInspectionView: View {
@Query(sort: \LocalFacility.name) private var facilities: [LocalFacility]
@State private var selectedTemplateId: Int?
@State private var selectedProjectId: Int?
@State private var selectedFacilityId: Int?
@State private var selectedAreaId: Int?
@State private var navigateToExecution = false
@@ -24,17 +26,34 @@ struct StartInspectionView: View {
@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
// Derived lists
/// Unique contracts (projectId, projectName) sorted by name.
/// Facilities with projectId == 0 are grouped under "No Contract".
private var contracts: [(id: Int, name: String)] {
var seen = Set<Int>()
var result: [(id: Int, name: String)] = []
for f in facilities {
if seen.insert(f.projectId).inserted {
result.append((id: f.projectId, name: f.projectName))
}
}
return result.sorted { $0.name < $1.name }
}
/// Facilities that belong to the selected contract.
private var filteredFacilities: [LocalFacility] {
guard let pid = selectedProjectId else { return [] }
return facilities.filter { $0.projectId == pid }
}
private var selectedFacility: LocalFacility? {
facilities.first { $0.serverId == selectedFacilityId }
filteredFacilities.first { $0.serverId == selectedFacilityId }
}
private var areas: [LocalArea] {
@@ -91,16 +110,39 @@ struct StartInspectionView: View {
}
}
// Facility picker
// Contract picker
Section("Contract") {
if contracts.isEmpty {
Text("No contracts available. Sync required.")
.foregroundStyle(.secondary)
.font(.callout)
} else {
Picker("Contract", selection: $selectedProjectId) {
Text("Select a contract…").tag(Optional<Int>(nil))
ForEach(contracts, id: \.id) { contract in
Text(contract.name).tag(Optional(contract.id))
}
}
.pickerStyle(.navigationLink)
.onChange(of: selectedProjectId) {
// Reset downstream selections when contract changes
selectedFacilityId = nil
selectedAreaId = nil
}
}
}
// Facility picker (gated on contract selection)
if selectedProjectId != nil {
Section("Facility") {
if facilities.isEmpty {
Text("No facilities available. Sync required.")
if filteredFacilities.isEmpty {
Text("No facilities in this contract.")
.foregroundStyle(.secondary)
.font(.callout)
} else {
Picker("Facility", selection: $selectedFacilityId) {
Text("Select a facility…").tag(Optional<Int>(nil))
ForEach(facilities) { facility in
ForEach(filteredFacilities) { facility in
Text(facility.name).tag(Optional(facility.serverId))
}
}
@@ -110,10 +152,16 @@ struct StartInspectionView: View {
}
}
}
}
// Area picker (optional)
// Area picker (optional, gated on facility selection)
if selectedFacilityId != nil {
Section("Area (Optional)") {
if areas.isEmpty {
Text("No areas defined for this facility.")
.foregroundStyle(.secondary)
.font(.callout)
} else {
Picker("Area", selection: $selectedAreaId) {
Text("No specific area").tag(Optional<Int>(nil))
ForEach(areas) { area in
@@ -121,7 +169,7 @@ struct StartInspectionView: View {
}
}
.pickerStyle(.navigationLink)
.disabled(areas.isEmpty)
}
}
}
@@ -158,13 +206,31 @@ struct StartInspectionView: View {
}
}
.onAppear {
// Apply pre-fill from re-inspection launch
if let tid = preFillTemplateId { selectedTemplateId = tid }
if let fid = preFillFacilityId { selectedFacilityId = fid }
applyPreFill()
}
}
}
// Pre-fill
/// Apply pre-fill values from re-inspection launch.
/// Contract must be resolved first so the facility picker shows the
/// correct filtered list before selectedFacilityId is applied.
private func applyPreFill() {
if let tid = preFillTemplateId {
selectedTemplateId = tid
}
if let fid = preFillFacilityId {
// Resolve the contract that owns this facility
if let facility = facilities.first(where: { $0.serverId == fid }) {
selectedProjectId = facility.projectId
}
selectedFacilityId = fid
}
}
// Create inspection
private func startInspection() {
guard let templateId = selectedTemplateId,
let facilityId = selectedFacilityId
@@ -186,15 +252,16 @@ struct StartInspectionView: View {
// 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 }),
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
// Fetch the template schema to identify field types.
// Split into two statements avoids Xcode 26 #Predicate
// ambiguity under SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor.
let allTemplates = (try? context.fetch(FetchDescriptor<LocalTemplate>())) ?? []
let tid = templateId
let schema = (try? context.fetch(
FetchDescriptor<LocalTemplate>(predicate: #Predicate { $0.serverId == tid })
).first?.formSchema) ?? []
let schema = allTemplates.first(where: { $0.serverId == tid })?.formSchema ?? []
// Build the set of field IDs that must NOT be carried over
let excludeTypes: Set<String> = ["rating", "pass_fail", "image", "signature"]