Добавил логику работы с ИИ-агентом и работу с расписанием
This commit is contained in:
@@ -13,6 +13,10 @@ data class TaskEntity(
|
|||||||
val completed: Boolean = false,
|
val completed: Boolean = false,
|
||||||
val scheduledTime: Long?, // timestamp
|
val scheduledTime: Long?, // timestamp
|
||||||
val duration: Int? = null,
|
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())
|
LocalDateTime.ofInstant(Instant.ofEpochMilli(it), ZoneId.systemDefault())
|
||||||
},
|
},
|
||||||
duration = duration,
|
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,
|
completed = completed,
|
||||||
scheduledTime = scheduledTime?.atZone(ZoneId.systemDefault())?.toInstant()?.toEpochMilli(),
|
scheduledTime = scheduledTime?.atZone(ZoneId.systemDefault())?.toInstant()?.toEpochMilli(),
|
||||||
duration = duration,
|
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.ChatRequest
|
||||||
import com.novayaplaneta.data.remote.dto.ChatResponse
|
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.LoginRequest
|
||||||
import com.novayaplaneta.data.remote.dto.LoginResponse
|
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.Response
|
||||||
import retrofit2.http.Body
|
import retrofit2.http.Body
|
||||||
|
import retrofit2.http.DELETE
|
||||||
import retrofit2.http.GET
|
import retrofit2.http.GET
|
||||||
import retrofit2.http.Header
|
import retrofit2.http.Header
|
||||||
|
import retrofit2.http.PATCH
|
||||||
import retrofit2.http.POST
|
import retrofit2.http.POST
|
||||||
|
import retrofit2.http.Path
|
||||||
|
import retrofit2.http.Query
|
||||||
|
|
||||||
interface BackendApi {
|
interface BackendApi {
|
||||||
@POST("api/v1/auth/login")
|
@POST("api/v1/auth/login")
|
||||||
@@ -21,5 +30,54 @@ interface BackendApi {
|
|||||||
@Header("Authorization") token: String,
|
@Header("Authorization") token: String,
|
||||||
@Body request: ChatRequest
|
@Body request: ChatRequest
|
||||||
): Response<ChatResponse>
|
): 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
|
@Serializable
|
||||||
data class ChatRequest(
|
data class ChatRequest(
|
||||||
val message: String
|
val message: String,
|
||||||
|
val conversation_id: String? = null
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,9 @@ import kotlinx.serialization.Serializable
|
|||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class ChatResponse(
|
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.dao.ChatMessageDao
|
||||||
import com.novayaplaneta.data.local.mapper.toDomain
|
import com.novayaplaneta.data.local.mapper.toDomain
|
||||||
import com.novayaplaneta.data.local.mapper.toEntity
|
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.ChatRequest
|
||||||
|
import com.novayaplaneta.data.remote.dto.GenerateScheduleRequest
|
||||||
import com.novayaplaneta.domain.model.ChatMessage
|
import com.novayaplaneta.domain.model.ChatMessage
|
||||||
import com.novayaplaneta.domain.repository.AIRepository
|
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.Flow
|
||||||
import kotlinx.coroutines.flow.first
|
|
||||||
import kotlinx.coroutines.flow.map
|
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.time.LocalDateTime
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class AIRepositoryImpl @Inject constructor(
|
class AIRepositoryImpl @Inject constructor(
|
||||||
private val chatMessageDao: ChatMessageDao,
|
private val chatMessageDao: ChatMessageDao,
|
||||||
private val api: BackendApi,
|
private val aiApi: AiApi
|
||||||
private val authRepository: com.novayaplaneta.domain.repository.AuthRepository
|
|
||||||
) : AIRepository {
|
) : 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 {
|
return try {
|
||||||
// Save user message
|
// Save user message
|
||||||
val userMessage = ChatMessage(
|
val userMessage = ChatMessage(
|
||||||
@@ -32,22 +36,33 @@ class AIRepositoryImpl @Inject constructor(
|
|||||||
)
|
)
|
||||||
chatMessageDao.insertMessage(userMessage.toEntity())
|
chatMessageDao.insertMessage(userMessage.toEntity())
|
||||||
|
|
||||||
// Тестовый ответ для демонстрации
|
// Send request to API
|
||||||
kotlinx.coroutines.delay(1500) // Небольшая задержка для анимации
|
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
|
// Save AI response
|
||||||
val aiMessage = ChatMessage(
|
val aiMessage = ChatMessage(
|
||||||
id = UUID.randomUUID().toString(),
|
id = UUID.randomUUID().toString(),
|
||||||
message = aiResponse,
|
message = chatResponse.response,
|
||||||
isFromAI = true,
|
isFromAI = true,
|
||||||
timestamp = LocalDateTime.now(),
|
timestamp = LocalDateTime.now(),
|
||||||
userId = userId
|
userId = userId
|
||||||
)
|
)
|
||||||
chatMessageDao.insertMessage(aiMessage.toEntity())
|
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) {
|
} catch (e: Exception) {
|
||||||
Result.failure(e)
|
Result.failure(e)
|
||||||
}
|
}
|
||||||
@@ -59,9 +74,79 @@ class AIRepositoryImpl @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun generateSchedule(userId: String, preferences: String): Result<String> {
|
override suspend fun generateSchedule(
|
||||||
// TODO: Implement schedule generation
|
childAge: Int,
|
||||||
return Result.failure(Exception("Not implemented"))
|
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.dao.TaskDao
|
||||||
import com.novayaplaneta.data.local.mapper.toDomain
|
import com.novayaplaneta.data.local.mapper.toDomain
|
||||||
import com.novayaplaneta.data.local.mapper.toEntity
|
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.model.Schedule
|
||||||
import com.novayaplaneta.domain.repository.ScheduleRepository
|
import com.novayaplaneta.domain.repository.ScheduleRepository
|
||||||
import kotlinx.coroutines.flow.Flow
|
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 kotlinx.coroutines.flow.map
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class ScheduleRepositoryImpl @Inject constructor(
|
class ScheduleRepositoryImpl @Inject constructor(
|
||||||
private val scheduleDao: ScheduleDao,
|
private val scheduleDao: ScheduleDao,
|
||||||
private val taskDao: TaskDao
|
private val taskDao: TaskDao,
|
||||||
|
private val backendApi: BackendApi
|
||||||
) : ScheduleRepository {
|
) : ScheduleRepository {
|
||||||
|
|
||||||
|
private val _schedulesCache = MutableStateFlow<List<Schedule>>(emptyList())
|
||||||
|
val schedulesCache: StateFlow<List<Schedule>> = _schedulesCache.asStateFlow()
|
||||||
|
|
||||||
override fun getSchedules(userId: String): Flow<List<Schedule>> {
|
override fun getSchedules(userId: String): Flow<List<Schedule>> {
|
||||||
return scheduleDao.getSchedulesByUserId(userId).map { schedules ->
|
return _schedulesCache.asStateFlow()
|
||||||
schedules.map { scheduleEntity ->
|
|
||||||
// Note: In production, you'd need to fetch tasks for each schedule
|
|
||||||
scheduleEntity.toDomain(emptyList())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getScheduleById(id: String): Schedule? {
|
override suspend fun getScheduleById(id: String): Schedule? {
|
||||||
val scheduleEntity = scheduleDao.getScheduleById(id) ?: return null
|
return try {
|
||||||
val tasks = taskDao.getTasksByScheduleId(id)
|
val response = backendApi.getScheduleById(id)
|
||||||
// Simplified - would need proper Flow handling
|
if (response.isSuccessful && response.body() != null) {
|
||||||
return scheduleEntity.toDomain(emptyList())
|
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) {
|
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) {
|
override suspend fun updateSchedule(schedule: Schedule) {
|
||||||
|
// TODO: Implement if API supports update
|
||||||
scheduleDao.updateSchedule(schedule.toEntity())
|
scheduleDao.updateSchedule(schedule.toEntity())
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun deleteSchedule(id: String) {
|
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) {
|
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.dao.TaskDao
|
||||||
import com.novayaplaneta.data.local.mapper.toDomain
|
import com.novayaplaneta.data.local.mapper.toDomain
|
||||||
import com.novayaplaneta.data.local.mapper.toEntity
|
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.model.Task
|
||||||
import com.novayaplaneta.domain.repository.TaskRepository
|
import com.novayaplaneta.domain.repository.TaskRepository
|
||||||
import kotlinx.coroutines.flow.Flow
|
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 kotlinx.coroutines.flow.map
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class TaskRepositoryImpl @Inject constructor(
|
class TaskRepositoryImpl @Inject constructor(
|
||||||
private val taskDao: TaskDao
|
private val taskDao: TaskDao,
|
||||||
|
private val backendApi: BackendApi
|
||||||
) : TaskRepository {
|
) : TaskRepository {
|
||||||
|
|
||||||
override fun getTasks(scheduleId: String): Flow<List<Task>> {
|
// Кэш задач по scheduleId
|
||||||
return taskDao.getTasksByScheduleId(scheduleId).map { tasks ->
|
private val _tasksCache = mutableMapOf<String, MutableStateFlow<List<Task>>>()
|
||||||
tasks.map { it.toDomain() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun getTaskById(id: String): Task? {
|
override suspend fun loadTasks(scheduleId: String): Result<Unit> {
|
||||||
return taskDao.getTaskById(id)?.toDomain()
|
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())
|
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) {
|
override suspend fun updateTask(task: Task) {
|
||||||
taskDao.updateTask(task.toEntity())
|
taskDao.updateTask(task.toEntity())
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun deleteTask(id: String) {
|
override suspend fun deleteTask(id: String): Result<Unit> {
|
||||||
taskDao.deleteTask(id)
|
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
|
package com.novayaplaneta.di
|
||||||
|
|
||||||
import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory
|
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.AuthApi
|
||||||
import com.novayaplaneta.data.remote.AuthInterceptor
|
import com.novayaplaneta.data.remote.AuthInterceptor
|
||||||
import com.novayaplaneta.data.remote.BackendApi
|
import com.novayaplaneta.data.remote.BackendApi
|
||||||
@@ -72,5 +73,13 @@ object NetworkModule {
|
|||||||
): BackendApi {
|
): BackendApi {
|
||||||
return retrofit.create(BackendApi::class.java)
|
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 completed: Boolean = false,
|
||||||
val scheduledTime: LocalDateTime?,
|
val scheduledTime: LocalDateTime?,
|
||||||
val duration: Int? = null, // in minutes
|
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
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
interface AIRepository {
|
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>>
|
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 com.novayaplaneta.domain.model.Schedule
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import java.time.LocalDateTime
|
|
||||||
|
|
||||||
interface ScheduleRepository {
|
interface ScheduleRepository {
|
||||||
fun getSchedules(userId: String): Flow<List<Schedule>>
|
fun getSchedules(userId: String): Flow<List<Schedule>>
|
||||||
@@ -11,5 +10,6 @@ interface ScheduleRepository {
|
|||||||
suspend fun updateSchedule(schedule: Schedule)
|
suspend fun updateSchedule(schedule: Schedule)
|
||||||
suspend fun deleteSchedule(id: String)
|
suspend fun deleteSchedule(id: String)
|
||||||
suspend fun syncSchedules(userId: 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
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
interface TaskRepository {
|
interface TaskRepository {
|
||||||
|
suspend fun loadTasks(scheduleId: String): Result<Unit>
|
||||||
fun getTasks(scheduleId: String): Flow<List<Task>>
|
fun getTasks(scheduleId: String): Flow<List<Task>>
|
||||||
suspend fun getTaskById(id: String): Task?
|
suspend fun getTaskById(id: String): Result<Task>
|
||||||
suspend fun createTask(task: Task)
|
suspend fun createTask(task: Task): Result<Task>
|
||||||
suspend fun updateTask(task: Task)
|
suspend fun updateTask(task: Task)
|
||||||
suspend fun deleteTask(id: String)
|
suspend fun deleteTask(id: String): Result<Unit>
|
||||||
suspend fun completeTask(id: String)
|
suspend fun completeTask(id: String, completed: Boolean): Result<Task>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
package com.novayaplaneta.domain.usecase
|
package com.novayaplaneta.domain.usecase
|
||||||
|
|
||||||
|
import com.novayaplaneta.domain.model.Task
|
||||||
import com.novayaplaneta.domain.repository.TaskRepository
|
import com.novayaplaneta.domain.repository.TaskRepository
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class CompleteTaskUseCase @Inject constructor(
|
class CompleteTaskUseCase @Inject constructor(
|
||||||
private val repository: TaskRepository
|
private val repository: TaskRepository
|
||||||
) {
|
) {
|
||||||
suspend operator fun invoke(taskId: String) {
|
suspend operator fun invoke(id: String, completed: Boolean): Result<Task> {
|
||||||
repository.completeTask(taskId)
|
return repository.completeTask(id, completed)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,8 +7,17 @@ import javax.inject.Inject
|
|||||||
class CreateScheduleUseCase @Inject constructor(
|
class CreateScheduleUseCase @Inject constructor(
|
||||||
private val repository: ScheduleRepository
|
private val repository: ScheduleRepository
|
||||||
) {
|
) {
|
||||||
suspend operator fun invoke(schedule: Schedule) {
|
suspend operator fun invoke(schedule: Schedule): Result<Schedule> {
|
||||||
|
return try {
|
||||||
repository.createSchedule(schedule)
|
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>> {
|
operator fun invoke(userId: String): Flow<List<Schedule>> {
|
||||||
return repository.getSchedules(userId)
|
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
|
package com.novayaplaneta.domain.usecase
|
||||||
|
|
||||||
import com.novayaplaneta.domain.repository.AIRepository
|
import com.novayaplaneta.domain.repository.AIRepository
|
||||||
|
import com.novayaplaneta.domain.repository.ChatResponse
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class SendAIMessageUseCase @Inject constructor(
|
class SendAIMessageUseCase @Inject constructor(
|
||||||
private val repository: AIRepository
|
private val repository: AIRepository
|
||||||
) {
|
) {
|
||||||
suspend operator fun invoke(userId: String, message: String): Result<String> {
|
suspend operator fun invoke(userId: String, message: String, conversationId: String? = null): Result<ChatResponse> {
|
||||||
return repository.sendMessage(userId, message)
|
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.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
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.shape.RoundedCornerShape
|
||||||
import androidx.compose.foundation.text.KeyboardOptions
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
@@ -50,10 +53,17 @@ fun AIScreen(
|
|||||||
) {
|
) {
|
||||||
val uiState by viewModel.uiState.collectAsState()
|
val uiState by viewModel.uiState.collectAsState()
|
||||||
var messageText by remember { mutableStateOf("") }
|
var messageText by remember { mutableStateOf("") }
|
||||||
|
val snackbarHostState = remember { SnackbarHostState() }
|
||||||
|
|
||||||
// Загружаем историю чата при первом открытии экрана
|
// Показываем ошибки через Snackbar
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(uiState.error) {
|
||||||
viewModel.loadChatHistory("default")
|
uiState.error?.let { error ->
|
||||||
|
snackbarHostState.showSnackbar(
|
||||||
|
message = error,
|
||||||
|
duration = SnackbarDuration.Long
|
||||||
|
)
|
||||||
|
viewModel.clearError()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val configuration = LocalConfiguration.current
|
val configuration = LocalConfiguration.current
|
||||||
@@ -235,7 +245,7 @@ fun AIScreen(
|
|||||||
Button(
|
Button(
|
||||||
onClick = {
|
onClick = {
|
||||||
if (messageText.isNotBlank()) {
|
if (messageText.isNotBlank()) {
|
||||||
viewModel.sendMessage("default", messageText)
|
viewModel.sendMessage(messageText)
|
||||||
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.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class AIViewModel @Inject constructor(
|
class AIViewModel @Inject constructor(
|
||||||
private val aiRepository: AIRepository
|
private val aiRepository: AIRepository,
|
||||||
|
private val authRepository: com.novayaplaneta.domain.repository.AuthRepository
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
private val _uiState = MutableStateFlow(AIUiState())
|
private val _uiState = MutableStateFlow(AIUiState())
|
||||||
val uiState: StateFlow<AIUiState> = _uiState.asStateFlow()
|
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 {
|
viewModelScope.launch {
|
||||||
aiRepository.getChatHistory(userId).collect { messages ->
|
try {
|
||||||
_uiState.value = _uiState.value.copy(messages = messages)
|
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 {
|
viewModelScope.launch {
|
||||||
_uiState.value = _uiState.value.copy(isLoading = true)
|
try {
|
||||||
aiRepository.sendMessage(userId, message).fold(
|
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 ->
|
onSuccess = { response ->
|
||||||
|
// Сохраняем conversation_id для следующих сообщений
|
||||||
|
conversationId = response.conversationId
|
||||||
|
// Перезагружаем историю чата
|
||||||
loadChatHistory(userId)
|
loadChatHistory(userId)
|
||||||
_uiState.value = _uiState.value.copy(isLoading = false)
|
_uiState.value = _uiState.value.copy(isLoading = false)
|
||||||
},
|
},
|
||||||
onFailure = { error ->
|
onFailure = { error ->
|
||||||
_uiState.value = _uiState.value.copy(
|
_uiState.value = _uiState.value.copy(
|
||||||
isLoading = false,
|
isLoading = false,
|
||||||
error = error.message
|
error = error.message ?: "Ошибка отправки сообщения"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun clearError() {
|
||||||
|
_uiState.value = _uiState.value.copy(error = null)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
data class AIUiState(
|
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.Icons
|
||||||
import androidx.compose.material.icons.filled.Add
|
import androidx.compose.material.icons.filled.Add
|
||||||
import androidx.compose.material.icons.filled.CalendarToday
|
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.Person
|
||||||
import androidx.compose.material.icons.filled.Public
|
import androidx.compose.material.icons.filled.Public
|
||||||
import androidx.compose.material.icons.filled.Star
|
import androidx.compose.material.icons.filled.Star
|
||||||
@@ -171,28 +173,96 @@ fun ScheduleScreen(
|
|||||||
|
|
||||||
Spacer(modifier = Modifier.height(48.dp))
|
Spacer(modifier = Modifier.height(48.dp))
|
||||||
|
|
||||||
// Основной контент: только сегодняшняя дата (опущена ниже)
|
// Основной контент: список расписаний
|
||||||
DateSection(
|
if (uiState.isLoading) {
|
||||||
date = dateOnly,
|
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,
|
dayOfWeek = dayOfWeek,
|
||||||
dateCardColor = dateCardColor,
|
dateCardColor = dateCardColor,
|
||||||
accentGreen = accentGreen,
|
accentGreen = accentGreen,
|
||||||
screenHeightDp = screenHeightDp,
|
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) {
|
if (uiState.showAddDialog) {
|
||||||
AddTaskDialog(
|
CreateScheduleDialog(
|
||||||
selectedTaskType = uiState.selectedTaskType,
|
title = uiState.newScheduleTitle,
|
||||||
onTaskTypeSelected = { viewModel.selectTaskType(it) },
|
description = uiState.newScheduleDescription,
|
||||||
onSelect = { viewModel.addTask() },
|
date = uiState.newScheduleDate,
|
||||||
|
onTitleChange = { viewModel.updateNewScheduleTitle(it) },
|
||||||
|
onDescriptionChange = { viewModel.updateNewScheduleDescription(it) },
|
||||||
|
onDateChange = { viewModel.updateNewScheduleDate(it) },
|
||||||
|
onCreate = { viewModel.createScheduleFromDialog() },
|
||||||
onDismiss = { viewModel.hideAddDialog() },
|
onDismiss = { viewModel.hideAddDialog() },
|
||||||
accentGreen = accentGreen,
|
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
|
@Composable
|
||||||
fun DateSection(
|
fun SchedulesListSection(
|
||||||
date: String,
|
schedules: List<com.novayaplaneta.domain.model.Schedule>,
|
||||||
|
dateOnly: String,
|
||||||
dayOfWeek: String,
|
dayOfWeek: String,
|
||||||
dateCardColor: Color,
|
dateCardColor: Color,
|
||||||
accentGreen: Color,
|
accentGreen: Color,
|
||||||
screenHeightDp: Int,
|
screenHeightDp: Int,
|
||||||
tasks: List<TaskType>,
|
onAddClick: () -> Unit,
|
||||||
onAddClick: () -> Unit
|
onGenerateClick: () -> Unit,
|
||||||
|
onScheduleClick: (String) -> Unit,
|
||||||
|
onDeleteClick: (String) -> Unit
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
) {
|
) {
|
||||||
// Дата и кнопка + в одной строке
|
// Дата и кнопки в одной строке
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
@@ -267,7 +340,7 @@ fun DateSection(
|
|||||||
horizontalAlignment = Alignment.Start
|
horizontalAlignment = Alignment.Start
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = date,
|
text = dateOnly,
|
||||||
fontSize = dateTextSize,
|
fontSize = dateTextSize,
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
color = Color.Black
|
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(
|
FloatingActionButton(
|
||||||
onClick = onAddClick,
|
onClick = onAddClick,
|
||||||
@@ -296,65 +383,106 @@ fun DateSection(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Задачи в ряд
|
// Список расписаний
|
||||||
if (tasks.isNotEmpty()) {
|
if (schedules.isEmpty()) {
|
||||||
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()
|
|
||||||
) {
|
|
||||||
// Верхняя часть (для изображения)
|
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.weight(0.6f)
|
.padding(vertical = 48.dp),
|
||||||
.background(color = Color.White.copy(alpha = 0.5f))
|
|
||||||
)
|
|
||||||
|
|
||||||
// Нижняя часть (для текста)
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.weight(0.4f)
|
|
||||||
.background(color = Color.LightGray)
|
|
||||||
.padding(12.dp),
|
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
val textSize = (screenHeightDp * 0.02f).toInt().coerceIn(16, 24).sp
|
|
||||||
Text(
|
Text(
|
||||||
text = taskType.title,
|
text = "Нет расписаний",
|
||||||
fontSize = textSize,
|
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,
|
fontWeight = FontWeight.Bold,
|
||||||
color = Color.Black,
|
color = Color.Black
|
||||||
textAlign = TextAlign.Center
|
)
|
||||||
|
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
|
@Composable
|
||||||
fun AddTaskDialog(
|
fun CreateScheduleDialog(
|
||||||
selectedTaskType: TaskType?,
|
title: String,
|
||||||
onTaskTypeSelected: (TaskType) -> Unit,
|
description: String,
|
||||||
onSelect: () -> Unit,
|
date: java.time.LocalDate?,
|
||||||
|
onTitleChange: (String) -> Unit,
|
||||||
|
onDescriptionChange: (String) -> Unit,
|
||||||
|
onDateChange: (java.time.LocalDate) -> Unit,
|
||||||
|
onCreate: () -> Unit,
|
||||||
onDismiss: () -> Unit,
|
onDismiss: () -> Unit,
|
||||||
accentGreen: Color,
|
accentGreen: Color,
|
||||||
screenHeightDp: Int
|
screenHeightDp: Int,
|
||||||
|
isLoading: Boolean
|
||||||
) {
|
) {
|
||||||
Dialog(onDismissRequest = onDismiss) {
|
Dialog(onDismissRequest = onDismiss) {
|
||||||
Card(
|
Card(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth(0.8f)
|
.fillMaxWidth(0.9f)
|
||||||
.wrapContentHeight(),
|
.wrapContentHeight(),
|
||||||
shape = RoundedCornerShape(24.dp),
|
shape = RoundedCornerShape(24.dp),
|
||||||
colors = CardDefaults.cardColors(
|
colors = CardDefaults.cardColors(
|
||||||
@@ -385,47 +518,42 @@ fun AddTaskDialog(
|
|||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(24.dp),
|
.padding(24.dp),
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
verticalArrangement = Arrangement.spacedBy(24.dp)
|
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
) {
|
) {
|
||||||
// Заголовок
|
// Заголовок
|
||||||
val titleSize = (screenHeightDp * 0.03f).toInt().coerceIn(24, 36).sp
|
val titleSize = (screenHeightDp * 0.03f).toInt().coerceIn(24, 36).sp
|
||||||
Text(
|
Text(
|
||||||
text = "Выберите задачу",
|
text = "Создать расписание",
|
||||||
fontSize = titleSize,
|
fontSize = titleSize,
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
color = Color.Black
|
color = Color.Black
|
||||||
)
|
)
|
||||||
|
|
||||||
// Опции выбора
|
// Поле названия
|
||||||
Column(
|
OutlinedTextField(
|
||||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
value = title,
|
||||||
modifier = Modifier.fillMaxWidth()
|
onValueChange = onTitleChange,
|
||||||
) {
|
label = { Text("Название") },
|
||||||
// Подарок
|
modifier = Modifier.fillMaxWidth(),
|
||||||
TaskOption(
|
enabled = !isLoading
|
||||||
taskType = TaskType.Gift,
|
|
||||||
isSelected = selectedTaskType == TaskType.Gift,
|
|
||||||
onClick = { onTaskTypeSelected(TaskType.Gift) },
|
|
||||||
accentGreen = accentGreen,
|
|
||||||
screenHeightDp = screenHeightDp
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Кушать ложкой
|
// Поле описания
|
||||||
TaskOption(
|
OutlinedTextField(
|
||||||
taskType = TaskType.EatWithSpoon,
|
value = description,
|
||||||
isSelected = selectedTaskType == TaskType.EatWithSpoon,
|
onValueChange = onDescriptionChange,
|
||||||
onClick = { onTaskTypeSelected(TaskType.EatWithSpoon) },
|
label = { Text("Описание") },
|
||||||
accentGreen = accentGreen,
|
modifier = Modifier.fillMaxWidth(),
|
||||||
screenHeightDp = screenHeightDp
|
maxLines = 3,
|
||||||
|
enabled = !isLoading
|
||||||
)
|
)
|
||||||
}
|
|
||||||
|
|
||||||
// Кнопки внизу
|
// Кнопки внизу
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
horizontalArrangement = Arrangement.spacedBy(16.dp)
|
horizontalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
) {
|
) {
|
||||||
// Кнопка "Назад"
|
// Кнопка "Отмена"
|
||||||
Button(
|
Button(
|
||||||
onClick = onDismiss,
|
onClick = onDismiss,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -435,23 +563,24 @@ fun AddTaskDialog(
|
|||||||
colors = ButtonDefaults.buttonColors(
|
colors = ButtonDefaults.buttonColors(
|
||||||
containerColor = Color.LightGray,
|
containerColor = Color.LightGray,
|
||||||
contentColor = Color.Black
|
contentColor = Color.Black
|
||||||
)
|
),
|
||||||
|
enabled = !isLoading
|
||||||
) {
|
) {
|
||||||
val buttonTextSize = (screenHeightDp * 0.022f).toInt().coerceIn(18, 26).sp
|
val buttonTextSize = (screenHeightDp * 0.022f).toInt().coerceIn(18, 26).sp
|
||||||
Text(
|
Text(
|
||||||
text = "Назад",
|
text = "Отмена",
|
||||||
fontSize = buttonTextSize,
|
fontSize = buttonTextSize,
|
||||||
fontWeight = FontWeight.Bold
|
fontWeight = FontWeight.Bold
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Кнопка "Выбрать"
|
// Кнопка "Создать"
|
||||||
Button(
|
Button(
|
||||||
onClick = onSelect,
|
onClick = onCreate,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.weight(1f)
|
.weight(1f)
|
||||||
.height(56.dp),
|
.height(56.dp),
|
||||||
enabled = selectedTaskType != null,
|
enabled = title.isNotBlank() && !isLoading,
|
||||||
shape = RoundedCornerShape(16.dp),
|
shape = RoundedCornerShape(16.dp),
|
||||||
colors = ButtonDefaults.buttonColors(
|
colors = ButtonDefaults.buttonColors(
|
||||||
containerColor = accentGreen,
|
containerColor = accentGreen,
|
||||||
@@ -460,9 +589,15 @@ fun AddTaskDialog(
|
|||||||
disabledContentColor = Color.Gray
|
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
|
val buttonTextSize = (screenHeightDp * 0.022f).toInt().coerceIn(18, 26).sp
|
||||||
Text(
|
Text(
|
||||||
text = "Выбрать",
|
text = "Создать",
|
||||||
fontSize = buttonTextSize,
|
fontSize = buttonTextSize,
|
||||||
fontWeight = FontWeight.Bold
|
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.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
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 com.novayaplaneta.domain.usecase.GetSchedulesUseCase
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import java.time.LocalDate
|
||||||
|
import java.time.format.DateTimeFormatter
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
sealed class TaskType(val title: String) {
|
sealed class TaskType(val title: String) {
|
||||||
@@ -17,52 +25,247 @@ sealed class TaskType(val title: String) {
|
|||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class ScheduleViewModel @Inject constructor(
|
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() {
|
) : ViewModel() {
|
||||||
|
|
||||||
private val _uiState = MutableStateFlow(ScheduleUiState())
|
private val _uiState = MutableStateFlow(ScheduleUiState())
|
||||||
val uiState: StateFlow<ScheduleUiState> = _uiState.asStateFlow()
|
val uiState: StateFlow<ScheduleUiState> = _uiState.asStateFlow()
|
||||||
|
|
||||||
fun loadSchedules(userId: String) {
|
init {
|
||||||
|
loadSchedules()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun loadSchedules(scheduleDate: String? = null) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
_uiState.value = _uiState.value.copy(isLoading = true)
|
_uiState.value = _uiState.value.copy(isLoading = true, errorMessage = null)
|
||||||
getSchedulesUseCase(userId).collect { schedules ->
|
|
||||||
|
try {
|
||||||
|
// Получаем текущего пользователя
|
||||||
|
val user = authRepository.getCurrentUser().first()
|
||||||
|
val userId = user?.id ?: ""
|
||||||
|
|
||||||
|
// Загружаем расписания с сервера
|
||||||
|
val loadResult = getSchedulesUseCase.loadSchedules(scheduleDate)
|
||||||
|
if (loadResult.isFailure) {
|
||||||
_uiState.value = _uiState.value.copy(
|
_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() {
|
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() {
|
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(
|
_uiState.value = _uiState.value.copy(
|
||||||
tasks = newTasks,
|
|
||||||
showAddDialog = false,
|
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(
|
data class ScheduleUiState(
|
||||||
val schedules: List<com.novayaplaneta.domain.model.Schedule> = emptyList(),
|
val schedules: List<Schedule> = emptyList(),
|
||||||
val tasks: List<TaskType> = emptyList(),
|
|
||||||
val isLoading: Boolean = false,
|
val isLoading: Boolean = false,
|
||||||
|
val errorMessage: String? = null,
|
||||||
val showAddDialog: Boolean = false,
|
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.layout.*
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.items
|
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.material3.*
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
|
import androidx.navigation.NavController
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun TaskScreen(
|
fun TaskScreen(
|
||||||
|
scheduleId: String? = null,
|
||||||
|
navController: NavController? = null,
|
||||||
viewModel: TaskViewModel = hiltViewModel(),
|
viewModel: TaskViewModel = hiltViewModel(),
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
val uiState by viewModel.uiState.collectAsState()
|
val uiState by viewModel.uiState.collectAsState()
|
||||||
|
|
||||||
|
// Загружаем задачи при открытии экрана
|
||||||
|
LaunchedEffect(scheduleId) {
|
||||||
|
scheduleId?.let {
|
||||||
|
viewModel.loadTasks(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
TopAppBar(
|
TopAppBar(
|
||||||
title = { Text("Задания") }
|
title = { Text("Задания") },
|
||||||
|
actions = {
|
||||||
|
if (scheduleId != null) {
|
||||||
|
IconButton(onClick = { /* TODO: Show create task dialog */ }) {
|
||||||
|
Icon(Icons.Default.Add, contentDescription = "Добавить задачу")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
) { paddingValues ->
|
) { paddingValues ->
|
||||||
@@ -32,12 +54,38 @@ fun TaskScreen(
|
|||||||
.padding(paddingValues)
|
.padding(paddingValues)
|
||||||
.padding(16.dp)
|
.padding(16.dp)
|
||||||
) {
|
) {
|
||||||
if (uiState.isLoading) {
|
// Отображение ошибок
|
||||||
CircularProgressIndicator(
|
uiState.errorMessage?.let { error ->
|
||||||
|
Card(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.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 {
|
} else {
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
@@ -50,7 +98,8 @@ fun TaskScreen(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(16.dp),
|
.padding(16.dp),
|
||||||
horizontalArrangement = Arrangement.SpaceBetween
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
Column(modifier = Modifier.weight(1f)) {
|
Column(modifier = Modifier.weight(1f)) {
|
||||||
Text(
|
Text(
|
||||||
@@ -63,11 +112,28 @@ fun TaskScreen(
|
|||||||
style = MaterialTheme.typography.bodySmall
|
style = MaterialTheme.typography.bodySmall
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
task.duration?.let {
|
||||||
|
Text(
|
||||||
|
text = "Длительность: $it мин",
|
||||||
|
style = MaterialTheme.typography.bodySmall
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
Checkbox(
|
Checkbox(
|
||||||
checked = task.completed,
|
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.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
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 dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class TaskViewModel @Inject constructor(
|
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() {
|
) : ViewModel() {
|
||||||
|
|
||||||
private val _uiState = MutableStateFlow(TaskUiState())
|
private val _uiState = MutableStateFlow(TaskUiState())
|
||||||
val uiState: StateFlow<TaskUiState> = _uiState.asStateFlow()
|
val uiState: StateFlow<TaskUiState> = _uiState.asStateFlow()
|
||||||
|
|
||||||
|
private var currentScheduleId: String? = null
|
||||||
|
|
||||||
fun loadTasks(scheduleId: String) {
|
fun loadTasks(scheduleId: String) {
|
||||||
|
currentScheduleId = scheduleId
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
_uiState.value = _uiState.value.copy(isLoading = true)
|
_uiState.value = _uiState.value.copy(isLoading = true, errorMessage = null)
|
||||||
taskRepository.getTasks(scheduleId).collect { tasks ->
|
|
||||||
|
try {
|
||||||
|
val loadResult = getTasksUseCase.loadTasks(scheduleId)
|
||||||
|
if (loadResult.isFailure) {
|
||||||
_uiState.value = _uiState.value.copy(
|
_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 {
|
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(
|
data class TaskUiState(
|
||||||
val tasks: List<com.novayaplaneta.domain.model.Task> = emptyList(),
|
val tasks: List<Task> = emptyList(),
|
||||||
val isLoading: Boolean = false
|
val selectedTask: Task? = null,
|
||||||
|
val isLoading: Boolean = false,
|
||||||
|
val errorMessage: String? = null
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user