From 4e14352cd1ed9485539956889a35f35ae3966052 Mon Sep 17 00:00:00 2001 From: Trey t Date: Fri, 7 Nov 2025 12:57:25 -0600 Subject: [PATCH] wip --- .../com/example/mycrib/network/TaskApi.kt | 32 ++++++++++ .../mycrib/ui/components/task/TaskCard.kt | 47 ++++++++++++++- .../ui/components/task/TaskKanbanView.kt | 30 ++++++++-- .../mycrib/ui/screens/AllTasksScreen.kt | 14 +++++ .../ui/screens/ResidenceDetailScreen.kt | 14 +++++ .../example/mycrib/viewmodel/TaskViewModel.kt | 42 +++++++++++++ .../Residence/ResidenceDetailView.swift | 10 ++++ iosApp/iosApp/Subviews/Task/TaskCard.swift | 28 ++++++++- .../iosApp/Subviews/Task/TasksSection.swift | 23 +++++-- iosApp/iosApp/Task/AllTasksView.swift | 44 +++++++++++--- iosApp/iosApp/Task/TaskViewModel.swift | 60 +++++++++++++++++++ 11 files changed, 323 insertions(+), 21 deletions(-) diff --git a/composeApp/src/commonMain/kotlin/com/example/mycrib/network/TaskApi.kt b/composeApp/src/commonMain/kotlin/com/example/mycrib/network/TaskApi.kt index 48e78af..1d749a8 100644 --- a/composeApp/src/commonMain/kotlin/com/example/mycrib/network/TaskApi.kt +++ b/composeApp/src/commonMain/kotlin/com/example/mycrib/network/TaskApi.kt @@ -165,4 +165,36 @@ class TaskApi(private val client: HttpClient = ApiClient.httpClient) { ApiResult.Error(e.message ?: "Unknown error occurred") } } + + suspend fun archiveTask(token: String, id: Int): ApiResult { + return try { + val response = client.post("$baseUrl/tasks/$id/archive/") { + header("Authorization", "Token $token") + } + + if (response.status.isSuccess()) { + ApiResult.Success(response.body()) + } else { + ApiResult.Error("Failed to archive task", response.status.value) + } + } catch (e: Exception) { + ApiResult.Error(e.message ?: "Unknown error occurred") + } + } + + suspend fun unarchiveTask(token: String, id: Int): ApiResult { + return try { + val response = client.post("$baseUrl/tasks/$id/unarchive/") { + header("Authorization", "Token $token") + } + + if (response.status.isSuccess()) { + ApiResult.Success(response.body()) + } else { + ApiResult.Error("Failed to unarchive task", response.status.value) + } + } catch (e: Exception) { + ApiResult.Error(e.message ?: "Unknown error occurred") + } + } } diff --git a/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/components/task/TaskCard.kt b/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/components/task/TaskCard.kt index 2849c8c..1b3d5f0 100644 --- a/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/components/task/TaskCard.kt +++ b/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/components/task/TaskCard.kt @@ -24,7 +24,9 @@ fun TaskCard( onEditClick: () -> Unit, onCancelClick: (() -> Unit)?, onUncancelClick: (() -> Unit)?, - onMarkInProgressClick: (() -> Unit)? = null + onMarkInProgressClick: (() -> Unit)? = null, + onArchiveClick: (() -> Unit)? = null, + onUnarchiveClick: (() -> Unit)? = null ) { Card( modifier = Modifier.fillMaxWidth(), @@ -356,6 +358,49 @@ fun TaskCard( } } } + + // Archive/Unarchive button row + if (task.archived) { + if (onUnarchiveClick != null) { + Spacer(modifier = Modifier.height(8.dp)) + OutlinedButton( + onClick = onUnarchiveClick, + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(12.dp), + colors = ButtonDefaults.outlinedButtonColors( + contentColor = MaterialTheme.colorScheme.primary + ) + ) { + Icon( + Icons.Default.Unarchive, + contentDescription = null, + modifier = Modifier.size(18.dp) + ) + Spacer(modifier = Modifier.width(4.dp)) + Text("Unarchive") + } + } + } else { + if (onArchiveClick != null) { + Spacer(modifier = Modifier.height(8.dp)) + OutlinedButton( + onClick = onArchiveClick, + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(12.dp), + colors = ButtonDefaults.outlinedButtonColors( + contentColor = MaterialTheme.colorScheme.outline + ) + ) { + Icon( + Icons.Default.Archive, + contentDescription = null, + modifier = Modifier.size(18.dp) + ) + Spacer(modifier = Modifier.width(4.dp)) + Text("Archive") + } + } + } } } } diff --git a/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/components/task/TaskKanbanView.kt b/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/components/task/TaskKanbanView.kt index 727500a..f67e407 100644 --- a/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/components/task/TaskKanbanView.kt +++ b/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/components/task/TaskKanbanView.kt @@ -33,7 +33,9 @@ fun TaskKanbanView( onEditTask: (TaskDetail) -> Unit, onCancelTask: ((TaskDetail) -> Unit)?, onUncancelTask: ((TaskDetail) -> Unit)?, - onMarkInProgress: ((TaskDetail) -> Unit)? + onMarkInProgress: ((TaskDetail) -> Unit)?, + onArchiveTask: ((TaskDetail) -> Unit)?, + onUnarchiveTask: ((TaskDetail) -> Unit)? ) { val pagerState = rememberPagerState(pageCount = { 4 }) @@ -55,7 +57,9 @@ fun TaskKanbanView( onEditTask = onEditTask, onCancelTask = onCancelTask, onUncancelTask = onUncancelTask, - onMarkInProgress = onMarkInProgress + onMarkInProgress = onMarkInProgress, + onArchiveTask = onArchiveTask, + onUnarchiveTask = null ) 1 -> TaskColumn( title = "In Progress", @@ -67,7 +71,9 @@ fun TaskKanbanView( onEditTask = onEditTask, onCancelTask = onCancelTask, onUncancelTask = onUncancelTask, - onMarkInProgress = null + onMarkInProgress = null, + onArchiveTask = onArchiveTask, + onUnarchiveTask = null ) 2 -> TaskColumn( title = "Done", @@ -79,7 +85,9 @@ fun TaskKanbanView( onEditTask = onEditTask, onCancelTask = null, onUncancelTask = null, - onMarkInProgress = null + onMarkInProgress = null, + onArchiveTask = onArchiveTask, + onUnarchiveTask = null ) 3 -> TaskColumn( title = "Archived", @@ -91,7 +99,9 @@ fun TaskKanbanView( onEditTask = onEditTask, onCancelTask = null, onUncancelTask = null, - onMarkInProgress = null + onMarkInProgress = null, + onArchiveTask = null, + onUnarchiveTask = onUnarchiveTask ) } } @@ -109,7 +119,9 @@ private fun TaskColumn( onEditTask: (TaskDetail) -> Unit, onCancelTask: ((TaskDetail) -> Unit)?, onUncancelTask: ((TaskDetail) -> Unit)?, - onMarkInProgress: ((TaskDetail) -> Unit)? + onMarkInProgress: ((TaskDetail) -> Unit)?, + onArchiveTask: ((TaskDetail) -> Unit)?, + onUnarchiveTask: ((TaskDetail) -> Unit)? ) { Column( modifier = Modifier @@ -202,6 +214,12 @@ private fun TaskColumn( } else null, onMarkInProgressClick = if (onMarkInProgress != null) { { onMarkInProgress(task) } + } else null, + onArchiveClick = if (onArchiveTask != null) { + { onArchiveTask(task) } + } else null, + onUnarchiveClick = if (onUnarchiveTask != null) { + { onUnarchiveTask(task) } } else null ) } diff --git a/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/screens/AllTasksScreen.kt b/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/screens/AllTasksScreen.kt index 86d307e..489be49 100644 --- a/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/screens/AllTasksScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/screens/AllTasksScreen.kt @@ -247,6 +247,20 @@ fun AllTasksScreen( viewModel.loadTasks() } } + }, + onArchiveTask = { task -> + viewModel.archiveTask(task.id) { success -> + if (success) { + viewModel.loadTasks() + } + } + }, + onUnarchiveTask = { task -> + viewModel.unarchiveTask(task.id) { success -> + if (success) { + viewModel.loadTasks() + } + } } ) } diff --git a/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/screens/ResidenceDetailScreen.kt b/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/screens/ResidenceDetailScreen.kt index 433f41c..e653234 100644 --- a/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/screens/ResidenceDetailScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/screens/ResidenceDetailScreen.kt @@ -456,6 +456,20 @@ fun ResidenceDetailScreen( residenceViewModel.loadResidenceTasks(residenceId) } } + }, + onArchiveTask = { task -> + taskViewModel.archiveTask(task.id) { success -> + if (success) { + residenceViewModel.loadResidenceTasks(residenceId) + } + } + }, + onUnarchiveTask = { task -> + taskViewModel.unarchiveTask(task.id) { success -> + if (success) { + residenceViewModel.loadResidenceTasks(residenceId) + } + } } ) } diff --git a/composeApp/src/commonMain/kotlin/com/example/mycrib/viewmodel/TaskViewModel.kt b/composeApp/src/commonMain/kotlin/com/example/mycrib/viewmodel/TaskViewModel.kt index 31e4d5e..4396e40 100644 --- a/composeApp/src/commonMain/kotlin/com/example/mycrib/viewmodel/TaskViewModel.kt +++ b/composeApp/src/commonMain/kotlin/com/example/mycrib/viewmodel/TaskViewModel.kt @@ -94,4 +94,46 @@ class TaskViewModel : ViewModel() { } } } + + fun archiveTask(taskId: Int, onComplete: (Boolean) -> Unit) { + viewModelScope.launch { + val token = TokenStorage.getToken() + if (token != null) { + when (val result = taskApi.archiveTask(token, taskId)) { + is ApiResult.Success -> { + onComplete(true) + } + is ApiResult.Error -> { + onComplete(false) + } + else -> { + onComplete(false) + } + } + } else { + onComplete(false) + } + } + } + + fun unarchiveTask(taskId: Int, onComplete: (Boolean) -> Unit) { + viewModelScope.launch { + val token = TokenStorage.getToken() + if (token != null) { + when (val result = taskApi.unarchiveTask(token, taskId)) { + is ApiResult.Success -> { + onComplete(true) + } + is ApiResult.Error -> { + onComplete(false) + } + else -> { + onComplete(false) + } + } + } else { + onComplete(false) + } + } + } } diff --git a/iosApp/iosApp/Residence/ResidenceDetailView.swift b/iosApp/iosApp/Residence/ResidenceDetailView.swift index d29757e..700b215 100644 --- a/iosApp/iosApp/Residence/ResidenceDetailView.swift +++ b/iosApp/iosApp/Residence/ResidenceDetailView.swift @@ -60,6 +60,16 @@ struct ResidenceDetailView: View { }, onCompleteTask: { task in selectedTaskForComplete = task + }, + onArchiveTask: { task in + taskViewModel.archiveTask(id: task.id) { _ in + loadResidenceTasks() + } + }, + onUnarchiveTask: { task in + taskViewModel.unarchiveTask(id: task.id) { _ in + loadResidenceTasks() + } } ) .padding(.horizontal) diff --git a/iosApp/iosApp/Subviews/Task/TaskCard.swift b/iosApp/iosApp/Subviews/Task/TaskCard.swift index 8af8771..5dffba8 100644 --- a/iosApp/iosApp/Subviews/Task/TaskCard.swift +++ b/iosApp/iosApp/Subviews/Task/TaskCard.swift @@ -8,6 +8,8 @@ struct TaskCard: View { let onUncancel: (() -> Void)? let onMarkInProgress: (() -> Void)? let onComplete: (() -> Void)? + let onArchive: (() -> Void)? + let onUnarchive: (() -> Void)? var body: some View { VStack(alignment: .leading, spacing: 12) { @@ -116,6 +118,28 @@ struct TaskCard: View { .buttonStyle(.borderedProminent) .tint(.blue) } + + if task.archived { + if let onUnarchive = onUnarchive { + Button(action: onUnarchive) { + Label("Unarchive", systemImage: "tray.and.arrow.up") + .font(.subheadline) + .frame(maxWidth: .infinity) + } + .buttonStyle(.bordered) + .tint(.blue) + } + } else { + if let onArchive = onArchive { + Button(action: onArchive) { + Label("Archive", systemImage: "archivebox") + .font(.subheadline) + .frame(maxWidth: .infinity) + } + .buttonStyle(.bordered) + .tint(.gray) + } + } } } .padding(16) @@ -162,7 +186,9 @@ struct TaskCard: View { onCancel: {}, onUncancel: nil, onMarkInProgress: {}, - onComplete: {} + onComplete: {}, + onArchive: {}, + onUnarchive: {} ) } .padding() diff --git a/iosApp/iosApp/Subviews/Task/TasksSection.swift b/iosApp/iosApp/Subviews/Task/TasksSection.swift index 39771ad..8ce9ca5 100644 --- a/iosApp/iosApp/Subviews/Task/TasksSection.swift +++ b/iosApp/iosApp/Subviews/Task/TasksSection.swift @@ -8,6 +8,8 @@ struct TasksSection: View { let onUncancelTask: (TaskDetail) -> Void let onMarkInProgress: (TaskDetail) -> Void let onCompleteTask: (TaskDetail) -> Void + let onArchiveTask: (TaskDetail) -> Void + let onUnarchiveTask: (TaskDetail) -> Void var body: some View { VStack(alignment: .leading, spacing: 12) { @@ -32,7 +34,9 @@ struct TasksSection: View { onCancelTask: onCancelTask, onUncancelTask: onUncancelTask, onMarkInProgress: onMarkInProgress, - onCompleteTask: onCompleteTask + onCompleteTask: onCompleteTask, + onArchiveTask: onArchiveTask, + onUnarchiveTask: onUnarchiveTask ) .frame(width: geometry.size.width - 48) @@ -47,7 +51,9 @@ struct TasksSection: View { onCancelTask: onCancelTask, onUncancelTask: onUncancelTask, onMarkInProgress: nil, - onCompleteTask: onCompleteTask + onCompleteTask: onCompleteTask, + onArchiveTask: onArchiveTask, + onUnarchiveTask: onUnarchiveTask ) .frame(width: geometry.size.width - 48) @@ -62,7 +68,9 @@ struct TasksSection: View { onCancelTask: nil, onUncancelTask: nil, onMarkInProgress: nil, - onCompleteTask: nil + onCompleteTask: nil, + onArchiveTask: onArchiveTask, + onUnarchiveTask: onUnarchiveTask ) .frame(width: geometry.size.width - 48) @@ -77,7 +85,9 @@ struct TasksSection: View { onCancelTask: nil, onUncancelTask: nil, onMarkInProgress: nil, - onCompleteTask: nil + onCompleteTask: nil, + onArchiveTask: nil, + onUnarchiveTask: onUnarchiveTask ) .frame(width: geometry.size.width - 48) } @@ -100,7 +110,8 @@ struct TasksSection: View { summary: CategorizedTaskSummary( upcoming: 3, inProgress: 1, - done: 2 + done: 2, + archived: 0 ), upcomingTasks: [ TaskDetail( @@ -154,6 +165,8 @@ struct TasksSection: View { onUncancelTask: { _ in }, onMarkInProgress: { _ in }, onCompleteTask: { _ in } + , onArchiveTask: { _ in } + , onUnarchiveTask: { _ in } ) .padding() } diff --git a/iosApp/iosApp/Task/AllTasksView.swift b/iosApp/iosApp/Task/AllTasksView.swift index eb8d3f1..44b3bae 100644 --- a/iosApp/iosApp/Task/AllTasksView.swift +++ b/iosApp/iosApp/Task/AllTasksView.swift @@ -113,7 +113,13 @@ struct AllTasksView: View { }, onCompleteTask: { task in selectedTaskForComplete = task - } + }, + onArchiveTask: { task in + taskViewModel.archiveTask(id: task.id) { _ in + loadAllTasks() + } + }, + onUnarchiveTask: nil ) .frame(width: geometry.size.width - 48) @@ -142,10 +148,16 @@ struct AllTasksView: View { onMarkInProgress: nil, onCompleteTask: { task in selectedTaskForComplete = task - } + }, + onArchiveTask: { task in + taskViewModel.archiveTask(id: task.id) { _ in + loadAllTasks() + } + }, + onUnarchiveTask: nil ) .frame(width: geometry.size.width - 48) - + // Done Column TaskColumnView( title: "Done", @@ -160,10 +172,16 @@ struct AllTasksView: View { onCancelTask: nil, onUncancelTask: nil, onMarkInProgress: nil, - onCompleteTask: nil + onCompleteTask: nil, + onArchiveTask: { task in + taskViewModel.archiveTask(id: task.id) { _ in + loadAllTasks() + } + }, + onUnarchiveTask: nil ) .frame(width: geometry.size.width - 48) - + // Archived Column TaskColumnView( title: "Archived", @@ -178,7 +196,13 @@ struct AllTasksView: View { onCancelTask: nil, onUncancelTask: nil, onMarkInProgress: nil, - onCompleteTask: nil + onCompleteTask: nil, + onArchiveTask: nil, + onUnarchiveTask: { task in + taskViewModel.unarchiveTask(id: task.id) { _ in + loadAllTasks() + } + } ) .frame(width: geometry.size.width - 48) } @@ -269,7 +293,9 @@ struct TaskColumnView: View { let onUncancelTask: ((TaskDetail) -> Void)? let onMarkInProgress: ((TaskDetail) -> Void)? let onCompleteTask: ((TaskDetail) -> Void)? - + let onArchiveTask: ((TaskDetail) -> Void)? + let onUnarchiveTask: ((TaskDetail) -> Void)? + var body: some View { VStack(spacing: 0) { // Tasks List @@ -316,7 +342,9 @@ struct TaskColumnView: View { onCancel: onCancelTask != nil ? { onCancelTask?(task) } : nil, onUncancel: onUncancelTask != nil ? { onUncancelTask?(task) } : nil, onMarkInProgress: onMarkInProgress != nil ? { onMarkInProgress?(task) } : nil, - onComplete: onCompleteTask != nil ? { onCompleteTask?(task) } : nil + onComplete: onCompleteTask != nil ? { onCompleteTask?(task) } : nil, + onArchive: onArchiveTask != nil ? { onArchiveTask?(task) } : nil, + onUnarchive: onUnarchiveTask != nil ? { onUnarchiveTask?(task) } : nil ) } } diff --git a/iosApp/iosApp/Task/TaskViewModel.swift b/iosApp/iosApp/Task/TaskViewModel.swift index feba848..df8cd61 100644 --- a/iosApp/iosApp/Task/TaskViewModel.swift +++ b/iosApp/iosApp/Task/TaskViewModel.swift @@ -12,6 +12,8 @@ class TaskViewModel: ObservableObject { @Published var taskCancelled: Bool = false @Published var taskUncancelled: Bool = false @Published var taskMarkedInProgress: Bool = false + @Published var taskArchived: Bool = false + @Published var taskUnarchived: Bool = false // MARK: - Private Properties private let taskApi: TaskApi @@ -168,6 +170,62 @@ class TaskViewModel: ObservableObject { } } + func archiveTask(id: Int32, completion: @escaping (Bool) -> Void) { + guard let token = tokenStorage.getToken() else { + errorMessage = "Not authenticated" + completion(false) + return + } + + isLoading = true + errorMessage = nil + taskArchived = false + + taskApi.archiveTask(token: token, id: id) { result, error in + if result is ApiResultSuccess { + self.isLoading = false + self.taskArchived = true + completion(true) + } else if let errorResult = result as? ApiResultError { + self.errorMessage = errorResult.message + self.isLoading = false + completion(false) + } else if let error = error { + self.errorMessage = error.localizedDescription + self.isLoading = false + completion(false) + } + } + } + + func unarchiveTask(id: Int32, completion: @escaping (Bool) -> Void) { + guard let token = tokenStorage.getToken() else { + errorMessage = "Not authenticated" + completion(false) + return + } + + isLoading = true + errorMessage = nil + taskUnarchived = false + + taskApi.unarchiveTask(token: token, id: id) { result, error in + if result is ApiResultSuccess { + self.isLoading = false + self.taskUnarchived = true + completion(true) + } else if let errorResult = result as? ApiResultError { + self.errorMessage = errorResult.message + self.isLoading = false + completion(false) + } else if let error = error { + self.errorMessage = error.localizedDescription + self.isLoading = false + completion(false) + } + } + } + func completeTask(taskId: Int32, completion: @escaping (Bool) -> Void) { guard let token = tokenStorage.getToken() else { errorMessage = "Not authenticated" @@ -215,6 +273,8 @@ class TaskViewModel: ObservableObject { taskCancelled = false taskUncancelled = false taskMarkedInProgress = false + taskArchived = false + taskUnarchived = false errorMessage = nil } }