diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml
index ca16a99..12f8d73 100644
--- a/.idea/deploymentTargetSelector.xml
+++ b/.idea/deploymentTargetSelector.xml
@@ -6,6 +6,18 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index f77778f..f77192d 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 = 6
- versionName = "1.0.5"
+ versionCode = 7
+ versionName = "1.0.6"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables { useSupportLibrary = true }
}
diff --git a/app/src/main/java/com/oqba26/monthlypaymentapp/MainActivity.kt b/app/src/main/java/com/oqba26/monthlypaymentapp/MainActivity.kt
index c072b1f..2b2ab67 100644
--- a/app/src/main/java/com/oqba26/monthlypaymentapp/MainActivity.kt
+++ b/app/src/main/java/com/oqba26/monthlypaymentapp/MainActivity.kt
@@ -490,7 +490,7 @@ fun AuthenticatedContent(viewModel: PersonViewModel, contactViewModel: ContactVi
)
}
composable(Screen.Archive.route) {
- ArchiveScreen(viewModel)
+ ArchiveScreen(viewModel = viewModel, navController = navController)
}
composable(Screen.Settings.route) {
val authViewModel: AuthViewModel = hiltViewModel()
diff --git a/app/src/main/java/com/oqba26/monthlypaymentapp/data/dao/PaymentDao.kt b/app/src/main/java/com/oqba26/monthlypaymentapp/data/dao/PaymentDao.kt
index 739b745..3f6f0f3 100644
--- a/app/src/main/java/com/oqba26/monthlypaymentapp/data/dao/PaymentDao.kt
+++ b/app/src/main/java/com/oqba26/monthlypaymentapp/data/dao/PaymentDao.kt
@@ -25,6 +25,9 @@ interface PaymentDao {
@Query("DELETE FROM payments WHERE id = :paymentId")
suspend fun deleteById(paymentId: String)
+ @Query("DELETE FROM payments WHERE personId = :personId")
+ suspend fun deletePaymentsForPerson(personId: String)
+
/** حذف گروهی — برای اعمال [com.oqba26.monthlypaymentapp.data.sync.MergePlan] در یک کوئری. */
@Query("DELETE FROM payments WHERE id IN (:ids)")
suspend fun deleteByIds(ids: List)
diff --git a/app/src/main/java/com/oqba26/monthlypaymentapp/data/repository/LocalPersonRepository.kt b/app/src/main/java/com/oqba26/monthlypaymentapp/data/repository/LocalPersonRepository.kt
index 184649f..77214e4 100644
--- a/app/src/main/java/com/oqba26/monthlypaymentapp/data/repository/LocalPersonRepository.kt
+++ b/app/src/main/java/com/oqba26/monthlypaymentapp/data/repository/LocalPersonRepository.kt
@@ -135,6 +135,7 @@ class LocalPersonRepository(
suspend fun deletePersonLocally(personId: String) {
database.withTransaction {
personDao.deleteById(personId)
+ paymentDao.deletePaymentsForPerson(personId)
syncQueueDao.insert(SyncQueue(entityId = personId, type = SyncType.PERSON, operation = SyncOperation.DELETE))
}
}
diff --git a/app/src/main/java/com/oqba26/monthlypaymentapp/ui/screens/ArchiveScreen.kt b/app/src/main/java/com/oqba26/monthlypaymentapp/ui/screens/ArchiveScreen.kt
index b6fc5f5..11eea30 100644
--- a/app/src/main/java/com/oqba26/monthlypaymentapp/ui/screens/ArchiveScreen.kt
+++ b/app/src/main/java/com/oqba26/monthlypaymentapp/ui/screens/ArchiveScreen.kt
@@ -1,5 +1,6 @@
package com.oqba26.monthlypaymentapp.ui.screens
+import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
@@ -38,12 +39,13 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
+import androidx.navigation.NavController
import com.oqba26.monthlypaymentapp.viewmodel.PersonScreenEvent
import com.oqba26.monthlypaymentapp.viewmodel.PersonUiModel
import com.oqba26.monthlypaymentapp.viewmodel.PersonViewModel
@Composable
-fun ArchiveScreen(viewModel: PersonViewModel) {
+fun ArchiveScreen(viewModel: PersonViewModel, navController: NavController) {
val uiState by viewModel.uiState.collectAsState()
var personToDelete by remember { mutableStateOf(null) }
var personToRestore by remember { mutableStateOf(null) }
@@ -63,6 +65,9 @@ fun ArchiveScreen(viewModel: PersonViewModel) {
items(uiState.archivedPersons, key = { it.id }) { person ->
ArchivedPersonListItem(
person = person,
+ onPersonClick = {
+ navController.navigate("person_detail/${person.id}")
+ },
onRestore = {
personToRestore = person
},
@@ -148,12 +153,14 @@ fun ArchiveScreen(viewModel: PersonViewModel) {
@Composable
fun ArchivedPersonListItem(
person: PersonUiModel,
+ onPersonClick: () -> Unit,
onRestore: () -> Unit,
onDelete: () -> Unit
) {
Row(
modifier = Modifier
.fillMaxWidth()
+ .clickable(onClick = onPersonClick)
.padding(horizontal = 16.dp, vertical = 12.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
@@ -188,4 +195,4 @@ fun ArchivedPersonListItem(
}
}
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/com/oqba26/monthlypaymentapp/ui/screens/PersonDetailScreen.kt b/app/src/main/java/com/oqba26/monthlypaymentapp/ui/screens/PersonDetailScreen.kt
index cc6efbe..5ed627f 100644
--- a/app/src/main/java/com/oqba26/monthlypaymentapp/ui/screens/PersonDetailScreen.kt
+++ b/app/src/main/java/com/oqba26/monthlypaymentapp/ui/screens/PersonDetailScreen.kt
@@ -25,6 +25,7 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.ChevronLeft
import androidx.compose.material.icons.filled.ChevronRight
+import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.Edit
import androidx.compose.material.icons.filled.Search
import androidx.compose.material3.Button
@@ -43,7 +44,6 @@ import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.RadioButton
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
-import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
@@ -128,7 +128,6 @@ fun PersonDetailScreen(
items(detailState.monthStates) { monthModel ->
MonthListItem(
month = monthModel,
- personName = detailState.person?.name ?: "",
startMonth = detailState.person?.startMonth ?: 1,
onHeaderClick = {
expandedMonth = if (expandedMonth == monthModel.month) null else monthModel.month
@@ -207,12 +206,18 @@ fun PersonDetailScreen(
initialPhone = detailState.person?.phoneNumber,
initialCommitment = detailState.person?.monthlyCommitment ?: 0.0,
initialStartMonth = detailState.person?.startMonth ?: 1,
- initialStartYear = detailState.person?.startYear ?: com.oqba26.monthlypaymentapp.utils.getCurrentShamsiYear(),
+ initialStartYear = detailState.person?.startYear ?: getCurrentShamsiYear(),
+ initialIsAnonymous = detailState.person?.isAnonymous ?: false,
isMosqueCategory = currentCategory == "mosque",
- onConfirm = { name, phone, commitment, month, year ->
- viewModel.onEvent(PersonScreenEvent.UpdatePerson(personId, name, phone, commitment, month, year))
+ onConfirm = { name, phone, commitment, month, year, isAnon ->
+ viewModel.onEvent(PersonScreenEvent.UpdatePerson(personId, name, phone, commitment, month, year, isAnon))
editingPerson = false
},
+ onDelete = {
+ viewModel.onEvent(PersonScreenEvent.DeletePerson(personId))
+ editingPerson = false
+ navController.popBackStack()
+ },
onDismiss = { editingPerson = false },
onSearchContact = { name ->
contactViewModel.findSimilarContacts(name)
@@ -256,16 +261,20 @@ fun UpdatePersonDialog(
initialCommitment: Double = 0.0,
initialStartMonth: Int = 1,
initialStartYear: Int = 1403,
+ initialIsAnonymous: Boolean = false,
isMosqueCategory: Boolean = false,
- onConfirm: (String, String, Double, Int, Int) -> Unit,
+ onConfirm: (String, String, Double, Int, Int, Boolean) -> Unit,
+ onDelete: () -> Unit,
onDismiss: () -> Unit,
onSearchContact: (String) -> List,
) {
var name by remember { mutableStateOf(initialName) }
var phoneNumber by remember { mutableStateOf(initialPhone ?: "") }
var monthlyCommitment by remember { mutableStateOf(if (initialCommitment > 0) initialCommitment.toInt().toString() else "") }
+ var isAnonymous by remember { mutableStateOf(initialIsAnonymous) }
+ var showDeleteConfirm by remember { mutableStateOf(false) }
- val currentShamsiYear = com.oqba26.monthlypaymentapp.utils.getCurrentShamsiYear()
+ val currentShamsiYear = getCurrentShamsiYear()
var startMonth by remember { mutableIntStateOf(initialStartMonth) }
var startYear by remember { mutableIntStateOf(initialStartYear) }
@@ -307,6 +316,20 @@ fun UpdatePersonDialog(
fontWeight = FontWeight.Bold
)
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .clickable { isAnonymous = !isAnonymous },
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Checkbox(
+ checked = isAnonymous,
+ onCheckedChange = { isAnonymous = it }
+ )
+ Spacer(modifier = Modifier.width(8.dp))
+ Text("ثبت به صورت خیر ناشناس")
+ }
+
OutlinedTextField(
value = name,
onValueChange = { name = it },
@@ -458,12 +481,12 @@ fun UpdatePersonDialog(
onClick = { expandedMonth = true },
modifier = Modifier.fillMaxWidth()
) {
- Text(com.oqba26.monthlypaymentapp.utils.getPersianMonthName(startMonth))
+ Text(getPersianMonthName(startMonth))
}
DropdownMenu(expanded = expandedMonth, onDismissRequest = { expandedMonth = false }) {
(1..12).forEach { m ->
DropdownMenuItem(
- text = { Text(com.oqba26.monthlypaymentapp.utils.getPersianMonthName(m)) },
+ text = { Text(getPersianMonthName(m)) },
onClick = { startMonth = m; expandedMonth = false }
)
}
@@ -481,7 +504,7 @@ fun UpdatePersonDialog(
onClick = {
if (name.isNotBlank()) {
val commitment = monthlyCommitment.toDoubleOrNull() ?: 0.0
- onConfirm(name, phoneNumber, commitment, startMonth, startYear)
+ onConfirm(name, phoneNumber, commitment, startMonth, startYear, isAnonymous)
}
},
enabled = name.isNotBlank()
@@ -498,6 +521,47 @@ fun UpdatePersonDialog(
Text("لغو")
}
}
+
+ Button(
+ onClick = { showDeleteConfirm = true },
+ modifier = Modifier.fillMaxWidth(),
+ colors = ButtonDefaults.buttonColors(
+ containerColor = MaterialTheme.colorScheme.errorContainer,
+ contentColor = MaterialTheme.colorScheme.error
+ )
+ ) {
+ Icon(Icons.Default.Delete, contentDescription = null)
+ Spacer(Modifier.width(4.dp))
+ Text("حذف این شخص")
+ }
+
+ if (showDeleteConfirm) {
+ Dialog(onDismissRequest = { showDeleteConfirm = false }) {
+ CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) {
+ Card(
+ modifier = Modifier.fillMaxWidth(),
+ shape = RoundedCornerShape(24.dp)
+ ) {
+ Column(modifier = Modifier.padding(20.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) {
+ Text(text = "حذف شخص", style = MaterialTheme.typography.headlineSmall, fontWeight = FontWeight.Bold)
+ Text(text = "آیا از حذف این شخص و تمام پرداختهای او مطمئن هستید؟", style = MaterialTheme.typography.bodyMedium)
+ Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically) {
+ Button(onClick = { showDeleteConfirm = false }) { Text("لغو") }
+ Button(
+ onClick = {
+ showDeleteConfirm = false
+ onDelete()
+ },
+ colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.error)
+ ) {
+ Text("حذف")
+ }
+ }
+ }
+ }
+ }
+ }
+ }
}
}
}
@@ -535,7 +599,6 @@ fun YearSelector(year: Int, onYearChange: (Int) -> Unit) {
@Composable
fun MonthListItem(
month: MonthUiModel,
- personName: String,
startMonth: Int,
onHeaderClick: () -> Unit,
onEditClick: () -> Unit
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 73b1ee7..d348312 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
@@ -201,10 +201,10 @@ fun PersonScreen(
if (currentCategory == "mosque" && uiState.unpaidPersons.isNotEmpty()) {
item {
Text(
- text = "لیست خیرین (بدهکار)",
+ text = "لیست خیرین",
modifier = Modifier.padding(16.dp),
style = MaterialTheme.typography.titleMedium,
- color = MaterialTheme.colorScheme.error
+ color = MaterialTheme.colorScheme.primary
)
}
}
@@ -303,14 +303,12 @@ fun PersonScreen(
personForPaymentDialog?.let { person ->
val currentDay = getCurrentShamsiDay()
val currentMonth = getCurrentShamsiMonth()
- val currentYear = getCurrentShamsiYear()
val targetMonth = if (currentDay >= 20) currentMonth else if (currentMonth == 1) 12 else currentMonth - 1
- val targetYear = if (currentDay < 20 && currentMonth == 1) currentYear - 1 else currentYear
val monthName = getPersianMonthName(targetMonth)
val currentDateTime = formatTimestampToPersianDateTime(System.currentTimeMillis())
- val isCurrentMonth = (targetMonth == currentMonth) && (targetYear == currentYear)
+ val isCurrentMonth = targetMonth == currentMonth
val initialDesc = if (isCurrentMonth) {
"پرداخت برای ماه جاری در تاریخ $currentDateTime ثبت شد."
} else {
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 b7308ad..414dbcd 100644
--- a/app/src/main/java/com/oqba26/monthlypaymentapp/viewmodel/PersonViewModel.kt
+++ b/app/src/main/java/com/oqba26/monthlypaymentapp/viewmodel/PersonViewModel.kt
@@ -189,18 +189,8 @@ class PersonViewModel @Inject constructor(
val uiModels = activePersons.map { person ->
val hasPaid = relevantPayments.any { it.personId == person.id }
- val unpaidResult = if (category == "mosque") {
- if (person.monthlyCommitment > 0) {
- val debtEndMonth = if (currentDay >= 20) currentMonth else currentMonth - 1
- val debtStartMonth = if (person.startYear == currentYear) person.startMonth else 1
-
- val unpaid = (debtStartMonth..debtEndMonth).filter { m ->
- allPayments.none { it.personId == person.id && it.shamsiYear == currentYear && it.shamsiMonth == m && it.category == category }
- }
- Triple(unpaid.size, unpaid.size * person.monthlyCommitment, unpaid.map { getPersianMonthName(it) } to unpaid)
- } else {
- Triple(0, 0.0, emptyList() to emptyList())
- }
+ val unpaidResult = if (category == "mosque" || person.isAnonymous) {
+ Triple(0, 0.0, emptyList() to emptyList())
} else {
// Salary logic
val debtEndMonth = if (currentDay >= 20) currentMonth else currentMonth - 1
@@ -374,7 +364,8 @@ class PersonViewModel @Inject constructor(
is PersonScreenEvent.UpdatePerson -> {
val persons = localPersonRepository.getAllPersonsFlow().first()
- val isDuplicate = persons.any { it.id != event.personId && it.category == _currentCategory.value && it.name.trim().equals(event.name.trim(), ignoreCase = true) }
+ val finalName = if (event.isAnonymous && event.name.isBlank()) "ناشناس" else event.name
+ val isDuplicate = !event.isAnonymous && persons.any { it.id != event.personId && it.category == _currentCategory.value && it.name.trim().equals(finalName.trim(), ignoreCase = true) }
if (isDuplicate) {
_toastMessage.emit("خطا: این نام از قبل وجود دارد.")
@@ -382,13 +373,15 @@ class PersonViewModel @Inject constructor(
val currentPerson = persons.find { it.id == event.personId }
if (currentPerson != null) {
val updatedPerson = currentPerson.copy(
- name = event.name,
+ name = finalName,
phoneNumber = event.phoneNumber,
- monthlyCommitment = event.monthlyCommitment,
+ monthlyCommitment = if (event.isAnonymous) 0.0 else event.monthlyCommitment,
startMonth = event.startMonth,
- startYear = event.startYear
+ startYear = event.startYear,
+ isAnonymous = event.isAnonymous
)
localPersonRepository.updatePersonLocally(updatedPerson)
+ _toastMessage.emit("اطلاعات شخص با موفقیت بروز شد.")
}
}
}
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 29c1043..b24c814 100644
--- a/app/src/main/java/com/oqba26/monthlypaymentapp/viewmodel/UiModels.kt
+++ b/app/src/main/java/com/oqba26/monthlypaymentapp/viewmodel/UiModels.kt
@@ -84,7 +84,8 @@ sealed class PersonScreenEvent {
val phoneNumber: String?,
val monthlyCommitment: Double = 0.0,
val startMonth: Int = 1,
- val startYear: Int = 1403
+ val startYear: Int = 1403,
+ val isAnonymous: Boolean = false
) : PersonScreenEvent()
data class DeletePerson(val personId: String) : PersonScreenEvent()
data class ArchivePerson(val personId: String) : PersonScreenEvent()
diff --git a/update.json b/update.json
index c15c866..44c7f61 100644
--- a/update.json
+++ b/update.json
@@ -1,7 +1,7 @@
{
- "versionCode": 6,
- "versionName": "1.0.5",
- "url": "https://github.com/oqba26/MonthlyPaymentApp/releases/download/v1.0.5/MonthlyPaymentApp-v1.0.5.apk",
- "releaseNotes": "اصلاح چیدمان دکمههای دیالوگ آپدیت و تغییر متن دکمه تایید",
+ "versionCode": 7,
+ "versionName": "1.0.6",
+ "url": "https://github.com/oqba26/MonthlyPaymentApp/releases/download/v1.0.6/MonthlyPaymentApp-v1.0.6.apk",
+ "releaseNotes": "بهینهسازی کدهای UI، رفع هشدارهای کامپایلر در صفحات شخص و جزئیات شخص، و بهبود پایداری برنامه",
"isForceUpdate": false
}