diff --git a/donextv2/src/main/java/com/wismna/geoffroy/donext/domain/usecase/AddTaskUseCase.kt b/donextv2/src/main/java/com/wismna/geoffroy/donext/domain/usecase/AddTaskUseCase.kt index e791588..d293da3 100644 --- a/donextv2/src/main/java/com/wismna/geoffroy/donext/domain/usecase/AddTaskUseCase.kt +++ b/donextv2/src/main/java/com/wismna/geoffroy/donext/domain/usecase/AddTaskUseCase.kt @@ -3,12 +3,13 @@ package com.wismna.geoffroy.donext.domain.usecase import com.wismna.geoffroy.donext.domain.model.Priority import com.wismna.geoffroy.donext.domain.model.Task import com.wismna.geoffroy.donext.domain.repository.TaskRepository +import java.time.Instant import javax.inject.Inject class AddTaskUseCase @Inject constructor( private val repository: TaskRepository ) { - suspend operator fun invoke(taskListId: Long, title: String, description: String?, priority: Priority) { + suspend operator fun invoke(taskListId: Long, title: String, description: String?, priority: Priority, dueDate: Instant?) { repository.insertTask( Task( taskListId = taskListId, @@ -17,6 +18,7 @@ class AddTaskUseCase @Inject constructor( isDeleted = false, isDone = false, priority = priority, + dueDate = dueDate ) ) } diff --git a/donextv2/src/main/java/com/wismna/geoffroy/donext/domain/usecase/UpdateTaskUseCase.kt b/donextv2/src/main/java/com/wismna/geoffroy/donext/domain/usecase/UpdateTaskUseCase.kt index c4473be..d5949cc 100644 --- a/donextv2/src/main/java/com/wismna/geoffroy/donext/domain/usecase/UpdateTaskUseCase.kt +++ b/donextv2/src/main/java/com/wismna/geoffroy/donext/domain/usecase/UpdateTaskUseCase.kt @@ -3,12 +3,13 @@ package com.wismna.geoffroy.donext.domain.usecase import com.wismna.geoffroy.donext.domain.model.Priority import com.wismna.geoffroy.donext.domain.model.Task import com.wismna.geoffroy.donext.domain.repository.TaskRepository +import java.time.Instant import javax.inject.Inject class UpdateTaskUseCase @Inject constructor( private val repository: TaskRepository ) { - suspend operator fun invoke(taskId: Long, taskListId: Long, title: String, description: String?, priority: Priority) { + suspend operator fun invoke(taskId: Long, taskListId: Long, title: String, description: String?, priority: Priority, dueDate: Instant?) { repository.updateTask( Task( id = taskId, @@ -18,6 +19,7 @@ class UpdateTaskUseCase @Inject constructor( isDeleted = false, isDone = false, priority = priority, + dueDate = dueDate ) ) } diff --git a/donextv2/src/main/java/com/wismna/geoffroy/donext/presentation/screen/TaskScreen.kt b/donextv2/src/main/java/com/wismna/geoffroy/donext/presentation/screen/TaskScreen.kt index 61d41fd..8b336f0 100644 --- a/donextv2/src/main/java/com/wismna/geoffroy/donext/presentation/screen/TaskScreen.kt +++ b/donextv2/src/main/java/com/wismna/geoffroy/donext/presentation/screen/TaskScreen.kt @@ -7,21 +7,37 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Clear +import androidx.compose.material.icons.filled.DateRange import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.DatePicker +import androidx.compose.material3.DatePickerDialog import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.material3.rememberDatePickerState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.derivedStateOf +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.focus.FocusRequester import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.unit.dp import com.wismna.geoffroy.donext.presentation.viewmodel.TaskViewModel +import java.time.ZoneId +import java.time.format.DateTimeFormatter @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -30,6 +46,7 @@ fun TaskBottomSheet( onDismiss: () -> Unit ) { val titleFocusRequester = remember { FocusRequester() } + var showDatePicker by remember { mutableStateOf(false) } LaunchedEffect(Unit) { titleFocusRequester.requestFocus() @@ -54,7 +71,7 @@ fun TaskBottomSheet( .focusRequester(titleFocusRequester), isError = viewModel.title.isEmpty(), ) - Spacer(Modifier.height(8.dp)) + Spacer(Modifier.height(12.dp)) // --- Description --- OutlinedTextField( @@ -64,15 +81,67 @@ fun TaskBottomSheet( maxLines = 3, modifier = Modifier.fillMaxWidth() ) - Spacer(Modifier.height(8.dp)) + Spacer(Modifier.height(12.dp)) // --- Priority --- - Text("Priority", style = MaterialTheme.typography.labelLarge) - Spacer(Modifier.height(4.dp)) - SingleChoiceSegmentedButton( - value = viewModel.priority, - onValueChange = { viewModel.onPriorityChanged(it) } + Row ( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically) { + Text("Priority", style = MaterialTheme.typography.labelLarge) + SingleChoiceSegmentedButton( + value = viewModel.priority, + onValueChange = { viewModel.onPriorityChanged(it) } + ) + } + Spacer(Modifier.height(12.dp)) + + // --- Due Date --- + var showDatePicker by remember { mutableStateOf(false) } + + OutlinedTextField( + value = viewModel.dueDate?.atZone(ZoneId.systemDefault()) + ?.toLocalDate() + ?.format(DateTimeFormatter.ofPattern("MMM d, yyyy")) ?: "", + onValueChange = {}, + readOnly = true, + label = { Text("Due Date") }, + trailingIcon = { + Row { + if (viewModel.dueDate != null) { + IconButton(onClick = { viewModel.onDueDateChanged(null) }) { + Icon(Icons.Default.Clear, contentDescription = "Clear due date") + } + } + IconButton(onClick = { showDatePicker = true }) { + Icon(Icons.Default.DateRange, contentDescription = "Pick due date") + } + } + }, + modifier = Modifier.fillMaxWidth() ) + + if (showDatePicker) { + val datePickerState = rememberDatePickerState( + initialSelectedDateMillis = viewModel.dueDate?.toEpochMilli() + ) + + DatePickerDialog( + onDismissRequest = { showDatePicker = false }, + confirmButton = { + TextButton(onClick = { + datePickerState.selectedDateMillis?.let { viewModel.onDueDateChanged(it) } + showDatePicker = false + }) { Text("OK") } + }, + dismissButton = { + TextButton(onClick = { showDatePicker = false }) { Text("Cancel") } + } + ) { + DatePicker(state = datePickerState) + } + } + Spacer(Modifier.height(16.dp)) Row (modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { diff --git a/donextv2/src/main/java/com/wismna/geoffroy/donext/presentation/viewmodel/TaskViewModel.kt b/donextv2/src/main/java/com/wismna/geoffroy/donext/presentation/viewmodel/TaskViewModel.kt index 230a976..70c847a 100644 --- a/donextv2/src/main/java/com/wismna/geoffroy/donext/presentation/viewmodel/TaskViewModel.kt +++ b/donextv2/src/main/java/com/wismna/geoffroy/donext/presentation/viewmodel/TaskViewModel.kt @@ -1,5 +1,6 @@ package com.wismna.geoffroy.donext.presentation.viewmodel +import android.util.Log import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue @@ -12,6 +13,7 @@ import com.wismna.geoffroy.donext.domain.usecase.DeleteTaskUseCase import com.wismna.geoffroy.donext.domain.usecase.UpdateTaskUseCase import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch +import java.time.Instant import javax.inject.Inject @HiltViewModel @@ -27,6 +29,8 @@ class TaskViewModel @Inject constructor( private set var priority by mutableStateOf(Priority.NORMAL) private set + var dueDate by mutableStateOf(null) + private set private var editingTaskId: Long? = null private var taskListId: Long? = null @@ -39,6 +43,7 @@ class TaskViewModel @Inject constructor( title = "" description = "" priority = Priority.NORMAL + dueDate = null } fun startEditTask(task: Task) { @@ -47,20 +52,25 @@ class TaskViewModel @Inject constructor( title = task.name description = task.description ?: "" priority = task.priority + dueDate = task.dueDate } fun onTitleChanged(value: String) { title = value } fun onDescriptionChanged(value: String) { description = value } fun onPriorityChanged(value: Priority) { priority = value } + fun onDueDateChanged(value: Long?) { + dueDate = value?.let { Instant.ofEpochMilli(it) } + Log.d("TaskViewModel", "onDueDateChanged -> $dueDate (millis=$value)") + } - fun save(onDone: (() -> Unit)? = null) { + fun save(onDone: (() -> Unit)? = null) { if (title.isBlank()) return viewModelScope.launch { if (isEditing()) { - updateTaskUseCase(editingTaskId!!, taskListId!!, title, description, priority) + updateTaskUseCase(editingTaskId!!, taskListId!!, title, description, priority, dueDate) } else { - createTaskUseCase(taskListId!!, title, description, priority) + createTaskUseCase(taskListId!!, title, description, priority, dueDate) } // reset state after save reset() @@ -77,7 +87,6 @@ class TaskViewModel @Inject constructor( } } - /** Optional: manual reset */ fun reset() { editingTaskId = null taskListId = null