Добавил логику работы с ИИ-агентом и работу с расписанием

This commit is contained in:
2025-12-25 22:40:40 +03:00
parent d8a0237e43
commit 4ff516d06a
41 changed files with 1778 additions and 259 deletions

View File

@@ -13,6 +13,10 @@ data class TaskEntity(
val completed: Boolean = false,
val scheduledTime: Long?, // timestamp
val duration: Int? = null,
val scheduleId: String
val scheduleId: String,
val order: Int? = null,
val category: String? = null,
val createdAt: Long? = null, // timestamp
val updatedAt: Long? = null // timestamp
)

View File

@@ -17,7 +17,15 @@ fun TaskEntity.toDomain(): Task {
LocalDateTime.ofInstant(Instant.ofEpochMilli(it), ZoneId.systemDefault())
},
duration = duration,
scheduleId = scheduleId
scheduleId = scheduleId,
order = order ?: 0,
category = category,
createdAt = createdAt?.let {
LocalDateTime.ofInstant(Instant.ofEpochMilli(it), ZoneId.systemDefault())
},
updatedAt = updatedAt?.let {
LocalDateTime.ofInstant(Instant.ofEpochMilli(it), ZoneId.systemDefault())
}
)
}
@@ -30,7 +38,11 @@ fun Task.toEntity(): TaskEntity {
completed = completed,
scheduledTime = scheduledTime?.atZone(ZoneId.systemDefault())?.toInstant()?.toEpochMilli(),
duration = duration,
scheduleId = scheduleId
scheduleId = scheduleId,
order = order,
category = category,
createdAt = createdAt?.atZone(ZoneId.systemDefault())?.toInstant()?.toEpochMilli(),
updatedAt = updatedAt?.atZone(ZoneId.systemDefault())?.toInstant()?.toEpochMilli()
)
}

View File

@@ -0,0 +1,22 @@
package com.novayaplaneta.data.remote
import com.novayaplaneta.data.remote.dto.ChatRequest
import com.novayaplaneta.data.remote.dto.ChatResponse
import com.novayaplaneta.data.remote.dto.GenerateScheduleRequest
import com.novayaplaneta.data.remote.dto.GenerateScheduleResponse
import retrofit2.Response
import retrofit2.http.Body
import retrofit2.http.POST
interface AiApi {
@POST("api/v1/ai/chat")
suspend fun chat(
@Body request: ChatRequest
): Response<ChatResponse>
@POST("api/v1/ai/schedule/generate")
suspend fun generateSchedule(
@Body request: GenerateScheduleRequest
): Response<GenerateScheduleResponse>
}

View File

@@ -2,13 +2,22 @@ package com.novayaplaneta.data.remote
import com.novayaplaneta.data.remote.dto.ChatRequest
import com.novayaplaneta.data.remote.dto.ChatResponse
import com.novayaplaneta.data.remote.dto.CompleteTaskResponse
import com.novayaplaneta.data.remote.dto.CreateScheduleRequest
import com.novayaplaneta.data.remote.dto.CreateTaskRequest
import com.novayaplaneta.data.remote.dto.LoginRequest
import com.novayaplaneta.data.remote.dto.LoginResponse
import com.novayaplaneta.data.remote.dto.ScheduleDto
import com.novayaplaneta.data.remote.dto.TaskDto
import retrofit2.Response
import retrofit2.http.Body
import retrofit2.http.DELETE
import retrofit2.http.GET
import retrofit2.http.Header
import retrofit2.http.PATCH
import retrofit2.http.POST
import retrofit2.http.Path
import retrofit2.http.Query
interface BackendApi {
@POST("api/v1/auth/login")
@@ -21,5 +30,54 @@ interface BackendApi {
@Header("Authorization") token: String,
@Body request: ChatRequest
): Response<ChatResponse>
@GET("api/v1/schedules")
suspend fun getSchedules(
@Query("skip") skip: Int = 0,
@Query("limit") limit: Int = 100,
@Query("schedule_date") scheduleDate: String? = null
): Response<List<ScheduleDto>>
@POST("api/v1/schedules")
suspend fun createSchedule(
@Body request: CreateScheduleRequest
): Response<ScheduleDto>
@GET("api/v1/schedules/{schedule_id}")
suspend fun getScheduleById(
@Path("schedule_id") scheduleId: String
): Response<ScheduleDto>
@DELETE("api/v1/schedules/{schedule_id}")
suspend fun deleteSchedule(
@Path("schedule_id") scheduleId: String
): Response<Unit>
// Tasks endpoints
@GET("api/v1/tasks/schedule/{schedule_id}")
suspend fun getTasksByScheduleId(
@Path("schedule_id") scheduleId: String
): Response<List<TaskDto>>
@GET("api/v1/tasks/{task_id}")
suspend fun getTaskById(
@Path("task_id") taskId: String
): Response<TaskDto>
@POST("api/v1/tasks")
suspend fun createTask(
@Body request: CreateTaskRequest
): Response<TaskDto>
@DELETE("api/v1/tasks/{task_id}")
suspend fun deleteTask(
@Path("task_id") taskId: String
): Response<Unit>
@PATCH("api/v1/tasks/{task_id}/complete")
suspend fun completeTask(
@Path("task_id") taskId: String,
@Query("completed") completed: Boolean
): Response<CompleteTaskResponse>
}

View File

@@ -4,6 +4,7 @@ import kotlinx.serialization.Serializable
@Serializable
data class ChatRequest(
val message: String
val message: String,
val conversation_id: String? = null
)

View File

@@ -4,6 +4,9 @@ import kotlinx.serialization.Serializable
@Serializable
data class ChatResponse(
val message: String
val response: String,
val conversation_id: String,
val tokens_used: Int,
val model: String
)

View File

@@ -0,0 +1,19 @@
package com.novayaplaneta.data.remote.dto
import kotlinx.serialization.Serializable
@Serializable
data class CompleteTaskResponse(
val id: String,
val title: String,
val description: String?,
val image_url: String?,
val duration_minutes: Int?,
val order: Int,
val category: String?,
val schedule_id: String,
val completed: Boolean,
val created_at: String,
val updated_at: String
)

View File

@@ -0,0 +1,11 @@
package com.novayaplaneta.data.remote.dto
import kotlinx.serialization.Serializable
@Serializable
data class CreateScheduleRequest(
val title: String,
val date: String, // yyyy-MM-dd
val description: String
)

View File

@@ -0,0 +1,15 @@
package com.novayaplaneta.data.remote.dto
import kotlinx.serialization.Serializable
@Serializable
data class CreateTaskRequest(
val title: String,
val description: String? = null,
val image_url: String? = null,
val duration_minutes: Int? = null,
val order: Int = 0,
val category: String? = null,
val schedule_id: String
)

View File

@@ -0,0 +1,12 @@
package com.novayaplaneta.data.remote.dto
import kotlinx.serialization.Serializable
@Serializable
data class GenerateScheduleRequest(
val child_age: Int,
val preferences: List<String>,
val date: String,
val description: String
)

View File

@@ -0,0 +1,13 @@
package com.novayaplaneta.data.remote.dto
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonObject
@Serializable
data class GenerateScheduleResponse(
val schedule_id: String,
val title: String,
val tasks: List<JsonObject>,
val tokens_used: Int
)

View File

@@ -0,0 +1,18 @@
package com.novayaplaneta.data.remote.dto
import kotlinx.serialization.Serializable
@Serializable
data class ScheduleDto(
val id: String,
val title: String,
val date: String, // yyyy-MM-dd
val description: String,
val user_id: String,
val created_at: String, // ISO date
val updated_at: String, // ISO date
val tasks: List<TaskDto> = emptyList()
)
// TaskDto moved to separate file

View File

@@ -0,0 +1,19 @@
package com.novayaplaneta.data.remote.dto
import kotlinx.serialization.Serializable
@Serializable
data class TaskDto(
val id: String,
val title: String,
val description: String?,
val image_url: String?,
val duration_minutes: Int?,
val order: Int,
val category: String?,
val schedule_id: String,
val completed: Boolean,
val created_at: String,
val updated_at: String
)

View File

@@ -0,0 +1,50 @@
package com.novayaplaneta.data.remote.mapper
import com.novayaplaneta.data.remote.dto.ScheduleDto
import com.novayaplaneta.data.remote.dto.TaskDto
import com.novayaplaneta.domain.model.Schedule
import java.time.LocalDate
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
fun ScheduleDto.toDomain(): Schedule {
val dateFormatter = DateTimeFormatter.ISO_DATE_TIME
val dateLocalDate = LocalDate.parse(date, DateTimeFormatter.ISO_DATE)
val dateLocalDateTime = dateLocalDate.atStartOfDay()
val createdAtDateTime = try {
LocalDateTime.parse(created_at, dateFormatter)
} catch (e: Exception) {
LocalDateTime.now()
}
val updatedAtDateTime = try {
LocalDateTime.parse(updated_at, dateFormatter)
} catch (e: Exception) {
LocalDateTime.now()
}
return Schedule(
id = id,
title = title,
description = description,
tasks = tasks.map { it.toDomain() },
date = dateLocalDateTime,
createdAt = createdAtDateTime,
userId = user_id
)
}
// TaskDto.toDomain() moved to TaskMapper.kt
fun Schedule.toCreateRequest(): com.novayaplaneta.data.remote.dto.CreateScheduleRequest {
val dateFormatter = DateTimeFormatter.ISO_DATE
val dateString = date.toLocalDate().format(dateFormatter)
return com.novayaplaneta.data.remote.dto.CreateScheduleRequest(
title = title,
date = dateString,
description = description ?: ""
)
}

