feat: [#13] priorités, labels, sous-tâches (PriorityPicker, LabelPicker, SubTaskSection, LabelRepository)
This commit is contained in:
@@ -2,6 +2,7 @@ package com.planify.mobile.data.local
|
|||||||
|
|
||||||
import androidx.room.Database
|
import androidx.room.Database
|
||||||
import androidx.room.RoomDatabase
|
import androidx.room.RoomDatabase
|
||||||
|
import com.planify.mobile.data.local.dao.LabelDao
|
||||||
import com.planify.mobile.data.local.dao.ProjectDao
|
import com.planify.mobile.data.local.dao.ProjectDao
|
||||||
import com.planify.mobile.data.local.dao.SectionDao
|
import com.planify.mobile.data.local.dao.SectionDao
|
||||||
import com.planify.mobile.data.local.dao.TaskDao
|
import com.planify.mobile.data.local.dao.TaskDao
|
||||||
@@ -28,4 +29,5 @@ abstract class AppDatabase : RoomDatabase() {
|
|||||||
abstract fun taskDao(): TaskDao
|
abstract fun taskDao(): TaskDao
|
||||||
abstract fun projectDao(): ProjectDao
|
abstract fun projectDao(): ProjectDao
|
||||||
abstract fun sectionDao(): SectionDao
|
abstract fun sectionDao(): SectionDao
|
||||||
|
abstract fun labelDao(): LabelDao
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package com.planify.mobile.data.local.dao
|
||||||
|
|
||||||
|
import androidx.room.Dao
|
||||||
|
import androidx.room.Insert
|
||||||
|
import androidx.room.OnConflictStrategy
|
||||||
|
import androidx.room.Query
|
||||||
|
import androidx.room.Update
|
||||||
|
import com.planify.mobile.data.local.entity.LabelEntity
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
|
@Dao
|
||||||
|
interface LabelDao {
|
||||||
|
@Query("SELECT * FROM labels WHERE is_deleted = 0 ORDER BY `order` ASC")
|
||||||
|
fun getAllLabels(): Flow<List<LabelEntity>>
|
||||||
|
|
||||||
|
@Query("SELECT * FROM labels WHERE id = :id")
|
||||||
|
suspend fun getById(id: String): LabelEntity?
|
||||||
|
|
||||||
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
|
suspend fun insert(label: LabelEntity)
|
||||||
|
|
||||||
|
@Update
|
||||||
|
suspend fun update(label: LabelEntity)
|
||||||
|
|
||||||
|
@Query("UPDATE labels SET is_deleted = 1 WHERE id = :id")
|
||||||
|
suspend fun softDelete(id: String)
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
package com.planify.mobile.data.repository
|
||||||
|
|
||||||
|
import com.planify.mobile.data.local.dao.LabelDao
|
||||||
|
import com.planify.mobile.data.local.entity.LabelEntity
|
||||||
|
import com.planify.mobile.domain.model.BackendType
|
||||||
|
import com.planify.mobile.domain.model.Label
|
||||||
|
import com.planify.mobile.domain.repository.LabelRepository
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class LabelRepositoryImpl @Inject constructor(
|
||||||
|
private val dao: LabelDao,
|
||||||
|
) : LabelRepository {
|
||||||
|
|
||||||
|
override fun getAllLabels(): Flow<List<Label>> =
|
||||||
|
dao.getAllLabels().map { it.map { e -> e.toDomain() } }
|
||||||
|
|
||||||
|
override suspend fun getLabelById(id: String): Label? = dao.getById(id)?.toDomain()
|
||||||
|
override suspend fun insertLabel(label: Label) = dao.insert(label.toEntity())
|
||||||
|
override suspend fun updateLabel(label: Label) = dao.update(label.toEntity())
|
||||||
|
override suspend fun deleteLabel(id: String) = dao.softDelete(id)
|
||||||
|
|
||||||
|
private fun LabelEntity.toDomain() = Label(
|
||||||
|
id = id, name = name, color = color, order = order, sourceId = sourceId,
|
||||||
|
backendType = runCatching { BackendType.valueOf(backendType) }.getOrDefault(BackendType.LOCAL),
|
||||||
|
isFavorite = isFavorite, isDeleted = isDeleted,
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun Label.toEntity() = LabelEntity(
|
||||||
|
id = id, name = name, color = color, order = order, sourceId = sourceId,
|
||||||
|
backendType = backendType.name, isFavorite = isFavorite, isDeleted = isDeleted,
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -24,4 +24,5 @@ object DatabaseModule {
|
|||||||
@Provides fun provideTaskDao(db: AppDatabase) = db.taskDao()
|
@Provides fun provideTaskDao(db: AppDatabase) = db.taskDao()
|
||||||
@Provides fun provideProjectDao(db: AppDatabase) = db.projectDao()
|
@Provides fun provideProjectDao(db: AppDatabase) = db.projectDao()
|
||||||
@Provides fun provideSectionDao(db: AppDatabase) = db.sectionDao()
|
@Provides fun provideSectionDao(db: AppDatabase) = db.sectionDao()
|
||||||
|
@Provides fun provideLabelDao(db: AppDatabase) = db.labelDao()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
package com.planify.mobile.di
|
package com.planify.mobile.di
|
||||||
|
|
||||||
|
import com.planify.mobile.data.repository.LabelRepositoryImpl
|
||||||
import com.planify.mobile.data.repository.ProjectRepositoryImpl
|
import com.planify.mobile.data.repository.ProjectRepositoryImpl
|
||||||
import com.planify.mobile.data.repository.SectionRepositoryImpl
|
import com.planify.mobile.data.repository.SectionRepositoryImpl
|
||||||
import com.planify.mobile.data.repository.TaskRepositoryImpl
|
import com.planify.mobile.data.repository.TaskRepositoryImpl
|
||||||
|
import com.planify.mobile.domain.repository.LabelRepository
|
||||||
import com.planify.mobile.domain.repository.ProjectRepository
|
import com.planify.mobile.domain.repository.ProjectRepository
|
||||||
import com.planify.mobile.domain.repository.SectionRepository
|
import com.planify.mobile.domain.repository.SectionRepository
|
||||||
import com.planify.mobile.domain.repository.TaskRepository
|
import com.planify.mobile.domain.repository.TaskRepository
|
||||||
@@ -24,4 +26,7 @@ abstract class RepositoryModule {
|
|||||||
|
|
||||||
@Binds @Singleton
|
@Binds @Singleton
|
||||||
abstract fun bindSectionRepository(impl: SectionRepositoryImpl): SectionRepository
|
abstract fun bindSectionRepository(impl: SectionRepositoryImpl): SectionRepository
|
||||||
|
|
||||||
|
@Binds @Singleton
|
||||||
|
abstract fun bindLabelRepository(impl: LabelRepositoryImpl): LabelRepository
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package com.planify.mobile.domain.repository
|
||||||
|
|
||||||
|
import com.planify.mobile.domain.model.Label
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
|
interface LabelRepository {
|
||||||
|
fun getAllLabels(): Flow<List<Label>>
|
||||||
|
suspend fun getLabelById(id: String): Label?
|
||||||
|
suspend fun insertLabel(label: Label)
|
||||||
|
suspend fun updateLabel(label: Label)
|
||||||
|
suspend fun deleteLabel(id: String)
|
||||||
|
}
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
package com.planify.mobile.ui.task
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.outlined.Label
|
||||||
|
import androidx.compose.material3.Checkbox
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.ModalBottomSheet
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
|
import androidx.compose.material3.rememberModalBottomSheetState
|
||||||
|
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.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.planify.mobile.domain.model.Label
|
||||||
|
import com.planify.mobile.domain.repository.LabelRepository
|
||||||
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
|
import kotlinx.coroutines.flow.stateIn
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@HiltViewModel
|
||||||
|
class LabelPickerViewModel @Inject constructor(
|
||||||
|
labelRepository: LabelRepository,
|
||||||
|
) : ViewModel() {
|
||||||
|
val labels = labelRepository.getAllLabels()
|
||||||
|
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), emptyList())
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun LabelPickerSheet(
|
||||||
|
selectedLabels: List<String>,
|
||||||
|
onConfirm: (List<String>) -> Unit,
|
||||||
|
onDismiss: () -> Unit,
|
||||||
|
viewModel: LabelPickerViewModel = hiltViewModel(),
|
||||||
|
) {
|
||||||
|
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
|
||||||
|
val labels by viewModel.labels.collectAsState()
|
||||||
|
var selected by remember { mutableStateOf(selectedLabels.toSet()) }
|
||||||
|
|
||||||
|
ModalBottomSheet(onDismissRequest = onDismiss, sheetState = sheetState) {
|
||||||
|
Column {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "Labels",
|
||||||
|
style = androidx.compose.material3.MaterialTheme.typography.titleMedium,
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
)
|
||||||
|
TextButton(onClick = { onConfirm(selected.toList()) }) { Text("OK") }
|
||||||
|
}
|
||||||
|
LazyColumn {
|
||||||
|
items(labels, key = { it.id }) { label ->
|
||||||
|
LabelRow(
|
||||||
|
label = label,
|
||||||
|
checked = label.name in selected,
|
||||||
|
onToggle = {
|
||||||
|
selected = if (label.name in selected) selected - label.name else selected + label.name
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (labels.isEmpty()) {
|
||||||
|
item {
|
||||||
|
Text(
|
||||||
|
"Aucun label. Créez-en un dans les paramètres.",
|
||||||
|
modifier = Modifier.padding(16.dp),
|
||||||
|
color = androidx.compose.material3.MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun LabelRow(label: Label, checked: Boolean, onToggle: () -> Unit) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
Icons.Outlined.Label,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = runCatching { Color(android.graphics.Color.parseColor(label.color)) }.getOrDefault(Color.Gray),
|
||||||
|
)
|
||||||
|
Spacer(Modifier.width(12.dp))
|
||||||
|
Text(text = label.name, modifier = Modifier.weight(1f))
|
||||||
|
Checkbox(checked = checked, onCheckedChange = { onToggle() })
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
package com.planify.mobile.ui.task
|
||||||
|
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.ModalBottomSheet
|
||||||
|
import androidx.compose.material3.RadioButton
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.rememberModalBottomSheetState
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.planify.mobile.ui.components.PriorityBadge
|
||||||
|
import com.planify.mobile.ui.components.priorityColor
|
||||||
|
|
||||||
|
private val priorityLabels = mapOf(
|
||||||
|
1 to "Urgente",
|
||||||
|
2 to "Haute",
|
||||||
|
3 to "Moyenne",
|
||||||
|
4 to "Aucune",
|
||||||
|
)
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun PriorityPickerSheet(
|
||||||
|
current: Int,
|
||||||
|
onSelect: (Int) -> Unit,
|
||||||
|
onDismiss: () -> Unit,
|
||||||
|
) {
|
||||||
|
val sheetState = rememberModalBottomSheetState()
|
||||||
|
|
||||||
|
ModalBottomSheet(onDismissRequest = onDismiss, sheetState = sheetState) {
|
||||||
|
Column(Modifier.padding(bottom = 16.dp)) {
|
||||||
|
Text(
|
||||||
|
text = "Priorité",
|
||||||
|
style = androidx.compose.material3.MaterialTheme.typography.titleMedium,
|
||||||
|
modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||||
|
)
|
||||||
|
listOf(1, 2, 3, 4).forEach { priority ->
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clickable { onSelect(priority); onDismiss() }
|
||||||
|
.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
PriorityBadge(priority = priority)
|
||||||
|
Spacer(Modifier.width(12.dp))
|
||||||
|
Text(
|
||||||
|
text = priorityLabels[priority] ?: "",
|
||||||
|
color = priorityColor[priority] ?: androidx.compose.ui.graphics.Color.Gray,
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
)
|
||||||
|
RadioButton(selected = current == priority, onClick = null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
package com.planify.mobile.ui.task
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.text.KeyboardActions
|
||||||
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.outlined.Add
|
||||||
|
import androidx.compose.material.icons.outlined.Close
|
||||||
|
import androidx.compose.material3.Checkbox
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.OutlinedTextField
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.text.input.ImeAction
|
||||||
|
import androidx.compose.ui.text.style.TextDecoration
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.planify.mobile.domain.model.Task
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SubTaskSection(
|
||||||
|
subTasks: List<Task>,
|
||||||
|
onAddSubTask: (String) -> Unit,
|
||||||
|
onToggleSubTask: (Task) -> Unit,
|
||||||
|
onDeleteSubTask: (Task) -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
var newSubTaskContent by remember { mutableStateOf("") }
|
||||||
|
var showInput by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
Column(modifier = modifier) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.padding(horizontal = 16.dp, vertical = 4.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "Sous-tâches (${subTasks.count { it.checked }}/${subTasks.size})",
|
||||||
|
style = MaterialTheme.typography.labelMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
subTasks.forEach { subTask ->
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(start = 8.dp, end = 8.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
Checkbox(
|
||||||
|
checked = subTask.checked,
|
||||||
|
onCheckedChange = { onToggleSubTask(subTask) },
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = subTask.content,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
textDecoration = if (subTask.checked) TextDecoration.LineThrough else null,
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
)
|
||||||
|
IconButton(onClick = { onDeleteSubTask(subTask) }) {
|
||||||
|
Icon(Icons.Outlined.Close, contentDescription = "Supprimer")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showInput) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.padding(horizontal = 8.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
Spacer(Modifier.width(40.dp))
|
||||||
|
OutlinedTextField(
|
||||||
|
value = newSubTaskContent,
|
||||||
|
onValueChange = { newSubTaskContent = it },
|
||||||
|
placeholder = { Text("Nouvelle sous-tâche") },
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
singleLine = true,
|
||||||
|
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
|
||||||
|
keyboardActions = KeyboardActions(onDone = {
|
||||||
|
if (newSubTaskContent.isNotBlank()) {
|
||||||
|
onAddSubTask(newSubTaskContent.trim())
|
||||||
|
newSubTaskContent = ""
|
||||||
|
}
|
||||||
|
showInput = false
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TextButton(
|
||||||
|
onClick = { showInput = true },
|
||||||
|
modifier = Modifier.padding(start = 8.dp),
|
||||||
|
) {
|
||||||
|
Icon(Icons.Outlined.Add, contentDescription = null)
|
||||||
|
Spacer(Modifier.width(4.dp))
|
||||||
|
Text("Ajouter une sous-tâche")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user