Simplify due date data type

Due date displays proper date
Overdue tasks display as red
This commit is contained in:
Geoffroy Bonneville
2025-09-12 22:32:39 -04:00
parent cc25aa4b05
commit e250ac91d0
9 changed files with 50 additions and 43 deletions

View File

@@ -2,19 +2,8 @@ package com.wismna.geoffroy.donext.data
import androidx.room.TypeConverter import androidx.room.TypeConverter
import com.wismna.geoffroy.donext.domain.model.Priority import com.wismna.geoffroy.donext.domain.model.Priority
import java.time.Instant
class Converters { class Converters {
@TypeConverter
fun fromTimestamp(value: Long?): Instant? {
return value?.let { Instant.ofEpochMilli(it) }
}
@TypeConverter
fun instantToTimestamp(instant: Instant?): Long? {
return instant?.toEpochMilli()
}
@TypeConverter @TypeConverter
fun fromPriority(priority: Priority): Int = priority.value fun fromPriority(priority: Priority): Int = priority.value

View File

@@ -4,7 +4,6 @@ import androidx.room.ColumnInfo
import androidx.room.Entity import androidx.room.Entity
import androidx.room.PrimaryKey import androidx.room.PrimaryKey
import com.wismna.geoffroy.donext.domain.model.Priority import com.wismna.geoffroy.donext.domain.model.Priority
import java.time.Instant
@Entity(tableName = "tasks") @Entity(tableName = "tasks")
data class TaskEntity( data class TaskEntity(
@@ -20,5 +19,5 @@ data class TaskEntity(
@ColumnInfo(name = "task_list_id") @ColumnInfo(name = "task_list_id")
val taskListId: Long, val taskListId: Long,
@ColumnInfo(name = "due_date") @ColumnInfo(name = "due_date")
val dueDate: Instant? = null val dueDate: Long? = null
) )

View File

@@ -1,7 +1,5 @@
package com.wismna.geoffroy.donext.domain.model package com.wismna.geoffroy.donext.domain.model
import java.time.Instant
data class Task( data class Task(
val id: Long? = null, val id: Long? = null,
val name: String, val name: String,
@@ -10,5 +8,5 @@ data class Task(
val isDone: Boolean, val isDone: Boolean,
val isDeleted: Boolean, val isDeleted: Boolean,
val taskListId: Long, val taskListId: Long,
val dueDate: Instant? = null val dueDate: Long? = null
) )

View File

@@ -3,13 +3,12 @@ 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, dueDate: Instant?) { suspend operator fun invoke(taskListId: Long, title: String, description: String?, priority: Priority, dueDate: Long?) {
repository.insertTask( repository.insertTask(
Task( Task(
taskListId = taskListId, taskListId = taskListId,

View File

@@ -3,13 +3,12 @@ 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, dueDate: Instant?) { suspend operator fun invoke(taskId: Long, taskListId: Long, title: String, description: String?, priority: Priority, dueDate: Long?) {
repository.updateTask( repository.updateTask(
Task( Task(
id = taskId, id = taskId,

View File

@@ -7,6 +7,8 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.Add
import androidx.compose.material3.Badge
import androidx.compose.material3.BadgedBox
import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExtendedFloatingActionButton import androidx.compose.material3.ExtendedFloatingActionButton
@@ -78,11 +80,19 @@ fun MainScreen(
selectedDestination = index selectedDestination = index
}, },
text = { text = {
/*BadgedBox(
badge = {
if (overdueCount > 0) {
Badge { Text(overdueCount.toString()) }
}
}
) {*/
Text( Text(
text = destination.name, text = destination.name,
maxLines = 2, maxLines = 2,
overflow = TextOverflow.Ellipsis overflow = TextOverflow.Ellipsis
) )
//}
} }
) )
} }

View File

@@ -17,6 +17,7 @@ import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
@@ -28,6 +29,9 @@ import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
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.presentation.viewmodel.TaskListViewModel import com.wismna.geoffroy.donext.presentation.viewmodel.TaskListViewModel
import java.time.Instant
import java.time.LocalDate
import java.time.ZoneOffset
@Composable @Composable
fun TaskListScreen(viewModel: TaskListViewModel = hiltViewModel(), onTaskClick: (Task) -> Unit) { fun TaskListScreen(viewModel: TaskListViewModel = hiltViewModel(), onTaskClick: (Task) -> Unit) {
@@ -74,13 +78,23 @@ fun TaskItem(
onClick: () -> Unit, onClick: () -> Unit,
onToggleDone: (Boolean) -> Unit onToggleDone: (Boolean) -> Unit
) { ) {
val today = remember {
LocalDate.now(ZoneOffset.UTC)
}
val isOverdue = task.dueDate?.let { millis ->
val dueDate = Instant.ofEpochMilli(millis)
.atZone(ZoneOffset.UTC)
.toLocalDate()
dueDate.isBefore(today)
} ?: false
val baseStyle = MaterialTheme.typography.bodyLarge.copy( val baseStyle = MaterialTheme.typography.bodyLarge.copy(
fontWeight = when (task.priority) { fontWeight = when (task.priority) {
Priority.HIGH -> FontWeight.Bold Priority.HIGH -> FontWeight.Bold
Priority.NORMAL -> FontWeight.Normal Priority.NORMAL -> FontWeight.Normal
Priority.LOW -> FontWeight.Normal Priority.LOW -> FontWeight.Normal
}, },
color = when (task.priority) { color = if (isOverdue && !task.isDone) MaterialTheme.colorScheme.error else when (task.priority) {
Priority.HIGH -> MaterialTheme.colorScheme.onSurface Priority.HIGH -> MaterialTheme.colorScheme.onSurface
Priority.NORMAL -> MaterialTheme.colorScheme.onSurface Priority.NORMAL -> MaterialTheme.colorScheme.onSurface
Priority.LOW -> MaterialTheme.colorScheme.onSurfaceVariant Priority.LOW -> MaterialTheme.colorScheme.onSurfaceVariant

View File

@@ -25,7 +25,6 @@ import androidx.compose.material3.TextButton
import androidx.compose.material3.rememberDatePickerState 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.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
@@ -36,8 +35,10 @@ 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.Instant
import java.time.ZoneOffset
import java.time.format.DateTimeFormatter import java.time.format.DateTimeFormatter
import java.time.format.FormatStyle
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
@@ -46,7 +47,6 @@ 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()
@@ -98,11 +98,15 @@ fun TaskBottomSheet(
// --- Due Date --- // --- Due Date ---
var showDatePicker by remember { mutableStateOf(false) } var showDatePicker by remember { mutableStateOf(false) }
val formattedDate = viewModel.dueDate?.let {
Instant.ofEpochMilli(it)
.atZone(ZoneOffset.UTC)
.toLocalDate()
.format(DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM))
} ?: ""
OutlinedTextField( OutlinedTextField(
value = viewModel.dueDate?.atZone(ZoneId.systemDefault()) value = formattedDate,
?.toLocalDate()
?.format(DateTimeFormatter.ofPattern("MMM d, yyyy")) ?: "",
onValueChange = {}, onValueChange = {},
readOnly = true, readOnly = true,
label = { Text("Due Date") }, label = { Text("Due Date") },
@@ -122,9 +126,7 @@ fun TaskBottomSheet(
) )
if (showDatePicker) { if (showDatePicker) {
val datePickerState = rememberDatePickerState( val datePickerState = rememberDatePickerState(initialSelectedDateMillis = viewModel.dueDate)
initialSelectedDateMillis = viewModel.dueDate?.toEpochMilli()
)
DatePickerDialog( DatePickerDialog(
onDismissRequest = { showDatePicker = false }, onDismissRequest = { showDatePicker = false },
@@ -144,7 +146,9 @@ fun TaskBottomSheet(
Spacer(Modifier.height(16.dp)) Spacer(Modifier.height(16.dp))
Row (modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { Row (
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = if (viewModel.isEditing()) Arrangement.SpaceBetween else Arrangement.End) {
// --- Delete Button --- // --- Delete Button ---
if (viewModel.isEditing()) { if (viewModel.isEditing()) {
Button( Button(

View File

@@ -1,6 +1,5 @@
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
@@ -13,7 +12,6 @@ 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
@@ -29,7 +27,7 @@ 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) var dueDate by mutableStateOf<Long?>(null)
private set private set
private var editingTaskId: Long? = null private var editingTaskId: Long? = null
@@ -58,10 +56,7 @@ class TaskViewModel @Inject constructor(
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?) { fun onDueDateChanged(value: Long?) { dueDate = value }
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