This commit is contained in:
Trey t
2025-11-05 21:35:52 -06:00
parent a8083380aa
commit e272e45689
5 changed files with 129 additions and 108 deletions

View File

@@ -74,10 +74,18 @@ data class TaskDetail(
@Serializable @Serializable
data class TasksByResidenceResponse( data class TasksByResidenceResponse(
@SerialName("residence_id") val residenceId: String, @SerialName("residence_id") val residenceId: String,
val summary: TaskSummary, @SerialName("days_threshold") val daysThreshold: Int,
val tasks: List<TaskDetail>, val summary: CategorizedTaskSummary,
@SerialName("completed_tasks") val completedTasks: List<TaskDetail> = emptyList(), @SerialName("upcoming_tasks") val upcomingTasks: List<TaskDetail>,
@SerialName("cancelled_tasks") val cancelledTasks: List<TaskDetail> = emptyList() @SerialName("in_progress_tasks") val inProgressTasks: List<TaskDetail>,
@SerialName("done_tasks") val doneTasks: List<TaskDetail>
)
@Serializable
data class CategorizedTaskSummary(
val upcoming: Int,
@SerialName("in_progress") val inProgress: Int,
val done: Int
) )
@Serializable @Serializable

View File

@@ -94,10 +94,15 @@ class TaskApi(private val client: HttpClient = ApiClient.httpClient) {
} }
} }
suspend fun getTasksByResidence(token: String, residenceId: Int): ApiResult<TasksByResidenceResponse> { suspend fun getTasksByResidence(
token: String,
residenceId: Int,
days: Int = 30
): ApiResult<TasksByResidenceResponse> {
return try { return try {
val response = client.get("$baseUrl/tasks/by-residence/$residenceId/") { val response = client.get("$baseUrl/tasks/by-residence/$residenceId/") {
header("Authorization", "Token $token") header("Authorization", "Token $token")
parameter("days", days)
} }
if (response.status.isSuccess()) { if (response.status.isSuccess()) {

View File

@@ -48,8 +48,8 @@ fun ResidenceDetailScreen(
var showCompleteDialog by remember { mutableStateOf(false) } var showCompleteDialog by remember { mutableStateOf(false) }
var selectedTask by remember { mutableStateOf<TaskDetail?>(null) } var selectedTask by remember { mutableStateOf<TaskDetail?>(null) }
var showNewTaskDialog by remember { mutableStateOf(false) } var showNewTaskDialog by remember { mutableStateOf(false) }
var showCompletedTasks by remember { mutableStateOf(false) } var showInProgressTasks by remember { mutableStateOf(false) }
var showCancelledTasks by remember { mutableStateOf(false) } var showDoneTasks by remember { mutableStateOf(false) }
LaunchedEffect(residenceId) { LaunchedEffect(residenceId) {
residenceViewModel.getResidence(residenceId) { result -> residenceViewModel.getResidence(residenceId) { result ->
@@ -394,7 +394,7 @@ fun ResidenceDetailScreen(
} }
is ApiResult.Success -> { is ApiResult.Success -> {
val taskData = (tasksState as ApiResult.Success).data val taskData = (tasksState as ApiResult.Success).data
if (taskData.tasks.isEmpty() && taskData.completedTasks.isEmpty() && taskData.cancelledTasks.isEmpty()) { if (taskData.upcomingTasks.isEmpty() && taskData.inProgressTasks.isEmpty() && taskData.doneTasks.isEmpty()) {
item { item {
Card( Card(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
@@ -427,7 +427,8 @@ fun ResidenceDetailScreen(
} }
} }
} else { } else {
items(taskData.tasks) { task -> // Upcoming tasks section
items(taskData.upcomingTasks) { task ->
TaskCard( TaskCard(
task = task, task = task,
onCompleteClick = { onCompleteClick = {
@@ -444,14 +445,69 @@ fun ResidenceDetailScreen(
) )
} }
// Completed tasks section // In Progress tasks section
if (taskData.completedTasks.isNotEmpty()) { if (taskData.inProgressTasks.isNotEmpty()) {
item { item {
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.clickable { showCompletedTasks = !showCompletedTasks } .clickable { showInProgressTasks = !showInProgressTasks }
.padding(vertical = 8.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Row(verticalAlignment = Alignment.CenterVertically) {
Icon(
Icons.Default.PlayCircle,
contentDescription = null,
tint = MaterialTheme.colorScheme.tertiary,
modifier = Modifier.size(28.dp)
)
Spacer(modifier = Modifier.width(8.dp))
Text(
text = "In Progress (${taskData.inProgressTasks.size})",
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.tertiary
)
}
Icon(
if (showInProgressTasks) Icons.Default.KeyboardArrowUp else Icons.Default.KeyboardArrowDown,
contentDescription = null,
tint = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
if (showInProgressTasks) {
items(taskData.inProgressTasks) { task ->
TaskCard(
task = task,
onCompleteClick = {
selectedTask = task
showCompleteDialog = true
},
onEditClick = {
onNavigateToEditTask(task)
},
onCancelClick = {
residenceViewModel.cancelTask(task.id)
},
onUncancelClick = null
)
}
}
}
// Done tasks section
if (taskData.doneTasks.isNotEmpty()) {
item {
Spacer(modifier = Modifier.height(8.dp))
Row(
modifier = Modifier
.fillMaxWidth()
.clickable { showDoneTasks = !showDoneTasks }
.padding(vertical = 8.dp), .padding(vertical = 8.dp),
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
@@ -460,77 +516,27 @@ fun ResidenceDetailScreen(
Icon( Icon(
Icons.Default.CheckCircle, Icons.Default.CheckCircle,
contentDescription = null, contentDescription = null,
tint = MaterialTheme.colorScheme.tertiary, tint = MaterialTheme.colorScheme.primary,
modifier = Modifier.size(28.dp) modifier = Modifier.size(28.dp)
) )
Spacer(modifier = Modifier.width(8.dp)) Spacer(modifier = Modifier.width(8.dp))
Text( Text(
text = "Completed Tasks (${taskData.completedTasks.size})", text = "Done (${taskData.doneTasks.size})",
style = MaterialTheme.typography.titleMedium, style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.tertiary color = MaterialTheme.colorScheme.primary
) )
} }
Icon( Icon(
if (showCompletedTasks) Icons.Default.KeyboardArrowUp else Icons.Default.KeyboardArrowDown, if (showDoneTasks) Icons.Default.KeyboardArrowUp else Icons.Default.KeyboardArrowDown,
contentDescription = null, contentDescription = null,
tint = MaterialTheme.colorScheme.onSurfaceVariant tint = MaterialTheme.colorScheme.onSurfaceVariant
) )
} }
} }
if (showCompletedTasks) { if (showDoneTasks) {
items(taskData.completedTasks) { task -> items(taskData.doneTasks) { task ->
TaskCard(
task = task,
onCompleteClick = null,
onEditClick = {
onNavigateToEditTask(task)
},
onCancelClick = null,
onUncancelClick = null
)
}
}
}
// Cancelled tasks section
if (taskData.cancelledTasks.isNotEmpty()) {
item {
Spacer(modifier = Modifier.height(8.dp))
Row(
modifier = Modifier
.fillMaxWidth()
.clickable { showCancelledTasks = !showCancelledTasks }
.padding(vertical = 8.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Row(verticalAlignment = Alignment.CenterVertically) {
Icon(
Icons.Default.Cancel,
contentDescription = null,
tint = MaterialTheme.colorScheme.error,
modifier = Modifier.size(28.dp)
)
Spacer(modifier = Modifier.width(8.dp))
Text(
text = "Cancelled Tasks (${taskData.cancelledTasks.size})",
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.error
)
}
Icon(
if (showCancelledTasks) Icons.Default.KeyboardArrowUp else Icons.Default.KeyboardArrowDown,
contentDescription = null,
tint = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
if (showCancelledTasks) {
items(taskData.cancelledTasks) { task ->
TaskCard( TaskCard(
task = task, task = task,
onCompleteClick = null, onCompleteClick = null,

View File

@@ -12,8 +12,8 @@ struct ResidenceDetailView: View {
@State private var showEditResidence = false @State private var showEditResidence = false
@State private var showEditTask = false @State private var showEditTask = false
@State private var selectedTaskForEdit: TaskDetail? @State private var selectedTaskForEdit: TaskDetail?
@State private var showCompletedTasks = false @State private var showInProgressTasks = false
@State private var showCancelledTasks = false @State private var showDoneTasks = false
var body: some View { var body: some View {
ZStack { ZStack {
@@ -38,8 +38,8 @@ struct ResidenceDetailView: View {
if let tasksResponse = tasksResponse { if let tasksResponse = tasksResponse {
TasksSection( TasksSection(
tasksResponse: tasksResponse, tasksResponse: tasksResponse,
showCompletedTasks: $showCompletedTasks, showInProgressTasks: $showInProgressTasks,
showCancelledTasks: $showCancelledTasks, showDoneTasks: $showDoneTasks,
onEditTask: { task in onEditTask: { task in
selectedTaskForEdit = task selectedTaskForEdit = task
showEditTask = true showEditTask = true
@@ -134,7 +134,7 @@ struct ResidenceDetailView: View {
tasksError = nil tasksError = nil
let taskApi = TaskApi(client: ApiClient_iosKt.createHttpClient()) let taskApi = TaskApi(client: ApiClient_iosKt.createHttpClient())
taskApi.getTasksByResidence(token: token, residenceId: residenceId) { result, error in taskApi.getTasksByResidence(token: token, residenceId: residenceId, days: 30) { result, error in
if let successResult = result as? ApiResultSuccess<TasksByResidenceResponse> { if let successResult = result as? ApiResultSuccess<TasksByResidenceResponse> {
self.tasksResponse = successResult.data self.tasksResponse = successResult.data
self.isLoadingTasks = false self.isLoadingTasks = false

View File

@@ -3,8 +3,8 @@ import ComposeApp
struct TasksSection: View { struct TasksSection: View {
let tasksResponse: TasksByResidenceResponse let tasksResponse: TasksByResidenceResponse
@Binding var showCompletedTasks: Bool @Binding var showInProgressTasks: Bool
@Binding var showCancelledTasks: Bool @Binding var showDoneTasks: Bool
let onEditTask: (TaskDetail) -> Void let onEditTask: (TaskDetail) -> Void
let onCancelTask: (TaskDetail) -> Void let onCancelTask: (TaskDetail) -> Void
let onUncancelTask: (TaskDetail) -> Void let onUncancelTask: (TaskDetail) -> Void
@@ -19,16 +19,17 @@ struct TasksSection: View {
Spacer() Spacer()
HStack(spacing: 8) { HStack(spacing: 8) {
TaskPill(count: tasksResponse.summary.total, label: "Total", color: .blue) TaskPill(count: Int32(tasksResponse.summary.upcoming), label: "Upcoming", color: .blue)
TaskPill(count: tasksResponse.summary.pending, label: "Pending", color: .orange) TaskPill(count: Int32(tasksResponse.summary.inProgress), label: "In Progress", color: .orange)
TaskPill(count: tasksResponse.summary.completed, label: "Done", color: .green) TaskPill(count: Int32(tasksResponse.summary.done), label: "Done", color: .green)
} }
} }
if tasksResponse.tasks.isEmpty && tasksResponse.completedTasks.isEmpty && tasksResponse.cancelledTasks.isEmpty { if tasksResponse.upcomingTasks.isEmpty && tasksResponse.inProgressTasks.isEmpty && tasksResponse.doneTasks.isEmpty {
EmptyTasksView() EmptyTasksView()
} else { } else {
ForEach(tasksResponse.tasks, id: \.id) { task in // Upcoming tasks
ForEach(tasksResponse.upcomingTasks, id: \.id) { task in
TaskCard( TaskCard(
task: task, task: task,
onEdit: { onEditTask(task) }, onEdit: { onEditTask(task) },
@@ -37,31 +38,32 @@ struct TasksSection: View {
) )
} }
if !tasksResponse.completedTasks.isEmpty { // In Progress tasks section
if !tasksResponse.inProgressTasks.isEmpty {
VStack(alignment: .leading, spacing: 12) { VStack(alignment: .leading, spacing: 12) {
HStack { HStack {
Label("Completed Tasks (\(tasksResponse.completedTasks.count))", systemImage: "checkmark.circle") Label("In Progress (\(tasksResponse.inProgressTasks.count))", systemImage: "play.circle")
.font(.headline) .font(.headline)
.foregroundColor(.green) .foregroundColor(.orange)
Spacer() Spacer()
Image(systemName: showCompletedTasks ? "chevron.up" : "chevron.down") Image(systemName: showInProgressTasks ? "chevron.up" : "chevron.down")
.foregroundColor(.secondary) .foregroundColor(.secondary)
.font(.caption) .font(.caption)
} }
.padding(.top, 8) .padding(.top, 8)
.contentShape(Rectangle()) .contentShape(Rectangle())
.onTapGesture { .onTapGesture {
showCompletedTasks.toggle() showInProgressTasks.toggle()
} }
if showCompletedTasks { if showInProgressTasks {
ForEach(tasksResponse.completedTasks, id: \.id) { task in ForEach(tasksResponse.inProgressTasks, id: \.id) { task in
TaskCard( TaskCard(
task: task, task: task,
onEdit: { onEditTask(task) }, onEdit: { onEditTask(task) },
onCancel: nil, onCancel: { onCancelTask(task) },
onUncancel: nil onUncancel: nil
) )
} }
@@ -69,32 +71,33 @@ struct TasksSection: View {
} }
} }
if !tasksResponse.cancelledTasks.isEmpty { // Done tasks section
if !tasksResponse.doneTasks.isEmpty {
VStack(alignment: .leading, spacing: 12) { VStack(alignment: .leading, spacing: 12) {
HStack { HStack {
Label("Cancelled Tasks (\(tasksResponse.cancelledTasks.count))", systemImage: "xmark.circle") Label("Done (\(tasksResponse.doneTasks.count))", systemImage: "checkmark.circle")
.font(.headline) .font(.headline)
.foregroundColor(.red) .foregroundColor(.green)
Spacer() Spacer()
Image(systemName: showCancelledTasks ? "chevron.up" : "chevron.down") Image(systemName: showDoneTasks ? "chevron.up" : "chevron.down")
.foregroundColor(.secondary) .foregroundColor(.secondary)
.font(.caption) .font(.caption)
} }
.padding(.top, 8) .padding(.top, 8)
.contentShape(Rectangle()) .contentShape(Rectangle())
.onTapGesture { .onTapGesture {
showCancelledTasks.toggle() showDoneTasks.toggle()
} }
if showCancelledTasks { if showDoneTasks {
ForEach(tasksResponse.cancelledTasks, id: \.id) { task in ForEach(tasksResponse.doneTasks, id: \.id) { task in
TaskCard( TaskCard(
task: task, task: task,
onEdit: { onEditTask(task) }, onEdit: { onEditTask(task) },
onCancel: nil, onCancel: nil,
onUncancel: { onUncancelTask(task) } onUncancel: nil
) )
} }
} }
@@ -109,14 +112,13 @@ struct TasksSection: View {
TasksSection( TasksSection(
tasksResponse: TasksByResidenceResponse( tasksResponse: TasksByResidenceResponse(
residenceId: "1", residenceId: "1",
summary: TaskSummary( daysThreshold: 30,
total: 3, summary: CategorizedTaskSummary(
completed: 1, upcoming: 3,
pending: 2, inProgress: 1,
inProgress: 0, done: 2
overdue: 1
), ),
tasks: [ upcomingTasks: [
TaskDetail( TaskDetail(
id: 1, id: 1,
residence: 1, residence: 1,
@@ -137,7 +139,8 @@ struct TasksSection: View {
completions: [] completions: []
) )
], ],
completedTasks: [ inProgressTasks: [],
doneTasks: [
TaskDetail( TaskDetail(
id: 2, id: 2,
residence: 1, residence: 1,
@@ -157,11 +160,10 @@ struct TasksSection: View {
showCompletedButton: false, showCompletedButton: false,
completions: [] completions: []
) )
], ]
cancelledTasks: []
), ),
showCompletedTasks: .constant(true), showInProgressTasks: .constant(true),
showCancelledTasks: .constant(true), showDoneTasks: .constant(true),
onEditTask: { _ in }, onEditTask: { _ in },
onCancelTask: { _ in }, onCancelTask: { _ in },
onUncancelTask: { _ in } onUncancelTask: { _ in }