View File

@@ -0,0 +1,83 @@
package com.novayaplaneta.data.remote.mapper
import com.novayaplaneta.data.remote.dto.CompleteTaskResponse
import com.novayaplaneta.data.remote.dto.CreateTaskRequest
import com.novayaplaneta.data.remote.dto.TaskDto
import com.novayaplaneta.domain.model.Task
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
fun TaskDto.toDomain(): Task {
val dateFormatter = DateTimeFormatter.ISO_DATE_TIME
val createdAtDateTime = try {
LocalDateTime.parse(created_at, dateFormatter)
} catch (e: Exception) {
null
}
val updatedAtDateTime = try {
LocalDateTime.parse(updated_at, dateFormatter)
} catch (e: Exception) {
null
}
return Task(
id = id,
title = title,
description = description,
imageUrl = image_url,
completed = completed,
scheduledTime = null, // Not provided by API
duration = duration_minutes,
scheduleId = schedule_id,
order = order,
category = category,
createdAt = createdAtDateTime,
updatedAt = updatedAtDateTime
)
}
fun CompleteTaskResponse.toDomain(): Task {
val dateFormatter = DateTimeFormatter.ISO_DATE_TIME
val createdAtDateTime = try {
LocalDateTime.parse(created_at, dateFormatter)
} catch (e: Exception) {
null
}
val updatedAtDateTime = try {
LocalDateTime.parse(updated_at, dateFormatter)
} catch (e: Exception) {
null
}
return Task(
id = id,
title = title,
description = description,
imageUrl = image_url,
completed = completed,
scheduledTime = null,
duration = duration_minutes,
scheduleId = schedule_id,
order = order,
category = category,
createdAt = createdAtDateTime,
updatedAt = updatedAtDateTime
)
}
fun Task.toCreateRequest(): CreateTaskRequest {
return CreateTaskRequest(
title = title,
description = description,
image_url = imageUrl,
duration_minutes = duration,
order = order,
category = category,
schedule_id = scheduleId
)
}

View File

