payfrit-beacon-ios/PayfritBeacon/Views/ServicePointListScreen.swift
2026-02-01 23:39:29 -08:00

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() }
}
}
}
}
}