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

Link balance items to receipt #373

Merged
merged 16 commits into from
May 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package com.github.se.assocify.screens

import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.assertIsEnabled
import androidx.compose.ui.test.assertIsNotDisplayed
import androidx.compose.ui.test.assertIsNotEnabled
import androidx.compose.ui.test.assertTextContains
import androidx.compose.ui.test.hasTestTag
import androidx.compose.ui.test.junit4.createComposeRule
Expand Down Expand Up @@ -103,7 +105,7 @@ class BalanceDetailedScreenTest :
"Sidonie Bouthors",
Status.Reimbursed))

private val userReceipts =
private val receiptList =
listOf<Receipt>(
Receipt(
"00000000-0000-0000-0000-000000000000",
Expand All @@ -113,6 +115,15 @@ class BalanceDetailedScreenTest :
28,
Status.Pending,
null,
"userId"),
Receipt(
"00000000-0000-0000-0000-000000000001",
"r2",
"a bad receipt",
LocalDate.of(2023, 3, 11),
28,
Status.Approved,
null,
"userId"))

val mockBalanceAPI: BalanceAPI =
Expand Down Expand Up @@ -171,11 +182,13 @@ class BalanceDetailedScreenTest :

val mockReceiptAPI: ReceiptAPI =
mockk<ReceiptAPI>() {
every { getUserReceipts(any(), any()) } answers
every { getAllReceipts(any(), any()) } answers
{
val onSuccessCallback = firstArg<(List<Receipt>) -> Unit>()
onSuccessCallback(userReceipts)
onSuccessCallback(receiptList)
}

every { uploadReceipt(any(), any(), any(), any()) } answers { thirdArg<() -> Unit>()() }
}

lateinit var budgetDetailedViewModel: BudgetDetailedViewModel
Expand Down Expand Up @@ -552,4 +565,52 @@ class BalanceDetailedScreenTest :
onNodeWithText("lots of money").assertIsDisplayed()
}
}

/** Tests what happens when you select or not a receipt */
@Test
fun testReceiptAmountLinkedToBalanceAmount() {
with(composeTestRule) {
onNodeWithTag("createNewItem").performClick()

onNodeWithTag("receiptDropdown").assertIsDisplayed()
// select the receipt r1
onNodeWithTag("receiptDropdown").performClick()
onNodeWithText("r1").performClick()
onNodeWithTag("editAmount").assertIsNotEnabled()
onNodeWithText("0.28").assertIsDisplayed() // the amount of r1
assert(!balanceDetailedViewModel.uiState.value.noReceiptSelected)

// select no receipt
onNodeWithTag("receiptDropdown").performClick()
onNodeWithText("No receipt").performClick()
assert(balanceDetailedViewModel.uiState.value.noReceiptSelected)
onNodeWithTag("editAmount").assertIsEnabled().performTextInput("500")
}
}

