import SwiftUI struct ServicePointListScreen: View { @EnvironmentObject var appState: AppState @State private var servicePoints: [ServicePoint] = [] @State private var beacons: [Beacon] = [] @State private var isLoading = true @State private var error: String? @State private var assigningPointId: Int? var body: some View { NavigationStack { Group { if isLoading { ProgressView("Loading service points...") .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") { loadData() } .buttonStyle(.borderedProminent) .tint(.payfritGreen) } .padding() } else if servicePoints.isEmpty { VStack(spacing: 16) { Image(systemName: "mappin.and.ellipse") .font(.system(size: 48)) .foregroundColor(.secondary) Text("No service points") .font(.title3) .foregroundColor(.secondary) } } else { List(servicePoints) { sp in ServicePointRow( servicePoint: sp, beacons: beacons, isAssigning: assigningPointId == sp.id, onAssignBeacon: { beaconId in assignBeacon(servicePointId: sp.id, beaconId: beaconId) } ) } } } .navigationTitle("Service Points") .refreshable { await withCheckedContinuation { continuation in loadData { continuation.resume() } } } } .task { loadData() } } private func loadData(completion: (() -> Void)? = nil) { isLoading = servicePoints.isEmpty error = nil Task { do { async let sp = APIService.shared.listServicePoints() async let b = APIService.shared.listBeacons() servicePoints = try await sp beacons = try await b } catch let apiError as APIError where apiError == .unauthorized { await appState.handleUnauthorized() } catch { self.error = error.localizedDescription } isLoading = false completion?() } } private func assignBeacon(servicePointId: Int, beaconId: Int?) { assigningPointId = servicePointId Task { do { try await APIService.shared.assignBeaconToServicePoint( servicePointId: servicePointId, beaconId: beaconId ) loadData() } catch let apiError as APIError where apiError == .unauthorized { await appState.handleUnauthorized() } catch { self.error = error.localizedDescription } assigningPointId = nil } } } // MARK: - Service Point Row struct ServicePointRow: View { let servicePoint: ServicePoint let beacons: [Beacon] let isAssigning: Bool var onAssignBeacon: (Int?) -> Void @State private var showBeaconPicker = false private var assignedBeacon: Beacon? { guard let bid = servicePoint.beaconId else { return nil } return beacons.first { $0.id == bid } } var body: some View { VStack(alignment: .leading, spacing: 6) { HStack { VStack(alignment: .leading, spacing: 2) { Text(servicePoint.name) .font(.headline) if !servicePoint.typeName.isEmpty { Text(servicePoint.typeName) .font(.caption) .foregroundColor(.secondary) } } Spacer() if servicePoint.isActive { Circle() .fill(.green) .frame(width: 8, height: 8) } else { Circle() .fill(.red) .frame(width: 8, height: 8) } } // Beacon assignment HStack { Image(systemName: "sensor.tag.radiowaves.forward.fill") .font(.caption) .foregroundColor(.secondary) if isAssigning { ProgressView() .controlSize(.small) } else if let beacon = assignedBeacon { Text(beacon.name) .font(.subheadline) .foregroundColor(.payfritGreen) } else { Text("No beacon assigned") .font(.subheadline) .foregroundColor(.secondary) } Spacer() Button { showBeaconPicker = true } label: { Text(assignedBeacon != nil ? "Change" : "Assign") .font(.caption) } .buttonStyle(.bordered) .controlSize(.small) if assignedBeacon != nil { Button { onAssignBeacon(nil) } label: { Image(systemName: "xmark.circle.fill") .foregroundColor(.red) .font(.caption) } .buttonStyle(.plain) } } } .padding(.vertical, 4) .sheet(isPresented: $showBeaconPicker) { BeaconPickerSheet(beacons: beacons, currentBeaconId: servicePoint.beaconId) { selectedId in onAssignBeacon(selectedId) } } } } // MARK: - Beacon Picker Sheet struct BeaconPickerSheet: View { let beacons: [Beacon] let currentBeaconId: Int? var onSelect: (Int) -> Void @Environment(\.dismiss) private var dismiss var body: some View { NavigationStack { List(beacons) { beacon in Button { onSelect(beacon.id) dismiss() } label: { HStack { VStack(alignment: .leading, spacing: 2) { Text(beacon.name) .font(.headline) .foregroundColor(.primary) Text(beacon.formattedUUID) .font(.caption) .foregroundColor(.secondary) } Spacer() if beacon.id == currentBeaconId { Image(systemName: "checkmark.circle.fill") .foregroundColor(.payfritGreen) } } } } .navigationTitle("Select Beacon") .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .cancellationAction) { Button("Cancel") { dismiss() } } } } } }