// // EmployeesViewModel.swift // EmployeeDirectory // // Created by Matt Bruce on 1/20/25. // import Foundation import Combine /// ViewModel that will be bound to an Employees model and used /// specifically with the EmployeesViewController. @MainActor public class EmployeesViewModel: ObservableObject { private var serviceMode: EmployeeServiceMode = .production private var employeeService: EmployeeServiceProtocol = MockEmployeeService.shared private var allEmployees: [Employee] = [] @Published public private(set) var employees: [Employee] = [] @Published public private(set) var errorMessage: String? = nil @Published public private(set) var isLoading: Bool = false @Published public private(set) var hasMorePages: Bool = true @Published public var searchText: String = "" @Published public var sortField: EmployeeSortField = .fullName @Published public var sortOrder: EmployeeSortOrder = .ascending private var cancellables = Set() private var currentPage = 1 private let perPage = 10 private var totalEmployees = 0 public init() { setupSearch() observeSortingChanges() } /// Sets up Combine to filter employees as the search text changes private func setupSearch() { $searchText .debounce(for: .milliseconds(300), scheduler: DispatchQueue.main) // Add delay to avoid frequent filtering .removeDuplicates() .sink { [weak self] query in self?.filterEmployees(by: query) } .store(in: &cancellables) } /// Observe changes to sortField and sortOrder and debounce fetch calls private func observeSortingChanges() { Publishers.CombineLatest($sortField, $sortOrder) .dropFirst() .debounce(for: .milliseconds(300), scheduler: DispatchQueue.main) .sink { [weak self] _, _ in self?.resetAndFetchEmployees() } .store(in: &cancellables) } /// Fetch employees for the given page public func fetchEmployees(page: Int = 1) { // Prevent duplicate calls guard !isLoading else { return } // resetting values out the values before fetching new data errorMessage = nil isLoading = true Task { do { // Fetch employees using the paginated API let wrapper = try await employeeService.getEmployees(page: page, perPage: perPage, sortField: sortField, sortOrder: sortOrder) totalEmployees = wrapper.total currentPage = page hasMorePages = wrapper.employees.count < totalEmployees // Update published properties if page == 1 { allEmployees = wrapper.employees // Replace list for the first page } else { allEmployees.append(contentsOf: wrapper.employees) // Append for subsequent pages } filterEmployees(by: searchText) } catch { // Handle errors errorMessage = "An unexpected error occurred, please try to refresh." } isLoading = false } } /// Load the next page of employees public func loadNextPage() { guard hasMorePages else { return } fetchEmployees(page: currentPage + 1) } /// Resets the current employee list and fetches data from page 1 private func resetAndFetchEmployees() { currentPage = 1 employees = [] allEmployees = [] hasMorePages = true fetchEmployees(page: 1) } public func changeService(to employeeService: EmployeeServiceProtocol) { self.employeeService = employeeService resetAndFetchEmployees() } /// Filters employees based on search query private func filterEmployees(by query: String) { if query.isEmpty { employees = allEmployees // Reset to all employees if no search text } else { employees = allEmployees.filter { employee in employee.fullName.lowercased().contains(query.lowercased()) || employee.team.lowercased().contains(query.lowercased()) || employee.emailAddress.lowercased().contains(query.lowercased()) } } } }