/** Tests that status of the receipt is correctly changed */
@Test
fun testReceiptStatusLinkedToBalanceStatus() {
with(composeTestRule) {
onNodeWithTag("createNewItem").performClick()
onNodeWithTag("editDialogName").performTextClearance()
onNodeWithTag("editDialogName").performTextInput("lots of money")
onNodeWithTag("receiptDropdown").assertIsDisplayed()
onNodeWithTag("receiptDropdown").performClick()
onNodeWithText("r1").performClick()
onNodeWithTag("editDialogColumn").performScrollToNode(hasTestTag("editConfirmButton"))
onNodeWithTag("editStatusDropdown").performClick()
onNodeWithText("Approved").performClick()
onNodeWithTag("editDialogAssignee").performTextInput("new name")
onNodeWithTag("editDialogDate").performClick()
onNodeWithContentDescription("Switch to text input mode").performClick()
onNodeWithContentDescription("Date", true).performClick().performTextInput("01012023")
onNodeWithText("OK").performClick()
onNodeWithTag("editConfirmButton").performClick()
verify { mockReceiptAPI.uploadReceipt(any(), any(), any(), any()) }
assert(
balanceDetailedViewModel.uiState.value.receiptList.find { it.title == "r1" }!!.status ==
Status.Approved)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package com.github.se.assocify.screens

import android.net.Uri
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.assertIsNotDisplayed
import androidx.compose.ui.test.assertTextContains
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithContentDescription
Expand Down Expand Up @@ -86,7 +85,6 @@ class ReceiptScreenTest : TestCase(kaspressoBuilder = Kaspresso.Builder.withComp
onNodeWithTag("backButton").assertIsDisplayed()
onNodeWithTag("receiptScreen").assertIsDisplayed()
onNodeWithTag("titleField").assertIsDisplayed()
onNodeWithTag("statusDropdownChip").assertIsDisplayed()
onNodeWithTag("descriptionField").assertIsDisplayed()
onNodeWithTag("amountField").performScrollTo().assertIsDisplayed()
onNodeWithTag("dateField").performScrollTo().assertIsDisplayed()
Expand Down Expand Up @@ -240,20 +238,6 @@ class ReceiptScreenTest : TestCase(kaspressoBuilder = Kaspresso.Builder.withComp
onNodeWithTag("photoSelectionSheet").assertDoesNotExist()
}
}

@Test
fun status() {
with(composeTestRule) {
onNodeWithTag("statusChip").assertTextContains("Pending")
onNodeWithTag("statusChip").performScrollTo().performClick()
onNodeWithText("Approved", true).assertIsDisplayed()
onNodeWithText("Reimbursed", true).assertIsDisplayed()
onNodeWithText("Reimbursed", true).performClick()
onNodeWithTag("statusChip").assertTextContains("Reimbursed")
onNodeWithText("Approved", true).assertIsNotDisplayed()
onNodeWithText("Pending", true).assertIsNotDisplayed()
}
}
}

@RunWith(AndroidJUnit4::class)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.github.se.assocify.ui.screens.treasury.accounting

import androidx.compose.foundation.clickable
import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
Expand All @@ -13,6 +14,7 @@ import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.rememberScrollState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.CheckCircle
import androidx.compose.material.icons.outlined.Add
import androidx.compose.material.icons.outlined.Edit
import androidx.compose.material3.CenterAlignedTopAppBar
Expand Down Expand Up @@ -304,10 +306,11 @@ fun DisplayBalanceItem(
testTag: String
) {
val balanceState by balanceDetailedViewModel.uiState.collectAsState()
val receipt = balanceState.receiptList.find { it.uid == balanceItem.receiptUID }
ListItem(
headlineContent = { Text(balanceItem.nameItem) },
trailingContent = {
Row(verticalAlignment = Alignment.CenterVertically) {
Column(horizontalAlignment = Alignment.End) {
Text(
text =
if (balanceState.filterActive)
Expand All @@ -317,6 +320,11 @@ fun DisplayBalanceItem(
else PriceUtil.fromCents(balanceItem.amount),
modifier = Modifier.padding(end = 4.dp),
style = MaterialTheme.typography.bodyMedium)

Icon(
receipt?.status?.getIcon() ?: Icons.Filled.CheckCircle,
contentDescription = "Status",
)
}
},
supportingContent = { Text(balanceItem.assignee) },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import com.github.se.assocify.model.entities.Receipt
import com.github.se.assocify.model.entities.Status
import com.github.se.assocify.navigation.NavigationActions
import com.github.se.assocify.ui.util.PriceUtil
import io.ktor.client.utils.EmptyContent.status
import java.time.LocalDate
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
Expand Down Expand Up @@ -87,7 +86,7 @@ class BalanceDetailedViewModel(
private fun updateDatabaseValuesInBalance() {
var innerLoadCounter = 2

receiptAPI.getUserReceipts(
receiptAPI.getAllReceipts(
{ receiptList -> _uiState.value = _uiState.value.copy(receiptList = receiptList) }, {})

subCategoryAPI.getSubCategories(
Expand Down Expand Up @@ -232,6 +231,12 @@ class BalanceDetailedViewModel(
_uiState.value.errorDate != null) {
return
}
// update the status of the receipt
val receipt = _uiState.value.receiptList.find { it.uid == balanceItem.receiptUID }
if (receipt != null && receipt.status != balanceItem.status) {
modifyReceiptStatus(receipt, balanceItem.status)
}

balanceApi.updateBalance(
CurrentUser.associationUid!!,
balanceItem,
Expand Down Expand Up @@ -259,6 +264,13 @@ class BalanceDetailedViewModel(
}

fun deleteBalanceItem(balanceItemUid: String) {
// before deleting, put the status of receipt to pending
val balanceItem = _uiState.value.balanceList.find { it.uid == balanceItemUid } ?: return
val receipt = _uiState.value.receiptList.find { it.uid == balanceItem.receiptUID }
if (receipt != null) {
modifyReceiptStatus(receipt, Status.Pending)
}

balanceApi.deleteBalance(
balanceItemUid,
{
Expand Down Expand Up @@ -292,6 +304,12 @@ class BalanceDetailedViewModel(
_uiState.value.errorDate != null) {
return
}
// update the status of the receipt
val receipt = _uiState.value.receiptList.find { it.uid == balanceItem.receiptUID }
if (receipt != null && receipt.status != balanceItem.status) {
modifyReceiptStatus(receipt, balanceItem.status)
}

balanceApi.addBalance(
CurrentUser.associationUid!!,
balanceItem.subcategoryUID,
Expand Down Expand Up @@ -322,7 +340,7 @@ class BalanceDetailedViewModel(
}

fun checkReceipt(receiptUid: String) {
if (receiptUid.isEmpty()) {
if (receiptUid.isEmpty() && !_uiState.value.noReceiptSelected) {
_uiState.value = _uiState.value.copy(errorReceipt = "The receipt cannot be empty!")
} else {
_uiState.value = _uiState.value.copy(errorReceipt = null)
Expand All @@ -332,7 +350,7 @@ class BalanceDetailedViewModel(
fun checkAmount(amount: String) {
if (amount.isEmpty()) {
_uiState.value = _uiState.value.copy(errorAmount = "You cannot have an empty amount!")
} else if (amount.toDoubleOrNull() == null || amount.toDouble() < 0) {
} else if (amount.toDoubleOrNull() == null) {
_uiState.value = _uiState.value.copy(errorAmount = "You have to input a correct amount!!")
} else if (PriceUtil.isTooLarge(amount)) {
_uiState.value = _uiState.value.copy(errorAmount = "Amount is too large!")
Expand Down Expand Up @@ -381,6 +399,47 @@ class BalanceDetailedViewModel(
checkDescription(description)
checkDate(date)
}

/**
* Set the no receipt selected state
*
* @param selected whether no receipt is selected
*/
fun noReceiptSelected(selected: Boolean) {
_uiState.value = _uiState.value.copy(noReceiptSelected = selected)
}

/**
* Modify the status receipt
*
* @param receipt the receipt to modify
* @param status the new status of the receipt
*/
fun modifyReceiptStatus(receipt: Receipt, status: Status) {
receiptAPI.uploadReceipt(
receipt.copy(status = status),
{},
{
_uiState.value =
_uiState.value.copy(
receiptList =
_uiState.value.receiptList.map {
if (it.uid == receipt.uid) {
receipt.copy(status = status)
} else it
})
},
{ receiptFail, _ ->
if (receiptFail) {
CoroutineScope(Dispatchers.Main).launch {
_uiState.value.snackbarState.showSnackbar(
message = "Failed to save status of the receipt",
actionLabel = "Retry",
)
}
}
})
}
}

/**
Expand All @@ -394,6 +453,7 @@ class BalanceDetailedViewModel(
* @param loadingCategory whether the categories are loading
* @param status the current status filter
* @param receiptList the list of receipts
* @param noReceiptSelected whether no receipt is selected
* @param subCategoryList the list of subcategories
* @param editing whether the item is being edited
* @param editedBalanceItem the item being edited
Expand All @@ -410,6 +470,7 @@ data class BalanceItemState(
val loadingCategory: Boolean = false,
val status: Status? = null,
val receiptList: List<Receipt> = emptyList(),
val noReceiptSelected: Boolean = true,
val subCategoryList: List<AccountingSubCategory> = emptyList(),
val editing: Boolean = false,
val creating: Boolean = false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -148,10 +148,24 @@ fun BalancePopUpScreen(balanceDetailedViewModel: BalanceDetailedViewModel) {
ExposedDropdownMenu(
expanded = receiptExpanded,
onDismissRequest = { receiptExpanded = false }) {

// Adding the "No receipt" item
DropdownMenuItem(
text = { Text("No receipt") },
onClick = {
balanceDetailedViewModel.noReceiptSelected(true)
amountString = "" // Clear the amount
receiptUid = "" // Clear the receipt UID
receiptName = "No receipt" // Set the receipt name to "No receipt"
receiptExpanded = false
})

balanceModel.receiptList.forEach { receipt ->
DropdownMenuItem(
text = { Text(receipt.title) },
onClick = {
balanceDetailedViewModel.noReceiptSelected(false)
amountString = PriceUtil.fromCents(receipt.cents)
receiptUid = receipt.uid
receiptName = receipt.title
receiptExpanded = false
Expand All @@ -166,16 +180,20 @@ fun BalancePopUpScreen(balanceDetailedViewModel: BalanceDetailedViewModel) {
OutlinedTextField(
singleLine = true,
isError = balanceModel.errorAmount != null,
modifier = Modifier.padding(8.dp),
modifier = Modifier.padding(8.dp).testTag("editAmount"),
value = amountString,
onValueChange = {
amountString = it
balanceDetailedViewModel.checkAmount(amountString)
if (receiptUid == "") {
amountString = it
balanceDetailedViewModel.checkAmount(amountString)
}
},
label = { Text("Amount") },
keyboardOptions =
KeyboardOptions.Default.copy(keyboardType = KeyboardType.Decimal),
supportingText = { Text(balanceModel.errorAmount ?: "") })
supportingText = { Text(balanceModel.errorAmount ?: "") },
enabled = receiptUid == "" // Disable editing if receipt is not null
)
}

// The TVA box
Expand All @@ -184,7 +202,7 @@ fun BalancePopUpScreen(balanceDetailedViewModel: BalanceDetailedViewModel) {
ExposedDropdownMenuBox(
expanded = balanceTvaExpanded,
onExpandedChange = { balanceTvaExpanded = !balanceTvaExpanded },
modifier = Modifier.testTag("categoryDropdown").padding(8.dp)) {
modifier = Modifier.testTag("editTVADropdown").padding(8.dp)) {
OutlinedTextField(
value = "$tvaString%",
onValueChange = {},
Expand Down Expand Up @@ -262,7 +280,7 @@ fun BalancePopUpScreen(balanceDetailedViewModel: BalanceDetailedViewModel) {
ExposedDropdownMenuBox(
expanded = statusExpanded,
onExpandedChange = { statusExpanded = !statusExpanded },
modifier = Modifier.testTag("categoryDropdown").padding(8.dp)) {
modifier = Modifier.testTag("editStatusDropdown").padding(8.dp)) {
OutlinedTextField(
value = mutableStatus.name,
onValueChange = {},
Expand Down
Loading
Loading