@@ -3,24 +3,28 @@ package com.novayaplaneta.data.repository
import com.novayaplaneta.data.local.dao.ChatMessageDao
import com.novayaplaneta.data.local.mapper.toDomain
import com.novayaplaneta.data.local.mapper.toEntity
import com.novayaplaneta.data.remote.BackendApi
import com.novayaplaneta.data.remote.AiApi
import com.novayaplaneta.data.remote.dto.ChatRequest
import com.novayaplaneta.data.remote.dto.GenerateScheduleRequest
import com.novayaplaneta.domain.model.ChatMessage
import com.novayaplaneta.domain.repository.AIRepository
import com.novayaplaneta.domain.repository.ChatResponse
import com.novayaplaneta.domain.repository.GenerateScheduleResult
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive
import java.time.LocalDateTime
import java.util.UUID
import javax.inject.Inject
class AIRepositoryImpl @Inject constructor(
private val chatMessageDao: ChatMessageDao,
private val api: BackendApi,
private val authRepository: com.novayaplaneta.domain.repository.AuthRepository
private val aiApi: AiApi
) : AIRepository {
override suspend fun sendMessage(userId: String, message: String): Result<String> {
override suspend fun sendMessage(userId: String, message: String, conversationId: String?): Result<ChatResponse> {
return try {
// Save user message
val userMessage = ChatMessage(
@@ -32,22 +36,33 @@ class AIRepositoryImpl @Inject constructor(
)
chatMessageDao.insertMessage(userMessage.toEntity())
// Тестовый ответ для демонстрации
kotlinx.coroutines.delay(1500) // Небольшая задержка для анимации
// Send request to API
val request = ChatRequest(message = message, conversation_id = conversationId)
val response = aiApi.chat(request)
val aiResponse = "!!!!"
if (response.isSuccessful && response.body() != null) {
val chatResponse = response.body()!!
// Save AI response
val aiMessage = ChatMessage(
id = UUID.randomUUID().toString(),
message = aiResponse,
message = chatResponse.response,
isFromAI = true,
timestamp = LocalDateTime.now(),
userId = userId
)
chatMessageDao.insertMessage(aiMessage.toEntity())
Result.success(aiResponse)
Result.success(
ChatResponse(
response = chatResponse.response,
conversationId = chatResponse.conversation_id
)
)
} else {
val errorBody = response.errorBody()?.string() ?: "Unknown error"
Result.failure(Exception("Failed to send message: $errorBody (code: ${response.code()})"))
}
} catch (e: Exception) {
Result.failure(e)
}
@@ -59,9 +74,79 @@ class AIRepositoryImpl @Inject constructor(
}
}
override suspend fun generateSchedule(userId: String, preferences: String): Result<String> {
// TODO: Implement schedule generation
return Result.failure(Exception("Not implemented"))
override suspend fun generateSchedule(
childAge: Int,
preferences: List<String>,
date: String,
description: String
): Result<GenerateScheduleResult> {
return try {
val request = GenerateScheduleRequest(
child_age = childAge,
preferences = preferences,
date = date,
description = description
)
val response = aiApi.generateSchedule(request)
if (response.isSuccessful && response.body() != null) {
val generateResponse = response.body()!!
// Конвертируем JsonObject в Map<String, Any>
val tasksAsMap = generateResponse.tasks.map { jsonObject ->
jsonObject.toMap()
}
Result.success(
GenerateScheduleResult(
scheduleId = generateResponse.schedule_id,
title = generateResponse.title,
tasks = tasksAsMap
)
)
} else {
val errorBody = response.errorBody()?.string() ?: "Unknown error"
Result.failure(Exception("Failed to generate schedule: $errorBody (code: ${response.code()})"))
}
} catch (e: Exception) {
Result.failure(e)
}
}
private fun JsonObject.toMap(): Map<String, Any> {
return entries.associate { (key, value) ->
key to when (value) {
is JsonPrimitive -> {
when {
value.isString -> value.content
else -> {
// Пытаемся парсить как число или boolean
val content = value.content
when {
content == "true" || content == "false" -> content.toBoolean()
content.toLongOrNull() != null -> content.toLong()
content.toDoubleOrNull() != null -> content.toDouble()
else -> content
}
}
}
}
is JsonObject -> value.toMap()
is JsonArray -> value.map {
if (it is JsonObject) it.toMap()
else if (it is JsonPrimitive) {
val content = it.content
when {
content == "true" || content == "false" -> content.toBoolean()
content.toLongOrNull() != null -> content.toLong()
content.toDoubleOrNull() != null -> content.toDouble()
else -> content
}
} else {
it.toString()
}
}
else -> value.toString()
}
}
}
}

View File

@@ -4,47 +4,126 @@ import com.novayaplaneta.data.local.dao.ScheduleDao
import com.novayaplaneta.data.local.dao.TaskDao
import com.novayaplaneta.data.local.mapper.toDomain
import com.novayaplaneta.data.local.mapper.toEntity
import com.novayaplaneta.data.remote.BackendApi
import com.novayaplaneta.data.remote.mapper.toCreateRequest
import com.novayaplaneta.data.remote.mapper.toDomain
import com.novayaplaneta.domain.model.Schedule
import com.novayaplaneta.domain.repository.ScheduleRepository
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.map
import javax.inject.Inject
class ScheduleRepositoryImpl @Inject constructor(
private val scheduleDao: ScheduleDao,
private val taskDao: TaskDao
private val taskDao: TaskDao,
private val backendApi: BackendApi
) : ScheduleRepository {
private val _schedulesCache = MutableStateFlow<List<Schedule>>(emptyList())
val schedulesCache: StateFlow<List<Schedule>> = _schedulesCache.asStateFlow()
override fun getSchedules(userId: String): Flow<List<Schedule>> {
return scheduleDao.getSchedulesByUserId(userId).map { schedules ->
schedules.map { scheduleEntity ->
// Note: In production, you'd need to fetch tasks for each schedule
scheduleEntity.toDomain(emptyList())
}
}
return _schedulesCache.asStateFlow()
}
override suspend fun getScheduleById(id: String): Schedule? {
val scheduleEntity = scheduleDao.getScheduleById(id) ?: return null
val tasks = taskDao.getTasksByScheduleId(id)
// Simplified - would need proper Flow handling
return scheduleEntity.toDomain(emptyList())
return try {
val response = backendApi.getScheduleById(id)
if (response.isSuccessful && response.body() != null) {
val schedule = response.body()!!.toDomain()
// Обновляем кэш, если расписание уже есть, или добавляем новое
val currentSchedules = _schedulesCache.value.toMutableList()
val index = currentSchedules.indexOfFirst { it.id == id }
if (index >= 0) {
currentSchedules[index] = schedule
} else {
currentSchedules.add(schedule)
}
_schedulesCache.value = currentSchedules
schedule
} else {
null
}
} catch (e: Exception) {
null
}
}
override suspend fun createSchedule(schedule: Schedule) {
scheduleDao.insertSchedule(schedule.toEntity())
try {
val request = schedule.toCreateRequest()
val response = backendApi.createSchedule(request)
if (response.isSuccessful && response.body() != null) {
val createdSchedule = response.body()!!.toDomain()
// Обновляем кэш
val currentSchedules = _schedulesCache.value.toMutableList()
currentSchedules.add(createdSchedule)
_schedulesCache.value = currentSchedules
} else {
val errorBody = response.errorBody()?.string() ?: "Unknown error"
throw Exception("Failed to create schedule: $errorBody (code: ${response.code()})")
}
} catch (e: Exception) {
throw e
}
}
override suspend fun updateSchedule(schedule: Schedule) {
// TODO: Implement if API supports update
scheduleDao.updateSchedule(schedule.toEntity())
}
override suspend fun deleteSchedule(id: String) {
scheduleDao.deleteSchedule(id)
try {
val response = backendApi.deleteSchedule(id)
if (response.isSuccessful) {
// Удаляем из кэша
val currentSchedules = _schedulesCache.value.toMutableList()
currentSchedules.removeAll { it.id == id }
_schedulesCache.value = currentSchedules
} else {
val errorBody = response.errorBody()?.string() ?: "Unknown error"
throw Exception("Failed to delete schedule: $errorBody (code: ${response.code()})")
}
} catch (e: Exception) {
throw e
}
}
override suspend fun syncSchedules(userId: String) {
// TODO: Implement sync with backend
try {
val response = backendApi.getSchedules(skip = 0, limit = 100)
if (response.isSuccessful && response.body() != null) {
val schedules = response.body()!!.map { it.toDomain() }
_schedulesCache.value = schedules
} else {
throw Exception("Failed to sync schedules: ${response.errorBody()?.string()}")
}
} catch (e: Exception) {
throw e
}
}
override suspend fun loadSchedules(scheduleDate: String?) {
try {
val response = backendApi.getSchedules(
skip = 0,
limit = 100,
scheduleDate = scheduleDate
)
if (response.isSuccessful && response.body() != null) {
val schedules = response.body()!!.map { it.toDomain() }
_schedulesCache.value = schedules
} else {
val errorBody = response.errorBody()?.string() ?: "Unknown error"
throw Exception("Failed to load schedules: $errorBody (code: ${response.code()})")
}
} catch (e: Exception) {
throw e
}
}
}

View File

@@ -3,40 +3,182 @@ package com.novayaplaneta.data.repository
import com.novayaplaneta.data.local.dao.TaskDao
import com.novayaplaneta.data.local.mapper.toDomain
import com.novayaplaneta.data.local.mapper.toEntity
import com.novayaplaneta.data.remote.BackendApi
import com.novayaplaneta.data.remote.dto.CreateTaskRequest
import com.novayaplaneta.data.remote.mapper.toCreateRequest
import com.novayaplaneta.data.remote.mapper.toDomain
import com.novayaplaneta.domain.model.Task
import com.novayaplaneta.domain.repository.TaskRepository
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.map
import javax.inject.Inject
class TaskRepositoryImpl @Inject constructor(
private val taskDao: TaskDao
private val taskDao: TaskDao,
private val backendApi: BackendApi
) : TaskRepository {
override fun getTasks(scheduleId: String): Flow<List<Task>> {
return taskDao.getTasksByScheduleId(scheduleId).map { tasks ->
tasks.map { it.toDomain() }
}
}
// Кэш задач по scheduleId
private val _tasksCache = mutableMapOf<String, MutableStateFlow<List<Task>>>()
override suspend fun getTaskById(id: String): Task? {
return taskDao.getTaskById(id)?.toDomain()
}
override suspend fun loadTasks(scheduleId: String): Result<Unit> {
return try {
val response = backendApi.getTasksByScheduleId(scheduleId)
if (response.isSuccessful && response.body() != null) {
val tasks = response.body()!!.map { it.toDomain() }.sortedBy { it.order }
override suspend fun createTask(task: Task) {
// Обновляем кэш
val cache = _tasksCache.getOrPut(scheduleId) { MutableStateFlow(emptyList()) }
cache.value = tasks
// Сохраняем в локальную БД
tasks.forEach { task ->
taskDao.insertTask(task.toEntity())
}
Result.success(Unit)
} else {
val errorBody = response.errorBody()?.string() ?: "Unknown error"
Result.failure(Exception("Failed to load tasks: $errorBody (code: ${response.code()})"))
}
} catch (e: Exception) {
Result.failure(e)
}
}
override fun getTasks(scheduleId: String): Flow<List<Task>> {
val cache = _tasksCache.getOrPut(scheduleId) { MutableStateFlow(emptyList()) }
return cache.asStateFlow()
}
override suspend fun getTaskById(id: String): Result<Task> {
return try {
val response = backendApi.getTaskById(id)
if (response.isSuccessful && response.body() != null) {
val task = response.body()!!.toDomain()
// Обновляем в кэше
val scheduleId = task.scheduleId
val cache = _tasksCache[scheduleId]
if (cache != null) {
val currentTasks = cache.value.toMutableList()
val index = currentTasks.indexOfFirst { it.id == id }
if (index >= 0) {
currentTasks[index] = task
} else {
currentTasks.add(task)
}
cache.value = currentTasks.sortedBy { it.order }
}
// Сохраняем в локальную БД
taskDao.insertTask(task.toEntity())
Result.success(task)
} else {
val errorBody = response.errorBody()?.string() ?: "Unknown error"
Result.failure(Exception("Failed to get task: $errorBody (code: ${response.code()})"))
}
} catch (e: Exception) {
Result.failure(e)
}
}
override suspend fun createTask(task: Task): Result<Task> {
return try {
val request = task.toCreateRequest()
val response = backendApi.createTask(request)
if (response.isSuccessful && response.body() != null) {
val createdTask = response.body()!!.toDomain()
// Обновляем кэш
val cache = _tasksCache[task.scheduleId]
if (cache != null) {
val currentTasks = cache.value.toMutableList()
currentTasks.add(createdTask)
cache.value = currentTasks.sortedBy { it.order }
}
// Сохраняем в локальную БД
taskDao.insertTask(createdTask.toEntity())
Result.success(createdTask)
} else {
val errorBody = response.errorBody()?.string() ?: "Unknown error"
Result.failure(Exception("Failed to create task: $errorBody (code: ${response.code()})"))
}
} catch (e: Exception) {
Result.failure(e)
}
}
override suspend fun updateTask(task: Task) {
taskDao.updateTask(task.toEntity())
}
override suspend fun deleteTask(id: String) {
taskDao.deleteTask(id)
override suspend fun deleteTask(id: String): Result<Unit> {
return try {
// Сначала получаем задачу для определения scheduleId
val task = taskDao.getTaskById(id)?.toDomain()
val scheduleId = task?.scheduleId
val response = backendApi.deleteTask(id)
if (response.isSuccessful) {
// Удаляем из кэша
if (scheduleId != null) {
val cache = _tasksCache[scheduleId]
if (cache != null) {
val currentTasks = cache.value.toMutableList()
currentTasks.removeAll { it.id == id }
cache.value = currentTasks
}
}
override suspend fun completeTask(id: String) {
taskDao.completeTask(id)
// Удаляем из локальной БД
taskDao.deleteTask(id)
Result.success(Unit)
} else {
val errorBody = response.errorBody()?.string() ?: "Unknown error"
Result.failure(Exception("Failed to delete task: $errorBody (code: ${response.code()})"))
}
} catch (e: Exception) {
Result.failure(e)
}
}
override suspend fun completeTask(id: String, completed: Boolean): Result<Task> {
return try {
val response = backendApi.completeTask(id, completed)
if (response.isSuccessful && response.body() != null) {
val updatedTask = response.body()!!.toDomain()
// Обновляем в кэше
val scheduleId = updatedTask.scheduleId
val cache = _tasksCache[scheduleId]
if (cache != null) {
val currentTasks = cache.value.toMutableList()
val index = currentTasks.indexOfFirst { it.id == id }
if (index >= 0) {
currentTasks[index] = updatedTask
cache.value = currentTasks.sortedBy { it.order }
}
}
// Сохраняем в локальную БД
taskDao.insertTask(updatedTask.toEntity())
Result.success(updatedTask)
} else {
val errorBody = response.errorBody()?.string() ?: "Unknown error"
Result.failure(Exception("Failed to complete task: $errorBody (code: ${response.code()})"))
}
} catch (e: Exception) {
Result.failure(e)
}
}
}

View File

@@ -1,6 +1,7 @@
package com.novayaplaneta.di
import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory
import com.novayaplaneta.data.remote.AiApi
import com.novayaplaneta.data.remote.AuthApi
import com.novayaplaneta.data.remote.AuthInterceptor
import com.novayaplaneta.data.remote.BackendApi
@@ -72,5 +73,13 @@ object NetworkModule {
): BackendApi {
return retrofit.create(BackendApi::class.java)
}
@Provides
@Singleton
fun provideAiApi(
retrofit: Retrofit
): AiApi {
return retrofit.create(AiApi::class.java)
}
}

View File

@@ -10,6 +10,10 @@ data class Task(
val completed: Boolean = false,
val scheduledTime: LocalDateTime?,
val duration: Int? = null, // in minutes
val scheduleId: String
val scheduleId: String,
val order: Int = 0,
val category: String? = null,
val createdAt: LocalDateTime? = null,
val updatedAt: LocalDateTime? = null
)

View File

@@ -4,8 +4,24 @@ import com.novayaplaneta.domain.model.ChatMessage
import kotlinx.coroutines.flow.Flow
interface AIRepository {
suspend fun sendMessage(userId: String, message: String): Result<String>
suspend fun sendMessage(userId: String, message: String, conversationId: String?): Result<ChatResponse>
fun getChatHistory(userId: String): Flow<List<ChatMessage>>
suspend fun generateSchedule(userId: String, preferences: String): Result<String>
suspend fun generateSchedule(
childAge: Int,
preferences: List<String>,
date: String,
description: String
): Result<GenerateScheduleResult>
}
data class ChatResponse(
val response: String,
val conversationId: String
)
data class GenerateScheduleResult(
val scheduleId: String,
val title: String,
val tasks: List<Map<String, Any>>
)

View File

@@ -2,7 +2,6 @@ package com.novayaplaneta.domain.repository
import com.novayaplaneta.domain.model.Schedule
import kotlinx.coroutines.flow.Flow
import java.time.LocalDateTime
interface ScheduleRepository {
fun getSchedules(userId: String): Flow<List<Schedule>>
@@ -11,5 +10,6 @@ interface ScheduleRepository {
suspend fun updateSchedule(schedule: Schedule)
suspend fun deleteSchedule(id: String)
suspend fun syncSchedules(userId: String)
suspend fun loadSchedules(scheduleDate: String? = null)
}

View File

@@ -4,11 +4,12 @@ import com.novayaplaneta.domain.model.Task
import kotlinx.coroutines.flow.Flow
interface TaskRepository {
suspend fun loadTasks(scheduleId: String): Result<Unit>
fun getTasks(scheduleId: String): Flow<List<Task>>
suspend fun getTaskById(id: String): Task?
suspend fun createTask(task: Task)
suspend fun getTaskById(id: String): Result<Task>
suspend fun createTask(task: Task): Result<Task>
suspend fun updateTask(task: Task)
suspend fun deleteTask(id: String)
suspend fun completeTask(id: String)
suspend fun deleteTask(id: String): Result<Unit>
suspend fun completeTask(id: String, completed: Boolean): Result<Task>
}

View File

@@ -1,13 +1,13 @@
package com.novayaplaneta.domain.usecase
import com.novayaplaneta.domain.model.Task
import com.novayaplaneta.domain.repository.TaskRepository
import javax.inject.Inject
class CompleteTaskUseCase @Inject constructor(
private val repository: TaskRepository
) {
suspend operator fun invoke(taskId: String) {
repository.completeTask(taskId)
suspend operator fun invoke(id: String, completed: Boolean): Result<Task> {
return repository.completeTask(id, completed)
}
}

View File

@@ -7,8 +7,17 @@ import javax.inject.Inject
class CreateScheduleUseCase @Inject constructor(
private val repository: ScheduleRepository
) {
suspend operator fun invoke(schedule: Schedule) {
suspend operator fun invoke(schedule: Schedule): Result<Schedule> {
return try {
repository.createSchedule(schedule)
// После создания перезагружаем список, чтобы получить актуальные данные
repository.loadSchedules()
// Возвращаем успешный результат
// Созданное расписание уже добавлено в кэш через createSchedule
Result.success(schedule)
} catch (e: Exception) {
Result.failure(e)
}
}
}

View File

@@ -0,0 +1,14 @@
package com.novayaplaneta.domain.usecase
import com.novayaplaneta.domain.model.Task
import com.novayaplaneta.domain.repository.TaskRepository
import javax.inject.Inject
class CreateTaskUseCase @Inject constructor(
private val repository: TaskRepository
) {
suspend operator fun invoke(task: Task): Result<Task> {
return repository.createTask(task)
}
}

View File

@@ -0,0 +1,18 @@
package com.novayaplaneta.domain.usecase
import com.novayaplaneta.domain.repository.ScheduleRepository
import javax.inject.Inject
class DeleteScheduleUseCase @Inject constructor(
private val repository: ScheduleRepository
) {
suspend operator fun invoke(id: String): Result<Unit> {
return try {
repository.deleteSchedule(id)
Result.success(Unit)
} catch (e: Exception) {
Result.failure(e)
}
}
}

View File

@@ -0,0 +1,13 @@
package com.novayaplaneta.domain.usecase
import com.novayaplaneta.domain.repository.TaskRepository
import javax.inject.Inject
class DeleteTaskUseCase @Inject constructor(
private val repository: TaskRepository
) {
suspend operator fun invoke(id: String): Result<Unit> {
return repository.deleteTask(id)
}
}

View File

@@ -0,0 +1,19 @@
package com.novayaplaneta.domain.usecase
import com.novayaplaneta.domain.repository.AIRepository
import com.novayaplaneta.domain.repository.GenerateScheduleResult
import javax.inject.Inject
class GenerateScheduleUseCase @Inject constructor(
private val aiRepository: AIRepository
) {
suspend operator fun invoke(
childAge: Int,
preferences: List<String>,
date: String,
description: String
): Result<GenerateScheduleResult> {
return aiRepository.generateSchedule(childAge, preferences, date, description)
}
}

View File

@@ -0,0 +1,23 @@
package com.novayaplaneta.domain.usecase
import com.novayaplaneta.domain.model.Schedule
import com.novayaplaneta.domain.repository.ScheduleRepository
import javax.inject.Inject
class GetScheduleByIdUseCase @Inject constructor(
private val repository: ScheduleRepository
) {
suspend operator fun invoke(id: String): Result<Schedule> {
return try {
val schedule = repository.getScheduleById(id)
if (schedule != null) {
Result.success(schedule)
} else {
Result.failure(Exception("Schedule not found"))
}
} catch (e: Exception) {
Result.failure(e)
}
}
}

View File

@@ -11,5 +11,14 @@ class GetSchedulesUseCase @Inject constructor(
operator fun invoke(userId: String): Flow<List<Schedule>> {
return repository.getSchedules(userId)
}
suspend fun loadSchedules(scheduleDate: String? = null): Result<Unit> {
return try {
repository.loadSchedules(scheduleDate)
Result.success(Unit)
} catch (e: Exception) {
Result.failure(e)
}
}
}

View File

@@ -0,0 +1,14 @@
package com.novayaplaneta.domain.usecase
import com.novayaplaneta.domain.model.Task
import com.novayaplaneta.domain.repository.TaskRepository
import javax.inject.Inject
class GetTaskByIdUseCase @Inject constructor(
private val repository: TaskRepository
) {
suspend operator fun invoke(id: String): Result<Task> {
return repository.getTaskById(id)
}
}

View File

@@ -0,0 +1,23 @@
package com.novayaplaneta.domain.usecase
import com.novayaplaneta.domain.model.Task
import com.novayaplaneta.domain.repository.TaskRepository
import kotlinx.coroutines.flow.Flow
import javax.inject.Inject
class GetTasksUseCase @Inject constructor(
private val repository: TaskRepository
) {
operator fun invoke(scheduleId: String): Flow<List<Task>> {
return repository.getTasks(scheduleId)
}
suspend fun loadTasks(scheduleId: String): Result<Unit> {
return try {
repository.loadTasks(scheduleId)
} catch (e: Exception) {
Result.failure(e)
}
}
}

View File

@@ -1,13 +1,14 @@
package com.novayaplaneta.domain.usecase
import com.novayaplaneta.domain.repository.AIRepository
import com.novayaplaneta.domain.repository.ChatResponse
import javax.inject.Inject
class SendAIMessageUseCase @Inject constructor(
private val repository: AIRepository
) {
suspend operator fun invoke(userId: String, message: String): Result<String> {
return repository.sendMessage(userId, message)
suspend operator fun invoke(userId: String, message: String, conversationId: String? = null): Result<ChatResponse> {
return repository.sendMessage(userId, message, conversationId)
}
}

View File

@@ -7,6 +7,9 @@ import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.SnackbarDuration
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
@@ -50,10 +53,17 @@ fun AIScreen(
) {
val uiState by viewModel.uiState.collectAsState()
var messageText by remember { mutableStateOf("") }
val snackbarHostState = remember { SnackbarHostState() }
// Загружаем историю чата при первом открытии экрана
LaunchedEffect(Unit) {
viewModel.loadChatHistory("default")
// Показываем ошибки через Snackbar
LaunchedEffect(uiState.error) {
uiState.error?.let { error ->
snackbarHostState.showSnackbar(
message = error,
duration = SnackbarDuration.Long
)
viewModel.clearError()
}
}
val configuration = LocalConfiguration.current
@@ -235,7 +245,7 @@ fun AIScreen(
Button(
onClick = {
if (messageText.isNotBlank()) {
viewModel.sendMessage("default", messageText)
viewModel.sendMessage(messageText)
messageText = ""
}
},
@@ -259,6 +269,12 @@ fun AIScreen(
}
}
}
// Snackbar для отображения ошибок
SnackbarHost(
hostState = snackbarHostState,
modifier = Modifier.align(Alignment.BottomCenter)
)
}
}

View File

@@ -7,42 +7,82 @@ import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import javax.inject.Inject
@HiltViewModel
class AIViewModel @Inject constructor(
private val aiRepository: AIRepository
private val aiRepository: AIRepository,
private val authRepository: com.novayaplaneta.domain.repository.AuthRepository
) : ViewModel() {
private val _uiState = MutableStateFlow(AIUiState())
val uiState: StateFlow<AIUiState> = _uiState.asStateFlow()
fun loadChatHistory(userId: String) {
private var conversationId: String? = null
private var currentUserId: String? = null
init {
loadUserId()
}
private fun loadUserId() {
viewModelScope.launch {
aiRepository.getChatHistory(userId).collect { messages ->
_uiState.value = _uiState.value.copy(messages = messages)
try {
val user = authRepository.getCurrentUser().first()
currentUserId = user?.id
if (currentUserId != null) {
loadChatHistory(currentUserId!!)
}
} catch (e: Exception) {
_uiState.value = _uiState.value.copy(
error = e.message ?: "Ошибка загрузки пользователя"
)
}
}
}
fun sendMessage(userId: String, message: String) {
fun loadChatHistory(userId: String? = null) {
val targetUserId = userId ?: currentUserId ?: return
viewModelScope.launch {
_uiState.value = _uiState.value.copy(isLoading = true)
aiRepository.sendMessage(userId, message).fold(
try {
val messages = aiRepository.getChatHistory(targetUserId).first()
_uiState.value = _uiState.value.copy(messages = messages)
} catch (e: Exception) {
_uiState.value = _uiState.value.copy(
error = e.message ?: "Ошибка загрузки истории чата"
)
}
}
}
fun sendMessage(message: String) {
val userId = currentUserId ?: return
viewModelScope.launch {
_uiState.value = _uiState.value.copy(isLoading = true, error = null)
aiRepository.sendMessage(userId, message, conversationId).fold(
onSuccess = { response ->
// Сохраняем conversation_id для следующих сообщений
conversationId = response.conversationId
// Перезагружаем историю чата
loadChatHistory(userId)
_uiState.value = _uiState.value.copy(isLoading = false)
},
onFailure = { error ->
_uiState.value = _uiState.value.copy(
isLoading = false,
error = error.message
error = error.message ?: "Ошибка отправки сообщения"
)
}
)
}
}
fun clearError() {
_uiState.value = _uiState.value.copy(error = null)
}
}
data class AIUiState(

View File

@@ -0,0 +1,154 @@
package com.novayaplaneta.ui.screens.schedule
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Dialog
@Composable
fun GenerateScheduleDialog(
childAge: Int,
preferences: List<String>,
date: String,
description: String,
onChildAgeChange: (Int) -> Unit,
onPreferencesChange: (List<String>) -> Unit,
onDateChange: (String) -> Unit,
onDescriptionChange: (String) -> Unit,
onGenerate: () -> Unit,
onDismiss: () -> Unit,
accentGreen: Color,
screenHeightDp: Int,
isLoading: Boolean
) {
Dialog(onDismissRequest = onDismiss) {
Card(
modifier = Modifier
.fillMaxWidth(0.9f)
.wrapContentHeight(),
shape = RoundedCornerShape(24.dp),
colors = CardDefaults.cardColors(
containerColor = Color.White
)
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(24.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
// Заголовок
val titleSize = (screenHeightDp * 0.03f).toInt().coerceIn(24, 36).sp
Text(
text = "Сгенерировать расписание через ИИ",
fontSize = titleSize,
fontWeight = FontWeight.Bold,
color = Color.Black
)
// Поле возраста ребенка
OutlinedTextField(
value = childAge.toString(),
onValueChange = {
it.toIntOrNull()?.let { age ->
if (age > 0 && age <= 18) onChildAgeChange(age)
}
},
label = { Text("Возраст ребенка") },
modifier = Modifier.fillMaxWidth(),
enabled = !isLoading,
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number)
)
// Поле даты
OutlinedTextField(
value = date,
onValueChange = onDateChange,
label = { Text("Дата (yyyy-MM-dd)") },
modifier = Modifier.fillMaxWidth(),
enabled = !isLoading,
placeholder = { Text("2025-12-25") }
)
// Поле описания/предпочтений
OutlinedTextField(
value = description,
onValueChange = onDescriptionChange,
label = { Text("Описание/предпочтения") },
modifier = Modifier.fillMaxWidth(),
maxLines = 3,
enabled = !isLoading,
placeholder = { Text("Опишите предпочтения ребенка...") }
)
// Кнопки внизу
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(16.dp)
) {
// Кнопка "Отмена"
Button(
onClick = onDismiss,
modifier = Modifier
.weight(1f)
.height(56.dp),
shape = RoundedCornerShape(16.dp),
colors = ButtonDefaults.buttonColors(
containerColor = Color.LightGray,
contentColor = Color.Black
),
enabled = !isLoading
) {
val buttonTextSize = (screenHeightDp * 0.022f).toInt().coerceIn(18, 26).sp
Text(
text = "Отмена",
fontSize = buttonTextSize,
fontWeight = FontWeight.Bold
)
}
// Кнопка "Сгенерировать"
Button(
onClick = onGenerate,
modifier = Modifier
.weight(1f)
.height(56.dp),
enabled = date.isNotBlank() && !isLoading,
shape = RoundedCornerShape(16.dp),
colors = ButtonDefaults.buttonColors(
containerColor = accentGreen,
contentColor = Color.White,
disabledContainerColor = Color.LightGray,
disabledContentColor = Color.Gray
)
) {
if (isLoading) {
CircularProgressIndicator(
modifier = Modifier.size(24.dp),
color = Color.White
)
} else {
val buttonTextSize = (screenHeightDp * 0.022f).toInt().coerceIn(18, 26).sp
Text(
text = "Сгенерировать",
fontSize = buttonTextSize,
fontWeight = FontWeight.Bold
)
}
}
}
}
}
}
}

View File

@@ -12,6 +12,8 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.CalendarToday
import androidx.compose.material.icons.filled.SmartToy
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.Person
import androidx.compose.material.icons.filled.Public
import androidx.compose.material.icons.filled.Star
@@ -171,28 +173,96 @@ fun ScheduleScreen(
Spacer(modifier = Modifier.height(48.dp))
// Основной контент: только сегодняшняя дата (опущена ниже)
DateSection(
date = dateOnly,
// Основной контент: список расписаний
if (uiState.isLoading) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator(color = accentGreen)
}
} else if (uiState.errorMessage != null) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
Text(
text = "Ошибка",
fontSize = 24.sp,
fontWeight = FontWeight.Bold,
color = Color.Red
)
Text(
text = uiState.errorMessage ?: "Неизвестная ошибка",
fontSize = 18.sp,
color = Color.Black
)
Button(
onClick = { viewModel.loadSchedules() },
colors = ButtonDefaults.buttonColors(containerColor = accentGreen)
) {
Text("Повторить")
}
}
}
} else {
SchedulesListSection(
schedules = uiState.schedules,
dateOnly = dateOnly,
dayOfWeek = dayOfWeek,
dateCardColor = dateCardColor,
accentGreen = accentGreen,
screenHeightDp = screenHeightDp,
tasks = uiState.tasks,
onAddClick = { viewModel.showAddDialog() }
onAddClick = { viewModel.showAddDialog() },
onGenerateClick = { viewModel.showGenerateDialog() },
onScheduleClick = { scheduleId ->
// TODO: Navigate to schedule details
},
onDeleteClick = { scheduleId ->
viewModel.deleteSchedule(scheduleId)
}
)
}
}
}
// Диалог выбора задачи
// Диалог создания расписания
if (uiState.showAddDialog) {
AddTaskDialog(
selectedTaskType = uiState.selectedTaskType,
onTaskTypeSelected = { viewModel.selectTaskType(it) },
onSelect = { viewModel.addTask() },
CreateScheduleDialog(
title = uiState.newScheduleTitle,
description = uiState.newScheduleDescription,
date = uiState.newScheduleDate,
onTitleChange = { viewModel.updateNewScheduleTitle(it) },
onDescriptionChange = { viewModel.updateNewScheduleDescription(it) },
onDateChange = { viewModel.updateNewScheduleDate(it) },
onCreate = { viewModel.createScheduleFromDialog() },
onDismiss = { viewModel.hideAddDialog() },
accentGreen = accentGreen,
screenHeightDp = screenHeightDp
screenHeightDp = screenHeightDp,
isLoading = uiState.isLoading
)
}
// Диалог генерации расписания через ИИ
if (uiState.showGenerateDialog) {
GenerateScheduleDialog(
childAge = uiState.generateChildAge,
preferences = uiState.generatePreferences,
date = uiState.generateDate,
description = uiState.generateDescription,
onChildAgeChange = { viewModel.updateGenerateChildAge(it) },
onPreferencesChange = { viewModel.updateGeneratePreferences(it) },
onDateChange = { viewModel.updateGenerateDate(it) },
onDescriptionChange = { viewModel.updateGenerateDescription(it) },
onGenerate = { viewModel.generateScheduleWithAI() },
onDismiss = { viewModel.hideGenerateDialog() },
accentGreen = accentGreen,
screenHeightDp = screenHeightDp,
isLoading = uiState.isLoading
)
}
}
@@ -236,19 +306,22 @@ fun NavItem(
}
@Composable
fun DateSection(
date: String,
fun SchedulesListSection(
schedules: List<com.novayaplaneta.domain.model.Schedule>,
dateOnly: String,
dayOfWeek: String,
dateCardColor: Color,
accentGreen: Color,
screenHeightDp: Int,
tasks: List<TaskType>,
onAddClick: () -> Unit
onAddClick: () -> Unit,
onGenerateClick: () -> Unit,
onScheduleClick: (String) -> Unit,
onDeleteClick: (String) -> Unit
) {
Column(
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
// Дата и кнопка + в одной строке
// Дата и кнопки в одной строке
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(16.dp),
@@ -267,7 +340,7 @@ fun DateSection(
horizontalAlignment = Alignment.Start
) {
Text(
text = date,
text = dateOnly,
fontSize = dateTextSize,
fontWeight = FontWeight.Bold,
color = Color.Black
@@ -281,6 +354,20 @@ fun DateSection(
}
}
// Кнопка генерации через ИИ (синяя круглая)
FloatingActionButton(
onClick = onGenerateClick,
modifier = Modifier.size(80.dp),
containerColor = Color(0xFF2196F3),
contentColor = Color.White
) {
Icon(
imageVector = Icons.Default.SmartToy,
contentDescription = "Сгенерировать через ИИ",
modifier = Modifier.size(40.dp)
)
}
// Кнопка добавления (зеленая круглая с плюсом)
FloatingActionButton(
onClick = onAddClick,
@@ -296,65 +383,106 @@ fun DateSection(
}
}
// Задачи в ряд
if (tasks.isNotEmpty()) {
LazyRow(
horizontalArrangement = Arrangement.spacedBy(16.dp),
modifier = Modifier.fillMaxWidth()
) {
items(tasks) { task ->
TaskCard(
taskType = task,
screenHeightDp = screenHeightDp
)
}
}
}
}
}
@Composable
fun TaskCard(
taskType: TaskType,
screenHeightDp: Int
) {
val cardWidth = 200.dp
val cardHeight = 180.dp
Box(
modifier = Modifier
.width(cardWidth)
.height(cardHeight)
.clip(RoundedCornerShape(16.dp))
.background(color = Color.LightGray)
) {
Column(
modifier = Modifier.fillMaxSize()
) {
// Верхняя часть (для изображения)
// Список расписаний
if (schedules.isEmpty()) {
Box(
modifier = Modifier
.fillMaxWidth()
.weight(0.6f)
.background(color = Color.White.copy(alpha = 0.5f))
)
// Нижняя часть (для текста)
Box(
modifier = Modifier
.fillMaxWidth()
.weight(0.4f)
.background(color = Color.LightGray)
.padding(12.dp),
.padding(vertical = 48.dp),
contentAlignment = Alignment.Center
) {
val textSize = (screenHeightDp * 0.02f).toInt().coerceIn(16, 24).sp
Text(
text = taskType.title,
fontSize = textSize,
text = "Нет расписаний",
fontSize = 20.sp,
color = Color.Gray
)
}
} else {
LazyColumn(
verticalArrangement = Arrangement.spacedBy(16.dp),
modifier = Modifier.fillMaxWidth()
) {
items(schedules) { schedule ->
ScheduleCard(
schedule = schedule,
screenHeightDp = screenHeightDp,
accentGreen = accentGreen,
dateCardColor = dateCardColor,
onClick = { onScheduleClick(schedule.id) },
onDeleteClick = { onDeleteClick(schedule.id) }
)
}
}
}
}
}
@Composable
fun ScheduleCard(
schedule: com.novayaplaneta.domain.model.Schedule,
screenHeightDp: Int,
accentGreen: Color,
dateCardColor: Color,
onClick: () -> Unit,
onDeleteClick: () -> Unit
) {
val dateFormatter = java.time.format.DateTimeFormatter.ofPattern("dd.MM.yyyy")
val scheduleDate = schedule.date.toLocalDate().format(dateFormatter)
Card(
modifier = Modifier
.fillMaxWidth()
.clickable { onClick() },
shape = RoundedCornerShape(16.dp),
colors = CardDefaults.cardColors(containerColor = dateCardColor)
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Column(modifier = Modifier.weight(1f)) {
Text(
text = schedule.title,
fontSize = 20.sp,
fontWeight = FontWeight.Bold,
color = Color.Black,
textAlign = TextAlign.Center
color = Color.Black
)
Text(
text = scheduleDate,
fontSize = 16.sp,
color = accentGreen,
fontWeight = FontWeight.Medium
)
}
IconButton(onClick = onDeleteClick) {
Icon(
imageVector = Icons.Default.Delete,
contentDescription = "Удалить",
tint = Color.Red
)
}
}
if (!schedule.description.isNullOrBlank()) {
Text(
text = schedule.description ?: "",
fontSize = 16.sp,
color = Color.Black
)
}
if (schedule.tasks.isNotEmpty()) {
Text(
text = "Задач: ${schedule.tasks.size}",
fontSize = 14.sp,
color = Color.Gray
)
}
}
@@ -362,18 +490,23 @@ fun TaskCard(
}
@Composable
fun AddTaskDialog(
selectedTaskType: TaskType?,
onTaskTypeSelected: (TaskType) -> Unit,
onSelect: () -> Unit,
fun CreateScheduleDialog(
title: String,
description: String,
date: java.time.LocalDate?,
onTitleChange: (String) -> Unit,
onDescriptionChange: (String) -> Unit,
onDateChange: (java.time.LocalDate) -> Unit,
onCreate: () -> Unit,
onDismiss: () -> Unit,
accentGreen: Color,
screenHeightDp: Int
screenHeightDp: Int,
isLoading: Boolean
) {
Dialog(onDismissRequest = onDismiss) {
Card(
modifier = Modifier
.fillMaxWidth(0.8f)
.fillMaxWidth(0.9f)
.wrapContentHeight(),
shape = RoundedCornerShape(24.dp),
colors = CardDefaults.cardColors(
@@ -385,47 +518,42 @@ fun AddTaskDialog(
.fillMaxWidth()
.padding(24.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(24.dp)
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
// Заголовок
val titleSize = (screenHeightDp * 0.03f).toInt().coerceIn(24, 36).sp
Text(
text = "Выберите задачу",
text = "Создать расписание",
fontSize = titleSize,
fontWeight = FontWeight.Bold,
color = Color.Black
)
// Опции выбора
Column(
verticalArrangement = Arrangement.spacedBy(16.dp),
modifier = Modifier.fillMaxWidth()
) {
// Подарок
TaskOption(
taskType = TaskType.Gift,
isSelected = selectedTaskType == TaskType.Gift,
onClick = { onTaskTypeSelected(TaskType.Gift) },
accentGreen = accentGreen,
screenHeightDp = screenHeightDp
// Поле названия
OutlinedTextField(
value = title,
onValueChange = onTitleChange,
label = { Text("Название") },
modifier = Modifier.fillMaxWidth(),
enabled = !isLoading
)
// Кушать ложкой
TaskOption(
taskType = TaskType.EatWithSpoon,
isSelected = selectedTaskType == TaskType.EatWithSpoon,
onClick = { onTaskTypeSelected(TaskType.EatWithSpoon) },
accentGreen = accentGreen,
screenHeightDp = screenHeightDp
// Поле описания
OutlinedTextField(
value = description,
onValueChange = onDescriptionChange,
label = { Text("Описание") },
modifier = Modifier.fillMaxWidth(),
maxLines = 3,
enabled = !isLoading
)
}
// Кнопки внизу
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(16.dp)
) {
// Кнопка "Назад"
// Кнопка "Отмена"
Button(
onClick = onDismiss,
modifier = Modifier
@@ -435,23 +563,24 @@ fun AddTaskDialog(
colors = ButtonDefaults.buttonColors(
containerColor = Color.LightGray,
contentColor = Color.Black
)
),
enabled = !isLoading
) {
val buttonTextSize = (screenHeightDp * 0.022f).toInt().coerceIn(18, 26).sp
Text(
text = "Назад",
text = "Отмена",
fontSize = buttonTextSize,
fontWeight = FontWeight.Bold
)
}
// Кнопка "Выбрать"
// Кнопка "Создать"
Button(
onClick = onSelect,
onClick = onCreate,
modifier = Modifier
.weight(1f)
.height(56.dp),
enabled = selectedTaskType != null,
enabled = title.isNotBlank() && !isLoading,
shape = RoundedCornerShape(16.dp),
colors = ButtonDefaults.buttonColors(
containerColor = accentGreen,
@@ -460,9 +589,15 @@ fun AddTaskDialog(
disabledContentColor = Color.Gray
)
) {
if (isLoading) {
CircularProgressIndicator(
modifier = Modifier.size(24.dp),
color = Color.White
)
} else {
val buttonTextSize = (screenHeightDp * 0.022f).toInt().coerceIn(18, 26).sp
Text(
text = "Выбрать",
text = "Создать",
fontSize = buttonTextSize,
fontWeight = FontWeight.Bold
)
@@ -471,36 +606,7 @@ fun AddTaskDialog(
}
}
}
}
@Composable
fun TaskOption(
taskType: TaskType,
isSelected: Boolean,
onClick: () -> Unit,
accentGreen: Color,
screenHeightDp: Int
) {
val borderWidth = if (isSelected) 4.dp else 2.dp
val borderColor = if (isSelected) accentGreen else Color.Gray
Box(
modifier = Modifier
.fillMaxWidth()
.height(120.dp)
.clip(RoundedCornerShape(16.dp))
.border(borderWidth, borderColor, RoundedCornerShape(16.dp))
.background(color = if (isSelected) accentGreen.copy(alpha = 0.1f) else Color.Transparent)
.clickable { onClick() },
contentAlignment = Alignment.Center
) {
val textSize = (screenHeightDp * 0.025f).toInt().coerceIn(20, 28).sp
Text(
text = taskType.title,
fontSize = textSize,
fontWeight = FontWeight.Bold,
color = if (isSelected) accentGreen else Color.Black
)
}
}

View File

@@ -2,12 +2,20 @@ package com.novayaplaneta.ui.screens.schedule
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.novayaplaneta.domain.model.Schedule
import com.novayaplaneta.domain.repository.AuthRepository
import com.novayaplaneta.domain.usecase.CreateScheduleUseCase
import com.novayaplaneta.domain.usecase.DeleteScheduleUseCase
import com.novayaplaneta.domain.usecase.GenerateScheduleUseCase
import com.novayaplaneta.domain.usecase.GetSchedulesUseCase
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import java.time.LocalDate
import java.time.format.DateTimeFormatter
import javax.inject.Inject
sealed class TaskType(val title: String) {
@@ -17,52 +25,247 @@ sealed class TaskType(val title: String) {
@HiltViewModel
class ScheduleViewModel @Inject constructor(
private val getSchedulesUseCase: GetSchedulesUseCase
private val getSchedulesUseCase: GetSchedulesUseCase,
private val createScheduleUseCase: CreateScheduleUseCase,
private val deleteScheduleUseCase: DeleteScheduleUseCase,
private val generateScheduleUseCase: GenerateScheduleUseCase,
private val authRepository: AuthRepository
) : ViewModel() {
private val _uiState = MutableStateFlow(ScheduleUiState())
val uiState: StateFlow<ScheduleUiState> = _uiState.asStateFlow()
fun loadSchedules(userId: String) {
init {
loadSchedules()
}
fun loadSchedules(scheduleDate: String? = null) {
viewModelScope.launch {
_uiState.value = _uiState.value.copy(isLoading = true)
getSchedulesUseCase(userId).collect { schedules ->
_uiState.value = _uiState.value.copy(isLoading = true, errorMessage = null)
try {
// Получаем текущего пользователя
val user = authRepository.getCurrentUser().first()
val userId = user?.id ?: ""
// Загружаем расписания с сервера
val loadResult = getSchedulesUseCase.loadSchedules(scheduleDate)
if (loadResult.isFailure) {
_uiState.value = _uiState.value.copy(
schedules = schedules,
isLoading = false
isLoading = false,
errorMessage = loadResult.exceptionOrNull()?.message ?: "Ошибка загрузки расписаний"
)
return@launch
}
// Получаем актуальный список расписаний из Flow
val schedules = getSchedulesUseCase(userId).first()
// Фильтруем расписания по дате, если указана
val filteredSchedules = if (scheduleDate != null) {
schedules.filter { schedule ->
val scheduleDateStr = schedule.date.toLocalDate().format(DateTimeFormatter.ISO_DATE)
scheduleDateStr == scheduleDate
}
} else {
schedules
}
_uiState.value = _uiState.value.copy(
schedules = filteredSchedules,
isLoading = false,
errorMessage = null
)
} catch (e: Exception) {
_uiState.value = _uiState.value.copy(
isLoading = false,
errorMessage = e.message ?: "Ошибка загрузки расписаний"
)
}
}
}
fun showAddDialog() {
_uiState.value = _uiState.value.copy(showAddDialog = true, selectedTaskType = null)
_uiState.value = _uiState.value.copy(
showAddDialog = true,
newScheduleTitle = "",
newScheduleDescription = "",
newScheduleDate = LocalDate.now()
)
}
fun hideAddDialog() {
_uiState.value = _uiState.value.copy(showAddDialog = false, selectedTaskType = null)
}
fun selectTaskType(taskType: TaskType) {
_uiState.value = _uiState.value.copy(selectedTaskType = taskType)
}
fun addTask() {
val selected = _uiState.value.selectedTaskType ?: return
val newTasks = _uiState.value.tasks + selected
_uiState.value = _uiState.value.copy(
tasks = newTasks,
showAddDialog = false,
selectedTaskType = null
newScheduleTitle = "",
newScheduleDescription = "",
newScheduleDate = null
)
}
fun updateNewScheduleTitle(title: String) {
_uiState.value = _uiState.value.copy(newScheduleTitle = title)
}
fun updateNewScheduleDescription(description: String) {
_uiState.value = _uiState.value.copy(newScheduleDescription = description)
}
fun updateNewScheduleDate(date: LocalDate) {
_uiState.value = _uiState.value.copy(newScheduleDate = date)
}
fun createScheduleFromDialog() {
val state = _uiState.value
if (state.newScheduleTitle.isNotBlank() && state.newScheduleDate != null) {
createSchedule(
title = state.newScheduleTitle,
description = state.newScheduleDescription,
date = state.newScheduleDate
)
}
}
fun createSchedule(title: String, description: String, date: LocalDate) {
viewModelScope.launch {
_uiState.value = _uiState.value.copy(isLoading = true, errorMessage = null)
try {
val user = authRepository.getCurrentUser().first()
val userId = user?.id ?: ""
val schedule = Schedule(
id = "", // Будет присвоен сервером
title = title,
description = description,
tasks = emptyList(),
date = date.atStartOfDay(),
createdAt = java.time.LocalDateTime.now(),
userId = userId
)
val result = createScheduleUseCase(schedule)
result.onSuccess {
_uiState.value = _uiState.value.copy(
isLoading = false,
showAddDialog = false
)
// Перезагружаем расписания
loadSchedules()
}.onFailure { exception ->
_uiState.value = _uiState.value.copy(
isLoading = false,
errorMessage = exception.message ?: "Ошибка создания расписания"
)
}
} catch (e: Exception) {
_uiState.value = _uiState.value.copy(
isLoading = false,
errorMessage = e.message ?: "Ошибка создания расписания"
)
}
}
}
fun deleteSchedule(scheduleId: String) {
viewModelScope.launch {
_uiState.value = _uiState.value.copy(isLoading = true, errorMessage = null)
val result = deleteScheduleUseCase(scheduleId)
result.onSuccess {
_uiState.value = _uiState.value.copy(isLoading = false)
// Перезагружаем расписания
loadSchedules()
}.onFailure { exception ->
_uiState.value = _uiState.value.copy(
isLoading = false,
errorMessage = exception.message ?: "Ошибка удаления расписания"
)
}
}
}
fun showGenerateDialog() {
_uiState.value = _uiState.value.copy(
showGenerateDialog = true,
generateChildAge = 5,
generatePreferences = emptyList(),
generateDate = LocalDate.now().format(DateTimeFormatter.ISO_DATE),
generateDescription = ""
)
}
fun hideGenerateDialog() {
_uiState.value = _uiState.value.copy(
showGenerateDialog = false,
generateChildAge = 5,
generatePreferences = emptyList(),
generateDate = "",
generateDescription = ""
)
}
fun updateGenerateChildAge(age: Int) {
_uiState.value = _uiState.value.copy(generateChildAge = age)
}
fun updateGeneratePreferences(preferences: List<String>) {
_uiState.value = _uiState.value.copy(generatePreferences = preferences)
}
fun updateGenerateDate(date: String) {
_uiState.value = _uiState.value.copy(generateDate = date)
}
fun updateGenerateDescription(description: String) {
_uiState.value = _uiState.value.copy(generateDescription = description)
}
fun generateScheduleWithAI() {
viewModelScope.launch {
val state = _uiState.value
if (state.generateDate.isNotBlank()) {
_uiState.value = _uiState.value.copy(isLoading = true, errorMessage = null)
val result = generateScheduleUseCase(
childAge = state.generateChildAge,
preferences = state.generatePreferences,
date = state.generateDate,
description = state.generateDescription
)
result.onSuccess { generateResult ->
_uiState.value = _uiState.value.copy(
isLoading = false,
showGenerateDialog = false,
generatedScheduleId = generateResult.scheduleId
)
// Перезагружаем расписания, чтобы показать новое
loadSchedules()
}.onFailure { exception ->
_uiState.value = _uiState.value.copy(
isLoading = false,
errorMessage = exception.message ?: "Ошибка генерации расписания"
)
}
}
}
}
}
data class ScheduleUiState(
val schedules: List<com.novayaplaneta.domain.model.Schedule> = emptyList(),
val tasks: List<TaskType> = emptyList(),
val schedules: List<Schedule> = emptyList(),
val isLoading: Boolean = false,
val errorMessage: String? = null,
val showAddDialog: Boolean = false,
val selectedTaskType: TaskType? = null
val newScheduleTitle: String = "",
val newScheduleDescription: String = "",
val newScheduleDate: LocalDate? = null,
val showGenerateDialog: Boolean = false,
val generateChildAge: Int = 5,
val generatePreferences: List<String> = emptyList(),
val generateDate: String = "",
val generateDescription: String = "",
val generatedScheduleId: String? = null
)

View File

@@ -3,26 +3,48 @@ package com.novayaplaneta.ui.screens.task
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavController
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun TaskScreen(
scheduleId: String? = null,
navController: NavController? = null,
viewModel: TaskViewModel = hiltViewModel(),
modifier: Modifier = Modifier
) {
val uiState by viewModel.uiState.collectAsState()
// Загружаем задачи при открытии экрана
LaunchedEffect(scheduleId) {
scheduleId?.let {
viewModel.loadTasks(it)
}
}
Scaffold(
topBar = {
TopAppBar(
title = { Text("Задания") }
title = { Text("Задания") },
actions = {
if (scheduleId != null) {
IconButton(onClick = { /* TODO: Show create task dialog */ }) {
Icon(Icons.Default.Add, contentDescription = "Добавить задачу")
}
}
}
)
}
) { paddingValues ->
@@ -32,12 +54,38 @@ fun TaskScreen(
.padding(paddingValues)
.padding(16.dp)
) {
if (uiState.isLoading) {
CircularProgressIndicator(
// Отображение ошибок
uiState.errorMessage?.let { error ->
Card(
modifier = Modifier
.fillMaxWidth()
.wrapContentWidth()
.padding(bottom = 16.dp),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.errorContainer
)
) {
Text(
text = error,
modifier = Modifier.padding(16.dp),
color = MaterialTheme.colorScheme.onErrorContainer
)
}
}
if (uiState.isLoading && uiState.tasks.isEmpty()) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator()
}
} else if (uiState.tasks.isEmpty() && !uiState.isLoading) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Text("Нет задач")
}
} else {
LazyColumn(
verticalArrangement = Arrangement.spacedBy(8.dp)
@@ -50,7 +98,8 @@ fun TaskScreen(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
horizontalArrangement = Arrangement.SpaceBetween
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Column(modifier = Modifier.weight(1f)) {
Text(
@@ -63,11 +112,28 @@ fun TaskScreen(
style = MaterialTheme.typography.bodySmall
)
}
task.duration?.let {
Text(
text = "Длительность: $it мин",
style = MaterialTheme.typography.bodySmall
)
}
}
Row(verticalAlignment = Alignment.CenterVertically) {
Checkbox(
checked = task.completed,
onCheckedChange = { viewModel.completeTask(task.id) }
onCheckedChange = { completed ->
viewModel.completeTask(task.id, completed)
}
)
IconButton(onClick = { viewModel.deleteTask(task.id) }) {
Icon(
Icons.Default.Delete,
contentDescription = "Удалить",
tint = MaterialTheme.colorScheme.error
)
}
}
}
}
}

View File

@@ -2,43 +2,158 @@ package com.novayaplaneta.ui.screens.task
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.novayaplaneta.domain.repository.TaskRepository
import com.novayaplaneta.domain.model.Task
import com.novayaplaneta.domain.usecase.CompleteTaskUseCase
import com.novayaplaneta.domain.usecase.CreateTaskUseCase
import com.novayaplaneta.domain.usecase.DeleteTaskUseCase
import com.novayaplaneta.domain.usecase.GetTaskByIdUseCase
import com.novayaplaneta.domain.usecase.GetTasksUseCase
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import javax.inject.Inject
@HiltViewModel
class TaskViewModel @Inject constructor(
private val taskRepository: TaskRepository
private val getTasksUseCase: GetTasksUseCase,
private val getTaskByIdUseCase: GetTaskByIdUseCase,
private val createTaskUseCase: CreateTaskUseCase,
private val deleteTaskUseCase: DeleteTaskUseCase,
private val completeTaskUseCase: CompleteTaskUseCase
) : ViewModel() {
private val _uiState = MutableStateFlow(TaskUiState())
val uiState: StateFlow<TaskUiState> = _uiState.asStateFlow()
private var currentScheduleId: String? = null
fun loadTasks(scheduleId: String) {
currentScheduleId = scheduleId
viewModelScope.launch {
_uiState.value = _uiState.value.copy(isLoading = true)
taskRepository.getTasks(scheduleId).collect { tasks ->
_uiState.value = _uiState.value.copy(isLoading = true, errorMessage = null)
try {
val loadResult = getTasksUseCase.loadTasks(scheduleId)
if (loadResult.isFailure) {
_uiState.value = _uiState.value.copy(
tasks = tasks,
isLoading = false
isLoading = false,
errorMessage = loadResult.exceptionOrNull()?.message ?: "Ошибка загрузки задач"
)
return@launch
}
// Подписываемся на Flow для обновлений
getTasksUseCase(scheduleId).collect { tasks ->
_uiState.value = _uiState.value.copy(
tasks = tasks.sortedBy { it.order },
isLoading = false,
errorMessage = null
)
}
} catch (e: Exception) {
_uiState.value = _uiState.value.copy(
isLoading = false,
errorMessage = e.message ?: "Ошибка загрузки задач"
)
}
}
}
fun completeTask(taskId: String) {
fun loadTaskById(taskId: String) {
viewModelScope.launch {
taskRepository.completeTask(taskId)
_uiState.value = _uiState.value.copy(isLoading = true, errorMessage = null)
val result = getTaskByIdUseCase(taskId)
result.onSuccess { task ->
_uiState.value = _uiState.value.copy(
selectedTask = task,
isLoading = false,
errorMessage = null
)
}.onFailure { exception ->
_uiState.value = _uiState.value.copy(
isLoading = false,
errorMessage = exception.message ?: "Ошибка загрузки задачи"
)
}
}
}
fun createTask(task: Task) {
viewModelScope.launch {
_uiState.value = _uiState.value.copy(isLoading = true, errorMessage = null)
val result = createTaskUseCase(task)
result.onSuccess {
_uiState.value = _uiState.value.copy(
isLoading = false,
errorMessage = null
)
// Список задач обновится автоматически через Flow
}.onFailure { exception ->
_uiState.value = _uiState.value.copy(
isLoading = false,
errorMessage = exception.message ?: "Ошибка создания задачи"
)
}
}
}
fun deleteTask(taskId: String) {
viewModelScope.launch {
_uiState.value = _uiState.value.copy(isLoading = true, errorMessage = null)
val result = deleteTaskUseCase(taskId)
result.onSuccess {
_uiState.value = _uiState.value.copy(isLoading = false)
// Список задач обновится автоматически через Flow
}.onFailure { exception ->
_uiState.value = _uiState.value.copy(
isLoading = false,
errorMessage = exception.message ?: "Ошибка удаления задачи"
)
}
}
}
fun completeTask(taskId: String, completed: Boolean) {
viewModelScope.launch {
// Оптимистичное обновление UI
val currentTasks = _uiState.value.tasks.toMutableList()
val taskIndex = currentTasks.indexOfFirst { it.id == taskId }
if (taskIndex >= 0) {
val oldTask = currentTasks[taskIndex]
currentTasks[taskIndex] = oldTask.copy(completed = completed)
_uiState.value = _uiState.value.copy(tasks = currentTasks.sortedBy { it.order })
}
val result = completeTaskUseCase(taskId, completed)
result.onFailure { exception ->
// Откатываем изменение в случае ошибки
if (taskIndex >= 0) {
val oldTask = currentTasks[taskIndex]
currentTasks[taskIndex] = oldTask.copy(completed = !completed)
_uiState.value = _uiState.value.copy(
tasks = currentTasks.sortedBy { it.order },
errorMessage = exception.message ?: "Ошибка обновления задачи"
)
}
}
}
}
fun clearError() {
_uiState.value = _uiState.value.copy(errorMessage = null)
}
}
data class TaskUiState(
val tasks: List<com.novayaplaneta.domain.model.Task> = emptyList(),
val isLoading: Boolean = false
val tasks: List<Task> = emptyList(),
val selectedTask: Task? = null,
val isLoading: Boolean = false,
val errorMessage: String? = null
)