4 Commits
6 changed files with 236 additions and 181 deletions

No files matched your search

+9 -3
View File
@@ -30,8 +30,8 @@ android {
applicationId = "com.oqba26.monthlypaymentapp" applicationId = "com.oqba26.monthlypaymentapp"
minSdk = 25 minSdk = 25
targetSdk = 34 targetSdk = 34
versionCode = 9 versionCode = 12
versionName = "1.0.8" versionName = "1.1.1"
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)
} }
@@ -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
@@ -1117,39 +1117,22 @@ fun DashboardCard(
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,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.error
) )
} }
} }
@@ -1185,7 +1168,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 +1195,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 +1325,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
)
} }
} }
} }
+4 -4
View File
@@ -1,7 +1,7 @@
{ {
"versionCode": 9, "versionCode": 12,
"versionName": "1.0.8", "versionName": "1.1.1",
"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.1/MonthlyPaymentApp-v1.1.1.apk",
"releaseNotes": "بهبودهای تب مسجد و افزودن حالت اپراتور (محدودسازی دسترسی)", "releaseNotes": "بهبودهای همگام‌سازی شبکه و پایداری لایو PocketBase",
"isForceUpdate": false "isForceUpdate": false
} }