feat: [#7] vue Inbox (InboxScreen, InboxViewModel, TaskRepositoryImpl)
This commit is contained in:
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user