mirror of
https://github.com/wismna/DoNext.git
synced 2025-10-03 07:30:13 -04:00
Simplify due date data type
Due date displays proper date Overdue tasks display as red
This commit is contained in:
@@ -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
|
||||||
|
|
||||||
|
@@ -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
|
||||||
)
|
)
|
||||||
|
@@ -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
|
||||||
)
|
)
|
||||||
|
@@ -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,
|
||||||
|
@@ -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,
|
||||||
|
@@ -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 = {
|
||||||
Text(
|
/*BadgedBox(
|
||||||
text = destination.name,
|
badge = {
|
||||||
maxLines = 2,
|
if (overdueCount > 0) {
|
||||||
overflow = TextOverflow.Ellipsis
|
Badge { Text(overdueCount.toString()) }
|
||||||
)
|
}
|
||||||
|
}
|
||||||
|
) {*/
|
||||||
|
Text(
|
||||||
|
text = destination.name,
|
||||||
|
maxLines = 2,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
//}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@@ -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
|
||||||
|
@@ -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(
|
||||||
|
@@ -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
|
||||||
|
Reference in New Issue
Block a user