164 lines
6.3 KiB
Swift
164 lines
6.3 KiB
Swift
// Views/Inspection/FlagIssueView.swift
|
|
// -------------------------------------
|
|
// Sheet for flagging an issue during an inspection.
|
|
// Saves locally immediately; syncs to server when online.
|
|
|
|
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 selectedAreaId: Int?
|
|
@State private var severity = "medium"
|
|
@State private var description = ""
|
|
@State private var selectedImage: UIImage?
|
|
@State private var photoLocalPath: String?
|
|
@State private var showImagePicker = false
|
|
|
|
private let severities = ["low", "medium", "high", "critical"]
|
|
|
|
private var areas: [LocalArea] {
|
|
let facilityId = inspection.facilityServerId
|
|
let results = try? context.fetch(
|
|
FetchDescriptor<LocalArea>(
|
|
predicate: #Predicate { $0.facilityServerId == facilityId },
|
|
sortBy: [SortDescriptor(\.name)]
|
|
)
|
|
)
|
|
return results ?? []
|
|
}
|
|
|
|
private var canSubmit: Bool {
|
|
selectedAreaId != nil && !description.trimmingCharacters(in: .whitespaces).isEmpty
|
|
}
|
|
|
|
var body: some View {
|
|
NavigationStack {
|
|
Form {
|
|
// ── Area ───────────────────────────────────────────────────
|
|
Section("Area") {
|
|
Picker("Area", selection: $selectedAreaId) {
|
|
Text("Select area…").tag(Optional<Int>(nil))
|
|
ForEach(areas) { area in
|
|
Text(area.name).tag(Optional(area.serverId))
|
|
}
|
|
}
|
|
.pickerStyle(.navigationLink)
|
|
}
|
|
|
|
// ── 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 {
|
|
showImagePicker = 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)
|
|
}
|
|
}
|
|
.sheet(isPresented: $showImagePicker) {
|
|
ImagePickerView(image: $selectedImage) { img in
|
|
savePhoto(img)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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() {
|
|
guard let areaId = selectedAreaId else { return }
|
|
|
|
let issue = LocalIssue(
|
|
inspectionLocalId: inspection.localId,
|
|
areaServerId: areaId,
|
|
severity: severity,
|
|
description: description.trimmingCharacters(in: .whitespaces)
|
|
)
|
|
issue.photoLocalPath = photoLocalPath
|
|
issue.inspection = inspection
|
|
inspection.localIssues.append(issue)
|
|
context.insert(issue)
|
|
|
|
// Create PendingPhoto if a photo was attached
|
|
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()
|
|
}
|
|
}
|