From 721a85149b5a04dc4a0fb7e9e4866581662d8491 Mon Sep 17 00:00:00 2001 From: s665li Date: Fri, 19 Jul 2024 03:28:29 -0400 Subject: [PATCH 1/5] My Receipts Page --- .../main/java/edu/card/clarity/MainScreen.kt | 2 +- .../domain/creditCard/CreditCardInfo.kt | 5 +- .../edu/card/clarity/enums/PurchaseType.kt | 3 +- .../myReceiptsScreen/MyReceiptsScreen.kt | 74 +++++++ .../MyReceiptsScreenViewModel.kt | 189 +++++++++++++++++ .../presentation/myReceiptsScreen/Receipts.kt | 113 ++++++++++ .../myReceiptsScreen/ReceiptsFilter.kt | 44 ++++ .../myReceiptsScreen/ReceiptsUiState.kt | 13 ++ .../presentation/myReceiptsScreen/temp.kt | 200 ++++++++++++++++++ .../presentation/navigation/BottomNavBar.kt | 6 + .../presentation/navigation/BottomNavGraph.kt | 9 +- .../presentation/utils/EnumExtensions.kt | 6 +- 12 files changed, 658 insertions(+), 6 deletions(-) create mode 100644 app/src/main/java/edu/card/clarity/presentation/myReceiptsScreen/MyReceiptsScreen.kt create mode 100644 app/src/main/java/edu/card/clarity/presentation/myReceiptsScreen/MyReceiptsScreenViewModel.kt create mode 100644 app/src/main/java/edu/card/clarity/presentation/myReceiptsScreen/Receipts.kt create mode 100644 app/src/main/java/edu/card/clarity/presentation/myReceiptsScreen/ReceiptsFilter.kt create mode 100644 app/src/main/java/edu/card/clarity/presentation/myReceiptsScreen/ReceiptsUiState.kt create mode 100644 app/src/main/java/edu/card/clarity/presentation/myReceiptsScreen/temp.kt diff --git a/app/src/main/java/edu/card/clarity/MainScreen.kt b/app/src/main/java/edu/card/clarity/MainScreen.kt index 665d176..e8f3072 100644 --- a/app/src/main/java/edu/card/clarity/MainScreen.kt +++ b/app/src/main/java/edu/card/clarity/MainScreen.kt @@ -44,7 +44,7 @@ fun BottomBar(navController: NavHostController) { val screens = listOf( BottomNavBar.Home, BottomNavBar.AddCard, - BottomNavBar.MyBenefits, + BottomNavBar.MyReceipts, BottomNavBar.Purchase, ) val navBackStackEntry by navController.currentBackStackEntryAsState() 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..cfe7352 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 @@ -1,10 +1,13 @@ package edu.card.clarity.domain.creditCard import android.icu.util.Calendar +import android.os.Parcelable import edu.card.clarity.enums.CardNetworkType import edu.card.clarity.enums.RewardType +import kotlinx.parcelize.Parcelize import java.util.UUID +@Parcelize data class CreditCardInfo( val id: UUID? = null, val name: String, @@ -13,4 +16,4 @@ data class CreditCardInfo( val statementDate: Calendar, val paymentDueDate: Calendar, val isReminderEnabled: Boolean, -) \ No newline at end of file +): Parcelable \ No newline at end of file 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..7f0843c --- /dev/null +++ b/app/src/main/java/edu/card/clarity/presentation/myReceiptsScreen/MyReceiptsScreen.kt @@ -0,0 +1,74 @@ +package edu.card.clarity.presentation.myReceiptsScreen + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +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 edu.card.clarity.ui.theme.CardClarityTheme +import edu.card.clarity.ui.theme.CardClarityTypography +import androidx.compose.foundation.layout.Column +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Text +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.ui.Alignment +import androidx.navigation.NavController +import androidx.navigation.compose.rememberNavController + +@Composable +fun MyReceiptsScreen(navController: NavController) { + + 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) + ) + + ReceiptsFilter() + + Box(modifier = Modifier.weight(0.5f)) { + Receipts(navController) + } + + 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() { + val navController = rememberNavController() + MyReceiptsScreen(navController) +} \ 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..8ab1492 --- /dev/null +++ b/app/src/main/java/edu/card/clarity/presentation/myReceiptsScreen/MyReceiptsScreenViewModel.kt @@ -0,0 +1,189 @@ +package edu.card.clarity.presentation.myReceiptsScreen + +import android.icu.text.SimpleDateFormat +import android.icu.util.Calendar +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import edu.card.clarity.domain.creditCard.CreditCardInfo +import edu.card.clarity.enums.CardNetworkType +import edu.card.clarity.enums.PurchaseType +import edu.card.clarity.enums.RewardType +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.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.map +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() { + + companion object { + private const val KEY_SELECTED_CARD_FILTER = "selected_card_filter" + private const val KEY_SELECTED_PURCHASE_TYPE_FILTER = "selected_purchase_type_filter" + private val dateFormatter = SimpleDateFormat.getDateInstance() + } + + private val allCardsOption = flow> { + emit( + listOf( + CreditCardInfo( + null, "All", RewardType.CashBack, + CardNetworkType.MasterCard, + Calendar.getInstance(), + Calendar.getInstance(), false + ) + ) + ) + } + + /*// Temporary Data + private val creditCards = listOf( + CreditCardInfo( + UUID.randomUUID(), "Visa Dividend", RewardType.CashBack, + CardNetworkType.MasterCard, + Calendar.getInstance(), + Calendar.getInstance(), false + ), + CreditCardInfo( + UUID.randomUUID(), "MasterCard", RewardType.CashBack, + CardNetworkType.MasterCard, + Calendar.getInstance(), + Calendar.getInstance(), false + ) + ) + private val receipts: StateFlow> = MutableStateFlow( + listOf( + ReceiptsUiState( + UUID.randomUUID(), + "2024-05-19", + "Shell", + "Gas", + "30.22", + creditCards[0].id!!, + creditCards[0].name + ), + ReceiptsUiState( + UUID.randomUUID(), + "2024-06-06", + "Air Canada", + "Travel", + "520.87", + creditCards[1].id!!, + creditCards[1].name + ), + ReceiptsUiState( + UUID.randomUUID(), + "2024-07-21", + "Hilton", + "Hotel", + "837.25", + creditCards[1].id!!, + creditCards[1].name + ) + ) + ) + val cards = combine( + allCardsOption, flow> { emit(creditCards) } + ) { list1, list2 -> + list1 + list2 + }.stateIn( + scope = viewModelScope, + started = WhileUiSubscribed, + initialValue = listOf() + )*/ + + private val receipts = receiptRepository.getAllPurchasesStream().map { receipts -> + receipts.map { + ReceiptsUiState( + it.id!!, + dateFormatter.format(it.time), + it.merchant, + it.total.toString(), + it.type.toString(), + it.creditCardId, + getCardName(it.creditCardId) + ) + } + }.stateIn( + scope = viewModelScope, + started = WhileUiSubscribed, + initialValue = listOf() + ) + + val cards = combine( + allCardsOption, + cashBackCreditCardRepository.getAllCreditCardInfoStream(), + pointBackCreditCardRepository.getAllCreditCardInfoStream() + ) + { list1, list2 , list3-> + list1 + list2 + list3 + }.stateIn( + scope = viewModelScope, + started = WhileUiSubscribed, + initialValue = listOf() + ) + + val purchaseTypeOptions = PurchaseType.displayStrings + + private val _selectedCardFilter = + MutableStateFlow(savedStateHandle.get(KEY_SELECTED_CARD_FILTER)) + val selectedCardFilter: StateFlow = _selectedCardFilter + + private val _selectedPurchaseTypeFilter = + MutableStateFlow(savedStateHandle.get(KEY_SELECTED_PURCHASE_TYPE_FILTER)) + val selectedPurchaseTypeFilter: StateFlow = _selectedPurchaseTypeFilter + + val filteredReceipts: StateFlow> = combine( + receipts, selectedCardFilter, selectedPurchaseTypeFilter + ) { receipts, cardFilter, purchaseTypeFilter -> + var filtered = receipts + if (cardFilter?.id != null) { + cardFilter.let { + filtered = filtered.filter { it.creditCardId == cardFilter.id } + } + } + if (purchaseTypeFilter != "All") { + purchaseTypeFilter?.let { + filtered = filtered.filter { it.type == purchaseTypeFilter } + } + } + filtered + }.stateIn(viewModelScope, SharingStarted.Lazily, emptyList()) + + private suspend fun getCardName(id: UUID): String { + val card = cashBackCreditCardRepository.getCreditCardInfo(id) + ?: pointBackCreditCardRepository.getCreditCardInfo(id) + return card!!.name + } + + fun setCardFilter(card: CreditCardInfo) { + _selectedCardFilter.value = card + savedStateHandle[KEY_SELECTED_CARD_FILTER] = card + } + + fun setPurchaseTypeFilter(purchaseType: String) { + _selectedPurchaseTypeFilter.value = purchaseType + savedStateHandle[KEY_SELECTED_PURCHASE_TYPE_FILTER] = purchaseType + } + + fun deleteReceipt(id: UUID) = viewModelScope.launch { + receiptRepository.removePurchase(id) + } +} \ No newline at end of file diff --git a/app/src/main/java/edu/card/clarity/presentation/myReceiptsScreen/Receipts.kt b/app/src/main/java/edu/card/clarity/presentation/myReceiptsScreen/Receipts.kt new file mode 100644 index 0000000..57dfab9 --- /dev/null +++ b/app/src/main/java/edu/card/clarity/presentation/myReceiptsScreen/Receipts.kt @@ -0,0 +1,113 @@ +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.lazy.LazyColumn +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.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.navigation.NavController + +@Composable +fun Receipts( + navController: NavController, + viewModel: MyReceiptsScreenViewModel = hiltViewModel() +) { + val receipts by viewModel.filteredReceipts.collectAsStateWithLifecycle() + + LazyColumn { + items(receipts.size) { index -> + ReceiptsItem(receipts[index]) + } + } +} + + + +@Composable +fun ReceiptsItem(receipt: ReceiptsUiState, + viewModel: MyReceiptsScreenViewModel = hiltViewModel()) { + 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.creditCard}", + fontSize = 16.sp + ) + Text( + text = "Purchase Type: ${receipt.type}", + fontSize = 16.sp + ) + Text( + text = "Date: ${receipt.time}", + 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 = { viewModel.deleteReceipt(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/ReceiptsFilter.kt b/app/src/main/java/edu/card/clarity/presentation/myReceiptsScreen/ReceiptsFilter.kt new file mode 100644 index 0000000..ec43b27 --- /dev/null +++ b/app/src/main/java/edu/card/clarity/presentation/myReceiptsScreen/ReceiptsFilter.kt @@ -0,0 +1,44 @@ +package edu.card.clarity.presentation.myReceiptsScreen + + +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import edu.card.clarity.presentation.common.DropdownMenu + +@Composable +fun ReceiptsFilter( + viewModel: MyReceiptsScreenViewModel = hiltViewModel() +) { + val creditCards by viewModel.cards.collectAsStateWithLifecycle() + val selectedCardFilter by viewModel.selectedCardFilter.collectAsStateWithLifecycle() + val selectedPurchaseTypeFilter by viewModel.selectedPurchaseTypeFilter.collectAsStateWithLifecycle() + + + DropdownMenu( + label = "Credit Card", + options = creditCards.map { if (it.name == "") "Unnamed Card" else it.name }, + selectedOption = if (selectedCardFilter?.name == "") "Unnamed Card" else selectedCardFilter?.name ?: "All" + ) { index -> + viewModel.setCardFilter(creditCards[index]) + } + + Spacer(modifier = Modifier.height(8.dp)) + + val purchaseTypeOptions = listOf("All") + viewModel.purchaseTypeOptions + + DropdownMenu( + label = "Purchase Type", + options = purchaseTypeOptions, + selectedOption = selectedPurchaseTypeFilter ?: "All" + ) { index -> + viewModel.setPurchaseTypeFilter(purchaseTypeOptions[index]) + } + + Spacer(modifier = Modifier.height(16.dp)) +} \ No newline at end of file diff --git a/app/src/main/java/edu/card/clarity/presentation/myReceiptsScreen/ReceiptsUiState.kt b/app/src/main/java/edu/card/clarity/presentation/myReceiptsScreen/ReceiptsUiState.kt new file mode 100644 index 0000000..f416c82 --- /dev/null +++ b/app/src/main/java/edu/card/clarity/presentation/myReceiptsScreen/ReceiptsUiState.kt @@ -0,0 +1,13 @@ +package edu.card.clarity.presentation.myReceiptsScreen + +import java.util.UUID + +data class ReceiptsUiState ( + val id: UUID, + val time: String, + val merchant: String, + val type: String, + val total: String, + val creditCardId: UUID, + val creditCard: String +) \ No newline at end of file diff --git a/app/src/main/java/edu/card/clarity/presentation/myReceiptsScreen/temp.kt b/app/src/main/java/edu/card/clarity/presentation/myReceiptsScreen/temp.kt new file mode 100644 index 0000000..c8cf07f --- /dev/null +++ b/app/src/main/java/edu/card/clarity/presentation/myReceiptsScreen/temp.kt @@ -0,0 +1,200 @@ +package edu.card.clarity.presentation.myReceiptsScreen + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +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.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.OutlinedButton +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.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.navigation.compose.rememberNavController +import edu.card.clarity.presentation.common.DropdownMenu +import edu.card.clarity.ui.theme.CardClarityTheme +import edu.card.clarity.ui.theme.CardClarityTypography +import java.util.UUID + + +@Composable +fun MyReceiptsScreen() { + val cardFilterOptions = listOf("Visa Dividend", "MasterCard", "American Express") + val purchaseTypeOptions = listOf("Gas", "Groceries", "Electronics") + + var selectedCardFilter by remember { mutableStateOf(cardFilterOptions[0]) } + var selectedPurchaseType by remember { mutableStateOf(purchaseTypeOptions[0]) } + 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 = cardFilterOptions, + selectedOption = selectedCardFilter + ) { index -> + selectedCardFilter = cardFilterOptions[index] + } + + Spacer(modifier = Modifier.height(8.dp)) + + DropdownMenu( + label = "Purchase Type", + options = purchaseTypeOptions, + selectedOption = selectedPurchaseType + ) { index -> + selectedPurchaseType = purchaseTypeOptions[index] + } + + Spacer(modifier = Modifier.height(16.dp)) + + // Sample Receipt List + val receipts = listOf( + Receipt("Shell", "Visa Dividend", "Gas", "2024-03-30", "$50.00"), + Receipt("Best Buy", "MasterCard", "Electronics", "2024-04-02", "$300.00") + ) + + + LazyColumn { + items(receipts.size) { index -> + ReceiptBox(receipts[index]) + Spacer(modifier = Modifier.height(16.dp)) + } + } + + Spacer(modifier = Modifier.weight(1f)) + + 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") + } + } + } +} + + + +@Composable +fun ReceiptBox(receipt: Receipt) { + Card( + shape = RoundedCornerShape(8.dp), + elevation = CardDefaults.cardElevation(defaultElevation = 4.dp), + modifier = Modifier + .fillMaxWidth() + .background(Color.LightGray) + ) { + Column( + modifier = Modifier.padding(16.dp) + ) { + Text( + text = "Merchant: ${receipt.merchant}", + fontSize = 16.sp + ) + Text( + text = "Card Used: ${receipt.cardUsed}", + fontSize = 16.sp + ) + Text( + text = "Purchase Type: ${receipt.purchaseType}", + fontSize = 16.sp + ) + Text( + text = "Date: ${receipt.date}", + fontSize = 16.sp + ) + Text( + text = "Total Amount: ${receipt.amount}", + 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 = { /* TODO: Handle delete 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 = "Delete") + } + Spacer(modifier = Modifier.weight(2f)) + } + } + } +} + +data class Receipt( + val merchant: String, + val cardUsed: String, + val purchaseType: String, + val date: String, + val amount: String +) + +@Composable +@Preview +fun TempMyReceiptsScreenPreview() { + MyReceiptsScreen() +} 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 5baaebc..c76d363 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 @@ -27,6 +28,11 @@ sealed class BottomNavBar ( title = "Benefits", icon = Icons.Default.Star ) + data object MyReceipts: BottomNavBar( + route = "Receipts", + title = "Receipts", + icon = Icons.Default.ShoppingCart + ) data object Purchase: BottomNavBar( route = "purchase", title = "Purchase", 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 636ca4b..f510a7a 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 @@ -2,14 +2,18 @@ package edu.card.clarity.presentation.navigation import androidx.compose.runtime.Composable import androidx.navigation.NavHostController +import androidx.navigation.NavType import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable +import androidx.navigation.navArgument import edu.card.clarity.presentation.addCardScreen.AddCardScreen import edu.card.clarity.presentation.HomeScreen import edu.card.clarity.presentation.MyBenefitsScreen import edu.card.clarity.presentation.myCardScreen.MyCardsScreen import edu.card.clarity.presentation.PurchaseScreen import edu.card.clarity.presentation.UpcomingPaymentsScreen +import edu.card.clarity.presentation.myReceiptsScreen.MyReceiptsScreen +import java.util.UUID @Composable fun BottomNavGraph(navController: NavHostController) { @@ -23,8 +27,8 @@ fun BottomNavGraph(navController: NavHostController) { composable(route = BottomNavBar.AddCard.route) { AddCardScreen(navController) } - composable(route = BottomNavBar.MyBenefits.route) { - MyBenefitsScreen() + composable(route = BottomNavBar.MyReceipts.route) { + MyReceiptsScreen(navController) } composable(route = BottomNavBar.Purchase.route) { PurchaseScreen() @@ -35,5 +39,6 @@ fun BottomNavGraph(navController: NavHostController) { composable("upcomingPayments") { UpcomingPaymentsScreen() } + } } \ 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 From 7865173c5c5cb9b81f9aa27e200cb46efb531eb3 Mon Sep 17 00:00:00 2001 From: SamuelLiSiyuan Date: Fri, 19 Jul 2024 03:32:55 -0400 Subject: [PATCH 2/5] Delete app/src/main/java/edu/card/clarity/presentation/myReceiptsScreen/temp.kt Delet Temporary File Signed-off-by: SamuelLiSiyuan --- .../presentation/myReceiptsScreen/temp.kt | 200 ------------------ 1 file changed, 200 deletions(-) delete mode 100644 app/src/main/java/edu/card/clarity/presentation/myReceiptsScreen/temp.kt diff --git a/app/src/main/java/edu/card/clarity/presentation/myReceiptsScreen/temp.kt b/app/src/main/java/edu/card/clarity/presentation/myReceiptsScreen/temp.kt deleted file mode 100644 index c8cf07f..0000000 --- a/app/src/main/java/edu/card/clarity/presentation/myReceiptsScreen/temp.kt +++ /dev/null @@ -1,200 +0,0 @@ -package edu.card.clarity.presentation.myReceiptsScreen - -import android.os.Bundle -import androidx.activity.ComponentActivity -import androidx.activity.compose.setContent -import androidx.compose.foundation.BorderStroke -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.width -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.Card -import androidx.compose.material3.CardDefaults -import androidx.compose.material3.DropdownMenu -import androidx.compose.material3.DropdownMenuItem -import androidx.compose.material3.OutlinedButton -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.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.navigation.compose.rememberNavController -import edu.card.clarity.presentation.common.DropdownMenu -import edu.card.clarity.ui.theme.CardClarityTheme -import edu.card.clarity.ui.theme.CardClarityTypography -import java.util.UUID - - -@Composable -fun MyReceiptsScreen() { - val cardFilterOptions = listOf("Visa Dividend", "MasterCard", "American Express") - val purchaseTypeOptions = listOf("Gas", "Groceries", "Electronics") - - var selectedCardFilter by remember { mutableStateOf(cardFilterOptions[0]) } - var selectedPurchaseType by remember { mutableStateOf(purchaseTypeOptions[0]) } - 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 = cardFilterOptions, - selectedOption = selectedCardFilter - ) { index -> - selectedCardFilter = cardFilterOptions[index] - } - - Spacer(modifier = Modifier.height(8.dp)) - - DropdownMenu( - label = "Purchase Type", - options = purchaseTypeOptions, - selectedOption = selectedPurchaseType - ) { index -> - selectedPurchaseType = purchaseTypeOptions[index] - } - - Spacer(modifier = Modifier.height(16.dp)) - - // Sample Receipt List - val receipts = listOf( - Receipt("Shell", "Visa Dividend", "Gas", "2024-03-30", "$50.00"), - Receipt("Best Buy", "MasterCard", "Electronics", "2024-04-02", "$300.00") - ) - - - LazyColumn { - items(receipts.size) { index -> - ReceiptBox(receipts[index]) - Spacer(modifier = Modifier.height(16.dp)) - } - } - - Spacer(modifier = Modifier.weight(1f)) - - 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") - } - } - } -} - - - -@Composable -fun ReceiptBox(receipt: Receipt) { - Card( - shape = RoundedCornerShape(8.dp), - elevation = CardDefaults.cardElevation(defaultElevation = 4.dp), - modifier = Modifier - .fillMaxWidth() - .background(Color.LightGray) - ) { - Column( - modifier = Modifier.padding(16.dp) - ) { - Text( - text = "Merchant: ${receipt.merchant}", - fontSize = 16.sp - ) - Text( - text = "Card Used: ${receipt.cardUsed}", - fontSize = 16.sp - ) - Text( - text = "Purchase Type: ${receipt.purchaseType}", - fontSize = 16.sp - ) - Text( - text = "Date: ${receipt.date}", - fontSize = 16.sp - ) - Text( - text = "Total Amount: ${receipt.amount}", - 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 = { /* TODO: Handle delete 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 = "Delete") - } - Spacer(modifier = Modifier.weight(2f)) - } - } - } -} - -data class Receipt( - val merchant: String, - val cardUsed: String, - val purchaseType: String, - val date: String, - val amount: String -) - -@Composable -@Preview -fun TempMyReceiptsScreenPreview() { - MyReceiptsScreen() -} From a2f3c53a42a6c912934c5ce309eaad185352d32e Mon Sep 17 00:00:00 2001 From: s665li Date: Tue, 23 Jul 2024 14:20:49 -0400 Subject: [PATCH 3/5] resolve domain model pollution --- .../domain/creditCard/CreditCardInfo.kt | 6 +-- .../MyReceiptsScreenViewModel.kt | 53 ++++++++----------- .../ParcelableCreditCardInfo.kt | 11 ++++ 3 files changed, 34 insertions(+), 36 deletions(-) create mode 100644 app/src/main/java/edu/card/clarity/presentation/myReceiptsScreen/ParcelableCreditCardInfo.kt 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 cfe7352..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 @@ -1,13 +1,11 @@ package edu.card.clarity.domain.creditCard import android.icu.util.Calendar -import android.os.Parcelable import edu.card.clarity.enums.CardNetworkType import edu.card.clarity.enums.RewardType -import kotlinx.parcelize.Parcelize import java.util.UUID -@Parcelize + data class CreditCardInfo( val id: UUID? = null, val name: String, @@ -16,4 +14,4 @@ data class CreditCardInfo( val statementDate: Calendar, val paymentDueDate: Calendar, val isReminderEnabled: Boolean, -): Parcelable \ No newline at end of file +) \ 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 index 8ab1492..cdf4685 100644 --- a/app/src/main/java/edu/card/clarity/presentation/myReceiptsScreen/MyReceiptsScreenViewModel.kt +++ b/app/src/main/java/edu/card/clarity/presentation/myReceiptsScreen/MyReceiptsScreenViewModel.kt @@ -1,15 +1,11 @@ package edu.card.clarity.presentation.myReceiptsScreen import android.icu.text.SimpleDateFormat -import android.icu.util.Calendar import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel -import edu.card.clarity.domain.creditCard.CreditCardInfo -import edu.card.clarity.enums.CardNetworkType import edu.card.clarity.enums.PurchaseType -import edu.card.clarity.enums.RewardType import edu.card.clarity.presentation.utils.WhileUiSubscribed import edu.card.clarity.presentation.utils.displayStrings import edu.card.clarity.repositories.PurchaseRepository @@ -40,33 +36,18 @@ class MyReceiptsScreenViewModel @Inject constructor ( private val dateFormatter = SimpleDateFormat.getDateInstance() } - private val allCardsOption = flow> { + private val allCardsOption = flow> { emit( - listOf( - CreditCardInfo( - null, "All", RewardType.CashBack, - CardNetworkType.MasterCard, - Calendar.getInstance(), - Calendar.getInstance(), false - ) - ) + listOf(ParcelableCreditCardInfo(null, "All")) ) } - /*// Temporary Data + /*// Temporary Test Data private val creditCards = listOf( - CreditCardInfo( - UUID.randomUUID(), "Visa Dividend", RewardType.CashBack, - CardNetworkType.MasterCard, - Calendar.getInstance(), - Calendar.getInstance(), false - ), - CreditCardInfo( - UUID.randomUUID(), "MasterCard", RewardType.CashBack, - CardNetworkType.MasterCard, - Calendar.getInstance(), - Calendar.getInstance(), false - ) + ParcelableCreditCardInfo( + UUID.randomUUID(), "Visa Dividend",), + ParcelableCreditCardInfo( + UUID.randomUUID(), "MasterCard") ) private val receipts: StateFlow> = MutableStateFlow( listOf( @@ -100,7 +81,7 @@ class MyReceiptsScreenViewModel @Inject constructor ( ) ) val cards = combine( - allCardsOption, flow> { emit(creditCards) } + allCardsOption, flow> { emit(creditCards) } ) { list1, list2 -> list1 + list2 }.stateIn( @@ -129,8 +110,16 @@ class MyReceiptsScreenViewModel @Inject constructor ( val cards = combine( allCardsOption, - cashBackCreditCardRepository.getAllCreditCardInfoStream(), - pointBackCreditCardRepository.getAllCreditCardInfoStream() + cashBackCreditCardRepository.getAllCreditCardInfoStream().map { cashBackCreditCards -> + cashBackCreditCards.map { + ParcelableCreditCardInfo(it.id, it.name) + } + }, + pointBackCreditCardRepository.getAllCreditCardInfoStream().map { pointBackCreditCards -> + pointBackCreditCards.map { + ParcelableCreditCardInfo(it.id, it.name) + } + } ) { list1, list2 , list3-> list1 + list2 + list3 @@ -143,8 +132,8 @@ class MyReceiptsScreenViewModel @Inject constructor ( val purchaseTypeOptions = PurchaseType.displayStrings private val _selectedCardFilter = - MutableStateFlow(savedStateHandle.get(KEY_SELECTED_CARD_FILTER)) - val selectedCardFilter: StateFlow = _selectedCardFilter + MutableStateFlow(savedStateHandle.get(KEY_SELECTED_CARD_FILTER)) + val selectedCardFilter: StateFlow = _selectedCardFilter private val _selectedPurchaseTypeFilter = MutableStateFlow(savedStateHandle.get(KEY_SELECTED_PURCHASE_TYPE_FILTER)) @@ -173,7 +162,7 @@ class MyReceiptsScreenViewModel @Inject constructor ( return card!!.name } - fun setCardFilter(card: CreditCardInfo) { + fun setCardFilter(card: ParcelableCreditCardInfo) { _selectedCardFilter.value = card savedStateHandle[KEY_SELECTED_CARD_FILTER] = card } diff --git a/app/src/main/java/edu/card/clarity/presentation/myReceiptsScreen/ParcelableCreditCardInfo.kt b/app/src/main/java/edu/card/clarity/presentation/myReceiptsScreen/ParcelableCreditCardInfo.kt new file mode 100644 index 0000000..a978597 --- /dev/null +++ b/app/src/main/java/edu/card/clarity/presentation/myReceiptsScreen/ParcelableCreditCardInfo.kt @@ -0,0 +1,11 @@ +package edu.card.clarity.presentation.myReceiptsScreen + +import android.os.Parcelable +import kotlinx.parcelize.Parcelize +import java.util.UUID + +@Parcelize +data class ParcelableCreditCardInfo( + val id: UUID? = null, + val name: String +): Parcelable \ No newline at end of file From 5944c5bc6f694d479ce5130c31d7d21ca034f2d3 Mon Sep 17 00:00:00 2001 From: Linus Zhang Date: Tue, 23 Jul 2024 21:25:53 -0400 Subject: [PATCH 4/5] Refactored to prevent race condition. --- .../myReceiptsScreen/MyReceiptsScreen.kt | 57 +++-- .../MyReceiptsScreenViewModel.kt | 220 ++++++++---------- .../ParcelableCreditCardInfo.kt | 11 - .../myReceiptsScreen/ReceiptFilter.kt | 12 + .../myReceiptsScreen/ReceiptFilterUiState.kt | 10 + .../{Receipts.kt => ReceiptItem.kt} | 48 ++-- .../{ReceiptsUiState.kt => ReceiptUiState.kt} | 8 +- .../myReceiptsScreen/ReceiptsFilter.kt | 44 ---- 8 files changed, 176 insertions(+), 234 deletions(-) delete mode 100644 app/src/main/java/edu/card/clarity/presentation/myReceiptsScreen/ParcelableCreditCardInfo.kt create mode 100644 app/src/main/java/edu/card/clarity/presentation/myReceiptsScreen/ReceiptFilter.kt create mode 100644 app/src/main/java/edu/card/clarity/presentation/myReceiptsScreen/ReceiptFilterUiState.kt rename app/src/main/java/edu/card/clarity/presentation/myReceiptsScreen/{Receipts.kt => ReceiptItem.kt} (68%) rename app/src/main/java/edu/card/clarity/presentation/myReceiptsScreen/{ReceiptsUiState.kt => ReceiptUiState.kt} (59%) delete mode 100644 app/src/main/java/edu/card/clarity/presentation/myReceiptsScreen/ReceiptsFilter.kt 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 index 7f0843c..9562ebf 100644 --- a/app/src/main/java/edu/card/clarity/presentation/myReceiptsScreen/MyReceiptsScreen.kt +++ b/app/src/main/java/edu/card/clarity/presentation/myReceiptsScreen/MyReceiptsScreen.kt @@ -2,31 +2,37 @@ package edu.card.clarity.presentation.myReceiptsScreen import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.background -import androidx.compose.foundation.layout.* +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 -import androidx.compose.foundation.layout.Column -import androidx.compose.material3.Button -import androidx.compose.material3.ButtonDefaults -import androidx.compose.material3.Text -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.ui.Alignment -import androidx.navigation.NavController -import androidx.navigation.compose.rememberNavController @Composable -fun MyReceiptsScreen(navController: NavController) { +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( @@ -44,10 +50,28 @@ fun MyReceiptsScreen(navController: NavController) { modifier = Modifier.padding(bottom = 16.dp) ) - ReceiptsFilter() + 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)) { - Receipts(navController) + LazyColumn { + items(receipts.size) { index -> + ReceiptsItem(receipts[index], viewModel::deleteReceipt) + } + } } Button( @@ -69,6 +93,5 @@ fun MyReceiptsScreen(navController: NavController) { @Composable @Preview fun MyReceiptsScreenPreview() { - val navController = rememberNavController() - MyReceiptsScreen(navController) + 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 index cdf4685..3b2302e 100644 --- a/app/src/main/java/edu/card/clarity/presentation/myReceiptsScreen/MyReceiptsScreenViewModel.kt +++ b/app/src/main/java/edu/card/clarity/presentation/myReceiptsScreen/MyReceiptsScreenViewModel.kt @@ -5,18 +5,22 @@ 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.flow import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.mapLatest import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import java.util.UUID @@ -29,150 +33,112 @@ class MyReceiptsScreenViewModel @Inject constructor ( private val pointBackCreditCardRepository: PointBackCreditCardRepository, private val savedStateHandle: SavedStateHandle ) : ViewModel() { + private val dateFormatter = SimpleDateFormat.getDateInstance() - companion object { - private const val KEY_SELECTED_CARD_FILTER = "selected_card_filter" - private const val KEY_SELECTED_PURCHASE_TYPE_FILTER = "selected_purchase_type_filter" - private val dateFormatter = SimpleDateFormat.getDateInstance() - } - - private val allCardsOption = flow> { - emit( - listOf(ParcelableCreditCardInfo(null, "All")) + 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() + ) - /*// Temporary Test Data - private val creditCards = listOf( - ParcelableCreditCardInfo( - UUID.randomUUID(), "Visa Dividend",), - ParcelableCreditCardInfo( - UUID.randomUUID(), "MasterCard") + private val _receiptFilterUiState = MutableStateFlow( + ReceiptFilterUiState() ) - private val receipts: StateFlow> = MutableStateFlow( - listOf( - ReceiptsUiState( - UUID.randomUUID(), - "2024-05-19", - "Shell", - "Gas", - "30.22", - creditCards[0].id!!, - creditCards[0].name - ), - ReceiptsUiState( - UUID.randomUUID(), - "2024-06-06", - "Air Canada", - "Travel", - "520.87", - creditCards[1].id!!, - creditCards[1].name - ), - ReceiptsUiState( - UUID.randomUUID(), - "2024-07-21", - "Hilton", - "Hotel", - "837.25", - creditCards[1].id!!, - creditCards[1].name - ) + + val receiptFilterUiState = _receiptFilterUiState.asStateFlow() + + private val creditCards: StateFlow> = combine( + cashBackCreditCardRepository.getAllCreditCardInfoStream(), + pointBackCreditCardRepository.getAllCreditCardInfoStream() + ) { cashBack, pointBack -> + cashBack + pointBack + } + .stateIn( + scope = viewModelScope, + started = WhileUiSubscribed, + initialValue = emptyList() ) - ) - val cards = combine( - allCardsOption, flow> { emit(creditCards) } - ) { list1, list2 -> - list1 + list2 - }.stateIn( - scope = viewModelScope, - started = WhileUiSubscribed, - initialValue = listOf() - )*/ - - private val receipts = receiptRepository.getAllPurchasesStream().map { receipts -> - receipts.map { - ReceiptsUiState( - it.id!!, - dateFormatter.format(it.time), - it.merchant, - it.total.toString(), - it.type.toString(), - it.creditCardId, - getCardName(it.creditCardId) - ) - } - }.stateIn( - scope = viewModelScope, - started = WhileUiSubscribed, - initialValue = listOf() - ) - val cards = combine( - allCardsOption, - cashBackCreditCardRepository.getAllCreditCardInfoStream().map { cashBackCreditCards -> - cashBackCreditCards.map { - ParcelableCreditCardInfo(it.id, it.name) - } - }, - pointBackCreditCardRepository.getAllCreditCardInfoStream().map { pointBackCreditCards -> - pointBackCreditCards.map { - ParcelableCreditCardInfo(it.id, it.name) - } + @OptIn(ExperimentalCoroutinesApi::class) + val creditCardFilterOptionStrings: StateFlow> = creditCards + .mapLatest { + listOf(ReceiptFilterUiState.ALL_OPTION) + it.map { creditCard -> creditCard.name } } - ) - { list1, list2 , list3-> - list1 + list2 + list3 - }.stateIn( - scope = viewModelScope, - started = WhileUiSubscribed, - initialValue = listOf() - ) + .stateIn( + scope = viewModelScope, + started = SharingStarted.Eagerly, + initialValue = emptyList() + ) - val purchaseTypeOptions = PurchaseType.displayStrings + val purchaseTypeFilterOptionStrings = + listOf(ReceiptFilterUiState.ALL_OPTION) + PurchaseType.displayStrings - private val _selectedCardFilter = - MutableStateFlow(savedStateHandle.get(KEY_SELECTED_CARD_FILTER)) - val selectedCardFilter: StateFlow = _selectedCardFilter + 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) - private val _selectedPurchaseTypeFilter = - MutableStateFlow(savedStateHandle.get(KEY_SELECTED_PURCHASE_TYPE_FILTER)) - val selectedPurchaseTypeFilter: StateFlow = _selectedPurchaseTypeFilter + _receiptFilterUiState.value = _receiptFilterUiState.value.copy( + selectedCreditCardFilterOption = creditCards.value[optionIndex - 1].name + ) + } - val filteredReceipts: StateFlow> = combine( - receipts, selectedCardFilter, selectedPurchaseTypeFilter - ) { receipts, cardFilter, purchaseTypeFilter -> - var filtered = receipts - if (cardFilter?.id != null) { - cardFilter.let { - filtered = filtered.filter { it.creditCardId == cardFilter.id } - } - } - if (purchaseTypeFilter != "All") { - purchaseTypeFilter?.let { - filtered = filtered.filter { it.type == purchaseTypeFilter } - } - } - filtered - }.stateIn(viewModelScope, SharingStarted.Lazily, emptyList()) + fun setPurchaseTypeFilter(optionIndex: Int) { + savedStateHandle[MY_RECEIPTS_SCREEN_SAVED_FILTER_KEY] = savedCreditCardFilter + .value + .copy(filteredPurchaseType = if (optionIndex == 0) null else PurchaseType.entries[optionIndex - 1]) - private suspend fun getCardName(id: UUID): String { - val card = cashBackCreditCardRepository.getCreditCardInfo(id) - ?: pointBackCreditCardRepository.getCreditCardInfo(id) - return card!!.name + _receiptFilterUiState.value = _receiptFilterUiState.value.copy( + selectedPurchaseTypeFilterOption = purchaseTypeFilterOptionStrings[optionIndex] + ) } - fun setCardFilter(card: ParcelableCreditCardInfo) { - _selectedCardFilter.value = card - savedStateHandle[KEY_SELECTED_CARD_FILTER] = card + fun deleteReceipt(id: UUID) = viewModelScope.launch { + receiptRepository.removePurchase(id) } - fun setPurchaseTypeFilter(purchaseType: String) { - _selectedPurchaseTypeFilter.value = purchaseType - savedStateHandle[KEY_SELECTED_PURCHASE_TYPE_FILTER] = purchaseType + 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!! } - fun deleteReceipt(id: UUID) = viewModelScope.launch { - receiptRepository.removePurchase(id) + 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/ParcelableCreditCardInfo.kt b/app/src/main/java/edu/card/clarity/presentation/myReceiptsScreen/ParcelableCreditCardInfo.kt deleted file mode 100644 index a978597..0000000 --- a/app/src/main/java/edu/card/clarity/presentation/myReceiptsScreen/ParcelableCreditCardInfo.kt +++ /dev/null @@ -1,11 +0,0 @@ -package edu.card.clarity.presentation.myReceiptsScreen - -import android.os.Parcelable -import kotlinx.parcelize.Parcelize -import java.util.UUID - -@Parcelize -data class ParcelableCreditCardInfo( - val id: UUID? = null, - val name: String -): Parcelable \ 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/Receipts.kt b/app/src/main/java/edu/card/clarity/presentation/myReceiptsScreen/ReceiptItem.kt similarity index 68% rename from app/src/main/java/edu/card/clarity/presentation/myReceiptsScreen/Receipts.kt rename to app/src/main/java/edu/card/clarity/presentation/myReceiptsScreen/ReceiptItem.kt index 57dfab9..41ce5ee 100644 --- a/app/src/main/java/edu/card/clarity/presentation/myReceiptsScreen/Receipts.kt +++ b/app/src/main/java/edu/card/clarity/presentation/myReceiptsScreen/ReceiptItem.kt @@ -8,7 +8,6 @@ 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.lazy.LazyColumn import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults @@ -16,34 +15,17 @@ import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import androidx.hilt.navigation.compose.hiltViewModel -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.navigation.NavController +import java.util.UUID @Composable -fun Receipts( - navController: NavController, - viewModel: MyReceiptsScreenViewModel = hiltViewModel() +fun ReceiptsItem( + receipt: ReceiptUiState, + onRemoveReceipt: (receiptId: UUID) -> Unit ) { - val receipts by viewModel.filteredReceipts.collectAsStateWithLifecycle() - - LazyColumn { - items(receipts.size) { index -> - ReceiptsItem(receipts[index]) - } - } -} - - - -@Composable -fun ReceiptsItem(receipt: ReceiptsUiState, - viewModel: MyReceiptsScreenViewModel = hiltViewModel()) { Card( modifier = Modifier .fillMaxWidth() @@ -62,15 +44,15 @@ fun ReceiptsItem(receipt: ReceiptsUiState, fontSize = 16.sp ) Text( - text = "Card Used: ${receipt.creditCard}", + text = "Card Used: ${receipt.creditCardName}", fontSize = 16.sp ) Text( - text = "Purchase Type: ${receipt.type}", + text = "Purchase Type: ${receipt.purchaseType}", fontSize = 16.sp ) Text( - text = "Date: ${receipt.time}", + text = "Date: ${receipt.purchaseTime}", fontSize = 16.sp ) Text( @@ -86,9 +68,11 @@ fun ReceiptsItem(receipt: ReceiptsUiState, ) { Spacer(modifier = Modifier.weight(2f)) Button( - onClick = { /*TODO: Handle view click*/ }, - colors = ButtonDefaults.buttonColors(containerColor = Color.White, - contentColor = Color.Black), + 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) @@ -97,9 +81,11 @@ fun ReceiptsItem(receipt: ReceiptsUiState, } Spacer(modifier = Modifier.weight(0.2f)) Button( - onClick = { viewModel.deleteReceipt(receipt.id) }, - colors = ButtonDefaults.buttonColors(containerColor = Color.White, - contentColor = Color.Black), + 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) diff --git a/app/src/main/java/edu/card/clarity/presentation/myReceiptsScreen/ReceiptsUiState.kt b/app/src/main/java/edu/card/clarity/presentation/myReceiptsScreen/ReceiptUiState.kt similarity index 59% rename from app/src/main/java/edu/card/clarity/presentation/myReceiptsScreen/ReceiptsUiState.kt rename to app/src/main/java/edu/card/clarity/presentation/myReceiptsScreen/ReceiptUiState.kt index f416c82..28a1006 100644 --- a/app/src/main/java/edu/card/clarity/presentation/myReceiptsScreen/ReceiptsUiState.kt +++ b/app/src/main/java/edu/card/clarity/presentation/myReceiptsScreen/ReceiptUiState.kt @@ -2,12 +2,12 @@ package edu.card.clarity.presentation.myReceiptsScreen import java.util.UUID -data class ReceiptsUiState ( +data class ReceiptUiState( val id: UUID, - val time: String, + val purchaseTime: String, val merchant: String, - val type: String, + val purchaseType: String, val total: String, val creditCardId: UUID, - val creditCard: String + val creditCardName: String ) \ No newline at end of file diff --git a/app/src/main/java/edu/card/clarity/presentation/myReceiptsScreen/ReceiptsFilter.kt b/app/src/main/java/edu/card/clarity/presentation/myReceiptsScreen/ReceiptsFilter.kt deleted file mode 100644 index ec43b27..0000000 --- a/app/src/main/java/edu/card/clarity/presentation/myReceiptsScreen/ReceiptsFilter.kt +++ /dev/null @@ -1,44 +0,0 @@ -package edu.card.clarity.presentation.myReceiptsScreen - - -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.height -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import edu.card.clarity.presentation.common.DropdownMenu - -@Composable -fun ReceiptsFilter( - viewModel: MyReceiptsScreenViewModel = hiltViewModel() -) { - val creditCards by viewModel.cards.collectAsStateWithLifecycle() - val selectedCardFilter by viewModel.selectedCardFilter.collectAsStateWithLifecycle() - val selectedPurchaseTypeFilter by viewModel.selectedPurchaseTypeFilter.collectAsStateWithLifecycle() - - - DropdownMenu( - label = "Credit Card", - options = creditCards.map { if (it.name == "") "Unnamed Card" else it.name }, - selectedOption = if (selectedCardFilter?.name == "") "Unnamed Card" else selectedCardFilter?.name ?: "All" - ) { index -> - viewModel.setCardFilter(creditCards[index]) - } - - Spacer(modifier = Modifier.height(8.dp)) - - val purchaseTypeOptions = listOf("All") + viewModel.purchaseTypeOptions - - DropdownMenu( - label = "Purchase Type", - options = purchaseTypeOptions, - selectedOption = selectedPurchaseTypeFilter ?: "All" - ) { index -> - viewModel.setPurchaseTypeFilter(purchaseTypeOptions[index]) - } - - Spacer(modifier = Modifier.height(16.dp)) -} \ No newline at end of file From 31919eff76951e0c9923194a96a23258f2dec3bb Mon Sep 17 00:00:00 2001 From: Linus Zhang Date: Tue, 23 Jul 2024 21:29:27 -0400 Subject: [PATCH 5/5] Merged upstream. --- .../clarity/presentation/navigation/BottomNavGraph.kt | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) 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 fce977a..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.myReceiptsScreen.MyReceiptsScreen 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) {