ContentUnavailableView and .symbolEffect(.pulse) require iOS 17+ but deployment target is iOS 16. Wrapped all usages in if #available(iOS 17.0, *) with VStack-based fallbacks for iOS 16. Files fixed: - ScanView.swift (4 ContentUnavailableView + 1 symbolEffect) - QRScannerView.swift (1 ContentUnavailableView) - BusinessListView.swift (2 ContentUnavailableView)
152 lines
5.4 KiB
Swift
152 lines
5.4 KiB
Swift
import SwiftUI
|
|
|
|
/// Business selector screen (matches Android MainActivity)
|
|
struct BusinessListView: View {
|
|
@EnvironmentObject var appState: AppState
|
|
|
|
@State private var businesses: [Business] = []
|
|
@State private var isLoading = true
|
|
@State private var errorMessage: String?
|
|
|
|
var body: some View {
|
|
NavigationStack {
|
|
Group {
|
|
if isLoading {
|
|
ProgressView("Loading businesses…")
|
|
} else if let error = errorMessage {
|
|
if #available(iOS 17.0, *) {
|
|
ContentUnavailableView {
|
|
Label("Error", systemImage: "exclamationmark.triangle")
|
|
} description: {
|
|
Text(error)
|
|
} actions: {
|
|
Button("Retry") { Task { await loadBusinesses() } }
|
|
}
|
|
} else {
|
|
VStack(spacing: 12) {
|
|
Image(systemName: "exclamationmark.triangle")
|
|
.font(.system(size: 48))
|
|
.foregroundStyle(.orange)
|
|
Text("Error")
|
|
.font(.headline)
|
|
Text(error)
|
|
.font(.subheadline)
|
|
.foregroundStyle(.secondary)
|
|
.multilineTextAlignment(.center)
|
|
Button("Retry") { Task { await loadBusinesses() } }
|
|
.buttonStyle(.borderedProminent)
|
|
}
|
|
.padding()
|
|
}
|
|
} else if businesses.isEmpty {
|
|
if #available(iOS 17.0, *) {
|
|
ContentUnavailableView(
|
|
"No Businesses",
|
|
systemImage: "building.2",
|
|
description: Text("You don't have any businesses yet.")
|
|
)
|
|
} else {
|
|
VStack(spacing: 12) {
|
|
Image(systemName: "building.2")
|
|
.font(.system(size: 48))
|
|
.foregroundStyle(.secondary)
|
|
Text("No Businesses")
|
|
.font(.headline)
|
|
Text("You don't have any businesses yet.")
|
|
.font(.subheadline)
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
.frame(maxHeight: .infinity)
|
|
.padding()
|
|
}
|
|
} else {
|
|
List(businesses) { business in
|
|
Button {
|
|
appState.selectBusiness(business)
|
|
} label: {
|
|
BusinessRow(business: business)
|
|
}
|
|
}
|
|
.listStyle(.insetGrouped)
|
|
}
|
|
}
|
|
.navigationTitle("Select Business")
|
|
.toolbar(content: {
|
|
ToolbarItem(placement: .topBarTrailing) {
|
|
Button("Logout") {
|
|
appState.logout()
|
|
}
|
|
}
|
|
})
|
|
}
|
|
.task {
|
|
await loadBusinesses()
|
|
}
|
|
}
|
|
|
|
private func loadBusinesses() async {
|
|
guard let token = appState.token else { return }
|
|
|
|
isLoading = true
|
|
errorMessage = nil
|
|
|
|
do {
|
|
let list = try await APIClient.shared.listBusinesses(token: token)
|
|
businesses = list
|
|
|
|
// Auto-navigate if only one business (like Android)
|
|
if list.count == 1, let only = list.first {
|
|
appState.selectBusiness(only)
|
|
return
|
|
}
|
|
|
|
// Auto-navigate to last used business
|
|
if let lastId = AppPrefs.lastBusinessId,
|
|
let last = list.first(where: { $0.id == lastId }) {
|
|
appState.selectBusiness(last)
|
|
return
|
|
}
|
|
} catch let e as APIError where e.errorDescription == APIError.unauthorized.errorDescription {
|
|
appState.logout()
|
|
} catch {
|
|
errorMessage = error.localizedDescription
|
|
}
|
|
|
|
isLoading = false
|
|
}
|
|
}
|
|
|
|
// MARK: - Business Row
|
|
|
|
struct BusinessRow: View {
|
|
let business: Business
|
|
|
|
var body: some View {
|
|
HStack(spacing: 12) {
|
|
// Business header image
|
|
AsyncImage(url: business.headerImageURL) { image in
|
|
image.resizable().aspectRatio(contentMode: .fill)
|
|
} placeholder: {
|
|
Image(systemName: "building.2")
|
|
.font(.title2)
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
.frame(width: 56, height: 56)
|
|
.clipShape(RoundedRectangle(cornerRadius: 8))
|
|
|
|
VStack(alignment: .leading, spacing: 4) {
|
|
Text(business.name)
|
|
.font(.headline)
|
|
Text("ID: \(business.id)")
|
|
.font(.caption)
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
|
|
Spacer()
|
|
|
|
Image(systemName: "chevron.right")
|
|
.foregroundStyle(.tertiary)
|
|
}
|
|
.padding(.vertical, 4)
|
|
}
|
|
}
|