From ab905b14cbcdccab74a1e1c916e4c86993d685be Mon Sep 17 00:00:00 2001 From: oqba26 Date: Tue, 4 Aug 2026 18:55:41 +0330 Subject: [PATCH] =?UTF-8?q?=D8=A8=D9=87=D8=A8=D9=88=D8=AF=20=D8=B3=DB=8C?= =?UTF-8?q?=D8=B3=D8=AA=D9=85=20=D9=BE=D8=B1=D8=AF=D8=A7=D8=AE=D8=AA=20?= =?UTF-8?q?=D8=AF=D8=B3=D8=AA=D9=87=20=D8=AC=D9=85=D8=B9=DB=8C=20=D9=88=20?= =?UTF-8?q?=D8=A7=D8=B6=D8=A7=D9=81=D9=87=20=D8=B4=D8=AF=D9=86=20=D9=82?= =?UTF-8?q?=D8=A7=D8=A8=D9=84=DB=8C=D8=AA=20=D9=86=D9=88=D8=B3=D8=A7=D8=B2?= =?UTF-8?q?=DB=8C=20(Pull-to-Refresh)=20=D8=AF=D8=B1=20=D9=84=DB=8C=D8=B3?= =?UTF-8?q?=D8=AA=20=D9=BE=D8=B1=D8=AF=D8=A7=D8=AE=D8=AA=20=D8=B4=D8=AF?= =?UTF-8?q?=D9=87=E2=80=8C=D9=87=D8=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/screens/PaidListScreen.kt | 104 ++++++++---- .../ui/screens/PersonDetailScreen.kt | 150 ------------------ .../ui/screens/PersonScreen.kt | 62 +++++++- .../ui/screens/SharedComponents.kt | 109 +++++++++++++ .../viewmodel/PersonViewModel.kt | 29 +++- .../monthlypaymentapp/viewmodel/UiModels.kt | 1 + 6 files changed, 265 insertions(+), 190 deletions(-) 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 26d2840..58fc81c 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 @@ -4,13 +4,18 @@ 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.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Search +import androidx.compose.material.pullrefresh.PullRefreshIndicator +import androidx.compose.material.pullrefresh.pullRefresh +import androidx.compose.material.pullrefresh.rememberPullRefreshState import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Card @@ -45,6 +50,7 @@ import org.burnoutcrew.reorderable.detectReorderAfterLongPress import org.burnoutcrew.reorderable.rememberReorderableLazyListState import org.burnoutcrew.reorderable.reorderable +@OptIn(ExperimentalMaterialApi::class) @Composable fun PaidListScreen( viewModel: PersonViewModel, @@ -53,8 +59,11 @@ fun PaidListScreen( ) { val uiState by viewModel.uiState.collectAsState() val searchQuery by viewModel.searchQuery.collectAsState() + val defaultPaymentAmount by viewModel.defaultPaymentAmountFlow.collectAsState(initial = 0.0) + val isRefreshing by viewModel.isRefreshing.collectAsState() val personToArchive = remember { mutableStateOf(null) } + val personForBulkPayment = remember { mutableStateOf(null) } val state = rememberReorderableLazyListState( onMove = { from, to -> @@ -69,40 +78,79 @@ fun PaidListScreen( } ) - Scaffold { innerPadding -> - Column(modifier = Modifier.padding(innerPadding)) { - OutlinedTextField( - value = searchQuery, - onValueChange = { viewModel.onSearchQueryChange(it) }, - modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 8.dp), - label = { Text("جستجوی نام...") }, - leadingIcon = { Icon(Icons.Default.Search, contentDescription = "Search") }, - singleLine = true - ) + @OptIn(ExperimentalMaterialApi::class) + val pullRefreshState = rememberPullRefreshState( + refreshing = isRefreshing, + onRefresh = { viewModel.onEvent(PersonScreenEvent.RefreshData) } + ) - LazyColumn( - state = state.listState, - modifier = Modifier.reorderable(state) - ) { - itemsIndexed(uiState.paidPersons, key = { _, person -> person.id }) { index, person -> - ReorderableItem(state, key = person.id) { _ -> - val rowDragModifier = if (searchQuery.isBlank()) Modifier.detectReorderAfterLongPress(state) else Modifier - Row(modifier = rowDragModifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) { - Box(modifier = Modifier.weight(1f)) { - PersonListItem( - person = person, - index = index + 1, - onPersonClick = { navController.navigate("person_detail/${person.id}") }, - onQuickPayClick = { }, - onArchiveClick = { personToArchive.value = it }, - onSmsClick = { contactViewModel.sendSmsReminder(it) } - ) + Scaffold { innerPadding -> + Box( + modifier = Modifier + .fillMaxSize() + .padding(innerPadding) + .pullRefresh(pullRefreshState) + ) { + Column(modifier = Modifier.fillMaxSize()) { + OutlinedTextField( + value = searchQuery, + onValueChange = { viewModel.onSearchQueryChange(it) }, + modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 8.dp), + label = { Text("جستجوی نام...") }, + leadingIcon = { Icon(Icons.Default.Search, contentDescription = "Search") }, + singleLine = true + ) + + LazyColumn( + state = state.listState, + modifier = Modifier.reorderable(state) + ) { + itemsIndexed(uiState.paidPersons, key = { _, person -> person.id }) { index, person -> + ReorderableItem(state, key = person.id) { _ -> + val rowDragModifier = if (searchQuery.isBlank()) Modifier.detectReorderAfterLongPress(state) else Modifier + Row(modifier = rowDragModifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) { + Box(modifier = Modifier.weight(1f)) { + PersonListItem( + person = person, + index = index + 1, + onPersonClick = { navController.navigate("person_detail/${person.id}") }, + onQuickPayClick = { }, + onArchiveClick = { personToArchive.value = it }, + onSmsClick = { contactViewModel.sendSmsReminder(it) }, + onDebtClick = { personForBulkPayment.value = it } + ) + } } + HorizontalDivider(color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.1f)) } - HorizontalDivider(color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.1f)) } } } + + PullRefreshIndicator( + refreshing = isRefreshing, + state = pullRefreshState, + modifier = Modifier.align(Alignment.TopCenter) + ) + } + + personForBulkPayment.value?.let { person: PersonUiModel -> + BulkPaymentDialog( + availableMonths = person.unpaidMonths, + defaultAmount = if (person.monthlyCommitment > 0) person.monthlyCommitment else defaultPaymentAmount, + onConfirm = { selected: List, amount: Double -> + viewModel.onEvent( + PersonScreenEvent.AddBulkPayments( + personId = person.id, + months = selected, + year = com.oqba26.monthlypaymentapp.utils.getCurrentShamsiYear(), + amount = amount + ) + ) + personForBulkPayment.value = null + }, + onDismiss = { personForBulkPayment.value = null } + ) } personToArchive.value?.let { person -> 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 4bbc066..cc6efbe 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 @@ -26,7 +26,6 @@ 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.Edit -import androidx.compose.material.icons.filled.Payments import androidx.compose.material.icons.filled.Search import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults @@ -51,7 +50,6 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf -import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue @@ -102,7 +100,6 @@ fun PersonDetailScreen( var editingPayment by remember { mutableStateOf(null) } var addingPaymentMonth by remember { mutableStateOf(null) } var editingPerson by remember { mutableStateOf(value = false) } - var showBulkPaymentDialog by remember { mutableStateOf(value = false) } Column(modifier = Modifier.fillMaxSize()) { val displayName = detailState.person?.let { p -> @@ -125,24 +122,6 @@ fun PersonDetailScreen( viewModel.onEvent(PersonScreenEvent.ChangeYear(offset)) } - val availableMonths = detailState.monthStates - .asSequence() - .filter { it.status == MonthStatus.AVAILABLE } - .map { it.month } - .toList() - - if (availableMonths.isNotEmpty()) { - Button( - onClick = { showBulkPaymentDialog = true }, - modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 4.dp), - colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.tertiary) - ) { - Icon(Icons.Default.Payments, contentDescription = null) - Spacer(Modifier.width(8.dp)) - Text("پرداخت دسته جمعی (${availableMonths.size.toString().toPersianDigits()} ماه)") - } - } - Spacer(modifier = Modifier.height(4.dp)) LazyColumn(modifier = Modifier.fillMaxSize().padding(bottom = 8.dp)) { @@ -240,135 +219,6 @@ fun PersonDetailScreen( } ) } - - if (showBulkPaymentDialog) { - val availableMonths = detailState.monthStates - .asSequence() - .filter { it.status == MonthStatus.AVAILABLE } - .map { it.month } - .toList() - - val bulkDefaultAmount = if ((detailState.person?.monthlyCommitment ?: 0.0) > 0) - detailState.person!!.monthlyCommitment - else defaultPaymentAmount - - BulkPaymentDialog( - availableMonths = availableMonths, - defaultAmount = bulkDefaultAmount, - onConfirm = { selected, amount -> - viewModel.onEvent( - PersonScreenEvent.AddBulkPayments( - personId = personId, - months = selected, - year = detailState.selectedYear, - amount = amount - ) - ) - showBulkPaymentDialog = false - }, - ) { showBulkPaymentDialog = false } - } -} - -@Composable -fun BulkPaymentDialog( - availableMonths: List, - defaultAmount: Double, - onConfirm: (List, Double) -> Unit, - onDismiss: () -> Unit, -) { - val selectedMonths = remember { mutableStateListOf().apply { addAll(availableMonths) } } - var amountText by remember { mutableStateOf(defaultAmount.toInt().toString()) } - - Dialog( - onDismissRequest = onDismiss - ) { - 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("ماه‌های مورد نظر را انتخاب کنید:") - - LazyColumn(modifier = Modifier.heightIn(max = 250.dp)) { - items(availableMonths) { month -> - Row( - modifier = Modifier - .fillMaxWidth() - .clickable { - if (selectedMonths.contains(month)) selectedMonths.remove(month) - else selectedMonths.add(month) - } - .padding(vertical = 4.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Checkbox( - checked = selectedMonths.contains(month), - onCheckedChange = { checked -> - if (checked) selectedMonths.add(month) - else selectedMonths.remove(month) - } - ) - Text(getPersianMonthName(month)) - } - } - } - - OutlinedTextField( - value = amountText, - onValueChange = { value -> - // اجازه ورود فقط به اعداد (چه انگلیسی چه فارسی) - val digitsOnly = value.filter { it.isDigit() || (((it in '\u0660'..'\u0669') || (it in '\u06f0'..'\u06f9'))) } - // تبدیل اعداد فارسی به انگلیسی برای ذخیره در استیت - amountText = digitsOnly.map { - when (it) { - in '\u0660'..'\u0669' -> ((it.code - '\u0660'.code) + '0'.code).toChar() - in '\u06f0'..'\u06f9' -> ((it.code - '\u06f0'.code) + '0'.code).toChar() - else -> it - } - }.joinToString("") - }, - label = { Text("مبلغ برای هر ماه (تومان)") }, - modifier = Modifier.fillMaxWidth(), - keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), - visualTransformation = PersianNumberVisualTransformation() - ) - - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - ) { - Button( - onClick = { - val finalAmount = amountText.toDoubleOrNull() ?: defaultAmount - onConfirm(selectedMonths.toList(), finalAmount) - }, - enabled = selectedMonths.isNotEmpty() - ) { - Text("ثبت پرداخت‌ها") - } - - TextButton(onClick = onDismiss) { - Text("لغو") - } - } - } - } - } - } } @Composable 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 66fa805..73b1ee7 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 @@ -83,6 +83,11 @@ import androidx.hilt.navigation.compose.hiltViewModel import androidx.navigation.NavController import com.oqba26.monthlypaymentapp.utils.PersianDigitsTransformation import com.oqba26.monthlypaymentapp.utils.formatNumberAsPersian +import com.oqba26.monthlypaymentapp.utils.formatTimestampToPersianDateTime +import com.oqba26.monthlypaymentapp.utils.getCurrentShamsiDay +import com.oqba26.monthlypaymentapp.utils.getCurrentShamsiMonth +import com.oqba26.monthlypaymentapp.utils.getCurrentShamsiYear +import com.oqba26.monthlypaymentapp.utils.getPersianMonthName import com.oqba26.monthlypaymentapp.utils.toPersianDigits import com.oqba26.monthlypaymentapp.viewmodel.ContactMatch import com.oqba26.monthlypaymentapp.viewmodel.ContactSuggestion @@ -129,6 +134,7 @@ fun PersonScreen( } var personToArchive by remember { mutableStateOf(null) } + var personForBulkPayment by remember { mutableStateOf(null) } val showBulkSelection = contactState.showBulkSmsDialog val reorderableState = rememberReorderableLazyListState( @@ -232,6 +238,9 @@ fun PersonScreen( }, onSmsClick = { selectedPerson -> contactViewModel.onSmsClicked(selectedPerson) + }, + onDebtClick = { selectedPerson -> + personForBulkPayment = selectedPerson } ) } @@ -271,6 +280,9 @@ fun PersonScreen( }, onSmsClick = { selectedPerson -> contactViewModel.onSmsClicked(selectedPerson) + }, + onDebtClick = { selectedPerson -> + personForBulkPayment = selectedPerson } ) } @@ -289,9 +301,26 @@ fun PersonScreen( // Dialogs 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 initialDesc = if (isCurrentMonth) { + "پرداخت برای ماه جاری در تاریخ $currentDateTime ثبت شد." + } else { + "پرداخت برای $monthName در تاریخ $currentDateTime ثبت شد." + } + AddPaymentDialog( personName = person.name, defaultAmount = if (person.monthlyCommitment > 0) person.monthlyCommitment else defaultPaymentAmount, + initialDescription = initialDesc, onConfirm = { amount, description -> viewModel.onEvent(PersonScreenEvent.AddQuickPayment(person.id, amount, description)) viewModel.onDismissPaymentDialog() @@ -348,6 +377,25 @@ fun PersonScreen( ) } + personForBulkPayment?.let { person: PersonUiModel -> + BulkPaymentDialog( + availableMonths = person.unpaidMonths, + defaultAmount = if (person.monthlyCommitment > 0) person.monthlyCommitment else defaultPaymentAmount, + onConfirm = { selected: List, amount: Double -> + viewModel.onEvent( + PersonScreenEvent.AddBulkPayments( + personId = person.id, + months = selected, + year = getCurrentShamsiYear(), + amount = amount + ) + ) + personForBulkPayment = null + }, + onDismiss = { personForBulkPayment = null } + ) + } + if (showBulkSelection) { val debtors = (uiState.unpaidPersons + uiState.paidPersons).filter { it.debtCount > 0 } BulkSmsSelectionDialog( @@ -511,8 +559,8 @@ fun AddNewPersonDialog( var monthlyCommitment by remember { mutableStateOf("") } var initialPaymentAmount by remember { mutableStateOf("") } - val currentShamsiYear = com.oqba26.monthlypaymentapp.utils.getCurrentShamsiYear() - val currentShamsiMonth = com.oqba26.monthlypaymentapp.utils.getCurrentShamsiMonth() + val currentShamsiYear = getCurrentShamsiYear() + val currentShamsiMonth = getCurrentShamsiMonth() var startMonth by remember { mutableIntStateOf(currentShamsiMonth) } var startYear by remember { mutableIntStateOf(currentShamsiYear) } @@ -755,14 +803,14 @@ fun AddNewPersonDialog( 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 } ) } @@ -1058,7 +1106,8 @@ fun PersonListItem( onLongClick: () -> Unit = {}, onQuickPayClick: (PersonUiModel) -> Unit, onArchiveClick: (PersonUiModel) -> Unit, - onSmsClick: (PersonUiModel) -> Unit + onSmsClick: (PersonUiModel) -> Unit, + onDebtClick: (PersonUiModel) -> Unit ) { Card( modifier = modifier @@ -1107,7 +1156,8 @@ fun PersonListItem( if (person.debtCount > 0) { Surface( color = MaterialTheme.colorScheme.errorContainer, - shape = MaterialTheme.shapes.small + shape = MaterialTheme.shapes.small, + modifier = Modifier.clickable { onDebtClick(person) } ) { Text( text = "${person.debtCount.toString().toPersianDigits()} بدهی", diff --git a/app/src/main/java/com/oqba26/monthlypaymentapp/ui/screens/SharedComponents.kt b/app/src/main/java/com/oqba26/monthlypaymentapp/ui/screens/SharedComponents.kt index 699c0ba..0964b98 100644 --- a/app/src/main/java/com/oqba26/monthlypaymentapp/ui/screens/SharedComponents.kt +++ b/app/src/main/java/com/oqba26/monthlypaymentapp/ui/screens/SharedComponents.kt @@ -2,26 +2,33 @@ package com.oqba26.monthlypaymentapp.ui.screens +import androidx.compose.foundation.clickable 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.heightIn import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.icons.filled.Delete import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Card +import androidx.compose.material3.Checkbox import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Text +import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue @@ -37,6 +44,7 @@ import com.oqba26.monthlypaymentapp.R import androidx.compose.ui.window.Dialog import com.oqba26.monthlypaymentapp.data.model.PaymentRecord import com.oqba26.monthlypaymentapp.utils.PersianNumberVisualTransformation +import com.oqba26.monthlypaymentapp.utils.getPersianMonthName @Composable fun EditPaymentDialog( @@ -333,3 +341,104 @@ fun AddPaymentDialog( ) } } + +@Composable +fun BulkPaymentDialog( + availableMonths: List, + defaultAmount: Double, + onConfirm: (List, Double) -> Unit, + onDismiss: () -> Unit, +) { + val selectedMonths = remember { mutableStateListOf().apply { addAll(availableMonths) } } + var amountText by remember { mutableStateOf(defaultAmount.toInt().toString()) } + + Dialog( + onDismissRequest = onDismiss + ) { + 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("ماه‌های مورد نظر را انتخاب کنید:") + + LazyColumn(modifier = Modifier.heightIn(max = 250.dp)) { + items(availableMonths) { month -> + Row( + modifier = Modifier + .fillMaxWidth() + .clickable { + if (selectedMonths.contains(month)) selectedMonths.remove(month) + else selectedMonths.add(month) + } + .padding(vertical = 4.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Checkbox( + checked = selectedMonths.contains(month), + onCheckedChange = { checked -> + if (checked) selectedMonths.add(month) + else selectedMonths.remove(month) + } + ) + Text(getPersianMonthName(month)) + } + } + } + + OutlinedTextField( + value = amountText, + onValueChange = { value -> + // اجازه ورود فقط به اعداد (چه انگلیسی چه فارسی) + val digitsOnly = value.filter { it.isDigit() || (((it in '\u0660'..'\u0669') || (it in '\u06f0'..'\u06f9'))) } + // تبدیل اعداد فارسی به انگلیسی برای ذخیره در استیت + amountText = digitsOnly.map { + when (it) { + in '\u0660'..'\u0669' -> ((it.code - '\u0660'.code) + '0'.code).toChar() + in '\u06f0'..'\u06f9' -> ((it.code - '\u06f0'.code) + '0'.code).toChar() + else -> it + } + }.joinToString("") + }, + label = { Text("مبلغ برای هر ماه (تومان)") }, + modifier = Modifier.fillMaxWidth(), + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), + visualTransformation = PersianNumberVisualTransformation() + ) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Button( + onClick = { + val finalAmount = amountText.toDoubleOrNull() ?: defaultAmount + onConfirm(selectedMonths.toList(), finalAmount) + }, + enabled = selectedMonths.isNotEmpty() + ) { + Text("ثبت پرداخت‌ها") + } + + TextButton(onClick = onDismiss) { + Text("لغو") + } + } + } + } + } + } +} 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 f3425be..b7308ad 100644 --- a/app/src/main/java/com/oqba26/monthlypaymentapp/viewmodel/PersonViewModel.kt +++ b/app/src/main/java/com/oqba26/monthlypaymentapp/viewmodel/PersonViewModel.kt @@ -9,6 +9,7 @@ import com.oqba26.monthlypaymentapp.data.model.Person import com.oqba26.monthlypaymentapp.data.repository.LocalPersonRepository import com.oqba26.monthlypaymentapp.data.repository.NetworkRepository import com.oqba26.monthlypaymentapp.data.repository.SettingsRepository +import com.oqba26.monthlypaymentapp.utils.formatTimestampToPersianDateTime import com.oqba26.monthlypaymentapp.utils.getCurrentShamsiDay import com.oqba26.monthlypaymentapp.utils.getCurrentShamsiMonth import com.oqba26.monthlypaymentapp.utils.getCurrentShamsiYear @@ -188,7 +189,7 @@ class PersonViewModel @Inject constructor( val uiModels = activePersons.map { person -> val hasPaid = relevantPayments.any { it.personId == person.id } - val (debtCount, totalDebtAmount, unpaidMonths) = if (category == "mosque") { + 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 @@ -196,9 +197,9 @@ class PersonViewModel @Inject constructor( 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) }) + Triple(unpaid.size, unpaid.size * person.monthlyCommitment, unpaid.map { getPersianMonthName(it) } to unpaid) } else { - Triple(0, 0.0, emptyList()) + Triple(0, 0.0, emptyList() to emptyList()) } } else { // Salary logic @@ -209,9 +210,12 @@ class PersonViewModel @Inject constructor( allPayments.none { it.personId == person.id && it.shamsiYear == currentYear && it.shamsiMonth == m && it.category == category } } val defaultAmount = settingsRepository.defaultPaymentAmountFlow.first() - Triple(unpaid.size, unpaid.size * defaultAmount, unpaid.map { getPersianMonthName(it) }) + Triple(unpaid.size, unpaid.size * defaultAmount, unpaid.map { getPersianMonthName(it) } to unpaid) } + val (debtCount, totalDebtAmount, unpaidData) = unpaidResult + val (unpaidMonthsNames, unpaidMonths) = unpaidData + val displayName = if (person.isAnonymous && category == "mosque") { "خیر ناشناس (${person.name})" } else { @@ -229,7 +233,8 @@ class PersonViewModel @Inject constructor( phoneNumber = person.phoneNumber, isAnonymous = person.isAnonymous, monthlyCommitment = person.monthlyCommitment, - unpaidMonthsNames = unpaidMonths, + unpaidMonthsNames = unpaidMonthsNames, + unpaidMonths = unpaidMonths, needsSync = person.needsSync ) } @@ -440,13 +445,25 @@ class PersonViewModel @Inject constructor( try { val person = localPersonRepository.getAllPersonsFlow().first().find { it.id == event.personId } val category = person?.category ?: _currentCategory.value + val currentMonth = getCurrentShamsiMonth() + val currentYear = getCurrentShamsiYear() + val currentDateTime = formatTimestampToPersianDateTime(System.currentTimeMillis()) + event.months.forEach { month -> + val monthName = getPersianMonthName(month) + val isCurrentMonth = (month == currentMonth) && (event.year == currentYear) + val description = if (isCurrentMonth) { + "پرداخت برای ماه جاری در تاریخ $currentDateTime ثبت شد." + } else { + "پرداخت برای $monthName در تاریخ $currentDateTime ثبت شد." + } + val record = createPaymentRecord( personId = event.personId, month = month, year = event.year, amount = event.amount, - description = "پرداخت دسته جمعی" + description = description ).copy(category = category) localPersonRepository.insertPaymentLocally(record) } 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 410656d..29c1043 100644 --- a/app/src/main/java/com/oqba26/monthlypaymentapp/viewmodel/UiModels.kt +++ b/app/src/main/java/com/oqba26/monthlypaymentapp/viewmodel/UiModels.kt @@ -15,6 +15,7 @@ data class PersonUiModel( val isAnonymous: Boolean = false, val monthlyCommitment: Double = 0.0, val unpaidMonthsNames: List = emptyList(), + val unpaidMonths: List = emptyList(), val needsSync: Boolean = false, )