Добавил логику с наградами

This commit is contained in:
2025-12-25 22:49:00 +03:00
parent 4ff516d06a
commit 2de916cfd9
17 changed files with 715 additions and 169 deletions

View File

@@ -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
)

View File

@@ -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()
)
}

View File

@@ -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>
}

View File

@@ -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
)

View File

@@ -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
)

View File

@@ -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
)

View File

@@ -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
)
}

View File

@@ -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
}
// Сохраняем в локальную БД
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 suspend fun getRewardById(id: String): Reward? {
return rewardDao.getRewardById(id)?.toDomain()
override fun getRewards(userId: String): Flow<List<Reward>> {
return _rewardsCache.asStateFlow()
}
override suspend fun createReward(reward: Reward) {
rewardDao.insertReward(reward.toEntity())
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) {
rewardDao.deleteReward(id)
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)
}
}
}

View File

@@ -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
)

View File

@@ -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>
}

View File

@@ -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)
}
}

View File

@@ -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)
}
}

View File

@@ -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)
}
}

View File

@@ -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)
}
}

View File

@@ -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)
}
}
}

View File

@@ -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,18 +335,48 @@ 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
) {
val textSize = (screenHeightDp * 0.02f).toInt().coerceIn(16, 24).sp
Text(
text = reward.title,
fontSize = textSize,
fontWeight = FontWeight.Bold,
color = Color.Black,
textAlign = TextAlign.Center
)
Column(
horizontalAlignment = Alignment.CenterHorizontally
) {
val textSize = (screenHeightDp * 0.02f).toInt().coerceIn(16, 24).sp
Text(
text = reward.title,
fontSize = textSize,
fontWeight = FontWeight.Bold,
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,12 +564,19 @@ fun AddRewardDialog(
disabledContentColor = Color.Gray
)
) {
val buttonTextSize = (screenHeightDp * 0.022f).toInt().coerceIn(18, 26).sp
Text(
text = "Добавить",
fontSize = buttonTextSize,
fontWeight = FontWeight.Bold
)
if (isLoading) {
CircularProgressIndicator(
modifier = Modifier.size(24.dp),
color = Color.White
)
} else {
val buttonTextSize = (screenHeightDp * 0.022f).toInt().coerceIn(18, 26).sp
Text(
text = "Добавить",
fontSize = buttonTextSize,
fontWeight = FontWeight.Bold
)
}
}
}
}

View File

@@ -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
val newReward = Reward(
id = "reward_${System.currentTimeMillis()}",
title = title,
description = null,
imageUrl = null,
points = 10, // Базовое количество очков
earnedAt = null,
userId = userId
)
// Добавляем новую награду в список
val currentRewards = _uiState.value.rewards.toMutableList()
currentRewards.add(newReward)
_uiState.value = _uiState.value.copy(
rewards = currentRewards,
showAddDialog = false,
newRewardTitle = ""
)
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 = "", // Будет присвоен сервером
title = state.newRewardTitle,
description = state.newRewardDescription.takeIf { it.isNotBlank() },
imageUrl = null,
pointsRequired = state.newRewardPoints,
isClaimed = false,
earnedAt = null,
userId = userId
)
val result = createRewardUseCase(newReward)
result.onSuccess {
_uiState.value = _uiState.value.copy(
isLoading = false,
showAddDialog = false,
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
)