230 lines
10 KiB
Swift
230 lines
10 KiB
Swift
// Views/Inspection/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.
|
|
|
|
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 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 ───────────────────────────────────────
|
|
/// 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? {
|
|
facilities.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<Int>(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)
|
|
}
|
|
}
|
|
|
|
// ── Facility picker ────────────────────────────────────────
|
|
Section("Facility") {
|
|
if facilities.isEmpty {
|
|
Text("No facilities available. Sync required.")
|
|
.foregroundStyle(.secondary)
|
|
.font(.callout)
|
|
} else {
|
|
Picker("Facility", selection: $selectedFacilityId) {
|
|
Text("Select a facility…").tag(Optional<Int>(nil))
|
|
ForEach(facilities) { facility in
|
|
Text(facility.name).tag(Optional(facility.serverId))
|
|
}
|
|
}
|
|
.pickerStyle(.navigationLink)
|
|
.onChange(of: selectedFacilityId) {
|
|
selectedAreaId = nil // reset area when facility changes
|
|
}
|
|
}
|
|
}
|
|
|
|
// ── Area picker (optional) ─────────────────────────────────
|
|
if selectedFacilityId != nil {
|
|
Section("Area (Optional)") {
|
|
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)
|
|
.disabled(areas.isEmpty)
|
|
}
|
|
}
|
|
|
|
// ── 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 {
|
|
// Apply pre-fill from re-inspection launch
|
|
if let tid = preFillTemplateId { selectedTemplateId = tid }
|
|
if let fid = preFillFacilityId { selectedFacilityId = fid }
|
|
}
|
|
}
|
|
}
|
|
|
|
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<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)
|
|
try? context.save()
|
|
|
|
createdInspection = inspection
|
|
navigateToExecution = true
|
|
}
|
|
}
|