Добавил логику с наградами
This commit is contained in:
@@ -10,8 +10,11 @@ data class RewardEntity(
|
|||||||
val title: String,
|
val title: String,
|
||||||
val description: String?,
|
val description: String?,
|
||||||
val imageUrl: String?,
|
val imageUrl: String?,
|
||||||
val points: Int,
|
val pointsRequired: Int,
|
||||||
|
val isClaimed: Boolean = false,
|
||||||
val earnedAt: Long?, // timestamp
|
val earnedAt: Long?, // timestamp
|
||||||
val userId: String
|
val userId: String,
|
||||||
|
val createdAt: Long? = null, // timestamp
|
||||||
|
val updatedAt: Long? = null // timestamp
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -12,11 +12,18 @@ fun RewardEntity.toDomain(): Reward {
|
|||||||
title = title,
|
title = title,
|
||||||
description = description,
|
description = description,
|
||||||
imageUrl = imageUrl,
|
imageUrl = imageUrl,
|
||||||
points = points,
|
pointsRequired = pointsRequired,
|
||||||
|
isClaimed = isClaimed,
|
||||||
earnedAt = earnedAt?.let {
|
earnedAt = earnedAt?.let {
|
||||||
LocalDateTime.ofInstant(Instant.ofEpochMilli(it), ZoneId.systemDefault())
|
LocalDateTime.ofInstant(Instant.ofEpochMilli(it), ZoneId.systemDefault())
|
||||||
},
|
},
|
||||||
userId = userId
|
userId = userId,
|
||||||
|
createdAt = createdAt?.let {
|
||||||
|
LocalDateTime.ofInstant(Instant.ofEpochMilli(it), ZoneId.systemDefault())
|
||||||
|
},
|
||||||
|
updatedAt = updatedAt?.let {
|
||||||
|
LocalDateTime.ofInstant(Instant.ofEpochMilli(it), ZoneId.systemDefault())
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -26,9 +33,12 @@ fun Reward.toEntity(): RewardEntity {
|
|||||||
title = title,
|
title = title,
|
||||||
description = description,
|
description = description,
|
||||||
imageUrl = imageUrl,
|
imageUrl = imageUrl,
|
||||||
points = points,
|
pointsRequired = pointsRequired,
|
||||||
|
isClaimed = isClaimed,
|
||||||
earnedAt = earnedAt?.atZone(ZoneId.systemDefault())?.toInstant()?.toEpochMilli(),
|
earnedAt = earnedAt?.atZone(ZoneId.systemDefault())?.toInstant()?.toEpochMilli(),
|
||||||
userId = userId
|
userId = userId,
|
||||||
|
createdAt = createdAt?.atZone(ZoneId.systemDefault())?.toInstant()?.toEpochMilli(),
|
||||||
|
updatedAt = updatedAt?.atZone(ZoneId.systemDefault())?.toInstant()?.toEpochMilli()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,11 +2,14 @@ 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.ClaimRewardResponse
|
||||||
import com.novayaplaneta.data.remote.dto.CompleteTaskResponse
|
import com.novayaplaneta.data.remote.dto.CompleteTaskResponse
|
||||||
|
import com.novayaplaneta.data.remote.dto.CreateRewardRequest
|
||||||
import com.novayaplaneta.data.remote.dto.CreateScheduleRequest
|
import com.novayaplaneta.data.remote.dto.CreateScheduleRequest
|
||||||
import com.novayaplaneta.data.remote.dto.CreateTaskRequest
|
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.RewardDto
|
||||||
import com.novayaplaneta.data.remote.dto.ScheduleDto
|
import com.novayaplaneta.data.remote.dto.ScheduleDto
|
||||||
import com.novayaplaneta.data.remote.dto.TaskDto
|
import com.novayaplaneta.data.remote.dto.TaskDto
|
||||||
import retrofit2.Response
|
import retrofit2.Response
|
||||||
@@ -79,5 +82,33 @@ interface BackendApi {
|
|||||||
@Path("task_id") taskId: String,
|
@Path("task_id") taskId: String,
|
||||||
@Query("completed") completed: Boolean
|
@Query("completed") completed: Boolean
|
||||||
): Response<CompleteTaskResponse>
|
): Response<CompleteTaskResponse>
|
||||||
|
|
||||||
|
// Rewards endpoints
|
||||||
|
@GET("api/v1/rewards")
|
||||||
|
suspend fun getRewards(
|
||||||
|
@Query("skip") skip: Int = 0,
|
||||||
|
@Query("limit") limit: Int = 100,
|
||||||
|
@Query("is_claimed") isClaimed: Boolean? = null
|
||||||
|
): Response<List<RewardDto>>
|
||||||
|
|
||||||
|
@GET("api/v1/rewards/{reward_id}")
|
||||||
|
suspend fun getRewardById(
|
||||||
|
@Path("reward_id") rewardId: String
|
||||||
|
): Response<RewardDto>
|
||||||
|
|
||||||
|
@POST("api/v1/rewards")
|
||||||
|
suspend fun createReward(
|
||||||
|
@Body request: CreateRewardRequest
|
||||||
|
): Response<RewardDto>
|
||||||
|
|
||||||
|
@DELETE("api/v1/rewards/{reward_id}")
|
||||||
|
suspend fun deleteReward(
|
||||||
|
@Path("reward_id") rewardId: String
|
||||||
|
): Response<Unit>
|
||||||
|
|
||||||
|
@POST("api/v1/rewards/{reward_id}/claim")
|
||||||
|
suspend fun claimReward(
|
||||||
|
@Path("reward_id") rewardId: String
|
||||||
|
): Response<ClaimRewardResponse>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package com.novayaplaneta.data.remote.dto
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class ClaimRewardResponse(
|
||||||
|
val id: String,
|
||||||
|
val title: String,
|
||||||
|
val description: String?,
|
||||||
|
val image_url: String?,
|
||||||
|
val points_required: Int,
|
||||||
|
val user_id: String,
|
||||||
|
val is_claimed: Boolean,
|
||||||
|
val created_at: String,
|
||||||
|
val updated_at: String
|
||||||
|
)
|
||||||
|
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package com.novayaplaneta.data.remote.dto
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class CreateRewardRequest(
|
||||||
|
val title: String,
|
||||||
|
val description: String? = null,
|
||||||
|
val image_url: String? = null,
|
||||||
|
val points_required: Int
|
||||||
|
)
|
||||||
|
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package com.novayaplaneta.data.remote.dto
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class RewardDto(
|
||||||
|
val id: String,
|
||||||
|
val title: String,
|
||||||
|
val description: String?,
|
||||||
|
val image_url: String?,
|
||||||
|
val points_required: Int,
|
||||||
|
val user_id: String,
|
||||||
|
val is_claimed: Boolean,
|
||||||
|
val created_at: String,
|
||||||
|
val updated_at: String
|
||||||
|
)
|
||||||
|
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
package com.novayaplaneta.data.remote.mapper
|
||||||
|
|
||||||
|
import com.novayaplaneta.data.remote.dto.ClaimRewardResponse
|
||||||
|
import com.novayaplaneta.data.remote.dto.CreateRewardRequest
|
||||||
|
import com.novayaplaneta.data.remote.dto.RewardDto
|
||||||
|
import com.novayaplaneta.domain.model.Reward
|
||||||
|
import java.time.LocalDateTime
|
||||||
|
import java.time.format.DateTimeFormatter
|
||||||
|
|
||||||
|
fun RewardDto.toDomain(): Reward {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
val earnedAtDateTime = if (is_claimed) updatedAtDateTime else null
|
||||||
|
|
||||||
|
return Reward(
|
||||||
|
id = id,
|
||||||
|
title = title,
|
||||||
|
description = description,
|
||||||
|
imageUrl = image_url,
|
||||||
|
pointsRequired = points_required,
|
||||||
|
isClaimed = is_claimed,
|
||||||
|
earnedAt = earnedAtDateTime,
|
||||||
|
userId = user_id,
|
||||||
|
createdAt = createdAtDateTime,
|
||||||
|
updatedAt = updatedAtDateTime
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun ClaimRewardResponse.toDomain(): Reward {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
val earnedAtDateTime = if (is_claimed) updatedAtDateTime else null
|
||||||
|
|
||||||
|
return Reward(
|
||||||
|
id = id,
|
||||||
|
title = title,
|
||||||
|
description = description,
|
||||||
|
imageUrl = image_url,
|
||||||
|
pointsRequired = points_required,
|
||||||
|
isClaimed = is_claimed,
|
||||||
|
earnedAt = earnedAtDateTime,
|
||||||
|
userId = user_id,
|
||||||
|
createdAt = createdAtDateTime,
|
||||||
|
updatedAt = updatedAtDateTime
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Reward.toCreateRequest(): CreateRewardRequest {
|
||||||
|
return CreateRewardRequest(
|
||||||
|
title = title,
|
||||||
|
description = description,
|
||||||
|
image_url = imageUrl,
|
||||||
|
points_required = pointsRequired
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@@ -3,43 +3,165 @@ package com.novayaplaneta.data.repository
|
|||||||
import com.novayaplaneta.data.local.dao.RewardDao
|
import com.novayaplaneta.data.local.dao.RewardDao
|
||||||
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.CreateRewardRequest
|
||||||
|
import com.novayaplaneta.data.remote.mapper.toCreateRequest
|
||||||
|
import com.novayaplaneta.data.remote.mapper.toDomain
|
||||||
import com.novayaplaneta.domain.model.Reward
|
import com.novayaplaneta.domain.model.Reward
|
||||||
import com.novayaplaneta.domain.repository.RewardRepository
|
import com.novayaplaneta.domain.repository.RewardRepository
|
||||||
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 java.time.LocalDateTime
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class RewardRepositoryImpl @Inject constructor(
|
class RewardRepositoryImpl @Inject constructor(
|
||||||
private val rewardDao: RewardDao
|
private val rewardDao: RewardDao,
|
||||||
|
private val backendApi: BackendApi
|
||||||
) : RewardRepository {
|
) : RewardRepository {
|
||||||
|
|
||||||
override fun getRewards(userId: String): Flow<List<Reward>> {
|
// Кэш наград
|
||||||
return rewardDao.getRewardsByUserId(userId).map { rewards ->
|
private val _rewardsCache = MutableStateFlow<List<Reward>>(emptyList())
|
||||||
rewards.map { it.toDomain() }
|
|
||||||
}
|
override suspend fun loadRewards(skip: Int, limit: Int, isClaimed: Boolean?): Result<Unit> {
|
||||||
|
return try {
|
||||||
|
val response = backendApi.getRewards(skip = skip, limit = limit, isClaimed = isClaimed)
|
||||||
|
if (response.isSuccessful && response.body() != null) {
|
||||||
|
val rewards = response.body()!!.map { it.toDomain() }
|
||||||
|
|
||||||
|
// Обновляем кэш
|
||||||
|
if (skip == 0) {
|
||||||
|
_rewardsCache.value = rewards
|
||||||
|
} else {
|
||||||
|
val currentRewards = _rewardsCache.value.toMutableList()
|
||||||
|
currentRewards.addAll(rewards)
|
||||||
|
_rewardsCache.value = currentRewards
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getRewardById(id: String): Reward? {
|
// Сохраняем в локальную БД
|
||||||
return rewardDao.getRewardById(id)?.toDomain()
|
rewards.forEach { reward ->
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun createReward(reward: Reward) {
|
|
||||||
rewardDao.insertReward(reward.toEntity())
|
rewardDao.insertReward(reward.toEntity())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Result.success(Unit)
|
||||||
|
} else {
|
||||||
|
val errorBody = response.errorBody()?.string() ?: "Unknown error"
|
||||||
|
Result.failure(Exception("Failed to load rewards: $errorBody (code: ${response.code()})"))
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Result.failure(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getRewards(userId: String): Flow<List<Reward>> {
|
||||||
|
return _rewardsCache.asStateFlow()
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getRewardById(id: String): Result<Reward> {
|
||||||
|
return try {
|
||||||
|
val response = backendApi.getRewardById(id)
|
||||||
|
if (response.isSuccessful && response.body() != null) {
|
||||||
|
val reward = response.body()!!.toDomain()
|
||||||
|
|
||||||
|
// Обновляем в кэше
|
||||||
|
val currentRewards = _rewardsCache.value.toMutableList()
|
||||||
|
val index = currentRewards.indexOfFirst { it.id == id }
|
||||||
|
if (index >= 0) {
|
||||||
|
currentRewards[index] = reward
|
||||||
|
} else {
|
||||||
|
currentRewards.add(reward)
|
||||||
|
}
|
||||||
|
_rewardsCache.value = currentRewards
|
||||||
|
|
||||||
|
// Сохраняем в локальную БД
|
||||||
|
rewardDao.insertReward(reward.toEntity())
|
||||||
|
|
||||||
|
Result.success(reward)
|
||||||
|
} else {
|
||||||
|
val errorBody = response.errorBody()?.string() ?: "Unknown error"
|
||||||
|
Result.failure(Exception("Failed to get reward: $errorBody (code: ${response.code()})"))
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Result.failure(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun createReward(reward: Reward): Result<Reward> {
|
||||||
|
return try {
|
||||||
|
val request = reward.toCreateRequest()
|
||||||
|
val response = backendApi.createReward(request)
|
||||||
|
if (response.isSuccessful && response.body() != null) {
|
||||||
|
val createdReward = response.body()!!.toDomain()
|
||||||
|
|
||||||
|
// Обновляем кэш
|
||||||
|
val currentRewards = _rewardsCache.value.toMutableList()
|
||||||
|
currentRewards.add(createdReward)
|
||||||
|
_rewardsCache.value = currentRewards
|
||||||
|
|
||||||
|
// Сохраняем в локальную БД
|
||||||
|
rewardDao.insertReward(createdReward.toEntity())
|
||||||
|
|
||||||
|
Result.success(createdReward)
|
||||||
|
} else {
|
||||||
|
val errorBody = response.errorBody()?.string() ?: "Unknown error"
|
||||||
|
Result.failure(Exception("Failed to create reward: $errorBody (code: ${response.code()})"))
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Result.failure(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override suspend fun updateReward(reward: Reward) {
|
override suspend fun updateReward(reward: Reward) {
|
||||||
rewardDao.updateReward(reward.toEntity())
|
rewardDao.updateReward(reward.toEntity())
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun deleteReward(id: String) {
|
override suspend fun deleteReward(id: String): Result<Unit> {
|
||||||
|
return try {
|
||||||
|
val response = backendApi.deleteReward(id)
|
||||||
|
if (response.isSuccessful) {
|
||||||
|
// Удаляем из кэша
|
||||||
|
val currentRewards = _rewardsCache.value.toMutableList()
|
||||||
|
currentRewards.removeAll { it.id == id }
|
||||||
|
_rewardsCache.value = currentRewards
|
||||||
|
|
||||||
|
// Удаляем из локальной БД
|
||||||
rewardDao.deleteReward(id)
|
rewardDao.deleteReward(id)
|
||||||
|
|
||||||
|
Result.success(Unit)
|
||||||
|
} else {
|
||||||
|
val errorBody = response.errorBody()?.string() ?: "Unknown error"
|
||||||
|
Result.failure(Exception("Failed to delete reward: $errorBody (code: ${response.code()})"))
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Result.failure(e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun earnReward(userId: String, rewardId: String) {
|
override suspend fun claimReward(rewardId: String): Result<Reward> {
|
||||||
val reward = getRewardById(rewardId)
|
return try {
|
||||||
reward?.let {
|
val response = backendApi.claimReward(rewardId)
|
||||||
updateReward(it.copy(earnedAt = LocalDateTime.now()))
|
if (response.isSuccessful && response.body() != null) {
|
||||||
|
val claimedReward = response.body()!!.toDomain()
|
||||||
|
|
||||||
|
// Обновляем в кэше
|
||||||
|
val currentRewards = _rewardsCache.value.toMutableList()
|
||||||
|
val index = currentRewards.indexOfFirst { it.id == rewardId }
|
||||||
|
if (index >= 0) {
|
||||||
|
currentRewards[index] = claimedReward
|
||||||
|
_rewardsCache.value = currentRewards
|
||||||
|
}
|
||||||
|
|
||||||
|
// Сохраняем в локальную БД
|
||||||
|
rewardDao.insertReward(claimedReward.toEntity())
|
||||||
|
|
||||||
|
Result.success(claimedReward)
|
||||||
|
} else {
|
||||||
|
val errorBody = response.errorBody()?.string() ?: "Unknown error"
|
||||||
|
Result.failure(Exception("Failed to claim reward: $errorBody (code: ${response.code()})"))
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Result.failure(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,8 +7,11 @@ data class Reward(
|
|||||||
val title: String,
|
val title: String,
|
||||||
val description: String?,
|
val description: String?,
|
||||||
val imageUrl: String?,
|
val imageUrl: String?,
|
||||||
val points: Int,
|
val pointsRequired: Int,
|
||||||
|
val isClaimed: Boolean = false,
|
||||||
val earnedAt: LocalDateTime? = null,
|
val earnedAt: LocalDateTime? = null,
|
||||||
val userId: String
|
val userId: String,
|
||||||
|
val createdAt: LocalDateTime? = null,
|
||||||
|
val updatedAt: LocalDateTime? = null
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -4,11 +4,12 @@ import com.novayaplaneta.domain.model.Reward
|
|||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
interface RewardRepository {
|
interface RewardRepository {
|
||||||
|
suspend fun loadRewards(skip: Int = 0, limit: Int = 100, isClaimed: Boolean? = null): Result<Unit>
|
||||||
fun getRewards(userId: String): Flow<List<Reward>>
|
fun getRewards(userId: String): Flow<List<Reward>>
|
||||||
suspend fun getRewardById(id: String): Reward?
|
suspend fun getRewardById(id: String): Result<Reward>
|
||||||
suspend fun createReward(reward: Reward)
|
suspend fun createReward(reward: Reward): Result<Reward>
|
||||||
suspend fun updateReward(reward: Reward)
|
suspend fun updateReward(reward: Reward)
|
||||||
suspend fun deleteReward(id: String)
|
suspend fun deleteReward(id: String): Result<Unit>
|
||||||
suspend fun earnReward(userId: String, rewardId: String)
|
suspend fun claimReward(rewardId: String): Result<Reward>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package com.novayaplaneta.domain.usecase
|
||||||
|
|
||||||
|
import com.novayaplaneta.domain.model.Reward
|
||||||
|
import com.novayaplaneta.domain.repository.RewardRepository
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class ClaimRewardUseCase @Inject constructor(
|
||||||
|
private val repository: RewardRepository
|
||||||
|
) {
|
||||||
|
suspend operator fun invoke(rewardId: String): Result<Reward> {
|
||||||
|
return repository.claimReward(rewardId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package com.novayaplaneta.domain.usecase
|
||||||
|
|
||||||
|
import com.novayaplaneta.domain.model.Reward
|
||||||
|
import com.novayaplaneta.domain.repository.RewardRepository
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class CreateRewardUseCase @Inject constructor(
|
||||||
|
private val repository: RewardRepository
|
||||||
|
) {
|
||||||
|
suspend operator fun invoke(reward: Reward): Result<Reward> {
|
||||||
|
return repository.createReward(reward)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package com.novayaplaneta.domain.usecase
|
||||||
|
|
||||||
|
import com.novayaplaneta.domain.repository.RewardRepository
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class DeleteRewardUseCase @Inject constructor(
|
||||||
|
private val repository: RewardRepository
|
||||||
|
) {
|
||||||
|
suspend operator fun invoke(id: String): Result<Unit> {
|
||||||
|
return repository.deleteReward(id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package com.novayaplaneta.domain.usecase
|
||||||
|
|
||||||
|
import com.novayaplaneta.domain.model.Reward
|
||||||
|
import com.novayaplaneta.domain.repository.RewardRepository
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class GetRewardByIdUseCase @Inject constructor(
|
||||||
|
private val repository: RewardRepository
|
||||||
|
) {
|
||||||
|
suspend operator fun invoke(id: String): Result<Reward> {
|
||||||
|
return repository.getRewardById(id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package com.novayaplaneta.domain.usecase
|
||||||
|
|
||||||
|
import com.novayaplaneta.domain.model.Reward
|
||||||
|
import com.novayaplaneta.domain.repository.RewardRepository
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class GetRewardsUseCase @Inject constructor(
|
||||||
|
private val repository: RewardRepository
|
||||||
|
) {
|
||||||
|
operator fun invoke(userId: String): Flow<List<Reward>> {
|
||||||
|
return repository.getRewards(userId)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun loadRewards(skip: Int = 0, limit: Int = 100, isClaimed: Boolean? = null): Result<Unit> {
|
||||||
|
return try {
|
||||||
|
repository.loadRewards(skip, limit, isClaimed)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Result.failure(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -7,6 +7,7 @@ import androidx.compose.foundation.lazy.grid.GridCells
|
|||||||
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
||||||
import androidx.compose.foundation.lazy.grid.items
|
import androidx.compose.foundation.lazy.grid.items
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
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
|
||||||
@@ -22,7 +23,6 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.platform.LocalConfiguration
|
import androidx.compose.ui.platform.LocalConfiguration
|
||||||
import androidx.compose.foundation.text.KeyboardOptions
|
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.input.KeyboardType
|
import androidx.compose.ui.text.input.KeyboardType
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
@@ -181,8 +181,26 @@ fun RewardsScreen(
|
|||||||
modifier = Modifier.padding(bottom = 24.dp)
|
modifier = Modifier.padding(bottom = 24.dp)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Отображение ошибок
|
||||||
|
uiState.errorMessage?.let { error ->
|
||||||
|
Card(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.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) {
|
if (uiState.isLoading && uiState.rewards.isEmpty()) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
@@ -199,7 +217,10 @@ fun RewardsScreen(
|
|||||||
items(uiState.rewards) { reward ->
|
items(uiState.rewards) { reward ->
|
||||||
RewardCard(
|
RewardCard(
|
||||||
reward = reward,
|
reward = reward,
|
||||||
screenHeightDp = screenHeightDp
|
screenHeightDp = screenHeightDp,
|
||||||
|
accentGreen = accentGreen,
|
||||||
|
onClaimClick = { viewModel.claimReward(reward.id) },
|
||||||
|
onDeleteClick = { viewModel.deleteReward(reward.id) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -220,11 +241,16 @@ fun RewardsScreen(
|
|||||||
if (uiState.showAddDialog) {
|
if (uiState.showAddDialog) {
|
||||||
AddRewardDialog(
|
AddRewardDialog(
|
||||||
rewardTitle = uiState.newRewardTitle,
|
rewardTitle = uiState.newRewardTitle,
|
||||||
|
rewardDescription = uiState.newRewardDescription,
|
||||||
|
rewardPoints = uiState.newRewardPoints,
|
||||||
onTitleChange = { viewModel.updateNewRewardTitle(it) },
|
onTitleChange = { viewModel.updateNewRewardTitle(it) },
|
||||||
onAdd = { viewModel.addReward("default") },
|
onDescriptionChange = { viewModel.updateNewRewardDescription(it) },
|
||||||
|
onPointsChange = { viewModel.updateNewRewardPoints(it) },
|
||||||
|
onAdd = { viewModel.createReward() },
|
||||||
onDismiss = { viewModel.hideAddDialog() },
|
onDismiss = { viewModel.hideAddDialog() },
|
||||||
accentGreen = accentGreen,
|
accentGreen = accentGreen,
|
||||||
screenHeightDp = screenHeightDp
|
screenHeightDp = screenHeightDp,
|
||||||
|
isLoading = uiState.isLoading
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -270,16 +296,20 @@ fun NavItem(
|
|||||||
@Composable
|
@Composable
|
||||||
fun RewardCard(
|
fun RewardCard(
|
||||||
reward: com.novayaplaneta.domain.model.Reward,
|
reward: com.novayaplaneta.domain.model.Reward,
|
||||||
screenHeightDp: Int
|
screenHeightDp: Int,
|
||||||
|
accentGreen: Color,
|
||||||
|
onClaimClick: () -> Unit = {},
|
||||||
|
onDeleteClick: () -> Unit = {}
|
||||||
) {
|
) {
|
||||||
val cardHeight = 180.dp
|
val cardHeight = 180.dp
|
||||||
|
val isClaimed = reward.isClaimed
|
||||||
|
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.height(cardHeight)
|
.height(cardHeight)
|
||||||
.clip(RoundedCornerShape(16.dp))
|
.clip(RoundedCornerShape(16.dp))
|
||||||
.background(color = Color.LightGray)
|
.background(color = if (isClaimed) accentGreen.copy(alpha = 0.3f) else Color.LightGray)
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.fillMaxSize()
|
modifier = Modifier.fillMaxSize()
|
||||||
@@ -289,14 +319,14 @@ fun RewardCard(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.weight(0.6f)
|
.weight(0.6f)
|
||||||
.background(color = Color.White.copy(alpha = 0.5f)),
|
.background(color = if (isClaimed) accentGreen.copy(alpha = 0.2f) else Color.White.copy(alpha = 0.5f)),
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Filled.Star,
|
imageVector = Icons.Filled.Star,
|
||||||
contentDescription = reward.title,
|
contentDescription = reward.title,
|
||||||
modifier = Modifier.size(60.dp),
|
modifier = Modifier.size(60.dp),
|
||||||
tint = AccentGold
|
tint = if (isClaimed) accentGreen else AccentGold
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -305,9 +335,12 @@ fun RewardCard(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.weight(0.4f)
|
.weight(0.4f)
|
||||||
.background(color = Color.LightGray)
|
.background(color = if (isClaimed) accentGreen.copy(alpha = 0.3f) else Color.LightGray)
|
||||||
.padding(12.dp),
|
.padding(12.dp),
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
) {
|
) {
|
||||||
val textSize = (screenHeightDp * 0.02f).toInt().coerceIn(16, 24).sp
|
val textSize = (screenHeightDp * 0.02f).toInt().coerceIn(16, 24).sp
|
||||||
Text(
|
Text(
|
||||||
@@ -317,6 +350,33 @@ fun RewardCard(
|
|||||||
color = Color.Black,
|
color = Color.Black,
|
||||||
textAlign = TextAlign.Center
|
textAlign = TextAlign.Center
|
||||||
)
|
)
|
||||||
|
Text(
|
||||||
|
text = "${reward.pointsRequired} очков",
|
||||||
|
fontSize = textSize * 0.7f,
|
||||||
|
color = Color.Black.copy(alpha = 0.7f),
|
||||||
|
textAlign = TextAlign.Center
|
||||||
|
)
|
||||||
|
if (isClaimed) {
|
||||||
|
Text(
|
||||||
|
text = "Получено",
|
||||||
|
fontSize = textSize * 0.6f,
|
||||||
|
color = accentGreen,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
textAlign = TextAlign.Center
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Button(
|
||||||
|
onClick = onClaimClick,
|
||||||
|
modifier = Modifier.padding(top = 4.dp),
|
||||||
|
colors = ButtonDefaults.buttonColors(
|
||||||
|
containerColor = accentGreen,
|
||||||
|
contentColor = Color.White
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Text("Получить", fontSize = 10.sp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -356,11 +416,16 @@ fun AddRewardButton(
|
|||||||
@Composable
|
@Composable
|
||||||
fun AddRewardDialog(
|
fun AddRewardDialog(
|
||||||
rewardTitle: String,
|
rewardTitle: String,
|
||||||
|
rewardDescription: String,
|
||||||
|
rewardPoints: Int,
|
||||||
onTitleChange: (String) -> Unit,
|
onTitleChange: (String) -> Unit,
|
||||||
|
onDescriptionChange: (String) -> Unit,
|
||||||
|
onPointsChange: (Int) -> Unit,
|
||||||
onAdd: () -> Unit,
|
onAdd: () -> Unit,
|
||||||
onDismiss: () -> Unit,
|
onDismiss: () -> Unit,
|
||||||
accentGreen: Color,
|
accentGreen: Color,
|
||||||
screenHeightDp: Int
|
screenHeightDp: Int,
|
||||||
|
isLoading: Boolean = false
|
||||||
) {
|
) {
|
||||||
Dialog(onDismissRequest = onDismiss) {
|
Dialog(onDismissRequest = onDismiss) {
|
||||||
Card(
|
Card(
|
||||||
@@ -393,6 +458,7 @@ fun AddRewardDialog(
|
|||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
value = rewardTitle,
|
value = rewardTitle,
|
||||||
onValueChange = onTitleChange,
|
onValueChange = onTitleChange,
|
||||||
|
label = { Text("Название") },
|
||||||
placeholder = {
|
placeholder = {
|
||||||
Text(
|
Text(
|
||||||
text = "Введите название награды",
|
text = "Введите название награды",
|
||||||
@@ -402,6 +468,7 @@ fun AddRewardDialog(
|
|||||||
},
|
},
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
singleLine = true,
|
singleLine = true,
|
||||||
|
enabled = !isLoading,
|
||||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text),
|
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text),
|
||||||
colors = OutlinedTextFieldDefaults.colors(
|
colors = OutlinedTextFieldDefaults.colors(
|
||||||
focusedTextColor = Color.Black,
|
focusedTextColor = Color.Black,
|
||||||
@@ -414,6 +481,49 @@ fun AddRewardDialog(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Поле ввода описания
|
||||||
|
OutlinedTextField(
|
||||||
|
value = rewardDescription,
|
||||||
|
onValueChange = onDescriptionChange,
|
||||||
|
label = { Text("Описание") },
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
maxLines = 2,
|
||||||
|
enabled = !isLoading,
|
||||||
|
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text),
|
||||||
|
colors = OutlinedTextFieldDefaults.colors(
|
||||||
|
focusedTextColor = Color.Black,
|
||||||
|
unfocusedTextColor = Color.Black,
|
||||||
|
focusedBorderColor = accentGreen,
|
||||||
|
unfocusedBorderColor = Color.Gray
|
||||||
|
),
|
||||||
|
textStyle = androidx.compose.ui.text.TextStyle(
|
||||||
|
fontSize = inputTextSize
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Поле ввода очков
|
||||||
|
OutlinedTextField(
|
||||||
|
value = rewardPoints.toString(),
|
||||||
|
onValueChange = {
|
||||||
|
it.toIntOrNull()?.let { points ->
|
||||||
|
if (points > 0) onPointsChange(points)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
label = { Text("Очки") },
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
enabled = !isLoading,
|
||||||
|
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
||||||
|
colors = OutlinedTextFieldDefaults.colors(
|
||||||
|
focusedTextColor = Color.Black,
|
||||||
|
unfocusedTextColor = Color.Black,
|
||||||
|
focusedBorderColor = accentGreen,
|
||||||
|
unfocusedBorderColor = Color.Gray
|
||||||
|
),
|
||||||
|
textStyle = androidx.compose.ui.text.TextStyle(
|
||||||
|
fontSize = inputTextSize
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
// Кнопки внизу
|
// Кнопки внизу
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
@@ -445,7 +555,7 @@ fun AddRewardDialog(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.weight(1f)
|
.weight(1f)
|
||||||
.height(56.dp),
|
.height(56.dp),
|
||||||
enabled = rewardTitle.isNotBlank(),
|
enabled = rewardTitle.isNotBlank() && !isLoading,
|
||||||
shape = RoundedCornerShape(16.dp),
|
shape = RoundedCornerShape(16.dp),
|
||||||
colors = ButtonDefaults.buttonColors(
|
colors = ButtonDefaults.buttonColors(
|
||||||
containerColor = accentGreen,
|
containerColor = accentGreen,
|
||||||
@@ -454,6 +564,12 @@ fun AddRewardDialog(
|
|||||||
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 = "Добавить",
|
||||||
@@ -466,4 +582,5 @@ fun AddRewardDialog(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,166 +3,221 @@ package com.novayaplaneta.ui.screens.rewards
|
|||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.novayaplaneta.domain.model.Reward
|
import com.novayaplaneta.domain.model.Reward
|
||||||
import com.novayaplaneta.domain.repository.RewardRepository
|
import com.novayaplaneta.domain.repository.AuthRepository
|
||||||
|
import com.novayaplaneta.domain.usecase.ClaimRewardUseCase
|
||||||
|
import com.novayaplaneta.domain.usecase.CreateRewardUseCase
|
||||||
|
import com.novayaplaneta.domain.usecase.DeleteRewardUseCase
|
||||||
|
import com.novayaplaneta.domain.usecase.GetRewardsUseCase
|
||||||
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 RewardsViewModel @Inject constructor(
|
class RewardsViewModel @Inject constructor(
|
||||||
private val rewardRepository: RewardRepository
|
private val getRewardsUseCase: GetRewardsUseCase,
|
||||||
|
private val createRewardUseCase: CreateRewardUseCase,
|
||||||
|
private val deleteRewardUseCase: DeleteRewardUseCase,
|
||||||
|
private val claimRewardUseCase: ClaimRewardUseCase,
|
||||||
|
private val authRepository: AuthRepository
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
private val _uiState = MutableStateFlow(RewardsUiState())
|
private val _uiState = MutableStateFlow(RewardsUiState())
|
||||||
val uiState: StateFlow<RewardsUiState> = _uiState.asStateFlow()
|
val uiState: StateFlow<RewardsUiState> = _uiState.asStateFlow()
|
||||||
|
|
||||||
|
private var currentUserId: String? = null
|
||||||
|
|
||||||
init {
|
init {
|
||||||
loadDefaultRewards()
|
loadUserId()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loadDefaultRewards() {
|
private fun loadUserId() {
|
||||||
// Создаем список наград по умолчанию
|
|
||||||
val defaultRewards = listOf(
|
|
||||||
Reward(
|
|
||||||
id = "reward_1",
|
|
||||||
title = "Золотая звезда",
|
|
||||||
description = "За выполнение 3 задач",
|
|
||||||
imageUrl = null,
|
|
||||||
points = 10,
|
|
||||||
earnedAt = null,
|
|
||||||
userId = "default"
|
|
||||||
),
|
|
||||||
Reward(
|
|
||||||
id = "reward_2",
|
|
||||||
title = "Подарочная коробка",
|
|
||||||
description = "За выполнение 5 задач",
|
|
||||||
imageUrl = null,
|
|
||||||
points = 20,
|
|
||||||
earnedAt = null,
|
|
||||||
userId = "default"
|
|
||||||
),
|
|
||||||
Reward(
|
|
||||||
id = "reward_3",
|
|
||||||
title = "Игрушка",
|
|
||||||
description = "За выполнение всех задач дня",
|
|
||||||
imageUrl = null,
|
|
||||||
points = 30,
|
|
||||||
earnedAt = null,
|
|
||||||
userId = "default"
|
|
||||||
),
|
|
||||||
Reward(
|
|
||||||
id = "reward_4",
|
|
||||||
title = "Мультфильм",
|
|
||||||
description = "За неделю без пропусков",
|
|
||||||
imageUrl = null,
|
|
||||||
points = 50,
|
|
||||||
earnedAt = null,
|
|
||||||
userId = "default"
|
|
||||||
),
|
|
||||||
Reward(
|
|
||||||
id = "reward_5",
|
|
||||||
title = "Поход в парк",
|
|
||||||
description = "За месяц работы",
|
|
||||||
imageUrl = null,
|
|
||||||
points = 100,
|
|
||||||
earnedAt = null,
|
|
||||||
userId = "default"
|
|
||||||
),
|
|
||||||
Reward(
|
|
||||||
id = "reward_6",
|
|
||||||
title = "Новая книга",
|
|
||||||
description = "За выполнение 10 задач",
|
|
||||||
imageUrl = null,
|
|
||||||
points = 40,
|
|
||||||
earnedAt = null,
|
|
||||||
userId = "default"
|
|
||||||
),
|
|
||||||
Reward(
|
|
||||||
id = "reward_7",
|
|
||||||
title = "Любимая игра",
|
|
||||||
description = "За хорошее поведение",
|
|
||||||
imageUrl = null,
|
|
||||||
points = 25,
|
|
||||||
earnedAt = null,
|
|
||||||
userId = "default"
|
|
||||||
),
|
|
||||||
Reward(
|
|
||||||
id = "reward_8",
|
|
||||||
title = "Специальный обед",
|
|
||||||
description = "За старание в учебе",
|
|
||||||
imageUrl = null,
|
|
||||||
points = 35,
|
|
||||||
earnedAt = null,
|
|
||||||
userId = "default"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
_uiState.value = _uiState.value.copy(rewards = defaultRewards, isLoading = false)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun loadRewards(userId: String) {
|
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
_uiState.value = _uiState.value.copy(isLoading = true)
|
try {
|
||||||
rewardRepository.getRewards(userId).collect { rewards ->
|
val user = authRepository.getCurrentUser().first()
|
||||||
|
currentUserId = user?.id
|
||||||
|
if (currentUserId != null) {
|
||||||
|
loadRewards()
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
_uiState.value = _uiState.value.copy(
|
_uiState.value = _uiState.value.copy(
|
||||||
rewards = if (rewards.isEmpty()) _uiState.value.rewards else rewards,
|
errorMessage = e.message ?: "Ошибка загрузки пользователя"
|
||||||
isLoading = false
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun earnReward(userId: String, rewardId: String) {
|
fun loadRewards(isClaimed: Boolean? = null) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
rewardRepository.earnReward(userId, rewardId)
|
_uiState.value = _uiState.value.copy(isLoading = true, errorMessage = null)
|
||||||
|
|
||||||
|
try {
|
||||||
|
val loadResult = getRewardsUseCase.loadRewards(skip = 0, limit = 100, isClaimed = isClaimed)
|
||||||
|
if (loadResult.isFailure) {
|
||||||
|
_uiState.value = _uiState.value.copy(
|
||||||
|
isLoading = false,
|
||||||
|
errorMessage = loadResult.exceptionOrNull()?.message ?: "Ошибка загрузки наград"
|
||||||
|
)
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
|
||||||
|
val userId = currentUserId ?: return@launch
|
||||||
|
getRewardsUseCase(userId).collect { rewards ->
|
||||||
|
_uiState.value = _uiState.value.copy(
|
||||||
|
rewards = rewards,
|
||||||
|
isLoading = false,
|
||||||
|
errorMessage = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
_uiState.value = _uiState.value.copy(
|
||||||
|
isLoading = false,
|
||||||
|
errorMessage = e.message ?: "Ошибка загрузки наград"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun claimReward(rewardId: String) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
_uiState.value = _uiState.value.copy(isLoading = true, errorMessage = null)
|
||||||
|
|
||||||
|
// Оптимистичное обновление UI
|
||||||
|
val currentRewards = _uiState.value.rewards.toMutableList()
|
||||||
|
val rewardIndex = currentRewards.indexOfFirst { it.id == rewardId }
|
||||||
|
if (rewardIndex >= 0) {
|
||||||
|
val oldReward = currentRewards[rewardIndex]
|
||||||
|
if (oldReward.isClaimed) {
|
||||||
|
_uiState.value = _uiState.value.copy(
|
||||||
|
isLoading = false,
|
||||||
|
errorMessage = "Награда уже получена"
|
||||||
|
)
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
currentRewards[rewardIndex] = oldReward.copy(isClaimed = true)
|
||||||
|
_uiState.value = _uiState.value.copy(rewards = currentRewards)
|
||||||
|
}
|
||||||
|
|
||||||
|
val result = claimRewardUseCase(rewardId)
|
||||||
|
result.onSuccess {
|
||||||
|
_uiState.value = _uiState.value.copy(isLoading = false)
|
||||||
|
// Список обновится автоматически через Flow
|
||||||
|
}.onFailure { exception ->
|
||||||
|
// Откатываем изменение в случае ошибки
|
||||||
|
if (rewardIndex >= 0) {
|
||||||
|
val oldReward = currentRewards[rewardIndex]
|
||||||
|
currentRewards[rewardIndex] = oldReward.copy(isClaimed = false)
|
||||||
|
_uiState.value = _uiState.value.copy(
|
||||||
|
rewards = currentRewards,
|
||||||
|
isLoading = false,
|
||||||
|
errorMessage = exception.message ?: "Ошибка получения награды"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun showAddDialog() {
|
fun showAddDialog() {
|
||||||
_uiState.value = _uiState.value.copy(showAddDialog = true, newRewardTitle = "")
|
_uiState.value = _uiState.value.copy(
|
||||||
|
showAddDialog = true,
|
||||||
|
newRewardTitle = "",
|
||||||
|
newRewardDescription = "",
|
||||||
|
newRewardPoints = 10
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun hideAddDialog() {
|
fun hideAddDialog() {
|
||||||
_uiState.value = _uiState.value.copy(showAddDialog = false, newRewardTitle = "")
|
_uiState.value = _uiState.value.copy(
|
||||||
|
showAddDialog = false,
|
||||||
|
newRewardTitle = "",
|
||||||
|
newRewardDescription = "",
|
||||||
|
newRewardPoints = 10
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateNewRewardTitle(title: String) {
|
fun updateNewRewardTitle(title: String) {
|
||||||
_uiState.value = _uiState.value.copy(newRewardTitle = title)
|
_uiState.value = _uiState.value.copy(newRewardTitle = title)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun addReward(userId: String) {
|
fun updateNewRewardDescription(description: String) {
|
||||||
val title = _uiState.value.newRewardTitle.trim()
|
_uiState.value = _uiState.value.copy(newRewardDescription = description)
|
||||||
if (title.isBlank()) return
|
}
|
||||||
|
|
||||||
|
fun updateNewRewardPoints(points: Int) {
|
||||||
|
_uiState.value = _uiState.value.copy(newRewardPoints = points)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun createReward() {
|
||||||
|
viewModelScope.launch {
|
||||||
|
val state = _uiState.value
|
||||||
|
val userId = currentUserId ?: return@launch
|
||||||
|
|
||||||
|
if (state.newRewardTitle.isBlank()) return@launch
|
||||||
|
|
||||||
|
_uiState.value = _uiState.value.copy(isLoading = true, errorMessage = null)
|
||||||
|
|
||||||
val newReward = Reward(
|
val newReward = Reward(
|
||||||
id = "reward_${System.currentTimeMillis()}",
|
id = "", // Будет присвоен сервером
|
||||||
title = title,
|
title = state.newRewardTitle,
|
||||||
description = null,
|
description = state.newRewardDescription.takeIf { it.isNotBlank() },
|
||||||
imageUrl = null,
|
imageUrl = null,
|
||||||
points = 10, // Базовое количество очков
|
pointsRequired = state.newRewardPoints,
|
||||||
|
isClaimed = false,
|
||||||
earnedAt = null,
|
earnedAt = null,
|
||||||
userId = userId
|
userId = userId
|
||||||
)
|
)
|
||||||
|
|
||||||
// Добавляем новую награду в список
|
val result = createRewardUseCase(newReward)
|
||||||
val currentRewards = _uiState.value.rewards.toMutableList()
|
result.onSuccess {
|
||||||
currentRewards.add(newReward)
|
|
||||||
|
|
||||||
_uiState.value = _uiState.value.copy(
|
_uiState.value = _uiState.value.copy(
|
||||||
rewards = currentRewards,
|
isLoading = false,
|
||||||
showAddDialog = false,
|
showAddDialog = false,
|
||||||
newRewardTitle = ""
|
newRewardTitle = "",
|
||||||
|
newRewardDescription = "",
|
||||||
|
newRewardPoints = 10
|
||||||
)
|
)
|
||||||
|
// Список обновится автоматически через Flow
|
||||||
|
}.onFailure { exception ->
|
||||||
|
_uiState.value = _uiState.value.copy(
|
||||||
|
isLoading = false,
|
||||||
|
errorMessage = exception.message ?: "Ошибка создания награды"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun deleteReward(rewardId: String) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
_uiState.value = _uiState.value.copy(isLoading = true, errorMessage = null)
|
||||||
|
|
||||||
|
val result = deleteRewardUseCase(rewardId)
|
||||||
|
result.onSuccess {
|
||||||
|
_uiState.value = _uiState.value.copy(isLoading = false)
|
||||||
|
// Список обновится автоматически через Flow
|
||||||
|
}.onFailure { exception ->
|
||||||
|
_uiState.value = _uiState.value.copy(
|
||||||
|
isLoading = false,
|
||||||
|
errorMessage = exception.message ?: "Ошибка удаления награды"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clearError() {
|
||||||
|
_uiState.value = _uiState.value.copy(errorMessage = null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
data class RewardsUiState(
|
data class RewardsUiState(
|
||||||
val rewards: List<Reward> = emptyList(),
|
val rewards: List<Reward> = emptyList(),
|
||||||
val isLoading: Boolean = false,
|
val isLoading: Boolean = false,
|
||||||
|
val errorMessage: String? = null,
|
||||||
val showAddDialog: Boolean = false,
|
val showAddDialog: Boolean = false,
|
||||||
val newRewardTitle: String = ""
|
val newRewardTitle: String = "",
|
||||||
|
val newRewardDescription: String = "",
|
||||||
|
val newRewardPoints: Int = 10
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user