feat: [#9] vue Projet (ProjectScreen liste/board, ProjectViewModel, SectionRepository)
This commit is contained in:
@@ -0,0 +1,36 @@
|
||||
package com.planify.mobile.data.repository
|
||||
|
||||
import com.planify.mobile.data.local.dao.SectionDao
|
||||
import com.planify.mobile.data.local.entity.SectionEntity
|
||||
import com.planify.mobile.domain.model.Section
|
||||
import com.planify.mobile.domain.repository.SectionRepository
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import javax.inject.Inject
|
||||
|
||||
class SectionRepositoryImpl @Inject constructor(
|
||||
private val dao: SectionDao,
|
||||
) : SectionRepository {
|
||||
|
||||
override fun getSectionsByProject(projectId: String): Flow<List<Section>> =
|
||||
dao.getSectionsByProject(projectId).map { it.map { e -> e.toDomain() } }
|
||||
|
||||
override suspend fun getSectionById(id: String): Section? =
|
||||
dao.getById(id)?.toDomain()
|
||||
|
||||
override suspend fun insertSection(section: Section) = dao.insert(section.toEntity())
|
||||
override suspend fun updateSection(section: Section) = dao.update(section.toEntity())
|
||||
override suspend fun deleteSection(id: String) = dao.softDelete(id)
|
||||
|
||||
private fun SectionEntity.toDomain() = Section(
|
||||
id = id, name = name, projectId = projectId, order = order,
|
||||
isArchived = isArchived, isDeleted = isDeleted, collapsed = collapsed,
|
||||
icalUrl = icalUrl, etag = etag,
|
||||
)
|
||||
|
||||
private fun Section.toEntity() = SectionEntity(
|
||||
id = id, name = name, projectId = projectId, order = order,
|
||||
isArchived = isArchived, isDeleted = isDeleted, collapsed = collapsed,
|
||||
icalUrl = icalUrl, etag = etag,
|
||||
)
|
||||
}
|
||||
@@ -1,8 +1,10 @@
|
||||
package com.planify.mobile.di
|
||||
|
||||
import com.planify.mobile.data.repository.ProjectRepositoryImpl
|
||||
import com.planify.mobile.data.repository.SectionRepositoryImpl
|
||||
import com.planify.mobile.data.repository.TaskRepositoryImpl
|
||||
import com.planify.mobile.domain.repository.ProjectRepository
|
||||
import com.planify.mobile.domain.repository.SectionRepository
|
||||
import com.planify.mobile.domain.repository.TaskRepository
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
@@ -19,4 +21,7 @@ abstract class RepositoryModule {
|
||||
|
||||
@Binds @Singleton
|
||||
abstract fun bindTaskRepository(impl: TaskRepositoryImpl): TaskRepository
|
||||
|
||||
@Binds @Singleton
|
||||
abstract fun bindSectionRepository(impl: SectionRepositoryImpl): SectionRepository
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.planify.mobile.domain.repository
|
||||
|
||||
import com.planify.mobile.domain.model.Section
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface SectionRepository {
|
||||
fun getSectionsByProject(projectId: String): Flow<List<Section>>
|
||||
suspend fun getSectionById(id: String): Section?
|
||||
suspend fun insertSection(section: Section)
|
||||
suspend fun updateSection(section: Section)
|
||||
suspend fun deleteSection(id: String)
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
package com.planify.mobile.ui.project
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
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.LazyRow
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.FolderOpen
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
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.domain.model.ViewStyle
|
||||
import com.planify.mobile.ui.components.EmptyState
|
||||
import com.planify.mobile.ui.components.SectionHeader
|
||||
import com.planify.mobile.ui.components.TaskRow
|
||||
|
||||
@Composable
|
||||
fun ProjectScreen(
|
||||
projectId: String,
|
||||
onTaskClick: (Task) -> Unit,
|
||||
onBack: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
viewModel: ProjectViewModel = hiltViewModel(),
|
||||
) {
|
||||
val state by viewModel.uiState.collectAsState()
|
||||
val collapsedSections = remember { mutableStateOf(setOf<String>()) }
|
||||
|
||||
if (!state.isLoading && state.sections.all { it.tasks.isEmpty() }) {
|
||||
EmptyState(
|
||||
icon = Icons.Outlined.FolderOpen,
|
||||
title = "Projet vide",
|
||||
subtitle = "Créez votre première tâche avec le bouton +",
|
||||
modifier = modifier,
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
when (state.viewStyle) {
|
||||
ViewStyle.LIST -> ProjectListView(
|
||||
state = state,
|
||||
collapsedSections = collapsedSections.value,
|
||||
onToggleSection = { key ->
|
||||
collapsedSections.value = collapsedSections.value.let {
|
||||
if (it.contains(key)) it - key else it + key
|
||||
}
|
||||
},
|
||||
onTaskClick = onTaskClick,
|
||||
onCheckedChange = { task -> viewModel.toggleTask(task) },
|
||||
modifier = modifier,
|
||||
)
|
||||
ViewStyle.BOARD -> ProjectBoardView(
|
||||
state = state,
|
||||
onTaskClick = onTaskClick,
|
||||
onCheckedChange = { task -> viewModel.toggleTask(task) },
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ProjectListView(
|
||||
state: ProjectUiState,
|
||||
collapsedSections: Set<String>,
|
||||
onToggleSection: (String) -> Unit,
|
||||
onTaskClick: (Task) -> Unit,
|
||||
onCheckedChange: (Task) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
LazyColumn(modifier = modifier.fillMaxSize()) {
|
||||
state.sections.forEach { group ->
|
||||
val key = group.section?.id ?: "unsectioned"
|
||||
val name = group.section?.name ?: "Sans section"
|
||||
|
||||
item(key = "header_$key") {
|
||||
SectionHeader(
|
||||
name = name,
|
||||
taskCount = group.tasks.size,
|
||||
collapsed = key in collapsedSections,
|
||||
onToggleCollapse = { onToggleSection(key) },
|
||||
onAddTask = {},
|
||||
)
|
||||
}
|
||||
|
||||
if (key !in collapsedSections) {
|
||||
items(group.tasks, key = { it.id }) { task ->
|
||||
TaskRow(
|
||||
task = task,
|
||||
onCheckedChange = { onCheckedChange(task) },
|
||||
onClick = { onTaskClick(task) },
|
||||
)
|
||||
}
|
||||
item { HorizontalDivider(Modifier.padding(horizontal = 16.dp)) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ProjectBoardView(
|
||||
state: ProjectUiState,
|
||||
onTaskClick: (Task) -> Unit,
|
||||
onCheckedChange: (Task) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
LazyRow(
|
||||
modifier = modifier.fillMaxSize(),
|
||||
contentPadding = PaddingValues(horizontal = 12.dp),
|
||||
) {
|
||||
items(state.sections) { group ->
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillParentMaxHeight()
|
||||
.padding(horizontal = 4.dp)
|
||||
.fillMaxWidth(0.75f),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp),
|
||||
) {
|
||||
Column {
|
||||
Text(
|
||||
text = group.section?.name ?: "Sans section",
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
modifier = Modifier.padding(12.dp),
|
||||
)
|
||||
LazyColumn {
|
||||
items(group.tasks, key = { it.id }) { task ->
|
||||
TaskRow(
|
||||
task = task,
|
||||
onCheckedChange = { onCheckedChange(task) },
|
||||
onClick = { onTaskClick(task) },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package com.planify.mobile.ui.project
|
||||
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.planify.mobile.domain.model.Project
|
||||
import com.planify.mobile.domain.model.Section
|
||||
import com.planify.mobile.domain.model.Task
|
||||
import com.planify.mobile.domain.model.ViewStyle
|
||||
import com.planify.mobile.domain.repository.ProjectRepository
|
||||
import com.planify.mobile.domain.repository.SectionRepository
|
||||
import com.planify.mobile.domain.repository.TaskRepository
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
data class SectionWithTasks(val section: Section?, val tasks: List<Task>)
|
||||
|
||||
data class ProjectUiState(
|
||||
val project: Project? = null,
|
||||
val sections: List<SectionWithTasks> = emptyList(),
|
||||
val viewStyle: ViewStyle = ViewStyle.LIST,
|
||||
val isLoading: Boolean = true,
|
||||
)
|
||||
|
||||
@HiltViewModel
|
||||
class ProjectViewModel @Inject constructor(
|
||||
savedStateHandle: SavedStateHandle,
|
||||
private val taskRepository: TaskRepository,
|
||||
private val projectRepository: ProjectRepository,
|
||||
private val sectionRepository: SectionRepository,
|
||||
) : ViewModel() {
|
||||
|
||||
private val projectId: String = checkNotNull(savedStateHandle["projectId"])
|
||||
|
||||
val uiState = combine(
|
||||
projectRepository.getAllProjects(),
|
||||
sectionRepository.getSectionsByProject(projectId),
|
||||
taskRepository.getTasksByProject(projectId),
|
||||
) { projects, sections, tasks ->
|
||||
val project = projects.find { it.id == projectId }
|
||||
val unsectioned = tasks.filter { it.sectionId == null }
|
||||
val sectionedGroups = sections.map { section ->
|
||||
SectionWithTasks(
|
||||
section = section,
|
||||
tasks = tasks.filter { it.sectionId == section.id },
|
||||
)
|
||||
}
|
||||
val allGroups = buildList {
|
||||
if (unsectioned.isNotEmpty()) add(SectionWithTasks(null, unsectioned))
|
||||
addAll(sectionedGroups)
|
||||
}
|
||||
ProjectUiState(
|
||||
project = project,
|
||||
sections = allGroups,
|
||||
viewStyle = project?.viewStyle ?: ViewStyle.LIST,
|
||||
isLoading = false,
|
||||
)
|
||||
}.stateIn(
|
||||
scope = viewModelScope,
|
||||
started = SharingStarted.WhileSubscribed(5_000),
|
||||
initialValue = ProjectUiState(),
|
||||
)
|
||||
|
||||
fun toggleTask(task: Task) {
|
||||
viewModelScope.launch { taskRepository.checkTask(task.id, !task.checked) }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user