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.foundation.lazy.items
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.FolderOpen 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.Inbox
import androidx.compose.material.icons.outlined.Menu 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.Settings
import androidx.compose.material.icons.outlined.Today import androidx.compose.material.icons.outlined.Today
import androidx.compose.material3.DrawerValue import androidx.compose.material3.DrawerValue
@@ -58,6 +61,8 @@ fun MainScreen(viewModel: DrawerViewModel = hiltViewModel()) {
Route.Inbox.path to "Inbox", Route.Inbox.path to "Inbox",
Route.Today.path to "Aujourd'hui", Route.Today.path to "Aujourd'hui",
Route.Scheduled.path to "Planifié", Route.Scheduled.path to "Planifié",
Route.Search.path to "Recherche",
Route.Filter.path to "Filtres",
) )
val title = drawerTitles[currentRoute] val title = drawerTitles[currentRoute]
?: projects.find { "project/${it.id}" == currentRoute }?.name ?: projects.find { "project/${it.id}" == currentRoute }?.name
@@ -90,6 +95,33 @@ fun MainScreen(viewModel: DrawerViewModel = hiltViewModel()) {
scope.launch { drawerState.close() } 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)) HorizontalDivider(Modifier.padding(vertical = 8.dp))
Text( Text(
text = "Projets", 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.NavHost
import androidx.navigation.compose.composable import androidx.navigation.compose.composable
import androidx.navigation.navArgument import androidx.navigation.navArgument
import com.planify.mobile.ui.filter.FilterScreen
import com.planify.mobile.ui.inbox.InboxScreen 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.project.ProjectScreen
import com.planify.mobile.ui.scheduled.ScheduledScreen
import com.planify.mobile.ui.search.SearchScreen
import com.planify.mobile.ui.today.TodayScreen import com.planify.mobile.ui.today.TodayScreen
@Composable @Composable
@@ -40,9 +44,32 @@ fun PlanifyNavHost(
val projectId = backStack.arguments?.getString("projectId") ?: return@composable val projectId = backStack.arguments?.getString("projectId") ?: return@composable
ProjectScreen( ProjectScreen(
projectId = projectId, projectId = projectId,
onTaskClick = { /* TODO #11 : ouvrir édition */ }, onTaskClick = { /* TODO: ouvrir édition */ },
onBack = { navController.popBackStack() }, 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 Inbox : Route("inbox")
data object Today : Route("today") data object Today : Route("today")
data object Scheduled : Route("scheduled") data object Scheduled : Route("scheduled")
data object Search : Route("search")
data object Filter : Route("filter")
data class Project(val projectId: String = "{projectId}") : data class Project(val projectId: String = "{projectId}") :
Route("project/{projectId}") { Route("project/{projectId}") {
fun buildRoute(id: String) = "project/$id" fun buildRoute(id: String) = "project/$id"