Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7d865a3d6d | ||
|
|
fcf4d02caf | ||
|
|
6166a2f249 | ||
|
|
6213bbf30f | ||
|
|
2c52c2c538 | ||
|
|
a7620302e9 | ||
|
|
af0e7a76de |
No files matched your search
@@ -46,3 +46,30 @@ jobs:
|
|||||||
files: app/build/outputs/apk/release/MonthlyPaymentApp-${{ github.ref_name }}.apk
|
files: app/build/outputs/apk/release/MonthlyPaymentApp-${{ github.ref_name }}.apk
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
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"
|
applicationId = "com.oqba26.monthlypaymentapp"
|
||||||
minSdk = 25
|
minSdk = 25
|
||||||
targetSdk = 34
|
targetSdk = 34
|
||||||
versionCode = 9
|
versionCode = 14
|
||||||
versionName = "1.0.8"
|
versionName = "1.1.3"
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
vectorDrawables { useSupportLibrary = true }
|
vectorDrawables { useSupportLibrary = true }
|
||||||
}
|
}
|
||||||
@@ -76,7 +76,13 @@ android {
|
|||||||
targetCompatibility = JavaVersion.VERSION_21
|
targetCompatibility = JavaVersion.VERSION_21
|
||||||
isCoreLibraryDesugaringEnabled = true
|
isCoreLibraryDesugaringEnabled = true
|
||||||
}
|
}
|
||||||
kotlinOptions { jvmTarget = "21" }
|
kotlinOptions {
|
||||||
|
jvmTarget = "21"
|
||||||
|
freeCompilerArgs += listOf(
|
||||||
|
"-opt-in=kotlinx.serialization.InternalSerializationApi",
|
||||||
|
"-opt-in=kotlinx.serialization.ExperimentalSerializationApi"
|
||||||
|
)
|
||||||
|
}
|
||||||
kotlin { jvmToolchain(21) }
|
kotlin { jvmToolchain(21) }
|
||||||
|
|
||||||
packaging {
|
packaging {
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
@file:OptIn(InternalSerializationApi::class)
|
||||||
|
|
||||||
package com.oqba26.monthlypaymentapp.data.remote
|
package com.oqba26.monthlypaymentapp.data.remote
|
||||||
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
@@ -5,6 +7,7 @@ import io.ktor.client.HttpClient
|
|||||||
import io.ktor.client.engine.okhttp.OkHttp
|
import io.ktor.client.engine.okhttp.OkHttp
|
||||||
import io.ktor.client.plugins.HttpTimeout
|
import io.ktor.client.plugins.HttpTimeout
|
||||||
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
|
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
|
||||||
|
import io.ktor.client.plugins.timeout
|
||||||
import io.ktor.client.request.delete
|
import io.ktor.client.request.delete
|
||||||
import io.ktor.client.request.get
|
import io.ktor.client.request.get
|
||||||
import io.ktor.client.request.header
|
import io.ktor.client.request.header
|
||||||
@@ -25,6 +28,7 @@ import io.ktor.utils.io.readUTF8Line
|
|||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.flow
|
import kotlinx.coroutines.flow.flow
|
||||||
|
import kotlinx.serialization.InternalSerializationApi
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import kotlinx.serialization.json.JsonObject
|
import kotlinx.serialization.json.JsonObject
|
||||||
@@ -347,6 +351,10 @@ class PocketBaseClient(
|
|||||||
authToken?.let { token ->
|
authToken?.let { token ->
|
||||||
header(HttpHeaders.Authorization, "Bearer $token")
|
header(HttpHeaders.Authorization, "Bearer $token")
|
||||||
}
|
}
|
||||||
|
timeout {
|
||||||
|
requestTimeoutMillis = HttpTimeout.INFINITE_TIMEOUT_MS
|
||||||
|
socketTimeoutMillis = HttpTimeout.INFINITE_TIMEOUT_MS
|
||||||
|
}
|
||||||
}.execute { response ->
|
}.execute { response ->
|
||||||
val channel: ByteReadChannel = response.bodyAsChannel()
|
val channel: ByteReadChannel = response.bodyAsChannel()
|
||||||
var currentEvent = ""
|
var currentEvent = ""
|
||||||
@@ -362,6 +370,7 @@ class PocketBaseClient(
|
|||||||
val connectObj = json.decodeFromString<PocketBaseConnectEvent>(dataJson)
|
val connectObj = json.decodeFromString<PocketBaseConnectEvent>(dataJson)
|
||||||
subscribeRealtime(cleanBaseUrl, connectObj.clientId, subscriptions)
|
subscribeRealtime(cleanBaseUrl, connectObj.clientId, subscriptions)
|
||||||
Log.d(TAG, "اتصال ریلتایم برقرار شد. ClientId: ${connectObj.clientId}")
|
Log.d(TAG, "اتصال ریلتایم برقرار شد. ClientId: ${connectObj.clientId}")
|
||||||
|
emit("PB_CONNECT")
|
||||||
} else if (currentEvent.isNotEmpty()) {
|
} else if (currentEvent.isNotEmpty()) {
|
||||||
emit(currentEvent)
|
emit(currentEvent)
|
||||||
}
|
}
|
||||||
|
|||||||
+15
-37
@@ -14,9 +14,10 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
|||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
|
|
||||||
class NetworkRepository(
|
class NetworkRepository(
|
||||||
private val settingsRepository: SettingsRepository? = null
|
settingsRepository: SettingsRepository? = null
|
||||||
) {
|
) {
|
||||||
|
|
||||||
private val pocketBase = ApiClient.pocketBase
|
private val pocketBase = ApiClient.pocketBase
|
||||||
@@ -43,13 +44,16 @@ class NetworkRepository(
|
|||||||
// --- Realtime Operations ---
|
// --- Realtime Operations ---
|
||||||
fun observeRealtimeChanges() {
|
fun observeRealtimeChanges() {
|
||||||
repositoryScope.launch {
|
repositoryScope.launch {
|
||||||
try {
|
while (true) {
|
||||||
pocketBase.listenRealtime(listOf("persons", "payments")).collect { event ->
|
try {
|
||||||
Log.d("PocketBase-Realtime", "Change detected in PocketBase: $event")
|
pocketBase.listenRealtime(listOf("persons", "payments")).collect { event ->
|
||||||
refresh()
|
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) {
|
delay(3000.milliseconds)
|
||||||
Log.e("PocketBase-Realtime", "Realtime connection error, retrying...", e)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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()
|
fun getPersonsFlow(): Flow<List<Person>> = _personsFlow.asStateFlow()
|
||||||
|
|
||||||
@@ -192,19 +172,17 @@ class NetworkRepository(
|
|||||||
}
|
}
|
||||||
|
|
||||||
suspend fun deletePayment(paymentId: String): Boolean {
|
suspend fun deletePayment(paymentId: String): Boolean {
|
||||||
var retries = 0
|
|
||||||
val maxRetries = 2
|
val maxRetries = 2
|
||||||
|
for (retries in 1..maxRetries) {
|
||||||
while (retries <= maxRetries) {
|
|
||||||
try {
|
try {
|
||||||
pocketBase.deleteRecordByFilter("payments", "id='$paymentId'")
|
pocketBase.deleteRecordByFilter("payments", "id='$paymentId'")
|
||||||
Log.d("PocketBase", "deletePayment Success: $paymentId")
|
Log.d("PocketBase", "deletePayment Success: $paymentId")
|
||||||
return true
|
return true
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
retries++
|
|
||||||
Log.e("PocketBase", "deletePayment error (Attempt $retries): ${e.message}", e)
|
Log.e("PocketBase", "deletePayment error (Attempt $retries): ${e.message}", e)
|
||||||
if (retries > maxRetries) return false
|
if (retries < maxRetries) {
|
||||||
delay(1000L * retries)
|
delay((1000L * retries).milliseconds)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
|
|||||||
@@ -30,7 +30,6 @@ import androidx.compose.material.ExperimentalMaterialApi
|
|||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.automirrored.filled.Message
|
import androidx.compose.material.icons.automirrored.filled.Message
|
||||||
import androidx.compose.material.icons.filled.Archive
|
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.Check
|
||||||
import androidx.compose.material.icons.filled.Close
|
import androidx.compose.material.icons.filled.Close
|
||||||
import androidx.compose.material.icons.filled.Sync
|
import androidx.compose.material.icons.filled.Sync
|
||||||
@@ -117,7 +116,6 @@ fun PersonScreen(
|
|||||||
val dashboardData by viewModel.dashboardData.collectAsState()
|
val dashboardData by viewModel.dashboardData.collectAsState()
|
||||||
val financialReportData by viewModel.financialReportData.collectAsState()
|
val financialReportData by viewModel.financialReportData.collectAsState()
|
||||||
val showFinancialReportDialog by viewModel.showFinancialReportDialog.collectAsState()
|
val showFinancialReportDialog by viewModel.showFinancialReportDialog.collectAsState()
|
||||||
val isOperatorMode by viewModel.isOperatorMode.collectAsState()
|
|
||||||
val defaultPaymentAmount by viewModel.defaultPaymentAmountFlow.collectAsState(initial = 0.0)
|
val defaultPaymentAmount by viewModel.defaultPaymentAmountFlow.collectAsState(initial = 0.0)
|
||||||
val cardNumbers by viewModel.cardNumbersFlow.collectAsState(initial = emptyList())
|
val cardNumbers by viewModel.cardNumbersFlow.collectAsState(initial = emptyList())
|
||||||
val personForPaymentDialog by viewModel.personForPaymentDialog.collectAsState()
|
val personForPaymentDialog by viewModel.personForPaymentDialog.collectAsState()
|
||||||
@@ -188,8 +186,7 @@ fun PersonScreen(
|
|||||||
DashboardCard(
|
DashboardCard(
|
||||||
data = dashboardData,
|
data = dashboardData,
|
||||||
financialReportData = financialReportData,
|
financialReportData = financialReportData,
|
||||||
isOperatorMode = isOperatorMode,
|
isMosqueCategory = (currentCategory == "mosque")
|
||||||
onOpenReportClick = { viewModel.onShowFinancialReport() }
|
|
||||||
)
|
)
|
||||||
|
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
@@ -582,7 +579,7 @@ fun AddNewPersonDialog(
|
|||||||
val currentShamsiYear = getCurrentShamsiYear()
|
val currentShamsiYear = getCurrentShamsiYear()
|
||||||
val currentShamsiMonth = getCurrentShamsiMonth()
|
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 startYear by remember { mutableIntStateOf(currentShamsiYear) }
|
||||||
|
|
||||||
var similarContacts by remember { mutableStateOf<List<ContactMatch>>(emptyList()) }
|
var similarContacts by remember { mutableStateOf<List<ContactMatch>>(emptyList()) }
|
||||||
@@ -1072,8 +1069,7 @@ fun SelectCardDialog(
|
|||||||
fun DashboardCard(
|
fun DashboardCard(
|
||||||
data: DashboardUiModel,
|
data: DashboardUiModel,
|
||||||
financialReportData: FinancialReportData,
|
financialReportData: FinancialReportData,
|
||||||
isOperatorMode: Boolean = false,
|
isMosqueCategory: Boolean = false
|
||||||
onOpenReportClick: () -> Unit
|
|
||||||
) {
|
) {
|
||||||
val animatedProgress by animateFloatAsState(
|
val animatedProgress by animateFloatAsState(
|
||||||
targetValue = data.progress,
|
targetValue = data.progress,
|
||||||
@@ -1110,65 +1106,47 @@ fun DashboardCard(
|
|||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
verticalAlignment = Alignment.CenterVertically
|
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)
|
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()) {
|
if (financialReportData.totalOverdueDebt > 0 || financialReportData.debtors.isNotEmpty()) {
|
||||||
HorizontalDivider(modifier = Modifier.padding(vertical = 2.dp))
|
HorizontalDivider(modifier = Modifier.padding(vertical = 2.dp))
|
||||||
|
|
||||||
Column(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
verticalArrangement = Arrangement.spacedBy(4.dp)
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
Row(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = "🔴 مجموع بدهیهای معوقه:",
|
|
||||||
style = MaterialTheme.typography.titleSmall,
|
|
||||||
fontWeight = FontWeight.Bold,
|
|
||||||
color = MaterialTheme.colorScheme.error
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
text = "${formatNumberAsPersian(financialReportData.totalOverdueDebt)} تومان",
|
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
|
||||||
fontWeight = FontWeight.Bold,
|
|
||||||
color = MaterialTheme.colorScheme.error
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
val debtorNamesText = financialReportData.debtors.joinToString("، ") {
|
|
||||||
"${it.personName} (${formatNumberAsPersian(it.totalDebtAmount)})"
|
|
||||||
}
|
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = "بدهکاران (${financialReportData.totalDebtorsCount.toString().toPersianDigits()} نفر): $debtorNamesText",
|
text = "🔴 مجموع بدهیهای معوقه:",
|
||||||
style = MaterialTheme.typography.bodySmall,
|
style = MaterialTheme.typography.titleSmall,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
fontWeight = FontWeight.Bold,
|
||||||
maxLines = 3,
|
color = MaterialTheme.colorScheme.error
|
||||||
overflow = TextOverflow.Ellipsis
|
|
||||||
)
|
)
|
||||||
}
|
Text(
|
||||||
}
|
text = "${formatNumberAsPersian(financialReportData.totalOverdueDebt)} تومان",
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
if (!isOperatorMode) {
|
fontWeight = FontWeight.Bold,
|
||||||
OutlinedButton(
|
color = MaterialTheme.colorScheme.error
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1185,7 +1163,7 @@ fun FinancialReportDialog(
|
|||||||
Card(
|
Card(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.heightIn(max = 600.dp),
|
.heightIn(max = 650.dp),
|
||||||
shape = RoundedCornerShape(24.dp),
|
shape = RoundedCornerShape(24.dp),
|
||||||
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface)
|
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface)
|
||||||
) {
|
) {
|
||||||
@@ -1212,56 +1190,90 @@ fun FinancialReportDialog(
|
|||||||
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|
||||||
Card(
|
LazyColumn(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.4f))
|
verticalArrangement = Arrangement.spacedBy(10.dp)
|
||||||
) {
|
) {
|
||||||
Column(
|
item {
|
||||||
modifier = Modifier.padding(12.dp),
|
Card(
|
||||||
verticalArrangement = Arrangement.spacedBy(6.dp)
|
|
||||||
) {
|
|
||||||
Row(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
horizontalArrangement = Arrangement.SpaceBetween
|
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.4f))
|
||||||
) {
|
) {
|
||||||
Text("💰 کل درآمدهای تمام زمانها:", style = MaterialTheme.typography.bodyMedium, fontWeight = FontWeight.SemiBold)
|
Column(
|
||||||
Text("${formatNumberAsPersian(data.totalAllTimeIncome)} تومان", style = MaterialTheme.typography.bodyLarge, fontWeight = FontWeight.Bold)
|
modifier = Modifier.padding(12.dp),
|
||||||
}
|
verticalArrangement = Arrangement.spacedBy(6.dp)
|
||||||
Row(
|
) {
|
||||||
modifier = Modifier.fillMaxWidth(),
|
Row(
|
||||||
horizontalArrangement = Arrangement.SpaceBetween
|
modifier = Modifier.fillMaxWidth(),
|
||||||
) {
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
Text("🔴 کل بدهیهای معوقه فعلی:", style = MaterialTheme.typography.bodyMedium, fontWeight = FontWeight.SemiBold, color = MaterialTheme.colorScheme.error)
|
) {
|
||||||
Text("${formatNumberAsPersian(data.totalOverdueDebt)} تومان (${data.totalDebtorsCount.toString().toPersianDigits()} نفر)", style = MaterialTheme.typography.bodyMedium, fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.error)
|
Text("💰 کل درآمدهای تمام زمانها:", style = MaterialTheme.typography.bodyMedium, fontWeight = FontWeight.SemiBold)
|
||||||
|
Text("${formatNumberAsPersian(data.totalAllTimeIncome)} تومان", style = MaterialTheme.typography.bodyLarge, fontWeight = FontWeight.Bold)
|
||||||
|
}
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
|
) {
|
||||||
|
Text("🔴 کل بدهیهای معوقه فعلی:", style = MaterialTheme.typography.bodyMedium, fontWeight = FontWeight.SemiBold, color = MaterialTheme.colorScheme.error)
|
||||||
|
Text("${formatNumberAsPersian(data.totalOverdueDebt)} تومان (${data.totalDebtorsCount.toString().toPersianDigits()} نفر)", style = MaterialTheme.typography.bodyMedium, fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.error)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(12.dp))
|
if (data.debtors.isNotEmpty()) {
|
||||||
HorizontalDivider()
|
item {
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Card(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
Text(
|
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.errorContainer.copy(alpha = 0.15f))
|
||||||
text = "تفکیک درآمد ماهانه ماههای گذشته",
|
) {
|
||||||
style = MaterialTheme.typography.titleMedium,
|
Column(
|
||||||
fontWeight = FontWeight.Bold,
|
modifier = Modifier.padding(12.dp),
|
||||||
modifier = Modifier.padding(bottom = 8.dp)
|
verticalArrangement = Arrangement.spacedBy(4.dp)
|
||||||
)
|
) {
|
||||||
|
Text(
|
||||||
if (data.monthlyReports.isEmpty()) {
|
text = "لیست بدهکاران (${data.totalDebtorsCount.toString().toPersianDigits()} نفر):",
|
||||||
Box(
|
style = MaterialTheme.typography.titleSmall,
|
||||||
modifier = Modifier
|
fontWeight = FontWeight.Bold,
|
||||||
.fillMaxWidth()
|
color = MaterialTheme.colorScheme.error
|
||||||
.padding(24.dp),
|
)
|
||||||
contentAlignment = Alignment.Center
|
val debtorNamesText = data.debtors.joinToString("، ") {
|
||||||
) {
|
"${it.personName} (${formatNumberAsPersian(it.totalDebtAmount)})"
|
||||||
Text("هیچ دریافتی تا کنون ثبت نشده است.", style = MaterialTheme.typography.bodyMedium)
|
}
|
||||||
|
Text(
|
||||||
|
text = debtorNamesText,
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
LazyColumn(
|
item {
|
||||||
modifier = Modifier.weight(1f),
|
Column {
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
HorizontalDivider(modifier = Modifier.padding(vertical = 4.dp))
|
||||||
) {
|
Text(
|
||||||
|
text = "تفکیک درآمد ماهانه ماههای گذشته",
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
modifier = Modifier.padding(top = 4.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.monthlyReports.isEmpty()) {
|
||||||
|
item {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(24.dp),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Text("هیچ دریافتی تا کنون ثبت نشده است.", style = MaterialTheme.typography.bodyMedium)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
items(data.monthlyReports) { report ->
|
items(data.monthlyReports) { report ->
|
||||||
Card(
|
Card(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
@@ -1308,15 +1320,6 @@ fun FinancialReportDialog(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(12.dp))
|
|
||||||
|
|
||||||
TextButton(
|
|
||||||
onClick = onDismiss,
|
|
||||||
modifier = Modifier.align(Alignment.End)
|
|
||||||
) {
|
|
||||||
Text("بستن")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,24 +5,32 @@ import androidx.activity.compose.rememberLauncherForActivityResult
|
|||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.compose.animation.AnimatedContent
|
import androidx.compose.animation.AnimatedContent
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
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
|
||||||
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.fillMaxHeight
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
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.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.automirrored.filled.Logout
|
import androidx.compose.material.icons.automirrored.filled.Logout
|
||||||
import androidx.compose.material.icons.filled.Add
|
import androidx.compose.material.icons.filled.Add
|
||||||
|
import androidx.compose.material.icons.filled.AdminPanelSettings
|
||||||
import androidx.compose.material.icons.filled.ArrowDropDown
|
import androidx.compose.material.icons.filled.ArrowDropDown
|
||||||
import androidx.compose.material.icons.filled.ContentCopy
|
import androidx.compose.material.icons.filled.ContentCopy
|
||||||
import androidx.compose.material.icons.filled.Delete
|
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.Lock
|
||||||
|
import androidx.compose.material.icons.filled.LockOpen
|
||||||
import androidx.compose.ui.platform.LocalClipboardManager
|
import androidx.compose.ui.platform.LocalClipboardManager
|
||||||
import androidx.compose.ui.text.AnnotatedString
|
import androidx.compose.ui.text.AnnotatedString
|
||||||
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||||
@@ -38,6 +46,7 @@ import androidx.compose.material3.IconButton
|
|||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.OutlinedButton
|
import androidx.compose.material3.OutlinedButton
|
||||||
import androidx.compose.material3.OutlinedTextField
|
import androidx.compose.material3.OutlinedTextField
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
@@ -136,16 +145,18 @@ fun SettingsScreen(onLogout: () -> Unit) {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
val scrollState = rememberScrollState()
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(horizontal = 16.dp, vertical = 8.dp)
|
.fillMaxSize()
|
||||||
.fillMaxWidth()
|
.verticalScroll(scrollState)
|
||||||
.fillMaxHeight()
|
.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
stringResource(R.string.settings),
|
stringResource(R.string.settings),
|
||||||
style = MaterialTheme.typography.titleMedium,
|
style = MaterialTheme.typography.titleMedium
|
||||||
modifier = Modifier.padding(bottom = 8.dp)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
DefaultAmountSetting(
|
DefaultAmountSetting(
|
||||||
@@ -163,16 +174,12 @@ fun SettingsScreen(onLogout: () -> Unit) {
|
|||||||
onChangePin = { viewModel.setAdminPin(it) }
|
onChangePin = { viewModel.setAdminPin(it) }
|
||||||
)
|
)
|
||||||
|
|
||||||
Spacer(Modifier.weight(1f))
|
|
||||||
|
|
||||||
val reminderDay by viewModel.reminderDay.collectAsState()
|
val reminderDay by viewModel.reminderDay.collectAsState()
|
||||||
ReminderDaySetting(
|
ReminderDaySetting(
|
||||||
currentDay = reminderDay,
|
currentDay = reminderDay,
|
||||||
onSave = { viewModel.saveReminderDay(it) }
|
onSave = { viewModel.saveReminderDay(it) }
|
||||||
)
|
)
|
||||||
|
|
||||||
Spacer(Modifier.weight(1f))
|
|
||||||
|
|
||||||
val cardNumbers by viewModel.cardNumbers.collectAsState()
|
val cardNumbers by viewModel.cardNumbers.collectAsState()
|
||||||
CardNumbersSetting(
|
CardNumbersSetting(
|
||||||
cardNumbers = cardNumbers,
|
cardNumbers = cardNumbers,
|
||||||
@@ -181,20 +188,17 @@ fun SettingsScreen(onLogout: () -> Unit) {
|
|||||||
onRemove = { viewModel.removeCardNumber(it) }
|
onRemove = { viewModel.removeCardNumber(it) }
|
||||||
)
|
)
|
||||||
|
|
||||||
Spacer(Modifier.weight(1f))
|
|
||||||
|
|
||||||
FontSelectionDropdown(
|
FontSelectionDropdown(
|
||||||
selectedFont = selectedFont,
|
selectedFont = selectedFont,
|
||||||
onFontSelected = { viewModel.onFontSelected(it) }
|
onFontSelected = { viewModel.onFontSelected(it) }
|
||||||
)
|
)
|
||||||
|
|
||||||
if (!isOperatorMode) {
|
if (!isOperatorMode) {
|
||||||
HorizontalDivider(modifier = Modifier.padding(vertical = 16.dp))
|
HorizontalDivider(modifier = Modifier.padding(vertical = 4.dp))
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
"پشتیبانگیری و بازیابی",
|
"پشتیبانگیری و بازیابی",
|
||||||
style = MaterialTheme.typography.titleMedium,
|
style = MaterialTheme.typography.titleMedium
|
||||||
modifier = Modifier.padding(bottom = 8.dp)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||||
@@ -224,24 +228,26 @@ fun SettingsScreen(onLogout: () -> Unit) {
|
|||||||
viewModel.loadSnapshots()
|
viewModel.loadSnapshots()
|
||||||
showSnapshotDialog = true
|
showSnapshotDialog = true
|
||||||
},
|
},
|
||||||
modifier = Modifier.fillMaxWidth().padding(top = 8.dp)
|
modifier = Modifier.fillMaxWidth()
|
||||||
) {
|
) {
|
||||||
Text(stringResource(R.string.auto_backup_button), fontSize = 13.sp)
|
Text(stringResource(R.string.auto_backup_button), fontSize = 13.sp)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer(Modifier.weight(1f))
|
Spacer(Modifier.height(8.dp))
|
||||||
|
|
||||||
Button(
|
Button(
|
||||||
onClick = onLogout,
|
onClick = onLogout,
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.error),
|
colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.error),
|
||||||
contentPadding = androidx.compose.foundation.layout.PaddingValues(vertical = 8.dp)
|
contentPadding = PaddingValues(vertical = 12.dp)
|
||||||
) {
|
) {
|
||||||
Icon(Icons.AutoMirrored.Filled.Logout, contentDescription = null)
|
Icon(Icons.AutoMirrored.Filled.Logout, contentDescription = null)
|
||||||
Spacer(Modifier.width(8.dp))
|
Spacer(Modifier.width(8.dp))
|
||||||
Text(stringResource(R.string.logout))
|
Text(stringResource(R.string.logout))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Spacer(Modifier.height(16.dp))
|
||||||
}
|
}
|
||||||
|
|
||||||
if (showSnapshotDialog) {
|
if (showSnapshotDialog) {
|
||||||
@@ -621,57 +627,105 @@ fun OperatorModeSetting(
|
|||||||
|
|
||||||
Card(
|
Card(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
shape = RoundedCornerShape(16.dp),
|
||||||
colors = CardDefaults.cardColors(
|
colors = CardDefaults.cardColors(
|
||||||
containerColor = if (isOperatorMode) MaterialTheme.colorScheme.errorContainer.copy(alpha = 0.2f) else MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.2f)
|
containerColor = if (isOperatorMode) MaterialTheme.colorScheme.errorContainer.copy(alpha = 0.15f) else MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f)
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
Column(
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
modifier = Modifier.padding(16.dp),
|
||||||
Column(modifier = Modifier.weight(1f)) {
|
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||||
Text("حالت دسترسی برنامه", style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold)
|
) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
Icon(
|
||||||
|
imageVector = if (isOperatorMode) Icons.Default.Lock else Icons.Default.AdminPanelSettings,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = if (isOperatorMode) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.primary,
|
||||||
|
modifier = Modifier.size(24.dp)
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
Text(
|
Text(
|
||||||
if (isOperatorMode) "وضعیت فعلی: حالت اپراتور (محدود)" else "وضعیت فعلی: حالت مدیریت (کامل)",
|
text = "حالت دسترسی برنامه",
|
||||||
style = MaterialTheme.typography.bodySmall,
|
style = MaterialTheme.typography.titleMedium,
|
||||||
color = if (isOperatorMode) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.primary
|
fontWeight = FontWeight.Bold
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Surface(
|
||||||
|
color = if (isOperatorMode) MaterialTheme.colorScheme.errorContainer else MaterialTheme.colorScheme.primaryContainer,
|
||||||
|
shape = RoundedCornerShape(12.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = if (isOperatorMode) "اپراتور (محدود)" else "مدیریت (کامل)",
|
||||||
|
modifier = Modifier.padding(horizontal = 10.dp, vertical = 4.dp),
|
||||||
|
style = MaterialTheme.typography.labelMedium,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = if (isOperatorMode) MaterialTheme.colorScheme.onErrorContainer else MaterialTheme.colorScheme.onPrimaryContainer
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier.fillMaxWidth()) {
|
HorizontalDivider(thickness = 0.5.dp)
|
||||||
if (isOperatorMode) {
|
|
||||||
Button(
|
|
||||||
onClick = {
|
|
||||||
pinAction = PinAction.UNLOCK_ADMIN
|
|
||||||
enteredPin = ""
|
|
||||||
errorMessage = ""
|
|
||||||
showPinDialog = true
|
|
||||||
},
|
|
||||||
modifier = Modifier.fillMaxWidth()
|
|
||||||
) {
|
|
||||||
Text("خروج از حالت اپراتور")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Button(
|
|
||||||
onClick = {
|
|
||||||
onToggleMode(true)
|
|
||||||
},
|
|
||||||
modifier = Modifier.weight(1f)
|
|
||||||
) {
|
|
||||||
Text("فعالسازی حالت اپراتور")
|
|
||||||
}
|
|
||||||
|
|
||||||
OutlinedButton(
|
if (isOperatorMode) {
|
||||||
onClick = {
|
Button(
|
||||||
|
onClick = {
|
||||||
|
pinAction = PinAction.UNLOCK_ADMIN
|
||||||
|
enteredPin = ""
|
||||||
|
errorMessage = ""
|
||||||
|
showPinDialog = true
|
||||||
|
},
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
shape = RoundedCornerShape(10.dp),
|
||||||
|
colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.primary)
|
||||||
|
) {
|
||||||
|
Icon(Icons.Default.LockOpen, contentDescription = null, modifier = Modifier.size(18.dp))
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
Text("خروج از حالت اپراتور (ورود رمز مدیر)", style = MaterialTheme.typography.bodyMedium, fontWeight = FontWeight.Bold)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
OutlinedButton(
|
||||||
|
onClick = { onToggleMode(true) },
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
shape = RoundedCornerShape(10.dp)
|
||||||
|
) {
|
||||||
|
Icon(Icons.Default.Lock, contentDescription = null, modifier = Modifier.size(18.dp))
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
Text("فعالسازی حالت اپراتور (محدودسازی)", style = MaterialTheme.typography.bodyMedium, fontWeight = FontWeight.Bold)
|
||||||
|
}
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clickable {
|
||||||
pinAction = PinAction.CHANGE_PIN
|
pinAction = PinAction.CHANGE_PIN
|
||||||
enteredPin = ""
|
enteredPin = ""
|
||||||
newPinInput = ""
|
newPinInput = ""
|
||||||
errorMessage = ""
|
errorMessage = ""
|
||||||
showPinDialog = true
|
showPinDialog = true
|
||||||
},
|
}
|
||||||
modifier = Modifier.weight(1f)
|
.padding(vertical = 4.dp),
|
||||||
) {
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
Text("تغییر رمز مدیر")
|
horizontalArrangement = Arrangement.Center
|
||||||
}
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Edit,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = MaterialTheme.colorScheme.primary,
|
||||||
|
modifier = Modifier.size(18.dp)
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(6.dp))
|
||||||
|
Text(
|
||||||
|
text = "تغییر رمز عبور مدیریت",
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.primary,
|
||||||
|
fontWeight = FontWeight.SemiBold
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,3 +30,32 @@ fun formatTimestampToPersianDateTime(timestamp: Long): String {
|
|||||||
val time = formatTimestampToPersianTime(timestamp)
|
val time = formatTimestampToPersianTime(timestamp)
|
||||||
return "$date، ساعت $time"
|
return "$date، ساعت $time"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun isTimestampInCurrentCycle(timestamp: Long): Boolean {
|
||||||
|
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.getCurrentShamsiMonth
|
||||||
import com.oqba26.monthlypaymentapp.utils.getCurrentShamsiYear
|
import com.oqba26.monthlypaymentapp.utils.getCurrentShamsiYear
|
||||||
import com.oqba26.monthlypaymentapp.utils.getPersianMonthName
|
import com.oqba26.monthlypaymentapp.utils.getPersianMonthName
|
||||||
|
import com.oqba26.monthlypaymentapp.utils.isTimestampInCurrentCycle
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.coroutineScope
|
import kotlinx.coroutines.coroutineScope
|
||||||
@@ -69,9 +70,6 @@ class PersonViewModel @Inject constructor(
|
|||||||
private val _infoMessage = MutableSharedFlow<String>()
|
private val _infoMessage = MutableSharedFlow<String>()
|
||||||
val infoMessage = _infoMessage.asSharedFlow()
|
val infoMessage = _infoMessage.asSharedFlow()
|
||||||
|
|
||||||
val isOperatorMode: StateFlow<Boolean> = settingsRepository.isOperatorModeFlow
|
|
||||||
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000L), false)
|
|
||||||
|
|
||||||
private val _uiState = MutableStateFlow(PersonListUiState())
|
private val _uiState = MutableStateFlow(PersonListUiState())
|
||||||
val uiState: StateFlow<PersonListUiState> = _uiState.asStateFlow()
|
val uiState: StateFlow<PersonListUiState> = _uiState.asStateFlow()
|
||||||
|
|
||||||
@@ -248,13 +246,13 @@ class PersonViewModel @Inject constructor(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
updateDashboard(uiModels.size, relevantPayments)
|
val categoryPayments = allPayments.filter { it.category == category }
|
||||||
|
updateDashboard(uiModels.size, relevantPayments, categoryPayments)
|
||||||
|
|
||||||
val (paidModels, unpaidModels) = uiModels.partition { it.hasPaidThisMonth }
|
val (paidModels, unpaidModels) = uiModels.partition { it.hasPaidThisMonth }
|
||||||
|
|
||||||
// محاسبه گزارشات مالی جامع
|
// محاسبه گزارشات مالی جامع
|
||||||
val personsMap = persons.associateBy { it.id }
|
val personsMap = persons.associateBy { it.id }
|
||||||
val categoryPayments = allPayments.filter { it.category == category }
|
|
||||||
val totalAllTimeIncome = categoryPayments.sumOf { it.amount }
|
val totalAllTimeIncome = categoryPayments.sumOf { it.amount }
|
||||||
|
|
||||||
val debtorsList = unpaidModels.filter { it.debtCount > 0 }.map { personModel ->
|
val debtorsList = unpaidModels.filter { it.debtCount > 0 }.map { personModel ->
|
||||||
@@ -590,11 +588,20 @@ class PersonViewModel @Inject constructor(
|
|||||||
exportManager.shareReceipt(payment, personName)
|
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 paidCount = paymentsThisMonth.map { it.personId }.distinct().size
|
||||||
val totalIncome = paymentsThisMonth.sumOf { it.amount }
|
val totalIncome = paymentsThisMonth.sumOf { it.amount }
|
||||||
|
val cyclePayments = categoryPayments.filter { payment ->
|
||||||
|
val time = payment.createdAt ?: payment.timestamp
|
||||||
|
isTimestampInCurrentCycle(time)
|
||||||
|
}
|
||||||
|
val totalCycleIncome = cyclePayments.sumOf { it.amount }
|
||||||
val progress = if (totalPersonCount > 0) paidCount.toFloat() / totalPersonCount.toFloat() else 0f
|
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 {
|
private fun createPaymentRecord(personId: String, month: Int, year: Int, amount: Double, description: String): PaymentRecord {
|
||||||
|
|||||||
@@ -143,6 +143,7 @@ data class DashboardUiModel(
|
|||||||
val paidCount: Int = 0,
|
val paidCount: Int = 0,
|
||||||
val totalCount: Int = 0,
|
val totalCount: Int = 0,
|
||||||
val totalIncome: Double = 0.0,
|
val totalIncome: Double = 0.0,
|
||||||
|
val totalCycleIncome: Double = 0.0,
|
||||||
val progress: Float = 0f
|
val progress: Float = 0f
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
+4
-4
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"versionCode": 9,
|
"versionCode": 13,
|
||||||
"versionName": "1.0.8",
|
"versionName": "1.1.2",
|
||||||
"url": "https://github.com/oqba26/MonthlyPaymentApp/releases/download/v1.0.8/MonthlyPaymentApp-v1.0.8.apk",
|
"url": "https://github.com/oqba26/MonthlyPaymentApp/releases/download/v1.1.2/MonthlyPaymentApp-v1.1.2.apk",
|
||||||
"releaseNotes": "بهبودهای تب مسجد و افزودن حالت اپراتور (محدودسازی دسترسی)",
|
"releaseNotes": "بهبودهای پایداری و عملکرد همگامسازی",
|
||||||
"isForceUpdate": false
|
"isForceUpdate": false
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user