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