From ce22d498247387dfa21299f5f0ec15ad140b34f1 Mon Sep 17 00:00:00 2001 From: Gato Date: Sat, 6 Jun 2026 06:04:38 +0200 Subject: [PATCH] feat: [#7] vue Inbox (InboxScreen, InboxViewModel, TaskRepositoryImpl) --- .../data/repository/TaskRepositoryImpl.kt | 108 ++++++++++++++++++ .../planify/mobile/ui/inbox/InboxScreen.kt | 75 ++++++++++++ .../planify/mobile/ui/inbox/InboxViewModel.kt | 48 ++++++++ 3 files changed, 231 insertions(+) create mode 100644 app/src/main/java/com/planify/mobile/data/repository/TaskRepositoryImpl.kt create mode 100644 app/src/main/java/com/planify/mobile/ui/inbox/InboxScreen.kt create mode 100644 app/src/main/java/com/planify/mobile/ui/inbox/InboxViewModel.kt diff --git a/app/src/main/java/com/planify/mobile/data/repository/TaskRepositoryImpl.kt b/app/src/main/java/com/planify/mobile/data/repository/TaskRepositoryImpl.kt new file mode 100644 index 0000000..d049947 --- /dev/null +++ b/app/src/main/java/com/planify/mobile/data/repository/TaskRepositoryImpl.kt @@ -0,0 +1,108 @@ +package com.planify.mobile.data.repository + +import com.planify.mobile.data.local.dao.TaskDao +import com.planify.mobile.data.local.entity.TaskEntity +import com.planify.mobile.domain.model.DueDate +import com.planify.mobile.domain.model.ItemType +import com.planify.mobile.domain.model.RecurrencyType +import com.planify.mobile.domain.model.Task +import com.planify.mobile.domain.repository.TaskRepository +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import kotlinx.serialization.json.Json +import java.time.Instant +import java.time.ZoneOffset +import java.time.format.DateTimeFormatter +import javax.inject.Inject + +class TaskRepositoryImpl @Inject constructor( + private val dao: TaskDao, +) : TaskRepository { + + override fun getTasksByProject(projectId: String): Flow> = + dao.getTasksByProject(projectId).map { it.map { e -> e.toDomain() } } + + override fun getTasksBySection(sectionId: String): Flow> = + dao.getTasksBySection(sectionId).map { it.map { e -> e.toDomain() } } + + override fun getInboxTasks(): Flow> = + dao.getInboxTasks().map { it.map { e -> e.toDomain() } } + + override fun getTodayTasks(): Flow> = + dao.getTodayTasks().map { it.map { e -> e.toDomain() } } + + override fun getOverdueTasks(): Flow> = + dao.getOverdueTasks().map { it.map { e -> e.toDomain() } } + + override fun getSubTasks(parentId: String): Flow> = + dao.getSubTasks(parentId).map { it.map { e -> e.toDomain() } } + + override suspend fun getTaskById(id: String): Task? = + dao.getTaskById(id)?.toDomain() + + override suspend fun insertTask(task: Task) = + dao.insert(task.toEntity()) + + override suspend fun updateTask(task: Task) = + dao.update(task.toEntity()) + + override suspend fun deleteTask(id: String) { + val now = DateTimeFormatter.ISO_INSTANT.format(Instant.now().atOffset(ZoneOffset.UTC)) + dao.softDelete(id, now) + } + + override suspend fun checkTask(id: String, checked: Boolean) { + val now = DateTimeFormatter.ISO_INSTANT.format(Instant.now().atOffset(ZoneOffset.UTC)) + dao.setChecked(id, checked, if (checked) now else null, now) + } + + private fun TaskEntity.toDomain() = Task( + id = id, + content = content, + description = description, + projectId = projectId, + sectionId = sectionId, + parentId = parentId, + priority = priority, + checked = checked, + dueDate = dueDate?.let { runCatching { Json.decodeFromString(it) }.getOrNull() }, + deadlineDate = deadlineDate, + labels = runCatching { Json.decodeFromString>(labels) }.getOrDefault(emptyList()), + pinned = pinned, + collapsed = collapsed, + childOrder = childOrder, + addedAt = addedAt, + updatedAt = updatedAt, + completedAt = completedAt, + itemType = runCatching { ItemType.valueOf(itemType) }.getOrDefault(ItemType.TASK), + calendarEventUid = calendarEventUid, + icalUrl = icalUrl, + etag = etag, + responsibleUid = responsibleUid, + ) + + private fun Task.toEntity() = TaskEntity( + id = id, + content = content, + description = description, + projectId = projectId, + sectionId = sectionId, + parentId = parentId, + priority = priority, + checked = checked, + dueDate = dueDate?.let { Json.encodeToString(DueDate.serializer(), it) }, + deadlineDate = deadlineDate, + labels = Json.encodeToString(kotlinx.serialization.builtins.ListSerializer(kotlinx.serialization.builtins.serializer()), labels), + pinned = pinned, + collapsed = collapsed, + childOrder = childOrder, + addedAt = addedAt, + updatedAt = updatedAt, + completedAt = completedAt, + itemType = itemType.name, + calendarEventUid = calendarEventUid, + icalUrl = icalUrl, + etag = etag, + responsibleUid = responsibleUid, + ) +} diff --git a/app/src/main/java/com/planify/mobile/ui/inbox/InboxScreen.kt b/app/src/main/java/com/planify/mobile/ui/inbox/InboxScreen.kt new file mode 100644 index 0000000..15231e1 --- /dev/null +++ b/app/src/main/java/com/planify/mobile/ui/inbox/InboxScreen.kt @@ -0,0 +1,75 @@ +package com.planify.mobile.ui.inbox + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Inbox +import androidx.compose.material3.CircularProgressIndicator +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.ui.Alignment +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 + +@Composable +fun InboxScreen( + onTaskClick: (Task) -> Unit, + modifier: Modifier = Modifier, + viewModel: InboxViewModel = hiltViewModel(), +) { + val state by viewModel.uiState.collectAsState() + + when { + state.isLoading -> Box(modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + CircularProgressIndicator() + } + + state.tasks.isEmpty() && state.overdueTasks.isEmpty() -> + EmptyState( + icon = Icons.Outlined.Inbox, + title = "Inbox vide", + subtitle = "Créez une tâche avec le bouton +", + modifier = modifier, + ) + + else -> LazyColumn(modifier = modifier.fillMaxSize()) { + if (state.overdueTasks.isNotEmpty()) { + item { + Text( + text = "En retard", + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.error, + modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp), + ) + } + items(state.overdueTasks, key = { it.id }) { task -> + TaskRow( + task = task, + onCheckedChange = { viewModel.toggleTask(task) }, + onClick = { onTaskClick(task) }, + ) + } + item { HorizontalDivider(Modifier.padding(horizontal = 16.dp)) } + } + + items(state.tasks, key = { it.id }) { task -> + TaskRow( + task = task, + onCheckedChange = { viewModel.toggleTask(task) }, + onClick = { onTaskClick(task) }, + ) + } + } + } +} diff --git a/app/src/main/java/com/planify/mobile/ui/inbox/InboxViewModel.kt b/app/src/main/java/com/planify/mobile/ui/inbox/InboxViewModel.kt new file mode 100644 index 0000000..f9a1487 --- /dev/null +++ b/app/src/main/java/com/planify/mobile/ui/inbox/InboxViewModel.kt @@ -0,0 +1,48 @@ +package com.planify.mobile.ui.inbox + +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.flow.SharingStarted +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch +import javax.inject.Inject + +data class InboxUiState( + val tasks: List = emptyList(), + val overdueTasks: List = emptyList(), + val showCompleted: Boolean = false, + val isLoading: Boolean = false, +) + +@HiltViewModel +class InboxViewModel @Inject constructor( + private val taskRepository: TaskRepository, +) : ViewModel() { + + val uiState = combine( + taskRepository.getInboxTasks(), + taskRepository.getOverdueTasks(), + ) { tasks, overdue -> + InboxUiState(tasks = tasks, overdueTasks = overdue) + }.stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(5_000), + initialValue = InboxUiState(isLoading = true), + ) + + fun toggleTask(task: Task) { + viewModelScope.launch { + taskRepository.checkTask(task.id, !task.checked) + } + } + + fun deleteTask(task: Task) { + viewModelScope.launch { + taskRepository.deleteTask(task.id) + } + } +}