06/22 Fix Low impact items
This commit is contained in:
@@ -0,0 +1,149 @@
|
||||
// Views/Dashboard/SyncStatusView.swift
|
||||
import SwiftUI
|
||||
import SwiftData
|
||||
import MessageUI
|
||||
|
||||
// MARK: - Sync Status View
|
||||
|
||||
struct SyncStatusView: View {
|
||||
@EnvironmentObject private var sync: SyncManager
|
||||
@Environment(\.modelContext) private var context
|
||||
|
||||
@Query(
|
||||
filter: #Predicate<LocalInspection> { $0.syncStatus == "pending" || $0.syncStatus == "failed" },
|
||||
sort: \LocalInspection.createdAt
|
||||
) private var pendingInspections: [LocalInspection]
|
||||
|
||||
@Query(
|
||||
filter: #Predicate<LocalIssue> { $0.syncStatus == "pending" || $0.syncStatus == "failed" },
|
||||
sort: \LocalIssue.createdAt
|
||||
) private var pendingIssues: [LocalIssue]
|
||||
|
||||
private var failedInspections: [LocalInspection] { pendingInspections.filter { $0.syncStatus == "failed" } }
|
||||
private var failedIssues: [LocalIssue] { pendingIssues.filter { $0.syncStatus == "failed" } }
|
||||
private var hasFailedItems: Bool { !failedInspections.isEmpty || !failedIssues.isEmpty }
|
||||
|
||||
var body: some View {
|
||||
List {
|
||||
Section("Status") {
|
||||
HStack {
|
||||
Circle().fill(sync.isOnline ? Color.green : Color.orange)
|
||||
.frame(width: 8, height: 8)
|
||||
Text(sync.isOnline ? "Online" : "Offline")
|
||||
}
|
||||
if let lastSync = sync.lastSyncAt {
|
||||
LabeledContent("Last Sync",
|
||||
value: lastSync.formatted(date: .abbreviated, time: .shortened))
|
||||
}
|
||||
if sync.isSyncing {
|
||||
HStack {
|
||||
ProgressView()
|
||||
Text("Syncing…").foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
if let error = sync.syncError {
|
||||
Text(error).foregroundStyle(.red).font(.callout)
|
||||
}
|
||||
Button {
|
||||
Task { await sync.triggerSync() }
|
||||
} label: {
|
||||
Label("Sync Now", systemImage: "arrow.clockwise")
|
||||
}
|
||||
.disabled(!sync.isOnline || sync.isSyncing)
|
||||
|
||||
// Retry Failed Items — resets syncStatus back to "pending" so
|
||||
// the next triggerSync() will re-attempt them. Once an item
|
||||
// reaches syncStatus = "failed" (after 5 consecutive errors)
|
||||
// triggerSync() stops picking it up — this is the only way
|
||||
// to re-queue it without manual server intervention.
|
||||
if hasFailedItems {
|
||||
Button {
|
||||
retryAllFailed()
|
||||
} label: {
|
||||
Label("Retry Failed Items (\(failedInspections.count + failedIssues.count))",
|
||||
systemImage: "exclamationmark.arrow.circlepath")
|
||||
.foregroundStyle(.orange)
|
||||
}
|
||||
.disabled(!sync.isOnline || sync.isSyncing)
|
||||
}
|
||||
}
|
||||
|
||||
if !pendingInspections.isEmpty {
|
||||
Section("Pending Inspections (\(pendingInspections.count))") {
|
||||
ForEach(pendingInspections) { insp in
|
||||
SyncRowView(title: "Inspection", status: insp.syncStatus,
|
||||
retryCount: insp.syncRetryCount,
|
||||
error: insp.syncErrorMessage, date: insp.createdAt)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !pendingIssues.isEmpty {
|
||||
Section("Pending Issues (\(pendingIssues.count))") {
|
||||
ForEach(pendingIssues) { issue in
|
||||
SyncRowView(title: "\(issue.severity.capitalized) Issue",
|
||||
status: issue.syncStatus, retryCount: issue.syncRetryCount,
|
||||
error: issue.syncErrorMessage, date: issue.createdAt)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if pendingInspections.isEmpty && pendingIssues.isEmpty && !sync.isSyncing {
|
||||
Section {
|
||||
Label("All items synced.", systemImage: "checkmark.circle.fill")
|
||||
.foregroundStyle(.green)
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationTitle("Pending Sync")
|
||||
}
|
||||
|
||||
/// Reset all failed items back to pending so the next sync pass picks them up.
|
||||
/// Also clears syncRetryCount and syncErrorMessage so the retry counter
|
||||
/// starts fresh — prevents them immediately hitting the 5-retry cap again
|
||||
/// without any actual new attempt.
|
||||
private func retryAllFailed() {
|
||||
for insp in failedInspections {
|
||||
insp.syncStatus = "pending"
|
||||
insp.syncRetryCount = 0
|
||||
insp.syncErrorMessage = nil
|
||||
}
|
||||
for issue in failedIssues {
|
||||
issue.syncStatus = "pending"
|
||||
issue.syncRetryCount = 0
|
||||
issue.syncErrorMessage = nil
|
||||
}
|
||||
try? context.save()
|
||||
Task { await sync.triggerSync() }
|
||||
}
|
||||
}
|
||||
|
||||
struct SyncRowView: View {
|
||||
let title: String
|
||||
let status: String
|
||||
let retryCount: Int
|
||||
let error: String?
|
||||
let date: Date
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
HStack {
|
||||
Text(title).font(.callout)
|
||||
Spacer()
|
||||
Text(status.capitalized).font(.caption2)
|
||||
.foregroundStyle(status == "failed" ? .red : .orange)
|
||||
}
|
||||
Text(date.formatted(date: .abbreviated, time: .shortened))
|
||||
.font(.caption2).foregroundStyle(.tertiary)
|
||||
if let err = error {
|
||||
Text(err).font(.caption2).foregroundStyle(.red).lineLimit(2)
|
||||
}
|
||||
if retryCount > 0 {
|
||||
Text("Retried \(retryCount) time\(retryCount == 1 ? "" : "s")")
|
||||
.font(.caption2).foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
.padding(.vertical, 2)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user