From c0b47ec1324b47b5457bc2316f4c86f2d7525708 Mon Sep 17 00:00:00 2001 From: Nguyen Ngo Date: Mon, 11 May 2026 15:54:49 -0400 Subject: [PATCH] 05/11 Implement contract selection when starting an inspection --- .../Views/Dashboard/StartInspectionView.swift | 143 +++++++++++++----- 1 file changed, 105 insertions(+), 38 deletions(-) diff --git a/JanitorialQC/Views/Dashboard/StartInspectionView.swift b/JanitorialQC/Views/Dashboard/StartInspectionView.swift index 9f2c670..4d37ea8 100644 --- a/JanitorialQC/Views/Dashboard/StartInspectionView.swift +++ b/JanitorialQC/Views/Dashboard/StartInspectionView.swift @@ -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,25 +17,43 @@ 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 selectedAreaId: Int? @State private var navigateToExecution = false @State private var createdInspection: LocalInspection? @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() + 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,37 +110,66 @@ struct StartInspectionView: View { } } - // ── Facility picker ──────────────────────────────────────── - Section("Facility") { - if facilities.isEmpty { - Text("No facilities available. Sync required.") + // ── Contract picker ──────────────────────────────────────── + Section("Contract") { + if contracts.isEmpty { + Text("No contracts available. Sync required.") .foregroundStyle(.secondary) .font(.callout) } else { - Picker("Facility", selection: $selectedFacilityId) { - Text("Select a facility…").tag(Optional(nil)) - ForEach(facilities) { facility in - Text(facility.name).tag(Optional(facility.serverId)) + Picker("Contract", selection: $selectedProjectId) { + Text("Select a contract…").tag(Optional(nil)) + ForEach(contracts, id: \.id) { contract in + Text(contract.name).tag(Optional(contract.id)) } } .pickerStyle(.navigationLink) - .onChange(of: selectedFacilityId) { - selectedAreaId = nil // reset area when facility changes + .onChange(of: selectedProjectId) { + // Reset downstream selections when contract changes + selectedFacilityId = nil + selectedAreaId = nil } } } - // ── Area picker (optional) ───────────────────────────────── - if selectedFacilityId != nil { - Section("Area (Optional)") { - Picker("Area", selection: $selectedAreaId) { - Text("No specific area").tag(Optional(nil)) - ForEach(areas) { area in - Text(area.name).tag(Optional(area.serverId)) + // ── Facility picker (gated on contract selection) ────────── + if selectedProjectId != nil { + Section("Facility") { + if filteredFacilities.isEmpty { + Text("No facilities in this contract.") + .foregroundStyle(.secondary) + .font(.callout) + } else { + Picker("Facility", selection: $selectedFacilityId) { + Text("Select a facility…").tag(Optional(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(nil)) + ForEach(areas) { area in + Text(area.name).tag(Optional(area.serverId)) + } + } + .pickerStyle(.navigationLink) + } } } @@ -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()) - if let parent = allInspections?.first(where: { $0.serverId == parentId }), + let allInspections = (try? context.fetch(FetchDescriptor())) ?? [] + 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())) ?? [] let tid = templateId - let schema = (try? context.fetch( - FetchDescriptor(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 = ["rating", "pass_fail", "image", "signature"]