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