06/22 Fix Medium impact items
This commit is contained in:
@@ -256,10 +256,25 @@ struct DashboardStatsView: View {
|
||||
|
||||
@EnvironmentObject private var sync: SyncManager
|
||||
|
||||
// Draft inspections — shown as a resume banner at the top of the dashboard
|
||||
// so the inspector never has to hunt through My Inspections to find an
|
||||
// in-progress form they left open.
|
||||
@Query(
|
||||
filter: #Predicate<LocalInspection> { $0.status == "draft" },
|
||||
sort: \LocalInspection.lastModifiedAt,
|
||||
order: .reverse
|
||||
) private var draftInspections: [LocalInspection]
|
||||
@Environment(\.modelContext) private var context
|
||||
|
||||
var body: some View {
|
||||
ScrollView {
|
||||
VStack(alignment: .leading, spacing: 20) {
|
||||
|
||||
// ── Draft Resume Banner ────────────────────────────────────
|
||||
if !draftInspections.isEmpty {
|
||||
DraftResumeBanner(drafts: draftInspections, context: context)
|
||||
}
|
||||
|
||||
if let stats = sync.dashboardStats {
|
||||
// ── Today ──────────────────────────────────────────────
|
||||
statsSection(title: "Today") {
|
||||
@@ -446,6 +461,83 @@ struct DashboardStatsView: View {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Draft Resume Banner
|
||||
// Shown on the dashboard when the inspector has one or more in-progress
|
||||
// (draft) inspections. Tapping a draft opens ExecuteInspectionView as a
|
||||
// full-screen sheet — avoids cross-NavigationStack linking since the
|
||||
// dashboard and My Inspections stacks are independent.
|
||||
|
||||
struct DraftResumeBanner: View {
|
||||
let drafts: [LocalInspection]
|
||||
let context: ModelContext
|
||||
@State private var selectedDraft: LocalInspection? = nil
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Label(drafts.count == 1
|
||||
? "Inspection in progress"
|
||||
: "\(drafts.count) inspections in progress",
|
||||
systemImage: "pencil.and.list.clipboard")
|
||||
.font(.subheadline.bold())
|
||||
.foregroundStyle(.white)
|
||||
|
||||
ForEach(drafts) { draft in
|
||||
Button {
|
||||
selectedDraft = draft
|
||||
} label: {
|
||||
HStack {
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(templateName(for: draft))
|
||||
.font(.callout.bold())
|
||||
.foregroundStyle(.white)
|
||||
Text(facilityName(for: draft))
|
||||
.font(.caption)
|
||||
.foregroundStyle(.white.opacity(0.85))
|
||||
Text("Last saved \(draft.lastModifiedAt.formatted(.relative(presentation: .named)))")
|
||||
.font(.caption2)
|
||||
.foregroundStyle(.white.opacity(0.70))
|
||||
}
|
||||
Spacer()
|
||||
Label("Resume", systemImage: "play.fill")
|
||||
.font(.caption.bold())
|
||||
.foregroundStyle(.white)
|
||||
.padding(.horizontal, 10).padding(.vertical, 5)
|
||||
.background(Color.white.opacity(0.25))
|
||||
.clipShape(Capsule())
|
||||
}
|
||||
.padding(10)
|
||||
.background(Color.white.opacity(0.12))
|
||||
.clipShape(RoundedRectangle(cornerRadius: 8))
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
}
|
||||
.padding(14)
|
||||
.background(Color.blue.gradient)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 14))
|
||||
.fullScreenCover(item: $selectedDraft) { draft in
|
||||
// Wrap in NavigationStack so ExecuteInspectionView's toolbar
|
||||
// and dismiss work correctly when presented as a sheet.
|
||||
NavigationStack {
|
||||
ExecuteInspectionView(inspection: draft)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func templateName(for inspection: LocalInspection) -> String {
|
||||
let id = inspection.templateServerId
|
||||
return (try? context.fetch(
|
||||
FetchDescriptor<LocalTemplate>(predicate: #Predicate { $0.serverId == id })
|
||||
).first?.name) ?? "Inspection"
|
||||
}
|
||||
|
||||
private func facilityName(for inspection: LocalInspection) -> String {
|
||||
let id = inspection.facilityServerId
|
||||
let all = (try? context.fetch(FetchDescriptor<LocalFacility>())) ?? []
|
||||
return all.first(where: { $0.serverId == id })?.name ?? "Unknown Facility"
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - My Inspections
|
||||
|
||||
struct MyInspectionsView: View {
|
||||
|
||||
@@ -23,12 +23,13 @@ struct ExecuteInspectionView: View {
|
||||
let inspection: LocalInspection
|
||||
|
||||
@State private var formValues: [String: String] = [:]
|
||||
@State private var showFlagIssue = false
|
||||
@State private var showSubmitAlert = false
|
||||
@State private var showNoGPSAlert = false
|
||||
@State private var showFlagIssue = false
|
||||
@State private var showSubmitAlert = false
|
||||
@State private var showNoGPSAlert = false
|
||||
@State private var showValidationAlert = false
|
||||
@State private var missingFields: [String] = []
|
||||
@State private var isSaving = false
|
||||
@State private var isSaving = false
|
||||
@State private var autoSavedAt: Date? = nil // drives the auto-save toast
|
||||
@State private var isSubmitting = false
|
||||
@State private var submitResult: SubmitResult?
|
||||
|
||||
@@ -100,6 +101,10 @@ struct ExecuteInspectionView: View {
|
||||
while !Task.isCancelled {
|
||||
try? await Task.sleep(for: .seconds(autoSaveInterval))
|
||||
saveDraft()
|
||||
// Show "Draft saved" toast and dismiss after 2 seconds
|
||||
withAnimation { autoSavedAt = Date() }
|
||||
try? await Task.sleep(for: .seconds(2))
|
||||
withAnimation { autoSavedAt = nil }
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $showFlagIssue) {
|
||||
@@ -149,7 +154,25 @@ struct ExecuteInspectionView: View {
|
||||
.zIndex(10)
|
||||
}
|
||||
}
|
||||
// Auto-save toast — bottom of screen, fades out after 2 seconds
|
||||
.overlay(alignment: .bottom) {
|
||||
if autoSavedAt != nil {
|
||||
HStack(spacing: 6) {
|
||||
Image(systemName: "checkmark.circle.fill")
|
||||
.foregroundStyle(.green)
|
||||
Text("Draft saved")
|
||||
.font(.subheadline.bold())
|
||||
}
|
||||
.padding(.horizontal, 16).padding(.vertical, 10)
|
||||
.background(.regularMaterial, in: Capsule())
|
||||
.shadow(color: .black.opacity(0.12), radius: 6, y: 2)
|
||||
.padding(.bottom, 24)
|
||||
.transition(.move(edge: .bottom).combined(with: .opacity))
|
||||
.zIndex(9)
|
||||
}
|
||||
}
|
||||
.animation(.spring(duration: 0.35), value: submitResult != nil)
|
||||
.animation(.easeInOut(duration: 0.25), value: autoSavedAt)
|
||||
}
|
||||
|
||||
// ── Form Content ──────────────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user