05/02 Phase B

This commit is contained in:
Nguyen Ngo
2026-05-02 11:46:28 -04:00
parent ef0a6448cf
commit 8bdcbecd73
22 changed files with 3262 additions and 69 deletions
@@ -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()
}
}