feat: [#11] fiche tâche complète (TaskEditSheet + TaskEditViewModel + ReminderRepository)
This commit is contained in:
@@ -4,6 +4,7 @@ import androidx.room.Database
|
|||||||
import androidx.room.RoomDatabase
|
import androidx.room.RoomDatabase
|
||||||
import com.planify.mobile.data.local.dao.LabelDao
|
import com.planify.mobile.data.local.dao.LabelDao
|
||||||
import com.planify.mobile.data.local.dao.ProjectDao
|
import com.planify.mobile.data.local.dao.ProjectDao
|
||||||
|
import com.planify.mobile.data.local.dao.ReminderDao
|
||||||
import com.planify.mobile.data.local.dao.SectionDao
|
import com.planify.mobile.data.local.dao.SectionDao
|
||||||
import com.planify.mobile.data.local.dao.TaskDao
|
import com.planify.mobile.data.local.dao.TaskDao
|
||||||
import com.planify.mobile.data.local.entity.LabelEntity
|
import com.planify.mobile.data.local.entity.LabelEntity
|
||||||
@@ -30,4 +31,5 @@ abstract class AppDatabase : RoomDatabase() {
|
|||||||
abstract fun projectDao(): ProjectDao
|
abstract fun projectDao(): ProjectDao
|
||||||
abstract fun sectionDao(): SectionDao
|
abstract fun sectionDao(): SectionDao
|
||||||
abstract fun labelDao(): LabelDao
|
abstract fun labelDao(): LabelDao
|
||||||
|
abstract fun reminderDao(): ReminderDao
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package com.planify.mobile.data.local.dao
|
||||||
|
|
||||||
|
import androidx.room.Dao
|
||||||
|
import androidx.room.Delete
|
||||||
|
import androidx.room.Insert
|
||||||
|
import androidx.room.OnConflictStrategy
|
||||||
|
import androidx.room.Query
|
||||||
|
import com.planify.mobile.data.local.entity.ReminderEntity
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
|
@Dao
|
||||||
|
interface ReminderDao {
|
||||||
|
@Query("SELECT * FROM reminders WHERE task_id = :taskId")
|
||||||
|
fun getRemindersByTask(taskId: String): Flow<List<ReminderEntity>>
|
||||||
|
|
||||||
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
|
suspend fun insert(reminder: ReminderEntity)
|
||||||
|
|
||||||
|
@Delete
|
||||||
|
suspend fun delete(reminder: ReminderEntity)
|
||||||
|
|
||||||
|
@Query("DELETE FROM reminders WHERE task_id = :taskId")
|
||||||
|
suspend fun deleteByTask(taskId: String)
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
package com.planify.mobile.data.repository
|
||||||
|
|
||||||
|
import com.planify.mobile.data.local.dao.ReminderDao
|
||||||
|
import com.planify.mobile.data.local.entity.ReminderEntity
|
||||||
|
import com.planify.mobile.domain.model.DueDate
|
||||||
|
import com.planify.mobile.domain.model.Reminder
|
||||||
|
import com.planify.mobile.domain.model.ReminderType
|
||||||
|
import com.planify.mobile.domain.repository.ReminderRepository
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
import kotlinx.serialization.encodeToString
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class ReminderRepositoryImpl @Inject constructor(
|
||||||
|
private val dao: ReminderDao,
|
||||||
|
) : ReminderRepository {
|
||||||
|
|
||||||
|
override fun getRemindersByTask(taskId: String): Flow<List<Reminder>> =
|
||||||
|
dao.getRemindersByTask(taskId).map { list -> list.map { it.toDomain() } }
|
||||||
|
|
||||||
|
override suspend fun insertReminder(reminder: Reminder) = dao.insert(reminder.toEntity())
|
||||||
|
|
||||||
|
override suspend fun deleteReminder(reminder: Reminder) = dao.delete(reminder.toEntity())
|
||||||
|
|
||||||
|
override suspend fun deleteRemindersByTask(taskId: String) = dao.deleteByTask(taskId)
|
||||||
|
|
||||||
|
private fun ReminderEntity.toDomain() = Reminder(
|
||||||
|
id = id,
|
||||||
|
taskId = taskId,
|
||||||
|
type = ReminderType.valueOf(type),
|
||||||
|
dueDate = dueDate?.let { runCatching { Json.decodeFromString<DueDate>(it) }.getOrNull() },
|
||||||
|
minutesOffset = minutesOffset,
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun Reminder.toEntity() = ReminderEntity(
|
||||||
|
id = id,
|
||||||
|
taskId = taskId,
|
||||||
|
type = type.name,
|
||||||
|
dueDate = dueDate?.let { Json.encodeToString(it) },
|
||||||
|
minutesOffset = minutesOffset,
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -25,4 +25,5 @@ object DatabaseModule {
|
|||||||
@Provides fun provideProjectDao(db: AppDatabase) = db.projectDao()
|
@Provides fun provideProjectDao(db: AppDatabase) = db.projectDao()
|
||||||
@Provides fun provideSectionDao(db: AppDatabase) = db.sectionDao()
|
@Provides fun provideSectionDao(db: AppDatabase) = db.sectionDao()
|
||||||
@Provides fun provideLabelDao(db: AppDatabase) = db.labelDao()
|
@Provides fun provideLabelDao(db: AppDatabase) = db.labelDao()
|
||||||
|
@Provides fun provideReminderDao(db: AppDatabase) = db.reminderDao()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,10 +2,12 @@ package com.planify.mobile.di
|
|||||||
|
|
||||||
import com.planify.mobile.data.repository.LabelRepositoryImpl
|
import com.planify.mobile.data.repository.LabelRepositoryImpl
|
||||||
import com.planify.mobile.data.repository.ProjectRepositoryImpl
|
import com.planify.mobile.data.repository.ProjectRepositoryImpl
|
||||||
|
import com.planify.mobile.data.repository.ReminderRepositoryImpl
|
||||||
import com.planify.mobile.data.repository.SectionRepositoryImpl
|
import com.planify.mobile.data.repository.SectionRepositoryImpl
|
||||||
import com.planify.mobile.data.repository.TaskRepositoryImpl
|
import com.planify.mobile.data.repository.TaskRepositoryImpl
|
||||||
import com.planify.mobile.domain.repository.LabelRepository
|
import com.planify.mobile.domain.repository.LabelRepository
|
||||||
import com.planify.mobile.domain.repository.ProjectRepository
|
import com.planify.mobile.domain.repository.ProjectRepository
|
||||||
|
import com.planify.mobile.domain.repository.ReminderRepository
|
||||||
import com.planify.mobile.domain.repository.SectionRepository
|
import com.planify.mobile.domain.repository.SectionRepository
|
||||||
import com.planify.mobile.domain.repository.TaskRepository
|
import com.planify.mobile.domain.repository.TaskRepository
|
||||||
import dagger.Binds
|
import dagger.Binds
|
||||||
@@ -29,4 +31,7 @@ abstract class RepositoryModule {
|
|||||||
|
|
||||||
@Binds @Singleton
|
@Binds @Singleton
|
||||||
abstract fun bindLabelRepository(impl: LabelRepositoryImpl): LabelRepository
|
abstract fun bindLabelRepository(impl: LabelRepositoryImpl): LabelRepository
|
||||||
|
|
||||||
|
@Binds @Singleton
|
||||||
|
abstract fun bindReminderRepository(impl: ReminderRepositoryImpl): ReminderRepository
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package com.planify.mobile.domain.repository
|
||||||
|
|
||||||
|
import com.planify.mobile.domain.model.Reminder
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
|
interface ReminderRepository {
|
||||||
|
fun getRemindersByTask(taskId: String): Flow<List<Reminder>>
|
||||||
|
suspend fun insertReminder(reminder: Reminder)
|
||||||
|
suspend fun deleteReminder(reminder: Reminder)
|
||||||
|
suspend fun deleteRemindersByTask(taskId: String)
|
||||||
|
}
|
||||||
@@ -0,0 +1,210 @@
|
|||||||
|
package com.planify.mobile.ui.task
|
||||||
|
|
||||||
|
import androidx.compose.foundation.horizontalScroll
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
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.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.outlined.CalendarToday
|
||||||
|
import androidx.compose.material.icons.outlined.Flag
|
||||||
|
import androidx.compose.material.icons.outlined.Label
|
||||||
|
import androidx.compose.material.icons.outlined.List
|
||||||
|
import androidx.compose.material.icons.outlined.Notifications
|
||||||
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.FilterChip
|
||||||
|
import androidx.compose.material3.HorizontalDivider
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.ModalBottomSheet
|
||||||
|
import androidx.compose.material3.OutlinedTextField
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.rememberModalBottomSheetState
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
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.unit.dp
|
||||||
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
|
import com.planify.mobile.ui.components.PriorityBadge
|
||||||
|
import java.time.LocalDate
|
||||||
|
import java.time.format.DateTimeFormatter
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun TaskEditSheet(
|
||||||
|
projectId: String,
|
||||||
|
taskId: String? = null,
|
||||||
|
sectionId: String? = null,
|
||||||
|
parentId: String? = null,
|
||||||
|
onDismiss: () -> Unit,
|
||||||
|
viewModel: TaskEditViewModel = hiltViewModel(),
|
||||||
|
) {
|
||||||
|
LaunchedEffect(taskId, projectId) {
|
||||||
|
viewModel.init(taskId, projectId, sectionId, parentId)
|
||||||
|
}
|
||||||
|
|
||||||
|
val state by viewModel.state.collectAsState()
|
||||||
|
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
|
||||||
|
|
||||||
|
var showDueDatePicker by remember { mutableStateOf(false) }
|
||||||
|
var showPriorityPicker by remember { mutableStateOf(false) }
|
||||||
|
var showLabelPicker by remember { mutableStateOf(false) }
|
||||||
|
var showReminderPicker by remember { mutableStateOf(false) }
|
||||||
|
var showSubTasks by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
ModalBottomSheet(onDismissRequest = onDismiss, sheetState = sheetState) {
|
||||||
|
Column(Modifier.padding(horizontal = 16.dp)) {
|
||||||
|
Text(
|
||||||
|
text = if (taskId == null) "Nouvelle tâche" else "Modifier la tâche",
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
modifier = Modifier.padding(bottom = 12.dp),
|
||||||
|
)
|
||||||
|
|
||||||
|
OutlinedTextField(
|
||||||
|
value = state.content,
|
||||||
|
onValueChange = viewModel::setContent,
|
||||||
|
placeholder = { Text("Nom de la tâche") },
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
singleLine = true,
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(Modifier.height(8.dp))
|
||||||
|
|
||||||
|
OutlinedTextField(
|
||||||
|
value = state.description,
|
||||||
|
onValueChange = viewModel::setDescription,
|
||||||
|
placeholder = { Text("Description (optionnelle)") },
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
minLines = 2,
|
||||||
|
maxLines = 4,
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(Modifier.height(12.dp))
|
||||||
|
HorizontalDivider()
|
||||||
|
Spacer(Modifier.height(8.dp))
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.horizontalScroll(rememberScrollState()),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
) {
|
||||||
|
FilterChip(
|
||||||
|
selected = state.dueDate != null,
|
||||||
|
onClick = { showDueDatePicker = true },
|
||||||
|
label = { Text(state.dueDate?.date?.let { formatDateShort(it) } ?: "Date") },
|
||||||
|
leadingIcon = { Icon(Icons.Outlined.CalendarToday, contentDescription = null) },
|
||||||
|
)
|
||||||
|
FilterChip(
|
||||||
|
selected = state.priority != 4,
|
||||||
|
onClick = { showPriorityPicker = true },
|
||||||
|
label = { Text("P${state.priority}") },
|
||||||
|
leadingIcon = { PriorityBadge(priority = state.priority) },
|
||||||
|
)
|
||||||
|
FilterChip(
|
||||||
|
selected = state.labels.isNotEmpty(),
|
||||||
|
onClick = { showLabelPicker = true },
|
||||||
|
label = {
|
||||||
|
Text(if (state.labels.isNotEmpty()) "${state.labels.size} label(s)" else "Labels")
|
||||||
|
},
|
||||||
|
leadingIcon = { Icon(Icons.Outlined.Label, contentDescription = null) },
|
||||||
|
)
|
||||||
|
FilterChip(
|
||||||
|
selected = state.reminders.isNotEmpty(),
|
||||||
|
onClick = { showReminderPicker = true },
|
||||||
|
label = {
|
||||||
|
Text(if (state.reminders.isNotEmpty()) "${state.reminders.size} rappel(s)" else "Rappels")
|
||||||
|
},
|
||||||
|
leadingIcon = { Icon(Icons.Outlined.Notifications, contentDescription = null) },
|
||||||
|
)
|
||||||
|
FilterChip(
|
||||||
|
selected = state.subTasks.isNotEmpty() || showSubTasks,
|
||||||
|
onClick = { showSubTasks = !showSubTasks },
|
||||||
|
label = {
|
||||||
|
Text(if (state.subTasks.isNotEmpty()) "${state.subTasks.size} sous-tâche(s)" else "Sous-tâches")
|
||||||
|
},
|
||||||
|
leadingIcon = { Icon(Icons.Outlined.List, contentDescription = null) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showSubTasks) {
|
||||||
|
Spacer(Modifier.height(8.dp))
|
||||||
|
SubTaskSection(
|
||||||
|
subTasks = state.subTasks,
|
||||||
|
onAddSubTask = viewModel::addSubTask,
|
||||||
|
onToggleSubTask = viewModel::toggleSubTask,
|
||||||
|
onDeleteSubTask = viewModel::deleteSubTask,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(Modifier.height(12.dp))
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.End,
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
Spacer(Modifier.width(8.dp))
|
||||||
|
Button(
|
||||||
|
onClick = { viewModel.save(onDone = onDismiss) },
|
||||||
|
enabled = state.content.isNotBlank() && !state.isSaving,
|
||||||
|
) {
|
||||||
|
Text(if (taskId == null) "Créer" else "Enregistrer")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(Modifier.height(16.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showDueDatePicker) {
|
||||||
|
DueDatePickerSheet(
|
||||||
|
currentDueDate = state.dueDate,
|
||||||
|
onConfirm = { viewModel.setDueDate(it); showDueDatePicker = false },
|
||||||
|
onDismiss = { showDueDatePicker = false },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (showPriorityPicker) {
|
||||||
|
PriorityPickerSheet(
|
||||||
|
current = state.priority,
|
||||||
|
onSelect = viewModel::setPriority,
|
||||||
|
onDismiss = { showPriorityPicker = false },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (showLabelPicker) {
|
||||||
|
LabelPickerSheet(
|
||||||
|
selectedLabels = state.labels,
|
||||||
|
onConfirm = { viewModel.setLabels(it); showLabelPicker = false },
|
||||||
|
onDismiss = { showLabelPicker = false },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (showReminderPicker) {
|
||||||
|
ReminderPickerSheet(
|
||||||
|
taskId = state.taskId ?: "",
|
||||||
|
currentReminders = state.reminders,
|
||||||
|
onUpdate = viewModel::setReminders,
|
||||||
|
onDismiss = { showReminderPicker = false },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun formatDateShort(dateIso: String): String = runCatching {
|
||||||
|
val date = LocalDate.parse(dateIso)
|
||||||
|
val today = LocalDate.now()
|
||||||
|
when (date) {
|
||||||
|
today -> "Aujourd'hui"
|
||||||
|
today.plusDays(1) -> "Demain"
|
||||||
|
else -> date.format(DateTimeFormatter.ofPattern("d MMM", Locale.FRENCH))
|
||||||
|
}
|
||||||
|
}.getOrDefault(dateIso)
|
||||||
@@ -0,0 +1,157 @@
|
|||||||
|
package com.planify.mobile.ui.task
|
||||||
|
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.planify.mobile.data.notification.ReminderScheduler
|
||||||
|
import com.planify.mobile.domain.model.DueDate
|
||||||
|
import com.planify.mobile.domain.model.Reminder
|
||||||
|
import com.planify.mobile.domain.model.Task
|
||||||
|
import com.planify.mobile.domain.repository.ReminderRepository
|
||||||
|
import com.planify.mobile.domain.repository.TaskRepository
|
||||||
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
|
import kotlinx.coroutines.flow.update
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import java.time.LocalDateTime
|
||||||
|
import java.time.format.DateTimeFormatter
|
||||||
|
import java.util.UUID
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
data class TaskEditState(
|
||||||
|
val taskId: String? = null,
|
||||||
|
val projectId: String = "",
|
||||||
|
val sectionId: String? = null,
|
||||||
|
val parentId: String? = null,
|
||||||
|
val content: String = "",
|
||||||
|
val description: String = "",
|
||||||
|
val priority: Int = 4,
|
||||||
|
val dueDate: DueDate? = null,
|
||||||
|
val labels: List<String> = emptyList(),
|
||||||
|
val reminders: List<Reminder> = emptyList(),
|
||||||
|
val subTasks: List<Task> = emptyList(),
|
||||||
|
val isSaving: Boolean = false,
|
||||||
|
)
|
||||||
|
|
||||||
|
@HiltViewModel
|
||||||
|
class TaskEditViewModel @Inject constructor(
|
||||||
|
private val taskRepository: TaskRepository,
|
||||||
|
private val reminderRepository: ReminderRepository,
|
||||||
|
private val reminderScheduler: ReminderScheduler,
|
||||||
|
) : ViewModel() {
|
||||||
|
|
||||||
|
private val _state = MutableStateFlow(TaskEditState())
|
||||||
|
val state = _state.asStateFlow()
|
||||||
|
|
||||||
|
fun init(taskId: String?, projectId: String, sectionId: String? = null, parentId: String? = null) {
|
||||||
|
if (taskId != null) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
val task = taskRepository.getTaskById(taskId) ?: return@launch
|
||||||
|
val subTasks = taskRepository.getSubTasks(taskId).first()
|
||||||
|
val reminders = reminderRepository.getRemindersByTask(taskId).first()
|
||||||
|
_state.update {
|
||||||
|
it.copy(
|
||||||
|
taskId = taskId,
|
||||||
|
projectId = task.projectId,
|
||||||
|
sectionId = task.sectionId,
|
||||||
|
parentId = task.parentId,
|
||||||
|
content = task.content,
|
||||||
|
description = task.description,
|
||||||
|
priority = task.priority,
|
||||||
|
dueDate = task.dueDate,
|
||||||
|
labels = task.labels,
|
||||||
|
reminders = reminders,
|
||||||
|
subTasks = subTasks,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
_state.update { it.copy(projectId = projectId, sectionId = sectionId, parentId = parentId) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setContent(content: String) = _state.update { it.copy(content = content) }
|
||||||
|
fun setDescription(desc: String) = _state.update { it.copy(description = desc) }
|
||||||
|
fun setPriority(priority: Int) = _state.update { it.copy(priority = priority) }
|
||||||
|
fun setDueDate(dueDate: DueDate?) = _state.update { it.copy(dueDate = dueDate) }
|
||||||
|
fun setLabels(labels: List<String>) = _state.update { it.copy(labels = labels) }
|
||||||
|
fun setReminders(reminders: List<Reminder>) = _state.update { it.copy(reminders = reminders) }
|
||||||
|
|
||||||
|
fun addSubTask(content: String) {
|
||||||
|
val sub = Task(
|
||||||
|
id = UUID.randomUUID().toString(),
|
||||||
|
content = content,
|
||||||
|
projectId = _state.value.projectId,
|
||||||
|
parentId = _state.value.taskId ?: "pending",
|
||||||
|
)
|
||||||
|
_state.update { it.copy(subTasks = it.subTasks + sub) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toggleSubTask(subTask: Task) {
|
||||||
|
_state.update {
|
||||||
|
it.copy(subTasks = it.subTasks.map { t ->
|
||||||
|
if (t.id == subTask.id) t.copy(checked = !t.checked) else t
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun deleteSubTask(subTask: Task) {
|
||||||
|
_state.update { it.copy(subTasks = it.subTasks.filter { t -> t.id != subTask.id }) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun save(onDone: () -> Unit) {
|
||||||
|
val st = _state.value
|
||||||
|
if (st.content.isBlank()) return
|
||||||
|
_state.update { it.copy(isSaving = true) }
|
||||||
|
viewModelScope.launch {
|
||||||
|
val now = LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)
|
||||||
|
val id = st.taskId ?: UUID.randomUUID().toString()
|
||||||
|
|
||||||
|
val task = Task(
|
||||||
|
id = id,
|
||||||
|
content = st.content,
|
||||||
|
description = st.description,
|
||||||
|
projectId = st.projectId,
|
||||||
|
sectionId = st.sectionId,
|
||||||
|
parentId = st.parentId,
|
||||||
|
priority = st.priority,
|
||||||
|
dueDate = st.dueDate,
|
||||||
|
labels = st.labels,
|
||||||
|
addedAt = if (st.taskId == null) now else "",
|
||||||
|
updatedAt = now,
|
||||||
|
)
|
||||||
|
|
||||||
|
if (st.taskId == null) taskRepository.insertTask(task)
|
||||||
|
else taskRepository.updateTask(task)
|
||||||
|
|
||||||
|
// Sub-tasks: delete removed ones, then upsert remaining
|
||||||
|
if (st.taskId != null) {
|
||||||
|
val existingIds = taskRepository.getSubTasks(id).first().map { it.id }.toSet()
|
||||||
|
val currentIds = st.subTasks.map { it.id }.toSet()
|
||||||
|
(existingIds - currentIds).forEach { taskRepository.deleteTask(it) }
|
||||||
|
}
|
||||||
|
st.subTasks.forEach { sub ->
|
||||||
|
val actualSub = sub.copy(
|
||||||
|
parentId = id,
|
||||||
|
projectId = st.projectId,
|
||||||
|
addedAt = if (sub.addedAt.isBlank()) now else sub.addedAt,
|
||||||
|
updatedAt = now,
|
||||||
|
)
|
||||||
|
if (taskRepository.getTaskById(sub.id) != null) taskRepository.updateTask(actualSub)
|
||||||
|
else taskRepository.insertTask(actualSub)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reminders: replace all, reschedule
|
||||||
|
reminderRepository.deleteRemindersByTask(id)
|
||||||
|
st.reminders.forEach { reminder ->
|
||||||
|
val actual = reminder.copy(taskId = id)
|
||||||
|
reminderRepository.insertReminder(actual)
|
||||||
|
reminderScheduler.schedule(actual, task.content, task.dueDate?.date)
|
||||||
|
}
|
||||||
|
|
||||||
|
_state.update { it.copy(isSaving = false) }
|
||||||
|
onDone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user