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.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"
|
||||||
|
|||||||
Reference in New Issue
Block a user