diff --git a/composeApp/src/commonMain/kotlin/com/example/mycrib/cache/DataPrefetchManager.kt b/composeApp/src/commonMain/kotlin/com/example/mycrib/cache/DataPrefetchManager.kt index 8537b74..21103de 100644 --- a/composeApp/src/commonMain/kotlin/com/example/mycrib/cache/DataPrefetchManager.kt +++ b/composeApp/src/commonMain/kotlin/com/example/mycrib/cache/DataPrefetchManager.kt @@ -154,8 +154,8 @@ class DataPrefetchManager { search = null ) if (result is ApiResult.Success) { - DataCache.updateDocuments(result.data.results) - println("DataPrefetchManager: Cached ${result.data.results.size} documents") + DataCache.updateDocuments(result.data) + println("DataPrefetchManager: Cached ${result.data.size} documents") } } catch (e: Exception) { println("DataPrefetchManager: Error fetching documents: ${e.message}") @@ -173,9 +173,9 @@ class DataPrefetchManager { search = null ) if (result is ApiResult.Success) { - // ContractorListResponse.results is List, not List + // API returns List, not List // Skip caching for now - full Contractor objects will be cached when fetched individually - println("DataPrefetchManager: Fetched ${result.data.results.size} contractor summaries") + println("DataPrefetchManager: Fetched ${result.data.size} contractor summaries") } } catch (e: Exception) { println("DataPrefetchManager: Error fetching contractors: ${e.message}") diff --git a/composeApp/src/commonMain/kotlin/com/example/mycrib/models/Contractor.kt b/composeApp/src/commonMain/kotlin/com/example/mycrib/models/Contractor.kt index b057b4c..c4e2595 100644 --- a/composeApp/src/commonMain/kotlin/com/example/mycrib/models/Contractor.kt +++ b/composeApp/src/commonMain/kotlin/com/example/mycrib/models/Contractor.kt @@ -79,10 +79,5 @@ data class ContractorSummary( @SerialName("task_count") val taskCount: Int = 0 ) -@Serializable -data class ContractorListResponse( - val count: Int, - val next: String? = null, - val previous: String? = null, - val results: List -) +// Removed: ContractorListResponse - no longer using paginated responses +// API now returns List directly diff --git a/composeApp/src/commonMain/kotlin/com/example/mycrib/models/Document.kt b/composeApp/src/commonMain/kotlin/com/example/mycrib/models/Document.kt index 9633e82..f615485 100644 --- a/composeApp/src/commonMain/kotlin/com/example/mycrib/models/Document.kt +++ b/composeApp/src/commonMain/kotlin/com/example/mycrib/models/Document.kt @@ -107,13 +107,8 @@ data class DocumentUpdateRequest( @SerialName("is_active") val isActive: Boolean? = null ) -@Serializable -data class DocumentListResponse( - val count: Int, - val next: String? = null, - val previous: String? = null, - val results: List -) +// Removed: DocumentListResponse - no longer using paginated responses +// API now returns List directly // Document type choices enum class DocumentType(val value: String, val displayName: String) { diff --git a/composeApp/src/commonMain/kotlin/com/example/mycrib/network/APILayer.kt b/composeApp/src/commonMain/kotlin/com/example/mycrib/network/APILayer.kt index 1d97fc8..b499e4b 100644 --- a/composeApp/src/commonMain/kotlin/com/example/mycrib/network/APILayer.kt +++ b/composeApp/src/commonMain/kotlin/com/example/mycrib/network/APILayer.kt @@ -513,7 +513,7 @@ object APILayer { tags: String? = null, search: String? = null, forceRefresh: Boolean = false - ): ApiResult { + ): ApiResult> { val hasFilters = residenceId != null || documentType != null || category != null || contractorId != null || isActive != null || expiringSoon != null || tags != null || search != null @@ -522,10 +522,7 @@ object APILayer { if (!forceRefresh && !hasFilters) { val cached = DataCache.documents.value if (cached.isNotEmpty()) { - return ApiResult.Success(DocumentListResponse( - count = cached.size, - results = cached - )) + return ApiResult.Success(cached) } } @@ -538,7 +535,7 @@ object APILayer { // Update cache on success if no filters if (result is ApiResult.Success && !hasFilters) { - DataCache.updateDocuments(result.data.results) + DataCache.updateDocuments(result.data) } return result @@ -688,10 +685,10 @@ object APILayer { isActive: Boolean? = null, search: String? = null, forceRefresh: Boolean = false - ): ApiResult { + ): ApiResult> { val hasFilters = specialty != null || isFavorite != null || isActive != null || search != null - // Note: Cannot use cache here because ContractorListResponse expects List + // Note: Cannot use cache here because API returns List // but DataCache stores List. Cache is only used for individual contractor lookups. // Fetch from API @@ -700,7 +697,7 @@ object APILayer { // Update cache on success if no filters if (result is ApiResult.Success && !hasFilters) { - // ContractorListResponse.results is List, but we need List + // API returns List, but we need List for cache // For now, we'll skip caching from this endpoint since it returns summaries // Cache will be populated from getContractor() or create/update operations } diff --git a/composeApp/src/commonMain/kotlin/com/example/mycrib/network/ContractorApi.kt b/composeApp/src/commonMain/kotlin/com/example/mycrib/network/ContractorApi.kt index 0b744bb..d291769 100644 --- a/composeApp/src/commonMain/kotlin/com/example/mycrib/network/ContractorApi.kt +++ b/composeApp/src/commonMain/kotlin/com/example/mycrib/network/ContractorApi.kt @@ -15,7 +15,7 @@ class ContractorApi(private val client: HttpClient = ApiClient.httpClient) { isFavorite: Boolean? = null, isActive: Boolean? = null, search: String? = null - ): ApiResult { + ): ApiResult> { return try { val response = client.get("$baseUrl/contractors/") { header("Authorization", "Token $token") diff --git a/composeApp/src/commonMain/kotlin/com/example/mycrib/network/DocumentApi.kt b/composeApp/src/commonMain/kotlin/com/example/mycrib/network/DocumentApi.kt index 62fdca4..c241f44 100644 --- a/composeApp/src/commonMain/kotlin/com/example/mycrib/network/DocumentApi.kt +++ b/composeApp/src/commonMain/kotlin/com/example/mycrib/network/DocumentApi.kt @@ -21,7 +21,7 @@ class DocumentApi(private val client: HttpClient = ApiClient.httpClient) { expiringSoon: Int? = null, tags: String? = null, search: String? = null - ): ApiResult { + ): ApiResult> { return try { val response = client.get("$baseUrl/documents/") { header("Authorization", "Token $token") diff --git a/composeApp/src/commonMain/kotlin/com/example/mycrib/network/LookupsApi.kt b/composeApp/src/commonMain/kotlin/com/example/mycrib/network/LookupsApi.kt index 994d1eb..395ebd6 100644 --- a/composeApp/src/commonMain/kotlin/com/example/mycrib/network/LookupsApi.kt +++ b/composeApp/src/commonMain/kotlin/com/example/mycrib/network/LookupsApi.kt @@ -112,8 +112,7 @@ class LookupsApi(private val client: HttpClient = ApiClient.httpClient) { } if (response.status.isSuccess()) { - val data: PaginatedResponse = response.body() - ApiResult.Success(data.results) + ApiResult.Success(response.body()) } else { ApiResult.Error("Failed to fetch tasks", response.status.value) } diff --git a/composeApp/src/commonMain/kotlin/com/example/mycrib/network/ResidenceApi.kt b/composeApp/src/commonMain/kotlin/com/example/mycrib/network/ResidenceApi.kt index 302daab..f5724f9 100644 --- a/composeApp/src/commonMain/kotlin/com/example/mycrib/network/ResidenceApi.kt +++ b/composeApp/src/commonMain/kotlin/com/example/mycrib/network/ResidenceApi.kt @@ -16,8 +16,7 @@ class ResidenceApi(private val client: HttpClient = ApiClient.httpClient) { } if (response.status.isSuccess()) { - val data: PaginatedResponse = response.body() - ApiResult.Success(data.results) + ApiResult.Success(response.body()) } else { ApiResult.Error("Failed to fetch residences", response.status.value) } @@ -245,10 +244,5 @@ data class GenerateReportResponse( val recipient_email: String ) -@kotlinx.serialization.Serializable -data class PaginatedResponse( - val count: Int, - val next: String?, - val previous: String?, - val results: List -) +// Removed: PaginatedResponse - no longer using paginated responses +// All API endpoints now return direct lists instead of paginated responses diff --git a/composeApp/src/commonMain/kotlin/com/example/mycrib/network/TaskCompletionApi.kt b/composeApp/src/commonMain/kotlin/com/example/mycrib/network/TaskCompletionApi.kt index da8018c..dc1363a 100644 --- a/composeApp/src/commonMain/kotlin/com/example/mycrib/network/TaskCompletionApi.kt +++ b/composeApp/src/commonMain/kotlin/com/example/mycrib/network/TaskCompletionApi.kt @@ -16,8 +16,7 @@ class TaskCompletionApi(private val client: HttpClient = ApiClient.httpClient) { } if (response.status.isSuccess()) { - val data: PaginatedResponse = response.body() - ApiResult.Success(data.results) + ApiResult.Success(response.body()) } else { ApiResult.Error("Failed to fetch completions", response.status.value) } diff --git a/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/components/CompleteTaskDialog.kt b/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/components/CompleteTaskDialog.kt index 6fb3ee3..a1ebf37 100644 --- a/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/components/CompleteTaskDialog.kt +++ b/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/components/CompleteTaskDialog.kt @@ -102,7 +102,7 @@ fun CompleteTaskDialog( // Contractor list when (val state = contractorsState) { is ApiResult.Success -> { - state.data.results.forEach { contractor -> + state.data.forEach { contractor -> DropdownMenuItem( text = { Column { diff --git a/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/components/documents/DocumentsTabContent.kt b/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/components/documents/DocumentsTabContent.kt index 50e6d0f..996524a 100644 --- a/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/components/documents/DocumentsTabContent.kt +++ b/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/components/documents/DocumentsTabContent.kt @@ -13,13 +13,13 @@ import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp -import com.mycrib.shared.models.DocumentListResponse +import com.mycrib.shared.models.Document import com.mycrib.shared.network.ApiResult @OptIn(ExperimentalMaterial3Api::class) @Composable fun DocumentsTabContent( - state: ApiResult, + state: ApiResult>, isWarrantyTab: Boolean, onDocumentClick: (Int) -> Unit, onRetry: () -> Unit @@ -42,7 +42,7 @@ fun DocumentsTabContent( } } is ApiResult.Success -> { - val documents = state.data.results + val documents = state.data if (documents.isEmpty()) { EmptyState( icon = if (isWarrantyTab) Icons.Default.ReceiptLong else Icons.Default.Description, diff --git a/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/screens/ContractorsScreen.kt b/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/screens/ContractorsScreen.kt index 5f87998..d3a61ff 100644 --- a/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/screens/ContractorsScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/screens/ContractorsScreen.kt @@ -246,7 +246,7 @@ fun ContractorsScreen( } } ) { state -> - val contractors = state.results + val contractors = state if (contractors.isEmpty()) { Box( diff --git a/composeApp/src/commonMain/kotlin/com/example/mycrib/viewmodel/ContractorViewModel.kt b/composeApp/src/commonMain/kotlin/com/example/mycrib/viewmodel/ContractorViewModel.kt index 1188f31..1170cfe 100644 --- a/composeApp/src/commonMain/kotlin/com/example/mycrib/viewmodel/ContractorViewModel.kt +++ b/composeApp/src/commonMain/kotlin/com/example/mycrib/viewmodel/ContractorViewModel.kt @@ -11,8 +11,8 @@ import kotlinx.coroutines.launch class ContractorViewModel : ViewModel() { - private val _contractorsState = MutableStateFlow>(ApiResult.Idle) - val contractorsState: StateFlow> = _contractorsState + private val _contractorsState = MutableStateFlow>>(ApiResult.Idle) + val contractorsState: StateFlow>> = _contractorsState private val _contractorDetailState = MutableStateFlow>(ApiResult.Idle) val contractorDetailState: StateFlow> = _contractorDetailState diff --git a/composeApp/src/commonMain/kotlin/com/example/mycrib/viewmodel/DocumentViewModel.kt b/composeApp/src/commonMain/kotlin/com/example/mycrib/viewmodel/DocumentViewModel.kt index eb5ca47..db10c15 100644 --- a/composeApp/src/commonMain/kotlin/com/example/mycrib/viewmodel/DocumentViewModel.kt +++ b/composeApp/src/commonMain/kotlin/com/example/mycrib/viewmodel/DocumentViewModel.kt @@ -12,8 +12,8 @@ import kotlinx.coroutines.launch class DocumentViewModel : ViewModel() { - private val _documentsState = MutableStateFlow>(ApiResult.Idle) - val documentsState: StateFlow> = _documentsState + private val _documentsState = MutableStateFlow>>(ApiResult.Idle) + val documentsState: StateFlow>> = _documentsState private val _documentDetailState = MutableStateFlow>(ApiResult.Idle) val documentDetailState: StateFlow> = _documentDetailState diff --git a/iosApp/MyCrib/MyCrib.swift b/iosApp/MyCrib/MyCrib.swift index 2abf840..b3f3c54 100644 --- a/iosApp/MyCrib/MyCrib.swift +++ b/iosApp/MyCrib/MyCrib.swift @@ -302,7 +302,7 @@ struct TaskRowView: View { VStack(alignment: .leading, spacing: 2) { Text(task.title) .font(.system(size: 11, weight: .semibold)) - .lineLimit(1) +// .lineLimit(1) .foregroundStyle(.primary) if let dueDate = task.dueDate { diff --git a/iosApp/MyCribUITests/ComprehensiveContractorTests.swift b/iosApp/MyCribUITests/ComprehensiveContractorTests.swift index 8c802eb..c06a15e 100644 --- a/iosApp/MyCribUITests/ComprehensiveContractorTests.swift +++ b/iosApp/MyCribUITests/ComprehensiveContractorTests.swift @@ -151,8 +151,55 @@ final class ComprehensiveContractorTests: XCTestCase { return true } - private func findContractor(name: String) -> XCUIElement { - return app.staticTexts.containing(NSPredicate(format: "label CONTAINS '\(name)'")).firstMatch + private func findContractor(name: String, scrollIfNeeded: Bool = true) -> XCUIElement { + let element = app.staticTexts.containing(NSPredicate(format: "label CONTAINS[c] %@", name)).firstMatch + + // If element is visible, return it immediately + if element.exists && element.isHittable { + return element + } + + // If scrolling is not needed, return the element as-is + guard scrollIfNeeded else { + return element + } + + // Get the scroll view + let scrollView = app.scrollViews.firstMatch + guard scrollView.exists else { + return element + } + + // First, scroll to the top of the list + scrollView.swipeDown(velocity: .fast) + usleep(30_000) // 0.03 second delay + + // Now scroll down from top, checking after each swipe + var lastVisibleRow = "" + for _ in 0.. { + } else if let success = state as? ApiResultSuccess { await MainActor.run { - self.contractors = success.data?.results ?? [] + self.contractors = success.data as? [ContractorSummary] ?? [] self.isLoading = false } break diff --git a/iosApp/iosApp/Contractor/ContractorsListView.swift b/iosApp/iosApp/Contractor/ContractorsListView.swift index a9827f7..d8e8ede 100644 --- a/iosApp/iosApp/Contractor/ContractorsListView.swift +++ b/iosApp/iosApp/Contractor/ContractorsListView.swift @@ -159,6 +159,7 @@ struct ContractorsListView: View { loadContractors() } ) + .presentationDetents([.large]) } .onAppear { loadContractors() @@ -183,8 +184,17 @@ struct ContractorsListView: View { private func loadContractorSpecialties() { Task { + // Small delay to ensure DataCache is populated + try? await Task.sleep(nanoseconds: 100_000_000) // 0.1 seconds + await MainActor.run { - self.contractorSpecialties = DataCache.shared.contractorSpecialties.value as! [ContractorSpecialty] + if let specialties = DataCache.shared.contractorSpecialties.value as? [ContractorSpecialty] { + self.contractorSpecialties = specialties + print("✅ ContractorsList: Loaded \(specialties.count) contractor specialties") + } else { + print("❌ ContractorsList: Failed to load contractor specialties from DataCache") + self.contractorSpecialties = [] + } } } } diff --git a/iosApp/iosApp/Documents/Components/DocumentCard.swift b/iosApp/iosApp/Documents/Components/DocumentCard.swift index be96b9a..b98ff5e 100644 --- a/iosApp/iosApp/Documents/Components/DocumentCard.swift +++ b/iosApp/iosApp/Documents/Components/DocumentCard.swift @@ -42,7 +42,7 @@ struct DocumentCard: View { .font(.title3.weight(.semibold)) .fontWeight(.bold) .foregroundColor(Color.appTextPrimary) - .lineLimit(1) +// .lineLimit(1) if let description = document.description_, !description.isEmpty { Text(description) diff --git a/iosApp/iosApp/Documents/DocumentViewModel.swift b/iosApp/iosApp/Documents/DocumentViewModel.swift index 9f1d8f4..cb593e9 100644 --- a/iosApp/iosApp/Documents/DocumentViewModel.swift +++ b/iosApp/iosApp/Documents/DocumentViewModel.swift @@ -49,9 +49,9 @@ class DocumentViewModel: ObservableObject { await MainActor.run { self.isLoading = true } - } else if let success = state as? ApiResultSuccess { + } else if let success = state as? ApiResultSuccess { await MainActor.run { - self.documents = success.data?.results as? [Document] ?? [] + self.documents = success.data as? [Document] ?? [] self.isLoading = false } break diff --git a/iosApp/iosApp/Documents/DocumentViewModelWrapper.swift b/iosApp/iosApp/Documents/DocumentViewModelWrapper.swift index 36c6902..aae7df0 100644 --- a/iosApp/iosApp/Documents/DocumentViewModelWrapper.swift +++ b/iosApp/iosApp/Documents/DocumentViewModelWrapper.swift @@ -94,8 +94,8 @@ class DocumentViewModelWrapper: ObservableObject { ) await MainActor.run { - if let success = result as? ApiResultSuccess { - let documents = success.data?.results as? [Document] ?? [] + if let success = result as? ApiResultSuccess { + let documents = success.data as? [Document] ?? [] self.documentsState = DocumentStateSuccess(documents: documents) } else if let error = result as? ApiResultError { self.documentsState = DocumentStateError(message: error.message) diff --git a/iosApp/iosApp/Subviews/Residence/ResidenceCard.swift b/iosApp/iosApp/Subviews/Residence/ResidenceCard.swift index 119c5f7..b7f33d8 100644 --- a/iosApp/iosApp/Subviews/Residence/ResidenceCard.swift +++ b/iosApp/iosApp/Subviews/Residence/ResidenceCard.swift @@ -24,7 +24,7 @@ struct ResidenceCard: View { .font(.title3.weight(.semibold)) .fontWeight(.bold) .foregroundColor(Color.appTextPrimary) - .lineLimit(1) +// .lineLimit(1) if let propertyType = residence.propertyType { Text(propertyType)