184 lines
6.2 KiB
Swift
184 lines
6.2 KiB
Swift
// Views/Inspection/InspectionHistoryView.swift
|
|
// --------------------------------------------
|
|
// Shows the inspector's synced inspection history fetched from the server.
|
|
// Only available when online. Displays score, facility, template, and date.
|
|
|
|
import SwiftUI
|
|
|
|
struct InspectionHistoryView: View {
|
|
|
|
@EnvironmentObject private var sync: SyncManager
|
|
@State private var inspections: [APIInspectionSummary] = []
|
|
@State private var isLoading = false
|
|
@State private var errorMessage: String?
|
|
@State private var total = 0
|
|
@State private var offset = 0
|
|
private let limit = 30
|
|
|
|
var body: some View {
|
|
Group {
|
|
if !sync.isOnline && inspections.isEmpty {
|
|
ContentUnavailableView(
|
|
"Offline",
|
|
systemImage: "wifi.slash",
|
|
description: Text("Inspection history requires an internet connection.")
|
|
)
|
|
} else if isLoading && inspections.isEmpty {
|
|
ProgressView("Loading history…")
|
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
|
} else if let error = errorMessage, inspections.isEmpty {
|
|
ContentUnavailableView(
|
|
"Could Not Load",
|
|
systemImage: "exclamationmark.triangle",
|
|
description: Text(error)
|
|
)
|
|
} else if inspections.isEmpty {
|
|
ContentUnavailableView(
|
|
"No History",
|
|
systemImage: "clock.arrow.circlepath",
|
|
description: Text("Completed inspections will appear here after syncing.")
|
|
)
|
|
} else {
|
|
List {
|
|
ForEach(inspections) { inspection in
|
|
HistoryRowView(inspection: inspection)
|
|
}
|
|
|
|
// Load more
|
|
if inspections.count < total {
|
|
HStack {
|
|
Spacer()
|
|
Button("Load More") {
|
|
Task { await loadMore() }
|
|
}
|
|
.disabled(isLoading)
|
|
Spacer()
|
|
}
|
|
.listRowSeparator(.hidden)
|
|
}
|
|
|
|
if isLoading {
|
|
HStack {
|
|
Spacer()
|
|
ProgressView()
|
|
Spacer()
|
|
}
|
|
.listRowSeparator(.hidden)
|
|
}
|
|
}
|
|
.refreshable {
|
|
await load(reset: true)
|
|
}
|
|
}
|
|
}
|
|
.navigationTitle("Inspection History")
|
|
.task {
|
|
if sync.isOnline {
|
|
await load(reset: true)
|
|
}
|
|
}
|
|
.onChange(of: sync.isOnline) {
|
|
if sync.isOnline && inspections.isEmpty {
|
|
Task { await load(reset: true) }
|
|
}
|
|
}
|
|
}
|
|
|
|
private func load(reset: Bool) async {
|
|
if reset { offset = 0 }
|
|
isLoading = true
|
|
errorMessage = nil
|
|
defer { isLoading = false }
|
|
|
|
do {
|
|
let result = try await APIClient.shared.fetchInspectionHistory(
|
|
limit: limit, offset: reset ? 0 : offset
|
|
)
|
|
if reset {
|
|
inspections = result.inspections
|
|
} else {
|
|
inspections.append(contentsOf: result.inspections)
|
|
}
|
|
total = result.total
|
|
offset = inspections.count
|
|
} catch {
|
|
errorMessage = error.localizedDescription
|
|
}
|
|
}
|
|
|
|
private func loadMore() async {
|
|
await load(reset: false)
|
|
}
|
|
}
|
|
|
|
// MARK: - History Row
|
|
|
|
struct HistoryRowView: View {
|
|
let inspection: APIInspectionSummary
|
|
|
|
private var scoreColor: Color {
|
|
guard let score = inspection.overallScore else { return .secondary }
|
|
return score >= 80 ? .green : score >= 60 ? .orange : .red
|
|
}
|
|
|
|
private var dateText: String {
|
|
guard let date = inspection.inspectionDateParsed else {
|
|
return inspection.inspectionDate ?? ""
|
|
}
|
|
return date.formatted(date: .abbreviated, time: .shortened)
|
|
}
|
|
|
|
var body: some View {
|
|
VStack(alignment: .leading, spacing: 6) {
|
|
HStack(alignment: .top) {
|
|
VStack(alignment: .leading, spacing: 2) {
|
|
Text(inspection.templateName)
|
|
.font(.headline)
|
|
.lineLimit(1)
|
|
Text(inspection.facilityName)
|
|
.font(.callout)
|
|
.foregroundStyle(.secondary)
|
|
.lineLimit(1)
|
|
if let area = inspection.areaName {
|
|
Text(area)
|
|
.font(.caption)
|
|
.foregroundStyle(.tertiary)
|
|
}
|
|
}
|
|
Spacer()
|
|
if let score = inspection.overallScore {
|
|
VStack(alignment: .trailing, spacing: 2) {
|
|
Text(String(format: "%.1f%%", score))
|
|
.font(.title3.bold())
|
|
.foregroundStyle(scoreColor)
|
|
Text("Score")
|
|
.font(.caption2)
|
|
.foregroundStyle(.tertiary)
|
|
}
|
|
}
|
|
}
|
|
|
|
HStack {
|
|
Image(systemName: "calendar")
|
|
.font(.caption2)
|
|
.foregroundStyle(.tertiary)
|
|
Text(dateText)
|
|
.font(.caption2)
|
|
.foregroundStyle(.tertiary)
|
|
Spacer()
|
|
// Sync origin badge
|
|
if inspection.mobileLocalId != nil {
|
|
Label("Mobile", systemImage: "ipad")
|
|
.font(.caption2)
|
|
.foregroundStyle(.blue)
|
|
.padding(.horizontal, 6)
|
|
.padding(.vertical, 2)
|
|
.background(Color.blue.opacity(0.1))
|
|
.clipShape(Capsule())
|
|
}
|
|
}
|
|
}
|
|
.padding(.vertical, 4)
|
|
}
|
|
}
|