fix(system,kanban): make doctor optional and render dynamic kanban columns

This commit is contained in:
M57 committed 2026-06-16 10:13:11 +03:00
1 parent a994c7d5db
commit 4c572509ec
3 files changed
+55 -52

No files matched your search

@@ -145,23 +145,25 @@ fun KanbanScreen(
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Text("No boards found.")
}
} else if (state.columns.isEmpty()) {
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Text("No columns found for this board.")
}
} else {
// Columns list: To Do, In Progress, Done
val columns = listOf("todo", "in_progress", "done")
val columnNames =
mapOf(
"todo" to "To Do",
"in_progress" to "In Progress",
"done" to "Done",
)
LazyRow(
modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(16.dp),
horizontalArrangement = Arrangement.spacedBy(16.dp),
) {
items(columns) { col ->
val colTasks = state.tasks.filter { it.status.lowercase() == col }
items(state.columns) { column ->
val colName = column.name
val colTasks =
state.tasks.filter {
it.status.equals(colName, ignoreCase = true)
}
val columnIndex = state.columns.indexOf(column)
val prevColumn = state.columns.getOrNull(columnIndex - 1)
val nextColumn = state.columns.getOrNull(columnIndex + 1)
Column(
modifier =
@@ -170,7 +172,7 @@ fun KanbanScreen(
.fillMaxSize(),
) {
Text(
text = "${columnNames[col]} (${colTasks.size})",
text = "${colName.replaceFirstChar { it.uppercase() }} (${colTasks.size})",
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold,
modifier = Modifier.padding(bottom = 8.dp),
@@ -181,30 +183,16 @@ fun KanbanScreen(
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
items(colTasks) { task ->
val onMoveLeft: (() -> Unit)? =
if (col != "todo") {
{
val prevStatus = if (col == "done") "in_progress" else "todo"
viewModel.moveTask(task, prevStatus)
}
} else {
null
}
val onMoveRight: (() -> Unit)? =
if (col != "done") {
{
val nextStatus = if (col == "todo") "in_progress" else "done"
viewModel.moveTask(task, nextStatus)
}
} else {
null
}
TaskCard(
task = task,
onMoveLeft = onMoveLeft,
onMoveRight = onMoveRight,
onMoveLeft =
prevColumn?.let {
{ viewModel.moveTask(task, it.name) }
},
onMoveRight =
nextColumn?.let {
{ viewModel.moveTask(task, it.name) }
},
)
}
}
@@ -219,7 +207,7 @@ fun KanbanScreen(
AddTaskDialog(
onDismiss = { showAddTaskDialog = false },
onConfirm = { title, desc ->
viewModel.createTask(title, desc, "todo")
viewModel.createTask(title, desc, state.columns.firstOrNull()?.name ?: "todo")
showAddTaskDialog = false
},
)
@@ -3,6 +3,7 @@ package com.m57.hermescontrol.ui.kanban
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.m57.hermescontrol.data.model.KanbanBoard
import com.m57.hermescontrol.data.model.KanbanColumn
import com.m57.hermescontrol.data.model.KanbanTask
import com.m57.hermescontrol.data.remote.ApiClient
import kotlinx.coroutines.Dispatchers
@@ -17,6 +18,7 @@ data class KanbanUiState(
val isLoading: Boolean = false,
val boards: List<KanbanBoard> = emptyList(),
val selectedBoard: KanbanBoard? = null,
val columns: List<KanbanColumn> = emptyList(),
val tasks: List<KanbanTask> = emptyList(),
val errorMessage: String? = null,
val toastMessage: String? = null,
@@ -75,13 +77,15 @@ class KanbanViewModel : ViewModel() {
ApiClient.hermesApi.getKanbanBoard()
}
if (response.isSuccessful) {
val allTasks =
response
.body()
?.columns
?.flatMap { it.tasks }
.orEmpty()
_uiState.update { it.copy(isLoading = false, tasks = allTasks) }
val body = response.body()
val allTasks = body?.columns?.flatMap { it.tasks }.orEmpty()
_uiState.update {
it.copy(
isLoading = false,
columns = body?.columns.orEmpty(),
tasks = allTasks,
)
}
} else {
_uiState.update {
it.copy(
@@ -33,26 +33,21 @@ class SystemViewModel : ViewModel() {
withContext(Dispatchers.IO) {
ApiClient.hermesApi.getSystemStats()
}
val doctorResponse =
withContext(Dispatchers.IO) {
ApiClient.hermesApi.runDoctor()
}
if (statsResponse.isSuccessful && doctorResponse.isSuccessful) {
if (statsResponse.isSuccessful) {
_uiState.update {
it.copy(
isLoading = false,
stats = statsResponse.body(),
doctorReport = doctorResponse.body(),
)
}
// Doctor is optional; many servers do not expose it.
loadDoctorReport()
} else {
_uiState.update {
it.copy(
isLoading = false,
errorMessage =
"Failed to load system data: HTTP " +
"${statsResponse.code()} / ${doctorResponse.code()}",
errorMessage = "Failed to load system stats: HTTP ${statsResponse.code()}",
)
}
}
@@ -60,13 +55,29 @@ class SystemViewModel : ViewModel() {
_uiState.update {
it.copy(
isLoading = false,
errorMessage = "Failed to load system data: ${e.message}",
errorMessage = "Failed to load system stats: ${e.message}",
)
}
}
}
}
private fun loadDoctorReport() {
viewModelScope.launch {
try {
val doctorResponse =
withContext(Dispatchers.IO) {
ApiClient.hermesApi.runDoctor()
}
if (doctorResponse.isSuccessful) {
_uiState.update { it.copy(doctorReport = doctorResponse.body()) }
}
} catch (_: Exception) {
// Ignore optional doctor failures.
}
}
}
fun triggerBackup() {
viewModelScope.launch {
try {