payfrit-works-ios/PayfritWorks/Views/BusinessSelectionScreen.swift
2026-02-01 23:38:34 -08:00

352 lines
12 KiB
Swift

import SwiftUI
struct BusinessSelectionScreen: View {
@EnvironmentObject var appState: AppState
@State private var businesses: [Employment] = []
@State private var isLoading = true
@State private var error: String?
@State private var showingTaskList = false
@State private var showingMyTasks = false
@State private var showingAccount = false
@State private var showingDebug = false
@State private var selectedBusiness: Employment?
@State private var debugText = ""
private let refreshTimer = Timer.publish(every: 2, on: .main, in: .common).autoconnect()
var body: some View {
NavigationStack {
ZStack(alignment: .bottomTrailing) {
Group {
if isLoading {
ProgressView()
} else if let error = error {
errorView(error)
} else if businesses.isEmpty {
emptyView
} else {
businessList
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
myTasksFAB
}
.navigationTitle("Select Business")
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Menu {
Label(appState.userName ?? "Worker", systemImage: "person.circle.fill")
Divider()
Button { showingMyTasks = true } label: {
Label("My Tasks", systemImage: "checkmark.circle")
}
Button { showingAccount = true } label: {
Label("Account", systemImage: "person.crop.circle")
}
Button { loadDebugInfo() } label: {
Label("Debug API", systemImage: "ladybug")
}
Button(role: .destructive) { logout() } label: {
Label("Logout", systemImage: "rectangle.portrait.and.arrow.right")
}
} label: {
Image(systemName: "ellipsis.circle")
}
}
ToolbarItem(placement: .navigationBarTrailing) {
Button { loadBusinesses() } label: {
Image(systemName: "arrow.clockwise")
}
}
}
.navigationDestination(isPresented: $showingTaskList) {
if let biz = selectedBusiness {
TaskListScreen(businessName: biz.businessName)
.onAppear {
Task { await APIService.shared.setBusinessId(biz.businessId) }
appState.setBusinessId(biz.businessId)
}
}
}
.navigationDestination(isPresented: $showingMyTasks) {
MyTasksScreen()
}
.navigationDestination(isPresented: $showingAccount) {
AccountScreen()
}
}
.sheet(isPresented: $showingDebug) {
NavigationStack {
ScrollView {
Text(debugText)
.font(.system(.caption2, design: .monospaced))
.padding()
.frame(maxWidth: .infinity, alignment: .leading)
}
.navigationTitle("API Debug")
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button("Done") { showingDebug = false }
}
}
}
}
.task { loadBusinesses() }
.onReceive(refreshTimer) { _ in loadBusinesses(silent: true) }
}
// MARK: - Business List
private var businessList: some View {
ScrollView {
LazyVStack(spacing: 12) {
ForEach(businesses) { emp in
businessCard(emp)
.onTapGesture { selectBusiness(emp) }
}
}
.padding(.horizontal, 16)
.padding(.top, 8)
.padding(.bottom, 80) // space for FAB
}
.refreshable { loadBusinesses() }
}
private func businessCard(_ emp: Employment) -> some View {
VStack(spacing: 0) {
// Header image with brand color background
BusinessHeaderImage(businessId: emp.businessId)
// Info bar
HStack(spacing: 12) {
VStack(alignment: .leading, spacing: 2) {
Text(emp.businessName)
.font(.subheadline.weight(.semibold))
.foregroundColor(.primary)
.lineLimit(1)
Text(emp.statusName)
.font(.caption2)
.fontWeight(.medium)
.foregroundColor(statusColor(emp.employeeStatusId))
}
Spacer()
// Task count badge
if emp.pendingTaskCount > 0 {
Text("\(emp.pendingTaskCount)")
.font(.caption.bold())
.foregroundColor(.white)
.padding(.horizontal, 8)
.padding(.vertical, 3)
.background(Color.payfritGreen)
.clipShape(Capsule())
} else {
Image(systemName: "checkmark.circle.fill")
.foregroundColor(.green)
.font(.body)
}
Image(systemName: "chevron.right")
.font(.caption)
.foregroundColor(.secondary)
}
.padding(.horizontal, 12)
.padding(.vertical, 8)
}
.background(Color(.systemBackground))
.clipShape(RoundedRectangle(cornerRadius: 12, style: .continuous))
.overlay(
RoundedRectangle(cornerRadius: 12, style: .continuous)
.stroke(Color(.systemGray4), lineWidth: 0.5)
)
.shadow(color: .black.opacity(0.06), radius: 4, x: 0, y: 2)
.contentShape(Rectangle())
}
private var myTasksFAB: some View {
Button { showingMyTasks = true } label: {
Image(systemName: "checkmark.circle.fill")
.font(.title3)
.padding(12)
.background(Color.payfritGreen)
.foregroundColor(.white)
.clipShape(Circle())
.shadow(radius: 4)
}
.padding(.trailing, 16)
.padding(.bottom, 16)
}
// MARK: - Empty / Error
private var emptyView: some View {
VStack(spacing: 16) {
Image(systemName: "briefcase")
.font(.system(size: 64))
.foregroundColor(.secondary)
Text("No businesses found")
.font(.title3)
.foregroundColor(.secondary)
Text("You are not currently employed at any business")
.foregroundColor(.secondary)
.multilineTextAlignment(.center)
}
.padding()
}
private func errorView(_ message: String) -> some View {
VStack(spacing: 16) {
Image(systemName: "exclamationmark.circle")
.font(.system(size: 48))
.foregroundColor(.red)
Text(message)
.multilineTextAlignment(.center)
Button("Retry") { loadBusinesses() }
.buttonStyle(.borderedProminent)
}
.padding()
}
// MARK: - Actions
private func loadBusinesses(silent: Bool = false) {
if !silent {
isLoading = true
error = nil
}
Task {
do {
let result = try await APIService.shared.getMyBusinesses()
businesses = result
isLoading = false
} catch {
if !silent {
self.error = error.localizedDescription
isLoading = false
}
}
}
}
private func selectBusiness(_ emp: Employment) {
selectedBusiness = emp
showingTaskList = true
}
private func logout() {
Task {
await APIService.shared.logout()
await AuthStorage.shared.clearAuth()
appState.clearAuth()
}
}
private func loadDebugInfo() {
debugText = "Loading..."
showingDebug = true
Task {
let uid = await APIService.shared.getUserId() ?? 0
let bid = await APIService.shared.getBusinessId()
var text = "=== RAW API RESPONSES ===\n\n"
text += "UserID: \(uid), BusinessID: \(bid)\n\n"
text += "--- /workers/myBusinesses.cfm ---\n"
let bizRaw = await APIService.shared.debugRawJSON(
"/workers/myBusinesses.cfm", payload: ["UserID": uid])
text += bizRaw + "\n\n"
if bid > 0 {
text += "--- /tasks/listPending.cfm ---\n"
let taskRaw = await APIService.shared.debugRawJSON(
"/tasks/listPending.cfm", payload: ["BusinessID": bid])
text += taskRaw + "\n\n"
}
debugText = text
}
}
private func statusColor(_ id: Int) -> Color {
switch id {
case 0: return .payfritGreen
case 1: return .green
case 2: return .gray
default: return .gray
}
}
}
// MARK: - Business Header Image
struct BusinessHeaderImage: View {
let businessId: Int
@State private var loadedImage: UIImage?
@State private var isLoading = true
private var imageURLs: [URL] {
[
"https://dev.payfrit.com/uploads/headers/\(businessId).png",
"https://dev.payfrit.com/uploads/headers/\(businessId).jpg",
].compactMap { URL(string: $0) }
}
var body: some View {
ZStack {
Color(.systemGray6)
if let image = loadedImage {
Image(uiImage: image)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(maxWidth: .infinity)
} else if isLoading {
ProgressView()
.tint(.payfritGreen)
.frame(maxWidth: .infinity)
.frame(height: 100)
} else {
// No image fallback
Image(systemName: "building.2")
.font(.system(size: 30))
.foregroundColor(.secondary)
.frame(maxWidth: .infinity)
.frame(height: 100)
}
}
.task {
await loadImage()
}
}
private func loadImage() async {
for url in imageURLs {
do {
let (data, response) = try await URLSession.shared.data(from: url)
if let httpResponse = response as? HTTPURLResponse,
httpResponse.statusCode == 200,
let image = UIImage(data: data) {
await MainActor.run {
loadedImage = image
isLoading = false
}
return
}
} catch {
continue
}
}
await MainActor.run {
isLoading = false
}
}
}