import SwiftUI struct BusinessSelectionScreen: View { @EnvironmentObject var appState: AppState @State private var businesses: [Employment] = [] @State private var myTaskBusinessIds: Set = [] @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() } .onChange(of: appState.shouldPopToRoot) { shouldPop in if shouldPop { showingTaskList = false showingMyTasks = false showingAccount = false appState.shouldPopToRoot = false } } .onChange(of: appState.needsRefresh) { needsRefresh in if needsRefresh { loadBusinesses() appState.needsRefresh = false } } } .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 let hasActivity = emp.pendingTaskCount > 0 || myTaskBusinessIds.contains(emp.businessId) if hasActivity { Button { selectBusiness(emp) } label: { businessCard(emp) } .buttonStyle(.plain) } else { businessCard(emp) .opacity(0.5) } } } .padding(.horizontal, 16) .padding(.top, 8) .padding(.bottom, 80) // space for FAB } .refreshable { loadBusinesses() } } private func businessCard(_ emp: Employment) -> some View { HStack(spacing: 14) { // Initial letter Text(String(emp.businessName.prefix(1)).uppercased()) .font(.system(size: 32, weight: .bold)) .foregroundColor(.primary.opacity(0.7)) .frame(width: 50) // Name + status VStack(alignment: .leading, spacing: 4) { Text(emp.businessName) .font(.body.weight(.semibold)) .foregroundColor(.primary) .lineLimit(1) Text(emp.statusName) .font(.caption.weight(.medium)) .foregroundColor(.white) .padding(.horizontal, 8) .padding(.vertical, 2) .background(statusColor(emp.employeeStatusId)) .clipShape(Capsule()) } Spacer() // Task indicators HStack(spacing: 8) { // Red indicator if user has active tasks with this business if myTaskBusinessIds.contains(emp.businessId) { HStack(spacing: 4) { Image(systemName: "exclamationmark.circle.fill") .font(.body) .foregroundColor(.red) Text("active") .font(.caption2.weight(.medium)) .foregroundColor(.red) } } // Pending task count or clear checkmark if emp.pendingTaskCount > 0 { Text("\(emp.pendingTaskCount)") .font(.title3.bold()) .foregroundColor(.payfritGreen) } else if !myTaskBusinessIds.contains(emp.businessId) { VStack(spacing: 2) { Image(systemName: "checkmark.circle.fill") .font(.title2) .foregroundColor(.payfritGreen) Text("clear") .font(.caption2) .foregroundColor(.secondary) } } } Image(systemName: "chevron.right") .font(.caption.weight(.semibold)) .foregroundColor(.secondary) } .padding(.horizontal, 16) .padding(.vertical, 14) .background(Color.payfritGreen.opacity(0.08)) .clipShape(RoundedRectangle(cornerRadius: 12, style: .continuous)) .contentShape(Rectangle()) } private var myTasksFAB: some View { Button { showingMyTasks = true } label: { HStack(spacing: 8) { Image(systemName: "checkmark.square.fill") .font(.title3) Text("My Tasks") .font(.body.weight(.semibold)) } .padding(.horizontal, 20) .padding(.vertical, 14) .background(Color.payfritGreen) .foregroundColor(.white) .clipShape(RoundedRectangle(cornerRadius: 16, style: .continuous)) .shadow(color: .black.opacity(0.15), radius: 6, x: 0, y: 3) } .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 // Fetch user's active tasks to show indicator let myTasks = try? await APIService.shared.listMyTasks(filterType: "active") if let tasks = myTasks { myTaskBusinessIds = Set(tasks.map { $0.businessId }) } 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] { let domain = IS_DEV ? "https://dev.payfrit.com" : "https://biz.payfrit.com" return [ "\(domain)/uploads/headers/\(businessId).png", "\(domain)/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 } } }