Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1931e20deb | ||
|
|
ee0bd0ba2b | ||
|
|
7d865a3d6d | ||
|
|
fcf4d02caf | ||
|
|
6166a2f249 | ||
|
|
6213bbf30f |
No files matched your search
@@ -46,3 +46,30 @@ jobs:
|
||||
files: app/build/outputs/apk/release/MonthlyPaymentApp-${{ github.ref_name }}.apk
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Update update.json after release
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
TAG_NAME="${{ github.ref_name }}"
|
||||
VERSION_NAME="${TAG_NAME#v}"
|
||||
VERSION_CODE=$(grep 'versionCode' app/build.gradle.kts | head -n 1 | awk '{print $3}')
|
||||
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
git fetch origin main
|
||||
git checkout main
|
||||
|
||||
cat <<EOF > update.json
|
||||
{
|
||||
"versionCode": ${VERSION_CODE},
|
||||
"versionName": "${VERSION_NAME}",
|
||||
"url": "https://github.com/oqba26/MonthlyPaymentApp/releases/download/${TAG_NAME}/MonthlyPaymentApp-${TAG_NAME}.apk",
|
||||
"releaseNotes": "بهبودهای پایداری و عملکرد همگامسازی",
|
||||
"isForceUpdate": false
|
||||
}
|
||||
EOF
|
||||
|
||||
git add update.json
|
||||
git commit -m "auto-update update.json for release ${TAG_NAME}" || true
|
||||
git push origin main
|
||||
@@ -30,8 +30,8 @@ android {
|
||||
applicationId = "com.oqba26.monthlypaymentapp"
|
||||
minSdk = 25
|
||||
targetSdk = 34
|
||||
versionCode = 11
|
||||
versionName = "1.1.0"
|
||||
versionCode = 15
|
||||
versionName = "1.1.4"
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
vectorDrawables { useSupportLibrary = true }
|
||||
}
|
||||
@@ -76,7 +76,13 @@ android {
|
||||
targetCompatibility = JavaVersion.VERSION_21
|
||||
isCoreLibraryDesugaringEnabled = true
|
||||
}
|
||||
kotlinOptions { jvmTarget = "21" }
|
||||
kotlinOptions {
|
||||
jvmTarget = "21"
|
||||
freeCompilerArgs += listOf(
|
||||
"-opt-in=kotlinx.serialization.InternalSerializationApi",
|
||||
"-opt-in=kotlinx.serialization.ExperimentalSerializationApi"
|
||||
)
|
||||
}
|
||||
kotlin { jvmToolchain(21) }
|
||||
|
||||
packaging {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
@file:OptIn(InternalSerializationApi::class)
|
||||
|
||||
package com.oqba26.monthlypaymentapp.data.remote
|
||||
|
||||
import android.util.Log
|
||||
@@ -5,6 +7,7 @@ import io.ktor.client.HttpClient
|
||||
import io.ktor.client.engine.okhttp.OkHttp
|
||||
import io.ktor.client.plugins.HttpTimeout
|
||||
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
|
||||
import io.ktor.client.plugins.timeout
|
||||
import io.ktor.client.request.delete
|
||||
import io.ktor.client.request.get
|
||||
import io.ktor.client.request.header
|
||||
@@ -25,6 +28,7 @@ import io.ktor.utils.io.readUTF8Line
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.serialization.InternalSerializationApi
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
@@ -347,6 +351,10 @@ class PocketBaseClient(
|
||||
authToken?.let { token ->
|
||||
header(HttpHeaders.Authorization, "Bearer $token")
|
||||
}
|
||||
timeout {
|
||||
requestTimeoutMillis = HttpTimeout.INFINITE_TIMEOUT_MS
|
||||
socketTimeoutMillis = HttpTimeout.INFINITE_TIMEOUT_MS
|
||||
}
|
||||
}.execute { response ->
|
||||
val channel: ByteReadChannel = response.bodyAsChannel()
|
||||
var currentEvent = ""
|
||||
@@ -362,6 +370,7 @@ class PocketBaseClient(
|
||||
val connectObj = json.decodeFromString<PocketBaseConnectEvent>(dataJson)
|
||||
subscribeRealtime(cleanBaseUrl, connectObj.clientId, subscriptions)
|
||||
Log.d(TAG, "اتصال ریلتایم برقرار شد. ClientId: ${connectObj.clientId}")
|
||||
emit("PB_CONNECT")
|
||||
} else if (currentEvent.isNotEmpty()) {
|
||||
emit(currentEvent)
|
||||
}
|
||||
|
||||
+15
-37
@@ -14,9 +14,10 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import java.util.UUID
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
|
||||
class NetworkRepository(
|
||||
private val settingsRepository: SettingsRepository? = null
|
||||
settingsRepository: SettingsRepository? = null
|
||||
) {
|
||||
|
||||
private val pocketBase = ApiClient.pocketBase
|
||||
@@ -43,13 +44,16 @@ class NetworkRepository(
|
||||
// --- Realtime Operations ---
|
||||
fun observeRealtimeChanges() {
|
||||
repositoryScope.launch {
|
||||
try {
|
||||
pocketBase.listenRealtime(listOf("persons", "payments")).collect { event ->
|
||||
Log.d("PocketBase-Realtime", "Change detected in PocketBase: $event")
|
||||
refresh()
|
||||
while (true) {
|
||||
try {
|
||||
pocketBase.listenRealtime(listOf("persons", "payments")).collect { event ->
|
||||
Log.d("PocketBase-Realtime", "Change detected in PocketBase: $event")
|
||||
refresh()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e("PocketBase-Realtime", "Realtime connection error, retrying...", e)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e("PocketBase-Realtime", "Realtime connection error, retrying...", e)
|
||||
delay(3000.milliseconds)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -152,31 +156,7 @@ class NetworkRepository(
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun updatePersonArchivedStatus(personId: String, isArchived: Boolean): Boolean {
|
||||
return try {
|
||||
val currentPerson = _personsFlow.value.find { it.id == personId }
|
||||
if (currentPerson != null) {
|
||||
pocketBase.upsertRecord("persons", "id='$personId'", currentPerson.copy(isArchived = isArchived))
|
||||
}
|
||||
true
|
||||
} catch (e: Exception) {
|
||||
Log.e("PocketBase", "updateArchived error", e)
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun updatePersonDisplayOrder(personId: String, displayOrder: Long): Boolean {
|
||||
return try {
|
||||
val currentPerson = _personsFlow.value.find { it.id == personId }
|
||||
if (currentPerson != null) {
|
||||
pocketBase.upsertRecord("persons", "id='$personId'", currentPerson.copy(displayOrder = displayOrder))
|
||||
}
|
||||
true
|
||||
} catch (e: Exception) {
|
||||
Log.e("PocketBase", "updateOrder error", e)
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fun getPersonsFlow(): Flow<List<Person>> = _personsFlow.asStateFlow()
|
||||
|
||||
@@ -192,19 +172,17 @@ class NetworkRepository(
|
||||
}
|
||||
|
||||
suspend fun deletePayment(paymentId: String): Boolean {
|
||||
var retries = 0
|
||||
val maxRetries = 2
|
||||
|
||||
while (retries <= maxRetries) {
|
||||
for (retries in 1..maxRetries) {
|
||||
try {
|
||||
pocketBase.deleteRecordByFilter("payments", "id='$paymentId'")
|
||||
Log.d("PocketBase", "deletePayment Success: $paymentId")
|
||||
return true
|
||||
} catch (e: Exception) {
|
||||
retries++
|
||||
Log.e("PocketBase", "deletePayment error (Attempt $retries): ${e.message}", e)
|
||||
if (retries > maxRetries) return false
|
||||
delay(1000L * retries)
|
||||
if (retries < maxRetries) {
|
||||
delay((1000L * retries).milliseconds)
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
|
||||
@@ -113,6 +113,7 @@ fun PaidListScreen(
|
||||
PersonListItem(
|
||||
person = person,
|
||||
index = index + 1,
|
||||
listType = PersonListType.PAID,
|
||||
onPersonClick = { navController.navigate("person_detail/${person.id}") },
|
||||
onQuickPayClick = { },
|
||||
onArchiveClick = { personToArchive.value = it },
|
||||
|
||||
@@ -30,7 +30,6 @@ import androidx.compose.material.ExperimentalMaterialApi
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.Message
|
||||
import androidx.compose.material.icons.filled.Archive
|
||||
import androidx.compose.material.icons.filled.Assessment
|
||||
import androidx.compose.material.icons.filled.Check
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material.icons.filled.Sync
|
||||
@@ -117,7 +116,6 @@ fun PersonScreen(
|
||||
val dashboardData by viewModel.dashboardData.collectAsState()
|
||||
val financialReportData by viewModel.financialReportData.collectAsState()
|
||||
val showFinancialReportDialog by viewModel.showFinancialReportDialog.collectAsState()
|
||||
val isOperatorMode by viewModel.isOperatorMode.collectAsState()
|
||||
val defaultPaymentAmount by viewModel.defaultPaymentAmountFlow.collectAsState(initial = 0.0)
|
||||
val cardNumbers by viewModel.cardNumbersFlow.collectAsState(initial = emptyList())
|
||||
val personForPaymentDialog by viewModel.personForPaymentDialog.collectAsState()
|
||||
@@ -188,8 +186,7 @@ fun PersonScreen(
|
||||
DashboardCard(
|
||||
data = dashboardData,
|
||||
financialReportData = financialReportData,
|
||||
isOperatorMode = isOperatorMode,
|
||||
onOpenReportClick = { viewModel.onShowFinancialReport() }
|
||||
isMosqueCategory = (currentCategory == "mosque")
|
||||
)
|
||||
|
||||
OutlinedTextField(
|
||||
@@ -582,7 +579,7 @@ fun AddNewPersonDialog(
|
||||
val currentShamsiYear = getCurrentShamsiYear()
|
||||
val currentShamsiMonth = getCurrentShamsiMonth()
|
||||
|
||||
var startMonth by remember { mutableIntStateOf(currentShamsiMonth) }
|
||||
var startMonth by remember { mutableIntStateOf(if (isMosqueCategory) currentShamsiMonth else 1) }
|
||||
var startYear by remember { mutableIntStateOf(currentShamsiYear) }
|
||||
|
||||
var similarContacts by remember { mutableStateOf<List<ContactMatch>>(emptyList()) }
|
||||
@@ -1072,8 +1069,7 @@ fun SelectCardDialog(
|
||||
fun DashboardCard(
|
||||
data: DashboardUiModel,
|
||||
financialReportData: FinancialReportData,
|
||||
isOperatorMode: Boolean = false,
|
||||
onOpenReportClick: () -> Unit
|
||||
isMosqueCategory: Boolean = false
|
||||
) {
|
||||
val animatedProgress by animateFloatAsState(
|
||||
targetValue = data.progress,
|
||||
@@ -1110,10 +1106,27 @@ fun DashboardCard(
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text("مجموع درآمد ماه جاری", style = MaterialTheme.typography.titleSmall, fontWeight = FontWeight.Bold)
|
||||
Text(
|
||||
text = if (isMosqueCategory) "کمکهای جمعآوری شده ماه جاری" else "حقوق جمعآوری شده ماه جاری",
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
Text("${formatNumberAsPersian(data.totalIncome)} تومان", style = MaterialTheme.typography.bodyMedium, fontWeight = FontWeight.Bold)
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = if (isMosqueCategory) "مجموع کمکهای جمعآوری شده دوره جاری" else "مجموع حقوق جمعآوری شده دوره جاری",
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
Text("${formatNumberAsPersian(data.totalCycleIncome)} تومان", style = MaterialTheme.typography.bodyMedium, fontWeight = FontWeight.Bold)
|
||||
}
|
||||
|
||||
if (financialReportData.totalOverdueDebt > 0 || financialReportData.debtors.isNotEmpty()) {
|
||||
HorizontalDivider(modifier = Modifier.padding(vertical = 2.dp))
|
||||
|
||||
@@ -1136,24 +1149,6 @@ fun DashboardCard(
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (!isOperatorMode) {
|
||||
OutlinedButton(
|
||||
onClick = onOpenReportClick,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 2.dp),
|
||||
shape = MaterialTheme.shapes.medium
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Assessment,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(18.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(6.dp))
|
||||
Text("مشاهده گزارش کامل مالی و درآمدهای گذشته", style = MaterialTheme.typography.labelLarge)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1337,6 +1332,7 @@ fun PersonListItem(
|
||||
person: PersonUiModel,
|
||||
index: Int,
|
||||
modifier: Modifier = Modifier,
|
||||
listType: PersonListType = PersonListType.UNPAID,
|
||||
isSelectionMode: Boolean = false,
|
||||
isSelected: Boolean = false,
|
||||
reorderableState: org.burnoutcrew.reorderable.ReorderableLazyListState? = null,
|
||||
@@ -1373,6 +1369,15 @@ fun PersonListItem(
|
||||
)
|
||||
}
|
||||
|
||||
if (listType == PersonListType.PAID && person.hasPaidThisMonth) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Check,
|
||||
contentDescription = "Paid",
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.size(20.dp).padding(end = 2.dp)
|
||||
)
|
||||
}
|
||||
|
||||
val dragModifier = if (reorderableState != null && !isSelectionMode) {
|
||||
Modifier.detectReorderAfterLongPress(reorderableState)
|
||||
} else Modifier
|
||||
@@ -1382,14 +1387,33 @@ fun PersonListItem(
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
modifier = dragModifier.padding(end = 4.dp)
|
||||
)
|
||||
Text(
|
||||
person.name,
|
||||
modifier = Modifier.weight(1f),
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
fontWeight = FontWeight.Bold,
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Visible
|
||||
)
|
||||
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
person.name,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
fontWeight = FontWeight.Bold,
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Visible
|
||||
)
|
||||
if (listType == PersonListType.PAID) {
|
||||
person.lastPaymentTime?.let { time ->
|
||||
Text(
|
||||
text = formatTimestampToPersianDateTime(time),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
person.lastPaymentAmount?.let { amount ->
|
||||
Text(
|
||||
text = "مبلغ: ${formatNumberAsPersian(amount)} تومان",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
fontWeight = FontWeight.Medium
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (person.debtCount > 0) {
|
||||
Surface(
|
||||
@@ -1425,29 +1449,31 @@ fun PersonListItem(
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(Modifier.width(4.dp))
|
||||
if (listType != PersonListType.PAID) {
|
||||
Spacer(Modifier.width(4.dp))
|
||||
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||
if (person.hasPaidThisMonth) {
|
||||
Icon(Icons.Filled.Check, contentDescription = "Paid", tint = MaterialTheme.colorScheme.primary)
|
||||
} else {
|
||||
IconButton(onClick = { onQuickPayClick(person) }, modifier = Modifier.size(32.dp)) {
|
||||
Icon(Icons.Filled.Payments, contentDescription = "Pay", tint = MaterialTheme.colorScheme.primary)
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||
if (person.hasPaidThisMonth) {
|
||||
Icon(Icons.Filled.Check, contentDescription = "Paid", tint = MaterialTheme.colorScheme.primary)
|
||||
} else {
|
||||
IconButton(onClick = { onQuickPayClick(person) }, modifier = Modifier.size(32.dp)) {
|
||||
Icon(Icons.Filled.Payments, contentDescription = "Pay", tint = MaterialTheme.colorScheme.primary)
|
||||
}
|
||||
}
|
||||
}
|
||||
IconButton(onClick = { onArchiveClick(person) }, modifier = Modifier.size(32.dp)) {
|
||||
Icon(Icons.Filled.Archive, contentDescription = "Archive", tint = MaterialTheme.colorScheme.secondary)
|
||||
}
|
||||
if (person.debtCount > 0) {
|
||||
IconButton(onClick = { onSmsClick(person) }, modifier = Modifier.size(32.dp)) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.Message,
|
||||
contentDescription = "SMS",
|
||||
tint = MaterialTheme.colorScheme.error,
|
||||
modifier = Modifier
|
||||
.size(20.dp)
|
||||
.graphicsLayer(scaleX = -1f)
|
||||
)
|
||||
IconButton(onClick = { onArchiveClick(person) }, modifier = Modifier.size(32.dp)) {
|
||||
Icon(Icons.Filled.Archive, contentDescription = "Archive", tint = MaterialTheme.colorScheme.secondary)
|
||||
}
|
||||
if (person.debtCount > 0) {
|
||||
IconButton(onClick = { onSmsClick(person) }, modifier = Modifier.size(32.dp)) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.Message,
|
||||
contentDescription = "SMS",
|
||||
tint = MaterialTheme.colorScheme.error,
|
||||
modifier = Modifier
|
||||
.size(20.dp)
|
||||
.graphicsLayer(scaleX = -1f)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,3 +30,36 @@ fun formatTimestampToPersianDateTime(timestamp: Long): String {
|
||||
val time = formatTimestampToPersianTime(timestamp)
|
||||
return "$date، ساعت $time"
|
||||
}
|
||||
|
||||
fun isTimestampInCurrentCycle(rawTimestamp: Long): Boolean {
|
||||
if (rawTimestamp <= 0L) return false
|
||||
|
||||
val timestamp = if (rawTimestamp < 100000000000L) rawTimestamp * 1000L else rawTimestamp
|
||||
|
||||
val now = PersianDate()
|
||||
val curYear = now.shYear
|
||||
val curMonth = now.shMonth
|
||||
val curDay = now.shDay
|
||||
|
||||
val (startYear, startMonth) = if (curDay >= 20) {
|
||||
curYear to curMonth
|
||||
} else {
|
||||
if (curMonth == 1) (curYear - 1) to 12 else curYear to (curMonth - 1)
|
||||
}
|
||||
|
||||
val (endYear, endMonth) = if (startMonth == 12) {
|
||||
(startYear + 1) to 1
|
||||
} else {
|
||||
startYear to (startMonth + 1)
|
||||
}
|
||||
|
||||
val recordDate = PersianDate(Date(timestamp))
|
||||
val rYear = recordDate.shYear
|
||||
val rMonth = recordDate.shMonth
|
||||
val rDay = recordDate.shDay
|
||||
|
||||
val isFirstHalf = (rYear == startYear && rMonth == startMonth && rDay >= 20)
|
||||
val isSecondHalf = (rYear == endYear && rMonth == endMonth && rDay < 20)
|
||||
|
||||
return isFirstHalf || isSecondHalf
|
||||
}
|
||||
@@ -14,6 +14,7 @@ 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.isTimestampInCurrentCycle
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
@@ -69,9 +70,6 @@ class PersonViewModel @Inject constructor(
|
||||
private val _infoMessage = MutableSharedFlow<String>()
|
||||
val infoMessage = _infoMessage.asSharedFlow()
|
||||
|
||||
val isOperatorMode: StateFlow<Boolean> = settingsRepository.isOperatorModeFlow
|
||||
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000L), false)
|
||||
|
||||
private val _uiState = MutableStateFlow(PersonListUiState())
|
||||
val uiState: StateFlow<PersonListUiState> = _uiState.asStateFlow()
|
||||
|
||||
@@ -200,12 +198,21 @@ class PersonViewModel @Inject constructor(
|
||||
it.shamsiMonth == targetMonth && it.shamsiYear == targetYear && it.category == category
|
||||
}
|
||||
|
||||
val categoryPayments = allPayments.filter { it.category == category }
|
||||
|
||||
val activePersons = persons
|
||||
.filter { !it.isArchived && it.category == category }
|
||||
.sortedWith(compareBy({ it.displayOrder }, { it.createdAt }))
|
||||
|
||||
val uiModels = activePersons.map { person ->
|
||||
val hasPaid = relevantPayments.any { it.personId == person.id }
|
||||
|
||||
val personCyclePayments = categoryPayments.filter {
|
||||
it.personId == person.id && isTimestampInCurrentCycle(if ((it.createdAt ?: 0L) > 0L) it.createdAt!! else it.timestamp)
|
||||
}
|
||||
val latestPayment = personCyclePayments.maxByOrNull { if ((it.createdAt ?: 0L) > 0L) it.createdAt!! else it.timestamp }
|
||||
val lastPaymentTime = latestPayment?.let { if ((it.createdAt ?: 0L) > 0L) it.createdAt!! else it.timestamp }
|
||||
val lastPaymentAmount = latestPayment?.amount
|
||||
|
||||
val unpaidResult = if (category == "mosque" || person.isAnonymous) {
|
||||
Triple(0, 0.0, emptyList<String>() to emptyList())
|
||||
@@ -244,17 +251,19 @@ class PersonViewModel @Inject constructor(
|
||||
monthlyCommitment = person.monthlyCommitment,
|
||||
unpaidMonthsNames = unpaidMonthsNames,
|
||||
unpaidMonths = unpaidMonths,
|
||||
needsSync = person.needsSync
|
||||
needsSync = person.needsSync,
|
||||
lastPaymentTime = lastPaymentTime,
|
||||
lastPaymentAmount = lastPaymentAmount
|
||||
)
|
||||
}
|
||||
|
||||
updateDashboard(uiModels.size, relevantPayments)
|
||||
updateDashboard(uiModels.size, relevantPayments, categoryPayments)
|
||||
|
||||
val (paidModels, unpaidModels) = uiModels.partition { it.hasPaidThisMonth }
|
||||
val (rawPaidModels, unpaidModels) = uiModels.partition { it.hasPaidThisMonth }
|
||||
val paidModels = rawPaidModels.sortedByDescending { it.lastPaymentTime ?: 0L }
|
||||
|
||||
// محاسبه گزارشات مالی جامع
|
||||
val personsMap = persons.associateBy { it.id }
|
||||
val categoryPayments = allPayments.filter { it.category == category }
|
||||
val totalAllTimeIncome = categoryPayments.sumOf { it.amount }
|
||||
|
||||
val debtorsList = unpaidModels.filter { it.debtCount > 0 }.map { personModel ->
|
||||
@@ -590,11 +599,20 @@ class PersonViewModel @Inject constructor(
|
||||
exportManager.shareReceipt(payment, personName)
|
||||
}
|
||||
|
||||
private fun updateDashboard(totalPersonCount: Int, paymentsThisMonth: List<PaymentRecord>) {
|
||||
private fun updateDashboard(
|
||||
totalPersonCount: Int,
|
||||
paymentsThisMonth: List<PaymentRecord>,
|
||||
categoryPayments: List<PaymentRecord>
|
||||
) {
|
||||
val paidCount = paymentsThisMonth.map { it.personId }.distinct().size
|
||||
val totalIncome = paymentsThisMonth.sumOf { it.amount }
|
||||
val cyclePayments = categoryPayments.filter { payment ->
|
||||
val time = if ((payment.createdAt ?: 0L) > 0L) payment.createdAt!! else payment.timestamp
|
||||
isTimestampInCurrentCycle(time)
|
||||
}
|
||||
val totalCycleIncome = cyclePayments.sumOf { it.amount }
|
||||
val progress = if (totalPersonCount > 0) paidCount.toFloat() / totalPersonCount.toFloat() else 0f
|
||||
_dashboardData.value = DashboardUiModel(paidCount, totalPersonCount, totalIncome, progress)
|
||||
_dashboardData.value = DashboardUiModel(paidCount, totalPersonCount, totalIncome, totalCycleIncome, progress)
|
||||
}
|
||||
|
||||
private fun createPaymentRecord(personId: String, month: Int, year: Int, amount: Double, description: String): PaymentRecord {
|
||||
|
||||
@@ -17,6 +17,8 @@ data class PersonUiModel(
|
||||
val unpaidMonthsNames: List<String> = emptyList(),
|
||||
val unpaidMonths: List<Int> = emptyList(),
|
||||
val needsSync: Boolean = false,
|
||||
val lastPaymentTime: Long? = null,
|
||||
val lastPaymentAmount: Double? = null
|
||||
)
|
||||
|
||||
data class PersonListUiState(
|
||||
@@ -143,6 +145,7 @@ data class DashboardUiModel(
|
||||
val paidCount: Int = 0,
|
||||
val totalCount: Int = 0,
|
||||
val totalIncome: Double = 0.0,
|
||||
val totalCycleIncome: Double = 0.0,
|
||||
val progress: Float = 0f
|
||||
)
|
||||
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@
|
||||
org.gradle.jvmargs=-Xmx2560m -XX:+UseParallelGC -Dfile.encoding=UTF-8
|
||||
|
||||
android.useAndroidX=true
|
||||
android.enableJetifier=true
|
||||
android.enableJetifier=false
|
||||
kotlin.code.style=official
|
||||
android.nonTransitiveRClass=true
|
||||
android.suppressUnsupportedCompileSdk=34
|
||||
|
||||
+4
-4
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"versionCode": 11,
|
||||
"versionName": "1.1.0",
|
||||
"url": "https://github.com/oqba26/MonthlyPaymentApp/releases/download/v1.1.0/MonthlyPaymentApp-v1.1.0.apk",
|
||||
"releaseNotes": "اصلاح اسکرول یکپارچه دیالوگ گزارش مالی و نمایش کامل تفکیک ماههای گذشته",
|
||||
"versionCode": 14,
|
||||
"versionName": "1.1.3",
|
||||
"url": "https://github.com/oqba26/MonthlyPaymentApp/releases/download/v1.1.3/MonthlyPaymentApp-v1.1.3.apk",
|
||||
"releaseNotes": "بهبودهای پایداری و عملکرد همگامسازی",
|
||||
"isForceUpdate": false
|
||||
}
|
||||
Reference in new issue
Block a user