feat: [#7] vue Inbox (InboxScreen, InboxViewModel, TaskRepositoryImpl)

This commit is contained in:
2026-06-06 06:04:38 +02:00
parent 38b96c0c72
commit ce22d49824
3 changed files with 231 additions and 0 deletions
@@ -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<List<Task>> =
dao.getTasksByProject(projectId).map { it.map { e -> e.toDomain() } }
override fun getTasksBySection(sectionId: String): Flow<List<Task>> =
dao.getTasksBySection(sectionId).map { it.map { e -> e.toDomain() } }
override fun getInboxTasks(): Flow<List<Task>> =
dao.getInboxTasks().map { it.map { e -> e.toDomain() } }
override fun getTodayTasks(): Flow<List<Task>> =
dao.getTodayTasks().map { it.map { e -> e.toDomain() } }
override fun getOverdueTasks(): Flow<List<Task>> =
dao.getOverdueTasks().map { it.map { e -> e.toDomain() } }
override fun getSubTasks(parentId: String): Flow<List<Task>> =
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<DueDate>(it) }.getOrNull() },
deadlineDate = deadlineDate,
labels = runCatching { Json.decodeFromString<List<String>>(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,
)
}
@@ -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) },
)
}
}
}
}
@@ -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<Task> = emptyList(),
val overdueTasks: List<Task> = 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)
}
}
}