import SwiftUI struct ServicePointListView: View { let businessId: Int let businessName: String var onBack: () -> Void @State private var namespace: BusinessNamespace? @State private var servicePoints: [ServicePoint] = [] @State private var isLoading = true @State private var errorMessage: String? // Add service point @State private var showAddSheet = false @State private var newServicePointName = "" @State private var isAdding = false // Beacon scan @State private var showScanView = false // Re-provision existing service point @State private var tappedServicePoint: ServicePoint? @State private var reprovisionServicePoint: ServicePoint? var body: some View { NavigationStack { Group { if isLoading { VStack { Spacer() ProgressView("Loading...") Spacer() } } else if let error = errorMessage { VStack(spacing: 16) { Spacer() Image(systemName: "exclamationmark.triangle") .font(.largeTitle) .foregroundColor(.orange) Text(error) .foregroundColor(.secondary) .multilineTextAlignment(.center) Button("Retry") { loadData() } .buttonStyle(.borderedProminent) .tint(.payfritGreen) Spacer() } .padding() } else { List { // Namespace section if let ns = namespace { Section { VStack(alignment: .leading, spacing: 8) { HStack { Text("UUID") .font(.caption) .foregroundColor(.secondary) Spacer() Text(formatUuidWithDashes(ns.uuid)) .font(.system(.caption, design: .monospaced)) Button { UIPasteboard.general.string = formatUuidWithDashes(ns.uuid) } label: { Image(systemName: "doc.on.doc") .font(.caption) } } HStack { Text("Major") .font(.caption) .foregroundColor(.secondary) Spacer() Text("\(ns.major)") .font(.system(.body, design: .monospaced).weight(.semibold)) Button { UIPasteboard.general.string = "\(ns.major)" } label: { Image(systemName: "doc.on.doc") .font(.caption) } } } } header: { Label("Beacon Namespace", systemImage: "antenna.radiowaves.left.and.right") } } // Service points section Section { if servicePoints.isEmpty { VStack(spacing: 8) { Text("No service points yet") .foregroundColor(.secondary) Text("Tap + to add one") .font(.caption) .foregroundColor(.secondary) } .frame(maxWidth: .infinity) .padding(.vertical, 20) } else { ForEach(servicePoints) { sp in Button { tappedServicePoint = sp } label: { HStack { Text(sp.name) .foregroundColor(.primary) Spacer() if let minor = sp.beaconMinor { Text("Minor: \(minor)") .font(.caption) .foregroundColor(.secondary) } Image(systemName: "chevron.right") .font(.caption) .foregroundColor(.secondary) } } } } } header: { Text("Service Points") } } } } .navigationTitle(businessName) .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .navigationBarLeading) { Button("Back", action: onBack) } ToolbarItemGroup(placement: .navigationBarTrailing) { Button { showScanView = true } label: { Image(systemName: "antenna.radiowaves.left.and.right") } Button { showAddSheet = true } label: { Image(systemName: "plus") } } } } .onAppear { loadData() } .sheet(isPresented: $showAddSheet) { addServicePointSheet } .fullScreenCover(isPresented: $showScanView) { ScanView( businessId: businessId, businessName: businessName, onBack: { showScanView = false loadData() } ) } .fullScreenCover(item: $reprovisionServicePoint) { sp in ScanView( businessId: businessId, businessName: businessName, reprovisionServicePoint: sp, onBack: { reprovisionServicePoint = nil loadData() } ) } .confirmationDialog( tappedServicePoint?.name ?? "Service Point", isPresented: Binding( get: { tappedServicePoint != nil }, set: { if !$0 { tappedServicePoint = nil } } ), titleVisibility: .visible ) { Button("Re-provision Beacon") { reprovisionServicePoint = tappedServicePoint tappedServicePoint = nil } Button("Delete", role: .destructive) { if let sp = tappedServicePoint { deleteServicePoint(sp) } tappedServicePoint = nil } Button("Cancel", role: .cancel) { tappedServicePoint = nil } } } // MARK: - Add Sheet private var addServicePointSheet: some View { NavigationStack { VStack(spacing: 20) { VStack(alignment: .leading, spacing: 8) { Text("Service Point Name") .font(.subheadline) .foregroundColor(.secondary) TextField("e.g., Table 1", text: $newServicePointName) .textFieldStyle(.roundedBorder) .font(.title3) } .padding(.horizontal) if let ns = namespace { VStack(alignment: .leading, spacing: 8) { Text("Beacon config will be assigned automatically:") .font(.caption) .foregroundColor(.secondary) HStack { VStack(alignment: .leading) { Text("UUID").font(.caption2).foregroundColor(.secondary) Text(formatUuidWithDashes(ns.uuid)) .font(.system(.caption2, design: .monospaced)) } } HStack(spacing: 24) { VStack(alignment: .leading) { Text("Major").font(.caption2).foregroundColor(.secondary) Text("\(ns.major)").font(.system(.caption, design: .monospaced).weight(.semibold)) } VStack(alignment: .leading) { Text("Minor").font(.caption2).foregroundColor(.secondary) Text("Auto").font(.caption.italic()).foregroundColor(.secondary) } } } .padding() .background(Color(.systemGray6)) .cornerRadius(8) .padding(.horizontal) } Spacer() } .padding(.top) .navigationTitle("Add Service Point") .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .cancellationAction) { Button("Cancel") { showAddSheet = false } .disabled(isAdding) } ToolbarItem(placement: .confirmationAction) { if isAdding { ProgressView() } else { Button("Add") { addServicePoint() } .disabled(newServicePointName.trimmingCharacters(in: .whitespaces).isEmpty) } } } } .presentationDetents([.medium]) .interactiveDismissDisabled(isAdding) .onAppear { newServicePointName = "Table \(nextTableNumber)" } } // MARK: - Actions private func loadData() { isLoading = true errorMessage = nil Task { do { // Get namespace let ns = try await Api.shared.allocateBusinessNamespace(businessId: businessId) namespace = ns // Get service points let sps = try await Api.shared.listServicePoints(businessId: businessId) servicePoints = sps.sorted { $0.name.localizedCompare($1.name) == .orderedAscending } isLoading = false } catch { errorMessage = error.localizedDescription isLoading = false } } } private func deleteServicePoint(_ sp: ServicePoint) { Task { do { try await Api.shared.deleteServicePoint(businessId: businessId, servicePointId: sp.servicePointId) servicePoints.removeAll { $0.servicePointId == sp.servicePointId } } catch { errorMessage = error.localizedDescription } } } private func addServicePoint() { let name = newServicePointName.trimmingCharacters(in: .whitespaces) guard !name.isEmpty else { return } isAdding = true Task { do { let sp = try await Api.shared.saveServicePoint(businessId: businessId, name: name) servicePoints.append(sp) servicePoints.sort { $0.name.localizedCompare($1.name) == .orderedAscending } showAddSheet = false newServicePointName = "" isAdding = false } catch { // Show error somehow isAdding = false } } } private var nextTableNumber: Int { let maxNumber = servicePoints.compactMap { sp -> Int? in guard let match = sp.name.range(of: #"Table\s+(\d+)"#, options: [.regularExpression, .caseInsensitive]) else { return nil } let numberStr = sp.name[match].split(separator: " ").last.flatMap { String($0) } ?? "" return Int(numberStr) }.max() ?? 0 return maxNumber + 1 } private func formatUuidWithDashes(_ raw: String) -> String { let clean = raw.replacingOccurrences(of: "-", with: "").uppercased() guard clean.count == 32 else { return raw } let chars = Array(clean) return "\(String(chars[0..<8]))-\(String(chars[8..<12]))-\(String(chars[12..<16]))-\(String(chars[16..<20]))-\(String(chars[20..<32]))" } }