05/02 Phase B
This commit is contained in:
@@ -0,0 +1,163 @@
|
||||
// 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()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user