diff --git a/composeApp/src/commonMain/kotlin/com/example/casera/models/Residence.kt b/composeApp/src/commonMain/kotlin/com/example/casera/models/Residence.kt index d8e30d0..6b5be40 100644 --- a/composeApp/src/commonMain/kotlin/com/example/casera/models/Residence.kt +++ b/composeApp/src/commonMain/kotlin/com/example/casera/models/Residence.kt @@ -165,7 +165,8 @@ data class JoinResidenceRequest( @Serializable data class JoinResidenceResponse( val message: String, - val residence: ResidenceResponse + val residence: ResidenceResponse, + val summary: TotalSummary ) /** @@ -181,6 +182,22 @@ data class TotalSummary( @SerialName("tasks_due_next_month") val tasksDueNextMonth: Int = 0 ) +/** + * Generic wrapper for CRUD responses that include TotalSummary. + * Used for Task and TaskCompletion operations to eliminate extra API calls + * for updating dashboard stats. + * + * Usage examples: + * - WithSummaryResponse for task CRUD + * - WithSummaryResponse for completion CRUD + * - WithSummaryResponse for delete operations (data = "task deleted") + */ +@Serializable +data class WithSummaryResponse( + val data: T, + val summary: TotalSummary +) + /** * My residences response - list of user's residences * Go API returns array directly, this wraps for consistency diff --git a/composeApp/src/commonMain/kotlin/com/example/casera/network/APILayer.kt b/composeApp/src/commonMain/kotlin/com/example/casera/network/APILayer.kt index 1379344..46cc63e 100644 --- a/composeApp/src/commonMain/kotlin/com/example/casera/network/APILayer.kt +++ b/composeApp/src/commonMain/kotlin/com/example/casera/network/APILayer.kt @@ -424,42 +424,51 @@ object APILayer { val token = getToken() ?: return ApiResult.Error("Not authenticated", 401) val result = residenceApi.createResidence(token, request) - // Update DataManager on success + // Extract summary and update local cache if (result is ApiResult.Success) { - DataManager.addResidence(result.data) - // Also refresh my-residences to get updated list - refreshMyResidences() + DataManager.setTotalSummary(result.data.summary) + DataManager.addResidence(result.data.data) + return ApiResult.Success(result.data.data) } - return result + return when (result) { + is ApiResult.Error -> result + else -> ApiResult.Error("Unknown error") + } } suspend fun updateResidence(id: Int, request: ResidenceCreateRequest): ApiResult { val token = getToken() ?: return ApiResult.Error("Not authenticated", 401) val result = residenceApi.updateResidence(token, id, request) - // Update DataManager on success + // Extract summary and update local cache if (result is ApiResult.Success) { - DataManager.updateResidence(result.data) - // Also refresh my-residences to get updated list - refreshMyResidences() + DataManager.setTotalSummary(result.data.summary) + DataManager.updateResidence(result.data.data) + return ApiResult.Success(result.data.data) } - return result + return when (result) { + is ApiResult.Error -> result + else -> ApiResult.Error("Unknown error") + } } suspend fun deleteResidence(id: Int): ApiResult { val token = getToken() ?: return ApiResult.Error("Not authenticated", 401) val result = residenceApi.deleteResidence(token, id) - // Update DataManager on success + // Extract summary and update local cache if (result is ApiResult.Success) { + DataManager.setTotalSummary(result.data.summary) DataManager.removeResidence(id) - // Also refresh my-residences to get updated list - refreshMyResidences() + return ApiResult.Success(Unit) } - return result + return when (result) { + is ApiResult.Error -> result + else -> ApiResult.Error("Unknown error") + } } suspend fun generateTasksReport(residenceId: Int, email: String? = null): ApiResult { @@ -471,9 +480,10 @@ object APILayer { val token = getToken() ?: return ApiResult.Error("Not authenticated", 401) val result = residenceApi.joinWithCode(token, code) - // Refresh residences after joining + // Extract summary and update local cache if (result is ApiResult.Success) { - refreshMyResidences() + DataManager.setTotalSummary(result.data.summary) + DataManager.addResidence(result.data.residence) } return result @@ -552,24 +562,36 @@ object APILayer { val token = getToken() ?: return ApiResult.Error("Not authenticated", 401) val result = taskApi.createTask(token, request) - // Refresh tasks on success + // Extract summary and update local cache with new task if (result is ApiResult.Success) { - refreshTasks() + DataManager.setTotalSummary(result.data.summary) + // Add the new task to the appropriate kanban column (uses kanbanColumn from response) + DataManager.updateTask(result.data.data) + return ApiResult.Success(result.data.data) } - return result + return when (result) { + is ApiResult.Error -> result + else -> ApiResult.Error("Unknown error") + } } suspend fun updateTask(id: Int, request: TaskCreateRequest): ApiResult { val token = getToken() ?: return ApiResult.Error("Not authenticated", 401) val result = taskApi.updateTask(token, id, request) - // Refresh tasks on success + // Extract summary and update local cache with modified task if (result is ApiResult.Success) { - refreshTasks() + DataManager.setTotalSummary(result.data.summary) + // Update task in cache (handles column changes if due date changed) + DataManager.updateTask(result.data.data) + return ApiResult.Success(result.data.data) } - return result + return when (result) { + is ApiResult.Error -> result + else -> ApiResult.Error("Unknown error") + } } /** @@ -590,10 +612,15 @@ object APILayer { val result = taskApi.cancelTask(token, taskId, cancelledStatusId) if (result is ApiResult.Success) { - DataManager.updateTask(result.data) + DataManager.setTotalSummary(result.data.summary) + DataManager.updateTask(result.data.data) + return ApiResult.Success(result.data.data) } - return result + return when (result) { + is ApiResult.Error -> result + else -> ApiResult.Error("Unknown error") + } } suspend fun uncancelTask(taskId: Int): ApiResult { @@ -605,10 +632,15 @@ object APILayer { val result = taskApi.uncancelTask(token, taskId, pendingStatusId) if (result is ApiResult.Success) { - DataManager.updateTask(result.data) + DataManager.setTotalSummary(result.data.summary) + DataManager.updateTask(result.data.data) + return ApiResult.Success(result.data.data) } - return result + return when (result) { + is ApiResult.Error -> result + else -> ApiResult.Error("Unknown error") + } } suspend fun markInProgress(taskId: Int): ApiResult { @@ -621,10 +653,15 @@ object APILayer { val result = taskApi.markInProgress(token, taskId, inProgressStatusId) if (result is ApiResult.Success) { - DataManager.updateTask(result.data) + DataManager.setTotalSummary(result.data.summary) + DataManager.updateTask(result.data.data) + return ApiResult.Success(result.data.data) } - return result + return when (result) { + is ApiResult.Error -> result + else -> ApiResult.Error("Unknown error") + } } suspend fun archiveTask(taskId: Int): ApiResult { @@ -632,10 +669,15 @@ object APILayer { val result = taskApi.archiveTask(token, taskId) if (result is ApiResult.Success) { + DataManager.setTotalSummary(result.data.summary) DataManager.removeTask(taskId) + return ApiResult.Success(result.data.data) } - return result + return when (result) { + is ApiResult.Error -> result + else -> ApiResult.Error("Unknown error") + } } suspend fun unarchiveTask(taskId: Int): ApiResult { @@ -643,10 +685,15 @@ object APILayer { val result = taskApi.unarchiveTask(token, taskId) if (result is ApiResult.Success) { - DataManager.updateTask(result.data) + DataManager.setTotalSummary(result.data.summary) + DataManager.updateTask(result.data.data) + return ApiResult.Success(result.data.data) } - return result + return when (result) { + is ApiResult.Error -> result + else -> ApiResult.Error("Unknown error") + } } suspend fun createTaskCompletion(request: TaskCompletionCreateRequest): ApiResult { @@ -654,15 +701,19 @@ object APILayer { val result = taskCompletionApi.createCompletion(token, request) if (result is ApiResult.Success) { + // Update summary from response - eliminates need for separate getSummary call + DataManager.setTotalSummary(result.data.summary) // The response includes the updated task, update it in DataManager - result.data.updatedTask?.let { updatedTask -> + result.data.data.updatedTask?.let { updatedTask -> DataManager.updateTask(updatedTask) } - // Refresh my-residences to update per-residence overdueCount and summary - refreshMyResidences() + return ApiResult.Success(result.data.data) } - return result + return when (result) { + is ApiResult.Error -> result + else -> ApiResult.Error("Unknown error") + } } suspend fun createTaskCompletionWithImages( @@ -674,15 +725,19 @@ object APILayer { val result = taskCompletionApi.createCompletionWithImages(token, request, images, imageFileNames) if (result is ApiResult.Success) { + // Update summary from response - eliminates need for separate getSummary call + DataManager.setTotalSummary(result.data.summary) // The response includes the updated task, update it in DataManager - result.data.updatedTask?.let { updatedTask -> + result.data.data.updatedTask?.let { updatedTask -> DataManager.updateTask(updatedTask) } - // Refresh my-residences to update per-residence overdueCount and summary - refreshMyResidences() + return ApiResult.Success(result.data.data) } - return result + return when (result) { + is ApiResult.Error -> result + else -> ApiResult.Error("Unknown error") + } } /** @@ -1240,27 +1295,6 @@ object APILayer { // ==================== Helper Methods ==================== - /** - * Refresh all tasks from API - */ - private suspend fun refreshTasks() { - getTasks(forceRefresh = true) - } - - /** - * Refresh my-residences from API - */ - private suspend fun refreshMyResidences() { - getMyResidences(forceRefresh = true) - } - - /** - * Refresh just the summary counts (lightweight) - */ - private suspend fun refreshSummary() { - getSummary(forceRefresh = true) - } - /** * Prefetch all data after login */ diff --git a/composeApp/src/commonMain/kotlin/com/example/casera/network/ResidenceApi.kt b/composeApp/src/commonMain/kotlin/com/example/casera/network/ResidenceApi.kt index 06b9d02..5bb2bb3 100644 --- a/composeApp/src/commonMain/kotlin/com/example/casera/network/ResidenceApi.kt +++ b/composeApp/src/commonMain/kotlin/com/example/casera/network/ResidenceApi.kt @@ -41,7 +41,7 @@ class ResidenceApi(private val client: HttpClient = ApiClient.httpClient) { } } - suspend fun createResidence(token: String, request: ResidenceCreateRequest): ApiResult { + suspend fun createResidence(token: String, request: ResidenceCreateRequest): ApiResult> { return try { val response = client.post("$baseUrl/residences/") { header("Authorization", "Token $token") @@ -59,7 +59,7 @@ class ResidenceApi(private val client: HttpClient = ApiClient.httpClient) { } } - suspend fun updateResidence(token: String, id: Int, request: ResidenceCreateRequest): ApiResult { + suspend fun updateResidence(token: String, id: Int, request: ResidenceCreateRequest): ApiResult> { return try { val response = client.put("$baseUrl/residences/$id/") { header("Authorization", "Token $token") @@ -77,14 +77,14 @@ class ResidenceApi(private val client: HttpClient = ApiClient.httpClient) { } } - suspend fun deleteResidence(token: String, id: Int): ApiResult { + suspend fun deleteResidence(token: String, id: Int): ApiResult> { return try { val response = client.delete("$baseUrl/residences/$id/") { header("Authorization", "Token $token") } if (response.status.isSuccess()) { - ApiResult.Success(Unit) + ApiResult.Success(response.body()) } else { ApiResult.Error("Failed to delete residence", response.status.value) } diff --git a/composeApp/src/commonMain/kotlin/com/example/casera/network/TaskApi.kt b/composeApp/src/commonMain/kotlin/com/example/casera/network/TaskApi.kt index e0c6ff6..94b594d 100644 --- a/composeApp/src/commonMain/kotlin/com/example/casera/network/TaskApi.kt +++ b/composeApp/src/commonMain/kotlin/com/example/casera/network/TaskApi.kt @@ -47,7 +47,7 @@ class TaskApi(private val client: HttpClient = ApiClient.httpClient) { } } - suspend fun createTask(token: String, request: TaskCreateRequest): ApiResult { + suspend fun createTask(token: String, request: TaskCreateRequest): ApiResult> { return try { val response = client.post("$baseUrl/tasks/") { header("Authorization", "Token $token") @@ -66,7 +66,7 @@ class TaskApi(private val client: HttpClient = ApiClient.httpClient) { } } - suspend fun updateTask(token: String, id: Int, request: TaskCreateRequest): ApiResult { + suspend fun updateTask(token: String, id: Int, request: TaskCreateRequest): ApiResult> { return try { val response = client.put("$baseUrl/tasks/$id/") { header("Authorization", "Token $token") @@ -85,14 +85,14 @@ class TaskApi(private val client: HttpClient = ApiClient.httpClient) { } } - suspend fun deleteTask(token: String, id: Int): ApiResult { + suspend fun deleteTask(token: String, id: Int): ApiResult> { return try { val response = client.delete("$baseUrl/tasks/$id/") { header("Authorization", "Token $token") } if (response.status.isSuccess()) { - ApiResult.Success(Unit) + ApiResult.Success(response.body()) } else { val errorMessage = ErrorParser.parseError(response) ApiResult.Error(errorMessage, response.status.value) @@ -127,12 +127,9 @@ class TaskApi(private val client: HttpClient = ApiClient.httpClient) { /** * Generic PATCH method for partial task updates. * Used for status changes and archive/unarchive operations. - * - * NOTE: The old custom action endpoints (cancel, uncancel, mark-in-progress, - * archive, unarchive) have been REMOVED from the API. - * All task updates now use PATCH /tasks/{id}/. + * Returns TaskWithSummaryResponse to update dashboard stats in one call. */ - suspend fun patchTask(token: String, id: Int, request: TaskPatchRequest): ApiResult { + suspend fun patchTask(token: String, id: Int, request: TaskPatchRequest): ApiResult> { return try { val response = client.patch("$baseUrl/tasks/$id/") { header("Authorization", "Token $token") @@ -151,27 +148,26 @@ class TaskApi(private val client: HttpClient = ApiClient.httpClient) { } } - // DEPRECATED: These methods now use PATCH internally. - // They're kept for backward compatibility with existing ViewModel calls. - // New code should use patchTask directly with status IDs from DataManager. + // Convenience methods for common task actions + // These use PATCH internally to update task status/archived state - suspend fun cancelTask(token: String, id: Int, cancelledStatusId: Int): ApiResult { + suspend fun cancelTask(token: String, id: Int, cancelledStatusId: Int): ApiResult> { return patchTask(token, id, TaskPatchRequest(status = cancelledStatusId)) } - suspend fun uncancelTask(token: String, id: Int, pendingStatusId: Int): ApiResult { + suspend fun uncancelTask(token: String, id: Int, pendingStatusId: Int): ApiResult> { return patchTask(token, id, TaskPatchRequest(status = pendingStatusId)) } - suspend fun markInProgress(token: String, id: Int, inProgressStatusId: Int): ApiResult { + suspend fun markInProgress(token: String, id: Int, inProgressStatusId: Int): ApiResult> { return patchTask(token, id, TaskPatchRequest(status = inProgressStatusId)) } - suspend fun archiveTask(token: String, id: Int): ApiResult { + suspend fun archiveTask(token: String, id: Int): ApiResult> { return patchTask(token, id, TaskPatchRequest(archived = true)) } - suspend fun unarchiveTask(token: String, id: Int): ApiResult { + suspend fun unarchiveTask(token: String, id: Int): ApiResult> { return patchTask(token, id, TaskPatchRequest(archived = false)) } diff --git a/composeApp/src/commonMain/kotlin/com/example/casera/network/TaskCompletionApi.kt b/composeApp/src/commonMain/kotlin/com/example/casera/network/TaskCompletionApi.kt index b4ce339..ba6e0ff 100644 --- a/composeApp/src/commonMain/kotlin/com/example/casera/network/TaskCompletionApi.kt +++ b/composeApp/src/commonMain/kotlin/com/example/casera/network/TaskCompletionApi.kt @@ -41,7 +41,7 @@ class TaskCompletionApi(private val client: HttpClient = ApiClient.httpClient) { } } - suspend fun createCompletion(token: String, request: TaskCompletionCreateRequest): ApiResult { + suspend fun createCompletion(token: String, request: TaskCompletionCreateRequest): ApiResult> { return try { val response = client.post("$baseUrl/task-completions/") { header("Authorization", "Token $token") @@ -77,14 +77,14 @@ class TaskCompletionApi(private val client: HttpClient = ApiClient.httpClient) { } } - suspend fun deleteCompletion(token: String, id: Int): ApiResult { + suspend fun deleteCompletion(token: String, id: Int): ApiResult> { return try { val response = client.delete("$baseUrl/task-completions/$id/") { header("Authorization", "Token $token") } if (response.status.isSuccess()) { - ApiResult.Success(Unit) + ApiResult.Success(response.body()) } else { ApiResult.Error("Failed to delete completion", response.status.value) } @@ -98,7 +98,7 @@ class TaskCompletionApi(private val client: HttpClient = ApiClient.httpClient) { request: TaskCompletionCreateRequest, images: List = emptyList(), imageFileNames: List = emptyList() - ): ApiResult { + ): ApiResult> { return try { val response = client.post("$baseUrl/task-completions/") { header("Authorization", "Token $token")