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.Priority
import com.wismna.geoffroy.donext.domain.model.Task import com.wismna.geoffroy.donext.domain.model.Task
import com.wismna.geoffroy.donext.domain.repository.TaskRepository import com.wismna.geoffroy.donext.domain.repository.TaskRepository
import java.time.Instant
import javax.inject.Inject import javax.inject.Inject
class AddTaskUseCase @Inject constructor( class AddTaskUseCase @Inject constructor(
private val repository: TaskRepository 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( repository.insertTask(
Task( Task(
taskListId = taskListId, taskListId = taskListId,
@@ -17,6 +18,7 @@ class AddTaskUseCase @Inject constructor(
isDeleted = false, isDeleted = false,
isDone = false, isDone = false,
priority = priority, 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.Priority
import com.wismna.geoffroy.donext.domain.model.Task import com.wismna.geoffroy.donext.domain.model.Task
import com.wismna.geoffroy.donext.domain.repository.TaskRepository import com.wismna.geoffroy.donext.domain.repository.TaskRepository
import java.time.Instant
import javax.inject.Inject import javax.inject.Inject
class UpdateTaskUseCase @Inject constructor( class UpdateTaskUseCase @Inject constructor(
private val repository: TaskRepository 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( repository.updateTask(
Task( Task(
id = taskId, id = taskId,
@@ -18,6 +19,7 @@ class UpdateTaskUseCase @Inject constructor(
isDeleted = false, isDeleted = false,
isDone = false, isDone = false,
priority = priority, 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.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding 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.Button
import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.DatePicker
import androidx.compose.material3.DatePickerDialog
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.rememberDatePickerState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect 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.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.wismna.geoffroy.donext.presentation.viewmodel.TaskViewModel import com.wismna.geoffroy.donext.presentation.viewmodel.TaskViewModel
import java.time.ZoneId
import java.time.format.DateTimeFormatter
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
@@ -30,6 +46,7 @@ fun TaskBottomSheet(
onDismiss: () -> Unit onDismiss: () -> Unit
) { ) {
val titleFocusRequester = remember { FocusRequester() } val titleFocusRequester = remember { FocusRequester() }
var showDatePicker by remember { mutableStateOf(false) }
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
titleFocusRequester.requestFocus() titleFocusRequester.requestFocus()
@@ -54,7 +71,7 @@ fun TaskBottomSheet(
.focusRequester(titleFocusRequester), .focusRequester(titleFocusRequester),
isError = viewModel.title.isEmpty(), isError = viewModel.title.isEmpty(),
) )
Spacer(Modifier.height(8.dp)) Spacer(Modifier.height(12.dp))
// --- Description --- // --- Description ---
OutlinedTextField( OutlinedTextField(
@@ -64,15 +81,67 @@ fun TaskBottomSheet(
maxLines = 3, maxLines = 3,
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
) )
Spacer(Modifier.height(8.dp)) Spacer(Modifier.height(12.dp))
// --- Priority --- // --- Priority ---
Row (
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically) {
Text("Priority", style = MaterialTheme.typography.labelLarge) Text("Priority", style = MaterialTheme.typography.labelLarge)
Spacer(Modifier.height(4.dp))
SingleChoiceSegmentedButton( SingleChoiceSegmentedButton(
value = viewModel.priority, value = viewModel.priority,
onValueChange = { viewModel.onPriorityChanged(it) } 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)) Spacer(Modifier.height(16.dp))
Row (modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { Row (modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {

View File

@@ -1,5 +1,6 @@
package com.wismna.geoffroy.donext.presentation.viewmodel package com.wismna.geoffroy.donext.presentation.viewmodel
import android.util.Log
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue 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 com.wismna.geoffroy.donext.domain.usecase.UpdateTaskUseCase
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.time.Instant
import javax.inject.Inject import javax.inject.Inject
@HiltViewModel @HiltViewModel
@@ -27,6 +29,8 @@ class TaskViewModel @Inject constructor(
private set private set
var priority by mutableStateOf(Priority.NORMAL) var priority by mutableStateOf(Priority.NORMAL)
private set private set
var dueDate by mutableStateOf<Instant?>(null)
private set
private var editingTaskId: Long? = null private var editingTaskId: Long? = null
private var taskListId: Long? = null private var taskListId: Long? = null
@@ -39,6 +43,7 @@ class TaskViewModel @Inject constructor(
title = "" title = ""
description = "" description = ""
priority = Priority.NORMAL priority = Priority.NORMAL
dueDate = null
} }
fun startEditTask(task: Task) { fun startEditTask(task: Task) {
@@ -47,20 +52,25 @@ class TaskViewModel @Inject constructor(
title = task.name title = task.name
description = task.description ?: "" description = task.description ?: ""
priority = task.priority priority = task.priority
dueDate = task.dueDate
} }
fun onTitleChanged(value: String) { title = value } fun onTitleChanged(value: String) { title = value }
fun onDescriptionChanged(value: String) { description = value } fun onDescriptionChanged(value: String) { description = value }
fun onPriorityChanged(value: Priority) { priority = 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 if (title.isBlank()) return
viewModelScope.launch { viewModelScope.launch {
if (isEditing()) { if (isEditing()) {
updateTaskUseCase(editingTaskId!!, taskListId!!, title, description, priority) updateTaskUseCase(editingTaskId!!, taskListId!!, title, description, priority, dueDate)
} else { } else {
createTaskUseCase(taskListId!!, title, description, priority) createTaskUseCase(taskListId!!, title, description, priority, dueDate)
} }
// reset state after save // reset state after save
reset() reset()
@@ -77,7 +87,6 @@ class TaskViewModel @Inject constructor(
} }
} }
/** Optional: manual reset */
fun reset() { fun reset() {
editingTaskId = null editingTaskId = null
taskListId = null taskListId = null