diff --git a/app/src/main/java/com/planify/mobile/data/local/AppDatabase.kt b/app/src/main/java/com/planify/mobile/data/local/AppDatabase.kt index 0c86eaf..3312c5f 100644 --- a/app/src/main/java/com/planify/mobile/data/local/AppDatabase.kt +++ b/app/src/main/java/com/planify/mobile/data/local/AppDatabase.kt @@ -2,6 +2,7 @@ package com.planify.mobile.data.local import androidx.room.Database 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.SectionDao import com.planify.mobile.data.local.dao.TaskDao @@ -28,4 +29,5 @@ abstract class AppDatabase : RoomDatabase() { abstract fun taskDao(): TaskDao abstract fun projectDao(): ProjectDao abstract fun sectionDao(): SectionDao + abstract fun labelDao(): LabelDao } diff --git a/app/src/main/java/com/planify/mobile/data/local/dao/LabelDao.kt b/app/src/main/java/com/planify/mobile/data/local/dao/LabelDao.kt new file mode 100644 index 0000000..e33d513 --- /dev/null +++ b/app/src/main/java/com/planify/mobile/data/local/dao/LabelDao.kt @@ -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> + + @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) +} diff --git a/app/src/main/java/com/planify/mobile/data/repository/LabelRepositoryImpl.kt b/app/src/main/java/com/planify/mobile/data/repository/LabelRepositoryImpl.kt new file mode 100644 index 0000000..0cc7d01 --- /dev/null +++ b/app/src/main/java/com/planify/mobile/data/repository/LabelRepositoryImpl.kt @@ -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> = + 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, + ) +} diff --git a/app/src/main/java/com/planify/mobile/di/DatabaseModule.kt b/app/src/main/java/com/planify/mobile/di/DatabaseModule.kt index e8d5238..eb79e57 100644 --- a/app/src/main/java/com/planify/mobile/di/DatabaseModule.kt +++ b/app/src/main/java/com/planify/mobile/di/DatabaseModule.kt @@ -24,4 +24,5 @@ object DatabaseModule { @Provides fun provideTaskDao(db: AppDatabase) = db.taskDao() @Provides fun provideProjectDao(db: AppDatabase) = db.projectDao() @Provides fun provideSectionDao(db: AppDatabase) = db.sectionDao() + @Provides fun provideLabelDao(db: AppDatabase) = db.labelDao() } diff --git a/app/src/main/java/com/planify/mobile/di/RepositoryModule.kt b/app/src/main/java/com/planify/mobile/di/RepositoryModule.kt index 0eb6edd..5e84478 100644 --- a/app/src/main/java/com/planify/mobile/di/RepositoryModule.kt +++ b/app/src/main/java/com/planify/mobile/di/RepositoryModule.kt @@ -1,8 +1,10 @@ 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.SectionRepositoryImpl 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.SectionRepository import com.planify.mobile.domain.repository.TaskRepository @@ -24,4 +26,7 @@ abstract class RepositoryModule { @Binds @Singleton abstract fun bindSectionRepository(impl: SectionRepositoryImpl): SectionRepository + + @Binds @Singleton + abstract fun bindLabelRepository(impl: LabelRepositoryImpl): LabelRepository } diff --git a/app/src/main/java/com/planify/mobile/domain/repository/LabelRepository.kt b/app/src/main/java/com/planify/mobile/domain/repository/LabelRepository.kt new file mode 100644 index 0000000..f353d16 --- /dev/null +++ b/app/src/main/java/com/planify/mobile/domain/repository/LabelRepository.kt @@ -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> + suspend fun getLabelById(id: String): Label? + suspend fun insertLabel(label: Label) + suspend fun updateLabel(label: Label) + suspend fun deleteLabel(id: String) +} diff --git a/app/src/main/java/com/planify/mobile/ui/task/LabelPickerSheet.kt b/app/src/main/java/com/planify/mobile/ui/task/LabelPickerSheet.kt new file mode 100644 index 0000000..c667638 --- /dev/null +++ b/app/src/main/java/com/planify/mobile/ui/task/LabelPickerSheet.kt @@ -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, + onConfirm: (List) -> 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() }) + } +} diff --git a/app/src/main/java/com/planify/mobile/ui/task/PriorityPickerSheet.kt b/app/src/main/java/com/planify/mobile/ui/task/PriorityPickerSheet.kt new file mode 100644 index 0000000..f6baf73 --- /dev/null +++ b/app/src/main/java/com/planify/mobile/ui/task/PriorityPickerSheet.kt @@ -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) + } + } + } + } +} diff --git a/app/src/main/java/com/planify/mobile/ui/task/SubTaskSection.kt b/app/src/main/java/com/planify/mobile/ui/task/SubTaskSection.kt new file mode 100644 index 0000000..d56101d --- /dev/null +++ b/app/src/main/java/com/planify/mobile/ui/task/SubTaskSection.kt @@ -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, + 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") + } + } +}