Files
JQC_iOS_App/JanitorialQC/Views/Dashboard/FlagIssueView.swift
T

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()
}
}