import SwiftUI struct BeaconListScreen: View { @EnvironmentObject var appState: AppState @State private var beacons: [Beacon] = [] @State private var isLoading = true @State private var error: String? @State private var showAddSheet = false @State private var isDeleting = false var body: some View { NavigationStack { Group { if isLoading { ProgressView("Loading beacons...") .frame(maxWidth: .infinity, maxHeight: .infinity) } else if let error = error { VStack(spacing: 16) { Image(systemName: "exclamationmark.triangle") .font(.largeTitle) .foregroundColor(.orange) Text(error) .foregroundColor(.secondary) .multilineTextAlignment(.center) Button("Retry") { loadBeacons() } .buttonStyle(.borderedProminent) .tint(.payfritGreen) } .padding() } else if beacons.isEmpty { VStack(spacing: 16) { Image(systemName: "sensor.tag.radiowaves.forward.fill") .font(.system(size: 48)) .foregroundColor(.secondary) Text("No beacons yet") .font(.title3) .foregroundColor(.secondary) Text("Tap + to add your first beacon") .font(.subheadline) .foregroundColor(.secondary) } } else { List { ForEach(beacons) { beacon in NavigationLink(value: beacon) { BeaconRow(beacon: beacon) } } .onDelete(perform: deleteBeacons) } } } .navigationTitle("Beacons") .navigationDestination(for: Beacon.self) { beacon in BeaconDetailScreen(beacon: beacon, onSaved: { loadBeacons() }) } .toolbar { ToolbarItem(placement: .navigationBarTrailing) { Button { showAddSheet = true } label: { Image(systemName: "plus") } } } .sheet(isPresented: $showAddSheet) { BeaconEditSheet(onSaved: { loadBeacons() }) } .refreshable { await withCheckedContinuation { continuation in loadBeacons { continuation.resume() } } } } .task { loadBeacons() } } private func loadBeacons(completion: (() -> Void)? = nil) { isLoading = beacons.isEmpty error = nil Task { do { beacons = try await APIService.shared.listBeacons() } catch let apiError as APIError where apiError == .unauthorized { await appState.handleUnauthorized() } catch { self.error = error.localizedDescription } isLoading = false completion?() } } private func deleteBeacons(at offsets: IndexSet) { guard !isDeleting else { return } let toDelete = offsets.map { beacons[$0] } // Optimistic removal beacons.remove(atOffsets: offsets) isDeleting = true Task { var failedBeacons: [Beacon] = [] for beacon in toDelete { do { try await APIService.shared.deleteBeacon(beaconId: beacon.id) } catch { failedBeacons.append(beacon) self.error = error.localizedDescription } } // Restore any that failed to delete if !failedBeacons.isEmpty { beacons.append(contentsOf: failedBeacons) } isDeleting = false } } } // MARK: - Beacon Row struct BeaconRow: View { let beacon: Beacon var body: some View { VStack(alignment: .leading, spacing: 4) { HStack { Text(beacon.name) .font(.headline) Spacer() if beacon.isActive { Image(systemName: "checkmark.circle.fill") .foregroundColor(.green) .font(.caption) } else { Image(systemName: "xmark.circle.fill") .foregroundColor(.red) .font(.caption) } } Text(beacon.formattedUUID) .font(.caption) .foregroundColor(.secondary) .lineLimit(1) } .padding(.vertical, 2) } } // Make Beacon Hashable for NavigationLink extension Beacon: Hashable { static func == (lhs: Beacon, rhs: Beacon) -> Bool { lhs.id == rhs.id } func hash(into hasher: inout Hasher) { hasher.combine(id) } }