Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4dae0d4c0e | ||
|
|
6eb9ae8ed6 | ||
|
|
ab905b14cb |
No files matched your search
@@ -30,8 +30,8 @@ android {
|
||||
applicationId = "com.oqba26.monthlypaymentapp"
|
||||
minSdk = 25
|
||||
targetSdk = 34
|
||||
versionCode = 3
|
||||
versionName = "1.0.2"
|
||||
versionCode = 4
|
||||
versionName = "1.0.3"
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
vectorDrawables { useSupportLibrary = true }
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.LayoutDirection
|
||||
@@ -37,71 +36,81 @@ fun UpdateDialog(
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(24.dp)
|
||||
.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
Text(
|
||||
text = "بهروزرسانی جدید موجود است",
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
Text(
|
||||
text = "نسخه ${updateInfo.versionName}",
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
color = Color.Gray
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
Text(
|
||||
text = "تغییرات این نسخه:",
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
|
||||
Text(
|
||||
text = updateInfo.releaseNotes,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
modifier = Modifier.padding(vertical = 8.dp)
|
||||
)
|
||||
|
||||
if (updateInfo.isForceUpdate) {
|
||||
Text(
|
||||
text = "نصب این بهروزرسانی برای ادامه استفاده از برنامه الزامی است.",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
modifier = Modifier.padding(top = 8.dp)
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
Column(
|
||||
modifier = Modifier.padding(24.dp)
|
||||
) {
|
||||
Button(
|
||||
onClick = onConfirm,
|
||||
modifier = Modifier.weight(1f),
|
||||
shape = MaterialTheme.shapes.small
|
||||
// Header
|
||||
Text(
|
||||
text = "بهروزرسانی جدید موجود است",
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
Text(
|
||||
text = "نسخه ${updateInfo.versionName}",
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
// Content (Scrollable)
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.weight(1f, fill = false)
|
||||
.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
Text("تایید و بروزرسانی")
|
||||
Text(
|
||||
text = "تغییرات این نسخه:",
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
|
||||
Text(
|
||||
text = updateInfo.releaseNotes,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
modifier = Modifier.padding(vertical = 8.dp)
|
||||
)
|
||||
}
|
||||
|
||||
if (!updateInfo.isForceUpdate) {
|
||||
|
||||
// Footer
|
||||
if (updateInfo.isForceUpdate) {
|
||||
Text(
|
||||
text = "نصب این بهروزرسانی برای ادامه استفاده از برنامه الزامی است.",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
modifier = Modifier.padding(vertical = 8.dp)
|
||||
)
|
||||
} else {
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
if (!updateInfo.isForceUpdate) {
|
||||
OutlinedButton(
|
||||
onClick = onDismiss,
|
||||
modifier = Modifier.weight(1f),
|
||||
colors = ButtonDefaults.outlinedButtonColors(contentColor = MaterialTheme.colorScheme.error),
|
||||
border = ButtonDefaults.outlinedButtonBorder.copy(brush = androidx.compose.ui.graphics.SolidColor(MaterialTheme.colorScheme.error)),
|
||||
shape = MaterialTheme.shapes.medium
|
||||
) {
|
||||
Text("بعداً")
|
||||
}
|
||||
}
|
||||
|
||||
Button(
|
||||
onClick = onDismiss,
|
||||
onClick = onConfirm,
|
||||
modifier = Modifier.weight(1f),
|
||||
colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.error),
|
||||
shape = MaterialTheme.shapes.small
|
||||
shape = MaterialTheme.shapes.medium,
|
||||
elevation = ButtonDefaults.buttonElevation(defaultElevation = 2.dp)
|
||||
) {
|
||||
Text("بعداً")
|
||||
Text("تایید و بروزرسانی")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -109,4 +118,3 @@ fun UpdateDialog(
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<PersonUiModel?>(null) }
|
||||
val personForBulkPayment = remember { mutableStateOf<PersonUiModel?>(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<Int>, 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 ->
|
||||
|
||||
@@ -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<PaymentRecord?>(null) }
|
||||
var addingPaymentMonth by remember { mutableStateOf<Int?>(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<Int>,
|
||||
defaultAmount: Double,
|
||||
onConfirm: (List<Int>, Double) -> Unit,
|
||||
onDismiss: () -> Unit,
|
||||
) {
|
||||
val selectedMonths = remember { mutableStateListOf<Int>().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
|
||||
|
||||
@@ -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<PersonUiModel?>(null) }
|
||||
var personForBulkPayment by remember { mutableStateOf<PersonUiModel?>(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<Int>, 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()} بدهی",
|
||||
|
||||
@@ -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<Int>,
|
||||
defaultAmount: Double,
|
||||
onConfirm: (List<Int>, Double) -> Unit,
|
||||
onDismiss: () -> Unit,
|
||||
) {
|
||||
val selectedMonths = remember { mutableStateListOf<Int>().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("لغو")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<String>() to emptyList<Int>())
|
||||
}
|
||||
} 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)
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ data class PersonUiModel(
|
||||
val isAnonymous: Boolean = false,
|
||||
val monthlyCommitment: Double = 0.0,
|
||||
val unpaidMonthsNames: List<String> = emptyList(),
|
||||
val unpaidMonths: List<Int> = emptyList(),
|
||||
val needsSync: Boolean = false,
|
||||
)
|
||||
|
||||
|
||||
+5
-5
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"versionCode": 3,
|
||||
"versionName": "1.0.2",
|
||||
"url": "https://github.com/oqba26/MonthlyPaymentApp/releases/download/v1.0.2/MonthlyPaymentApp-v1.0.2.apk",
|
||||
"releaseNotes": "بهروزرسانی به نسخه ۱.۰.۲:\n- رفع مشکل چرخه بیانتهای آپدیت.\n- هماهنگسازی نسخههای بیلد و فایل تنظیمات سرور.",
|
||||
"isForceUpdate": true
|
||||
"versionCode": 4,
|
||||
"versionName": "1.0.3",
|
||||
"url": "https://github.com/oqba26/MonthlyPaymentApp/releases/download/v1.0.3/MonthlyPaymentApp-v1.0.3.apk",
|
||||
"releaseNotes": "بهبود طراحی دیالوگ آپدیت و رفع مشکل برش خوردگی دکمهها",
|
||||
"isForceUpdate": false
|
||||
}
|
||||
Reference in new issue
Block a user