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