Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

My Receipts Page #53

Merged
merged 9 commits into from
Jul 24, 2024
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
3 changes: 2 additions & 1 deletion app/src/main/java/edu/card/clarity/enums/PurchaseType.kt
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,6 @@ enum class PurchaseType {
Groceries,
Restaurants,
Travel,
Others
Others;
companion object
}
Original file line number Diff line number Diff line change
@@ -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()
}
Original file line number Diff line number Diff line change
@@ -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<ReceiptFilter> = savedStateHandle
.getStateFlow(
key = MY_RECEIPTS_SCREEN_SAVED_FILTER_KEY,
initialValue = ReceiptFilter()
)

val receipts: StateFlow<List<ReceiptUiState>> = 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<List<CreditCardInfo>> = combine(
cashBackCreditCardRepository.getAllCreditCardInfoStream(),
pointBackCreditCardRepository.getAllCreditCardInfoStream()
) { cashBack, pointBack ->
cashBack + pointBack
}
.stateIn(
scope = viewModelScope,
started = WhileUiSubscribed,
initialValue = emptyList()
)

@OptIn(ExperimentalCoroutinesApi::class)
val creditCardFilterOptionStrings: StateFlow<List<String>> = 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"
}
}
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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"
}
}
Original file line number Diff line number Diff line change
@@ -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))
}
}
}
}
Loading