feat: [#26] filtres intelligents (Toutes, Terminées, Récurrentes, Priorités) + navigation Scheduled/Search/Filter/Labels

This commit is contained in:
2026-06-06 06:39:14 +02:00
parent 5d1c69484a
commit 1316c6555b
5 changed files with 193 additions and 1 deletions
@@ -10,8 +10,11 @@ import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.FolderOpen
import androidx.compose.material.icons.outlined.CalendarMonth
import androidx.compose.material.icons.outlined.FilterList
import androidx.compose.material.icons.outlined.Inbox
import androidx.compose.material.icons.outlined.Menu
import androidx.compose.material.icons.outlined.Search
import androidx.compose.material.icons.outlined.Settings
import androidx.compose.material.icons.outlined.Today
import androidx.compose.material3.DrawerValue
@@ -58,6 +61,8 @@ fun MainScreen(viewModel: DrawerViewModel = hiltViewModel()) {
Route.Inbox.path to "Inbox",
Route.Today.path to "Aujourd'hui",
Route.Scheduled.path to "Planifié",
Route.Search.path to "Recherche",
Route.Filter.path to "Filtres",
)
val title = drawerTitles[currentRoute]
?: projects.find { "project/${it.id}" == currentRoute }?.name
@@ -90,6 +95,33 @@ fun MainScreen(viewModel: DrawerViewModel = hiltViewModel()) {
scope.launch { drawerState.close() }
},
)
NavigationDrawerItem(
icon = { Icon(Icons.Outlined.CalendarMonth, null) },
label = { Text("Planifié") },
selected = currentRoute == Route.Scheduled.path,
onClick = {
navController.navigate(Route.Scheduled.path)
scope.launch { drawerState.close() }
},
)
NavigationDrawerItem(
icon = { Icon(Icons.Outlined.Search, null) },
label = { Text("Recherche") },
selected = currentRoute == Route.Search.path,
onClick = {
navController.navigate(Route.Search.path)
scope.launch { drawerState.close() }
},
)
NavigationDrawerItem(
icon = { Icon(Icons.Outlined.FilterList, null) },
label = { Text("Filtres") },
selected = currentRoute == Route.Filter.path,
onClick = {
navController.navigate(Route.Filter.path)
scope.launch { drawerState.close() }
},
)
HorizontalDivider(Modifier.padding(vertical = 8.dp))
Text(
text = "Projets",
@@ -0,0 +1,80 @@
package com.planify.mobile.ui.filter
import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.rememberScrollState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.FilterList
import androidx.compose.material3.FilterChip
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
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.TaskRow
private val filterLabels = mapOf(
FilterType.ALL to "Toutes",
FilterType.COMPLETED to "Terminées",
FilterType.REPEATING to "Récurrentes",
FilterType.PRIORITY_1 to "Priorité urgente",
FilterType.PRIORITY_2 to "Priorité haute",
FilterType.PRIORITY_3 to "Priorité moyenne",
)
@Composable
fun FilterScreen(
initialFilter: FilterType = FilterType.ALL,
onTaskClick: (Task) -> Unit,
viewModel: FilterViewModel = hiltViewModel(),
) {
val tasks by viewModel.tasks.collectAsState()
val activeFilter by viewModel.activeFilter.collectAsState()
Column(modifier = Modifier.fillMaxSize()) {
Row(
modifier = Modifier
.fillMaxWidth()
.horizontalScroll(rememberScrollState())
.padding(horizontal = 12.dp, vertical = 8.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp),
) {
FilterType.entries.forEach { filter ->
FilterChip(
selected = activeFilter == filter,
onClick = { viewModel.setFilter(filter) },
label = { Text(filterLabels[filter] ?: filter.name) },
)
}
}
if (tasks.isEmpty()) {
EmptyState(
icon = Icons.Outlined.FilterList,
title = "Aucune tâche",
subtitle = "Aucune tâche ne correspond à ce filtre",
)
} else {
LazyColumn(modifier = Modifier.fillMaxSize()) {
items(tasks, key = { it.id }) { task ->
TaskRow(
task = task,
onClick = { onTaskClick(task) },
onCheckedChange = { viewModel.toggleTask(task) },
)
}
}
}
}
}
@@ -0,0 +1,51 @@
package com.planify.mobile.ui.filter
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.planify.mobile.domain.model.Task
import com.planify.mobile.domain.repository.TaskRepository
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import javax.inject.Inject
enum class FilterType { ALL, COMPLETED, REPEATING, PRIORITY_1, PRIORITY_2, PRIORITY_3 }
@HiltViewModel
class FilterViewModel @Inject constructor(
private val taskRepository: TaskRepository,
) : ViewModel() {
private val _filter = MutableStateFlow(FilterType.ALL)
@OptIn(ExperimentalCoroutinesApi::class)
val tasks = _filter.flatMapLatest { filter ->
when (filter) {
FilterType.ALL -> taskRepository.getInboxTasks().let {
// ALL = all uncompleted tasks across all projects
taskRepository.getTasksByPriority(4).let { _ ->
// Use a union approach via getRepeatingTasks as base — actually
// for ALL we use getScheduledTasks + inbox combined.
// Simplify: use priority 4 as "all" isn't perfect; provide getAll via search ""
taskRepository.searchTasks("")
}
}
FilterType.COMPLETED -> taskRepository.getCompletedTasks()
FilterType.REPEATING -> taskRepository.getRepeatingTasks()
FilterType.PRIORITY_1 -> taskRepository.getTasksByPriority(1)
FilterType.PRIORITY_2 -> taskRepository.getTasksByPriority(2)
FilterType.PRIORITY_3 -> taskRepository.getTasksByPriority(3)
}
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), emptyList())
fun setFilter(filter: FilterType) { _filter.value = filter }
val activeFilter get() = _filter
fun toggleTask(task: Task) {
viewModelScope.launch { taskRepository.checkTask(task.id, !task.checked) }
}
}
@@ -7,8 +7,12 @@ import androidx.navigation.NavType
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.navArgument
import com.planify.mobile.ui.filter.FilterScreen
import com.planify.mobile.ui.inbox.InboxScreen
import com.planify.mobile.ui.label.LabelScreen
import com.planify.mobile.ui.project.ProjectScreen
import com.planify.mobile.ui.scheduled.ScheduledScreen
import com.planify.mobile.ui.search.SearchScreen
import com.planify.mobile.ui.today.TodayScreen
@Composable
@@ -40,9 +44,32 @@ fun PlanifyNavHost(
val projectId = backStack.arguments?.getString("projectId") ?: return@composable
ProjectScreen(
projectId = projectId,
onTaskClick = { /* TODO #11 : ouvrir édition */ },
onTaskClick = { /* TODO: ouvrir édition */ },
onBack = { navController.popBackStack() },
)
}
composable(Route.Scheduled.path) {
ScheduledScreen(onTaskClick = { /* TODO: ouvrir édition */ })
}
composable(Route.Search.path) {
SearchScreen(onTaskClick = { /* TODO: ouvrir édition */ })
}
composable(Route.Filter.path) {
FilterScreen(onTaskClick = { /* TODO: ouvrir édition */ })
}
composable(
route = Route.Label().path,
arguments = listOf(navArgument("labelId") { type = NavType.StringType })
) { backStack ->
val labelId = backStack.arguments?.getString("labelId") ?: return@composable
LabelScreen(
labelId = labelId,
onTaskClick = { /* TODO: ouvrir édition */ },
)
}
}
}
@@ -4,6 +4,8 @@ sealed class Route(val path: String) {
data object Inbox : Route("inbox")
data object Today : Route("today")
data object Scheduled : Route("scheduled")
data object Search : Route("search")
data object Filter : Route("filter")
data class Project(val projectId: String = "{projectId}") :
Route("project/{projectId}") {
fun buildRoute(id: String) = "project/$id"