feat: [#26] filtres intelligents (Toutes, Terminées, Récurrentes, Priorités) + navigation Scheduled/Search/Filter/Labels
This commit is contained in:
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user