diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 0318805..05b0e24 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -30,8 +30,8 @@ android { applicationId = "com.oqba26.monthlypaymentapp" minSdk = 25 targetSdk = 34 - versionCode = 14 - versionName = "1.1.3" + versionCode = 15 + versionName = "1.1.4" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { useSupportLibrary = true } } diff --git a/app/src/main/java/com/oqba26/monthlypaymentapp/ui/screens/PaidListScreen.kt b/app/src/main/java/com/oqba26/monthlypaymentapp/ui/screens/PaidListScreen.kt index 58fc81c..d627f1d 100644 --- a/app/src/main/java/com/oqba26/monthlypaymentapp/ui/screens/PaidListScreen.kt +++ b/app/src/main/java/com/oqba26/monthlypaymentapp/ui/screens/PaidListScreen.kt @@ -113,6 +113,7 @@ fun PaidListScreen( PersonListItem( person = person, index = index + 1, + listType = PersonListType.PAID, onPersonClick = { navController.navigate("person_detail/${person.id}") }, onQuickPayClick = { }, onArchiveClick = { personToArchive.value = it }, diff --git a/app/src/main/java/com/oqba26/monthlypaymentapp/ui/screens/PersonScreen.kt b/app/src/main/java/com/oqba26/monthlypaymentapp/ui/screens/PersonScreen.kt index 484bfd1..c8d8486 100644 --- a/app/src/main/java/com/oqba26/monthlypaymentapp/ui/screens/PersonScreen.kt +++ b/app/src/main/java/com/oqba26/monthlypaymentapp/ui/screens/PersonScreen.kt @@ -1332,6 +1332,7 @@ fun PersonListItem( person: PersonUiModel, index: Int, modifier: Modifier = Modifier, + listType: PersonListType = PersonListType.UNPAID, isSelectionMode: Boolean = false, isSelected: Boolean = false, reorderableState: org.burnoutcrew.reorderable.ReorderableLazyListState? = null, @@ -1368,6 +1369,15 @@ fun PersonListItem( ) } + if (listType == PersonListType.PAID && person.hasPaidThisMonth) { + Icon( + imageVector = Icons.Filled.Check, + contentDescription = "Paid", + tint = MaterialTheme.colorScheme.primary, + modifier = Modifier.size(20.dp).padding(end = 2.dp) + ) + } + val dragModifier = if (reorderableState != null && !isSelectionMode) { Modifier.detectReorderAfterLongPress(reorderableState) } else Modifier @@ -1377,14 +1387,33 @@ fun PersonListItem( style = MaterialTheme.typography.bodyLarge, modifier = dragModifier.padding(end = 4.dp) ) - Text( - person.name, - modifier = Modifier.weight(1f), - style = MaterialTheme.typography.bodyLarge, - fontWeight = FontWeight.Bold, - maxLines = 2, - overflow = TextOverflow.Visible - ) + + Column(modifier = Modifier.weight(1f)) { + Text( + person.name, + style = MaterialTheme.typography.bodyLarge, + fontWeight = FontWeight.Bold, + maxLines = 2, + overflow = TextOverflow.Visible + ) + if (listType == PersonListType.PAID) { + person.lastPaymentTime?.let { time -> + Text( + text = formatTimestampToPersianDateTime(time), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + person.lastPaymentAmount?.let { amount -> + Text( + text = "مبلغ: ${formatNumberAsPersian(amount)} تومان", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.primary, + fontWeight = FontWeight.Medium + ) + } + } + } if (person.debtCount > 0) { Surface( @@ -1420,29 +1449,31 @@ fun PersonListItem( ) } - Spacer(Modifier.width(4.dp)) + if (listType != PersonListType.PAID) { + Spacer(Modifier.width(4.dp)) - Row(horizontalArrangement = Arrangement.spacedBy(4.dp)) { - if (person.hasPaidThisMonth) { - Icon(Icons.Filled.Check, contentDescription = "Paid", tint = MaterialTheme.colorScheme.primary) - } else { - IconButton(onClick = { onQuickPayClick(person) }, modifier = Modifier.size(32.dp)) { - Icon(Icons.Filled.Payments, contentDescription = "Pay", tint = MaterialTheme.colorScheme.primary) + Row(horizontalArrangement = Arrangement.spacedBy(4.dp)) { + if (person.hasPaidThisMonth) { + Icon(Icons.Filled.Check, contentDescription = "Paid", tint = MaterialTheme.colorScheme.primary) + } else { + IconButton(onClick = { onQuickPayClick(person) }, modifier = Modifier.size(32.dp)) { + Icon(Icons.Filled.Payments, contentDescription = "Pay", tint = MaterialTheme.colorScheme.primary) + } } - } - IconButton(onClick = { onArchiveClick(person) }, modifier = Modifier.size(32.dp)) { - Icon(Icons.Filled.Archive, contentDescription = "Archive", tint = MaterialTheme.colorScheme.secondary) - } - if (person.debtCount > 0) { - IconButton(onClick = { onSmsClick(person) }, modifier = Modifier.size(32.dp)) { - Icon( - imageVector = Icons.AutoMirrored.Filled.Message, - contentDescription = "SMS", - tint = MaterialTheme.colorScheme.error, - modifier = Modifier - .size(20.dp) - .graphicsLayer(scaleX = -1f) - ) + IconButton(onClick = { onArchiveClick(person) }, modifier = Modifier.size(32.dp)) { + Icon(Icons.Filled.Archive, contentDescription = "Archive", tint = MaterialTheme.colorScheme.secondary) + } + if (person.debtCount > 0) { + IconButton(onClick = { onSmsClick(person) }, modifier = Modifier.size(32.dp)) { + Icon( + imageVector = Icons.AutoMirrored.Filled.Message, + contentDescription = "SMS", + tint = MaterialTheme.colorScheme.error, + modifier = Modifier + .size(20.dp) + .graphicsLayer(scaleX = -1f) + ) + } } } } diff --git a/app/src/main/java/com/oqba26/monthlypaymentapp/utils/ShamsiUtils.kt b/app/src/main/java/com/oqba26/monthlypaymentapp/utils/ShamsiUtils.kt index 7a8996e..2862faf 100644 --- a/app/src/main/java/com/oqba26/monthlypaymentapp/utils/ShamsiUtils.kt +++ b/app/src/main/java/com/oqba26/monthlypaymentapp/utils/ShamsiUtils.kt @@ -31,7 +31,11 @@ fun formatTimestampToPersianDateTime(timestamp: Long): String { return "$date، ساعت $time" } -fun isTimestampInCurrentCycle(timestamp: Long): Boolean { +fun isTimestampInCurrentCycle(rawTimestamp: Long): Boolean { + if (rawTimestamp <= 0L) return false + + val timestamp = if (rawTimestamp < 100000000000L) rawTimestamp * 1000L else rawTimestamp + val now = PersianDate() val curYear = now.shYear val curMonth = now.shMonth diff --git a/app/src/main/java/com/oqba26/monthlypaymentapp/viewmodel/PersonViewModel.kt b/app/src/main/java/com/oqba26/monthlypaymentapp/viewmodel/PersonViewModel.kt index a59804b..051ea18 100644 --- a/app/src/main/java/com/oqba26/monthlypaymentapp/viewmodel/PersonViewModel.kt +++ b/app/src/main/java/com/oqba26/monthlypaymentapp/viewmodel/PersonViewModel.kt @@ -198,12 +198,21 @@ class PersonViewModel @Inject constructor( it.shamsiMonth == targetMonth && it.shamsiYear == targetYear && it.category == category } + val categoryPayments = allPayments.filter { it.category == category } + val activePersons = persons .filter { !it.isArchived && it.category == category } .sortedWith(compareBy({ it.displayOrder }, { it.createdAt })) val uiModels = activePersons.map { person -> val hasPaid = relevantPayments.any { it.personId == person.id } + + val personCyclePayments = categoryPayments.filter { + it.personId == person.id && isTimestampInCurrentCycle(if ((it.createdAt ?: 0L) > 0L) it.createdAt!! else it.timestamp) + } + val latestPayment = personCyclePayments.maxByOrNull { if ((it.createdAt ?: 0L) > 0L) it.createdAt!! else it.timestamp } + val lastPaymentTime = latestPayment?.let { if ((it.createdAt ?: 0L) > 0L) it.createdAt!! else it.timestamp } + val lastPaymentAmount = latestPayment?.amount val unpaidResult = if (category == "mosque" || person.isAnonymous) { Triple(0, 0.0, emptyList() to emptyList()) @@ -242,14 +251,16 @@ class PersonViewModel @Inject constructor( monthlyCommitment = person.monthlyCommitment, unpaidMonthsNames = unpaidMonthsNames, unpaidMonths = unpaidMonths, - needsSync = person.needsSync + needsSync = person.needsSync, + lastPaymentTime = lastPaymentTime, + lastPaymentAmount = lastPaymentAmount ) } - val categoryPayments = allPayments.filter { it.category == category } updateDashboard(uiModels.size, relevantPayments, categoryPayments) - val (paidModels, unpaidModels) = uiModels.partition { it.hasPaidThisMonth } + val (rawPaidModels, unpaidModels) = uiModels.partition { it.hasPaidThisMonth } + val paidModels = rawPaidModels.sortedByDescending { it.lastPaymentTime ?: 0L } // محاسبه گزارشات مالی جامع val personsMap = persons.associateBy { it.id } @@ -596,7 +607,7 @@ class PersonViewModel @Inject constructor( val paidCount = paymentsThisMonth.map { it.personId }.distinct().size val totalIncome = paymentsThisMonth.sumOf { it.amount } val cyclePayments = categoryPayments.filter { payment -> - val time = payment.createdAt ?: payment.timestamp + val time = if ((payment.createdAt ?: 0L) > 0L) payment.createdAt!! else payment.timestamp isTimestampInCurrentCycle(time) } val totalCycleIncome = cyclePayments.sumOf { it.amount } diff --git a/app/src/main/java/com/oqba26/monthlypaymentapp/viewmodel/UiModels.kt b/app/src/main/java/com/oqba26/monthlypaymentapp/viewmodel/UiModels.kt index a27d0b2..7214a0c 100644 --- a/app/src/main/java/com/oqba26/monthlypaymentapp/viewmodel/UiModels.kt +++ b/app/src/main/java/com/oqba26/monthlypaymentapp/viewmodel/UiModels.kt @@ -17,6 +17,8 @@ data class PersonUiModel( val unpaidMonthsNames: List = emptyList(), val unpaidMonths: List = emptyList(), val needsSync: Boolean = false, + val lastPaymentTime: Long? = null, + val lastPaymentAmount: Double? = null ) data class PersonListUiState( diff --git a/gradle.properties b/gradle.properties index 3c1cc5a..04c24b0 100644 --- a/gradle.properties +++ b/gradle.properties @@ -3,7 +3,7 @@ org.gradle.jvmargs=-Xmx2560m -XX:+UseParallelGC -Dfile.encoding=UTF-8 android.useAndroidX=true -android.enableJetifier=true +android.enableJetifier=false kotlin.code.style=official android.nonTransitiveRClass=true android.suppressUnsupportedCompileSdk=34