This commit is contained in:
Trey t
2025-11-07 12:57:25 -06:00
parent 1b777049a8
commit 4e14352cd1
11 changed files with 323 additions and 21 deletions

View File

@@ -165,4 +165,36 @@ class TaskApi(private val client: HttpClient = ApiClient.httpClient) {
ApiResult.Error(e.message ?: "Unknown error occurred") ApiResult.Error(e.message ?: "Unknown error occurred")
} }
} }
suspend fun archiveTask(token: String, id: Int): ApiResult<TaskCancelResponse> {
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<TaskCancelResponse> {
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")
}
}
} }

View File

@@ -24,7 +24,9 @@ fun TaskCard(
onEditClick: () -> Unit, onEditClick: () -> Unit,
onCancelClick: (() -> Unit)?, onCancelClick: (() -> Unit)?,
onUncancelClick: (() -> Unit)?, onUncancelClick: (() -> Unit)?,
onMarkInProgressClick: (() -> Unit)? = null onMarkInProgressClick: (() -> Unit)? = null,
onArchiveClick: (() -> Unit)? = null,
onUnarchiveClick: (() -> Unit)? = null
) { ) {
Card( Card(
modifier = Modifier.fillMaxWidth(), 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")
}
}
}
} }
} }
} }

View File

@@ -33,7 +33,9 @@ fun TaskKanbanView(
onEditTask: (TaskDetail) -> Unit, onEditTask: (TaskDetail) -> Unit,
onCancelTask: ((TaskDetail) -> Unit)?, onCancelTask: ((TaskDetail) -> Unit)?,
onUncancelTask: ((TaskDetail) -> Unit)?, onUncancelTask: ((TaskDetail) -> Unit)?,
onMarkInProgress: ((TaskDetail) -> Unit)? onMarkInProgress: ((TaskDetail) -> Unit)?,
onArchiveTask: ((TaskDetail) -> Unit)?,
onUnarchiveTask: ((TaskDetail) -> Unit)?
) { ) {
val pagerState = rememberPagerState(pageCount = { 4 }) val pagerState = rememberPagerState(pageCount = { 4 })
@@ -55,7 +57,9 @@ fun TaskKanbanView(
onEditTask = onEditTask, onEditTask = onEditTask,
onCancelTask = onCancelTask, onCancelTask = onCancelTask,
onUncancelTask = onUncancelTask, onUncancelTask = onUncancelTask,
onMarkInProgress = onMarkInProgress onMarkInProgress = onMarkInProgress,
onArchiveTask = onArchiveTask,
onUnarchiveTask = null
) )
1 -> TaskColumn( 1 -> TaskColumn(
title = "In Progress", title = "In Progress",
@@ -67,7 +71,9 @@ fun TaskKanbanView(
onEditTask = onEditTask, onEditTask = onEditTask,
onCancelTask = onCancelTask, onCancelTask = onCancelTask,
onUncancelTask = onUncancelTask, onUncancelTask = onUncancelTask,
onMarkInProgress = null onMarkInProgress = null,
onArchiveTask = onArchiveTask,
onUnarchiveTask = null
) )
2 -> TaskColumn( 2 -> TaskColumn(
title = "Done", title = "Done",
@@ -79,7 +85,9 @@ fun TaskKanbanView(
onEditTask = onEditTask, onEditTask = onEditTask,
onCancelTask = null, onCancelTask = null,
onUncancelTask = null, onUncancelTask = null,
onMarkInProgress = null onMarkInProgress = null,
onArchiveTask = onArchiveTask,
onUnarchiveTask = null
) )
3 -> TaskColumn( 3 -> TaskColumn(
title = "Archived", title = "Archived",
@@ -91,7 +99,9 @@ fun TaskKanbanView(
onEditTask = onEditTask, onEditTask = onEditTask,
onCancelTask = null, onCancelTask = null,
onUncancelTask = null, onUncancelTask = null,
onMarkInProgress = null onMarkInProgress = null,
onArchiveTask = null,
onUnarchiveTask = onUnarchiveTask
) )
} }
} }
@@ -109,7 +119,9 @@ private fun TaskColumn(
onEditTask: (TaskDetail) -> Unit, onEditTask: (TaskDetail) -> Unit,
onCancelTask: ((TaskDetail) -> Unit)?, onCancelTask: ((TaskDetail) -> Unit)?,
onUncancelTask: ((TaskDetail) -> Unit)?, onUncancelTask: ((TaskDetail) -> Unit)?,
onMarkInProgress: ((TaskDetail) -> Unit)? onMarkInProgress: ((TaskDetail) -> Unit)?,
onArchiveTask: ((TaskDetail) -> Unit)?,
onUnarchiveTask: ((TaskDetail) -> Unit)?
) { ) {
Column( Column(
modifier = Modifier modifier = Modifier
@@ -202,6 +214,12 @@ private fun TaskColumn(
} else null, } else null,
onMarkInProgressClick = if (onMarkInProgress != null) { onMarkInProgressClick = if (onMarkInProgress != null) {
{ onMarkInProgress(task) } { onMarkInProgress(task) }
} else null,
onArchiveClick = if (onArchiveTask != null) {
{ onArchiveTask(task) }
} else null,
onUnarchiveClick = if (onUnarchiveTask != null) {
{ onUnarchiveTask(task) }
} else null } else null
) )
} }

