diff --git a/app/src/main/java/com/resolum/intiva/features/finances/presentation/HomeScreen.kt b/app/src/main/java/com/resolum/intiva/features/finances/presentation/HomeScreen.kt index 1823e08..d32fcb7 100644 --- a/app/src/main/java/com/resolum/intiva/features/finances/presentation/HomeScreen.kt +++ b/app/src/main/java/com/resolum/intiva/features/finances/presentation/HomeScreen.kt @@ -15,6 +15,8 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons @@ -65,6 +67,7 @@ import com.resolum.intiva.features.finances.presentation.spendinglimits.Spending import com.resolum.intiva.features.finances.presentation.spendinglimits.components.SpendingLimitCard import com.resolum.intiva.features.finances.presentation.transactions.TransactionViewModel import com.resolum.intiva.features.finances.presentation.transactions.components.TransactionItem +import com.resolum.intiva.features.paymentmethodsandcategories.presentation.category.CategoryViewModel import com.resolum.intiva.features.profiles.domain.models.FirstTransactionTutorialStep import com.resolum.intiva.features.profiles.presentation.onboarding.OnboardingViewModel import com.resolum.intiva.features.profiles.presentation.onboarding.components.FirstTransactionPresentationOverlay @@ -97,6 +100,7 @@ fun HomeScreen( navController: NavController, viewModel: TransactionViewModel = hiltViewModel(), spendingLimitViewModel: SpendingLimitViewModel = hiltViewModel(), + categoryViewModel: CategoryViewModel = hiltViewModel(), profileViewModel: ProfileViewModel = hiltViewModel(), onNavigateToTransactions: () -> Unit, onNavigateToSpendingLimitAlert: () -> Unit = {}, @@ -105,6 +109,7 @@ fun HomeScreen( val snackBarHostState = remember { SnackbarHostState() } val uiState by viewModel.uiState.collectAsState() val spendingLimitUiState by spendingLimitViewModel.uiState.collectAsState() + val categoryUiState by categoryViewModel.uiState.collectAsState() val profileUiState by profileViewModel.uiState.collectAsState() val onboardingViewModel: OnboardingViewModel = hiltViewModel() val onboardingState by onboardingViewModel.state.collectAsState() @@ -116,6 +121,11 @@ fun HomeScreen( onboardingViewModel.loadStatus() viewModel.getTransactionsByOwnerId(onlyLatest = true) spendingLimitViewModel.loadMonthlySpendingLimit() + spendingLimitViewModel.loadSpendingLimits() + categoryViewModel.getCategories( + ownerType = com.resolum.intiva.features.shared.domain.model.OwnerType.INDIVIDUAL.name, + type = com.resolum.intiva.features.finances.domain.models.TransactionType.EXPENSE.name + ) profileViewModel.loadProfile() val success = navController.currentBackStackEntry @@ -323,11 +333,52 @@ fun HomeScreen( } item { - SpendingLimitCard( - state = spendingLimitUiState.spendingLimitState, - onRetry = { spendingLimitViewModel.loadMonthlySpendingLimit() }, - onOpenAlert = onNavigateToSpendingLimitAlert - ) + when (val state = spendingLimitUiState.spendingLimitsState) { + is UiState.Success -> { + if (state.data.isEmpty()) { + SpendingLimitCard( + state = UiState.Idle, + onRetry = { spendingLimitViewModel.loadSpendingLimits() }, + onOpenAlert = onNavigateToSpendingLimitAlert + ) + } else { + LazyRow( + horizontalArrangement = Arrangement.spacedBy(12.dp), + contentPadding = PaddingValues(horizontal = 0.dp) + ) { + items(state.data) { summary -> + SpendingLimitCard( + state = UiState.Success(summary), + category = categoryUiState.categories.firstOrNull { + it.id == summary.limit.targetId + }, + onRetry = { spendingLimitViewModel.loadSpendingLimits() }, + onOpenAlert = onNavigateToSpendingLimitAlert, + modifier = Modifier.fillParentMaxWidth(0.92f) + ) + } + } + } + } + + is UiState.Loading -> SpendingLimitCard( + state = UiState.Loading, + onRetry = { spendingLimitViewModel.loadSpendingLimits() }, + onOpenAlert = onNavigateToSpendingLimitAlert + ) + + is UiState.Error -> SpendingLimitCard( + state = UiState.Error(message = state.message, throwable = state.throwable), + onRetry = { spendingLimitViewModel.loadSpendingLimits() }, + onOpenAlert = onNavigateToSpendingLimitAlert + ) + + is UiState.Idle -> SpendingLimitCard( + state = spendingLimitUiState.spendingLimitState, + onRetry = { spendingLimitViewModel.loadSpendingLimits() }, + onOpenAlert = onNavigateToSpendingLimitAlert + ) + } } item { diff --git a/app/src/main/java/com/resolum/intiva/features/finances/presentation/spendinglimits/SpendingLimitContent.kt b/app/src/main/java/com/resolum/intiva/features/finances/presentation/spendinglimits/SpendingLimitContent.kt index f3f7ce5..6e9126b 100644 --- a/app/src/main/java/com/resolum/intiva/features/finances/presentation/spendinglimits/SpendingLimitContent.kt +++ b/app/src/main/java/com/resolum/intiva/features/finances/presentation/spendinglimits/SpendingLimitContent.kt @@ -3,6 +3,7 @@ package com.resolum.intiva.features.finances.presentation.spendinglimits import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth @@ -12,12 +13,14 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Add import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -29,6 +32,8 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.resolum.intiva.core.common.state.UiState import com.resolum.intiva.core.ui.theme.IntivaColors +import com.resolum.intiva.features.finances.domain.models.SpendingLimit +import com.resolum.intiva.features.finances.presentation.spendinglimits.components.AdjustSpendingLimitSheet import com.resolum.intiva.features.finances.presentation.spendinglimits.components.BudgetSummaryCard import com.resolum.intiva.features.finances.presentation.spendinglimits.components.EmptyLimitsCard import com.resolum.intiva.features.finances.presentation.spendinglimits.components.MessageCard @@ -41,15 +46,20 @@ fun SpendingLimitListContent( limitsState: UiState>, categories: List, onAddClick: () -> Unit, + updateState: UiState, + selectedLimitToAdjust: SpendingLimitSummary?, + onAdjustClick: (SpendingLimitSummary) -> Unit, + onDismissAdjust: () -> Unit, + onSaveAdjust: (SpendingLimit, String, SpendingLimitFrequency, Boolean) -> Unit, onBack: () -> Unit ) { Scaffold( - containerColor = Color(0xFFFAF7FF), + containerColor = IntivaColors.BackgroundDefault, floatingActionButton = { FloatingActionButton( onClick = onAddClick, - containerColor = IntivaColors.PrimaryGreen, - contentColor = IntivaColors.TextPrimary, + containerColor = IntivaColors.PrimaryBrand, + contentColor = IntivaColors.TextInverse, shape = CircleShape ) { Icon( @@ -65,16 +75,28 @@ fun SpendingLimitListContent( .fillMaxSize() .padding(padding) .padding(horizontal = 24.dp), - contentPadding = PaddingValues(top = 32.dp, bottom = 112.dp), + contentPadding = PaddingValues(top = 18.dp, bottom = 112.dp), verticalArrangement = Arrangement.spacedBy(18.dp) ) { item { - Text( - text = "Límites de Gasto", - fontSize = 32.sp, - fontWeight = FontWeight.Bold, - color = IntivaColors.TextPrimary - ) + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically + ) { + IconButton(onClick = onBack) { + Icon( + imageVector = Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = "Volver", + tint = IntivaColors.TextPrimary + ) + } + Text( + text = "Límites de Gasto", + fontSize = 30.sp, + fontWeight = FontWeight.Bold, + color = IntivaColors.TextPrimary + ) + } Spacer(modifier = Modifier.height(6.dp)) Text( text = "Monitorea tus presupuestos por categoría este mes.", @@ -109,7 +131,8 @@ fun SpendingLimitListContent( items(summaries) { summary -> SpendingLimitListItem( summary = summary, - category = categories.firstOrNull { it.id == summary.limit.targetId } + category = categories.firstOrNull { it.id == summary.limit.targetId }, + onAdjustClick = { onAdjustClick(summary) } ) } } @@ -135,4 +158,15 @@ fun SpendingLimitListContent( } } } + + selectedLimitToAdjust?.let { summary -> + AdjustSpendingLimitSheet( + limit = summary.limit, + updateState = updateState, + onDismiss = onDismissAdjust, + onSave = { amount, frequency, updatePeriod -> + onSaveAdjust(summary.limit, amount, frequency, updatePeriod) + } + ) + } } diff --git a/app/src/main/java/com/resolum/intiva/features/finances/presentation/spendinglimits/SpendingLimitScreen.kt b/app/src/main/java/com/resolum/intiva/features/finances/presentation/spendinglimits/SpendingLimitScreen.kt index 119df3a..3727816 100644 --- a/app/src/main/java/com/resolum/intiva/features/finances/presentation/spendinglimits/SpendingLimitScreen.kt +++ b/app/src/main/java/com/resolum/intiva/features/finances/presentation/spendinglimits/SpendingLimitScreen.kt @@ -23,6 +23,7 @@ fun SpendingLimitScreen( val uiState by viewModel.uiState.collectAsState() val categoryUiState by categoryViewModel.uiState.collectAsState() var showCreate by remember { mutableStateOf(false) } + var selectedLimitToAdjust by remember { mutableStateOf(null) } LaunchedEffect(Unit) { categoryViewModel.getCategories( @@ -39,6 +40,13 @@ fun SpendingLimitScreen( } } + LaunchedEffect(uiState.updateState) { + if (uiState.updateState is UiState.Success) { + selectedLimitToAdjust = null + viewModel.clearUpdateState() + } + } + if (showCreate) { SpendingLimitCreateContent( createState = uiState.createState, @@ -59,6 +67,21 @@ fun SpendingLimitScreen( limitsState = uiState.spendingLimitsState, categories = categoryUiState.categories, onAddClick = { showCreate = true }, + updateState = uiState.updateState, + selectedLimitToAdjust = selectedLimitToAdjust, + onAdjustClick = { selectedLimitToAdjust = it }, + onDismissAdjust = { + selectedLimitToAdjust = null + viewModel.clearUpdateState() + }, + onSaveAdjust = { limit, amount, frequency, updatePeriod -> + viewModel.updateSpendingLimit( + limit = limit, + amount = amount, + frequency = frequency, + updatePeriod = updatePeriod + ) + }, onBack = onNavigateBack ) } diff --git a/app/src/main/java/com/resolum/intiva/features/finances/presentation/spendinglimits/SpendingLimitUiState.kt b/app/src/main/java/com/resolum/intiva/features/finances/presentation/spendinglimits/SpendingLimitUiState.kt index da32e10..025ae34 100644 --- a/app/src/main/java/com/resolum/intiva/features/finances/presentation/spendinglimits/SpendingLimitUiState.kt +++ b/app/src/main/java/com/resolum/intiva/features/finances/presentation/spendinglimits/SpendingLimitUiState.kt @@ -8,7 +8,8 @@ import java.math.RoundingMode data class SpendingLimitUiState( val spendingLimitState: UiState = UiState.Idle, val spendingLimitsState: UiState> = UiState.Idle, - val createState: UiState = UiState.Idle + val createState: UiState = UiState.Idle, + val updateState: UiState = UiState.Idle ) data class SpendingLimitSummary( diff --git a/app/src/main/java/com/resolum/intiva/features/finances/presentation/spendinglimits/SpendingLimitViewModel.kt b/app/src/main/java/com/resolum/intiva/features/finances/presentation/spendinglimits/SpendingLimitViewModel.kt index 276343c..63c24f7 100644 --- a/app/src/main/java/com/resolum/intiva/features/finances/presentation/spendinglimits/SpendingLimitViewModel.kt +++ b/app/src/main/java/com/resolum/intiva/features/finances/presentation/spendinglimits/SpendingLimitViewModel.kt @@ -4,10 +4,15 @@ import com.resolum.intiva.core.common.state.UiState import com.resolum.intiva.core.common.viewmodel.BaseViewModel import com.resolum.intiva.core.network.model.NetworkResult import com.resolum.intiva.features.finances.domain.models.CreateSpendingLimitRequest +import com.resolum.intiva.features.finances.domain.models.SpendingLimit import com.resolum.intiva.features.finances.domain.models.SpendingLimitOwnerType import com.resolum.intiva.features.finances.domain.models.SpendingLimitTargetType +import com.resolum.intiva.features.finances.domain.models.UpdateSpendingLimitAmountRequest +import com.resolum.intiva.features.finances.domain.models.UpdateSpendingLimitPeriodRequest import com.resolum.intiva.features.finances.domain.usecase.CreateSpendingLimitUseCase import com.resolum.intiva.features.finances.domain.usecase.GetSpendingLimitsUseCase +import com.resolum.intiva.features.finances.domain.usecase.UpdateSpendingLimitAmountUseCase +import com.resolum.intiva.features.finances.domain.usecase.UpdateSpendingLimitPeriodUseCase import com.resolum.intiva.features.iam.domain.repositories.SessionRepository import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow @@ -23,6 +28,8 @@ import javax.inject.Inject class SpendingLimitViewModel @Inject constructor( private val getSpendingLimitsUseCase: GetSpendingLimitsUseCase, private val createSpendingLimitUseCase: CreateSpendingLimitUseCase, + private val updateSpendingLimitAmountUseCase: UpdateSpendingLimitAmountUseCase, + private val updateSpendingLimitPeriodUseCase: UpdateSpendingLimitPeriodUseCase, private val sessionRepository: SessionRepository ) : BaseViewModel() { @@ -185,6 +192,94 @@ class SpendingLimitViewModel @Inject constructor( _uiState.update { it.copy(createState = UiState.Idle) } } + fun updateSpendingLimit( + limit: SpendingLimit, + amount: String, + frequency: SpendingLimitFrequency, + updatePeriod: Boolean + ) { + safeLaunch { + val limitAmount = amount.toBigDecimalOrNull() + if (limitAmount == null || limitAmount <= BigDecimal.ZERO) { + _uiState.update { + it.copy(updateState = UiState.Error("Ingresa un monto válido.")) + } + return@safeLaunch + } + + val period = frequency.toPeriod() + val amountChanged = limitAmount.compareTo(limit.limitAmount) != 0 + val periodChanged = updatePeriod && + (period.first.toString() != limit.startDate || period.second.toString() != limit.endDate) + + if (!amountChanged && !periodChanged) { + _uiState.update { it.copy(updateState = UiState.Success(limit)) } + return@safeLaunch + } + + _uiState.update { it.copy(updateState = UiState.Loading) } + + if (amountChanged) { + val amountResult = updateSpendingLimitAmountUseCase( + spendingLimitId = limit.id, + request = UpdateSpendingLimitAmountRequest( + limitAmount = limitAmount, + currencyCode = limit.currencyCode + ) + ) + + if (amountResult is NetworkResult.Error) { + _uiState.update { + it.copy( + updateState = UiState.Error( + message = amountResult.message, + throwable = amountResult.throwable + ) + ) + } + return@safeLaunch + } + } + + if (periodChanged) { + when ( + val periodResult = updateSpendingLimitPeriodUseCase( + spendingLimitId = limit.id, + request = UpdateSpendingLimitPeriodRequest( + startDate = period.first.toString(), + endDate = period.second.toString() + ) + ) + ) { + is NetworkResult.Success -> { + _uiState.update { it.copy(updateState = UiState.Success(periodResult.data)) } + } + + is NetworkResult.Error -> { + _uiState.update { + it.copy( + updateState = UiState.Error( + message = periodResult.message, + throwable = periodResult.throwable + ) + ) + } + return@safeLaunch + } + } + } else { + _uiState.update { it.copy(updateState = UiState.Success(limit.copy(limitAmount = limitAmount))) } + } + + loadSpendingLimits() + loadMonthlySpendingLimit() + } + } + + fun clearUpdateState() { + _uiState.update { it.copy(updateState = UiState.Idle) } + } + override fun handleError(throwable: Throwable) { _uiState.update { it.copy( diff --git a/app/src/main/java/com/resolum/intiva/features/finances/presentation/spendinglimits/components/AdjustSpendingLimitSheet.kt b/app/src/main/java/com/resolum/intiva/features/finances/presentation/spendinglimits/components/AdjustSpendingLimitSheet.kt new file mode 100644 index 0000000..889b573 --- /dev/null +++ b/app/src/main/java/com/resolum/intiva/features/finances/presentation/spendinglimits/components/AdjustSpendingLimitSheet.kt @@ -0,0 +1,180 @@ +package com.resolum.intiva.features.finances.presentation.spendinglimits.components + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.CheckCircle +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.OutlinedTextFieldDefaults +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.resolum.intiva.core.common.state.UiState +import com.resolum.intiva.core.ui.theme.IntivaColors +import com.resolum.intiva.features.finances.domain.models.SpendingLimit +import com.resolum.intiva.features.finances.presentation.spendinglimits.SpendingLimitFrequency +import java.math.BigDecimal +import java.time.LocalDate +import java.time.temporal.ChronoUnit + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun AdjustSpendingLimitSheet( + limit: SpendingLimit, + updateState: UiState, + onDismiss: () -> Unit, + onSave: (String, SpendingLimitFrequency, Boolean) -> Unit +) { + var amount by remember(limit.id) { mutableStateOf(formatAmount(limit.limitAmount)) } + val initialFrequency = remember(limit.id, limit.startDate, limit.endDate) { + inferFrequency(limit) + } + var frequency by remember(limit.id) { mutableStateOf(initialFrequency) } + val isLoading = updateState is UiState.Loading + + ModalBottomSheet( + onDismissRequest = onDismiss, + containerColor = IntivaColors.BackgroundDefault + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 24.dp) + .padding(bottom = 28.dp) + ) { + Text( + text = "Ajustar límite", + fontSize = 24.sp, + fontWeight = FontWeight.Bold, + color = IntivaColors.TextPrimary + ) + Spacer(modifier = Modifier.height(6.dp)) + Text( + text = "Actualiza el presupuesto y el periodo activo.", + fontSize = 14.sp, + color = IntivaColors.TextSecondary + ) + Spacer(modifier = Modifier.height(22.dp)) + + OutlinedTextField( + value = amount, + onValueChange = { value -> + amount = value.filter { it.isDigit() || it == '.' } + }, + modifier = Modifier.fillMaxWidth(), + label = { Text("Monto máximo") }, + prefix = { Text("S/. ") }, + singleLine = true, + shape = RoundedCornerShape(14.dp), + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal), + colors = OutlinedTextFieldDefaults.colors( + focusedBorderColor = IntivaColors.PrimaryBrand, + focusedLabelColor = IntivaColors.PrimaryBrand, + cursorColor = IntivaColors.PrimaryBrand + ) + ) + + Spacer(modifier = Modifier.height(20.dp)) + Text( + text = "Frecuencia", + fontSize = 13.sp, + fontWeight = FontWeight.Bold, + color = IntivaColors.TextSecondary + ) + Spacer(modifier = Modifier.height(10.dp)) + Row(horizontalArrangement = Arrangement.spacedBy(10.dp)) { + SpendingLimitFrequency.entries.forEach { option -> + FrequencyButton( + frequency = option, + selected = frequency == option, + onClick = { frequency = option }, + modifier = Modifier.weight(1f) + ) + } + } + + if (updateState is UiState.Error) { + Spacer(modifier = Modifier.height(14.dp)) + Text( + text = updateState.message, + color = IntivaColors.StatusError, + fontSize = 13.sp, + fontWeight = FontWeight.Bold + ) + } + + Spacer(modifier = Modifier.height(24.dp)) + Button( + onClick = { onSave(amount, frequency, frequency != initialFrequency) }, + enabled = amount.isNotBlank() && !isLoading, + modifier = Modifier + .fillMaxWidth() + .height(56.dp), + shape = RoundedCornerShape(14.dp), + colors = ButtonDefaults.buttonColors( + containerColor = IntivaColors.PrimaryBrand, + contentColor = IntivaColors.TextInverse, + disabledContainerColor = IntivaColors.BackgroundSurface, + disabledContentColor = IntivaColors.TextSecondary + ) + ) { + if (isLoading) { + CircularProgressIndicator( + modifier = Modifier.size(22.dp), + color = IntivaColors.TextInverse, + strokeWidth = 2.dp + ) + } else { + Text( + text = "Guardar cambios", + fontSize = 16.sp, + fontWeight = FontWeight.Bold + ) + Spacer(modifier = Modifier.size(8.dp)) + androidx.compose.material3.Icon( + imageVector = Icons.Default.CheckCircle, + contentDescription = null + ) + } + } + } + } +} + +private fun formatAmount(amount: BigDecimal): String { + return amount.stripTrailingZeros().toPlainString() +} + +private fun inferFrequency(limit: SpendingLimit): SpendingLimitFrequency { + val start = runCatching { LocalDate.parse(limit.startDate) }.getOrNull() + val end = runCatching { LocalDate.parse(limit.endDate) }.getOrNull() + if (start == null || end == null) return SpendingLimitFrequency.MONTHLY + + val inclusiveDays = ChronoUnit.DAYS.between(start, end) + 1 + return when { + inclusiveDays <= 7 -> SpendingLimitFrequency.WEEKLY + start.dayOfYear == 1 && end.dayOfYear == end.lengthOfYear() -> SpendingLimitFrequency.YEARLY + else -> SpendingLimitFrequency.MONTHLY + } +} diff --git a/app/src/main/java/com/resolum/intiva/features/finances/presentation/spendinglimits/components/AmountInputCard.kt b/app/src/main/java/com/resolum/intiva/features/finances/presentation/spendinglimits/components/AmountInputCard.kt index cf59f03..e63db57 100644 --- a/app/src/main/java/com/resolum/intiva/features/finances/presentation/spendinglimits/components/AmountInputCard.kt +++ b/app/src/main/java/com/resolum/intiva/features/finances/presentation/spendinglimits/components/AmountInputCard.kt @@ -30,7 +30,7 @@ fun AmountInputCard( ) { Card( modifier = Modifier.fillMaxWidth(), - shape = RoundedCornerShape(18.dp), + shape = RoundedCornerShape(20.dp), colors = CardDefaults.cardColors(containerColor = Color.White) ) { Row( @@ -74,4 +74,4 @@ fun AmountInputCard( ) } } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/resolum/intiva/features/finances/presentation/spendinglimits/components/BudgetSummaryCard.kt b/app/src/main/java/com/resolum/intiva/features/finances/presentation/spendinglimits/components/BudgetSummaryCard.kt index 9b5ce1e..01c74f3 100644 --- a/app/src/main/java/com/resolum/intiva/features/finances/presentation/spendinglimits/components/BudgetSummaryCard.kt +++ b/app/src/main/java/com/resolum/intiva/features/finances/presentation/spendinglimits/components/BudgetSummaryCard.kt @@ -40,9 +40,9 @@ fun BudgetSummaryCard(summaries: List) { Card( modifier = Modifier.fillMaxWidth(), - shape = RoundedCornerShape(24.dp), + shape = RoundedCornerShape(20.dp), colors = CardDefaults.cardColors(containerColor = Color.White), - elevation = CardDefaults.cardElevation(defaultElevation = 6.dp) + elevation = CardDefaults.cardElevation(defaultElevation = 3.dp) ) { Column(modifier = Modifier.padding(24.dp)) { Row( @@ -87,7 +87,7 @@ fun BudgetSummaryCard(summaries: List) { .height(8.dp) .clip(RoundedCornerShape(6.dp)), color = IntivaColors.PrimaryBrand, - trackColor = Color(0xFFE0DCE8) + trackColor = Color(0xFFE8E5EF) ) Spacer(modifier = Modifier.height(8.dp)) Row( @@ -119,4 +119,4 @@ private fun daysUntilMonthEnd(): Long { private fun formatCurrency(amount: BigDecimal): String { val formatter = DecimalFormat("#,##0.00") return "S/ ${formatter.format(amount)}" -} \ No newline at end of file +} diff --git a/app/src/main/java/com/resolum/intiva/features/finances/presentation/spendinglimits/components/EmptyLimitsCard.kt b/app/src/main/java/com/resolum/intiva/features/finances/presentation/spendinglimits/components/EmptyLimitsCard.kt index 6d585e5..e9c31d2 100644 --- a/app/src/main/java/com/resolum/intiva/features/finances/presentation/spendinglimits/components/EmptyLimitsCard.kt +++ b/app/src/main/java/com/resolum/intiva/features/finances/presentation/spendinglimits/components/EmptyLimitsCard.kt @@ -30,9 +30,9 @@ fun EmptyLimitsCard(onAddClick: () -> Unit) { modifier = Modifier .fillMaxWidth() .clickable { onAddClick() }, - shape = RoundedCornerShape(18.dp), + shape = RoundedCornerShape(20.dp), colors = CardDefaults.cardColors(containerColor = Color.White), - elevation = CardDefaults.cardElevation(defaultElevation = 4.dp) + elevation = CardDefaults.cardElevation(defaultElevation = 3.dp) ) { Column( modifier = Modifier.padding(24.dp), @@ -59,4 +59,4 @@ fun EmptyLimitsCard(onAddClick: () -> Unit) { ) } } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/resolum/intiva/features/finances/presentation/spendinglimits/components/FrequencyButton.kt b/app/src/main/java/com/resolum/intiva/features/finances/presentation/spendinglimits/components/FrequencyButton.kt index 9d41312..885a74a 100644 --- a/app/src/main/java/com/resolum/intiva/features/finances/presentation/spendinglimits/components/FrequencyButton.kt +++ b/app/src/main/java/com/resolum/intiva/features/finances/presentation/spendinglimits/components/FrequencyButton.kt @@ -1,6 +1,7 @@ package com.resolum.intiva.features.finances.presentation.spendinglimits.components import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width @@ -32,26 +33,27 @@ fun FrequencyButton( modifier = modifier.height(54.dp), shape = RoundedCornerShape(28.dp), colors = ButtonDefaults.outlinedButtonColors( - containerColor = if (selected) Color(0xFFF4F0FF) else Color.White, - contentColor = if (selected) IntivaColors.PrimaryBrand else IntivaColors.TextSecondary + containerColor = if (selected) IntivaColors.PrimaryBrand else Color.White, + contentColor = if (selected) IntivaColors.TextInverse else IntivaColors.TextSecondary ), border = androidx.compose.foundation.BorderStroke( width = if (selected) 2.dp else 1.dp, color = if (selected) IntivaColors.PrimaryBrand else Color(0xFFD4CEDD) - ) + ), + contentPadding = PaddingValues(horizontal = 12.dp) ) { if (selected) { Icon( imageVector = Icons.Default.CheckCircle, contentDescription = null, - modifier = Modifier.size(16.dp) + modifier = Modifier.size(14.dp) ) - Spacer(modifier = Modifier.width(6.dp)) + Spacer(modifier = Modifier.width(4.dp)) } Text( text = frequency.label, - fontSize = 14.sp, + fontSize = if (selected) 13.sp else 14.sp, fontWeight = if (selected) FontWeight.Bold else FontWeight.Medium ) } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/resolum/intiva/features/finances/presentation/spendinglimits/components/SelectedCategoryCard.kt b/app/src/main/java/com/resolum/intiva/features/finances/presentation/spendinglimits/components/SelectedCategoryCard.kt index 1cbb989..0fa53ce 100644 --- a/app/src/main/java/com/resolum/intiva/features/finances/presentation/spendinglimits/components/SelectedCategoryCard.kt +++ b/app/src/main/java/com/resolum/intiva/features/finances/presentation/spendinglimits/components/SelectedCategoryCard.kt @@ -34,9 +34,9 @@ import com.resolum.intiva.features.paymentmethodsandcategories.presentation.cate fun SelectedCategoryCard(selectedCategory: Category?) { Card( modifier = Modifier.fillMaxWidth(), - shape = RoundedCornerShape(14.dp), + shape = RoundedCornerShape(18.dp), colors = CardDefaults.cardColors(containerColor = Color.White), - elevation = CardDefaults.cardElevation(defaultElevation = 2.dp) + elevation = CardDefaults.cardElevation(defaultElevation = 3.dp) ) { Row( modifier = Modifier.padding(16.dp), @@ -84,4 +84,4 @@ fun SelectedCategoryCard(selectedCategory: Category?) { } private fun parseColor(value: String?, fallback: Color): Color = - runCatching { Color(value?.toColorInt() ?: return@runCatching fallback) }.getOrElse { fallback } \ No newline at end of file + runCatching { Color(value?.toColorInt() ?: return@runCatching fallback) }.getOrElse { fallback } diff --git a/app/src/main/java/com/resolum/intiva/features/finances/presentation/spendinglimits/components/SpendingLimitCard.kt b/app/src/main/java/com/resolum/intiva/features/finances/presentation/spendinglimits/components/SpendingLimitCard.kt index ddd4f2e..1cd386b 100644 --- a/app/src/main/java/com/resolum/intiva/features/finances/presentation/spendinglimits/components/SpendingLimitCard.kt +++ b/app/src/main/java/com/resolum/intiva/features/finances/presentation/spendinglimits/components/SpendingLimitCard.kt @@ -11,16 +11,18 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import com.resolum.intiva.core.common.state.UiState import com.resolum.intiva.features.finances.presentation.spendinglimits.SpendingLimitSummary +import com.resolum.intiva.features.paymentmethodsandcategories.domain.models.Category @Composable fun SpendingLimitCard( state: UiState, + category: Category? = null, onRetry: () -> Unit, - onOpenAlert: () -> Unit + onOpenAlert: () -> Unit, + modifier: Modifier = Modifier.fillMaxWidth() ) { Card( - modifier = Modifier - .fillMaxWidth() + modifier = modifier .clickable { onOpenAlert() }, shape = RoundedCornerShape(12.dp), colors = CardDefaults.cardColors(containerColor = Color.White), @@ -32,7 +34,7 @@ fun SpendingLimitCard( ) { when (state) { is UiState.Loading -> SpendingLimitLoadingContent() - is UiState.Success -> SpendingLimitSuccessContent(summary = state.data) + is UiState.Success -> SpendingLimitSuccessContent(summary = state.data, category = category) is UiState.Error -> SpendingLimitErrorContent(onRetry = onRetry) is UiState.Idle -> SpendingLimitEmptyContent() } diff --git a/app/src/main/java/com/resolum/intiva/features/finances/presentation/spendinglimits/components/SpendingLimitCreateContent.kt b/app/src/main/java/com/resolum/intiva/features/finances/presentation/spendinglimits/components/SpendingLimitCreateContent.kt index 90aaf98..a737626 100644 --- a/app/src/main/java/com/resolum/intiva/features/finances/presentation/spendinglimits/components/SpendingLimitCreateContent.kt +++ b/app/src/main/java/com/resolum/intiva/features/finances/presentation/spendinglimits/components/SpendingLimitCreateContent.kt @@ -13,8 +13,8 @@ import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.filled.CheckCircle -import androidx.compose.material.icons.filled.Close import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.CircularProgressIndicator @@ -36,15 +36,10 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import androidx.core.graphics.toColorInt import com.resolum.intiva.core.common.state.UiState import com.resolum.intiva.core.ui.theme.IntivaColors import com.resolum.intiva.features.finances.presentation.spendinglimits.SpendingLimitFrequency import com.resolum.intiva.features.paymentmethodsandcategories.domain.models.Category -import java.math.BigDecimal -import java.text.DecimalFormat -import java.time.LocalDate -import java.time.temporal.ChronoUnit @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -70,8 +65,8 @@ fun SpendingLimitCreateContent( navigationIcon = { IconButton(onClick = onClose) { Icon( - imageVector = Icons.Default.Close, - contentDescription = "Cerrar", + imageVector = Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = "Volver", tint = Color.White ) } @@ -81,7 +76,7 @@ fun SpendingLimitCreateContent( ) ) }, - containerColor = Color(0xFFFAF7FF), + containerColor = IntivaColors.BackgroundDefault, bottomBar = { Button( onClick = { @@ -96,7 +91,7 @@ fun SpendingLimitCreateContent( .height(58.dp), shape = RoundedCornerShape(14.dp), colors = ButtonDefaults.buttonColors( - containerColor = IntivaColors.PrimaryGreen, + containerColor = IntivaColors.PrimaryAction, contentColor = IntivaColors.TextPrimary, disabledContainerColor = Color(0xFFE5E0EC), disabledContentColor = IntivaColors.TextSecondary @@ -134,8 +129,6 @@ fun SpendingLimitCreateContent( item { StepLabel(text = "PASO 1: SELECCIONA UN OBJETIVO") - SelectedCategoryCard(selectedCategory = selectedCategory) - Spacer(modifier = Modifier.height(12.dp)) SpendingLimitCategorySelector( selectedCategory = selectedCategory, onCategorySelected = { selectedCategory = it } diff --git a/app/src/main/java/com/resolum/intiva/features/finances/presentation/spendinglimits/components/SpendingLimitListItem.kt b/app/src/main/java/com/resolum/intiva/features/finances/presentation/spendinglimits/components/SpendingLimitListItem.kt index d58133d..69f753d 100644 --- a/app/src/main/java/com/resolum/intiva/features/finances/presentation/spendinglimits/components/SpendingLimitListItem.kt +++ b/app/src/main/java/com/resolum/intiva/features/finances/presentation/spendinglimits/components/SpendingLimitListItem.kt @@ -13,9 +13,14 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Edit +import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults +import androidx.compose.material3.Icon import androidx.compose.material3.LinearProgressIndicator +import androidx.compose.material3.OutlinedButton import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment @@ -36,7 +41,8 @@ import java.text.DecimalFormat @Composable fun SpendingLimitListItem( summary: SpendingLimitSummary, - category: Category? + category: Category?, + onAdjustClick: () -> Unit ) { val progressColor = when { summary.isExceeded -> IntivaColors.StatusError @@ -127,6 +133,33 @@ fun SpendingLimitListItem( color = progressColor, trackColor = Color(0xFFE5E0EC) ) + + Spacer(modifier = Modifier.height(14.dp)) + OutlinedButton( + onClick = onAdjustClick, + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(14.dp), + colors = ButtonDefaults.outlinedButtonColors( + containerColor = if (summary.isExceeded) IntivaColors.PrimaryBrand else Color.White, + contentColor = if (summary.isExceeded) IntivaColors.TextInverse else IntivaColors.PrimaryBrand + ), + border = androidx.compose.foundation.BorderStroke( + width = 1.dp, + color = IntivaColors.PrimaryBrand + ) + ) { + Icon( + imageVector = Icons.Default.Edit, + contentDescription = null, + modifier = Modifier.size(18.dp) + ) + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = "Ajustar límite", + fontSize = 14.sp, + fontWeight = FontWeight.Bold + ) + } } } } @@ -137,4 +170,4 @@ private fun parseColor(value: String?, fallback: Color): Color = private fun formatCurrency(amount: BigDecimal): String { val formatter = DecimalFormat("#,##0.00") return "S/. ${formatter.format(amount)}" -} \ No newline at end of file +} diff --git a/app/src/main/java/com/resolum/intiva/features/finances/presentation/spendinglimits/components/SpendingLimitSuccessContent.kt b/app/src/main/java/com/resolum/intiva/features/finances/presentation/spendinglimits/components/SpendingLimitSuccessContent.kt index 114a612..8f4a5f1 100644 --- a/app/src/main/java/com/resolum/intiva/features/finances/presentation/spendinglimits/components/SpendingLimitSuccessContent.kt +++ b/app/src/main/java/com/resolum/intiva/features/finances/presentation/spendinglimits/components/SpendingLimitSuccessContent.kt @@ -15,15 +15,19 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.resolum.intiva.core.ui.theme.IntivaColors import com.resolum.intiva.features.finances.presentation.spendinglimits.SpendingLimitSummary +import com.resolum.intiva.features.paymentmethodsandcategories.domain.models.Category import java.math.BigDecimal import java.text.DecimalFormat +import java.time.LocalDate +import java.time.temporal.ChronoUnit @Composable -fun SpendingLimitSuccessContent(summary: SpendingLimitSummary) { +fun SpendingLimitSuccessContent(summary: SpendingLimitSummary, category: Category? = null) { val progressColor = when { summary.isExceeded -> IntivaColors.StatusError summary.progressPercent >= 80 -> IntivaColors.StatusWarning @@ -64,17 +68,22 @@ fun SpendingLimitSuccessContent(summary: SpendingLimitSummary) { ) } Spacer(modifier = Modifier.width(16.dp)) - Column { + Column(modifier = Modifier.weight(1f)) { Text( - text = "Límite de Gasto Mensual", + text = category?.name?.let { "$it · ${periodLabel(summary)}" } + ?: "Límite ${summary.limit.targetId} · ${periodLabel(summary)}", fontWeight = FontWeight.Bold, fontSize = 14.sp, - color = IntivaColors.TextPrimary + color = IntivaColors.TextPrimary, + maxLines = 1, + overflow = TextOverflow.Ellipsis ) Text( text = subtitle, fontSize = 12.sp, - color = if (summary.isExceeded) IntivaColors.StatusError else IntivaColors.TextSecondary + color = if (summary.isExceeded) IntivaColors.StatusError else IntivaColors.TextSecondary, + maxLines = 1, + overflow = TextOverflow.Ellipsis ) } } @@ -84,4 +93,17 @@ fun SpendingLimitSuccessContent(summary: SpendingLimitSummary) { private fun formatCurrency(amount: BigDecimal): String { val formatter = DecimalFormat("#,##0.00") return "S/ ${formatter.format(amount)}" -} \ No newline at end of file +} + +private fun periodLabel(summary: SpendingLimitSummary): String { + val start = runCatching { LocalDate.parse(summary.limit.startDate) }.getOrNull() + val end = runCatching { LocalDate.parse(summary.limit.endDate) }.getOrNull() + if (start == null || end == null) return "Periodo" + + val inclusiveDays = ChronoUnit.DAYS.between(start, end) + 1 + return when { + inclusiveDays <= 7 -> "Semanal" + start.dayOfYear == 1 && end.dayOfYear == end.lengthOfYear() -> "Anual" + else -> "Mensual" + } +} diff --git a/app/src/main/java/com/resolum/intiva/features/paymentmethodsandcategories/presentation/category/components/CategoriesItem.kt b/app/src/main/java/com/resolum/intiva/features/paymentmethodsandcategories/presentation/category/components/CategoriesItem.kt index ef67663..42a5f9b 100644 --- a/app/src/main/java/com/resolum/intiva/features/paymentmethodsandcategories/presentation/category/components/CategoriesItem.kt +++ b/app/src/main/java/com/resolum/intiva/features/paymentmethodsandcategories/presentation/category/components/CategoriesItem.kt @@ -1,6 +1,7 @@ package com.resolum.intiva.features.paymentmethodsandcategories.presentation.category.components import androidx.compose.foundation.background +import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -25,6 +26,7 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import com.resolum.intiva.features.paymentmethodsandcategories.domain.models.Category import androidx.core.graphics.toColorInt +import com.resolum.intiva.core.ui.theme.IntivaColors /** * Composable function to display a single category item in a grid or list. @@ -49,9 +51,9 @@ fun CategoryItem( onClick = onClick, modifier = Modifier.aspectRatio(1f), shape = RoundedCornerShape(12.dp), - colors = if (isSelected) CardDefaults.cardColors(MaterialTheme.colorScheme.primary.copy(alpha = 0.1f)) - else CardDefaults.cardColors(MaterialTheme.colorScheme.surface), - elevation = CardDefaults.cardElevation(defaultElevation = 1.dp) + colors = CardDefaults.cardColors(Color.White), + border = if (isSelected) BorderStroke(2.dp, IntivaColors.PrimaryBrand) else BorderStroke(1.dp, Color(0xFFE7E4ED)), + elevation = CardDefaults.cardElevation(defaultElevation = if (isSelected) 3.dp else 1.dp) ) { Column( modifier = Modifier.fillMaxSize().padding(8.dp),