// Views/Dashboard/StartInspectionView.swift // ------------------------------------------ // 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 struct StartInspectionView: View { @Environment(\.modelContext) private var context @Environment(\.dismiss) private var dismiss @Query(sort: \LocalTemplate.name) private var templates: [LocalTemplate] @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 @State private var createdInspection: LocalInspection? @EnvironmentObject private var auth: AuthManager // ── Pre-fill for re-inspections ─────────────────────────────────────── var preFillTemplateId: Int? = nil var preFillFacilityId: Int? = nil var parentServerId: Int? = 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() 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? { filteredFacilities.first { $0.serverId == selectedFacilityId } } private var areas: [LocalArea] { selectedFacility?.areas.sorted { $0.name < $1.name } ?? [] } private var canStart: Bool { selectedTemplateId != nil && selectedFacilityId != nil } var body: some View { NavigationStack { 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 ──────────────────────────────────────── Section("Inspection Template") { if templates.isEmpty { Text("No templates available. Sync required.") .foregroundStyle(.secondary) .font(.callout) } else { Picker("Template", selection: $selectedTemplateId) { Text("Select a template…").tag(Optional(nil)) ForEach(templates) { template in VStack(alignment: .leading) { Text(template.name) if !template.frequency.isEmpty { Text(template.frequencyLabel) .font(.caption) .foregroundStyle(.secondary) } } .tag(Optional(template.serverId)) } } .pickerStyle(.navigationLink) } } // ── 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(nil)) ForEach(contracts, id: \.id) { contract in Text(contract.name).tag(Optional(contract.id)) } } .pickerStyle(.navigationLink) .onChange(of: selectedProjectId) { // Reset downstream selections when the user changes contract. // Do NOT reset if the current facilityId already belongs to // the newly selected contract — this covers the pre-fill path // where applyPreFill() sets both projectId and facilityId and // the onChange fires before facilityId is applied, wiping it. let facilityBelongsToContract = facilities.contains { $0.serverId == selectedFacilityId && $0.projectId == selectedProjectId } if !facilityBelongsToContract { selectedFacilityId = nil selectedAreaId = nil } } } } // ── 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 } } } } // ── 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) } } } // ── Start button ─────────────────────────────────────────── Section { Button { startInspection() } label: { HStack { Spacer() Label( parentServerId != nil ? "Start Re-inspection" : "Start Inspection", systemImage: parentServerId != nil ? "arrow.uturn.right.circle.fill" : "play.circle.fill" ) .font(.headline) Spacer() } } .disabled(!canStart) } } .navigationTitle(parentServerId != nil ? "Re-inspection" : "New Inspection") .navigationBarTitleDisplayMode(.large) .toolbar { ToolbarItem(placement: .cancellationAction) { Button("Cancel") { dismiss() } } } .navigationDestination(isPresented: $navigateToExecution) { if let inspection = createdInspection { ExecuteInspectionView(inspection: inspection) } } .onAppear { 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 else { return } let inspection = LocalInspection( templateServerId: templateId, facilityServerId: facilityId, areaServerId: selectedAreaId, 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())) ?? [] if let parent = allInspections.first(where: { $0.serverId == parentId }), !parent.formData.isEmpty { // 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 = 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"] var excludeIds = Set() 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) try? context.save() createdInspection = inspection navigateToExecution = true } }