diff --git a/.idea/compiler.xml b/.idea/compiler.xml
index b589d56..b86273d 100644
--- a/.idea/compiler.xml
+++ b/.idea/compiler.xml
@@ -1,6 +1,6 @@
-
+
\ No newline at end of file
diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml
index b268ef3..defe597 100644
--- a/.idea/deploymentTargetSelector.xml
+++ b/.idea/deploymentTargetSelector.xml
@@ -4,6 +4,14 @@
+
+
+
+
+
+
+
+
diff --git a/.idea/gradle.xml b/.idea/gradle.xml
index 5ef3273..0f2d2fa 100644
--- a/.idea/gradle.xml
+++ b/.idea/gradle.xml
@@ -6,10 +6,11 @@
-
+
diff --git a/.idea/misc.xml b/.idea/misc.xml
index 8978d23..b2c751a 100644
--- a/.idea/misc.xml
+++ b/.idea/misc.xml
@@ -1,6 +1,6 @@
-
+
diff --git a/PersianDate/build.gradle.kts b/PersianDate/build.gradle.kts
new file mode 100644
index 0000000..266caca
--- /dev/null
+++ b/PersianDate/build.gradle.kts
@@ -0,0 +1,35 @@
+@Suppress("DSL_SCOPE_VIOLATION")
+plugins {
+ alias(libs.plugins.android.library)
+ alias(libs.plugins.jetbrains.kotlin.android)
+}
+
+android {
+ namespace = "com.github.samanzamani.persiandate"
+ compileSdk = 34
+
+ defaultConfig {
+ minSdk = 21
+ }
+
+ buildTypes {
+ release {
+ isMinifyEnabled = false
+ }
+ }
+
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_17
+ targetCompatibility = JavaVersion.VERSION_17
+ }
+ kotlinOptions {
+ jvmTarget = "17"
+ }
+}
+
+dependencies {
+ implementation(libs.androidx.core.ktx)
+ implementation(libs.androidx.appcompat)
+ testImplementation("junit:junit:4.13.2")
+ testImplementation("org.json:json:20231013")
+}
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 22bff53..2ced059 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -47,14 +47,14 @@ android {
}
compileOptions {
- // همتراز با JDK 17
- sourceCompatibility = JavaVersion.VERSION_17
- targetCompatibility = JavaVersion.VERSION_17
+ // Align with the JDK version used by Android Studio
+ sourceCompatibility = JavaVersion.VERSION_21
+ targetCompatibility = JavaVersion.VERSION_21
isCoreLibraryDesugaringEnabled = true
}
- kotlinOptions { jvmTarget = "17" }
- // اطمینان از JDK 17
- kotlin { jvmToolchain(17) }
+ kotlinOptions { jvmTarget = "21" }
+ // Ensure JDK 21
+ kotlin { jvmToolchain(21) }
packaging {
resources { excludes += "/META-INF/{AL2.0,LGPL2.1}" }
@@ -72,7 +72,9 @@ dependencies {
implementation(libs.androidx.ui.tooling.preview)
implementation(libs.androidx.material3)
implementation(libs.androidx.navigation.compose)
+ implementation("androidx.compose.material:material") // برای PullRefresh
implementation("androidx.compose.material:material-icons-extended-android:1.6.7")
+ implementation("org.burnoutcrew.composereorderable:reorderable:0.9.6")
// Core & Coroutines
implementation(libs.androidx.lifecycle.viewmodel.savedstate)
@@ -98,7 +100,7 @@ dependencies {
implementation(libs.androidx.datastore.preferences)
// Utility
- implementation(libs.persiandate)
+ implementation(project(":PersianDate"))
implementation(libs.google.material)
// Desugaring
diff --git a/app/src/main/java/com/oqba26/monthlypaymentapp/MainActivity.kt b/app/src/main/java/com/oqba26/monthlypaymentapp/MainActivity.kt
index bf16292..1f58764 100644
--- a/app/src/main/java/com/oqba26/monthlypaymentapp/MainActivity.kt
+++ b/app/src/main/java/com/oqba26/monthlypaymentapp/MainActivity.kt
@@ -1,3 +1,5 @@
+@file:OptIn(ExperimentalMaterial3Api::class)
+
package com.oqba26.monthlypaymentapp
import android.os.Bundle
@@ -9,18 +11,31 @@ import androidx.activity.enableEdgeToEdge
import androidx.compose.animation.Crossfade
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.filled.List
+import androidx.compose.material.icons.filled.Archive
+import androidx.compose.material.icons.filled.Paid
+import androidx.compose.material.icons.filled.Settings
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.CircularProgressIndicator
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.NavigationBar
+import androidx.compose.material3.NavigationBarItem
+import androidx.compose.material3.NavigationBarItemDefaults
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
+import androidx.compose.material3.TopAppBar
+import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
@@ -31,17 +46,26 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
+import androidx.navigation.NavController
+import androidx.navigation.NavDestination.Companion.hierarchy
+import androidx.navigation.NavGraph.Companion.findStartDestination
+import androidx.navigation.NavType
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController
+import androidx.navigation.navArgument
import com.oqba26.monthlypaymentapp.core.PaymentApplication
+import com.oqba26.monthlypaymentapp.ui.screens.ArchiveScreen
import com.oqba26.monthlypaymentapp.ui.screens.AuthScreen
+import com.oqba26.monthlypaymentapp.ui.screens.PaidListScreen
+import com.oqba26.monthlypaymentapp.ui.screens.PersonDetailScreen
import com.oqba26.monthlypaymentapp.ui.screens.PersonScreen
import com.oqba26.monthlypaymentapp.ui.screens.SettingsScreen
import com.oqba26.monthlypaymentapp.ui.theme.MonthlyPaymentManagement2Theme
@@ -54,7 +78,8 @@ class MainActivity : ComponentActivity() {
enableEdgeToEdge()
setContent {
val application = application as PaymentApplication
- val personViewModel: PersonViewModel = viewModel(factory = application.personViewModelFactory)
+ val personViewModel: PersonViewModel =
+ viewModel(factory = application.personViewModelFactory)
MonthlyPaymentManagement2Theme {
CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) {
@@ -81,26 +106,108 @@ fun MainAppHost(viewModel: PersonViewModel) {
}
}
- Scaffold(
- modifier = Modifier.fillMaxSize()
- ) { paddingValues ->
+ Crossfade(targetState = authState, label = "AuthScreenSwitch") { state ->
+ when (state) {
+ is AuthState.Loading -> LoadingScreen()
+ is AuthState.Unauthenticated -> AuthScreen()
+ is AuthState.Authenticated -> AuthenticatedContent(viewModel)
+ }
+ }
+}
- Box(
- modifier = Modifier
- .fillMaxSize()
- .padding(paddingValues)
+
+@Composable
+fun AuthenticatedContent(viewModel: PersonViewModel) {
+ val navController = rememberNavController()
+ var showExitDialog by remember { mutableStateOf(false) }
+ val context = LocalContext.current
+
+ val navBackStackEntry by navController.currentBackStackEntryAsState()
+ val currentRoute = navBackStackEntry?.destination?.route ?: ""
+
+ val showBottomBar = currentRoute in listOf(
+ Screen.Unpaid.route,
+ Screen.Paid.route,
+ Screen.Archive.route,
+ Screen.Settings.route
+ )
+
+ // هندل کردن دکمه Back
+ BackHandler {
+ showExitDialog = true
+ }
+
+ if (showExitDialog) {
+ ExitDialog(
+ onConfirmExit = { (context as? ComponentActivity)?.finish() },
+ onDismiss = {
+ @Suppress("AssignedValueIsNeverRead")
+ showExitDialog = false
+ }
+ )
+ }
+
+ Scaffold(
+ topBar = {
+ TopAppBar(
+ title = { Text("مدیریت حقوق ماهانه") },
+ colors = TopAppBarDefaults.topAppBarColors(
+ containerColor = MaterialTheme.colorScheme.primary,
+ titleContentColor = Color.White,
+ actionIconContentColor = Color.White
+ ),
+ actions = {
+ if (currentRoute == Screen.Unpaid.route) {
+ Button(
+ onClick = { viewModel.onAddPersonClicked() },
+ modifier = Modifier.padding(horizontal = 8.dp),
+ colors = ButtonDefaults.buttonColors(
+ containerColor = MaterialTheme.colorScheme.secondary
+ )
+ ) {
+ Text("افزودن شخص")
+ }
+ }
+ }
+ )
+ },
+ bottomBar = {
+ if (showBottomBar) {
+ AppBottomNavigationBar(navController = navController)
+ }
+ }
+ ) { innerPadding ->
+ NavHost(
+ navController = navController,
+ startDestination = Screen.Unpaid.route,
+ modifier = Modifier.padding(innerPadding)
) {
- Crossfade(targetState = authState, label = "AuthScreenSwitch") { state ->
- when (state) {
- is AuthState.Loading -> {
- LoadingScreen()
- }
- is AuthState.Unauthenticated -> {
- AuthScreen()
- }
- is AuthState.Authenticated -> {
- AppNavigation(viewModel)
- }
+ composable(Screen.Unpaid.route) {
+ PersonScreen(
+ viewModel = viewModel,
+ navController = navController
+ )
+ }
+ composable(Screen.Paid.route) {
+ PaidListScreen(viewModel, navController)
+ }
+ composable(Screen.Archive.route) {
+ ArchiveScreen(viewModel)
+ }
+ composable(Screen.Settings.route) {
+ SettingsScreen(onLogout = { viewModel.logout() })
+ }
+ composable(
+ route = "person_detail/{personId}",
+ arguments = listOf(navArgument("personId") { type = NavType.StringType })
+ ) { backStackEntry ->
+ val personId = backStackEntry.arguments?.getString("personId")
+ if (personId != null) {
+ PersonDetailScreen(
+ personId = personId,
+ viewModel = viewModel,
+ navController = navController
+ )
}
}
}
@@ -108,64 +215,93 @@ fun MainAppHost(viewModel: PersonViewModel) {
}
@Composable
-fun AppNavigation(viewModel: PersonViewModel) {
- val navController = rememberNavController()
- val navBackStackEntry by navController.currentBackStackEntryAsState()
- val currentRoute = navBackStackEntry?.destination?.route
+fun AppBottomNavigationBar(navController: NavController) {
+ val items = listOf(
+ Screen.Unpaid,
+ Screen.Paid,
+ Screen.Archive,
+ Screen.Settings
+ )
- var showExitDialog by remember { mutableStateOf(false) }
- val context = LocalContext.current
+ NavigationBar(
+ containerColor = MaterialTheme.colorScheme.primary // Match TopAppBar
+ ) {
+ val navBackStackEntry by navController.currentBackStackEntryAsState()
+ val currentDestination = navBackStackEntry?.destination
- BackHandler(enabled = currentRoute == "personScreen") {
- showExitDialog = true
- }
-
- if (showExitDialog) {
- AlertDialog(
- onDismissRequest = { showExitDialog = false },
- title = { Text("خروج از برنامه") },
- text = { Text("آیا برای خروج از برنامه مطمئن هستید؟") },
- confirmButton = {
- Row(
- modifier = Modifier
- .fillMaxWidth()
- .padding(all = 8.dp),
- horizontalArrangement = Arrangement.SpaceBetween
- ) {
- Button(
- onClick = { (context as? ComponentActivity)?.finish() },
- colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.error)
- ) {
- Text("بله")
- }
- Button(onClick = { showExitDialog = false }) {
- Text("خیر")
+ items.forEach { screen ->
+ NavigationBarItem(
+ colors = NavigationBarItemDefaults.colors(
+ selectedTextColor = Color.White,
+ selectedIconColor = Color.White,
+ indicatorColor = MaterialTheme.colorScheme.primaryContainer,
+ unselectedTextColor = Color.White.copy(alpha = 0.7f),
+ unselectedIconColor = Color.White.copy(alpha = 0.7f)
+ ),
+ icon = { Icon(screen.icon, contentDescription = screen.title) },
+ label = { Text(screen.title) },
+ selected = currentDestination?.hierarchy?.any { it.route == screen.route } == true,
+ onClick = {
+ navController.navigate(screen.route) {
+ popUpTo(navController.graph.findStartDestination().id) {
+ saveState = true
+ }
+ launchSingleTop = true
+ restoreState = true
}
}
- },
- dismissButton = {}
- )
- }
-
-
- NavHost(navController = navController, startDestination = "personScreen") {
- composable("personScreen") {
- PersonScreen(
- onSettingsClick = { navController.navigate("settings") },
- //onExitApp = { viewModel.logout() }
)
}
- composable("settings") {
- SettingsScreen(onBackClick = { navController.popBackStack() })
- }
}
}
+sealed class Screen(
+ val route: String,
+ val title: String,
+ val icon: androidx.compose.ui.graphics.vector.ImageVector
+) {
+ data object Unpaid : Screen("unpaid_list", "پرداخت نشده", Icons.AutoMirrored.Filled.List)
+ data object Paid : Screen("paid_list", "پرداخت شده", Icons.Default.Paid)
+ data object Archive : Screen("archive", "آرشیو", Icons.Default.Archive)
+ data object Settings : Screen("settings", "تنظیمات", Icons.Default.Settings)
+}
+
+@Composable
+fun ExitDialog(onConfirmExit: () -> Unit, onDismiss: () -> Unit) {
+ AlertDialog(
+ onDismissRequest = onDismiss,
+ title = { Text("خروج از برنامه") },
+ text = { Text("آیا برای خروج از برنامه مطمئن هستید؟") },
+ confirmButton = {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(all = 8.dp),
+ horizontalArrangement = Arrangement.SpaceBetween
+ ) {
+ Button(
+ onClick = onConfirmExit,
+ colors = ButtonDefaults.buttonColors(
+ containerColor = MaterialTheme.colorScheme.error
+ )
+ ) {
+ Text("بله")
+ }
+ Button(onClick = onDismiss) {
+ Text("خیر")
+ }
+ }
+ },
+ dismissButton = {}
+ )
+}
@Composable
fun LoadingScreen() {
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
- CircularProgressIndicator()
- Text("در حال بررسی وضعیت...", modifier = Modifier.align(Alignment.Center).padding(top = 80.dp))
+ Column(horizontalAlignment = Alignment.CenterHorizontally) {
+ CircularProgressIndicator()
+ Text("در حال بررسی وضعیت...", modifier = Modifier.padding(top = 16.dp))
+ }
}
-}
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/oqba26/monthlypaymentapp/data/dao/PersonDao.kt b/app/src/main/java/com/oqba26/monthlypaymentapp/data/dao/PersonDao.kt
index 122d14c..4f89f75 100644
--- a/app/src/main/java/com/oqba26/monthlypaymentapp/data/dao/PersonDao.kt
+++ b/app/src/main/java/com/oqba26/monthlypaymentapp/data/dao/PersonDao.kt
@@ -10,7 +10,7 @@ import kotlinx.coroutines.flow.Flow
@Dao
interface PersonDao {
- @Query("SELECT * FROM persons ORDER BY name COLLATE NOCASE ASC")
+ @Query("SELECT * FROM persons ORDER BY displayOrder ASC")
fun getAllPersonsFlow(): Flow>
@Query("SELECT * FROM persons WHERE id = :id LIMIT 1")
@@ -19,6 +19,12 @@ interface PersonDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertAll(persons: List)
+ @Query("UPDATE persons SET isArchived = :isArchived WHERE id = :personId")
+ suspend fun updateArchivedStatus(personId: String, isArchived: Boolean)
+
+ @Query("UPDATE persons SET displayOrder = :displayOrder WHERE id = :personId")
+ suspend fun updateDisplayOrder(personId: String, displayOrder: Int)
+
@Query("DELETE FROM persons")
suspend fun deleteAll()
}
\ No newline at end of file
diff --git a/app/src/main/java/com/oqba26/monthlypaymentapp/data/database/AppDatabase.kt b/app/src/main/java/com/oqba26/monthlypaymentapp/data/database/AppDatabase.kt
index 201a674..cf154dc 100644
--- a/app/src/main/java/com/oqba26/monthlypaymentapp/data/database/AppDatabase.kt
+++ b/app/src/main/java/com/oqba26/monthlypaymentapp/data/database/AppDatabase.kt
@@ -1,3 +1,5 @@
+@file:Suppress("unused")
+
package com.oqba26.monthlypaymentapp.data.database
import android.content.Context
@@ -14,7 +16,7 @@ import java.util.UUID
@Database(
entities = [Person::class, PaymentRecord::class],
- version = 11, // حتماً نسخه را بالا ببری که اسکیمای جدید اعمال شود
+ version = 13, // حتماً نسخه را بالا ببری که اسکیمای جدید اعمال شود
exportSchema = false
)
abstract class AppDatabase : RoomDatabase() {
@@ -27,7 +29,7 @@ abstract class AppDatabase : RoomDatabase() {
// تمیزکاری قبل از درج بکاپ
val safePersons = backupData.persons.map { p ->
val name = p.name.trim()
- val id = if (p.id.isNotBlank()) p.id else UUID.randomUUID().toString()
+ val id = p.id.ifBlank { UUID.randomUUID().toString() }
Person(id = id, name = name)
}
personDao().deleteAll()
diff --git a/app/src/main/java/com/oqba26/monthlypaymentapp/data/model/PaymentRecord.kt b/app/src/main/java/com/oqba26/monthlypaymentapp/data/model/PaymentRecord.kt
index ba03b54..71c6955 100644
--- a/app/src/main/java/com/oqba26/monthlypaymentapp/data/model/PaymentRecord.kt
+++ b/app/src/main/java/com/oqba26/monthlypaymentapp/data/model/PaymentRecord.kt
@@ -1,3 +1,5 @@
+@file:OptIn(kotlinx.serialization.InternalSerializationApi::class)
+
package com.oqba26.monthlypaymentapp.data.model
import androidx.room.Entity
@@ -12,5 +14,6 @@ data class PaymentRecord(
val amount: Double,
val shamsiYear: Int,
val shamsiMonth: Int,
- val timestamp: Long = System.currentTimeMillis()
+ val timestamp: Long = System.currentTimeMillis(),
+ val description: String = ""
)
\ No newline at end of file
diff --git a/app/src/main/java/com/oqba26/monthlypaymentapp/data/model/Person.kt b/app/src/main/java/com/oqba26/monthlypaymentapp/data/model/Person.kt
index 1078cfd..838bdb5 100644
--- a/app/src/main/java/com/oqba26/monthlypaymentapp/data/model/Person.kt
+++ b/app/src/main/java/com/oqba26/monthlypaymentapp/data/model/Person.kt
@@ -2,14 +2,17 @@ package com.oqba26.monthlypaymentapp.data.model
import androidx.room.Entity
import androidx.room.PrimaryKey
+import kotlinx.serialization.InternalSerializationApi
import kotlinx.serialization.Serializable
-import java.util.UUID
+import kotlin.OptIn // CORRECTED IMPORT
+@OptIn(InternalSerializationApi::class)
@Serializable
@Entity(tableName = "persons")
data class Person(
- @PrimaryKey val id: String = UUID.randomUUID().toString(),
+ @PrimaryKey val id: String = "",
val name: String,
- val userId: String? = null,
- val createdAt: Long = System.currentTimeMillis()
-)
\ No newline at end of file
+ val isArchived: Boolean = false,
+ val displayOrder: Long = 0L,
+ val createdAt: Long = 0L
+)
diff --git a/app/src/main/java/com/oqba26/monthlypaymentapp/data/remote/ApiClient.kt b/app/src/main/java/com/oqba26/monthlypaymentapp/data/remote/ApiClient.kt
index 5fca29d..e0d17b6 100644
--- a/app/src/main/java/com/oqba26/monthlypaymentapp/data/remote/ApiClient.kt
+++ b/app/src/main/java/com/oqba26/monthlypaymentapp/data/remote/ApiClient.kt
@@ -22,8 +22,7 @@ class ApiClient(settingsRepository: SettingsRepository) {
private val authInterceptor = AuthInterceptor(settingsRepository)
private val logging = HttpLoggingInterceptor().apply {
- level = if (BuildConfig.DEBUG) HttpLoggingInterceptor.Level.BODY
- else HttpLoggingInterceptor.Level.NONE
+ level = if (BuildConfig.DEBUG) HttpLoggingInterceptor.Level.BODY else HttpLoggingInterceptor.Level.NONE
}
// ⭐️ افزودن AuthInterceptor به OkHttpClient
diff --git a/app/src/main/java/com/oqba26/monthlypaymentapp/data/remote/ApiService.kt b/app/src/main/java/com/oqba26/monthlypaymentapp/data/remote/ApiService.kt
index 273c0c7..514441d 100644
--- a/app/src/main/java/com/oqba26/monthlypaymentapp/data/remote/ApiService.kt
+++ b/app/src/main/java/com/oqba26/monthlypaymentapp/data/remote/ApiService.kt
@@ -8,6 +8,7 @@ import retrofit2.Response
import retrofit2.http.Body
import retrofit2.http.DELETE
import retrofit2.http.GET
+import retrofit2.http.PATCH
import retrofit2.http.POST
import retrofit2.http.PUT
import retrofit2.http.Path
@@ -33,6 +34,15 @@ interface ApiService {
@PUT("persons/{id}")
suspend fun updatePerson(@Path("id") personId: String, @Body person: Person): Response
+ @PATCH("persons/{id}/archived")
+ suspend fun updatePersonArchivedStatus(@Path("id") personId: String, @Body isArchived: Map): Response
+
+ @PATCH("persons/{id}/display-order")
+ suspend fun updatePersonDisplayOrder(@Path("id") personId: String, @Body displayOrder: Map): Response
+
+ @POST("persons/rebalance")
+ suspend fun rebalanceDisplayOrders(): Response
+
@DELETE("persons/{id}")
suspend fun deletePersonAndPayments(@Path("id") personId: String): Response
diff --git a/app/src/main/java/com/oqba26/monthlypaymentapp/data/remote/AuthInterceptor.kt b/app/src/main/java/com/oqba26/monthlypaymentapp/data/remote/AuthInterceptor.kt
index e48fbb5..75e8644 100644
--- a/app/src/main/java/com/oqba26/monthlypaymentapp/data/remote/AuthInterceptor.kt
+++ b/app/src/main/java/com/oqba26/monthlypaymentapp/data/remote/AuthInterceptor.kt
@@ -1,7 +1,7 @@
package com.oqba26.monthlypaymentapp.data.remote
import com.oqba26.monthlypaymentapp.data.repository.SettingsRepository
-import kotlinx.coroutines.flow.firstOrNull
+import kotlinx.coroutines.flow.first
import kotlinx.coroutines.runBlocking
import okhttp3.Interceptor
import okhttp3.Response
@@ -11,19 +11,20 @@ class AuthInterceptor(
) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
- val originalRequest = chain.request()
-
- // توکن را به صورت Blocking از DataStore میخوانیم
+ // Get the token from DataStore, blocking the current thread.
+ // This is acceptable for an I/O-bound operation in an interceptor.
val authToken = runBlocking {
- settingsRepository.authTokenFlow.firstOrNull()
+ settingsRepository.authTokenFlow.first() // Use first() to wait for a value
}
+ val originalRequest = chain.request()
val requestBuilder = originalRequest.newBuilder()
- if (authToken != null) {
- // اضافه کردن توکن به هدر (فرمت استاندارد Bearer)
+
+ // Add the Authorization header only if the token is not null or blank
+ if (!authToken.isNullOrBlank()) {
requestBuilder.header("Authorization", "Bearer $authToken")
}
return chain.proceed(requestBuilder.build())
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/com/oqba26/monthlypaymentapp/data/repository/NetworkRepository.kt b/app/src/main/java/com/oqba26/monthlypaymentapp/data/repository/NetworkRepository.kt
index 2599026..0ddc4c6 100644
--- a/app/src/main/java/com/oqba26/monthlypaymentapp/data/repository/NetworkRepository.kt
+++ b/app/src/main/java/com/oqba26/monthlypaymentapp/data/repository/NetworkRepository.kt
@@ -45,23 +45,20 @@ class NetworkRepository(private val api: ApiService) {
suspend fun addPerson(person: Person): Int? {
return try {
val trimmed = person.name.trim()
- if (trimmed.isEmpty()) return 400 // Bad request from client side
+ if (trimmed.isEmpty()) return 400
val resp = api.addPerson(person.copy(name = trimmed))
- if (resp.isSuccessful) refresh()
- resp.code() // Return the actual HTTP status code
+ resp.code()
} catch (e: HttpException) {
- e.code() // Return the HTTP error code (e.g., 409)
+ e.code()
} catch (e: Exception) {
Log.e("API", "addPerson error", e)
- null // Return null for non-HTTP errors
+ null
}
}
suspend fun deletePersonAndPayments(personId: String): Boolean {
return try {
- val resp = api.deletePersonAndPayments(personId)
- if (resp.isSuccessful) refresh()
- resp.isSuccessful
+ api.deletePersonAndPayments(personId).isSuccessful
} catch (e: Exception) {
Log.e("API", "deletePersonAndPayments error", e); false
}
@@ -71,23 +68,46 @@ class NetworkRepository(private val api: ApiService) {
return try {
val trimmed = name.trim()
if (trimmed.isEmpty()) return false
- val personUpdate = Person(id = personId, name = trimmed)
- val resp = api.updatePerson(personId, personUpdate)
- if (resp.isSuccessful) refresh()
- resp.isSuccessful
+ // Note: displayOrder is not updated here, only the name.
+ val personUpdate = Person(id = personId, name = trimmed, displayOrder = 0)
+ api.updatePerson(personId, personUpdate).isSuccessful
} catch (e: Exception) {
Log.e("API", "updatePerson error", e); false
}
}
+ suspend fun updatePersonArchivedStatus(personId: String, isArchived: Boolean): Boolean {
+ return try {
+ api.updatePersonArchivedStatus(personId, mapOf("isArchived" to isArchived)).isSuccessful
+ } catch (e: Exception) {
+ Log.e("API", "updatePersonArchivedStatus error", e); false
+ }
+ }
+
+ suspend fun updatePersonDisplayOrder(personId: String, displayOrder: Long): Boolean {
+ return try {
+ api.updatePersonDisplayOrder(personId, mapOf("displayOrder" to displayOrder)).isSuccessful
+ } catch (e: Exception) {
+ Log.e("API", "updatePersonDisplayOrder error", e); false
+ }
+ }
+
+ @Suppress("unused")
+ suspend fun rebalanceDisplayOrders(): Boolean {
+ return try {
+ api.rebalanceDisplayOrders().isSuccessful
+ } catch (e: Exception) {
+ Log.e("API", "rebalanceDisplayOrders error", e)
+ false
+ }
+ }
+
fun getPersonsFlow(): Flow> = _personsFlow.asStateFlow()
// --- Payment Operations ---
suspend fun addPayment(paymentRecord: PaymentRecord): Boolean {
return try {
- val resp = api.addPayment(paymentRecord)
- if (resp.isSuccessful) refresh()
- resp.isSuccessful
+ api.addPayment(paymentRecord).isSuccessful
} catch (e: Exception) {
Log.e("API", "addPayment error", e); false
}
@@ -95,9 +115,7 @@ class NetworkRepository(private val api: ApiService) {
suspend fun deletePayment(paymentId: String): Boolean {
return try {
- val resp = api.deletePayment(paymentId)
- if (resp.isSuccessful) refresh()
- resp.isSuccessful
+ api.deletePayment(paymentId).isSuccessful
} catch (e: Exception) {
Log.e("API", "deletePayment error", e); false
}
diff --git a/app/src/main/java/com/oqba26/monthlypaymentapp/data/repository/SettingsRepository.kt b/app/src/main/java/com/oqba26/monthlypaymentapp/data/repository/SettingsRepository.kt
index ff7d943..c71b034 100644
--- a/app/src/main/java/com/oqba26/monthlypaymentapp/data/repository/SettingsRepository.kt
+++ b/app/src/main/java/com/oqba26/monthlypaymentapp/data/repository/SettingsRepository.kt
@@ -19,7 +19,6 @@ class SettingsRepository(context: Context) {
companion object {
val DEFAULT_PAYMENT_AMOUNT = doublePreferencesKey("default_payment_amount")
val SELECTED_FONT_NAME = stringPreferencesKey("selected_font_name")
- // ⭐️ جدید: کلیدهای مربوط به احراز هویت
val AUTH_TOKEN_KEY = stringPreferencesKey("auth_token")
val USER_ID_KEY = stringPreferencesKey("user_id")
@@ -27,7 +26,6 @@ class SettingsRepository(context: Context) {
const val DEFAULT_FONT = "Estedad"
}
- // --- جریانهای تنظیمات عمومی ---
val defaultPaymentAmountFlow: Flow = dataStore.data.map {
it[DEFAULT_PAYMENT_AMOUNT] ?: FALLBACK_AMOUNT
}
@@ -44,19 +42,15 @@ class SettingsRepository(context: Context) {
dataStore.edit { it[SELECTED_FONT_NAME] = fontName }
}
-
- // --- جریانها و متدهای مربوط به احراز هویت ---
val authTokenFlow: Flow = dataStore.data.map {
it[AUTH_TOKEN_KEY]
}
+ @Suppress("unused")
val userIdFlow: Flow = dataStore.data.map {
it[USER_ID_KEY]
}
- /**
- * توکن و ID کاربر را ذخیره میکند. برای پاک کردن، null ارسال کنید.
- */
suspend fun saveAuthData(token: String?, userId: String?) {
dataStore.edit { preferences ->
if (token != null) {
diff --git a/app/src/main/java/com/oqba26/monthlypaymentapp/ui/components/AddPaymentDialog.kt b/app/src/main/java/com/oqba26/monthlypaymentapp/ui/components/AddPaymentDialog.kt
deleted file mode 100644
index c62c0ee..0000000
--- a/app/src/main/java/com/oqba26/monthlypaymentapp/ui/components/AddPaymentDialog.kt
+++ /dev/null
@@ -1,82 +0,0 @@
-package com.oqba26.monthlypaymentapp.ui.components
-
-import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.padding
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.OutlinedTextField
-import androidx.compose.material3.Surface
-import androidx.compose.material3.Text
-import androidx.compose.material3.TextButton
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.text.style.TextAlign
-import androidx.compose.ui.unit.dp
-import androidx.compose.ui.window.Dialog
-
-@Composable
-fun AddPaymentDialog(
- onDismiss: () -> Unit,
- onConfirm: (Double, String) -> Unit
-) {
- var amountText by remember { mutableStateOf("") }
- var description by remember { mutableStateOf("") }
-
- Dialog(onDismissRequest = onDismiss) {
- Surface(shape = MaterialTheme.shapes.medium) {
- Column(modifier = Modifier.padding(16.dp)) {
- Text(
- text = "افزودن پرداخت",
- style = MaterialTheme.typography.titleMedium,
- textAlign = TextAlign.Right,
- modifier = Modifier.fillMaxWidth()
- )
-
- Spacer(modifier = Modifier.height(16.dp))
-
- OutlinedTextField(
- value = amountText,
- onValueChange = { amountText = it },
- label = { Text("مبلغ (تومان)") },
- modifier = Modifier.fillMaxWidth()
- )
-
- Spacer(modifier = Modifier.height(8.dp))
-
- OutlinedTextField(
- value = description,
- onValueChange = { description = it },
- label = { Text("توضیحات") },
- modifier = Modifier.fillMaxWidth()
- )
-
- Spacer(modifier = Modifier.height(16.dp))
-
- Row(
- horizontalArrangement = Arrangement.End,
- modifier = Modifier.fillMaxWidth()
- ) {
- TextButton(onClick = onDismiss) {
- Text("لغو")
- }
- TextButton(onClick = {
- val amount = amountText.toDoubleOrNull() ?: 0.0
- if (amount > 0 && description.isNotBlank()) {
- onConfirm(amount, description.trim())
- }
- }) {
- Text("تأیید")
- }
- }
- }
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/oqba26/monthlypaymentapp/ui/screens/ArchiveScreen.kt b/app/src/main/java/com/oqba26/monthlypaymentapp/ui/screens/ArchiveScreen.kt
new file mode 100644
index 0000000..dafdecf
--- /dev/null
+++ b/app/src/main/java/com/oqba26/monthlypaymentapp/ui/screens/ArchiveScreen.kt
@@ -0,0 +1,91 @@
+package com.oqba26.monthlypaymentapp.ui.screens
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Restore
+import androidx.compose.material3.Button
+import androidx.compose.material3.ButtonDefaults
+import androidx.compose.material3.HorizontalDivider
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.dp
+import com.oqba26.monthlypaymentapp.viewmodel.PersonScreenEvent
+import com.oqba26.monthlypaymentapp.viewmodel.PersonUiModel
+import com.oqba26.monthlypaymentapp.viewmodel.PersonViewModel
+
+@Composable
+fun ArchiveScreen(viewModel: PersonViewModel) {
+ val uiState by viewModel.uiState.collectAsState()
+
+ if (uiState.archivedPersons.isEmpty()) {
+ Box(
+ modifier = Modifier.fillMaxSize(),
+ contentAlignment = Alignment.Center
+ ) {
+ Text("لیست آرشیو خالی است.")
+ }
+ } else {
+ LazyColumn(
+ modifier = Modifier.fillMaxSize(),
+ contentPadding = PaddingValues(vertical = 8.dp)
+ ) {
+ items(uiState.archivedPersons, key = { it.id }) { person ->
+ ArchivedPersonListItem(
+ person = person,
+ onRestore = {
+ viewModel.onEvent(PersonScreenEvent.RestorePerson(person.id))
+ }
+ )
+ HorizontalDivider()
+ }
+ }
+ }
+}
+
+@Composable
+fun ArchivedPersonListItem(
+ person: PersonUiModel,
+ onRestore: () -> Unit
+) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 16.dp, vertical = 12.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.SpaceBetween
+ ) {
+ Text(
+ text = person.name,
+ style = MaterialTheme.typography.bodyLarge,
+ fontWeight = FontWeight.Bold
+ )
+
+ Button(
+ onClick = onRestore,
+ colors = ButtonDefaults.buttonColors(
+ containerColor = MaterialTheme.colorScheme.primary
+ )
+ ) {
+ Icon(Icons.Default.Restore, contentDescription = "بازیابی")
+ Spacer(Modifier.width(4.dp))
+ Text("بازیابی")
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/oqba26/monthlypaymentapp/ui/screens/PaidListScreen.kt b/app/src/main/java/com/oqba26/monthlypaymentapp/ui/screens/PaidListScreen.kt
new file mode 100644
index 0000000..2aeb0fe
--- /dev/null
+++ b/app/src/main/java/com/oqba26/monthlypaymentapp/ui/screens/PaidListScreen.kt
@@ -0,0 +1,169 @@
+package com.oqba26.monthlypaymentapp.ui.screens
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.itemsIndexed
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Search
+import androidx.compose.material3.AlertDialog
+import androidx.compose.material3.Button
+import androidx.compose.material3.ButtonDefaults
+import androidx.compose.material3.HorizontalDivider
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.OutlinedTextField
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import androidx.navigation.NavController
+import com.oqba26.monthlypaymentapp.viewmodel.PersonListType
+import com.oqba26.monthlypaymentapp.viewmodel.PersonScreenEvent
+import com.oqba26.monthlypaymentapp.viewmodel.PersonUiModel
+import com.oqba26.monthlypaymentapp.viewmodel.PersonViewModel
+import org.burnoutcrew.reorderable.ReorderableItem
+import org.burnoutcrew.reorderable.detectReorderAfterLongPress
+import org.burnoutcrew.reorderable.rememberReorderableLazyListState
+import org.burnoutcrew.reorderable.reorderable
+
+@Composable
+fun PaidListScreen(viewModel: PersonViewModel, navController: NavController) {
+ val uiState by viewModel.uiState.collectAsState()
+ val searchQuery by viewModel.searchQuery.collectAsState()
+
+ // شخصی که قرار است در این صفحه با تأیید کاربر به آرشیو منتقل شود
+ val personToArchive = remember { mutableStateOf(null) }
+
+ val state = rememberReorderableLazyListState(
+ onMove = { from, to ->
+ if (searchQuery.isBlank()) {
+ viewModel.onEvent(
+ PersonScreenEvent.MovePersonNew(
+ fromIndex = from.index,
+ toIndex = to.index,
+ listType = PersonListType.PAID
+ )
+ )
+ }
+ },
+ onDragEnd = { _, _ ->
+ if (searchQuery.isBlank()) {
+ viewModel.onEvent(
+ PersonScreenEvent.CommitReorder(
+ listType = PersonListType.PAID
+ )
+ )
+ }
+ }
+ )
+
+ Scaffold { innerPadding ->
+ Column(modifier = Modifier.padding(innerPadding)) {
+
+ OutlinedTextField(
+ value = searchQuery,
+ onValueChange = { viewModel.onSearchQueryChange(it) },
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 16.dp, vertical = 8.dp),
+ label = { Text("جستجوی نام...") },
+ leadingIcon = { Icon(Icons.Filled.Search, contentDescription = "Search") },
+ singleLine = true
+ )
+
+ LazyColumn(
+ state = state.listState,
+ modifier = Modifier.reorderable(state)
+ ) {
+ itemsIndexed(
+ uiState.paidPersons,
+ key = { _, person -> person.id }
+ ) { index, person ->
+ ReorderableItem(state, key = person.id) { _ ->
+
+ val rowDragModifier =
+ if (searchQuery.isBlank()) {
+ Modifier.detectReorderAfterLongPress(state)
+ } else {
+ Modifier
+ }
+
+ Row(
+ modifier = rowDragModifier
+ .fillMaxWidth(),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Box(modifier = Modifier.weight(1f)) {
+ PersonListItem(
+ person = person,
+ index = index + 1,
+ onPersonClick = {
+ navController.navigate("person_detail/${person.id}")
+ },
+ // در لیست پرداخت شدهها، دکمه پرداخت سریع کاری نمیکند
+ onQuickPayClick = { },
+ onArchiveClick = { selectedPerson ->
+ personToArchive.value = selectedPerson
+ }
+ )
+ }
+ }
+
+ HorizontalDivider(
+ color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.1f)
+ )
+ }
+ }
+ }
+ }
+
+ // دیالوگ تأیید انتقال به آرشیو برای لیست پرداختشدهها
+ personToArchive.value?.let { person ->
+ AlertDialog(
+ onDismissRequest = { personToArchive.value = null },
+ title = { Text("انتقال به آرشیو") },
+ text = { Text("آیا از انتقال ${person.name} به آرشیو مطمئن هستید؟") },
+ confirmButton = {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 8.dp, vertical = 4.dp),
+ horizontalArrangement = Arrangement.SpaceBetween
+ ) {
+ // در RTL: این دکمه کاملاً به سمت راست دیالوگ میچسبد
+ Button(onClick = { personToArchive.value = null }) {
+ Text("لغو")
+ }
+
+ // در RTL: این دکمه کاملاً به سمت چپ دیالوگ میچسبد
+ Button(
+ onClick = {
+ viewModel.onEvent(
+ PersonScreenEvent.ArchivePerson(person.id)
+ )
+ personToArchive.value = null
+ },
+ colors = ButtonDefaults.buttonColors(
+ containerColor = MaterialTheme.colorScheme.secondary
+ )
+ ) {
+ Text("آرشیو")
+ }
+ }
+ },
+ dismissButton = {}
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/oqba26/monthlypaymentapp/ui/screens/PersonDetailScreen.kt b/app/src/main/java/com/oqba26/monthlypaymentapp/ui/screens/PersonDetailScreen.kt
index 6462701..8698166 100644
--- a/app/src/main/java/com/oqba26/monthlypaymentapp/ui/screens/PersonDetailScreen.kt
+++ b/app/src/main/java/com/oqba26/monthlypaymentapp/ui/screens/PersonDetailScreen.kt
@@ -1,430 +1,446 @@
+@file:Suppress("AssignedValueIsNeverRead")
+
package com.oqba26.monthlypaymentapp.ui.screens
-import android.widget.Toast
-import androidx.compose.animation.AnimatedContent
-import androidx.compose.animation.animateColorAsState
-import androidx.compose.animation.core.tween
-import androidx.compose.animation.fadeIn
-import androidx.compose.animation.fadeOut
-import androidx.compose.animation.togetherWith
-import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
-import androidx.compose.foundation.lazy.itemsIndexed
-import androidx.compose.foundation.text.KeyboardOptions
+import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
-import androidx.compose.material.icons.automirrored.filled.KeyboardArrowLeft
-import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight
-import androidx.compose.material.icons.filled.Add
-import androidx.compose.material.icons.filled.Check
-import androidx.compose.material.icons.filled.Close
+import androidx.compose.material.icons.filled.ChevronLeft
+import androidx.compose.material.icons.filled.ChevronRight
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
-import androidx.compose.material3.ExperimentalMaterial3Api
-import androidx.compose.material3.HorizontalDivider
+import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.OutlinedTextField
-import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
-import androidx.compose.material3.TopAppBar
-import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight
-import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp
-import androidx.lifecycle.viewmodel.compose.viewModel
+import androidx.navigation.NavController
import com.oqba26.monthlypaymentapp.data.model.PaymentRecord
-import com.oqba26.monthlypaymentapp.utils.PersianNumberVisualTransformation
import com.oqba26.monthlypaymentapp.utils.formatNumberAsPersian
-import com.oqba26.monthlypaymentapp.utils.getCurrentShamsiMonth
-import com.oqba26.monthlypaymentapp.utils.getCurrentShamsiYear
+import com.oqba26.monthlypaymentapp.utils.formatTimestampToPersianDate
+import com.oqba26.monthlypaymentapp.utils.formatTimestampToPersianTime
+import com.oqba26.monthlypaymentapp.utils.getPersianMonthName
import com.oqba26.monthlypaymentapp.utils.toPersianDigits
import com.oqba26.monthlypaymentapp.viewmodel.MonthStatus
-import com.oqba26.monthlypaymentapp.viewmodel.PersonDetailViewModel
-import kotlinx.coroutines.flow.collectLatest
+import com.oqba26.monthlypaymentapp.viewmodel.MonthUiModel
+import com.oqba26.monthlypaymentapp.viewmodel.PersonScreenEvent
+import com.oqba26.monthlypaymentapp.viewmodel.PersonViewModel
-@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun PersonDetailScreen(
- onBackClick: () -> Unit
+ personId: String,
+ viewModel: PersonViewModel,
+ navController: NavController
) {
- val viewModel: PersonDetailViewModel = viewModel(factory = PersonDetailViewModel.Factory)
- val context = LocalContext.current
- val person by viewModel.person.collectAsState()
- val selectedYear by viewModel.selectedYear.collectAsState()
- val paymentHistory by viewModel.paymentHistory.collectAsState()
- val defaultPaymentAmount by viewModel.defaultPaymentAmount.collectAsState()
+ LaunchedEffect(personId) {
+ viewModel.onEvent(PersonScreenEvent.SelectPerson(personId))
+ }
- var paymentToEdit by remember { mutableStateOf(null) }
- var showAddPaymentDialog by remember { mutableStateOf(false) }
- var showEditPaymentDialog by remember { mutableStateOf(false) }
- var showDeleteConfirmDialog by remember { mutableStateOf(false) }
- var showMessageDialog by remember { mutableStateOf(false) }
- var dialogMessage by remember { mutableStateOf("") }
+ val detailState by viewModel.personDetailState.collectAsState()
+ val defaultPaymentAmount by viewModel.defaultPaymentAmountFlow.collectAsState(initial = 0.0)
- var paymentAmount by remember { mutableStateOf("") }
- var selectedMonthForPayment by remember { mutableIntStateOf(1) }
+ var expandedMonth by remember { mutableStateOf(null) }
+ var paymentDialogState by remember { mutableStateOf(null) }
+ var infoDialogMessage by remember { mutableStateOf(null) }
+ var paymentToDelete by remember { mutableStateOf(null) }
- val currentYear = getCurrentShamsiYear()
- val currentMonth = getCurrentShamsiMonth()
- val persianMonths = listOf("فروردین", "اردیبهشت", "خرداد", "تیر", "مرداد", "شهریور", "مهر", "آبان", "آذر", "دی", "بهمن", "اسفند")
-
- LaunchedEffect(key1 = true) {
- viewModel.toastMessage.collectLatest {
- Toast.makeText(context, it, Toast.LENGTH_SHORT).show()
+ LaunchedEffect(Unit) {
+ viewModel.infoMessage.collect { message ->
+ infoDialogMessage = message
}
}
- Scaffold(
- topBar = {
- TopAppBar(
- title = { Text(person?.name ?: "...") },
- navigationIcon = { IconButton(onClick = onBackClick) { Icon(Icons.AutoMirrored.Filled.ArrowBack, "Back") } },
- colors = TopAppBarDefaults.topAppBarColors(
- containerColor = MaterialTheme.colorScheme.primaryContainer,
- titleContentColor = MaterialTheme.colorScheme.onPrimaryContainer,
- navigationIconContentColor = MaterialTheme.colorScheme.onPrimaryContainer
- )
- )
- }
- ) { innerPadding ->
- Column(modifier = Modifier.padding(innerPadding).padding(top = 16.dp)) {
- YearNavigator(
- modifier = Modifier.padding(horizontal = 16.dp),
- viewModel = viewModel,
- selectedYear = selectedYear,
- paymentHistory = paymentHistory,
- currentYear = currentYear,
- showMessage = { msg ->
- dialogMessage = msg
- showMessageDialog = true
+ Column(
+ modifier = Modifier.fillMaxSize()
+ ) {
+ PersonDetailHeader(
+ name = detailState.person?.name ?: "...",
+ onBack = { navController.popBackStack() }
+ )
+
+ if (detailState.person == null) {
+ Box(
+ modifier = Modifier.fillMaxSize(),
+ contentAlignment = Alignment.Center
+ ) {
+ CircularProgressIndicator()
+ }
+ } else {
+ YearSelector(
+ year = detailState.selectedYear,
+ onYearChange = { offset ->
+ viewModel.onEvent(PersonScreenEvent.ChangeYear(offset))
}
)
- Spacer(Modifier.height(20.dp))
- MonthsList(
- paymentHistory = paymentHistory,
- selectedYear = selectedYear,
- currentYear = currentYear,
- currentMonth = currentMonth,
- persianMonths = persianMonths,
- onAvailableClick = { month, _ ->
- selectedMonthForPayment = month
- paymentAmount = defaultPaymentAmount.toLong().toString()
- showAddPaymentDialog = true
- },
- onPaidClick = { payment ->
- paymentToEdit = payment
- paymentAmount = payment.amount.toLong().toString()
- showEditPaymentDialog = true
- },
- onDisabledClick = { message ->
- dialogMessage = message
- showMessageDialog = true
+
+ Spacer(modifier = Modifier.height(4.dp))
+
+ LazyColumn(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(bottom = 8.dp)
+ ) {
+ items(detailState.monthStates) { monthModel ->
+ MonthListItem(
+ month = monthModel,
+ isExpanded = expandedMonth == monthModel.month,
+ onHeaderClick = {
+ expandedMonth =
+ if (expandedMonth == monthModel.month) null else monthModel.month
+ },
+ onEditClick = {
+ when (monthModel.status) {
+ MonthStatus.PAID -> {
+ paymentDialogState =
+ PaymentDialogState.Edit(monthModel.payment!!)
+ }
+
+ MonthStatus.AVAILABLE -> {
+ paymentDialogState =
+ PaymentDialogState.Add(monthModel.month)
+ }
+
+ else -> { /* Do nothing for other states */ }
+ }
+ }
+ )
}
- )
+ }
}
}
- if (showAddPaymentDialog) {
- AddPaymentDialog(
- monthName = persianMonths[selectedMonthForPayment - 1],
- year = selectedYear,
- amount = paymentAmount,
- onAmountChange = { paymentAmount = it.filter { c -> c.isDigit() } },
+ infoDialogMessage?.let {
+ InfoDialog(message = it, onDismiss = { infoDialogMessage = null })
+ }
+
+ paymentToDelete?.let { payment ->
+ DeleteConfirmationDialog(
+ payment = payment,
onConfirm = {
- val amount = paymentAmount.toDoubleOrNull()
- if (amount != null && amount > 0) {
- viewModel.addPaymentForMonth(selectedMonthForPayment, selectedYear, amount)
- showAddPaymentDialog = false
- }
+ viewModel.onEvent(PersonScreenEvent.DeletePayment(payment))
+ paymentToDelete = null
},
- onDismiss = { showAddPaymentDialog = false }
+ onDismiss = { paymentToDelete = null }
)
}
- paymentToEdit?.let { payment ->
- if (showEditPaymentDialog) {
- EditPaymentDialog(
- amount = paymentAmount,
- onAmountChange = { paymentAmount = it.filter { c -> c.isDigit() } },
- onSave = {
- val amount = paymentAmount.toDoubleOrNull()
- if (amount != null && amount > 0) {
- viewModel.updatePayment(payment, amount)
- showEditPaymentDialog = false
+ paymentDialogState?.let { state ->
+ when (state) {
+ is PaymentDialogState.Add -> {
+ AddPaymentDialog(
+ personName = detailState.person!!.name,
+ defaultAmount = defaultPaymentAmount,
+ onConfirm = { amount, description ->
+ viewModel.onEvent(
+ PersonScreenEvent.AddPaymentForMonth(
+ personId = personId,
+ month = state.month,
+ year = detailState.selectedYear,
+ amount = amount,
+ description = description
+ )
+ )
+ paymentDialogState = null
+ },
+ onDismiss = { paymentDialogState = null }
+ )
+ }
+
+ is PaymentDialogState.Edit -> {
+ val payment = state.payment
+ AddPaymentDialog(
+ personName = detailState.person!!.name,
+ defaultAmount = payment.amount,
+ initialDescription = payment.description,
+ onConfirm = { amount, description ->
+ viewModel.onEvent(
+ PersonScreenEvent.UpdatePayment(
+ payment = payment,
+ newAmount = amount,
+ newDescription = description
+ )
+ )
+ paymentDialogState = null
+ },
+ onDismiss = { paymentDialogState = null },
+ onDelete = {
+ paymentToDelete = payment
+ paymentDialogState = null
}
- },
- onDelete = {
- showEditPaymentDialog = false
- showDeleteConfirmDialog = true
- },
- onDismiss = { showEditPaymentDialog = false }
- )
+ )
+ }
}
-
- if (showDeleteConfirmDialog) {
- DeleteConfirmDialog(
- onConfirm = {
- viewModel.deletePayment(payment)
- showDeleteConfirmDialog = false
- },
- onDismiss = { showDeleteConfirmDialog = false }
- )
- }
- }
-
- if (showMessageDialog) {
- MessageDialog(message = dialogMessage) { showMessageDialog = false }
}
}
@Composable
-private fun YearNavigator(
- modifier: Modifier = Modifier,
- viewModel: PersonDetailViewModel,
- selectedYear: Int,
- paymentHistory: List,
- currentYear: Int,
- showMessage: (String) -> Unit
-) {
- Card(modifier = modifier.fillMaxWidth(), colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.primaryContainer)) {
+private fun PersonDetailHeader(name: String, onBack: () -> Unit) {
+ Surface(
+ color = MaterialTheme.colorScheme.primary,
+ contentColor = Color.White
+ ) {
Row(
- modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 8.dp),
- horizontalArrangement = Arrangement.SpaceBetween,
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(44.dp)
+ .padding(horizontal = 8.dp),
verticalAlignment = Alignment.CenterVertically
) {
- IconButton(onClick = {
- val hasData = paymentHistory.any { it.shamsiYear == selectedYear - 1 }
- if (hasData || selectedYear - 1 >= currentYear - 5) { // Allow going back 5 years
- viewModel.changeYear(-1)
- } else {
- showMessage("هیچ دادهای برای سال قبل وجود ندارد")
- }
- }) { Icon(Icons.AutoMirrored.Filled.KeyboardArrowLeft, "Previous Year") }
- Text("سال ${selectedYear.toString().toPersianDigits()}", style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.onPrimaryContainer)
- IconButton(onClick = {
- if (selectedYear < currentYear) {
- viewModel.changeYear(1)
- } else {
- showMessage("نمیتوان به سال آینده رفت")
- }
- }) { Icon(Icons.AutoMirrored.Filled.KeyboardArrowRight, "Next Year") }
+ IconButton(onClick = onBack) {
+ Icon(
+ imageVector = Icons.AutoMirrored.Filled.ArrowBack,
+ contentDescription = "بازگشت"
+ )
+ }
+ Spacer(modifier = Modifier.padding(horizontal = 4.dp))
+ Text(
+ text = name,
+ style = MaterialTheme.typography.titleMedium,
+ fontWeight = FontWeight.Bold
+ )
}
}
}
@Composable
-private fun MonthsList(
- paymentHistory: List,
- selectedYear: Int,
- currentYear: Int,
- currentMonth: Int,
- persianMonths: List,
- onAvailableClick: (month: Int, year: Int) -> Unit,
- onPaidClick: (payment: PaymentRecord) -> Unit,
- onDisabledClick: (message: String) -> Unit
-) {
- LazyColumn {
- itemsIndexed(persianMonths, key = { index, _ -> index }) { index, monthName ->
- val monthNumber = index + 1
- val payment = paymentHistory.find { p -> p.shamsiMonth == monthNumber && p.shamsiYear == selectedYear }
-
- val monthStatus = when {
- payment != null -> MonthStatus.PAID
- selectedYear > currentYear -> MonthStatus.FUTURE_YEAR
- selectedYear < currentYear -> MonthStatus.PAST_YEAR
- monthNumber > currentMonth -> MonthStatus.FUTURE_MONTH
- else -> MonthStatus.AVAILABLE
+fun YearSelector(year: Int, onYearChange: (Int) -> Unit) {
+ Surface(
+ color = MaterialTheme.colorScheme.surfaceVariant,
+ tonalElevation = 1.dp,
+ modifier = Modifier.fillMaxWidth()
+ ) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(vertical = 6.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.Center
+ ) {
+ IconButton(onClick = { onYearChange(-1) }) {
+ Icon(
+ imageVector = Icons.Default.ChevronRight,
+ contentDescription = "سال قبل"
+ )
}
- MonthListItem(
- monthName = monthName,
- payment = payment,
- status = monthStatus,
- onItemClick = {
- when (monthStatus) {
- MonthStatus.AVAILABLE -> onAvailableClick(monthNumber, selectedYear)
- MonthStatus.PAID -> onPaidClick(payment!!)
- else -> onDisabledClick(getMessageForStatus(monthStatus, monthName))
- }
- }
+ Text(
+ text = "سال ${year.toString().toPersianDigits()}",
+ style = MaterialTheme.typography.titleLarge,
+ fontWeight = FontWeight.Bold,
+ modifier = Modifier.padding(horizontal = 12.dp)
)
- HorizontalDivider(color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.1f))
+
+ IconButton(onClick = { onYearChange(1) }) {
+ Icon(
+ imageVector = Icons.Default.ChevronLeft,
+ contentDescription = "سال بعد"
+ )
+ }
}
}
}
+@Suppress("UNUSED_PARAMETER")
@Composable
-private fun MonthListItem(
- monthName: String,
- payment: PaymentRecord?,
- status: MonthStatus,
- onItemClick: () -> Unit
+fun MonthListItem(
+ month: MonthUiModel,
+ isExpanded: Boolean, // فعلاً استفاده نمیشود
+ onHeaderClick: () -> Unit,
+ onEditClick: () -> Unit
) {
- val isClickable = status == MonthStatus.AVAILABLE || status == MonthStatus.PAID
- val backgroundColor by animateColorAsState(
- targetValue = if (status == MonthStatus.PAID) MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.1f) else Color.Transparent,
- animationSpec = tween(400),
- label = "MonthListItemColor"
- )
+ val containerColor = when (month.status) {
+ MonthStatus.PAID -> MaterialTheme.colorScheme.primaryContainer
+ MonthStatus.AVAILABLE -> MaterialTheme.colorScheme.surface
+ MonthStatus.PAST_YEAR, MonthStatus.FUTURE_YEAR, MonthStatus.FUTURE_MONTH ->
+ MaterialTheme.colorScheme.surfaceVariant
+ }
- Row(
+ Card(
modifier = Modifier
+ .padding(horizontal = 16.dp, vertical = 4.dp)
.fillMaxWidth()
- .clickable(enabled = isClickable, onClick = onItemClick)
- .background(backgroundColor)
- .padding(horizontal = 16.dp, vertical = 20.dp),
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.SpaceBetween
+ .clickable(onClick = onHeaderClick),
+ colors = CardDefaults.cardColors(containerColor = containerColor),
+ elevation = CardDefaults.cardElevation(defaultElevation = 1.dp)
) {
- Text(text = monthName, style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold)
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 12.dp, vertical = 10.dp)
+ ) {
+ // ردیف بالا: نام ماه + مبلغ + دکمه
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.SpaceBetween
+ ) {
+ // نام ماه + مبلغ در صورت پرداخت
+ Row(
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Text(
+ text = getPersianMonthName(month.month),
+ style = MaterialTheme.typography.titleMedium,
+ fontWeight = FontWeight.Bold
+ )
- AnimatedContent(
- targetState = status,
- transitionSpec = { fadeIn(tween(400)) togetherWith fadeOut(tween(400)) },
- label = "MonthListItemContent"
- ) { targetStatus ->
- when (targetStatus) {
- MonthStatus.PAID -> {
- payment?.let {
- Row(verticalAlignment = Alignment.CenterVertically) {
- Text(
- text = "${formatNumberAsPersian(it.amount)} تومان",
- style = MaterialTheme.typography.bodyMedium,
- color = MaterialTheme.colorScheme.primary,
- fontWeight = FontWeight.SemiBold
+ if (month.status == MonthStatus.PAID && month.payment != null) {
+ Spacer(modifier = Modifier.width(8.dp))
+ Text(
+ text = "${formatNumberAsPersian(month.payment.amount)} تومان",
+ style = MaterialTheme.typography.bodyMedium
+ )
+ }
+ }
+
+ // دکمه سمت چپ
+ when (month.status) {
+ MonthStatus.PAID -> {
+ Button(
+ onClick = onEditClick,
+ colors = ButtonDefaults.buttonColors(
+ containerColor = MaterialTheme.colorScheme.primary
)
- Spacer(Modifier.width(8.dp))
- Icon(Icons.Filled.Check, contentDescription = "Paid", tint = MaterialTheme.colorScheme.primary)
+ ) {
+ Text("ویرایش")
}
}
+
+ MonthStatus.AVAILABLE -> {
+ Button(
+ onClick = onEditClick,
+ colors = ButtonDefaults.buttonColors(
+ containerColor = MaterialTheme.colorScheme.secondary
+ )
+ ) {
+ Text("پرداخت")
+ }
+ }
+
+ else -> {
+ Text(
+ text = "غیرفعال",
+ style = MaterialTheme.typography.bodySmall,
+ color = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+ }
}
- MonthStatus.AVAILABLE -> {
- Icon(Icons.Filled.Add, contentDescription = "Add Payment", tint = MaterialTheme.colorScheme.secondary)
- }
- else -> {
- Icon(Icons.Filled.Close, contentDescription = "Disabled", tint = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.3f))
+ }
+
+ // جزئیات پرداخت: تاریخ + (در صورت وجود) توضیحات
+ month.payment?.let { payment ->
+ Spacer(modifier = Modifier.height(10.dp))
+
+ Text(
+ text = "ثبت شده در: " +
+ "${formatTimestampToPersianDate(payment.timestamp)} " +
+ "ساعت ${formatTimestampToPersianTime(payment.timestamp)}",
+ style = MaterialTheme.typography.bodySmall,
+ color = MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.8f)
+ )
+
+ if (payment.description.isNotBlank()) {
+ Spacer(modifier = Modifier.height(4.dp))
+ Text(
+ text = "توضیحات: ${payment.description}",
+ style = MaterialTheme.typography.bodyMedium,
+ color = MaterialTheme.colorScheme.onPrimaryContainer
+ )
}
}
}
}
}
-private fun getMessageForStatus(status: MonthStatus, monthName: String): String {
- return when (status) {
- MonthStatus.FUTURE_YEAR -> "این سال هنوز نرسیده است."
- MonthStatus.PAST_YEAR -> "شما در سال گذشته برای ماه $monthName پرداختی ثبت نکردهاید."
- MonthStatus.FUTURE_MONTH -> "این ماه هنوز نرسیده است."
- else -> ""
- }
-}
-
@Composable
-private fun AddPaymentDialog(
- monthName: String,
- year: Int,
- amount: String,
- onAmountChange: (String) -> Unit,
+fun DeleteConfirmationDialog(
+ payment: PaymentRecord,
onConfirm: () -> Unit,
onDismiss: () -> Unit
) {
AlertDialog(
onDismissRequest = onDismiss,
- title = { Text("ثبت پرداخت برای $monthName ${year.toString().toPersianDigits()}") },
+ title = { Text("تایید حذف") },
text = {
- OutlinedTextField(
- value = amount,
- onValueChange = onAmountChange,
- label = { Text("مبلغ (تومان)") },
- visualTransformation = PersianNumberVisualTransformation(),
- keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
- modifier = Modifier.fillMaxWidth()
- )
- },
- confirmButton = { Button(onClick = onConfirm) { Text("ثبت") } },
- dismissButton = { TextButton(onClick = onDismiss) { Text("لغو") } }
- )
-}
-
-@Composable
-private fun EditPaymentDialog(
- amount: String,
- onAmountChange: (String) -> Unit,
- onSave: () -> Unit,
- onDelete: () -> Unit,
- onDismiss: () -> Unit
-) {
- AlertDialog(
- onDismissRequest = onDismiss,
- title = { Text("ویرایش یا حذف پرداخت") },
- text = {
- OutlinedTextField(
- value = amount,
- onValueChange = onAmountChange,
- label = { Text("مبلغ جدید (تومان)") },
- visualTransformation = PersianNumberVisualTransformation(),
- keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
- modifier = Modifier.fillMaxWidth()
+ Text(
+ "آیا برای حذف پرداخت ماه ${
+ getPersianMonthName(payment.shamsiMonth)
+ } به مبلغ ${
+ formatNumberAsPersian(payment.amount)
+ } تومان مطمئن هستید؟"
)
},
confirmButton = {
- Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.End) {
- Button(onClick = onSave) { Text("ذخیره") }
- Spacer(Modifier.width(8.dp))
- TextButton(onClick = onDismiss) { Text("لغو") }
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 8.dp, vertical = 4.dp),
+ horizontalArrangement = Arrangement.SpaceBetween
+ ) {
+ // در RTL: این دکمه میچسبد به سمت راست دیالوگ
+ Button(onClick = onDismiss) {
+ Text("لغو")
+ }
+
+ // در RTL: این دکمه میچسبد به سمت چپ دیالوگ
+ Button(
+ onClick = onConfirm,
+ colors = ButtonDefaults.buttonColors(
+ containerColor = MaterialTheme.colorScheme.error
+ )
+ ) {
+ Text("حذف")
+ }
}
},
- dismissButton = {
- TextButton(onClick = onDelete, colors = ButtonDefaults.textButtonColors(contentColor = MaterialTheme.colorScheme.error)) { Text("حذف") }
- }
+ dismissButton = {}
)
}
@Composable
-private fun DeleteConfirmDialog(onConfirm: () -> Unit, onDismiss: () -> Unit) {
- AlertDialog(
- onDismissRequest = onDismiss,
- title = { Text("تایید حذف") },
- text = { Text("آیا از حذف این پرداخت مطمئن هستید؟ این عمل غیرقابل بازگشت است.") },
- confirmButton = {
- Button(onClick = onConfirm, colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.error)) { Text("حذف کن") }
- },
- dismissButton = { TextButton(onClick = onDismiss) { Text("لغو") } }
- )
-}
-
-@Composable
-private fun MessageDialog(message: String, onDismiss: () -> Unit) {
+fun InfoDialog(message: String, onDismiss: () -> Unit) {
AlertDialog(
onDismissRequest = onDismiss,
title = { Text("توجه") },
text = { Text(message) },
- confirmButton = { TextButton(onClick = onDismiss) { Text("باشه") } }
+ confirmButton = {
+ TextButton(onClick = onDismiss) {
+ Text("باشه")
+ }
+ }
)
}
+
+sealed class PaymentDialogState {
+ data class Add(val month: Int) : PaymentDialogState()
+ data class Edit(val payment: PaymentRecord) : PaymentDialogState()
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/oqba26/monthlypaymentapp/ui/screens/PersonScreen.kt b/app/src/main/java/com/oqba26/monthlypaymentapp/ui/screens/PersonScreen.kt
index 3581e02..f19ef82 100644
--- a/app/src/main/java/com/oqba26/monthlypaymentapp/ui/screens/PersonScreen.kt
+++ b/app/src/main/java/com/oqba26/monthlypaymentapp/ui/screens/PersonScreen.kt
@@ -1,10 +1,9 @@
+@file:Suppress("AssignedValueIsNeverRead")
+
package com.oqba26.monthlypaymentapp.ui.screens
-import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween
-import androidx.compose.foundation.ExperimentalFoundationApi
-import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
@@ -19,43 +18,27 @@ import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.text.KeyboardOptions
+import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.automirrored.filled.KeyboardArrowLeft
-import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight
-import androidx.compose.material.icons.filled.Add
-import androidx.compose.material.icons.filled.AddCard
import androidx.compose.material.icons.filled.Check
-import androidx.compose.material.icons.filled.Close
-import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.Search
-import androidx.compose.material.icons.filled.Settings
+import androidx.compose.material.pullrefresh.PullRefreshIndicator
+import androidx.compose.material.pullrefresh.pullRefresh
+import androidx.compose.material.pullrefresh.rememberPullRefreshState
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
-import androidx.compose.material3.ExperimentalMaterial3Api
-import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
-import androidx.compose.material3.IconButton
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.OutlinedTextField
-import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
-import androidx.compose.material3.TextButton
-import androidx.compose.material3.TopAppBar
-import androidx.compose.material3.TopAppBarDefaults
-import androidx.compose.material3.pulltorefresh.PullToRefreshContainer
-import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState
-import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
@@ -63,734 +46,358 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.input.nestedscroll.nestedScroll
-import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp
-import androidx.lifecycle.viewmodel.compose.viewModel
-import com.oqba26.monthlypaymentapp.core.PaymentApplication
-import com.oqba26.monthlypaymentapp.data.model.PaymentRecord
-import com.oqba26.monthlypaymentapp.utils.PersianNumberVisualTransformation
+import androidx.navigation.NavController
import com.oqba26.monthlypaymentapp.utils.formatNumberAsPersian
-import com.oqba26.monthlypaymentapp.utils.getCurrentShamsiMonth
-import com.oqba26.monthlypaymentapp.utils.getCurrentShamsiYear
-import com.oqba26.monthlypaymentapp.utils.toPersianDate
import com.oqba26.monthlypaymentapp.utils.toPersianDigits
import com.oqba26.monthlypaymentapp.viewmodel.DashboardUiModel
-import com.oqba26.monthlypaymentapp.viewmodel.MonthStatus
+import com.oqba26.monthlypaymentapp.viewmodel.PersonListType
import com.oqba26.monthlypaymentapp.viewmodel.PersonScreenEvent
import com.oqba26.monthlypaymentapp.viewmodel.PersonUiModel
import com.oqba26.monthlypaymentapp.viewmodel.PersonViewModel
+import org.burnoutcrew.reorderable.ReorderableItem
+import org.burnoutcrew.reorderable.detectReorderAfterLongPress
+import org.burnoutcrew.reorderable.rememberReorderableLazyListState
+import org.burnoutcrew.reorderable.reorderable
-@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
+@OptIn(ExperimentalMaterialApi::class)
@Composable
fun PersonScreen(
- onSettingsClick: () -> Unit,
- //onExitApp: () -> Unit
+ viewModel: PersonViewModel,
+ navController: NavController
) {
- val context = LocalContext.current
- val app = context.applicationContext as PaymentApplication
- val viewModel: PersonViewModel = viewModel(factory = app.personViewModelFactory)
-
- val uiState by viewModel.uiState.collectAsState()
- val persons = uiState.persons
- val allPayments = uiState.payments
-
val searchQuery by viewModel.searchQuery.collectAsState()
+ val uiState by viewModel.uiState.collectAsState()
val dashboardData by viewModel.dashboardData.collectAsState()
val defaultPaymentAmount by viewModel.defaultPaymentAmountFlow.collectAsState(initial = 0.0)
+ val personForPaymentDialog by viewModel.personForPaymentDialog.collectAsState()
+ val isAddPersonDialogShown by viewModel.showAddPersonDialog.collectAsState()
val isRefreshing by viewModel.isRefreshing.collectAsState()
- var showAddPersonDialog by remember { mutableStateOf(false) }
- var personToDelete by remember { mutableStateOf(null) }
+ // شخصی که قرار است با تأیید کاربر به آرشیو منتقل شود
+ var personToArchive by remember { mutableStateOf(null) }
- var showDetailsSheet by remember { mutableStateOf(false) }
- var selectedPerson by remember { mutableStateOf(null) }
-
- var personForQuickPay by remember { mutableStateOf(null) }
-
- val sheetState = rememberModalBottomSheetState()
- val pullToRefreshState = rememberPullToRefreshState()
-
- if (pullToRefreshState.isRefreshing) {
- LaunchedEffect(true) {
- viewModel.onEvent(PersonScreenEvent.RefreshData)
- }
- }
-
- LaunchedEffect(isRefreshing) {
- if (isRefreshing) {
- pullToRefreshState.startRefresh()
- } else {
- pullToRefreshState.endRefresh()
- }
- }
-
-
- Scaffold(
- topBar = {
- TopAppBar(
- title = { Text("مدیریت حقوق ماهانه") },
- colors = TopAppBarDefaults.topAppBarColors(
- containerColor = MaterialTheme.colorScheme.primary,
- titleContentColor = Color.White,
- actionIconContentColor = Color.White
- ),
- actions = {
- IconButton(onClick = onSettingsClick) {
- Icon(Icons.Filled.Settings, contentDescription = "تنظیمات")
- }
- /*IconButton(onClick = onExitApp) {
- Icon(Icons.AutoMirrored.Filled.ExitToApp, contentDescription = "خروج")
- }*/
- }
- )
+ val reorderableState = rememberReorderableLazyListState(
+ onMove = { from, to ->
+ if (searchQuery.isBlank()) {
+ viewModel.onEvent(
+ PersonScreenEvent.MovePersonNew(
+ fromIndex = from.index,
+ toIndex = to.index,
+ listType = PersonListType.UNPAID
+ )
+ )
+ }
},
- floatingActionButton = {
- FloatingActionButton(onClick = { showAddPersonDialog = true }) {
- Icon(Icons.Filled.Add, contentDescription = "افزودن شخص")
+ onDragEnd = { _, _ ->
+ if (searchQuery.isBlank()) {
+ viewModel.onEvent(PersonScreenEvent.CommitReorder(PersonListType.UNPAID))
}
}
- ) { innerPadding ->
- Box(modifier = Modifier.padding(innerPadding).nestedScroll(pullToRefreshState.nestedScrollConnection)) {
- Column {
- DashboardCard(dashboardData)
+ )
+
+ // Pull-to-refresh
+ val pullRefreshState = rememberPullRefreshState(
+ refreshing = isRefreshing,
+ onRefresh = { viewModel.onEvent(PersonScreenEvent.RefreshData) }
+ )
+
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .pullRefresh(pullRefreshState)
+ ) {
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(top = 8.dp)
+ ) {
+ Column(
+ modifier = Modifier.padding(horizontal = 16.dp),
+ verticalArrangement = Arrangement.spacedBy(8.dp)
+ ) {
+ DashboardCard(data = dashboardData)
OutlinedTextField(
value = searchQuery,
onValueChange = { viewModel.onSearchQueryChange(it) },
- modifier = Modifier
- .fillMaxWidth()
- .padding(horizontal = 16.dp, vertical = 8.dp),
- label = { Text("جستجوی نام...") },
- leadingIcon = { Icon(Icons.Filled.Search, contentDescription = "Search") },
- singleLine = true
+ modifier = Modifier.fillMaxWidth(),
+ placeholder = { Text("جستجوی نام...") },
+ leadingIcon = { Icon(Icons.Default.Search, contentDescription = "Search") }
)
+ }
- LazyColumn(Modifier.fillMaxSize()) {
- itemsIndexed(persons, key = { _, person -> person.id }) { index, person ->
- PersonListItem(
- person = person,
- index = index + 1,
- onPersonClick = {
- selectedPerson = person
- showDetailsSheet = true
- },
- onQuickPayClick = { personForQuickPay = person },
- onDeleteClick = { personToDelete = it }
+ Spacer(modifier = Modifier.height(8.dp))
+
+ LazyColumn(
+ state = reorderableState.listState,
+ modifier = Modifier
+ .fillMaxSize()
+ .reorderable(reorderableState)
+ ) {
+ itemsIndexed(uiState.unpaidPersons, key = { _, person -> person.id }) { index, person ->
+ ReorderableItem(reorderableState, key = person.id) { _ ->
+ val dragModifier =
+ if (searchQuery.isBlank()) Modifier.detectReorderAfterLongPress(
+ reorderableState
+ ) else Modifier
+
+ Box(modifier = dragModifier) {
+ PersonListItem(
+ person = person,
+ index = index + 1,
+ onPersonClick = {
+ navController.navigate("person_detail/${person.id}")
+ },
+ onQuickPayClick = { viewModel.onQuickPayClicked(it) },
+ onArchiveClick = { selectedPerson ->
+ personToArchive = selectedPerson
+ }
+ )
+ }
+ HorizontalDivider(
+ color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.1f)
)
- HorizontalDivider(color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.1f))
-
}
}
}
- PullToRefreshContainer(
- modifier = Modifier.align(Alignment.TopCenter),
- state = pullToRefreshState,
+ }
+
+ // اندیکاتور رفرش
+ PullRefreshIndicator(
+ refreshing = isRefreshing,
+ state = pullRefreshState,
+ modifier = Modifier.align(Alignment.TopCenter)
+ )
+
+ // دیالوگ پرداخت سریع
+ personForPaymentDialog?.let { person ->
+ AddPaymentDialog(
+ personName = person.name,
+ defaultAmount = defaultPaymentAmount,
+ onConfirm = { amount, description ->
+ viewModel.onEvent(
+ PersonScreenEvent.AddQuickPayment(
+ person.id,
+ amount,
+ description
+ )
+ )
+ viewModel.onDismissPaymentDialog()
+ },
+ onDismiss = { viewModel.onDismissPaymentDialog() }
)
}
- }
- if (showDetailsSheet) {
- ModalBottomSheet(
- onDismissRequest = { showDetailsSheet = false },
- sheetState = sheetState
- ) {
- selectedPerson?.let {
- DetailsSheetContent(
- person = it,
- allPayments = allPayments,
- defaultPaymentAmount = defaultPaymentAmount,
- viewModel = viewModel
- )
- }
+ // دیالوگ افزودن شخص جدید
+ if (isAddPersonDialogShown) {
+ AddNewPersonDialog(
+ onConfirm = { name ->
+ viewModel.onEvent(PersonScreenEvent.AddPerson(name))
+ viewModel.onDismissAddPersonDialog()
+ },
+ onDismiss = { viewModel.onDismissAddPersonDialog() }
+ )
}
- }
- personForQuickPay?.let { person ->
- var amount by remember { mutableStateOf(defaultPaymentAmount.toLong().toString()) }
- QuickPayDialog(
- personName = person.name,
- amount = amount,
- onAmountChange = { amount = it.filter { c -> c.isDigit() } },
- onConfirm = {
- val finalAmount = amount.toDoubleOrNull() ?: defaultPaymentAmount
- viewModel.onEvent(PersonScreenEvent.AddQuickPayment(person.id, finalAmount))
- personForQuickPay = null
- },
- onDismiss = { personForQuickPay = null }
- )
- }
-
- if (showAddPersonDialog) {
- var name by remember { mutableStateOf("") }
- AlertDialog(
- onDismissRequest = { showAddPersonDialog = false },
- title = { Text("افزودن شخص جدید") },
- text = { OutlinedTextField(value = name, onValueChange = { name = it }, label = { Text("نام") }) },
- confirmButton = {
- Row(
- modifier = Modifier.fillMaxWidth(),
- horizontalArrangement = Arrangement.SpaceBetween
- ) {
- Button(onClick = {
- if (name.isNotBlank()) {
- viewModel.onEvent(PersonScreenEvent.AddPerson(name))
- showAddPersonDialog = false
+ // دیالوگ تایید انتقال به آرشیو
+ personToArchive?.let { person ->
+ AlertDialog(
+ onDismissRequest = { personToArchive = null },
+ title = { Text("انتقال به آرشیو") },
+ text = { Text("آیا از انتقال ${person.name} به آرشیو مطمئن هستید؟") },
+ confirmButton = {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 8.dp, vertical = 4.dp),
+ horizontalArrangement = Arrangement.SpaceBetween
+ ) {
+ // در RTL: این دکمه کاملاً به سمت راست دیالوگ میچسبد
+ Button(onClick = { personToArchive = null }) {
+ Text("لغو")
}
- }) { Text("افزودن") }
- Button(
- onClick = { showAddPersonDialog = false },
- colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.error)
- ) { Text("لغو") }
- }
- }
- )
- }
-
- personToDelete?.let { person ->
- AlertDialog(
- onDismissRequest = { personToDelete = null },
- title = { Text("حذف ${person.name}") },
- text = { Text("آیا از حذف این شخص مطمئن هستید؟ تمام دادههای پرداخت او نیز حذف خواهد شد.") },
- confirmButton = {
- Button(
- onClick = {
- viewModel.onEvent(PersonScreenEvent.DeletePerson(person.id))
- personToDelete = null
- },
- colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.error)
- ) { Text("حذف") }
- },
- dismissButton = {
- TextButton(onClick = { personToDelete = null }) { Text("لغو") }
- }
- )
+ // در RTL: این دکمه کاملاً به سمت چپ دیالوگ میچسبد
+ Button(
+ onClick = {
+ viewModel.onEvent(
+ PersonScreenEvent.ArchivePerson(person.id)
+ )
+ personToArchive = null
+ },
+ colors = ButtonDefaults.buttonColors(
+ containerColor = MaterialTheme.colorScheme.secondary
+ )
+ ) {
+ Text("آرشیو")
+ }
+ }
+ },
+ dismissButton = {}
+ )
+ }
}
}
@Composable
-fun QuickPayDialog(
- personName: String,
- amount: String,
- onAmountChange: (String) -> Unit,
- onConfirm: () -> Unit,
+fun AddNewPersonDialog(
+ onConfirm: (String) -> Unit,
onDismiss: () -> Unit
) {
+ var name by remember { mutableStateOf("") }
+
AlertDialog(
onDismissRequest = onDismiss,
- title = { Text("پرداخت سریع برای $personName") },
+ title = { Text("افزودن شخص جدید") },
text = {
OutlinedTextField(
- value = amount,
- onValueChange = onAmountChange,
- label = { Text("مبلغ (تومان)") },
- keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
- visualTransformation = PersianNumberVisualTransformation(),
+ value = name,
+ onValueChange = { name = it },
+ label = { Text("نام شخص") },
+ singleLine = true,
modifier = Modifier.fillMaxWidth()
)
},
confirmButton = {
Row(
- modifier = Modifier.fillMaxWidth(),
- horizontalArrangement = Arrangement.SpaceBetween
- ) {
- Button(onClick = onConfirm) { Text("ثبت") }
- Button(
- onClick = onDismiss,
- colors = ButtonDefaults.buttonColors(
- containerColor = MaterialTheme.colorScheme.error
- )
- ) { Text("لغو") }
- }
- }
- )
-}
-
-@Composable
-fun DetailsSheetContent(
- person: PersonUiModel,
- allPayments: List,
- defaultPaymentAmount: Double,
- viewModel: PersonViewModel
-) {
- val selectedYear by viewModel.selectedYear.collectAsState()
-
- var paymentToEdit by remember { mutableStateOf(null) }
- var showPaymentInfoDialog by remember { mutableStateOf(false) }
- var showAddPaymentInSheetDialog by remember { mutableStateOf(false) }
- var showEditPaymentDialog by remember { mutableStateOf(false) }
- var showDeleteConfirmDialog by remember { mutableStateOf(false) }
- var showMessageDialog by remember { mutableStateOf(false) }
- var dialogMessage by remember { mutableStateOf("") }
-
- var paymentAmount by remember { mutableStateOf("") }
- var selectedMonthForPayment by remember { mutableIntStateOf(1) }
-
- val currentYear = getCurrentShamsiYear()
- val currentMonth = getCurrentShamsiMonth()
- val persianMonths = listOf("فروردین", "اردیبهشت", "خرداد", "تیر", "مرداد", "شهریور", "مهر", "آبان", "آذر", "دی", "بهمن", "اسفند")
-
- val personPaymentHistory = remember(allPayments, person.id) {
- allPayments.filter { it.personId == person.id }
- }
-
- Column(modifier = Modifier.background(MaterialTheme.colorScheme.surface).padding(16.dp)) {
- Text(person.name, style = MaterialTheme.typography.headlineSmall, modifier = Modifier.padding(bottom = 16.dp))
- YearNavigator(
- modifier = Modifier.padding(top = 8.dp),
- selectedYear = selectedYear,
- onYearChange = { offset -> viewModel.onEvent(PersonScreenEvent.ChangeYear(offset)) },
- showMessage = { msg ->
- dialogMessage = msg
- showMessageDialog = true
- }
- )
- Spacer(Modifier.height(16.dp))
- MonthsList(
- paymentHistory = personPaymentHistory,
- selectedYear = selectedYear,
- currentYear = currentYear,
- currentMonth = currentMonth,
- persianMonths = persianMonths,
- onAvailableClick = { month, _ ->
- selectedMonthForPayment = month
- paymentAmount = defaultPaymentAmount.toLong().toString()
- showAddPaymentInSheetDialog = true
- },
- onPaidClick = { payment ->
- paymentToEdit = payment
- paymentAmount = payment.amount.toLong().toString()
- showPaymentInfoDialog = true
- },
- onDisabledClick = { message ->
- dialogMessage = message
- showMessageDialog = true
- }
- )
- }
-
- if (showAddPaymentInSheetDialog) {
- AddPaymentDialog(
- monthName = persianMonths[selectedMonthForPayment - 1],
- year = selectedYear,
- amount = paymentAmount,
- onAmountChange = { paymentAmount = it.filter { c -> c.isDigit() } },
- onConfirm = {
- val amount = paymentAmount.toDoubleOrNull()
- if (amount != null && amount > 0) {
- viewModel.onEvent(PersonScreenEvent.AddPaymentForMonth(person.id, selectedMonthForPayment, selectedYear, amount))
- showAddPaymentInSheetDialog = false
- }
- },
- onDismiss = { showAddPaymentInSheetDialog = false }
- )
- }
-
- paymentToEdit?.let { payment ->
- if (showPaymentInfoDialog) {
- PaymentInfoDialog(
- payment = payment,
- onEdit = {
- showPaymentInfoDialog = false
- showEditPaymentDialog = true
- },
- onDismiss = {
- showPaymentInfoDialog = false
- paymentToEdit = null
- }
- )
- }
-
- if (showEditPaymentDialog) {
- EditPaymentDialog(
- amount = paymentAmount,
- onAmountChange = { paymentAmount = it.filter { c -> c.isDigit() } },
- onSave = {
- val amount = paymentAmount.toDoubleOrNull()
- if (amount != null && amount > 0) {
- viewModel.onEvent(PersonScreenEvent.UpdatePayment(payment, amount))
- showEditPaymentDialog = false
- paymentToEdit = null
- }
- },
- onDelete = {
- showEditPaymentDialog = false
- showDeleteConfirmDialog = true
- },
- onDismiss = {
- showEditPaymentDialog = false
- paymentToEdit = null
- }
- )
- }
-
- if (showDeleteConfirmDialog) {
- DeleteConfirmDialog(
- onConfirm = {
- viewModel.onEvent(PersonScreenEvent.DeletePayment(payment))
- showDeleteConfirmDialog = false
- paymentToEdit = null
- },
- onDismiss = {
- showDeleteConfirmDialog = false
- paymentToEdit = null
- }
- )
- }
- }
-
- if (showMessageDialog) {
- MessageDialog(message = dialogMessage) { showMessageDialog = false }
- }
-}
-
-@Composable
-private fun PaymentInfoDialog(
- payment: PaymentRecord,
- onEdit: () -> Unit,
- onDismiss: () -> Unit
-) {
- val currentMonth = getCurrentShamsiMonth()
- val persianMonths = listOf("فروردین", "اردیبهشت", "خرداد", "تیر", "مرداد", "شهریور", "مهر", "آبان", "آذر", "دی", "بهمن", "اسفند")
- val monthName = persianMonths[payment.shamsiMonth - 1]
-
- val persianDate = payment.timestamp.toPersianDate("Y/n/j").toPersianDigits()
- val persianTime = payment.timestamp.toPersianDate("H:i:s").toPersianDigits()
-
- val timeInfo = if (payment.shamsiMonth == currentMonth) {
- "پرداخت برای $monthName در ساعت $persianTime ثبت شد"
- } else {
- "پرداخت برای $monthName در تاریخ $persianDate ساعت $persianTime ثبت شد"
- }
-
- AlertDialog(
- onDismissRequest = onDismiss,
- title = { Text("جزئیات پرداخت") },
- text = {
- Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
- Text(
- text = timeInfo,
- style = MaterialTheme.typography.bodyMedium
- )
- Text(
- text = "مبلغ: ${formatNumberAsPersian(payment.amount)} تومان",
- style = MaterialTheme.typography.bodyLarge,
- fontWeight = FontWeight.Bold,
- color = MaterialTheme.colorScheme.primary
- )
- }
- },
- confirmButton = {
- Row(
- modifier = Modifier.fillMaxWidth(),
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(all = 8.dp),
horizontalArrangement = Arrangement.SpaceBetween
) {
Button(
- onClick = onEdit,
- colors = ButtonDefaults.buttonColors(
- containerColor = MaterialTheme.colorScheme.primary
- )
- ) { Text("ویرایش") }
-
- Button(
- onClick = onDismiss,
- colors = ButtonDefaults.buttonColors(
- containerColor = MaterialTheme.colorScheme.error
- )
- ) { Text("بستن") }
- }
- }
- )
-}
-
-@Composable
-private fun YearNavigator(
- modifier: Modifier = Modifier,
- selectedYear: Int,
- onYearChange: (Int) -> Unit,
- showMessage: (String) -> Unit
-) {
- val currentYear = getCurrentShamsiYear()
- Card(modifier = modifier.fillMaxWidth(), colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.secondaryContainer)) {
- Row(
- modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 4.dp),
- horizontalArrangement = Arrangement.SpaceBetween,
- verticalAlignment = Alignment.CenterVertically
- ) {
- IconButton(onClick = {
- if (selectedYear > currentYear) {
- onYearChange(-1)
- } else {
- showMessage("زمان ثبت پرداخت حقوق برای سالهای گذشته تمام شده است")
- }
- }) { Icon(Icons.AutoMirrored.Filled.KeyboardArrowLeft, "Previous Year") }
- Text("سال ${selectedYear.toString().toPersianDigits()}", style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold)
- IconButton(onClick = {
- if (selectedYear < currentYear) {
- onYearChange(1)
- } else {
- showMessage("زمان ثبت پرداخت حقوق برای سالهای آینده فرا نرسیده است")
- }
- }) { Icon(Icons.AutoMirrored.Filled.KeyboardArrowRight, "Next Year") }
- }
- }
-}
-
-@Composable
-private fun MonthsList(
- paymentHistory: List,
- selectedYear: Int,
- currentYear: Int,
- currentMonth: Int,
- persianMonths: List,
- onAvailableClick: (month: Int, year: Int) -> Unit,
- onPaidClick: (payment: PaymentRecord) -> Unit,
- onDisabledClick: (message: String) -> Unit
-) {
- LazyColumn(modifier= Modifier.height(320.dp)) {
- itemsIndexed(persianMonths, key = { index, _ -> "$selectedYear-$index" }) { index, monthName ->
- val monthNumber = index + 1
- val payment = paymentHistory.find { p -> p.shamsiMonth == monthNumber && p.shamsiYear == selectedYear }
-
- val monthStatus = when {
- payment != null -> MonthStatus.PAID
- selectedYear > currentYear -> MonthStatus.FUTURE_YEAR
- selectedYear < currentYear -> MonthStatus.PAST_YEAR
- monthNumber > currentMonth -> MonthStatus.FUTURE_MONTH
- else -> MonthStatus.AVAILABLE
- }
-
- MonthListItem(
- monthName = monthName,
- payment = payment,
- status = monthStatus,
- onItemClick = {
- when (monthStatus) {
- MonthStatus.AVAILABLE -> onAvailableClick(monthNumber, selectedYear)
- MonthStatus.PAID -> onPaidClick(payment!!)
- else -> onDisabledClick(getMessageForStatus(monthStatus, monthName))
- }
- }
- )
- if(index < persianMonths.size - 1) {
- HorizontalDivider(color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.05f))
- }
- }
- }
-}
-
-@Composable
-private fun MonthListItem(
- monthName: String,
- payment: PaymentRecord?,
- status: MonthStatus,
- onItemClick: () -> Unit
-) {
- val backgroundColor by animateColorAsState(
- targetValue = when(status) {
- MonthStatus.PAID -> MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.3f)
- MonthStatus.AVAILABLE -> MaterialTheme.colorScheme.secondaryContainer.copy(alpha = 0.15f)
- else -> MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f)
- },
- animationSpec = tween(300),
- label = "MonthListItemColor"
- )
-
- Card(
- modifier = Modifier
- .fillMaxWidth()
- .padding(horizontal = 8.dp, vertical = 4.dp)
- .clickable(onClick = onItemClick),
- colors = CardDefaults.cardColors(containerColor = backgroundColor),
- elevation = CardDefaults.cardElevation(
- defaultElevation = if (status == MonthStatus.PAID) 2.dp else 0.dp
- ),
- shape = MaterialTheme.shapes.medium
- ) {
- Row(
- modifier = Modifier
- .fillMaxWidth()
- .padding(horizontal = 16.dp, vertical = 14.dp),
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.SpaceBetween
- ) {
- Row(
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.spacedBy(12.dp)
- ) {
- val iconColor = when(status) {
- MonthStatus.PAID -> MaterialTheme.colorScheme.primary
- MonthStatus.AVAILABLE -> MaterialTheme.colorScheme.secondary
- else -> MaterialTheme.colorScheme.onSurface.copy(alpha = 0.3f)
- }
-
- Box(
- modifier = Modifier
- .background(
- color = iconColor.copy(alpha = 0.15f),
- shape = MaterialTheme.shapes.small
- )
- .padding(8.dp)
+ onClick = {
+ if (name.isNotBlank()) {
+ onConfirm(name)
+ }
+ },
+ enabled = name.isNotBlank()
) {
- Icon(
- imageVector = when(status) {
- MonthStatus.PAID -> Icons.Filled.Check
- MonthStatus.AVAILABLE -> Icons.Filled.Add
- else -> Icons.Filled.Close
- },
- contentDescription = status.name,
- tint = iconColor,
- modifier = Modifier.width(20.dp).height(20.dp)
- )
+ Text("افزودن")
}
-
- Text(
- text = monthName,
- style = MaterialTheme.typography.titleMedium,
- fontWeight = if (status == MonthStatus.PAID) FontWeight.SemiBold else FontWeight.Normal
- )
- }
-
- if (payment != null) {
- Text(
- text = "${formatNumberAsPersian(payment.amount)} تومان",
- style = MaterialTheme.typography.bodyMedium,
- color = MaterialTheme.colorScheme.primary,
- fontWeight = FontWeight.Bold
- )
- }
- }
- }
-}
-
-private fun getMessageForStatus(status: MonthStatus, monthName: String): String {
- return when (status) {
- MonthStatus.FUTURE_YEAR -> "این سال هنوز نرسیده است."
- MonthStatus.PAST_YEAR -> "شما در سال گذشته برای ماه $monthName پرداختی ثبت نکردهاید."
- MonthStatus.FUTURE_MONTH -> "پرداخت حقوق برای این ماه امکانپذیر نیست (زمانش که برسه این ماه باز میشه)"
- else -> ""
- }
-}
-
-@Composable
-private fun AddPaymentDialog(
- monthName: String,
- year: Int,
- amount: String,
- onAmountChange: (String) -> Unit,
- onConfirm: () -> Unit,
- onDismiss: () -> Unit
-) {
- AlertDialog(
- onDismissRequest = onDismiss,
- title = { Text("ثبت پرداخت برای $monthName ${year.toString().toPersianDigits()}") },
- text = {
- OutlinedTextField(
- value = amount,
- onValueChange = onAmountChange,
- label = { Text("مبلغ (تومان)") },
- visualTransformation = PersianNumberVisualTransformation(),
- keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
- modifier = Modifier.fillMaxWidth(),
- suffix = { Text("تومان") }
- )
- },
- confirmButton = {
- Row(
- modifier = Modifier.fillMaxWidth(),
- horizontalArrangement = Arrangement.SpaceBetween
- ) {
- Button(onClick = onConfirm) { Text("ثبت") }
Button(
onClick = onDismiss,
colors = ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.error
)
- ) { Text("لغو") }
+ ) {
+ Text("لغو")
+ }
}
- }
+ },
+ dismissButton = {}
)
}
@Composable
-private fun EditPaymentDialog(
- amount: String,
- onAmountChange: (String) -> Unit,
- onSave: () -> Unit,
- onDelete: () -> Unit,
- onDismiss: () -> Unit
+fun AddPaymentDialog(
+ personName: String,
+ defaultAmount: Double,
+ initialDescription: String = "",
+ onConfirm: (amount: Double, description: String) -> Unit,
+ onDismiss: () -> Unit,
+ onDelete: (() -> Unit)? = null
) {
+ var rawAmount by remember { mutableStateOf(defaultAmount.toLong().toString()) }
+ var description by remember { mutableStateOf(initialDescription) }
+
+ val title =
+ if (onDelete != null) "ویرایش پرداخت برای $personName" else "ثبت پرداخت برای $personName"
+
AlertDialog(
onDismissRequest = onDismiss,
- title = { Text("ویرایش یا حذف پرداخت") },
+ title = { Text(title) },
text = {
- OutlinedTextField(
- value = amount,
- onValueChange = onAmountChange,
- label = { Text("مبلغ جدید (تومان)") },
- visualTransformation = PersianNumberVisualTransformation(),
- keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
- modifier = Modifier.fillMaxWidth(),
- suffix = { Text("تومان") }
- )
+ Column {
+ OutlinedTextField(
+ value = formatNumberAsPersian(rawAmount.toDoubleOrNull() ?: 0.0)
+ .removeSuffix(".00"),
+ onValueChange = { value ->
+ rawAmount = value.filter { it.isDigit() }
+ },
+ label = { Text("مبلغ (به تومان)") },
+ keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
+ singleLine = true,
+ modifier = Modifier.fillMaxWidth(),
+ )
+ Spacer(modifier = Modifier.height(16.dp))
+ OutlinedTextField(
+ value = description,
+ onValueChange = { description = it },
+ label = { Text("توضیحات (اختیاری)") },
+ modifier = Modifier.fillMaxWidth()
+ )
+ }
},
confirmButton = {
- Row(
- modifier = Modifier.fillMaxWidth(),
- horizontalArrangement = Arrangement.SpaceBetween,
- verticalAlignment = Alignment.CenterVertically
- ) {
- Button(onClick = onSave) { Text("ذخیره") }
-
- Button(
- onClick = onDismiss,
- colors = ButtonDefaults.buttonColors(
- containerColor = MaterialTheme.colorScheme.secondary
- )
- ) { Text("لغو") }
-
- Button(
- onClick = onDelete,
- colors = ButtonDefaults.buttonColors(
- containerColor = MaterialTheme.colorScheme.error
- )
- ) { Text("حذف") }
+ if (onDelete != null) {
+ // حالت ویرایش: ویرایش راست، لغو وسط، حذف چپ (در RTL)
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 8.dp, vertical = 4.dp),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Button(
+ onClick = {
+ val amountAsDouble = rawAmount.toDoubleOrNull() ?: defaultAmount
+ onConfirm(amountAsDouble, description)
+ },
+ enabled = rawAmount.isNotBlank()
+ ) {
+ Text("ویرایش")
+ }
+ Button(
+ onClick = onDismiss,
+ colors = ButtonDefaults.buttonColors(
+ containerColor = MaterialTheme.colorScheme.error
+ )
+ ) {
+ Text("لغو")
+ }
+ Button(
+ onClick = onDelete,
+ colors = ButtonDefaults.buttonColors(
+ containerColor = MaterialTheme.colorScheme.error
+ )
+ ) {
+ Text("حذف")
+ }
+ }
+ } else {
+ // حالت افزودن: ثبت و لغو
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 8.dp, vertical = 4.dp),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Button(
+ onClick = {
+ val amountAsDouble = rawAmount.toDoubleOrNull() ?: defaultAmount
+ onConfirm(amountAsDouble, description)
+ },
+ enabled = rawAmount.isNotBlank()
+ ) {
+ Text("ثبت")
+ }
+ Button(
+ onClick = onDismiss,
+ colors = ButtonDefaults.buttonColors(
+ containerColor = MaterialTheme.colorScheme.error
+ )
+ ) {
+ Text("لغو")
+ }
+ }
}
- }
- )
-}
-
-@Composable
-private fun DeleteConfirmDialog(onConfirm: () -> Unit, onDismiss: () -> Unit) {
- AlertDialog(
- onDismissRequest = onDismiss,
- title = { Text("تایید حذف") },
- text = { Text("آیا از حذف این پرداخت مطمئن هستید؟") },
- confirmButton = {
- Row(
- modifier = Modifier.fillMaxWidth(),
- horizontalArrangement = Arrangement.SpaceBetween
- ) {
- Button(
- onClick = onConfirm,
- colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.error)
- ) { Text("حذف کن") }
- TextButton(onClick = onDismiss) { Text("لغو") }
- }
- }
- )
-}
-
-@Composable
-private fun MessageDialog(message: String, onDismiss: () -> Unit) {
- AlertDialog(
- onDismissRequest = onDismiss,
- title = { Text("توجه") },
- text = { Text(message) },
- confirmButton = { TextButton(onClick = onDismiss) { Text("باشه") } }
+ },
+ dismissButton = {}
)
}
@@ -804,16 +411,15 @@ fun DashboardCard(data: DashboardUiModel) {
Card(
modifier = Modifier
- .fillMaxWidth()
- .padding(16.dp),
- elevation = CardDefaults.cardElevation(4.dp),
+ .fillMaxWidth(),
+ elevation = CardDefaults.cardElevation(2.dp),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceVariant
)
) {
Column(
- modifier = Modifier.padding(16.dp),
- verticalArrangement = Arrangement.spacedBy(8.dp)
+ modifier = Modifier.padding(12.dp),
+ verticalArrangement = Arrangement.spacedBy(6.dp)
) {
Row(
modifier = Modifier.fillMaxWidth(),
@@ -827,7 +433,7 @@ fun DashboardCard(data: DashboardUiModel) {
)
Text(
"افراد باقیمانده: ${(data.totalCount - data.paidCount).toString().toPersianDigits()} نفر",
- style = MaterialTheme.typography.bodyLarge
+ style = MaterialTheme.typography.bodyMedium
)
}
@@ -839,7 +445,7 @@ fun DashboardCard(data: DashboardUiModel) {
.clip(MaterialTheme.shapes.medium)
)
- Spacer(modifier = Modifier.height(8.dp))
+ Spacer(modifier = Modifier.height(4.dp))
Row(
modifier = Modifier.fillMaxWidth(),
@@ -866,50 +472,70 @@ fun PersonListItem(
index: Int,
onPersonClick: () -> Unit,
onQuickPayClick: (PersonUiModel) -> Unit,
- onDeleteClick: (PersonUiModel) -> Unit
+ onArchiveClick: (PersonUiModel) -> Unit
) {
- Row(
+ Column(
modifier = Modifier
.fillMaxWidth()
.clickable { onPersonClick() }
- .padding(horizontal = 16.dp, vertical = 8.dp),
- verticalAlignment = Alignment.CenterVertically
+ .padding(horizontal = 16.dp, vertical = 8.dp)
) {
- Text(
- text = "${index.toString().toPersianDigits()} - ",
- style = MaterialTheme.typography.bodyLarge,
- fontWeight = FontWeight.Normal
- )
- Text(
- person.name,
- modifier = Modifier.weight(1f),
- style = MaterialTheme.typography.bodyLarge,
- fontWeight = FontWeight.Bold
- )
- Spacer(Modifier.width(8.dp))
-
- if (person.hasPaidThisMonth) {
- Icon(
- Icons.Filled.Check,
- contentDescription = "پرداخت شده",
- tint = MaterialTheme.colorScheme.primary
+ Row(
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Text(
+ text = "${index.toString().toPersianDigits()} - ",
+ style = MaterialTheme.typography.bodyLarge,
+ fontWeight = FontWeight.Normal
)
- } else {
- IconButton(onClick = { onQuickPayClick(person) }) {
- Icon(
- Icons.Filled.AddCard,
- contentDescription = "پرداخت سریع",
- tint = MaterialTheme.colorScheme.secondary
- )
+ Text(
+ person.name,
+ modifier = Modifier.weight(1f),
+ style = MaterialTheme.typography.bodyLarge,
+ fontWeight = FontWeight.Bold
+ )
+
+ Spacer(Modifier.width(8.dp))
+
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.spacedBy(6.dp)
+ ) {
+ if (person.hasPaidThisMonth) {
+ Icon(
+ Icons.Filled.Check,
+ contentDescription = "پرداخت شده",
+ tint = MaterialTheme.colorScheme.primary
+ )
+
+ Button(
+ onClick = { onArchiveClick(person) },
+ colors = ButtonDefaults.buttonColors(
+ containerColor = MaterialTheme.colorScheme.secondary
+ )
+ ) {
+ Text("آرشیو", color = Color.White)
+ }
+ } else {
+ Button(
+ onClick = { onQuickPayClick(person) },
+ colors = ButtonDefaults.buttonColors(
+ containerColor = MaterialTheme.colorScheme.primary
+ )
+ ) {
+ Text("پرداخت", color = Color.White)
+ }
+
+ Button(
+ onClick = { onArchiveClick(person) },
+ colors = ButtonDefaults.buttonColors(
+ containerColor = MaterialTheme.colorScheme.secondary
+ )
+ ) {
+ Text("آرشیو", color = Color.White)
+ }
+ }
}
}
-
- IconButton(onClick = { onDeleteClick(person) }) {
- Icon(
- Icons.Filled.Delete,
- contentDescription = "حذف",
- tint = MaterialTheme.colorScheme.error
- )
- }
}
-}
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/oqba26/monthlypaymentapp/ui/screens/SettingsScreen.kt b/app/src/main/java/com/oqba26/monthlypaymentapp/ui/screens/SettingsScreen.kt
index 717d94f..96599ac 100644
--- a/app/src/main/java/com/oqba26/monthlypaymentapp/ui/screens/SettingsScreen.kt
+++ b/app/src/main/java/com/oqba26/monthlypaymentapp/ui/screens/SettingsScreen.kt
@@ -1,3 +1,5 @@
+@file:Suppress("AssignedValueIsNeverRead")
+
package com.oqba26.monthlypaymentapp.ui.screens
import android.widget.Toast
@@ -6,8 +8,6 @@ import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.animation.AnimatedContent
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
-import androidx.compose.material3.TopAppBarDefaults
-import androidx.compose.ui.graphics.Color
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
@@ -17,7 +17,7 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.automirrored.filled.ArrowBack
+import androidx.compose.material.icons.automirrored.filled.Logout
import androidx.compose.material.icons.filled.ArrowDropDown
import androidx.compose.material.icons.filled.Edit
import androidx.compose.material3.AlertDialog
@@ -27,7 +27,6 @@ import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
-import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
@@ -37,7 +36,6 @@ import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
-import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
@@ -65,9 +63,8 @@ import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
-@OptIn(ExperimentalMaterial3Api::class)
@Composable
-fun SettingsScreen(onBackClick: () -> Unit) {
+fun SettingsScreen(onLogout: () -> Unit) {
val context = LocalContext.current
val app = context.applicationContext as PaymentApplication
val viewModel: SettingsViewModel = viewModel(factory = app.settingsViewModelFactory)
@@ -76,7 +73,7 @@ fun SettingsScreen(onBackClick: () -> Unit) {
val defaultAmount by viewModel.defaultPaymentAmount.collectAsState()
val selectedFont by viewModel.selectedFont.collectAsState()
- var showRestoreConfirmDialog by remember { mutableStateOf(null) }
+ var backupJsonToRestore by remember { mutableStateOf(null) }
LaunchedEffect(Unit) {
viewModel.toastMessage.collect { message ->
@@ -92,11 +89,21 @@ fun SettingsScreen(onBackClick: () -> Unit) {
try {
val jsonString = viewModel.createBackupJsonSuspend()
context.contentResolver.openOutputStream(it)?.use { outputStream ->
- OutputStreamWriter(outputStream).use { writer -> writer.write(jsonString) }
+ OutputStreamWriter(outputStream).use { writer ->
+ writer.write(jsonString)
+ }
}
- Toast.makeText(context, "پشتیبانگیری با موفقیت انجام شد", Toast.LENGTH_LONG).show()
+ Toast.makeText(
+ context,
+ "پشتیبانگیری با موفقیت انجام شد",
+ Toast.LENGTH_LONG
+ ).show()
} catch (_: Exception) {
- Toast.makeText(context, "خطا در ایجاد فایل پشتیبان", Toast.LENGTH_LONG).show()
+ Toast.makeText(
+ context,
+ "خطا در ایجاد فایل پشتیبان",
+ Toast.LENGTH_LONG
+ ).show()
}
}
}
@@ -110,7 +117,7 @@ fun SettingsScreen(onBackClick: () -> Unit) {
try {
context.contentResolver.openInputStream(it)?.use { inputStream ->
BufferedReader(InputStreamReader(inputStream)).use { reader ->
- showRestoreConfirmDialog = reader.readText()
+ backupJsonToRestore = reader.readText()
}
}
} catch (_: Exception) {
@@ -120,44 +127,42 @@ fun SettingsScreen(onBackClick: () -> Unit) {
}
)
- Scaffold(
- topBar = {
- TopAppBar(
- title = { Text("تنظیمات") },
- navigationIcon = {
- IconButton(onClick = onBackClick) {
- Icon(Icons.AutoMirrored.Filled.ArrowBack, "Back")
- }
- },
- colors = TopAppBarDefaults.topAppBarColors(
- containerColor = MaterialTheme.colorScheme.primary,
- titleContentColor = Color.White,
- navigationIconContentColor = Color.White
- )
- )
- }
- ) { innerPadding ->
+ Scaffold { innerPadding ->
Column(
modifier = Modifier
.padding(innerPadding)
- .padding(16.dp)
+ .padding(horizontal = 16.dp, vertical = 12.dp)
) {
- Text("تنظیمات عمومی", style = MaterialTheme.typography.titleMedium, modifier = Modifier.padding(bottom = 8.dp))
+ Text(
+ "تنظیمات عمومی",
+ style = MaterialTheme.typography.titleMedium,
+ modifier = Modifier.padding(bottom = 6.dp)
+ )
- DefaultAmountSetting(defaultAmount, onSave = { viewModel.saveDefaultPaymentAmount(it) })
+ DefaultAmountSetting(
+ defaultAmount = defaultAmount,
+ onSave = { viewModel.saveDefaultPaymentAmount(it) }
+ )
- Spacer(modifier = Modifier.height(24.dp))
+ Spacer(modifier = Modifier.height(18.dp))
- FontSelectionDropdown(selectedFont = selectedFont) {
- viewModel.onFontSelected(it)
- }
+ FontSelectionDropdown(
+ selectedFont = selectedFont,
+ onFontSelected = { viewModel.onFontSelected(it) }
+ )
- HorizontalDivider(modifier = Modifier.padding(vertical = 24.dp))
+ HorizontalDivider(modifier = Modifier.padding(vertical = 20.dp))
+
+ Text(
+ "پشتیبانگیری و بازیابی",
+ style = MaterialTheme.typography.titleMedium,
+ modifier = Modifier.padding(bottom = 6.dp)
+ )
- Text("پشتیبانگیری و بازیابی", style = MaterialTheme.typography.titleMedium, modifier = Modifier.padding(bottom = 8.dp))
OutlinedButton(
onClick = {
- val simpleDateFormat = SimpleDateFormat("yyyy-MM-dd_HH-mm", Locale.getDefault())
+ val simpleDateFormat =
+ SimpleDateFormat("yyyy-MM-dd_HH-mm", Locale.getDefault())
val fileName = "payment_backup_${simpleDateFormat.format(Date())}.json"
backupLauncher.launch(fileName)
},
@@ -165,67 +170,110 @@ fun SettingsScreen(onBackClick: () -> Unit) {
) {
Text("تهیه نسخه پشتیبان")
}
+
Spacer(modifier = Modifier.height(8.dp))
+
OutlinedButton(
onClick = { restoreLauncher.launch(arrayOf("application/json")) },
modifier = Modifier.fillMaxWidth(),
- colors = ButtonDefaults.outlinedButtonColors(contentColor = MaterialTheme.colorScheme.error)
+ colors = ButtonDefaults.outlinedButtonColors(
+ contentColor = MaterialTheme.colorScheme.error
+ )
) {
Text("بازیابی اطلاعات از فایل")
}
+
+ HorizontalDivider(modifier = Modifier.padding(vertical = 20.dp))
+
+ Button(
+ onClick = onLogout,
+ modifier = Modifier.fillMaxWidth(),
+ colors = ButtonDefaults.buttonColors(
+ containerColor = MaterialTheme.colorScheme.error
+ )
+ ) {
+ Icon(
+ Icons.AutoMirrored.Filled.Logout,
+ contentDescription = "خروج"
+ )
+ Spacer(Modifier.width(8.dp))
+ Text("خروج از حساب کاربری")
+ }
}
}
- showRestoreConfirmDialog?.let {
+ backupJsonToRestore?.let { json ->
AlertDialog(
- onDismissRequest = { showRestoreConfirmDialog = null },
+ onDismissRequest = { backupJsonToRestore = null },
title = { Text("هشدار جدی!") },
- text = { Text("آیا مطمئن هستید؟ با بازیابی اطلاعات، تمام دادههای فعلی برنامه (اشخاص و پرداختها) برای همیشه حذف شده و اطلاعات فایل پشتیبان جایگزین آن خواهد شد. این عمل غیرقابل بازگشت است.") },
+ text = {
+ Text(
+ "آیا مطمئن هستید؟ با بازیابی اطلاعات، تمام دادههای فعلی برنامه (اشخاص و پرداختها) برای همیشه حذف شده و اطلاعات فایل پشتیبان جایگزین آن خواهد شد. این عمل غیرقابل بازگشت است."
+ )
+ },
confirmButton = {
Button(
onClick = {
- viewModel.restoreFromBackupJson(it)
- showRestoreConfirmDialog = null
+ viewModel.restoreFromBackupJson(json)
+ backupJsonToRestore = null
},
- colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.error)
+ colors = ButtonDefaults.buttonColors(
+ containerColor = MaterialTheme.colorScheme.error
+ )
) {
Text("بله، بازیابی کن")
}
},
dismissButton = {
- TextButton(onClick = { showRestoreConfirmDialog = null }) { Text("لغو") }
+ TextButton(onClick = { backupJsonToRestore = null }) {
+ Text("لغو")
+ }
}
)
}
}
@Composable
-fun DefaultAmountSetting(defaultAmount: Double, onSave: (String) -> Unit) {
+fun DefaultAmountSetting(
+ defaultAmount: Double,
+ onSave: (String) -> Unit
+) {
var isEditing by remember { mutableStateOf(false) }
- var amountText by remember(defaultAmount, isEditing) { mutableStateOf(defaultAmount.toLong().toString()) }
+ var amountText by remember(defaultAmount, isEditing) {
+ mutableStateOf(defaultAmount.toLong().toString())
+ }
Card(
modifier = Modifier.fillMaxWidth(),
elevation = CardDefaults.cardElevation(0.dp),
- colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f))
+ colors = CardDefaults.cardColors(
+ containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f)
+ )
) {
AnimatedContent(
targetState = isEditing,
label = "EditAmountAnimation",
- modifier = Modifier.padding(16.dp)
+ modifier = Modifier.padding(12.dp)
) { editing ->
if (editing) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
OutlinedTextField(
value = amountText,
- onValueChange = { amountText = it.filter { c -> c.isDigit() } },
+ onValueChange = {
+ amountText = it.filter { c -> c.isDigit() }
+ },
modifier = Modifier.fillMaxWidth(),
label = { Text("مبلغ پیش فرض پرداخت (تومان)") },
visualTransformation = PersianNumberVisualTransformation(),
- keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number)
+ keyboardOptions = KeyboardOptions(
+ keyboardType = KeyboardType.Number
+ )
)
- Spacer(Modifier.height(16.dp))
- Row(horizontalArrangement = Arrangement.End, modifier = Modifier.fillMaxWidth()) {
+ Spacer(Modifier.height(12.dp))
+ Row(
+ horizontalArrangement = Arrangement.End,
+ modifier = Modifier.fillMaxWidth()
+ ) {
TextButton(onClick = { isEditing = false }) {
Text("لغو")
}
@@ -239,10 +287,22 @@ fun DefaultAmountSetting(defaultAmount: Double, onSave: (String) -> Unit) {
}
}
} else {
- Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.SpaceBetween, modifier = Modifier.fillMaxWidth()) {
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.SpaceBetween,
+ modifier = Modifier.fillMaxWidth()
+ ) {
Column {
- Text("مبلغ پیش فرض پرداخت", style = MaterialTheme.typography.bodyLarge)
- Text("${formatNumberAsPersian(defaultAmount)} تومان", style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.primary)
+ Text(
+ "مبلغ پیش فرض پرداخت",
+ style = MaterialTheme.typography.bodyLarge
+ )
+ Text(
+ "${formatNumberAsPersian(defaultAmount)} تومان",
+ style = MaterialTheme.typography.titleMedium,
+ fontWeight = FontWeight.Bold,
+ color = MaterialTheme.colorScheme.primary
+ )
}
IconButton(onClick = { isEditing = true }) {
Icon(Icons.Filled.Edit, contentDescription = "Edit Amount")
@@ -253,16 +313,21 @@ fun DefaultAmountSetting(defaultAmount: Double, onSave: (String) -> Unit) {
}
}
-
@Composable
-fun FontSelectionDropdown(selectedFont: String, onFontSelected: (String) -> Unit) {
+fun FontSelectionDropdown(
+ selectedFont: String,
+ onFontSelected: (String) -> Unit
+) {
var expanded by remember { mutableStateOf(false) }
val fonts = listOf("Estedad", "Vazirmatn", "BYekan", "Sahel", "IranianSans")
Box(modifier = Modifier.fillMaxWidth()) {
- OutlinedButton(onClick = { expanded = true }, modifier = Modifier.fillMaxWidth()) {
+ OutlinedButton(
+ onClick = { expanded = true },
+ modifier = Modifier.fillMaxWidth()
+ ) {
Text("فونت برنامه: $selectedFont")
- Icon(Icons.Default.ArrowDropDown, contentDescription = "Select Font")
+ Icon(Icons.Filled.ArrowDropDown, contentDescription = "Select Font")
}
DropdownMenu(
expanded = expanded,
diff --git a/app/src/main/java/com/oqba26/monthlypaymentapp/ui/screens/SharedComponents.kt b/app/src/main/java/com/oqba26/monthlypaymentapp/ui/screens/SharedComponents.kt
new file mode 100644
index 0000000..310a80c
--- /dev/null
+++ b/app/src/main/java/com/oqba26/monthlypaymentapp/ui/screens/SharedComponents.kt
@@ -0,0 +1,108 @@
+@file:Suppress("unused")
+
+package com.oqba26.monthlypaymentapp.ui.screens
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.text.KeyboardOptions
+import androidx.compose.material3.AlertDialog
+import androidx.compose.material3.Button
+import androidx.compose.material3.ButtonDefaults
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.OutlinedTextField
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.text.input.KeyboardType
+import androidx.compose.ui.unit.dp
+import com.oqba26.monthlypaymentapp.data.model.PaymentRecord
+import com.oqba26.monthlypaymentapp.utils.PersianNumberVisualTransformation
+
+@Composable
+fun EditPaymentDialog(
+ paymentRecord: PaymentRecord,
+ onConfirm: (amount: Double, description: String) -> Unit,
+ onDelete: () -> Unit,
+ onDismiss: () -> Unit
+) {
+ var amountText by remember { mutableStateOf(paymentRecord.amount.toLong().toString()) }
+ var description by remember { mutableStateOf(paymentRecord.description) }
+
+ AlertDialog(
+ onDismissRequest = onDismiss,
+ title = { Text("ویرایش پرداخت") },
+ text = {
+ Column {
+ OutlinedTextField(
+ value = amountText,
+ onValueChange = { value ->
+ amountText = value.filter { c -> c.isDigit() }
+ },
+ label = { Text("مبلغ") },
+ keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
+ visualTransformation = PersianNumberVisualTransformation(),
+ modifier = Modifier.fillMaxWidth()
+ )
+ Spacer(modifier = Modifier.height(8.dp))
+ OutlinedTextField(
+ value = description,
+ onValueChange = { description = it },
+ label = { Text("توضیحات (اختیاری)") },
+ modifier = Modifier.fillMaxWidth()
+ )
+ }
+ },
+ // تمام دکمهها را خودمان داخل confirmButton میچینیم
+ confirmButton = {
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 16.dp, vertical = 8.dp)
+ ) {
+ // در RTL: Start = راست، End = چپ
+
+ // راست: ذخیره
+ Button(
+ onClick = {
+ val amount = amountText.toDoubleOrNull() ?: paymentRecord.amount
+ onConfirm(amount, description)
+ },
+ modifier = Modifier.align(androidx.compose.ui.Alignment.CenterStart)
+ ) {
+ Text("ذخیره")
+ }
+
+ // وسط: لغو
+ Button(
+ onClick = onDismiss,
+ modifier = Modifier.align(androidx.compose.ui.Alignment.Center),
+ colors = ButtonDefaults.buttonColors(
+ containerColor = MaterialTheme.colorScheme.secondary
+ )
+ ) {
+ Text("لغو")
+ }
+
+ // چپ: حذف
+ Button(
+ onClick = onDelete,
+ modifier = Modifier.align(androidx.compose.ui.Alignment.CenterEnd),
+ colors = ButtonDefaults.buttonColors(
+ containerColor = MaterialTheme.colorScheme.error
+ )
+ ) {
+ Text("حذف")
+ }
+ }
+ },
+ dismissButton = {}
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/oqba26/monthlypaymentapp/utils/DateUtils.kt b/app/src/main/java/com/oqba26/monthlypaymentapp/utils/DateUtils.kt
index 3362ff5..8562015 100644
--- a/app/src/main/java/com/oqba26/monthlypaymentapp/utils/DateUtils.kt
+++ b/app/src/main/java/com/oqba26/monthlypaymentapp/utils/DateUtils.kt
@@ -1,9 +1,19 @@
package com.oqba26.monthlypaymentapp.utils
-import saman.zamani.persiandate.PersianDate
-import saman.zamani.persiandate.PersianDateFormat
-
-fun Long.toPersianDate(pattern: String = "Y/M/d"): String {
- val pd = PersianDate(this) // this = timestamp (ms)
- return PersianDateFormat(pattern).format(pd)
-}
\ No newline at end of file
+fun getPersianMonthName(month: Int): String {
+ return when (month) {
+ 1 -> "فروردین"
+ 2 -> "اردیبهشت"
+ 3 -> "خرداد"
+ 4 -> "تیر"
+ 5 -> "مرداد"
+ 6 -> "شهریور"
+ 7 -> "مهر"
+ 8 -> "آبان"
+ 9 -> "آذر"
+ 10 -> "دی"
+ 11 -> "بهمن"
+ 12 -> "اسفند"
+ else -> "نامشخص"
+ }
+}
diff --git a/app/src/main/java/com/oqba26/monthlypaymentapp/utils/ShamsiUtils.kt b/app/src/main/java/com/oqba26/monthlypaymentapp/utils/ShamsiUtils.kt
index 611f224..f88716e 100644
--- a/app/src/main/java/com/oqba26/monthlypaymentapp/utils/ShamsiUtils.kt
+++ b/app/src/main/java/com/oqba26/monthlypaymentapp/utils/ShamsiUtils.kt
@@ -1,7 +1,31 @@
+@file:Suppress("unused")
+
package com.oqba26.monthlypaymentapp.utils
+import android.icu.text.SimpleDateFormat
import saman.zamani.persiandate.PersianDate
+import java.util.Date
+import java.util.Locale
fun getCurrentShamsiYear(): Int = PersianDate().shYear
+fun getCurrentShamsiMonth(): Int = PersianDate().shMonth
-fun getCurrentShamsiMonth(): Int = PersianDate().shMonth
\ No newline at end of file
+fun formatTimestampToPersianDate(timestamp: Long): String {
+ val persianDate = PersianDate(Date(timestamp))
+ val year = persianDate.shYear.toString().toPersianDigits()
+ val month = persianDate.shMonth.toString().padStart(2, '0').toPersianDigits()
+ val day = persianDate.shDay.toString().padStart(2, '0').toPersianDigits()
+ return "$year/$month/$day"
+}
+
+fun formatTimestampToPersianTime(timestamp: Long): String {
+ val timeFormat = SimpleDateFormat("HH:mm", Locale.US)
+ val timeString = timeFormat.format(Date(timestamp))
+ return timeString.toPersianDigits()
+}
+
+fun formatTimestampToPersianDateTime(timestamp: Long): String {
+ val date = formatTimestampToPersianDate(timestamp)
+ val time = formatTimestampToPersianTime(timestamp)
+ return "$date، ساعت $time"
+}
diff --git a/app/src/main/java/com/oqba26/monthlypaymentapp/viewmodel/ApiModels.kt b/app/src/main/java/com/oqba26/monthlypaymentapp/viewmodel/ApiModels.kt
new file mode 100644
index 0000000..63af448
--- /dev/null
+++ b/app/src/main/java/com/oqba26/monthlypaymentapp/viewmodel/ApiModels.kt
@@ -0,0 +1,17 @@
+@file:OptIn(kotlinx.serialization.InternalSerializationApi::class)
+
+package com.oqba26.monthlypaymentapp.viewmodel
+
+import kotlinx.serialization.Serializable
+
+@Serializable
+@Suppress("unused")
+data class UpdateArchivedStatusRequest(
+ val isArchived: Boolean
+)
+
+@Serializable
+@Suppress("unused")
+data class UpdateDisplayOrderRequest(
+ val displayOrder: Long // FIX: Changed from Int to Long
+)
\ No newline at end of file
diff --git a/app/src/main/java/com/oqba26/monthlypaymentapp/viewmodel/PersonDetailViewModel.kt b/app/src/main/java/com/oqba26/monthlypaymentapp/viewmodel/PersonDetailViewModel.kt
deleted file mode 100644
index 1691bc0..0000000
--- a/app/src/main/java/com/oqba26/monthlypaymentapp/viewmodel/PersonDetailViewModel.kt
+++ /dev/null
@@ -1,97 +0,0 @@
-package com.oqba26.monthlypaymentapp.viewmodel
-
-import androidx.lifecycle.SavedStateHandle
-import androidx.lifecycle.ViewModel
-import androidx.lifecycle.ViewModelProvider
-import androidx.lifecycle.createSavedStateHandle
-import androidx.lifecycle.viewModelScope
-import androidx.lifecycle.viewmodel.CreationExtras
-import com.oqba26.monthlypaymentapp.core.PaymentApplication
-import com.oqba26.monthlypaymentapp.data.model.PaymentRecord
-import com.oqba26.monthlypaymentapp.data.model.Person
-import com.oqba26.monthlypaymentapp.data.repository.NetworkRepository
-import com.oqba26.monthlypaymentapp.data.repository.SettingsRepository
-import com.oqba26.monthlypaymentapp.utils.getCurrentShamsiYear
-import kotlinx.coroutines.flow.MutableSharedFlow
-import kotlinx.coroutines.flow.SharingStarted
-import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.asSharedFlow
-import kotlinx.coroutines.flow.asStateFlow
-import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.stateIn
-import kotlinx.coroutines.flow.update
-import kotlinx.coroutines.launch
-
-class PersonDetailViewModel(
- savedStateHandle: SavedStateHandle,
- private val networkRepository: NetworkRepository, // ⭐️ FIX: localRepository removed
- private val settingsRepository: SettingsRepository
-) : ViewModel() {
-
- private val personId: String = requireNotNull(savedStateHandle.get("personId"))
-
- // ⭐️ FIX: Fetch person from the network flow
- val person: StateFlow = networkRepository.getPersonsFlow()
- .map { persons -> persons.find { it.id == personId } }
- .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), null)
-
- private val _selectedYear = kotlinx.coroutines.flow.MutableStateFlow(getCurrentShamsiYear())
- val selectedYear = _selectedYear.asStateFlow()
-
- // ⭐️ FIX: Fetch payment history from the correct network flow
- val paymentHistory: StateFlow> = networkRepository.getPaymentsFlow()
- .map { payments -> payments.filter { it.personId == personId } }
- .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList())
-
- private val _toastMessage = MutableSharedFlow()
- val toastMessage = _toastMessage.asSharedFlow()
-
- val defaultPaymentAmount = settingsRepository.defaultPaymentAmountFlow
- .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), 0.0)
-
- fun changeYear(offset: Int) { _selectedYear.update { it + offset } }
-
- fun addPaymentForMonth(month: Int, year: Int, amount: Double) {
- viewModelScope.launch {
- val newPayment = PaymentRecord(
- amount = amount,
- personId = personId,
- shamsiYear = year,
- shamsiMonth = month
- )
- val ok = networkRepository.addPayment(newPayment)
- if (!ok) _toastMessage.emit("ثبت پرداخت با خطا مواجه شد.")
- }
- }
-
- fun updatePayment(payment: PaymentRecord, newAmount: Double) {
- viewModelScope.launch {
- val updatedPayment = payment.copy(amount = newAmount)
- val ok = networkRepository.addPayment(updatedPayment)
- if (!ok) _toastMessage.emit("بهروزرسانی پرداخت با خطا مواجه شد.")
- }
- }
-
- fun deletePayment(payment: PaymentRecord) {
- viewModelScope.launch {
- val ok = networkRepository.deletePayment(payment.id)
- if (!ok) _toastMessage.emit("حذف پرداخت با خطا مواجه شد.")
- }
- }
-
- companion object {
- val Factory: ViewModelProvider.Factory = object : ViewModelProvider.Factory {
- @Suppress("UNCHECKED_CAST")
- override fun create(modelClass: Class, extras: CreationExtras): T {
- val app = checkNotNull(extras[ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY]) as PaymentApplication
- val saved = extras.createSavedStateHandle()
- // ⭐️ FIX: Provide the correct dependencies
- return PersonDetailViewModel(
- saved,
- app.networkRepository,
- app.settingsRepository
- ) as T
- }
- }
- }
-}
diff --git a/app/src/main/java/com/oqba26/monthlypaymentapp/viewmodel/PersonViewModel.kt b/app/src/main/java/com/oqba26/monthlypaymentapp/viewmodel/PersonViewModel.kt
index a0ad1d4..b21f84b 100644
--- a/app/src/main/java/com/oqba26/monthlypaymentapp/viewmodel/PersonViewModel.kt
+++ b/app/src/main/java/com/oqba26/monthlypaymentapp/viewmodel/PersonViewModel.kt
@@ -31,84 +31,431 @@ sealed interface AuthState {
data object Unauthenticated : AuthState
}
+enum class PersonListType {
+ UNPAID,
+ PAID
+}
+
+@OptIn(ExperimentalCoroutinesApi::class)
class PersonViewModel(
private val networkRepository: NetworkRepository,
private val settingsRepository: SettingsRepository
) : ViewModel() {
private val _searchQuery = MutableStateFlow("")
- val searchQuery = _searchQuery.asStateFlow()
+ val searchQuery: StateFlow = _searchQuery.asStateFlow()
private val _isRefreshing = MutableStateFlow(false)
- val isRefreshing = _isRefreshing.asStateFlow()
-
- private val _expandedPersonId = MutableStateFlow(null)
- val expandedPersonId = _expandedPersonId.asStateFlow()
+ @Suppress("unused")
+ val isRefreshing: StateFlow = _isRefreshing.asStateFlow()
+ private val _selectedPersonId = MutableStateFlow(null)
private val _selectedYear = MutableStateFlow(getCurrentShamsiYear())
- val selectedYear = _selectedYear.asStateFlow()
private val _dashboardData = MutableStateFlow(DashboardUiModel())
- val dashboardData = _dashboardData.asStateFlow()
+ val dashboardData: StateFlow = _dashboardData.asStateFlow()
val defaultPaymentAmountFlow = settingsRepository.defaultPaymentAmountFlow
private val _toastMessage = MutableSharedFlow()
val toastMessage = _toastMessage.asSharedFlow()
- val authState: StateFlow = settingsRepository.authTokenFlow.map { token ->
- if (token != null) AuthState.Authenticated else AuthState.Unauthenticated
- }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), AuthState.Loading)
+ private val _infoMessage = MutableSharedFlow()
+ val infoMessage = _infoMessage.asSharedFlow()
- @OptIn(ExperimentalCoroutinesApi::class)
- val uiState: StateFlow = authState.flatMapLatest { auth ->
- when (auth) {
- is AuthState.Authenticated -> {
+ private val _uiState = MutableStateFlow(PersonListUiState())
+ val uiState: StateFlow = _uiState.asStateFlow()
+
+ private val _showAddPersonDialog = MutableStateFlow(false)
+ val showAddPersonDialog: StateFlow = _showAddPersonDialog.asStateFlow()
+
+ private val _personForPaymentDialog = MutableStateFlow(null)
+ val personForPaymentDialog: StateFlow = _personForPaymentDialog.asStateFlow()
+
+
+ val authState: StateFlow = settingsRepository.authTokenFlow
+ .map { token ->
+ if (token != null) AuthState.Authenticated else AuthState.Unauthenticated
+ }
+ .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), AuthState.Loading)
+
+ val personDetailState: StateFlow = _selectedPersonId
+ .flatMapLatest { personId ->
+ if (personId == null) {
+ flowOf(PersonDetailUiState())
+ } else {
combine(
networkRepository.getPersonsFlow(),
networkRepository.getPaymentsFlow(),
- _searchQuery
- ) { persons, allPayments, query ->
- val currentMonth = getCurrentShamsiMonth()
- val currentYear = getCurrentShamsiYear()
- val paymentsThisMonth = allPayments.filter { it.shamsiMonth == currentMonth && it.shamsiYear == currentYear }
-
- val uiModels = persons.map { person ->
- val hasPaid = paymentsThisMonth.any { it.personId == person.id }
- PersonUiModel(id = person.id, name = person.name, hasPaidThisMonth = hasPaid)
+ _selectedYear
+ ) { persons, allPayments, year ->
+ val person = persons.find { it.id == personId }
+ val paymentsForPersonInYear = allPayments.filter {
+ it.personId == personId && it.shamsiYear == year
}
-
- updateDashboard(uiModels.size, paymentsThisMonth)
-
- val filtered = if (query.isBlank()) uiModels
- else uiModels.filter { it.name.contains(query, ignoreCase = true) }
-
- PersonListUiState(persons = filtered, payments = allPayments)
+ val currentYear = getCurrentShamsiYear()
+ val currentMonth = getCurrentShamsiMonth()
+ val monthStates = (1..12).map { month ->
+ val payment = paymentsForPersonInYear.find { it.shamsiMonth == month }
+ val status = when {
+ year < currentYear -> MonthStatus.PAST_YEAR
+ year > currentYear -> MonthStatus.FUTURE_YEAR
+ month > currentMonth -> MonthStatus.FUTURE_MONTH
+ payment != null -> MonthStatus.PAID
+ else -> MonthStatus.AVAILABLE
+ }
+ MonthUiModel(month, payment, status)
+ }
+ PersonDetailUiState(person, year, monthStates)
}
}
- else -> {
- flowOf(PersonListUiState()) // Emit empty state if not authenticated
- }
}
- }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), PersonListUiState())
+ .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), PersonDetailUiState())
init {
viewModelScope.launch {
- // This will trigger a refresh whenever the user becomes authenticated.
- authState.collectLatest { state ->
- if (state is AuthState.Authenticated) {
+ authState.collectLatest { authState ->
+ if (authState is AuthState.Authenticated) {
+ // اولین بار دیتای سرور
networkRepository.refresh()
+
+ // گوش دادن به تغییرات اشخاص + پرداختها + سرچ
+ combine(
+ networkRepository.getPersonsFlow(),
+ networkRepository.getPaymentsFlow(),
+ _searchQuery
+ ) { persons, allPayments, query ->
+ val currentMonth = getCurrentShamsiMonth()
+ val currentYear = getCurrentShamsiYear()
+
+ val paymentsThisMonth = allPayments.filter {
+ it.shamsiMonth == currentMonth && it.shamsiYear == currentYear
+ }
+
+ val activePersons = persons
+ .filter { !it.isArchived }
+ .sortedWith(compareBy({ it.displayOrder }, { it.createdAt }))
+
+ val uiModels = activePersons.map { person ->
+ val hasPaid = paymentsThisMonth.any { it.personId == person.id }
+ PersonUiModel(
+ id = person.id,
+ name = person.name,
+ hasPaidThisMonth = hasPaid,
+ displayOrder = person.displayOrder,
+ createdAt = person.createdAt
+ )
+ }
+
+ updateDashboard(uiModels.size, paymentsThisMonth)
+
+ val (paidModels, unpaidModels) =
+ uiModels.partition { it.hasPaidThisMonth }
+
+ val filteredPaid =
+ if (query.isBlank()) paidModels
+ else paidModels.filter { it.name.contains(query, ignoreCase = true) }
+
+ val filteredUnpaid =
+ if (query.isBlank()) unpaidModels
+ else unpaidModels.filter { it.name.contains(query, ignoreCase = true) }
+
+ val archivedPersons = persons
+ .filter { it.isArchived }
+ .sortedBy { it.displayOrder }
+ .map {
+ PersonUiModel(
+ id = it.id,
+ name = it.name,
+ hasPaidThisMonth = false,
+ displayOrder = it.displayOrder,
+ createdAt = it.createdAt
+ )
+ }
+
+ PersonListUiState(
+ unpaidPersons = filteredUnpaid,
+ paidPersons = filteredPaid,
+ archivedPersons = archivedPersons,
+ payments = allPayments
+ )
+ }.collect { newUiState ->
+ _uiState.value = newUiState
+ }
+ } else {
+ _uiState.value = PersonListUiState()
}
}
}
}
- // Auth Actions
+ fun onSearchQueryChange(query: String) {
+ _searchQuery.value = query
+ }
+
+ fun onAddPersonClicked() {
+ _showAddPersonDialog.value = true
+ }
+
+ fun onDismissAddPersonDialog() {
+ _showAddPersonDialog.value = false
+ }
+
+ fun onQuickPayClicked(person: PersonUiModel) {
+ _personForPaymentDialog.value = person
+ }
+
+ fun onDismissPaymentDialog() {
+ _personForPaymentDialog.value = null
+ }
+
+
+ fun onEvent(event: PersonScreenEvent) {
+ viewModelScope.launch {
+ when (event) {
+ is PersonScreenEvent.MovePersonNew -> {
+ // فقط جابهجا کردن لیست لوکال (بدون شبکه)
+ val fromIndex = event.fromIndex
+ val toIndex = event.toIndex
+ val currentListState = _uiState.value
+
+ val list = when (event.listType) {
+ PersonListType.UNPAID -> currentListState.unpaidPersons
+ PersonListType.PAID -> currentListState.paidPersons
+ }
+
+ if (fromIndex !in list.indices || toIndex !in list.indices) return@launch
+
+ val reorderedList = list.toMutableList().apply {
+ add(toIndex, removeAt(fromIndex))
+ }
+
+ _uiState.update {
+ when (event.listType) {
+ PersonListType.UNPAID -> it.copy(unpaidPersons = reorderedList)
+ PersonListType.PAID -> it.copy(paidPersons = reorderedList)
+ }
+ }
+ }
+
+ is PersonScreenEvent.AddPerson -> {
+ if (event.name.isNotBlank()) {
+ val person = Person(name = event.name, displayOrder = 0)
+ val statusCode = networkRepository.addPerson(person)
+ when (statusCode) {
+ 409 -> {
+ _toastMessage.emit("خطا: این نام از قبل وجود دارد.")
+ }
+ null, !in 200..299 -> {
+ _toastMessage.emit("خطا در افزودن شخص.")
+ }
+ else -> {
+ networkRepository.refresh()
+ }
+ }
+ }
+ }
+ is PersonScreenEvent.AddQuickPayment -> {
+ val record = createPaymentRecord(
+ event.personId,
+ getCurrentShamsiMonth(),
+ getCurrentShamsiYear(),
+ event.amount,
+ event.description
+ )
+
+ if (networkRepository.addPayment(record)) {
+ networkRepository.refresh()
+ } else {
+ _toastMessage.emit("خطا در ثبت پرداخت. لطفا دوباره تلاش کنید.")
+ }
+ }
+
+ else -> handleOtherEvents(event)
+ }
+ }
+ }
+
+ private fun handleOtherEvents(event: PersonScreenEvent) {
+ viewModelScope.launch {
+ when (event) {
+ is PersonScreenEvent.SelectPerson -> {
+ _selectedPersonId.value = event.personId
+ _selectedYear.value = getCurrentShamsiYear()
+ }
+
+ is PersonScreenEvent.DeletePerson -> {
+ if (networkRepository.deletePersonAndPayments(event.personId)) {
+ networkRepository.refresh()
+ }
+ }
+
+ is PersonScreenEvent.ArchivePerson -> {
+ if (networkRepository.updatePersonArchivedStatus(event.personId, true)) {
+ _toastMessage.emit("شخص به آرشیو انتقال یافت.")
+ networkRepository.refresh()
+ } else {
+ _toastMessage.emit("خطا در آرشیو کردن.")
+ }
+ }
+
+ is PersonScreenEvent.RestorePerson -> {
+ if (networkRepository.updatePersonArchivedStatus(event.personId, false)) {
+ _toastMessage.emit("شخص از آرشیو بازیابی شد.")
+ networkRepository.refresh()
+ } else {
+ _toastMessage.emit("خطا در بازیابی.")
+ }
+ }
+
+ is PersonScreenEvent.UpdatePerson -> {
+ if (networkRepository.updatePerson(event.personId, event.name)) {
+ networkRepository.refresh()
+ }
+ }
+
+ is PersonScreenEvent.Login -> login(AuthRequest(event.username, event.password))
+
+ is PersonScreenEvent.Register -> register(AuthRequest(event.username, event.password))
+
+ is PersonScreenEvent.ChangeYear -> {
+ when (event.offset) {
+ 1 -> {
+ if (_selectedYear.value < getCurrentShamsiYear()) {
+ _selectedYear.update { it + 1 }
+ } else {
+ _infoMessage.emit("امکان مشاهده سال آینده وجود ندارد.")
+ }
+ }
+
+ -1 -> {
+ val personId = _selectedPersonId.value ?: return@launch
+ val hasPaymentsInPast = uiState.value.payments.any {
+ it.personId == personId && it.shamsiYear < _selectedYear.value
+ }
+ if (hasPaymentsInPast) {
+ _selectedYear.update { it - 1 }
+ } else {
+ _infoMessage.emit("هیچ پرداختی برای سالهای گذشته ثبت نشده است.")
+ }
+ }
+ }
+ }
+
+ is PersonScreenEvent.AddPaymentForMonth -> {
+ val record = createPaymentRecord(
+ event.personId,
+ event.month,
+ event.year,
+ event.amount,
+ event.description
+ )
+ if (networkRepository.addPayment(record)) {
+ networkRepository.refresh()
+ } else {
+ _toastMessage.emit("خطا در ثبت پرداخت")
+ }
+ }
+
+ is PersonScreenEvent.UpdatePayment -> {
+ val updatedRecord = event.payment.copy(
+ amount = event.newAmount,
+ description = event.newDescription,
+ timestamp = System.currentTimeMillis()
+ )
+ if (networkRepository.addPayment(updatedRecord)) {
+ networkRepository.refresh()
+ } else {
+ _toastMessage.emit("خطا در ویرایش پرداخت")
+ }
+ }
+
+ is PersonScreenEvent.DeletePayment -> {
+ if (networkRepository.deletePayment(event.payment.id)) {
+ networkRepository.refresh()
+ } else {
+ _toastMessage.emit("خطا در حذف پرداخت")
+ }
+ }
+
+ is PersonScreenEvent.CommitReorder -> {
+ // بعد از اتمام درگ: ترتیب فعلی لیست را به عنوان displayOrder جدید ذخیره کن
+ val currentListState = _uiState.value
+ val list = when (event.listType) {
+ PersonListType.UNPAID -> currentListState.unpaidPersons
+ PersonListType.PAID -> currentListState.paidPersons
+ }
+
+ if (list.size < 2) return@launch
+
+ try {
+ list.forEachIndexed { index, person ->
+ val newOrder = (index + 1) * 10_000L
+ if (person.displayOrder != newOrder) {
+ val success = networkRepository.updatePersonDisplayOrder(
+ person.id,
+ newOrder
+ )
+ if (!success) {
+ throw Exception("update failed")
+ }
+ }
+ }
+ // بعد از ذخیره، از سرور رفرش کن تا همهچیز باهم سینک بشه
+ networkRepository.refresh()
+ } catch (_: Exception) {
+ _toastMessage.emit("خطا در ذخیره ترتیب")
+ networkRepository.refresh()
+ }
+ }
+
+ PersonScreenEvent.RefreshData -> {
+ _isRefreshing.value = true
+ networkRepository.refresh()
+ _isRefreshing.value = false
+ }
+
+ else -> {}
+ }
+ }
+ }
+
+ private fun createPaymentRecord(
+ personId: String,
+ month: Int,
+ year: Int,
+ amount: Double,
+ description: String
+ ): PaymentRecord {
+ return PaymentRecord(
+ personId = personId,
+ amount = amount,
+ shamsiYear = year,
+ shamsiMonth = month,
+ description = description
+ )
+ }
+
+ private fun updateDashboard(totalPersonCount: Int, paymentsThisMonth: List) {
+ val paidCount = paymentsThisMonth.map { it.personId }.distinct().size
+ val totalIncome = paymentsThisMonth.sumOf { it.amount }
+ val progress =
+ if (totalPersonCount > 0) paidCount.toFloat() / totalPersonCount.toFloat() else 0f
+ _dashboardData.value =
+ DashboardUiModel(paidCount, totalPersonCount, totalIncome, progress)
+ }
+
+ fun logout() = viewModelScope.launch {
+ settingsRepository.saveAuthData(null, null)
+ _toastMessage.emit("از حساب کاربری خارج شدید.")
+ }
+
fun login(request: AuthRequest) = viewModelScope.launch {
val response = networkRepository.login(request)
if (response != null && response.token.isNotBlank()) {
settingsRepository.saveAuthData(response.token, response.userId)
_toastMessage.emit("ورود موفقیتآمیز بود")
+ networkRepository.refresh()
} else {
_toastMessage.emit("نام کاربری یا رمز عبور اشتباه است.")
}
@@ -119,110 +466,57 @@ class PersonViewModel(
if (response != null && response.token.isNotBlank()) {
settingsRepository.saveAuthData(response.token, response.userId)
_toastMessage.emit("ثبت نام و ورود موفقیتآمیز بود")
+ networkRepository.refresh()
} else {
_toastMessage.emit("خطا در ثبت نام.")
}
}
-
- fun logout() = viewModelScope.launch {
- settingsRepository.saveAuthData(null, null)
- _toastMessage.emit("از حساب کاربری خارج شدید.")
- }
-
- // UI Events
- fun onSearchQueryChange(query: String) { _searchQuery.value = query }
-
- fun onEvent(event: PersonScreenEvent) {
- viewModelScope.launch {
- when (event) {
- is PersonScreenEvent.AddPerson -> {
- if (event.name.isNotBlank()) {
- val statusCode = networkRepository.addPerson(Person(name = event.name))
- if (statusCode == 409) {
- _toastMessage.emit("خطا: این نام از قبل وجود دارد.")
- } else if (statusCode == null || statusCode !in 200..299) {
- _toastMessage.emit("خطا در افزودن شخص.")
- }
- }
- }
- is PersonScreenEvent.DeletePerson -> {
- networkRepository.deletePersonAndPayments(event.personId)
- }
- is PersonScreenEvent.AddQuickPayment -> {
- val record = createPaymentRecord(event.personId, getCurrentShamsiMonth(), getCurrentShamsiYear(), event.amount)
- networkRepository.addPayment(record)
- }
- is PersonScreenEvent.UpdatePerson -> {
- networkRepository.updatePerson(event.personId, event.name)
- }
- is PersonScreenEvent.Login -> login(AuthRequest(event.username, event.password))
- is PersonScreenEvent.Register -> register(AuthRequest(event.username, event.password))
- is PersonScreenEvent.TogglePersonExpansion -> {
- val currentId = _expandedPersonId.value
- if (currentId == event.personId) {
- _expandedPersonId.value = null
- } else {
- _expandedPersonId.value = event.personId
- _selectedYear.value = getCurrentShamsiYear() // Reset year when opening a new item
- }
- }
- is PersonScreenEvent.ChangeYear -> {
- _selectedYear.update { it + event.offset }
- }
- is PersonScreenEvent.AddPaymentForMonth -> {
- val record = createPaymentRecord(event.personId, event.month, event.year, event.amount)
- val success = networkRepository.addPayment(record)
- if (!success) _toastMessage.emit("خطا در ثبت پرداخت")
- }
- is PersonScreenEvent.UpdatePayment -> {
- val updatedRecord = event.payment.copy(amount = event.newAmount)
- val success = networkRepository.addPayment(updatedRecord)
- if (!success) _toastMessage.emit("خطا در ویرایش پرداخت")
- }
- is PersonScreenEvent.DeletePayment -> {
- val success = networkRepository.deletePayment(event.payment.id)
- if (!success) _toastMessage.emit("خطا در حذف پرداخت")
- }
-
- PersonScreenEvent.RefreshData -> {
- _isRefreshing.value = true
- networkRepository.refresh()
- _isRefreshing.value = false
- }
- }
- }
- }
-
- private fun updateDashboard(totalPersonCount: Int, paymentsThisMonth: List) {
- val paidCount = paymentsThisMonth.map { it.personId }.distinct().size
- val totalIncome = paymentsThisMonth.sumOf { it.amount }
- val progress = if (totalPersonCount > 0) paidCount.toFloat() / totalPersonCount.toFloat() else 0f
- _dashboardData.value = DashboardUiModel(paidCount, totalPersonCount, totalIncome, progress)
- }
-
- private fun createPaymentRecord(personId: String, month: Int, year: Int, amount: Double): PaymentRecord {
- return PaymentRecord(personId = personId, amount = amount, shamsiYear = year, shamsiMonth = month)
- }
}
-data class PersonListUiState(
- val persons: List = emptyList(),
- val payments: List = emptyList()
-)
-
sealed class PersonScreenEvent {
data object RefreshData : PersonScreenEvent()
data class AddPerson(val name: String) : PersonScreenEvent()
data class UpdatePerson(val personId: String, val name: String) : PersonScreenEvent()
data class DeletePerson(val personId: String) : PersonScreenEvent()
- data class AddQuickPayment(val personId: String, val amount: Double) : PersonScreenEvent()
+ data class ArchivePerson(val personId: String) : PersonScreenEvent()
+ data class RestorePerson(val personId: String) : PersonScreenEvent()
+
+ // درگ اند دراپ
+ data class MovePersonNew(
+ val fromIndex: Int,
+ val toIndex: Int,
+ val listType: PersonListType
+ ) : PersonScreenEvent()
+
+ // پایان درگ: ذخیره ترتیب
+ data class CommitReorder(
+ val listType: PersonListType
+ ) : PersonScreenEvent()
+
+ data class AddQuickPayment(
+ val personId: String,
+ val amount: Double,
+ val description: String
+ ) : PersonScreenEvent()
+
data class Login(val username: String, val password: String) : PersonScreenEvent()
data class Register(val username: String, val password: String) : PersonScreenEvent()
- data class TogglePersonExpansion(val personId: String) : PersonScreenEvent()
- // Events for the detail section
+ data class SelectPerson(val personId: String) : PersonScreenEvent()
data class ChangeYear(val offset: Int) : PersonScreenEvent()
- data class AddPaymentForMonth(val personId: String, val month: Int, val year: Int, val amount: Double) : PersonScreenEvent()
- data class UpdatePayment(val payment: PaymentRecord, val newAmount: Double) : PersonScreenEvent()
+ data class AddPaymentForMonth(
+ val personId: String,
+ val month: Int,
+ val year: Int,
+ val amount: Double,
+ val description: String
+ ) : PersonScreenEvent()
+
+ data class UpdatePayment(
+ val payment: PaymentRecord,
+ val newAmount: Double,
+ val newDescription: String
+ ) : PersonScreenEvent()
+
data class DeletePayment(val payment: PaymentRecord) : PersonScreenEvent()
}
diff --git a/app/src/main/java/com/oqba26/monthlypaymentapp/viewmodel/PersonViewModelFactory.kt b/app/src/main/java/com/oqba26/monthlypaymentapp/viewmodel/PersonViewModelFactory.kt
deleted file mode 100644
index 2d1488f..0000000
--- a/app/src/main/java/com/oqba26/monthlypaymentapp/viewmodel/PersonViewModelFactory.kt
+++ /dev/null
@@ -1,22 +0,0 @@
-package com.oqba26.monthlypaymentapp.viewmodel
-
-import androidx.lifecycle.ViewModel
-import androidx.lifecycle.ViewModelProvider
-import com.oqba26.monthlypaymentapp.data.repository.NetworkRepository
-import com.oqba26.monthlypaymentapp.data.repository.SettingsRepository
-
-// ⭐️ FIX: Removed the unnecessary 'localPersonRepository' dependency.
-class PersonViewModelFactory(
- private val networkRepository: NetworkRepository,
- private val settingsRepository: SettingsRepository
-) : ViewModelProvider.Factory {
-
- @Suppress("UNCHECKED_CAST")
- override fun create(modelClass: Class): T {
- if (modelClass.isAssignableFrom(PersonViewModel::class.java)) {
- // ⭐️ FIX: Call the ViewModel with the correct arguments.
- return PersonViewModel(networkRepository, settingsRepository) as T
- }
- throw IllegalArgumentException("Unknown ViewModel class")
- }
-}
diff --git a/app/src/main/java/com/oqba26/monthlypaymentapp/viewmodel/UiModels.kt b/app/src/main/java/com/oqba26/monthlypaymentapp/viewmodel/UiModels.kt
index 72776b3..b665610 100644
--- a/app/src/main/java/com/oqba26/monthlypaymentapp/viewmodel/UiModels.kt
+++ b/app/src/main/java/com/oqba26/monthlypaymentapp/viewmodel/UiModels.kt
@@ -1,16 +1,46 @@
package com.oqba26.monthlypaymentapp.viewmodel
+import com.oqba26.monthlypaymentapp.data.model.PaymentRecord
+import com.oqba26.monthlypaymentapp.data.model.Person
+
data class PersonUiModel(
val id: String,
val name: String,
- val hasPaidThisMonth: Boolean
+ val hasPaidThisMonth: Boolean,
+ val displayOrder: Long, // FIX: Changed from Int to Long
+ val createdAt: Long
)
+data class PersonListUiState(
+ val unpaidPersons: List = emptyList(),
+ val paidPersons: List = emptyList(),
+ val archivedPersons: List = emptyList(),
+ val payments: List = emptyList(),
+)
+
+data class PersonDetailUiState(
+ val person: Person? = null,
+ val selectedYear: Int = 0,
+ val monthStates: List = emptyList()
+)
+
+data class MonthUiModel(
+ val month: Int,
+ val payment: PaymentRecord?,
+ val status: MonthStatus
+)
+
+enum class MonthStatus {
+ PAID,
+ AVAILABLE,
+ FUTURE_MONTH,
+ PAST_YEAR,
+ FUTURE_YEAR
+}
+
data class DashboardUiModel(
val paidCount: Int = 0,
val totalCount: Int = 0,
val totalIncome: Double = 0.0,
val progress: Float = 0f
)
-
-enum class MonthStatus { PAID, AVAILABLE, FUTURE_YEAR, PAST_YEAR, FUTURE_MONTH }
\ No newline at end of file
diff --git a/build.gradle.kts b/build.gradle.kts
index 4c2d391..2977b40 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -1,7 +1,7 @@
plugins {
alias(libs.plugins.android.application) apply false
+ alias(libs.plugins.android.library) apply false
alias(libs.plugins.jetbrains.kotlin.android) apply false
alias(libs.plugins.kotlin.serialization) apply false
alias(libs.plugins.ksp) apply false
-
-}
\ No newline at end of file
+}
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 320b8cb..783705e 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -11,6 +11,7 @@ coreKtx = "1.13.1"
lifecycle = "2.7.0"
activityCompose = "1.9.0"
navigation = "2.7.7"
+appcompat = "1.7.0"
retrofit = "2.9.0"
okhttp = "4.12.0"
@@ -21,7 +22,7 @@ datastore = "1.1.1"
desugarLibs = "2.0.4"
serialization = "1.6.3"
-persianDate = "1.5.2"
+persianDate = "0.2.0"
material = "1.12.0"
junit = "4.13.2"
@@ -30,6 +31,7 @@ espressoCore = "3.5.1"
[libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
+androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycle" }
androidx-lifecycle-viewmodel-savedstate = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-savedstate", version.ref = "lifecycle" }
androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" }
@@ -59,7 +61,7 @@ androidx-datastore-preferences = { group = "androidx.datastore", name = "datasto
android-desugar-jdk-libs = { group = "com.android.tools", name = "desugar_jdk_libs", version.ref = "desugarLibs" }
-persiandate = { module = "com.github.samanzamani:PersianDate", version.ref = "persianDate" }
+persiandate = { module = "com.github.mfathi91:time", version.ref = "persianDate" }
google-material = { group = "com.google.android.material", name = "material", version.ref = "material" }
@@ -72,6 +74,7 @@ androidTest-compose-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
+android-library = { id = "com.android.library", version.ref = "agp" }
jetbrains-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
\ No newline at end of file
diff --git a/settings.gradle.kts b/settings.gradle.kts
index fbd0b1d..70a3684 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -33,4 +33,4 @@ dependencyResolutionManagement {
}
rootProject.name = "MonthlyPaymentApp"
-include(":app")
\ No newline at end of file
+include(":app", ":PersianDate")
\ No newline at end of file