mirror of
https://github.com/wismna/DoNext.git
synced 2025-10-03 23:50:13 -04:00
Display the due date in the bottom sheet
This commit is contained in:
@@ -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
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@@ -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
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@@ -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) {
|
||||||
|
@@ -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
|
||||||
|
Reference in New Issue
Block a user