Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a3a8448ee5 | ||
|
|
b05c7c7521 | ||
|
|
9db6cf4fa1 | ||
|
|
11030e9109 | ||
|
|
309d0d0b19 | ||
|
|
8b8696a75f | ||
|
|
83f0b82c7a | ||
|
|
5d80bb6f6a | ||
|
|
4dae0d4c0e | ||
|
|
6eb9ae8ed6 | ||
|
|
ab905b14cb |
No files matched your search
Generated
+12
@@ -6,6 +6,18 @@
|
|||||||
<option name="selectionMode" value="DROPDOWN" />
|
<option name="selectionMode" value="DROPDOWN" />
|
||||||
<DialogSelection />
|
<DialogSelection />
|
||||||
</SelectionState>
|
</SelectionState>
|
||||||
|
<SelectionState runConfigName="androidTest">
|
||||||
|
<option name="selectionMode" value="DROPDOWN" />
|
||||||
|
<DialogSelection />
|
||||||
|
</SelectionState>
|
||||||
|
<SelectionState runConfigName="main">
|
||||||
|
<option name="selectionMode" value="DROPDOWN" />
|
||||||
|
<DialogSelection />
|
||||||
|
</SelectionState>
|
||||||
|
<SelectionState runConfigName="unitTest">
|
||||||
|
<option name="selectionMode" value="DROPDOWN" />
|
||||||
|
<DialogSelection />
|
||||||
|
</SelectionState>
|
||||||
</selectionStates>
|
</selectionStates>
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</project>
|
||||||
@@ -30,8 +30,8 @@ android {
|
|||||||
applicationId = "com.oqba26.monthlypaymentapp"
|
applicationId = "com.oqba26.monthlypaymentapp"
|
||||||
minSdk = 25
|
minSdk = 25
|
||||||
targetSdk = 34
|
targetSdk = 34
|
||||||
versionCode = 3
|
versionCode = 7
|
||||||
versionName = "1.0.2"
|
versionName = "1.0.6"
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
vectorDrawables { useSupportLibrary = true }
|
vectorDrawables { useSupportLibrary = true }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -490,7 +490,7 @@ fun AuthenticatedContent(viewModel: PersonViewModel, contactViewModel: ContactVi
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
composable(Screen.Archive.route) {
|
composable(Screen.Archive.route) {
|
||||||
ArchiveScreen(viewModel)
|
ArchiveScreen(viewModel = viewModel, navController = navController)
|
||||||
}
|
}
|
||||||
composable(Screen.Settings.route) {
|
composable(Screen.Settings.route) {
|
||||||
val authViewModel: AuthViewModel = hiltViewModel()
|
val authViewModel: AuthViewModel = hiltViewModel()
|
||||||
|
|||||||
@@ -25,6 +25,9 @@ interface PaymentDao {
|
|||||||
@Query("DELETE FROM payments WHERE id = :paymentId")
|
@Query("DELETE FROM payments WHERE id = :paymentId")
|
||||||
suspend fun deleteById(paymentId: String)
|
suspend fun deleteById(paymentId: String)
|
||||||
|
|
||||||
|
@Query("DELETE FROM payments WHERE personId = :personId")
|
||||||
|
suspend fun deletePaymentsForPerson(personId: String)
|
||||||
|
|
||||||
/** حذف گروهی — برای اعمال [com.oqba26.monthlypaymentapp.data.sync.MergePlan] در یک کوئری. */
|
/** حذف گروهی — برای اعمال [com.oqba26.monthlypaymentapp.data.sync.MergePlan] در یک کوئری. */
|
||||||
@Query("DELETE FROM payments WHERE id IN (:ids)")
|
@Query("DELETE FROM payments WHERE id IN (:ids)")
|
||||||
suspend fun deleteByIds(ids: List<String>)
|
suspend fun deleteByIds(ids: List<String>)
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
@file:OptIn(kotlinx.serialization.InternalSerializationApi::class)
|
@file:OptIn(InternalSerializationApi::class)
|
||||||
|
|
||||||
package com.oqba26.monthlypaymentapp.data.model
|
package com.oqba26.monthlypaymentapp.data.model
|
||||||
|
|
||||||
import androidx.room.Entity
|
import androidx.room.Entity
|
||||||
import androidx.room.PrimaryKey
|
import androidx.room.PrimaryKey
|
||||||
|
import kotlinx.serialization.InternalSerializationApi
|
||||||
import kotlinx.serialization.SerialName
|
import kotlinx.serialization.SerialName
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.Transient
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
@@ -14,22 +16,22 @@ data class PaymentRecord(
|
|||||||
@PrimaryKey
|
@PrimaryKey
|
||||||
@SerialName("id") val id: String = UUID.randomUUID().toString(),
|
@SerialName("id") val id: String = UUID.randomUUID().toString(),
|
||||||
|
|
||||||
@SerialName("personId") val personId: String,
|
@SerialName("person_id") val personId: String,
|
||||||
|
|
||||||
@SerialName("amount") val amount: Double,
|
@SerialName("amount") val amount: Double,
|
||||||
|
|
||||||
@SerialName("shamsiYear") val shamsiYear: Int,
|
@SerialName("shamsi_year") val shamsiYear: Int,
|
||||||
|
|
||||||
@SerialName("shamsiMonth") val shamsiMonth: Int,
|
@SerialName("shamsi_month") val shamsiMonth: Int,
|
||||||
|
|
||||||
@SerialName("timestamp") val timestamp: Long = System.currentTimeMillis(),
|
@SerialName("timestamp") val timestamp: Long = System.currentTimeMillis(),
|
||||||
|
|
||||||
@SerialName("description") val description: String? = "", // ⭐️ تغییر به String? برای پذیرش مقادیر null
|
@SerialName("description") val description: String? = "",
|
||||||
|
|
||||||
@SerialName("createdAt") val createdAt: Long? = null,
|
@SerialName("created_at") val createdAt: Long? = null,
|
||||||
|
|
||||||
@SerialName("category") val category: String = "salary",
|
@SerialName("category") val category: String = "salary",
|
||||||
|
|
||||||
@kotlinx.serialization.Transient
|
@Transient
|
||||||
val needsSync: Boolean = false
|
val needsSync: Boolean = false
|
||||||
)
|
)
|
||||||
@@ -1,11 +1,13 @@
|
|||||||
@file:OptIn(kotlinx.serialization.InternalSerializationApi::class)
|
@file:OptIn(InternalSerializationApi::class)
|
||||||
|
|
||||||
package com.oqba26.monthlypaymentapp.data.model
|
package com.oqba26.monthlypaymentapp.data.model
|
||||||
|
|
||||||
import androidx.room.Entity
|
import androidx.room.Entity
|
||||||
import androidx.room.PrimaryKey
|
import androidx.room.PrimaryKey
|
||||||
|
import kotlinx.serialization.InternalSerializationApi
|
||||||
import kotlinx.serialization.SerialName
|
import kotlinx.serialization.SerialName
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.Transient
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
@Entity(tableName = "persons")
|
@Entity(tableName = "persons")
|
||||||
@@ -15,24 +17,24 @@ data class Person(
|
|||||||
|
|
||||||
@SerialName("name") val name: String,
|
@SerialName("name") val name: String,
|
||||||
|
|
||||||
@SerialName("isArchived") val isArchived: Boolean = false,
|
@SerialName("is_archived") val isArchived: Boolean = false,
|
||||||
|
|
||||||
@SerialName("displayOrder") val displayOrder: Long? = 0L,
|
@SerialName("display_order") val displayOrder: Long? = 0L,
|
||||||
|
|
||||||
@SerialName("createdAt") val createdAt: Long? = 0L,
|
@SerialName("created_at") val createdAt: Long? = 0L,
|
||||||
|
|
||||||
@SerialName("phoneNumber") val phoneNumber: String? = "",
|
@SerialName("phone_number") val phoneNumber: String? = "",
|
||||||
|
|
||||||
@SerialName("category") val category: String = "salary",
|
@SerialName("category") val category: String = "salary",
|
||||||
|
|
||||||
@SerialName("isAnonymous") val isAnonymous: Boolean = false,
|
@SerialName("is_anonymous") val isAnonymous: Boolean = false,
|
||||||
|
|
||||||
@SerialName("monthlyCommitment") val monthlyCommitment: Double = 0.0,
|
@SerialName("monthly_commitment") val monthlyCommitment: Double = 0.0,
|
||||||
|
|
||||||
@SerialName("startMonth") val startMonth: Int = 1,
|
@SerialName("start_month") val startMonth: Int = 1,
|
||||||
|
|
||||||
@SerialName("startYear") val startYear: Int = 1403,
|
@SerialName("start_year") val startYear: Int = 1403,
|
||||||
|
|
||||||
@kotlinx.serialization.Transient
|
@Transient
|
||||||
val needsSync: Boolean = false
|
val needsSync: Boolean = false
|
||||||
)
|
)
|
||||||
+1
@@ -135,6 +135,7 @@ class LocalPersonRepository(
|
|||||||
suspend fun deletePersonLocally(personId: String) {
|
suspend fun deletePersonLocally(personId: String) {
|
||||||
database.withTransaction {
|
database.withTransaction {
|
||||||
personDao.deleteById(personId)
|
personDao.deleteById(personId)
|
||||||
|
paymentDao.deletePaymentsForPerson(personId)
|
||||||
syncQueueDao.insert(SyncQueue(entityId = personId, type = SyncType.PERSON, operation = SyncOperation.DELETE))
|
syncQueueDao.insert(SyncQueue(entityId = personId, type = SyncType.PERSON, operation = SyncOperation.DELETE))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import androidx.compose.material3.*
|
|||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.CompositionLocalProvider
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.platform.LocalLayoutDirection
|
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.LayoutDirection
|
import androidx.compose.ui.unit.LayoutDirection
|
||||||
@@ -37,71 +36,81 @@ fun UpdateDialog(
|
|||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(16.dp)
|
.padding(16.dp)
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier.padding(24.dp)
|
||||||
.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)
|
|
||||||
) {
|
) {
|
||||||
Button(
|
// Header
|
||||||
onClick = onConfirm,
|
Text(
|
||||||
modifier = Modifier.weight(1f),
|
text = "بهروزرسانی جدید موجود است",
|
||||||
shape = MaterialTheme.shapes.small
|
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)
|
||||||
|
) {
|
||||||
Button(
|
Button(
|
||||||
onClick = onDismiss,
|
onClick = onConfirm,
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.error),
|
shape = MaterialTheme.shapes.medium,
|
||||||
shape = MaterialTheme.shapes.small
|
elevation = ButtonDefaults.buttonElevation(defaultElevation = 2.dp)
|
||||||
) {
|
) {
|
||||||
Text("بعداً")
|
Text("بروزرسانی")
|
||||||
|
}
|
||||||
|
|
||||||
|
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("بعداً")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -109,4 +118,3 @@ fun UpdateDialog(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.oqba26.monthlypaymentapp.ui.screens
|
package com.oqba26.monthlypaymentapp.ui.screens
|
||||||
|
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
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.LayoutDirection
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.window.Dialog
|
import androidx.compose.ui.window.Dialog
|
||||||
|
import androidx.navigation.NavController
|
||||||
import com.oqba26.monthlypaymentapp.viewmodel.PersonScreenEvent
|
import com.oqba26.monthlypaymentapp.viewmodel.PersonScreenEvent
|
||||||
import com.oqba26.monthlypaymentapp.viewmodel.PersonUiModel
|
import com.oqba26.monthlypaymentapp.viewmodel.PersonUiModel
|
||||||
import com.oqba26.monthlypaymentapp.viewmodel.PersonViewModel
|
import com.oqba26.monthlypaymentapp.viewmodel.PersonViewModel
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ArchiveScreen(viewModel: PersonViewModel) {
|
fun ArchiveScreen(viewModel: PersonViewModel, navController: NavController) {
|
||||||
val uiState by viewModel.uiState.collectAsState()
|
val uiState by viewModel.uiState.collectAsState()
|
||||||
var personToDelete by remember { mutableStateOf<PersonUiModel?>(null) }
|
var personToDelete by remember { mutableStateOf<PersonUiModel?>(null) }
|
||||||
var personToRestore by remember { mutableStateOf<PersonUiModel?>(null) }
|
var personToRestore by remember { mutableStateOf<PersonUiModel?>(null) }
|
||||||
@@ -63,6 +65,9 @@ fun ArchiveScreen(viewModel: PersonViewModel) {
|
|||||||
items(uiState.archivedPersons, key = { it.id }) { person ->
|
items(uiState.archivedPersons, key = { it.id }) { person ->
|
||||||
ArchivedPersonListItem(
|
ArchivedPersonListItem(
|
||||||
person = person,
|
person = person,
|
||||||
|
onPersonClick = {
|
||||||
|
navController.navigate("person_detail/${person.id}")
|
||||||
|
},
|
||||||
onRestore = {
|
onRestore = {
|
||||||
personToRestore = person
|
personToRestore = person
|
||||||
},
|
},
|
||||||
@@ -148,12 +153,14 @@ fun ArchiveScreen(viewModel: PersonViewModel) {
|
|||||||
@Composable
|
@Composable
|
||||||
fun ArchivedPersonListItem(
|
fun ArchivedPersonListItem(
|
||||||
person: PersonUiModel,
|
person: PersonUiModel,
|
||||||
|
onPersonClick: () -> Unit,
|
||||||
onRestore: () -> Unit,
|
onRestore: () -> Unit,
|
||||||
onDelete: () -> Unit
|
onDelete: () -> Unit
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
|
.clickable(onClick = onPersonClick)
|
||||||
.padding(horizontal = 16.dp, vertical = 12.dp),
|
.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.SpaceBetween
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
@@ -188,4 +195,4 @@ fun ArchivedPersonListItem(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4,13 +4,18 @@ import androidx.compose.foundation.layout.Arrangement
|
|||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.itemsIndexed
|
import androidx.compose.foundation.lazy.itemsIndexed
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material.ExperimentalMaterialApi
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.Search
|
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.Button
|
||||||
import androidx.compose.material3.ButtonDefaults
|
import androidx.compose.material3.ButtonDefaults
|
||||||
import androidx.compose.material3.Card
|
import androidx.compose.material3.Card
|
||||||
@@ -45,6 +50,7 @@ import org.burnoutcrew.reorderable.detectReorderAfterLongPress
|
|||||||
import org.burnoutcrew.reorderable.rememberReorderableLazyListState
|
import org.burnoutcrew.reorderable.rememberReorderableLazyListState
|
||||||
import org.burnoutcrew.reorderable.reorderable
|
import org.burnoutcrew.reorderable.reorderable
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterialApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun PaidListScreen(
|
fun PaidListScreen(
|
||||||
viewModel: PersonViewModel,
|
viewModel: PersonViewModel,
|
||||||
@@ -53,8 +59,11 @@ fun PaidListScreen(
|
|||||||
) {
|
) {
|
||||||
val uiState by viewModel.uiState.collectAsState()
|
val uiState by viewModel.uiState.collectAsState()
|
||||||
val searchQuery by viewModel.searchQuery.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 personToArchive = remember { mutableStateOf<PersonUiModel?>(null) }
|
||||||
|
val personForBulkPayment = remember { mutableStateOf<PersonUiModel?>(null) }
|
||||||
|
|
||||||
val state = rememberReorderableLazyListState(
|
val state = rememberReorderableLazyListState(
|
||||||
onMove = { from, to ->
|
onMove = { from, to ->
|
||||||
@@ -69,40 +78,79 @@ fun PaidListScreen(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
Scaffold { innerPadding ->
|
@OptIn(ExperimentalMaterialApi::class)
|
||||||
Column(modifier = Modifier.padding(innerPadding)) {
|
val pullRefreshState = rememberPullRefreshState(
|
||||||
OutlinedTextField(
|
refreshing = isRefreshing,
|
||||||
value = searchQuery,
|
onRefresh = { viewModel.onEvent(PersonScreenEvent.RefreshData) }
|
||||||
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(
|
Scaffold { innerPadding ->
|
||||||
state = state.listState,
|
Box(
|
||||||
modifier = Modifier.reorderable(state)
|
modifier = Modifier
|
||||||
) {
|
.fillMaxSize()
|
||||||
itemsIndexed(uiState.paidPersons, key = { _, person -> person.id }) { index, person ->
|
.padding(innerPadding)
|
||||||
ReorderableItem(state, key = person.id) { _ ->
|
.pullRefresh(pullRefreshState)
|
||||||
val rowDragModifier = if (searchQuery.isBlank()) Modifier.detectReorderAfterLongPress(state) else Modifier
|
) {
|
||||||
Row(modifier = rowDragModifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
|
Column(modifier = Modifier.fillMaxSize()) {
|
||||||
Box(modifier = Modifier.weight(1f)) {
|
OutlinedTextField(
|
||||||
PersonListItem(
|
value = searchQuery,
|
||||||
person = person,
|
onValueChange = { viewModel.onSearchQueryChange(it) },
|
||||||
index = index + 1,
|
modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 8.dp),
|
||||||
onPersonClick = { navController.navigate("person_detail/${person.id}") },
|
label = { Text("جستجوی نام...") },
|
||||||
onQuickPayClick = { },
|
leadingIcon = { Icon(Icons.Default.Search, contentDescription = "Search") },
|
||||||
onArchiveClick = { personToArchive.value = it },
|
singleLine = true
|
||||||
onSmsClick = { contactViewModel.sendSmsReminder(it) }
|
)
|
||||||
)
|
|
||||||
|
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 ->
|
personToArchive.value?.let { person ->
|
||||||
|
|||||||
@@ -25,8 +25,8 @@ import androidx.compose.material.icons.Icons
|
|||||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||||
import androidx.compose.material.icons.filled.ChevronLeft
|
import androidx.compose.material.icons.filled.ChevronLeft
|
||||||
import androidx.compose.material.icons.filled.ChevronRight
|
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.Edit
|
||||||
import androidx.compose.material.icons.filled.Payments
|
|
||||||
import androidx.compose.material.icons.filled.Search
|
import androidx.compose.material.icons.filled.Search
|
||||||
import androidx.compose.material3.Button
|
import androidx.compose.material3.Button
|
||||||
import androidx.compose.material3.ButtonDefaults
|
import androidx.compose.material3.ButtonDefaults
|
||||||
@@ -44,14 +44,12 @@ import androidx.compose.material3.OutlinedTextField
|
|||||||
import androidx.compose.material3.RadioButton
|
import androidx.compose.material3.RadioButton
|
||||||
import androidx.compose.material3.Surface
|
import androidx.compose.material3.Surface
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.CompositionLocalProvider
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableIntStateOf
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
import androidx.compose.runtime.mutableStateListOf
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
@@ -102,7 +100,6 @@ fun PersonDetailScreen(
|
|||||||
var editingPayment by remember { mutableStateOf<PaymentRecord?>(null) }
|
var editingPayment by remember { mutableStateOf<PaymentRecord?>(null) }
|
||||||
var addingPaymentMonth by remember { mutableStateOf<Int?>(null) }
|
var addingPaymentMonth by remember { mutableStateOf<Int?>(null) }
|
||||||
var editingPerson by remember { mutableStateOf(value = false) }
|
var editingPerson by remember { mutableStateOf(value = false) }
|
||||||
var showBulkPaymentDialog by remember { mutableStateOf(value = false) }
|
|
||||||
|
|
||||||
Column(modifier = Modifier.fillMaxSize()) {
|
Column(modifier = Modifier.fillMaxSize()) {
|
||||||
val displayName = detailState.person?.let { p ->
|
val displayName = detailState.person?.let { p ->
|
||||||
@@ -125,31 +122,12 @@ fun PersonDetailScreen(
|
|||||||
viewModel.onEvent(PersonScreenEvent.ChangeYear(offset))
|
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))
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
|
|
||||||
LazyColumn(modifier = Modifier.fillMaxSize().padding(bottom = 8.dp)) {
|
LazyColumn(modifier = Modifier.fillMaxSize().padding(bottom = 8.dp)) {
|
||||||
items(detailState.monthStates) { monthModel ->
|
items(detailState.monthStates) { monthModel ->
|
||||||
MonthListItem(
|
MonthListItem(
|
||||||
month = monthModel,
|
month = monthModel,
|
||||||
personName = detailState.person?.name ?: "",
|
|
||||||
startMonth = detailState.person?.startMonth ?: 1,
|
startMonth = detailState.person?.startMonth ?: 1,
|
||||||
onHeaderClick = {
|
onHeaderClick = {
|
||||||
expandedMonth = if (expandedMonth == monthModel.month) null else monthModel.month
|
expandedMonth = if (expandedMonth == monthModel.month) null else monthModel.month
|
||||||
@@ -228,147 +206,24 @@ fun PersonDetailScreen(
|
|||||||
initialPhone = detailState.person?.phoneNumber,
|
initialPhone = detailState.person?.phoneNumber,
|
||||||
initialCommitment = detailState.person?.monthlyCommitment ?: 0.0,
|
initialCommitment = detailState.person?.monthlyCommitment ?: 0.0,
|
||||||
initialStartMonth = detailState.person?.startMonth ?: 1,
|
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",
|
isMosqueCategory = currentCategory == "mosque",
|
||||||
onConfirm = { name, phone, commitment, month, year ->
|
onConfirm = { name, phone, commitment, month, year, isAnon ->
|
||||||
viewModel.onEvent(PersonScreenEvent.UpdatePerson(personId, name, phone, commitment, month, year))
|
viewModel.onEvent(PersonScreenEvent.UpdatePerson(personId, name, phone, commitment, month, year, isAnon))
|
||||||
editingPerson = false
|
editingPerson = false
|
||||||
},
|
},
|
||||||
|
onDelete = {
|
||||||
|
viewModel.onEvent(PersonScreenEvent.DeletePerson(personId))
|
||||||
|
editingPerson = false
|
||||||
|
navController.popBackStack()
|
||||||
|
},
|
||||||
onDismiss = { editingPerson = false },
|
onDismiss = { editingPerson = false },
|
||||||
onSearchContact = { name ->
|
onSearchContact = { name ->
|
||||||
contactViewModel.findSimilarContacts(name)
|
contactViewModel.findSimilarContacts(name)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
@Composable
|
||||||
@@ -406,16 +261,20 @@ fun UpdatePersonDialog(
|
|||||||
initialCommitment: Double = 0.0,
|
initialCommitment: Double = 0.0,
|
||||||
initialStartMonth: Int = 1,
|
initialStartMonth: Int = 1,
|
||||||
initialStartYear: Int = 1403,
|
initialStartYear: Int = 1403,
|
||||||
|
initialIsAnonymous: Boolean = false,
|
||||||
isMosqueCategory: Boolean = false,
|
isMosqueCategory: Boolean = false,
|
||||||
onConfirm: (String, String, Double, Int, Int) -> Unit,
|
onConfirm: (String, String, Double, Int, Int, Boolean) -> Unit,
|
||||||
|
onDelete: () -> Unit,
|
||||||
onDismiss: () -> Unit,
|
onDismiss: () -> Unit,
|
||||||
onSearchContact: (String) -> List<ContactMatch>,
|
onSearchContact: (String) -> List<ContactMatch>,
|
||||||
) {
|
) {
|
||||||
var name by remember { mutableStateOf(initialName) }
|
var name by remember { mutableStateOf(initialName) }
|
||||||
var phoneNumber by remember { mutableStateOf(initialPhone ?: "") }
|
var phoneNumber by remember { mutableStateOf(initialPhone ?: "") }
|
||||||
var monthlyCommitment by remember { mutableStateOf(if (initialCommitment > 0) initialCommitment.toInt().toString() else "") }
|
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 startMonth by remember { mutableIntStateOf(initialStartMonth) }
|
||||||
var startYear by remember { mutableIntStateOf(initialStartYear) }
|
var startYear by remember { mutableIntStateOf(initialStartYear) }
|
||||||
|
|
||||||
@@ -457,6 +316,20 @@ fun UpdatePersonDialog(
|
|||||||
fontWeight = FontWeight.Bold
|
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(
|
OutlinedTextField(
|
||||||
value = name,
|
value = name,
|
||||||
onValueChange = { name = it },
|
onValueChange = { name = it },
|
||||||
@@ -608,12 +481,12 @@ fun UpdatePersonDialog(
|
|||||||
onClick = { expandedMonth = true },
|
onClick = { expandedMonth = true },
|
||||||
modifier = Modifier.fillMaxWidth()
|
modifier = Modifier.fillMaxWidth()
|
||||||
) {
|
) {
|
||||||
Text(com.oqba26.monthlypaymentapp.utils.getPersianMonthName(startMonth))
|
Text(getPersianMonthName(startMonth))
|
||||||
}
|
}
|
||||||
DropdownMenu(expanded = expandedMonth, onDismissRequest = { expandedMonth = false }) {
|
DropdownMenu(expanded = expandedMonth, onDismissRequest = { expandedMonth = false }) {
|
||||||
(1..12).forEach { m ->
|
(1..12).forEach { m ->
|
||||||
DropdownMenuItem(
|
DropdownMenuItem(
|
||||||
text = { Text(com.oqba26.monthlypaymentapp.utils.getPersianMonthName(m)) },
|
text = { Text(getPersianMonthName(m)) },
|
||||||
onClick = { startMonth = m; expandedMonth = false }
|
onClick = { startMonth = m; expandedMonth = false }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -631,7 +504,7 @@ fun UpdatePersonDialog(
|
|||||||
onClick = {
|
onClick = {
|
||||||
if (name.isNotBlank()) {
|
if (name.isNotBlank()) {
|
||||||
val commitment = monthlyCommitment.toDoubleOrNull() ?: 0.0
|
val commitment = monthlyCommitment.toDoubleOrNull() ?: 0.0
|
||||||
onConfirm(name, phoneNumber, commitment, startMonth, startYear)
|
onConfirm(name, phoneNumber, commitment, startMonth, startYear, isAnonymous)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
enabled = name.isNotBlank()
|
enabled = name.isNotBlank()
|
||||||
@@ -648,6 +521,47 @@ fun UpdatePersonDialog(
|
|||||||
Text("لغو")
|
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("حذف")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -685,7 +599,6 @@ fun YearSelector(year: Int, onYearChange: (Int) -> Unit) {
|
|||||||
@Composable
|
@Composable
|
||||||
fun MonthListItem(
|
fun MonthListItem(
|
||||||
month: MonthUiModel,
|
month: MonthUiModel,
|
||||||
personName: String,
|
|
||||||
startMonth: Int,
|
startMonth: Int,
|
||||||
onHeaderClick: () -> Unit,
|
onHeaderClick: () -> Unit,
|
||||||
onEditClick: () -> Unit
|
onEditClick: () -> Unit
|
||||||
|
|||||||
@@ -83,6 +83,11 @@ import androidx.hilt.navigation.compose.hiltViewModel
|
|||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
import com.oqba26.monthlypaymentapp.utils.PersianDigitsTransformation
|
import com.oqba26.monthlypaymentapp.utils.PersianDigitsTransformation
|
||||||
import com.oqba26.monthlypaymentapp.utils.formatNumberAsPersian
|
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.utils.toPersianDigits
|
||||||
import com.oqba26.monthlypaymentapp.viewmodel.ContactMatch
|
import com.oqba26.monthlypaymentapp.viewmodel.ContactMatch
|
||||||
import com.oqba26.monthlypaymentapp.viewmodel.ContactSuggestion
|
import com.oqba26.monthlypaymentapp.viewmodel.ContactSuggestion
|
||||||
@@ -129,6 +134,7 @@ fun PersonScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
var personToArchive by remember { mutableStateOf<PersonUiModel?>(null) }
|
var personToArchive by remember { mutableStateOf<PersonUiModel?>(null) }
|
||||||
|
var personForBulkPayment by remember { mutableStateOf<PersonUiModel?>(null) }
|
||||||
val showBulkSelection = contactState.showBulkSmsDialog
|
val showBulkSelection = contactState.showBulkSmsDialog
|
||||||
|
|
||||||
val reorderableState = rememberReorderableLazyListState(
|
val reorderableState = rememberReorderableLazyListState(
|
||||||
@@ -195,10 +201,10 @@ fun PersonScreen(
|
|||||||
if (currentCategory == "mosque" && uiState.unpaidPersons.isNotEmpty()) {
|
if (currentCategory == "mosque" && uiState.unpaidPersons.isNotEmpty()) {
|
||||||
item {
|
item {
|
||||||
Text(
|
Text(
|
||||||
text = "لیست خیرین (بدهکار)",
|
text = "لیست خیرین",
|
||||||
modifier = Modifier.padding(16.dp),
|
modifier = Modifier.padding(16.dp),
|
||||||
style = MaterialTheme.typography.titleMedium,
|
style = MaterialTheme.typography.titleMedium,
|
||||||
color = MaterialTheme.colorScheme.error
|
color = MaterialTheme.colorScheme.primary
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -232,6 +238,9 @@ fun PersonScreen(
|
|||||||
},
|
},
|
||||||
onSmsClick = { selectedPerson ->
|
onSmsClick = { selectedPerson ->
|
||||||
contactViewModel.onSmsClicked(selectedPerson)
|
contactViewModel.onSmsClicked(selectedPerson)
|
||||||
|
},
|
||||||
|
onDebtClick = { selectedPerson ->
|
||||||
|
personForBulkPayment = selectedPerson
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -271,6 +280,9 @@ fun PersonScreen(
|
|||||||
},
|
},
|
||||||
onSmsClick = { selectedPerson ->
|
onSmsClick = { selectedPerson ->
|
||||||
contactViewModel.onSmsClicked(selectedPerson)
|
contactViewModel.onSmsClicked(selectedPerson)
|
||||||
|
},
|
||||||
|
onDebtClick = { selectedPerson ->
|
||||||
|
personForBulkPayment = selectedPerson
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -289,9 +301,24 @@ fun PersonScreen(
|
|||||||
|
|
||||||
// Dialogs
|
// Dialogs
|
||||||
personForPaymentDialog?.let { person ->
|
personForPaymentDialog?.let { person ->
|
||||||
|
val currentDay = getCurrentShamsiDay()
|
||||||
|
val currentMonth = getCurrentShamsiMonth()
|
||||||
|
val targetMonth = if (currentDay >= 20) currentMonth else if (currentMonth == 1) 12 else currentMonth - 1
|
||||||
|
|
||||||
|
val monthName = getPersianMonthName(targetMonth)
|
||||||
|
val currentDateTime = formatTimestampToPersianDateTime(System.currentTimeMillis())
|
||||||
|
|
||||||
|
val isCurrentMonth = targetMonth == currentMonth
|
||||||
|
val initialDesc = if (isCurrentMonth) {
|
||||||
|
"پرداخت برای ماه جاری در تاریخ $currentDateTime ثبت شد."
|
||||||
|
} else {
|
||||||
|
"پرداخت برای $monthName در تاریخ $currentDateTime ثبت شد."
|
||||||
|
}
|
||||||
|
|
||||||
AddPaymentDialog(
|
AddPaymentDialog(
|
||||||
personName = person.name,
|
personName = person.name,
|
||||||
defaultAmount = if (person.monthlyCommitment > 0) person.monthlyCommitment else defaultPaymentAmount,
|
defaultAmount = if (person.monthlyCommitment > 0) person.monthlyCommitment else defaultPaymentAmount,
|
||||||
|
initialDescription = initialDesc,
|
||||||
onConfirm = { amount, description ->
|
onConfirm = { amount, description ->
|
||||||
viewModel.onEvent(PersonScreenEvent.AddQuickPayment(person.id, amount, description))
|
viewModel.onEvent(PersonScreenEvent.AddQuickPayment(person.id, amount, description))
|
||||||
viewModel.onDismissPaymentDialog()
|
viewModel.onDismissPaymentDialog()
|
||||||
@@ -348,6 +375,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) {
|
if (showBulkSelection) {
|
||||||
val debtors = (uiState.unpaidPersons + uiState.paidPersons).filter { it.debtCount > 0 }
|
val debtors = (uiState.unpaidPersons + uiState.paidPersons).filter { it.debtCount > 0 }
|
||||||
BulkSmsSelectionDialog(
|
BulkSmsSelectionDialog(
|
||||||
@@ -511,8 +557,8 @@ fun AddNewPersonDialog(
|
|||||||
var monthlyCommitment by remember { mutableStateOf("") }
|
var monthlyCommitment by remember { mutableStateOf("") }
|
||||||
var initialPaymentAmount by remember { mutableStateOf("") }
|
var initialPaymentAmount by remember { mutableStateOf("") }
|
||||||
|
|
||||||
val currentShamsiYear = com.oqba26.monthlypaymentapp.utils.getCurrentShamsiYear()
|
val currentShamsiYear = getCurrentShamsiYear()
|
||||||
val currentShamsiMonth = com.oqba26.monthlypaymentapp.utils.getCurrentShamsiMonth()
|
val currentShamsiMonth = getCurrentShamsiMonth()
|
||||||
|
|
||||||
var startMonth by remember { mutableIntStateOf(currentShamsiMonth) }
|
var startMonth by remember { mutableIntStateOf(currentShamsiMonth) }
|
||||||
var startYear by remember { mutableIntStateOf(currentShamsiYear) }
|
var startYear by remember { mutableIntStateOf(currentShamsiYear) }
|
||||||
@@ -755,14 +801,14 @@ fun AddNewPersonDialog(
|
|||||||
onClick = { expandedMonth = true },
|
onClick = { expandedMonth = true },
|
||||||
modifier = Modifier.fillMaxWidth()
|
modifier = Modifier.fillMaxWidth()
|
||||||
) {
|
) {
|
||||||
Text(com.oqba26.monthlypaymentapp.utils.getPersianMonthName(startMonth))
|
Text(getPersianMonthName(startMonth))
|
||||||
}
|
}
|
||||||
DropdownMenu(
|
DropdownMenu(
|
||||||
expanded = expandedMonth,
|
expanded = expandedMonth,
|
||||||
onDismissRequest = { expandedMonth = false }) {
|
onDismissRequest = { expandedMonth = false }) {
|
||||||
(1..12).forEach { m ->
|
(1..12).forEach { m ->
|
||||||
DropdownMenuItem(
|
DropdownMenuItem(
|
||||||
text = { Text(com.oqba26.monthlypaymentapp.utils.getPersianMonthName(m)) },
|
text = { Text(getPersianMonthName(m)) },
|
||||||
onClick = { startMonth = m; expandedMonth = false }
|
onClick = { startMonth = m; expandedMonth = false }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -1058,7 +1104,8 @@ fun PersonListItem(
|
|||||||
onLongClick: () -> Unit = {},
|
onLongClick: () -> Unit = {},
|
||||||
onQuickPayClick: (PersonUiModel) -> Unit,
|
onQuickPayClick: (PersonUiModel) -> Unit,
|
||||||
onArchiveClick: (PersonUiModel) -> Unit,
|
onArchiveClick: (PersonUiModel) -> Unit,
|
||||||
onSmsClick: (PersonUiModel) -> Unit
|
onSmsClick: (PersonUiModel) -> Unit,
|
||||||
|
onDebtClick: (PersonUiModel) -> Unit
|
||||||
) {
|
) {
|
||||||
Card(
|
Card(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
@@ -1107,7 +1154,8 @@ fun PersonListItem(
|
|||||||
if (person.debtCount > 0) {
|
if (person.debtCount > 0) {
|
||||||
Surface(
|
Surface(
|
||||||
color = MaterialTheme.colorScheme.errorContainer,
|
color = MaterialTheme.colorScheme.errorContainer,
|
||||||
shape = MaterialTheme.shapes.small
|
shape = MaterialTheme.shapes.small,
|
||||||
|
modifier = Modifier.clickable { onDebtClick(person) }
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = "${person.debtCount.toString().toPersianDigits()} بدهی",
|
text = "${person.debtCount.toString().toPersianDigits()} بدهی",
|
||||||
|
|||||||
@@ -2,26 +2,33 @@
|
|||||||
|
|
||||||
package com.oqba26.monthlypaymentapp.ui.screens
|
package com.oqba26.monthlypaymentapp.ui.screens
|
||||||
|
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.heightIn
|
||||||
import androidx.compose.foundation.layout.padding
|
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.shape.RoundedCornerShape
|
||||||
import androidx.compose.foundation.text.KeyboardOptions
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
import androidx.compose.material.icons.filled.Delete
|
import androidx.compose.material.icons.filled.Delete
|
||||||
import androidx.compose.material3.Button
|
import androidx.compose.material3.Button
|
||||||
import androidx.compose.material3.ButtonDefaults
|
import androidx.compose.material3.ButtonDefaults
|
||||||
import androidx.compose.material3.Card
|
import androidx.compose.material3.Card
|
||||||
|
import androidx.compose.material3.Checkbox
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.OutlinedTextField
|
import androidx.compose.material3.OutlinedTextField
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.CompositionLocalProvider
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateListOf
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
@@ -37,6 +44,7 @@ import com.oqba26.monthlypaymentapp.R
|
|||||||
import androidx.compose.ui.window.Dialog
|
import androidx.compose.ui.window.Dialog
|
||||||
import com.oqba26.monthlypaymentapp.data.model.PaymentRecord
|
import com.oqba26.monthlypaymentapp.data.model.PaymentRecord
|
||||||
import com.oqba26.monthlypaymentapp.utils.PersianNumberVisualTransformation
|
import com.oqba26.monthlypaymentapp.utils.PersianNumberVisualTransformation
|
||||||
|
import com.oqba26.monthlypaymentapp.utils.getPersianMonthName
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun EditPaymentDialog(
|
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.LocalPersonRepository
|
||||||
import com.oqba26.monthlypaymentapp.data.repository.NetworkRepository
|
import com.oqba26.monthlypaymentapp.data.repository.NetworkRepository
|
||||||
import com.oqba26.monthlypaymentapp.data.repository.SettingsRepository
|
import com.oqba26.monthlypaymentapp.data.repository.SettingsRepository
|
||||||
|
import com.oqba26.monthlypaymentapp.utils.formatTimestampToPersianDateTime
|
||||||
import com.oqba26.monthlypaymentapp.utils.getCurrentShamsiDay
|
import com.oqba26.monthlypaymentapp.utils.getCurrentShamsiDay
|
||||||
import com.oqba26.monthlypaymentapp.utils.getCurrentShamsiMonth
|
import com.oqba26.monthlypaymentapp.utils.getCurrentShamsiMonth
|
||||||
import com.oqba26.monthlypaymentapp.utils.getCurrentShamsiYear
|
import com.oqba26.monthlypaymentapp.utils.getCurrentShamsiYear
|
||||||
@@ -188,18 +189,8 @@ class PersonViewModel @Inject constructor(
|
|||||||
val uiModels = activePersons.map { person ->
|
val uiModels = activePersons.map { person ->
|
||||||
val hasPaid = relevantPayments.any { it.personId == person.id }
|
val hasPaid = relevantPayments.any { it.personId == person.id }
|
||||||
|
|
||||||
val (debtCount, totalDebtAmount, unpaidMonths) = if (category == "mosque") {
|
val unpaidResult = if (category == "mosque" || person.isAnonymous) {
|
||||||
if (person.monthlyCommitment > 0) {
|
Triple(0, 0.0, emptyList<String>() to emptyList())
|
||||||
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) })
|
|
||||||
} else {
|
|
||||||
Triple(0, 0.0, emptyList())
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
// Salary logic
|
// Salary logic
|
||||||
val debtEndMonth = if (currentDay >= 20) currentMonth else currentMonth - 1
|
val debtEndMonth = if (currentDay >= 20) currentMonth else currentMonth - 1
|
||||||
@@ -209,9 +200,12 @@ class PersonViewModel @Inject constructor(
|
|||||||
allPayments.none { it.personId == person.id && it.shamsiYear == currentYear && it.shamsiMonth == m && it.category == category }
|
allPayments.none { it.personId == person.id && it.shamsiYear == currentYear && it.shamsiMonth == m && it.category == category }
|
||||||
}
|
}
|
||||||
val defaultAmount = settingsRepository.defaultPaymentAmountFlow.first()
|
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") {
|
val displayName = if (person.isAnonymous && category == "mosque") {
|
||||||
"خیر ناشناس (${person.name})"
|
"خیر ناشناس (${person.name})"
|
||||||
} else {
|
} else {
|
||||||
@@ -229,7 +223,8 @@ class PersonViewModel @Inject constructor(
|
|||||||
phoneNumber = person.phoneNumber,
|
phoneNumber = person.phoneNumber,
|
||||||
isAnonymous = person.isAnonymous,
|
isAnonymous = person.isAnonymous,
|
||||||
monthlyCommitment = person.monthlyCommitment,
|
monthlyCommitment = person.monthlyCommitment,
|
||||||
unpaidMonthsNames = unpaidMonths,
|
unpaidMonthsNames = unpaidMonthsNames,
|
||||||
|
unpaidMonths = unpaidMonths,
|
||||||
needsSync = person.needsSync
|
needsSync = person.needsSync
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -369,7 +364,8 @@ class PersonViewModel @Inject constructor(
|
|||||||
|
|
||||||
is PersonScreenEvent.UpdatePerson -> {
|
is PersonScreenEvent.UpdatePerson -> {
|
||||||
val persons = localPersonRepository.getAllPersonsFlow().first()
|
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) {
|
if (isDuplicate) {
|
||||||
_toastMessage.emit("خطا: این نام از قبل وجود دارد.")
|
_toastMessage.emit("خطا: این نام از قبل وجود دارد.")
|
||||||
@@ -377,13 +373,15 @@ class PersonViewModel @Inject constructor(
|
|||||||
val currentPerson = persons.find { it.id == event.personId }
|
val currentPerson = persons.find { it.id == event.personId }
|
||||||
if (currentPerson != null) {
|
if (currentPerson != null) {
|
||||||
val updatedPerson = currentPerson.copy(
|
val updatedPerson = currentPerson.copy(
|
||||||
name = event.name,
|
name = finalName,
|
||||||
phoneNumber = event.phoneNumber,
|
phoneNumber = event.phoneNumber,
|
||||||
monthlyCommitment = event.monthlyCommitment,
|
monthlyCommitment = if (event.isAnonymous) 0.0 else event.monthlyCommitment,
|
||||||
startMonth = event.startMonth,
|
startMonth = event.startMonth,
|
||||||
startYear = event.startYear
|
startYear = event.startYear,
|
||||||
|
isAnonymous = event.isAnonymous
|
||||||
)
|
)
|
||||||
localPersonRepository.updatePersonLocally(updatedPerson)
|
localPersonRepository.updatePersonLocally(updatedPerson)
|
||||||
|
_toastMessage.emit("اطلاعات شخص با موفقیت بروز شد.")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -440,13 +438,25 @@ class PersonViewModel @Inject constructor(
|
|||||||
try {
|
try {
|
||||||
val person = localPersonRepository.getAllPersonsFlow().first().find { it.id == event.personId }
|
val person = localPersonRepository.getAllPersonsFlow().first().find { it.id == event.personId }
|
||||||
val category = person?.category ?: _currentCategory.value
|
val category = person?.category ?: _currentCategory.value
|
||||||
|
val currentMonth = getCurrentShamsiMonth()
|
||||||
|
val currentYear = getCurrentShamsiYear()
|
||||||
|
val currentDateTime = formatTimestampToPersianDateTime(System.currentTimeMillis())
|
||||||
|
|
||||||
event.months.forEach { month ->
|
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(
|
val record = createPaymentRecord(
|
||||||
personId = event.personId,
|
personId = event.personId,
|
||||||
month = month,
|
month = month,
|
||||||
year = event.year,
|
year = event.year,
|
||||||
amount = event.amount,
|
amount = event.amount,
|
||||||
description = "پرداخت دسته جمعی"
|
description = description
|
||||||
).copy(category = category)
|
).copy(category = category)
|
||||||
localPersonRepository.insertPaymentLocally(record)
|
localPersonRepository.insertPaymentLocally(record)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ data class PersonUiModel(
|
|||||||
val isAnonymous: Boolean = false,
|
val isAnonymous: Boolean = false,
|
||||||
val monthlyCommitment: Double = 0.0,
|
val monthlyCommitment: Double = 0.0,
|
||||||
val unpaidMonthsNames: List<String> = emptyList(),
|
val unpaidMonthsNames: List<String> = emptyList(),
|
||||||
|
val unpaidMonths: List<Int> = emptyList(),
|
||||||
val needsSync: Boolean = false,
|
val needsSync: Boolean = false,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -83,7 +84,8 @@ sealed class PersonScreenEvent {
|
|||||||
val phoneNumber: String?,
|
val phoneNumber: String?,
|
||||||
val monthlyCommitment: Double = 0.0,
|
val monthlyCommitment: Double = 0.0,
|
||||||
val startMonth: Int = 1,
|
val startMonth: Int = 1,
|
||||||
val startYear: Int = 1403
|
val startYear: Int = 1403,
|
||||||
|
val isAnonymous: Boolean = false
|
||||||
) : PersonScreenEvent()
|
) : PersonScreenEvent()
|
||||||
data class DeletePerson(val personId: String) : PersonScreenEvent()
|
data class DeletePerson(val personId: String) : PersonScreenEvent()
|
||||||
data class ArchivePerson(val personId: String) : PersonScreenEvent()
|
data class ArchivePerson(val personId: String) : PersonScreenEvent()
|
||||||
|
|||||||
+5
-5
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"versionCode": 3,
|
"versionCode": 7,
|
||||||
"versionName": "1.0.2",
|
"versionName": "1.0.6",
|
||||||
"url": "https://github.com/oqba26/MonthlyPaymentApp/releases/download/v1.0.2/MonthlyPaymentApp-v1.0.2.apk",
|
"url": "https://github.com/oqba26/MonthlyPaymentApp/releases/download/v1.0.6/MonthlyPaymentApp-v1.0.6.apk",
|
||||||
"releaseNotes": "بهروزرسانی به نسخه ۱.۰.۲:\n- رفع مشکل چرخه بیانتهای آپدیت.\n- هماهنگسازی نسخههای بیلد و فایل تنظیمات سرور.",
|
"releaseNotes": "بهینهسازی کدهای UI، رفع هشدارهای کامپایلر در صفحات شخص و جزئیات شخص، و بهبود پایداری برنامه",
|
||||||
"isForceUpdate": true
|
"isForceUpdate": false
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user