Display the due date in the bottom sheet

This commit is contained in:
Geoffroy Bonneville
2025-09-12 16:09:29 -04:00
parent 7939257cd6
commit cc25aa4b05
4 changed files with 95 additions and 13 deletions

View File

@@ -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
)
)
}

View File

@@ -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
)
)
}

View File

@@ -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) {

View File

@@ -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<Instant?>(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