diff --git a/app/src/main/java/edu/card/clarity/domain/creditCard/CreditCardInfo.kt b/app/src/main/java/edu/card/clarity/domain/creditCard/CreditCardInfo.kt index f7a13b1..9705e83 100644 --- a/app/src/main/java/edu/card/clarity/domain/creditCard/CreditCardInfo.kt +++ b/app/src/main/java/edu/card/clarity/domain/creditCard/CreditCardInfo.kt @@ -5,6 +5,7 @@ import edu.card.clarity.enums.CardNetworkType import edu.card.clarity.enums.RewardType import java.util.UUID + data class CreditCardInfo( val id: UUID? = null, val name: String, diff --git a/app/src/main/java/edu/card/clarity/enums/PurchaseType.kt b/app/src/main/java/edu/card/clarity/enums/PurchaseType.kt index d2f2238..ecea27a 100644 --- a/app/src/main/java/edu/card/clarity/enums/PurchaseType.kt +++ b/app/src/main/java/edu/card/clarity/enums/PurchaseType.kt @@ -12,5 +12,6 @@ enum class PurchaseType { Groceries, Restaurants, Travel, - Others + Others; + companion object } \ No newline at end of file diff --git a/app/src/main/java/edu/card/clarity/presentation/myReceiptsScreen/MyReceiptsScreen.kt b/app/src/main/java/edu/card/clarity/presentation/myReceiptsScreen/MyReceiptsScreen.kt new file mode 100644 index 0000000..9562ebf --- /dev/null +++ b/app/src/main/java/edu/card/clarity/presentation/myReceiptsScreen/MyReceiptsScreen.kt @@ -0,0 +1,97 @@ +package edu.card.clarity.presentation.myReceiptsScreen + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +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.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.hilt.navigation.compose.hiltViewModel +import edu.card.clarity.presentation.common.DropdownMenu +import edu.card.clarity.ui.theme.CardClarityTheme +import edu.card.clarity.ui.theme.CardClarityTypography + +@Composable +fun MyReceiptsScreen(viewModel: MyReceiptsScreenViewModel = hiltViewModel()) { + val receipts by viewModel.receipts.collectAsState() + val receiptFilterUiState by viewModel.receiptFilterUiState.collectAsState() + val creditCardFilterOptions by viewModel.creditCardFilterOptionStrings.collectAsState() + + CardClarityTheme { + Column( + modifier = Modifier + .fillMaxSize() + .background(Color.White) + .padding(16.dp) + ) { + Spacer(modifier = Modifier.height(16.dp)) + Text( + text = "My Receipts", + fontSize = 24.sp, + fontWeight = FontWeight.Bold, + fontFamily = CardClarityTypography.bodyLarge.fontFamily, + modifier = Modifier.padding(bottom = 16.dp) + ) + + DropdownMenu( + label = "Credit Card", + options = creditCardFilterOptions, + selectedOption = receiptFilterUiState.selectedCreditCardFilterOption, + onOptionSelected = viewModel::setCreditCardFilter + ) + Spacer(modifier = Modifier.height(8.dp)) + DropdownMenu( + label = "Purchase Type", + options = viewModel.purchaseTypeFilterOptionStrings, + selectedOption = receiptFilterUiState.selectedPurchaseTypeFilterOption, + onOptionSelected = viewModel::setPurchaseTypeFilter + ) + + Spacer(modifier = Modifier.height(16.dp)) + + Box(modifier = Modifier.weight(0.5f)) { + LazyColumn { + items(receipts.size) { index -> + ReceiptsItem(receipts[index], viewModel::deleteReceipt) + } + } + } + + Button( + onClick = { /* TODO: Handle record receipt action */ }, + colors = ButtonDefaults.buttonColors(containerColor = Color.White, + contentColor = Color.Black), + border = BorderStroke(2.dp, Color.Black), + shape = RoundedCornerShape(25), + modifier = Modifier.align(Alignment.CenterHorizontally) + ) { + Text(text = "Record a Receipt") + } + + Spacer(modifier = Modifier.height(60.dp)) + } + } +} + +@Composable +@Preview +fun MyReceiptsScreenPreview() { + MyReceiptsScreen() +} \ No newline at end of file diff --git a/app/src/main/java/edu/card/clarity/presentation/myReceiptsScreen/MyReceiptsScreenViewModel.kt b/app/src/main/java/edu/card/clarity/presentation/myReceiptsScreen/MyReceiptsScreenViewModel.kt new file mode 100644 index 0000000..3b2302e --- /dev/null +++ b/app/src/main/java/edu/card/clarity/presentation/myReceiptsScreen/MyReceiptsScreenViewModel.kt @@ -0,0 +1,144 @@ +package edu.card.clarity.presentation.myReceiptsScreen + +import android.icu.text.SimpleDateFormat +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import edu.card.clarity.domain.Purchase +import edu.card.clarity.domain.creditCard.CreditCardInfo +import edu.card.clarity.enums.PurchaseType +import edu.card.clarity.presentation.utils.WhileUiSubscribed +import edu.card.clarity.presentation.utils.displayStrings +import edu.card.clarity.repositories.PurchaseRepository +import edu.card.clarity.repositories.creditCard.CashBackCreditCardRepository +import edu.card.clarity.repositories.creditCard.PointBackCreditCardRepository +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.mapLatest +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch +import java.util.UUID +import javax.inject.Inject + +@HiltViewModel +class MyReceiptsScreenViewModel @Inject constructor ( + private val receiptRepository: PurchaseRepository, + private val cashBackCreditCardRepository: CashBackCreditCardRepository, + private val pointBackCreditCardRepository: PointBackCreditCardRepository, + private val savedStateHandle: SavedStateHandle +) : ViewModel() { + private val dateFormatter = SimpleDateFormat.getDateInstance() + + private val savedCreditCardFilter: StateFlow = savedStateHandle + .getStateFlow( + key = MY_RECEIPTS_SCREEN_SAVED_FILTER_KEY, + initialValue = ReceiptFilter() + ) + + val receipts: StateFlow> = combine( + receiptRepository.getAllPurchasesStream(), + savedCreditCardFilter + ) { receipts, filter -> + receipts + .filter { + if (filter.filteredPurchaseType != null) + it.type == filter.filteredPurchaseType + else true + } + .filter { + if (filter.filteredCreditCardId != null) + it.creditCardId == filter.filteredCreditCardId + else true + } + } + .map { it.map { receipt -> receipt.toUiState() } } + .stateIn( + scope = viewModelScope, + started = WhileUiSubscribed, + initialValue = emptyList() + ) + + private val _receiptFilterUiState = MutableStateFlow( + ReceiptFilterUiState() + ) + + val receiptFilterUiState = _receiptFilterUiState.asStateFlow() + + private val creditCards: StateFlow> = combine( + cashBackCreditCardRepository.getAllCreditCardInfoStream(), + pointBackCreditCardRepository.getAllCreditCardInfoStream() + ) { cashBack, pointBack -> + cashBack + pointBack + } + .stateIn( + scope = viewModelScope, + started = WhileUiSubscribed, + initialValue = emptyList() + ) + + @OptIn(ExperimentalCoroutinesApi::class) + val creditCardFilterOptionStrings: StateFlow> = creditCards + .mapLatest { + listOf(ReceiptFilterUiState.ALL_OPTION) + it.map { creditCard -> creditCard.name } + } + .stateIn( + scope = viewModelScope, + started = SharingStarted.Eagerly, + initialValue = emptyList() + ) + + val purchaseTypeFilterOptionStrings = + listOf(ReceiptFilterUiState.ALL_OPTION) + PurchaseType.displayStrings + + fun setCreditCardFilter(optionIndex: Int) { + savedStateHandle[MY_RECEIPTS_SCREEN_SAVED_FILTER_KEY] = savedCreditCardFilter + .value + .copy(filteredCreditCardId = if (optionIndex == 0) null else creditCards.value[optionIndex - 1].id) + + _receiptFilterUiState.value = _receiptFilterUiState.value.copy( + selectedCreditCardFilterOption = creditCards.value[optionIndex - 1].name + ) + } + + fun setPurchaseTypeFilter(optionIndex: Int) { + savedStateHandle[MY_RECEIPTS_SCREEN_SAVED_FILTER_KEY] = savedCreditCardFilter + .value + .copy(filteredPurchaseType = if (optionIndex == 0) null else PurchaseType.entries[optionIndex - 1]) + + _receiptFilterUiState.value = _receiptFilterUiState.value.copy( + selectedPurchaseTypeFilterOption = purchaseTypeFilterOptionStrings[optionIndex] + ) + } + + fun deleteReceipt(id: UUID) = viewModelScope.launch { + receiptRepository.removePurchase(id) + } + + private suspend fun Purchase.toUiState() = ReceiptUiState( + id = this.id!!, + purchaseTime = dateFormatter.format(this.time), + merchant = this.merchant, + total = this.total.toString(), + purchaseType = this.type.name, + creditCardId = this.creditCardId, + creditCardName = getCreditCardName(this.creditCardId) + ) + + private suspend fun getCreditCardName(id: UUID): String { + val creditCardInfo = cashBackCreditCardRepository.getCreditCardInfo(id) + ?: pointBackCreditCardRepository.getCreditCardInfo(id) + + return creditCardInfo?.name!! + } + + companion object { + private const val MY_RECEIPTS_SCREEN_SAVED_FILTER_KEY: String = + "MY_RECEIPTS_SCREEN_SAVED_FILTER" + } +} \ No newline at end of file diff --git a/app/src/main/java/edu/card/clarity/presentation/myReceiptsScreen/ReceiptFilter.kt b/app/src/main/java/edu/card/clarity/presentation/myReceiptsScreen/ReceiptFilter.kt new file mode 100644 index 0000000..08fa2c2 --- /dev/null +++ b/app/src/main/java/edu/card/clarity/presentation/myReceiptsScreen/ReceiptFilter.kt @@ -0,0 +1,12 @@ +package edu.card.clarity.presentation.myReceiptsScreen + +import android.os.Parcelable +import edu.card.clarity.enums.PurchaseType +import kotlinx.parcelize.Parcelize +import java.util.UUID + +@Parcelize +data class ReceiptFilter( + val filteredCreditCardId: UUID? = null, + val filteredPurchaseType: PurchaseType? = null +) : Parcelable \ No newline at end of file diff --git a/app/src/main/java/edu/card/clarity/presentation/myReceiptsScreen/ReceiptFilterUiState.kt b/app/src/main/java/edu/card/clarity/presentation/myReceiptsScreen/ReceiptFilterUiState.kt new file mode 100644 index 0000000..0525f2d --- /dev/null +++ b/app/src/main/java/edu/card/clarity/presentation/myReceiptsScreen/ReceiptFilterUiState.kt @@ -0,0 +1,10 @@ +package edu.card.clarity.presentation.myReceiptsScreen + +data class ReceiptFilterUiState( + val selectedCreditCardFilterOption: String = ALL_OPTION, + val selectedPurchaseTypeFilterOption: String = ALL_OPTION +) { + companion object { + const val ALL_OPTION: String = "All" + } +} diff --git a/app/src/main/java/edu/card/clarity/presentation/myReceiptsScreen/ReceiptItem.kt b/app/src/main/java/edu/card/clarity/presentation/myReceiptsScreen/ReceiptItem.kt new file mode 100644 index 0000000..41ce5ee --- /dev/null +++ b/app/src/main/java/edu/card/clarity/presentation/myReceiptsScreen/ReceiptItem.kt @@ -0,0 +1,99 @@ +package edu.card.clarity.presentation.myReceiptsScreen + +import androidx.compose.foundation.BorderStroke +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.shape.RoundedCornerShape +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import java.util.UUID + +@Composable +fun ReceiptsItem( + receipt: ReceiptUiState, + onRemoveReceipt: (receiptId: UUID) -> Unit +) { + Card( + modifier = Modifier + .fillMaxWidth() + .padding(8.dp), + shape = RoundedCornerShape(8.dp), + elevation = CardDefaults.cardElevation(defaultElevation = 4.dp), + colors = CardDefaults.cardColors(Color.LightGray) + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp) + ) { + Text( + text = "Merchant: ${receipt.merchant}", + fontSize = 16.sp + ) + Text( + text = "Card Used: ${receipt.creditCardName}", + fontSize = 16.sp + ) + Text( + text = "Purchase Type: ${receipt.purchaseType}", + fontSize = 16.sp + ) + Text( + text = "Date: ${receipt.purchaseTime}", + fontSize = 16.sp + ) + Text( + text = "Total Amount: ${receipt.total}", + fontSize = 16.sp + ) + + Spacer(modifier = Modifier.height(8.dp)) + + Row( + horizontalArrangement = Arrangement.SpaceBetween, + modifier = Modifier.fillMaxWidth() + ) { + Spacer(modifier = Modifier.weight(2f)) + Button( + onClick = { /*TODO: Handle view click*/ }, + colors = ButtonDefaults.buttonColors( + containerColor = Color.White, + contentColor = Color.Black + ), + modifier = Modifier.weight(3f), + border = BorderStroke(2.dp, Color.Black), + shape = RoundedCornerShape(25) + ) { + Text(text = "View") + } + Spacer(modifier = Modifier.weight(0.2f)) + Button( + onClick = { onRemoveReceipt(receipt.id) }, + colors = ButtonDefaults.buttonColors( + containerColor = Color.White, + contentColor = Color.Black + ), + modifier = Modifier.weight(3f), + border = BorderStroke(2.dp, Color.Black), + shape = RoundedCornerShape(25) + ) { + Text(text = "Delete") + } + Spacer(modifier = Modifier.weight(2f)) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/edu/card/clarity/presentation/myReceiptsScreen/ReceiptUiState.kt b/app/src/main/java/edu/card/clarity/presentation/myReceiptsScreen/ReceiptUiState.kt new file mode 100644 index 0000000..28a1006 --- /dev/null +++ b/app/src/main/java/edu/card/clarity/presentation/myReceiptsScreen/ReceiptUiState.kt @@ -0,0 +1,13 @@ +package edu.card.clarity.presentation.myReceiptsScreen + +import java.util.UUID + +data class ReceiptUiState( + val id: UUID, + val purchaseTime: String, + val merchant: String, + val purchaseType: String, + val total: String, + val creditCardId: UUID, + val creditCardName: String +) \ No newline at end of file diff --git a/app/src/main/java/edu/card/clarity/presentation/navigation/BottomNavBar.kt b/app/src/main/java/edu/card/clarity/presentation/navigation/BottomNavBar.kt index 4e6ee1b..f6b4ded 100644 --- a/app/src/main/java/edu/card/clarity/presentation/navigation/BottomNavBar.kt +++ b/app/src/main/java/edu/card/clarity/presentation/navigation/BottomNavBar.kt @@ -4,6 +4,7 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.AddCircle import androidx.compose.material.icons.filled.Home import androidx.compose.material.icons.filled.Search +import androidx.compose.material.icons.filled.ShoppingCart import androidx.compose.material.icons.filled.Star import androidx.compose.ui.graphics.vector.ImageVector import edu.card.clarity.presentation.utils.Destinations diff --git a/app/src/main/java/edu/card/clarity/presentation/navigation/BottomNavGraph.kt b/app/src/main/java/edu/card/clarity/presentation/navigation/BottomNavGraph.kt index 58d0cbb..b003c8d 100644 --- a/app/src/main/java/edu/card/clarity/presentation/navigation/BottomNavGraph.kt +++ b/app/src/main/java/edu/card/clarity/presentation/navigation/BottomNavGraph.kt @@ -7,17 +7,15 @@ import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.navArgument import edu.card.clarity.presentation.MyReceiptsScreen -import edu.card.clarity.presentation.addCardScreen.AddCardScreen -import edu.card.clarity.presentation.homeScreen.HomeScreen -import edu.card.clarity.presentation.myCardScreen.MyCardsScreen import edu.card.clarity.presentation.PurchaseScreen import edu.card.clarity.presentation.addBenefitScreen.AddBenefitScreen import edu.card.clarity.presentation.addCardScreen.AddCardScreen +import edu.card.clarity.presentation.homeScreen.HomeScreen import edu.card.clarity.presentation.myBenefitsScreen.MyBenefitsScreen import edu.card.clarity.presentation.myCardScreen.MyCardsScreen +import edu.card.clarity.presentation.upcomingPaymentsScreen.UpcomingPaymentsScreen import edu.card.clarity.presentation.utils.ArgumentNames import edu.card.clarity.presentation.utils.Destinations -import edu.card.clarity.presentation.upcomingPaymentsScreen.UpcomingPaymentsScreen @Composable fun BottomNavGraph(navController: NavHostController) { @@ -92,4 +90,4 @@ fun BottomNavGraph(navController: NavHostController) { AddBenefitScreen(cardName, navController) } } -} +} \ No newline at end of file diff --git a/app/src/main/java/edu/card/clarity/presentation/utils/EnumExtensions.kt b/app/src/main/java/edu/card/clarity/presentation/utils/EnumExtensions.kt index fc2b883..f76d6e9 100644 --- a/app/src/main/java/edu/card/clarity/presentation/utils/EnumExtensions.kt +++ b/app/src/main/java/edu/card/clarity/presentation/utils/EnumExtensions.kt @@ -1,6 +1,7 @@ package edu.card.clarity.presentation.utils import edu.card.clarity.enums.CardNetworkType +import edu.card.clarity.enums.PurchaseType import edu.card.clarity.enums.RewardType internal val RewardType.displayString: String @@ -19,4 +20,7 @@ internal val CardNetworkType.Companion.displayStrings: List get() = CardNetworkType.entries.map { it.name } internal val CardNetworkType.Companion.ordinals: List - get() = CardNetworkType.entries.map { it.ordinal } \ No newline at end of file + get() = CardNetworkType.entries.map { it.ordinal } + +internal val PurchaseType.Companion.displayStrings: List + get() = PurchaseType.entries.map { it.name } \ No newline at end of file