Remove API pagination and fix contractor UI issues

- Remove pagination from all Django REST Framework endpoints
- Update Kotlin API clients to return direct lists instead of paginated responses
- Update iOS ViewModels to handle direct list responses
- Remove ContractorListResponse, DocumentListResponse, and PaginatedResponse models
- Fix contractor form specialty selector loading with improved DataCache access
- Fix contractor sheet presentation to use full screen (.presentationDetents([.large]))
- Improve UI test scrolling to handle lists of any size with smart end detection

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Trey t
2025-11-21 22:56:43 -06:00
parent 93bd50ac3e
commit e40aed31a7
25 changed files with 145 additions and 151 deletions

View File

@@ -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<ContractorSummary>, not List<Contractor>
// API returns List<ContractorSummary>, not List<Contractor>
// 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}")

View File

@@ -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<ContractorSummary>
)
// Removed: ContractorListResponse - no longer using paginated responses
// API now returns List<ContractorSummary> directly

View File

@@ -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<Document>
)
// Removed: DocumentListResponse - no longer using paginated responses
// API now returns List<Document> directly
// Document type choices
enum class DocumentType(val value: String, val displayName: String) {

View File

@@ -513,7 +513,7 @@ object APILayer {
tags: String? = null,
search: String? = null,
forceRefresh: Boolean = false
): ApiResult<DocumentListResponse> {
): ApiResult<List<Document>> {
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<ContractorListResponse> {
): ApiResult<List<ContractorSummary>> {
val hasFilters = specialty != null || isFavorite != null || isActive != null || search != null
// Note: Cannot use cache here because ContractorListResponse expects List<ContractorSummary>
// Note: Cannot use cache here because API returns List<ContractorSummary>
// but DataCache stores List<Contractor>. 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<ContractorSummary>, but we need List<Contractor>
// API returns List<ContractorSummary>, but we need List<Contractor> 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
}

View File

@@ -15,7 +15,7 @@ class ContractorApi(private val client: HttpClient = ApiClient.httpClient) {
isFavorite: Boolean? = null,
isActive: Boolean? = null,
search: String? = null
): ApiResult<ContractorListResponse> {
): ApiResult<List<ContractorSummary>> {
return try {
val response = client.get("$baseUrl/contractors/") {
header("Authorization", "Token $token")

View File

@@ -21,7 +21,7 @@ class DocumentApi(private val client: HttpClient = ApiClient.httpClient) {
expiringSoon: Int? = null,
tags: String? = null,
search: String? = null
): ApiResult<DocumentListResponse> {
): ApiResult<List<Document>> {
return try {
val response = client.get("$baseUrl/documents/") {
header("Authorization", "Token $token")

View File

@@ -112,8 +112,7 @@ class LookupsApi(private val client: HttpClient = ApiClient.httpClient) {
}
if (response.status.isSuccess()) {
val data: PaginatedResponse<CustomTask> = response.body()
ApiResult.Success(data.results)
ApiResult.Success(response.body())
} else {
ApiResult.Error("Failed to fetch tasks", response.status.value)
}

View File

@@ -16,8 +16,7 @@ class ResidenceApi(private val client: HttpClient = ApiClient.httpClient) {
}
if (response.status.isSuccess()) {
val data: PaginatedResponse<Residence> = 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<T>(
val count: Int,
val next: String?,
val previous: String?,
val results: List<T>
)
// Removed: PaginatedResponse - no longer using paginated responses
// All API endpoints now return direct lists instead of paginated responses

View File

@@ -16,8 +16,7 @@ class TaskCompletionApi(private val client: HttpClient = ApiClient.httpClient) {
}
if (response.status.isSuccess()) {
val data: PaginatedResponse<TaskCompletion> = response.body()
ApiResult.Success(data.results)
ApiResult.Success(response.body())
} else {
ApiResult.Error("Failed to fetch completions", response.status.value)
}

View File

@@ -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 {

View File

@@ -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<DocumentListResponse>,
state: ApiResult<List<Document>>,
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,

View File

@@ -246,7 +246,7 @@ fun ContractorsScreen(
}
}
) { state ->
val contractors = state.results
val contractors = state
if (contractors.isEmpty()) {
Box(

View File

@@ -11,8 +11,8 @@ import kotlinx.coroutines.launch
class ContractorViewModel : ViewModel() {
private val _contractorsState = MutableStateFlow<ApiResult<ContractorListResponse>>(ApiResult.Idle)
val contractorsState: StateFlow<ApiResult<ContractorListResponse>> = _contractorsState
private val _contractorsState = MutableStateFlow<ApiResult<List<ContractorSummary>>>(ApiResult.Idle)
val contractorsState: StateFlow<ApiResult<List<ContractorSummary>>> = _contractorsState
private val _contractorDetailState = MutableStateFlow<ApiResult<Contractor>>(ApiResult.Idle)
val contractorDetailState: StateFlow<ApiResult<Contractor>> = _contractorDetailState

View File

@@ -12,8 +12,8 @@ import kotlinx.coroutines.launch
class DocumentViewModel : ViewModel() {
private val _documentsState = MutableStateFlow<ApiResult<DocumentListResponse>>(ApiResult.Idle)
val documentsState: StateFlow<ApiResult<DocumentListResponse>> = _documentsState
private val _documentsState = MutableStateFlow<ApiResult<List<Document>>>(ApiResult.Idle)
val documentsState: StateFlow<ApiResult<List<Document>>> = _documentsState
private val _documentDetailState = MutableStateFlow<ApiResult<Document>>(ApiResult.Idle)
val documentDetailState: StateFlow<ApiResult<Document>> = _documentDetailState