From 1316c6555bff23824e75a8d73ca18de5156f2816 Mon Sep 17 00:00:00 2001 From: Gato Date: Sat, 6 Jun 2026 06:39:14 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20[#26]=20filtres=20intelligents=20(Toute?= =?UTF-8?q?s,=20Termin=C3=A9es,=20R=C3=A9currentes,=20Priorit=C3=A9s)=20+?= =?UTF-8?q?=20navigation=20Scheduled/Search/Filter/Labels?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/planify/mobile/ui/MainScreen.kt | 32 ++++++++ .../planify/mobile/ui/filter/FilterScreen.kt | 80 +++++++++++++++++++ .../mobile/ui/filter/FilterViewModel.kt | 51 ++++++++++++ .../mobile/ui/navigation/PlanifyNavHost.kt | 29 ++++++- .../com/planify/mobile/ui/navigation/Route.kt | 2 + 5 files changed, 193 insertions(+), 1 deletion(-) create mode 100644 app/src/main/java/com/planify/mobile/ui/filter/FilterScreen.kt create mode 100644 app/src/main/java/com/planify/mobile/ui/filter/FilterViewModel.kt diff --git a/app/src/main/java/com/planify/mobile/ui/MainScreen.kt b/app/src/main/java/com/planify/mobile/ui/MainScreen.kt index 48ded9f..0f4413d 100644 --- a/app/src/main/java/com/planify/mobile/ui/MainScreen.kt +++ b/app/src/main/java/com/planify/mobile/ui/MainScreen.kt @@ -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", diff --git a/app/src/main/java/com/planify/mobile/ui/filter/FilterScreen.kt b/app/src/main/java/com/planify/mobile/ui/filter/FilterScreen.kt new file mode 100644 index 0000000..3e1763b --- /dev/null +++ b/app/src/main/java/com/planify/mobile/ui/filter/FilterScreen.kt @@ -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) }, + ) + } + } + } + } +} diff --git a/app/src/main/java/com/planify/mobile/ui/filter/FilterViewModel.kt b/app/src/main/java/com/planify/mobile/ui/filter/FilterViewModel.kt new file mode 100644 index 0000000..423cd60 --- /dev/null +++ b/app/src/main/java/com/planify/mobile/ui/filter/FilterViewModel.kt @@ -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) } + } +} diff --git a/app/src/main/java/com/planify/mobile/ui/navigation/PlanifyNavHost.kt b/app/src/main/java/com/planify/mobile/ui/navigation/PlanifyNavHost.kt index bbd9e2e..e5c982f 100644 --- a/app/src/main/java/com/planify/mobile/ui/navigation/PlanifyNavHost.kt +++ b/app/src/main/java/com/planify/mobile/ui/navigation/PlanifyNavHost.kt @@ -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 */ }, + ) + } } } diff --git a/app/src/main/java/com/planify/mobile/ui/navigation/Route.kt b/app/src/main/java/com/planify/mobile/ui/navigation/Route.kt index aae7359..3046dd0 100644 --- a/app/src/main/java/com/planify/mobile/ui/navigation/Route.kt +++ b/app/src/main/java/com/planify/mobile/ui/navigation/Route.kt @@ -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"