View File

@@ -247,6 +247,20 @@ fun AllTasksScreen(
viewModel.loadTasks() viewModel.loadTasks()
} }
} }
},
onArchiveTask = { task ->
viewModel.archiveTask(task.id) { success ->
if (success) {
viewModel.loadTasks()
}
}
},
onUnarchiveTask = { task ->
viewModel.unarchiveTask(task.id) { success ->
if (success) {
viewModel.loadTasks()
}
}
} }
) )
} }

View File

@@ -456,6 +456,20 @@ fun ResidenceDetailScreen(
residenceViewModel.loadResidenceTasks(residenceId) 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)
}
}
} }
) )
} }

View File

@@ -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)
}
}
}
} }

View File

@@ -60,6 +60,16 @@ struct ResidenceDetailView: View {
}, },
onCompleteTask: { task in onCompleteTask: { task in
selectedTaskForComplete = task selectedTaskForComplete = task
},
onArchiveTask: { task in
taskViewModel.archiveTask(id: task.id) { _ in
loadResidenceTasks()
}
},
onUnarchiveTask: { task in
taskViewModel.unarchiveTask(id: task.id) { _ in
loadResidenceTasks()
}
} }
) )
.padding(.horizontal) .padding(.horizontal)

View File

@@ -8,6 +8,8 @@ struct TaskCard: View {
let onUncancel: (() -> Void)? let onUncancel: (() -> Void)?
let onMarkInProgress: (() -> Void)? let onMarkInProgress: (() -> Void)?
let onComplete: (() -> Void)? let onComplete: (() -> Void)?
let onArchive: (() -> Void)?
let onUnarchive: (() -> Void)?
var body: some View { var body: some View {
VStack(alignment: .leading, spacing: 12) { VStack(alignment: .leading, spacing: 12) {
@@ -116,6 +118,28 @@ struct TaskCard: View {
.buttonStyle(.borderedProminent) .buttonStyle(.borderedProminent)
.tint(.blue) .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) .padding(16)
@@ -162,7 +186,9 @@ struct TaskCard: View {
onCancel: {}, onCancel: {},
onUncancel: nil, onUncancel: nil,
onMarkInProgress: {}, onMarkInProgress: {},
onComplete: {} onComplete: {},
onArchive: {},
onUnarchive: {}
) )
} }
.padding() .padding()

View File

@@ -8,6 +8,8 @@ struct TasksSection: View {
let onUncancelTask: (TaskDetail) -> Void let onUncancelTask: (TaskDetail) -> Void
let onMarkInProgress: (TaskDetail) -> Void let onMarkInProgress: (TaskDetail) -> Void
let onCompleteTask: (TaskDetail) -> Void let onCompleteTask: (TaskDetail) -> Void
let onArchiveTask: (TaskDetail) -> Void
let onUnarchiveTask: (TaskDetail) -> Void
var body: some View { var body: some View {
VStack(alignment: .leading, spacing: 12) { VStack(alignment: .leading, spacing: 12) {
@@ -32,7 +34,9 @@ struct TasksSection: View {
onCancelTask: onCancelTask, onCancelTask: onCancelTask,
onUncancelTask: onUncancelTask, onUncancelTask: onUncancelTask,
onMarkInProgress: onMarkInProgress, onMarkInProgress: onMarkInProgress,
onCompleteTask: onCompleteTask onCompleteTask: onCompleteTask,
onArchiveTask: onArchiveTask,
onUnarchiveTask: onUnarchiveTask
) )
.frame(width: geometry.size.width - 48) .frame(width: geometry.size.width - 48)
@@ -47,7 +51,9 @@ struct TasksSection: View {
onCancelTask: onCancelTask, onCancelTask: onCancelTask,
onUncancelTask: onUncancelTask, onUncancelTask: onUncancelTask,
onMarkInProgress: nil, onMarkInProgress: nil,
onCompleteTask: onCompleteTask onCompleteTask: onCompleteTask,
onArchiveTask: onArchiveTask,
onUnarchiveTask: onUnarchiveTask
) )
.frame(width: geometry.size.width - 48) .frame(width: geometry.size.width - 48)
@@ -62,7 +68,9 @@ struct TasksSection: View {
onCancelTask: nil, onCancelTask: nil,
onUncancelTask: nil, onUncancelTask: nil,
onMarkInProgress: nil, onMarkInProgress: nil,
onCompleteTask: nil onCompleteTask: nil,
onArchiveTask: onArchiveTask,
onUnarchiveTask: onUnarchiveTask
) )
.frame(width: geometry.size.width - 48) .frame(width: geometry.size.width - 48)
@@ -77,7 +85,9 @@ struct TasksSection: View {
onCancelTask: nil, onCancelTask: nil,
onUncancelTask: nil, onUncancelTask: nil,
onMarkInProgress: nil, onMarkInProgress: nil,
onCompleteTask: nil onCompleteTask: nil,
onArchiveTask: nil,
onUnarchiveTask: onUnarchiveTask
) )
.frame(width: geometry.size.width - 48) .frame(width: geometry.size.width - 48)
} }
@@ -100,7 +110,8 @@ struct TasksSection: View {
summary: CategorizedTaskSummary( summary: CategorizedTaskSummary(
upcoming: 3, upcoming: 3,
inProgress: 1, inProgress: 1,
done: 2 done: 2,
archived: 0
), ),
upcomingTasks: [ upcomingTasks: [
TaskDetail( TaskDetail(
@@ -154,6 +165,8 @@ struct TasksSection: View {
onUncancelTask: { _ in }, onUncancelTask: { _ in },
onMarkInProgress: { _ in }, onMarkInProgress: { _ in },
onCompleteTask: { _ in } onCompleteTask: { _ in }
, onArchiveTask: { _ in }
, onUnarchiveTask: { _ in }
) )
.padding() .padding()
} }

View File

@@ -113,7 +113,13 @@ struct AllTasksView: View {
}, },
onCompleteTask: { task in onCompleteTask: { task in
selectedTaskForComplete = task selectedTaskForComplete = task
} },
onArchiveTask: { task in
taskViewModel.archiveTask(id: task.id) { _ in
loadAllTasks()
}
},
onUnarchiveTask: nil
) )
.frame(width: geometry.size.width - 48) .frame(width: geometry.size.width - 48)
@@ -142,7 +148,13 @@ struct AllTasksView: View {
onMarkInProgress: nil, onMarkInProgress: nil,
onCompleteTask: { task in onCompleteTask: { task in
selectedTaskForComplete = task selectedTaskForComplete = task
} },
onArchiveTask: { task in
taskViewModel.archiveTask(id: task.id) { _ in
loadAllTasks()
}
},
onUnarchiveTask: nil
) )
.frame(width: geometry.size.width - 48) .frame(width: geometry.size.width - 48)
@@ -160,7 +172,13 @@ struct AllTasksView: View {
onCancelTask: nil, onCancelTask: nil,
onUncancelTask: nil, onUncancelTask: nil,
onMarkInProgress: 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) .frame(width: geometry.size.width - 48)
@@ -178,7 +196,13 @@ struct AllTasksView: View {
onCancelTask: nil, onCancelTask: nil,
onUncancelTask: nil, onUncancelTask: nil,
onMarkInProgress: 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) .frame(width: geometry.size.width - 48)
} }
@@ -269,6 +293,8 @@ struct TaskColumnView: View {
let onUncancelTask: ((TaskDetail) -> Void)? let onUncancelTask: ((TaskDetail) -> Void)?
let onMarkInProgress: ((TaskDetail) -> Void)? let onMarkInProgress: ((TaskDetail) -> Void)?
let onCompleteTask: ((TaskDetail) -> Void)? let onCompleteTask: ((TaskDetail) -> Void)?
let onArchiveTask: ((TaskDetail) -> Void)?
let onUnarchiveTask: ((TaskDetail) -> Void)?
var body: some View { var body: some View {
VStack(spacing: 0) { VStack(spacing: 0) {
@@ -316,7 +342,9 @@ struct TaskColumnView: View {
onCancel: onCancelTask != nil ? { onCancelTask?(task) } : nil, onCancel: onCancelTask != nil ? { onCancelTask?(task) } : nil,
onUncancel: onUncancelTask != nil ? { onUncancelTask?(task) } : nil, onUncancel: onUncancelTask != nil ? { onUncancelTask?(task) } : nil,
onMarkInProgress: onMarkInProgress != nil ? { onMarkInProgress?(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
) )
} }
} }

View File

@@ -12,6 +12,8 @@ class TaskViewModel: ObservableObject {
@Published var taskCancelled: Bool = false @Published var taskCancelled: Bool = false
@Published var taskUncancelled: Bool = false @Published var taskUncancelled: Bool = false
@Published var taskMarkedInProgress: Bool = false @Published var taskMarkedInProgress: Bool = false
@Published var taskArchived: Bool = false
@Published var taskUnarchived: Bool = false
// MARK: - Private Properties // MARK: - Private Properties
private let taskApi: TaskApi 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<TaskCancelResponse> {
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<TaskCancelResponse> {
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) { func completeTask(taskId: Int32, completion: @escaping (Bool) -> Void) {
guard let token = tokenStorage.getToken() else { guard let token = tokenStorage.getToken() else {
errorMessage = "Not authenticated" errorMessage = "Not authenticated"
@@ -215,6 +273,8 @@ class TaskViewModel: ObservableObject {
taskCancelled = false taskCancelled = false
taskUncancelled = false taskUncancelled = false
taskMarkedInProgress = false taskMarkedInProgress = false
taskArchived = false
taskUnarchived = false
errorMessage = nil errorMessage = nil
} }
} }