Add contractors section to residence detail and fix search filtering
- Add GET /contractors/by-residence/:id endpoint integration - Display contractors on residence detail screen (iOS & Android) - Fix contractor search/filter to use client-side filtering - Backend doesn't support search query params, so filter locally 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -18,14 +18,23 @@ struct ContractorsListView: View {
|
||||
contractorSpecialties.map { $0.name }
|
||||
}
|
||||
|
||||
var filteredContractors: [ContractorSummary] {
|
||||
contractors
|
||||
}
|
||||
|
||||
var contractors: [ContractorSummary] {
|
||||
viewModel.contractors
|
||||
}
|
||||
|
||||
// Client-side filtering since backend doesn't support search/filter params
|
||||
var filteredContractors: [ContractorSummary] {
|
||||
contractors.filter { contractor in
|
||||
let matchesSearch = searchText.isEmpty ||
|
||||
contractor.name.localizedCaseInsensitiveContains(searchText) ||
|
||||
(contractor.company?.localizedCaseInsensitiveContains(searchText) ?? false)
|
||||
let matchesSpecialty = selectedSpecialty == nil ||
|
||||
contractor.specialties.contains { $0.name == selectedSpecialty }
|
||||
let matchesFavorite = !showFavoritesOnly || contractor.isFavorite
|
||||
return matchesSearch && matchesSpecialty && matchesFavorite
|
||||
}
|
||||
}
|
||||
|
||||
// Check if upgrade screen should be shown (disables add button)
|
||||
private var shouldShowUpgrade: Bool {
|
||||
subscriptionCache.shouldShowUpgradePrompt(currentCount: viewModel.contractors.count, limitKey: "contractors")
|
||||
@@ -65,9 +74,9 @@ struct ContractorsListView: View {
|
||||
.padding(.vertical, AppSpacing.xs)
|
||||
}
|
||||
|
||||
// Content
|
||||
// Content - use filteredContractors for client-side filtering
|
||||
ListAsyncContentView(
|
||||
items: contractors,
|
||||
items: filteredContractors,
|
||||
isLoading: viewModel.isLoading,
|
||||
errorMessage: viewModel.errorMessage,
|
||||
content: { contractorList in
|
||||
@@ -102,20 +111,18 @@ struct ContractorsListView: View {
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
HStack(spacing: AppSpacing.sm) {
|
||||
// Favorites Filter
|
||||
// Favorites Filter (client-side, no API call needed)
|
||||
Button(action: {
|
||||
showFavoritesOnly.toggle()
|
||||
loadContractors()
|
||||
}) {
|
||||
Image(systemName: showFavoritesOnly ? "star.fill" : "star")
|
||||
.foregroundColor(showFavoritesOnly ? Color.appAccent : Color.appTextSecondary)
|
||||
}
|
||||
|
||||
// Specialty Filter
|
||||
// Specialty Filter (client-side, no API call needed)
|
||||
Menu {
|
||||
Button(action: {
|
||||
selectedSpecialty = nil
|
||||
loadContractors()
|
||||
}) {
|
||||
Label("All Specialties", systemImage: selectedSpecialty == nil ? "checkmark" : "")
|
||||
}
|
||||
@@ -125,7 +132,6 @@ struct ContractorsListView: View {
|
||||
ForEach(specialties, id: \.self) { specialty in
|
||||
Button(action: {
|
||||
selectedSpecialty = specialty
|
||||
loadContractors()
|
||||
}) {
|
||||
Label(specialty, systemImage: selectedSpecialty == specialty ? "checkmark" : "")
|
||||
}
|
||||
@@ -167,17 +173,12 @@ struct ContractorsListView: View {
|
||||
loadContractors()
|
||||
loadContractorSpecialties()
|
||||
}
|
||||
.onChange(of: searchText) { newValue in
|
||||
loadContractors()
|
||||
}
|
||||
// No need for onChange on searchText - filtering is client-side
|
||||
}
|
||||
|
||||
private func loadContractors(forceRefresh: Bool = false) {
|
||||
viewModel.loadContractors(
|
||||
specialty: selectedSpecialty,
|
||||
isFavorite: showFavoritesOnly ? true : nil,
|
||||
search: searchText.isEmpty ? nil : searchText
|
||||
)
|
||||
// Load all contractors, filtering is done client-side
|
||||
viewModel.loadContractors()
|
||||
}
|
||||
|
||||
private func loadContractorSpecialties() {
|
||||
|
||||
@@ -10,6 +10,10 @@ struct ResidenceDetailView: View {
|
||||
@State private var tasksResponse: TaskColumnsResponse?
|
||||
@State private var isLoadingTasks = false
|
||||
@State private var tasksError: String?
|
||||
|
||||
@State private var contractors: [ContractorSummary] = []
|
||||
@State private var isLoadingContractors = false
|
||||
@State private var contractorsError: String?
|
||||
|
||||
@State private var showAddTask = false
|
||||
@State private var showEditResidence = false
|
||||
@@ -198,9 +202,12 @@ private extension ResidenceDetailView {
|
||||
PropertyHeaderCard(residence: residence)
|
||||
.padding(.horizontal)
|
||||
.padding(.top)
|
||||
|
||||
|
||||
tasksSection
|
||||
.padding(.horizontal)
|
||||
|
||||
contractorsSection
|
||||
.padding(.horizontal)
|
||||
}
|
||||
.padding(.bottom)
|
||||
}
|
||||
@@ -226,6 +233,67 @@ private extension ResidenceDetailView {
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
var contractorsSection: some View {
|
||||
VStack(alignment: .leading, spacing: AppSpacing.md) {
|
||||
// Section Header
|
||||
HStack(spacing: AppSpacing.sm) {
|
||||
Image(systemName: "person.2.fill")
|
||||
.font(.title2)
|
||||
.foregroundColor(Color.appPrimary)
|
||||
Text("Contractors")
|
||||
.font(.title2.weight(.bold))
|
||||
.foregroundColor(Color.appPrimary)
|
||||
}
|
||||
.padding(.top, AppSpacing.sm)
|
||||
|
||||
if isLoadingContractors {
|
||||
HStack {
|
||||
Spacer()
|
||||
ProgressView()
|
||||
Spacer()
|
||||
}
|
||||
.padding()
|
||||
} else if let error = contractorsError {
|
||||
Text("Error: \(error)")
|
||||
.foregroundColor(Color.appError)
|
||||
.padding()
|
||||
} else if contractors.isEmpty {
|
||||
// Empty state
|
||||
VStack(spacing: AppSpacing.md) {
|
||||
Image(systemName: "person.crop.circle.badge.plus")
|
||||
.font(.system(size: 48))
|
||||
.foregroundColor(Color.appTextSecondary.opacity(0.6))
|
||||
Text("No contractors yet")
|
||||
.font(.headline)
|
||||
.foregroundColor(Color.appTextPrimary)
|
||||
Text("Add contractors from the Contractors tab")
|
||||
.font(.subheadline)
|
||||
.foregroundColor(Color.appTextSecondary)
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(AppSpacing.xl)
|
||||
.background(Color.appBackgroundSecondary)
|
||||
.cornerRadius(AppRadius.lg)
|
||||
} else {
|
||||
// Contractors list
|
||||
VStack(spacing: AppSpacing.sm) {
|
||||
ForEach(contractors, id: \.id) { contractor in
|
||||
NavigationLink(destination: ContractorDetailView(contractorId: contractor.id)) {
|
||||
ContractorCard(
|
||||
contractor: contractor,
|
||||
onToggleFavorite: {
|
||||
// Could implement toggle favorite here if needed
|
||||
}
|
||||
)
|
||||
}
|
||||
.buttonStyle(PlainButtonStyle())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Toolbars
|
||||
@@ -299,6 +367,7 @@ private extension ResidenceDetailView {
|
||||
func loadResidenceData() {
|
||||
viewModel.getResidence(id: residenceId)
|
||||
loadResidenceTasks()
|
||||
loadResidenceContractors()
|
||||
}
|
||||
|
||||
func loadResidenceTasks() {
|
||||
@@ -365,6 +434,39 @@ private extension ResidenceDetailView {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func loadResidenceContractors() {
|
||||
guard TokenStorage.shared.getToken() != nil else { return }
|
||||
|
||||
isLoadingContractors = true
|
||||
contractorsError = nil
|
||||
|
||||
Task {
|
||||
do {
|
||||
let result = try await APILayer.shared.getContractorsByResidence(
|
||||
residenceId: Int32(Int(residenceId))
|
||||
)
|
||||
|
||||
await MainActor.run {
|
||||
if let successResult = result as? ApiResultSuccess<NSArray> {
|
||||
self.contractors = (successResult.data as? [ContractorSummary]) ?? []
|
||||
self.isLoadingContractors = false
|
||||
} else if let errorResult = result as? ApiResultError {
|
||||
self.contractorsError = errorResult.message
|
||||
self.isLoadingContractors = false
|
||||
} else {
|
||||
self.contractorsError = "Failed to load contractors"
|
||||
self.isLoadingContractors = false
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
await MainActor.run {
|
||||
self.contractorsError = error.localizedDescription
|
||||
self.isLoadingContractors = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct TasksSectionContainer: View {
|
||||
|
||||
@@ -135,4 +135,8 @@ class ResidenceViewModel: ObservableObject {
|
||||
func clearError() {
|
||||
errorMessage = nil
|
||||
}
|
||||
|
||||
func loadResidenceContractors(residenceId: Int32) {
|
||||
sharedViewModel.loadResidenceContractors(residenceId: residenceId)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user