186 lines
7.4 KiB
Swift
186 lines
7.4 KiB
Swift
// Views/Dashboard/FlagIssueView.swift
|
|
// ------------------------------------
|
|
// Sheet for flagging an issue during an inspection.
|
|
// Saves locally immediately; syncs to server when online.
|
|
//
|
|
// CHANGED: Area picker removed. Facility is derived directly from the
|
|
// inspection (inspection.facilityServerId) and displayed as read-only info,
|
|
// matching the web app's flag_issue.html behaviour where facility_id is
|
|
// a hidden field populated from the inspection context.
|
|
|
|
import SwiftUI
|
|
import SwiftData
|
|
|
|
struct FlagIssueView: View {
|
|
|
|
@Environment(\.modelContext) private var context
|
|
@Environment(\.dismiss) private var dismiss
|
|
@EnvironmentObject private var sync: SyncManager
|
|
|
|
let inspection: LocalInspection
|
|
|
|
@State private var severity = "medium"
|
|
@State private var description = ""
|
|
@State private var selectedImage: UIImage?
|
|
@State private var photoLocalPath: String?
|
|
@State private var showChoice = false
|
|
@State private var showCamera = false
|
|
@State private var showLibrary = false
|
|
|
|
private var cameraAvailable: Bool {
|
|
UIImagePickerController.isSourceTypeAvailable(.camera)
|
|
}
|
|
|
|
private let severities = ["low", "medium", "high", "critical"]
|
|
|
|
private var facility: LocalFacility? {
|
|
let id = inspection.facilityServerId
|
|
return try? context.fetch(
|
|
FetchDescriptor<LocalFacility>(
|
|
predicate: #Predicate { $0.serverId == id }
|
|
)
|
|
).first
|
|
}
|
|
|
|
private var canSubmit: Bool {
|
|
!description.trimmingCharacters(in: .whitespaces).isEmpty
|
|
}
|
|
|
|
var body: some View {
|
|
NavigationStack {
|
|
Form {
|
|
// ── Facility (read-only) — matches web alert banner ────────
|
|
Section {
|
|
HStack(spacing: 10) {
|
|
Image(systemName: "building.2")
|
|
.foregroundStyle(.secondary)
|
|
VStack(alignment: .leading, spacing: 2) {
|
|
Text("Facility")
|
|
.font(.caption)
|
|
.foregroundStyle(.secondary)
|
|
Text(facility?.name ?? "—")
|
|
.font(.body)
|
|
}
|
|
}
|
|
.padding(.vertical, 2)
|
|
} header: {
|
|
Text("Inspection Context")
|
|
} footer: {
|
|
Text("Issue will be logged against this facility.")
|
|
.font(.caption)
|
|
}
|
|
|
|
// ── Severity ───────────────────────────────────────────────
|
|
Section("Severity") {
|
|
Picker("Severity", selection: $severity) {
|
|
ForEach(severities, id: \.self) { s in
|
|
Text(s.capitalized).tag(s)
|
|
}
|
|
}
|
|
.pickerStyle(.segmented)
|
|
}
|
|
|
|
// ── Description ────────────────────────────────────────────
|
|
Section("Description") {
|
|
TextEditor(text: $description)
|
|
.frame(minHeight: 100)
|
|
}
|
|
|
|
// ── Photo ──────────────────────────────────────────────────
|
|
Section("Photo (Optional)") {
|
|
if let img = selectedImage {
|
|
Image(uiImage: img)
|
|
.resizable()
|
|
.scaledToFit()
|
|
.frame(maxHeight: 160)
|
|
.clipShape(RoundedRectangle(cornerRadius: 8))
|
|
}
|
|
Button {
|
|
if cameraAvailable { showChoice = true } else { showLibrary = true }
|
|
} label: {
|
|
Label(selectedImage == nil ? "Attach Photo" : "Replace Photo",
|
|
systemImage: "camera")
|
|
}
|
|
}
|
|
|
|
// ── Offline notice ─────────────────────────────────────────
|
|
if !sync.isOnline {
|
|
Section {
|
|
Label("You're offline — this issue will sync automatically.",
|
|
systemImage: "wifi.slash")
|
|
.font(.callout)
|
|
.foregroundStyle(.orange)
|
|
}
|
|
}
|
|
}
|
|
.navigationTitle("Flag Issue")
|
|
.navigationBarTitleDisplayMode(.inline)
|
|
.toolbar {
|
|
ToolbarItem(placement: .cancellationAction) {
|
|
Button("Cancel") { dismiss() }
|
|
}
|
|
ToolbarItem(placement: .confirmationAction) {
|
|
Button("Submit") { submitIssue() }
|
|
.disabled(!canSubmit)
|
|
.fontWeight(.semibold)
|
|
}
|
|
}
|
|
.confirmationDialog("Add Photo", isPresented: $showChoice, titleVisibility: .visible) {
|
|
Button("Take Photo") { showCamera = true }
|
|
Button("Photo Library") { showLibrary = true }
|
|
Button("Cancel", role: .cancel) {}
|
|
}
|
|
.fullScreenCover(isPresented: $showCamera) {
|
|
CameraPickerView(image: $selectedImage, onSelected: savePhoto)
|
|
.ignoresSafeArea()
|
|
}
|
|
.sheet(isPresented: $showLibrary) {
|
|
LibraryPickerView(image: $selectedImage, onSelected: savePhoto)
|
|
}
|
|
}
|
|
}
|
|
|
|
private func savePhoto(_ img: UIImage) {
|
|
guard let data = img.jpegData(compressionQuality: 0.8) else { return }
|
|
let docsDir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
|
|
let photosDir = docsDir.appendingPathComponent("JQC/Photos", isDirectory: true)
|
|
try? FileManager.default.createDirectory(at: photosDir,
|
|
withIntermediateDirectories: true)
|
|
let filename = "\(UUID().uuidString).jpg"
|
|
let fileURL = photosDir.appendingPathComponent(filename)
|
|
try? data.write(to: fileURL)
|
|
photoLocalPath = fileURL.path
|
|
selectedImage = img
|
|
}
|
|
|
|
private func submitIssue() {
|
|
let issue = LocalIssue(
|
|
inspectionLocalId: inspection.localId,
|
|
facilityServerId: inspection.facilityServerId,
|
|
severity: severity,
|
|
description: description.trimmingCharacters(in: .whitespaces)
|
|
)
|
|
issue.photoLocalPath = photoLocalPath
|
|
issue.inspection = inspection
|
|
inspection.localIssues.append(issue)
|
|
context.insert(issue)
|
|
|
|
if let path = photoLocalPath {
|
|
let photo = PendingPhoto(
|
|
localFilePath: path,
|
|
entityType: "issue",
|
|
entityLocalId: issue.localId
|
|
)
|
|
context.insert(photo)
|
|
}
|
|
|
|
try? context.save()
|
|
|
|
if sync.isOnline {
|
|
Task { await sync.triggerSync() }
|
|
}
|
|
|
|
dismiss()
|
|
}
|
|
}
|