Update API layer to use TotalSummary from CRUD responses

Frontend changes:
- Add generic WithSummaryResponse<T> model for CRUD responses
- Update TaskApi, TaskCompletionApi, ResidenceApi return types
- Update APILayer to extract summary from responses and call DataManager.setTotalSummary()
- Replace refreshTasks() calls with DataManager.updateTask() for local cache updates
- Remove redundant refreshMyResidences() calls
- Remove unused helper methods (refreshTasks, refreshMyResidences, refreshSummary)
- Add summary field to JoinResidenceResponse model

This pairs with the backend changes to eliminate redundant network calls
after CRUD operations - dashboard stats now update from the mutation response.

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Trey t
2025-12-08 10:39:53 -06:00
parent c334ce0bd0
commit c5b08befea
5 changed files with 133 additions and 86 deletions

View File

@@ -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<TaskResponse> for task CRUD
* - WithSummaryResponse<TaskCompletionResponse> for completion CRUD
* - WithSummaryResponse<String> for delete operations (data = "task deleted")
*/
@Serializable
data class WithSummaryResponse<T>(
val data: T,
val summary: TotalSummary
)
/**
* My residences response - list of user's residences
* Go API returns array directly, this wraps for consistency

View File

@@ -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<ResidenceResponse> {
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<Unit> {
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<GenerateReportResponse> {
@@ -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<TaskResponse> {
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<TaskResponse> {
@@ -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<TaskResponse> {
@@ -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<TaskResponse> {
@@ -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<TaskResponse> {
@@ -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<TaskCompletionResponse> {
@@ -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
*/

View File

@@ -41,7 +41,7 @@ class ResidenceApi(private val client: HttpClient = ApiClient.httpClient) {
}
}
suspend fun createResidence(token: String, request: ResidenceCreateRequest): ApiResult<ResidenceResponse> {
suspend fun createResidence(token: String, request: ResidenceCreateRequest): ApiResult<WithSummaryResponse<ResidenceResponse>> {
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<ResidenceResponse> {
suspend fun updateResidence(token: String, id: Int, request: ResidenceCreateRequest): ApiResult<WithSummaryResponse<ResidenceResponse>> {
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<Unit> {
suspend fun deleteResidence(token: String, id: Int): ApiResult<WithSummaryResponse<String>> {
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)
}

View File

@@ -47,7 +47,7 @@ class TaskApi(private val client: HttpClient = ApiClient.httpClient) {
}
}
suspend fun createTask(token: String, request: TaskCreateRequest): ApiResult<TaskResponse> {
suspend fun createTask(token: String, request: TaskCreateRequest): ApiResult<WithSummaryResponse<TaskResponse>> {
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<TaskResponse> {
suspend fun updateTask(token: String, id: Int, request: TaskCreateRequest): ApiResult<WithSummaryResponse<TaskResponse>> {
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<Unit> {
suspend fun deleteTask(token: String, id: Int): ApiResult<WithSummaryResponse<String>> {
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<TaskResponse> {
suspend fun patchTask(token: String, id: Int, request: TaskPatchRequest): ApiResult<WithSummaryResponse<TaskResponse>> {
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<TaskResponse> {
suspend fun cancelTask(token: String, id: Int, cancelledStatusId: Int): ApiResult<WithSummaryResponse<TaskResponse>> {
return patchTask(token, id, TaskPatchRequest(status = cancelledStatusId))
}
suspend fun uncancelTask(token: String, id: Int, pendingStatusId: Int): ApiResult<TaskResponse> {
suspend fun uncancelTask(token: String, id: Int, pendingStatusId: Int): ApiResult<WithSummaryResponse<TaskResponse>> {
return patchTask(token, id, TaskPatchRequest(status = pendingStatusId))
}
suspend fun markInProgress(token: String, id: Int, inProgressStatusId: Int): ApiResult<TaskResponse> {
suspend fun markInProgress(token: String, id: Int, inProgressStatusId: Int): ApiResult<WithSummaryResponse<TaskResponse>> {
return patchTask(token, id, TaskPatchRequest(status = inProgressStatusId))
}
suspend fun archiveTask(token: String, id: Int): ApiResult<TaskResponse> {
suspend fun archiveTask(token: String, id: Int): ApiResult<WithSummaryResponse<TaskResponse>> {
return patchTask(token, id, TaskPatchRequest(archived = true))
}
suspend fun unarchiveTask(token: String, id: Int): ApiResult<TaskResponse> {
suspend fun unarchiveTask(token: String, id: Int): ApiResult<WithSummaryResponse<TaskResponse>> {
return patchTask(token, id, TaskPatchRequest(archived = false))
}

View File

@@ -41,7 +41,7 @@ class TaskCompletionApi(private val client: HttpClient = ApiClient.httpClient) {
}
}
suspend fun createCompletion(token: String, request: TaskCompletionCreateRequest): ApiResult<TaskCompletionResponse> {
suspend fun createCompletion(token: String, request: TaskCompletionCreateRequest): ApiResult<WithSummaryResponse<TaskCompletionResponse>> {
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<Unit> {
suspend fun deleteCompletion(token: String, id: Int): ApiResult<WithSummaryResponse<String>> {
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<ByteArray> = emptyList(),
imageFileNames: List<String> = emptyList()
): ApiResult<TaskCompletionResponse> {
): ApiResult<WithSummaryResponse<TaskCompletionResponse>> {
return try {
val response = client.post("$baseUrl/task-completions/") {
header("Authorization", "Token $token")