From 65a54af66ad672814a932f6041aa36fffa94e5c8 Mon Sep 17 00:00:00 2001 From: Gato Date: Sat, 6 Jun 2026 06:05:03 +0200 Subject: [PATCH] feat: [#8] vue Today (TodayScreen, TodayViewModel, groupement par projet) --- .../planify/mobile/ui/today/TodayScreen.kt | 96 +++++++++++++++++++ .../planify/mobile/ui/today/TodayViewModel.kt | 50 ++++++++++ 2 files changed, 146 insertions(+) create mode 100644 app/src/main/java/com/planify/mobile/ui/today/TodayScreen.kt create mode 100644 app/src/main/java/com/planify/mobile/ui/today/TodayViewModel.kt diff --git a/app/src/main/java/com/planify/mobile/ui/today/TodayScreen.kt b/app/src/main/java/com/planify/mobile/ui/today/TodayScreen.kt new file mode 100644 index 0000000..dbe90d3 --- /dev/null +++ b/app/src/main/java/com/planify/mobile/ui/today/TodayScreen.kt @@ -0,0 +1,96 @@ +package com.planify.mobile.ui.today + +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Today +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import com.planify.mobile.domain.model.Task +import com.planify.mobile.ui.components.EmptyState +import com.planify.mobile.ui.components.SectionHeader +import com.planify.mobile.ui.components.TaskRow + +@Composable +fun TodayScreen( + onTaskClick: (Task) -> Unit, + modifier: Modifier = Modifier, + viewModel: TodayViewModel = hiltViewModel(), +) { + val state by viewModel.uiState.collectAsState() + val collapsedSections = remember { mutableStateOf(setOf()) } + + if (state.totalCount == 0 && !state.isLoading) { + EmptyState( + icon = Icons.Outlined.Today, + title = "Rien pour aujourd'hui", + subtitle = "Profitez de votre journée !", + modifier = modifier, + ) + return + } + + LazyColumn(modifier = modifier.fillMaxSize()) { + if (state.overdueTasks.isNotEmpty()) { + item { + SectionHeader( + name = "En retard", + taskCount = state.overdueTasks.size, + collapsed = "overdue" in collapsedSections.value, + onToggleCollapse = { + collapsedSections.value = collapsedSections.value.toggle("overdue") + }, + onAddTask = {}, + ) + } + if ("overdue" !in collapsedSections.value) { + items(state.overdueTasks, key = { "overdue_${it.id}" }) { task -> + TaskRow( + task = task, + onCheckedChange = { viewModel.toggleTask(task) }, + onClick = { onTaskClick(task) }, + ) + } + } + item { HorizontalDivider(Modifier.padding(horizontal = 16.dp)) } + } + + state.tasksByProject.forEach { (projectName, tasks) -> + item(key = "header_$projectName") { + SectionHeader( + name = projectName, + taskCount = tasks.size, + collapsed = projectName in collapsedSections.value, + onToggleCollapse = { + collapsedSections.value = collapsedSections.value.toggle(projectName) + }, + onAddTask = {}, + ) + } + if (projectName !in collapsedSections.value) { + items(tasks, key = { it.id }) { task -> + TaskRow( + task = task, + onCheckedChange = { viewModel.toggleTask(task) }, + onClick = { onTaskClick(task) }, + ) + } + } + } + } +} + +private fun Set.toggle(key: String) = + if (contains(key)) minus(key) else plus(key) diff --git a/app/src/main/java/com/planify/mobile/ui/today/TodayViewModel.kt b/app/src/main/java/com/planify/mobile/ui/today/TodayViewModel.kt new file mode 100644 index 0000000..a3f105e --- /dev/null +++ b/app/src/main/java/com/planify/mobile/ui/today/TodayViewModel.kt @@ -0,0 +1,50 @@ +package com.planify.mobile.ui.today + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.planify.mobile.domain.model.Task +import com.planify.mobile.domain.repository.ProjectRepository +import com.planify.mobile.domain.repository.TaskRepository +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch +import javax.inject.Inject + +data class TodayUiState( + val tasksByProject: Map> = emptyMap(), + val overdueTasks: List = emptyList(), + val totalCount: Int = 0, + val isLoading: Boolean = false, +) + +@HiltViewModel +class TodayViewModel @Inject constructor( + private val taskRepository: TaskRepository, + private val projectRepository: ProjectRepository, +) : ViewModel() { + + val uiState = combine( + taskRepository.getTodayTasks(), + taskRepository.getOverdueTasks(), + projectRepository.getAllProjects(), + ) { today, overdue, projects -> + val projectMap = projects.associateBy { it.id } + val grouped = today + .groupBy { task -> projectMap[task.projectId]?.name ?: task.projectId } + TodayUiState( + tasksByProject = grouped, + overdueTasks = overdue, + totalCount = today.size + overdue.size, + ) + }.stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(5_000), + initialValue = TodayUiState(isLoading = true), + ) + + fun toggleTask(task: Task) { + viewModelScope.launch { taskRepository.checkTask(task.id, !task.checked) } + } +}