Добавил логику работы с ИИ-агентом и работу с расписанием
This commit is contained in:
@@ -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
|
||||
)
|
||||
|
||||
|
||||
@@ -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()
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
22
app/src/main/java/com/novayaplaneta/data/remote/AiApi.kt
Normal file
22
app/src/main/java/com/novayaplaneta/data/remote/AiApi.kt
Normal 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>
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class ChatRequest(
|
||||
val message: String
|
||||
val message: String,
|
||||
val conversation_id: String? = null
|
||||
)
|
||||
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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 ?: ""
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
|
||||
@@ -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>>
|
||||
)
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user