Добавил логику с наградами
This commit is contained in:
@@ -10,8 +10,11 @@ data class RewardEntity(
|
||||
val title: String,
|
||||
val description: String?,
|
||||
val imageUrl: String?,
|
||||
val points: Int,
|
||||
val pointsRequired: Int,
|
||||
val isClaimed: Boolean = false,
|
||||
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,
|
||||
description = description,
|
||||
imageUrl = imageUrl,
|
||||
points = points,
|
||||
pointsRequired = pointsRequired,
|
||||
isClaimed = isClaimed,
|
||||
earnedAt = earnedAt?.let {
|
||||
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,
|
||||
description = description,
|
||||
imageUrl = imageUrl,
|
||||
points = points,
|
||||
pointsRequired = pointsRequired,
|
||||
isClaimed = isClaimed,
|
||||
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.ChatResponse
|
||||
import com.novayaplaneta.data.remote.dto.ClaimRewardResponse
|
||||
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.CreateTaskRequest
|
||||
import com.novayaplaneta.data.remote.dto.LoginRequest
|
||||
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.TaskDto
|
||||
import retrofit2.Response
|
||||
@@ -79,5 +82,33 @@ interface BackendApi {
|
||||
@Path("task_id") taskId: String,
|
||||
@Query("completed") completed: Boolean
|
||||
): 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.mapper.toDomain
|
||||
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.repository.RewardRepository
|
||||
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 java.time.LocalDateTime
|
||||
import javax.inject.Inject
|
||||
|
||||
class RewardRepositoryImpl @Inject constructor(
|
||||
private val rewardDao: RewardDao
|
||||
private val rewardDao: RewardDao,
|
||||
private val backendApi: BackendApi
|
||||
) : RewardRepository {
|
||||
|
||||
override fun getRewards(userId: String): Flow<List<Reward>> {
|
||||
return rewardDao.getRewardsByUserId(userId).map { rewards ->
|
||||
rewards.map { it.toDomain() }
|
||||
}
|
||||
// Кэш наград
|
||||
private val _rewardsCache = MutableStateFlow<List<Reward>>(emptyList())
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
override suspend fun createReward(reward: Reward) {
|
||||
// Сохраняем в локальную БД
|
||||
rewards.forEach { reward ->
|
||||
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) {
|
||||
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)
|
||||
|
||||
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) {
|
||||
val reward = getRewardById(rewardId)
|
||||
reward?.let {
|
||||
updateReward(it.copy(earnedAt = LocalDateTime.now()))
|
||||
override suspend fun claimReward(rewardId: String): Result<Reward> {
|
||||
return try {
|
||||
val response = backendApi.claimReward(rewardId)
|
||||
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 description: String?,
|
||||
val imageUrl: String?,
|
||||
val points: Int,
|
||||
val pointsRequired: Int,
|
||||
val isClaimed: Boolean = false,
|
||||
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
|
||||
|
||||
interface RewardRepository {
|
||||
suspend fun loadRewards(skip: Int = 0, limit: Int = 100, isClaimed: Boolean? = null): Result<Unit>
|
||||
fun getRewards(userId: String): Flow<List<Reward>>
|
||||
suspend fun getRewardById(id: String): Reward?
|
||||
suspend fun createReward(reward: Reward)
|
||||
suspend fun getRewardById(id: String): Result<Reward>
|
||||
suspend fun createReward(reward: Reward): Result<Reward>
|
||||
suspend fun updateReward(reward: Reward)
|
||||
suspend fun deleteReward(id: String)
|
||||
suspend fun earnReward(userId: String, rewardId: String)
|
||||
suspend fun deleteReward(id: String): Result<Unit>
|
||||
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.items
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
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.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalConfiguration
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
@@ -181,8 +181,26 @@ fun RewardsScreen(
|
||||
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(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center
|
||||
@@ -199,7 +217,10 @@ fun RewardsScreen(
|
||||
items(uiState.rewards) { reward ->
|
||||
RewardCard(
|
||||
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) {
|
||||
AddRewardDialog(
|
||||
rewardTitle = uiState.newRewardTitle,
|
||||
rewardDescription = uiState.newRewardDescription,
|
||||
rewardPoints = uiState.newRewardPoints,
|
||||
onTitleChange = { viewModel.updateNewRewardTitle(it) },
|
||||
onAdd = { viewModel.addReward("default") },
|
||||
onDescriptionChange = { viewModel.updateNewRewardDescription(it) },
|
||||
onPointsChange = { viewModel.updateNewRewardPoints(it) },
|
||||
onAdd = { viewModel.createReward() },
|
||||
onDismiss = { viewModel.hideAddDialog() },
|
||||
accentGreen = accentGreen,
|
||||
screenHeightDp = screenHeightDp
|
||||
screenHeightDp = screenHeightDp,
|
||||
isLoading = uiState.isLoading
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -270,16 +296,20 @@ fun NavItem(
|
||||
@Composable
|
||||
fun RewardCard(
|
||||
reward: com.novayaplaneta.domain.model.Reward,
|
||||
screenHeightDp: Int
|
||||
screenHeightDp: Int,
|
||||
accentGreen: Color,
|
||||
onClaimClick: () -> Unit = {},
|
||||
onDeleteClick: () -> Unit = {}
|
||||
) {
|
||||
val cardHeight = 180.dp
|
||||
val isClaimed = reward.isClaimed
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(cardHeight)
|
||||
.clip(RoundedCornerShape(16.dp))
|
||||
.background(color = Color.LightGray)
|
||||
.background(color = if (isClaimed) accentGreen.copy(alpha = 0.3f) else Color.LightGray)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxSize()
|
||||
@@ -289,14 +319,14 @@ fun RewardCard(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.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
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Star,
|
||||
contentDescription = reward.title,
|
||||
modifier = Modifier.size(60.dp),
|
||||
tint = AccentGold
|
||||
tint = if (isClaimed) accentGreen else AccentGold
|
||||
)
|
||||
}
|
||||
|
||||
@@ -305,9 +335,12 @@ fun RewardCard(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.weight(0.4f)
|
||||
.background(color = Color.LightGray)
|
||||
.background(color = if (isClaimed) accentGreen.copy(alpha = 0.3f) else Color.LightGray)
|
||||
.padding(12.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
val textSize = (screenHeightDp * 0.02f).toInt().coerceIn(16, 24).sp
|
||||
Text(
|
||||
@@ -317,6 +350,33 @@ fun RewardCard(
|
||||
color = Color.Black,
|
||||
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
|
||||
fun AddRewardDialog(
|
||||
rewardTitle: String,
|
||||
rewardDescription: String,
|
||||
rewardPoints: Int,
|
||||
onTitleChange: (String) -> Unit,
|
||||
onDescriptionChange: (String) -> Unit,
|
||||
onPointsChange: (Int) -> Unit,
|
||||
onAdd: () -> Unit,
|
||||
onDismiss: () -> Unit,
|
||||
accentGreen: Color,
|
||||
screenHeightDp: Int
|
||||
screenHeightDp: Int,
|
||||
isLoading: Boolean = false
|
||||
) {
|
||||
Dialog(onDismissRequest = onDismiss) {
|
||||
Card(
|
||||
@@ -393,6 +458,7 @@ fun AddRewardDialog(
|
||||
OutlinedTextField(
|
||||
value = rewardTitle,
|
||||
onValueChange = onTitleChange,
|
||||
label = { Text("Название") },
|
||||
placeholder = {
|
||||
Text(
|
||||
text = "Введите название награды",
|
||||
@@ -402,6 +468,7 @@ fun AddRewardDialog(
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true,
|
||||
enabled = !isLoading,
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text),
|
||||
colors = OutlinedTextFieldDefaults.colors(
|
||||
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(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
@@ -445,7 +555,7 @@ fun AddRewardDialog(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.height(56.dp),
|
||||
enabled = rewardTitle.isNotBlank(),
|
||||
enabled = rewardTitle.isNotBlank() && !isLoading,
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = accentGreen,
|
||||
@@ -454,6 +564,12 @@ fun AddRewardDialog(
|
||||
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 = "Добавить",
|
||||
@@ -466,4 +582,5 @@ fun AddRewardDialog(
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,166 +3,221 @@ package com.novayaplaneta.ui.screens.rewards
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
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 kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class 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() {
|
||||
|
||||
private val _uiState = MutableStateFlow(RewardsUiState())
|
||||
val uiState: StateFlow<RewardsUiState> = _uiState.asStateFlow()
|
||||
|
||||
private var currentUserId: String? = null
|
||||
|
||||
init {
|
||||
loadDefaultRewards()
|
||||
loadUserId()
|
||||
}
|
||||
|
||||
private fun loadDefaultRewards() {
|
||||
// Создаем список наград по умолчанию
|
||||
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) {
|
||||
private fun loadUserId() {
|
||||
viewModelScope.launch {
|
||||
_uiState.value = _uiState.value.copy(isLoading = true)
|
||||
rewardRepository.getRewards(userId).collect { rewards ->
|
||||
try {
|
||||
val user = authRepository.getCurrentUser().first()
|
||||
currentUserId = user?.id
|
||||
if (currentUserId != null) {
|
||||
loadRewards()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
_uiState.value = _uiState.value.copy(
|
||||
rewards = if (rewards.isEmpty()) _uiState.value.rewards else rewards,
|
||||
isLoading = false
|
||||
errorMessage = e.message ?: "Ошибка загрузки пользователя"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun earnReward(userId: String, rewardId: String) {
|
||||
fun loadRewards(isClaimed: Boolean? = null) {
|
||||
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() {
|
||||
_uiState.value = _uiState.value.copy(showAddDialog = true, newRewardTitle = "")
|
||||
_uiState.value = _uiState.value.copy(
|
||||
showAddDialog = true,
|
||||
newRewardTitle = "",
|
||||
newRewardDescription = "",
|
||||
newRewardPoints = 10
|
||||
)
|
||||
}
|
||||
|
||||
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) {
|
||||
_uiState.value = _uiState.value.copy(newRewardTitle = title)
|
||||
}
|
||||
|
||||
fun addReward(userId: String) {
|
||||
val title = _uiState.value.newRewardTitle.trim()
|
||||
if (title.isBlank()) return
|
||||
fun updateNewRewardDescription(description: String) {
|
||||
_uiState.value = _uiState.value.copy(newRewardDescription = description)
|
||||
}
|
||||
|
||||
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(
|
||||
id = "reward_${System.currentTimeMillis()}",
|
||||
title = title,
|
||||
description = null,
|
||||
id = "", // Будет присвоен сервером
|
||||
title = state.newRewardTitle,
|
||||
description = state.newRewardDescription.takeIf { it.isNotBlank() },
|
||||
imageUrl = null,
|
||||
points = 10, // Базовое количество очков
|
||||
pointsRequired = state.newRewardPoints,
|
||||
isClaimed = false,
|
||||
earnedAt = null,
|
||||
userId = userId
|
||||
)
|
||||
|
||||
// Добавляем новую награду в список
|
||||
val currentRewards = _uiState.value.rewards.toMutableList()
|
||||
currentRewards.add(newReward)
|
||||
|
||||
val result = createRewardUseCase(newReward)
|
||||
result.onSuccess {
|
||||
_uiState.value = _uiState.value.copy(
|
||||
rewards = currentRewards,
|
||||
isLoading = 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(
|
||||
val rewards: List<Reward> = emptyList(),
|
||||
val isLoading: Boolean = false,
|
||||
val errorMessage: String? = null,
|
||||
val showAddDialog: Boolean = false,
|
||||
val newRewardTitle: String = ""
|
||||
val newRewardTitle: String = "",
|
||||
val newRewardDescription: String = "",
|
||||
val newRewardPoints: Int = 10
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user