05/11 Implement contract selection when starting an inspection
This commit is contained in:
@@ -1,8 +1,9 @@
|
|||||||
// Views/Inspection/StartInspectionView.swift
|
// Views/Dashboard/StartInspectionView.swift
|
||||||
// ------------------------------------------
|
// ------------------------------------------
|
||||||
// Screen where the inspector chooses a template, facility, and optional area
|
// Screen where the inspector chooses a template, contract, facility,
|
||||||
// before starting a new inspection. Creates the LocalInspection record
|
// and optional area before starting a new inspection.
|
||||||
// immediately so the form can be resumed if the app is backgrounded.
|
// Creates the LocalInspection record immediately so the form can be
|
||||||
|
// resumed if the app is backgrounded.
|
||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import SwiftData
|
import SwiftData
|
||||||
@@ -16,25 +17,43 @@ struct StartInspectionView: View {
|
|||||||
@Query(sort: \LocalFacility.name) private var facilities: [LocalFacility]
|
@Query(sort: \LocalFacility.name) private var facilities: [LocalFacility]
|
||||||
|
|
||||||
@State private var selectedTemplateId: Int?
|
@State private var selectedTemplateId: Int?
|
||||||
|
@State private var selectedProjectId: Int?
|
||||||
@State private var selectedFacilityId: Int?
|
@State private var selectedFacilityId: Int?
|
||||||
@State private var selectedAreaId: Int?
|
@State private var selectedAreaId: Int?
|
||||||
@State private var navigateToExecution = false
|
@State private var navigateToExecution = false
|
||||||
@State private var createdInspection: LocalInspection?
|
@State private var createdInspection: LocalInspection?
|
||||||
|
|
||||||
@EnvironmentObject private var auth: AuthManager
|
@EnvironmentObject private var auth: AuthManager
|
||||||
|
|
||||||
// ── Pre-fill for re-inspections ───────────────────────────────────────
|
// ── 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 preFillTemplateId: Int? = nil
|
||||||
var preFillFacilityId: Int? = nil
|
var preFillFacilityId: Int? = nil
|
||||||
var parentServerId: 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
|
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? {
|
private var selectedFacility: LocalFacility? {
|
||||||
facilities.first { $0.serverId == selectedFacilityId }
|
filteredFacilities.first { $0.serverId == selectedFacilityId }
|
||||||
}
|
}
|
||||||
|
|
||||||
private var areas: [LocalArea] {
|
private var areas: [LocalArea] {
|
||||||
@@ -91,37 +110,66 @@ struct StartInspectionView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Facility picker ────────────────────────────────────────
|
// ── Contract picker ────────────────────────────────────────
|
||||||
Section("Facility") {
|
Section("Contract") {
|
||||||
if facilities.isEmpty {
|
if contracts.isEmpty {
|
||||||
Text("No facilities available. Sync required.")
|
Text("No contracts available. Sync required.")
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
.font(.callout)
|
.font(.callout)
|
||||||
} else {
|
} else {
|
||||||
Picker("Facility", selection: $selectedFacilityId) {
|
Picker("Contract", selection: $selectedProjectId) {
|
||||||
Text("Select a facility…").tag(Optional<Int>(nil))
|
Text("Select a contract…").tag(Optional<Int>(nil))
|
||||||
ForEach(facilities) { facility in
|
ForEach(contracts, id: \.id) { contract in
|
||||||
Text(facility.name).tag(Optional(facility.serverId))
|
Text(contract.name).tag(Optional(contract.id))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.pickerStyle(.navigationLink)
|
.pickerStyle(.navigationLink)
|
||||||
.onChange(of: selectedFacilityId) {
|
.onChange(of: selectedProjectId) {
|
||||||
selectedAreaId = nil // reset area when facility changes
|
// Reset downstream selections when contract changes
|
||||||
|
selectedFacilityId = nil
|
||||||
|
selectedAreaId = nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Area picker (optional) ─────────────────────────────────
|
// ── Facility picker (gated on contract selection) ──────────
|
||||||
if selectedFacilityId != nil {
|
if selectedProjectId != nil {
|
||||||
Section("Area (Optional)") {
|
Section("Facility") {
|
||||||
Picker("Area", selection: $selectedAreaId) {
|
if filteredFacilities.isEmpty {
|
||||||
Text("No specific area").tag(Optional<Int>(nil))
|
Text("No facilities in this contract.")
|
||||||
ForEach(areas) { area in
|
.foregroundStyle(.secondary)
|
||||||
Text(area.name).tag(Optional(area.serverId))
|
.font(.callout)
|
||||||
|
} else {
|
||||||
|
Picker("Facility", selection: $selectedFacilityId) {
|
||||||
|
Text("Select a facility…").tag(Optional<Int>(nil))
|
||||||
|
ForEach(filteredFacilities) { facility in
|
||||||
|
Text(facility.name).tag(Optional(facility.serverId))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.pickerStyle(.navigationLink)
|
||||||
|
.onChange(of: selectedFacilityId) {
|
||||||
|
selectedAreaId = nil // reset area when facility changes
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.pickerStyle(.navigationLink)
|
}
|
||||||
.disabled(areas.isEmpty)
|
}
|
||||||
|
|
||||||
|
// ── 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
|
||||||
|
Text(area.name).tag(Optional(area.serverId))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.pickerStyle(.navigationLink)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -158,13 +206,31 @@ struct StartInspectionView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onAppear {
|
.onAppear {
|
||||||
// Apply pre-fill from re-inspection launch
|
applyPreFill()
|
||||||
if let tid = preFillTemplateId { selectedTemplateId = tid }
|
|
||||||
if let fid = preFillFacilityId { selectedFacilityId = fid }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── 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() {
|
private func startInspection() {
|
||||||
guard let templateId = selectedTemplateId,
|
guard let templateId = selectedTemplateId,
|
||||||
let facilityId = selectedFacilityId
|
let facilityId = selectedFacilityId
|
||||||
@@ -186,15 +252,16 @@ struct StartInspectionView: View {
|
|||||||
// pass_fail) and media fields (image, signature) are always left blank
|
// pass_fail) and media fields (image, signature) are always left blank
|
||||||
// so every scoreable item must be re-evaluated fresh.
|
// so every scoreable item must be re-evaluated fresh.
|
||||||
if let parentId = parentServerId {
|
if let parentId = parentServerId {
|
||||||
let allInspections = try? context.fetch(FetchDescriptor<LocalInspection>())
|
let allInspections = (try? context.fetch(FetchDescriptor<LocalInspection>())) ?? []
|
||||||
if let parent = allInspections?.first(where: { $0.serverId == parentId }),
|
if let parent = allInspections.first(where: { $0.serverId == parentId }),
|
||||||
!parent.formData.isEmpty {
|
!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 tid = templateId
|
||||||
let schema = (try? context.fetch(
|
let schema = allTemplates.first(where: { $0.serverId == tid })?.formSchema ?? []
|
||||||
FetchDescriptor<LocalTemplate>(predicate: #Predicate { $0.serverId == tid })
|
|
||||||
).first?.formSchema) ?? []
|
|
||||||
|
|
||||||
// Build the set of field IDs that must NOT be carried over
|
// Build the set of field IDs that must NOT be carried over
|
||||||
let excludeTypes: Set<String> = ["rating", "pass_fail", "image", "signature"]
|
let excludeTypes: Set<String> = ["rating", "pass_fail", "image", "signature"]
|
||||||
|
|||||||
Reference in New Issue
Block a user