232 lines
7.9 KiB
Swift
232 lines
7.9 KiB
Swift
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() }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|