feat: add test items and group tests
This commit is contained in:
Generated
+1
-1
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="CompilerConfiguration">
|
||||
<bytecodeTargetLevel target="21" />
|
||||
<bytecodeTargetLevel target="17" />
|
||||
</component>
|
||||
</project>
|
||||
Generated
+2
-1
@@ -1,6 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="jbr-21" project-jdk-type="JavaSDK">
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="17" project-jdk-type="JavaSDK">
|
||||
<output url="file://$PROJECT_DIR$/build/classes" />
|
||||
</component>
|
||||
<component name="ProjectType">
|
||||
|
||||
@@ -31,11 +31,11 @@ android {
|
||||
}
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_11
|
||||
targetCompatibility = JavaVersion.VERSION_11
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
kotlinOptions {
|
||||
jvmTarget = "11"
|
||||
jvmTarget = "17"
|
||||
}
|
||||
buildFeatures {
|
||||
compose = true
|
||||
|
||||
@@ -29,4 +29,18 @@ interface TestGroupDao {
|
||||
|
||||
@Query("DELETE FROM testGroup")
|
||||
suspend fun deleteAllGroups()
|
||||
|
||||
@Query("""
|
||||
SELECT * FROM testGroup
|
||||
WHERE Fname LIKE '%' || :query || '%'
|
||||
OR Ename LIKE '%' || :query || '%'
|
||||
OR Detail LIKE '%' || :query || '%'
|
||||
ORDER BY
|
||||
CASE
|
||||
WHEN Fname LIKE '%' || :query || '%' THEN 1
|
||||
WHEN Ename LIKE '%' || :query || '%' THEN 2
|
||||
ELSE 3
|
||||
END
|
||||
""")
|
||||
fun searchGroups(query: String): Flow<List<TestGroupEntity>>
|
||||
}
|
||||
@@ -20,8 +20,18 @@ interface TestItemDao {
|
||||
|
||||
@Query("""
|
||||
SELECT ti.* FROM testItem ti
|
||||
INNER JOIN testGroup tg ON ti.Group_Id = tg.Id
|
||||
WHERE tg.Isparent = '0' AND ti.Title LIKE '%' || :searchQuery || '%'
|
||||
LEFT JOIN testGroup tg ON ti.Group_Id = tg.Id
|
||||
WHERE ti.Title LIKE '%' || :searchQuery || '%'
|
||||
OR ti.Normal_Value LIKE '%' || :searchQuery || '%'
|
||||
OR ti.Detail LIKE '%' || :searchQuery || '%'
|
||||
OR tg.Fname LIKE '%' || :searchQuery || '%'
|
||||
OR tg.Ename LIKE '%' || :searchQuery || '%'
|
||||
ORDER BY
|
||||
CASE
|
||||
WHEN ti.Title LIKE '%' || :searchQuery || '%' THEN 1
|
||||
WHEN tg.Fname LIKE '%' || :searchQuery || '%' THEN 2
|
||||
ELSE 3
|
||||
END
|
||||
""")
|
||||
fun searchTestItems(searchQuery: String): Flow<List<TestItemEntity>>
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import com.approagency.drug.data.local.dao.TestItemDao
|
||||
import com.approagency.drug.data.local.entities.TestGroupEntity
|
||||
import com.approagency.drug.data.local.entities.TestItemEntity
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
|
||||
class LabRepositoryImpl (
|
||||
private val testGroupDao: TestGroupDao,
|
||||
@@ -21,4 +22,14 @@ class LabRepositoryImpl (
|
||||
fun getItemsByGroupId(groupId: Int): Flow<List<TestItemEntity>> = testItemDao.getItemsByGroupId(groupId)
|
||||
suspend fun getItemById(itemId: Int): TestItemEntity? = testItemDao.getItemById(itemId)
|
||||
fun searchTestItems(query: String): Flow<List<TestItemEntity>> = testItemDao.searchTestItems(query)
|
||||
|
||||
|
||||
fun searchGroupsAndItems(searchQuery: String): Flow<Pair<List<TestGroupEntity>, List<TestItemEntity>>> {
|
||||
return combine(
|
||||
testGroupDao.searchGroups(searchQuery),
|
||||
testItemDao.searchTestItems(searchQuery)
|
||||
) { groups, items ->
|
||||
Pair(groups, items)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import com.approagency.drug.domain.usecase.GetDrugDetailUseCase
|
||||
import com.approagency.drug.domain.usecase.GetDrugSearchUseCase
|
||||
import com.approagency.drug.domain.usecase.GetTestGroupUseCase
|
||||
import com.approagency.drug.domain.usecase.GetTestItemByGroupId
|
||||
import com.approagency.drug.domain.usecase.SearchTestsUseCase
|
||||
import com.approagency.drug.presentation.viewModel.HomeViewModel
|
||||
import com.approagency.drug.presentation.viewModel.LabViewModel
|
||||
import com.approagency.drug.utils.Config
|
||||
@@ -68,12 +69,14 @@ val appModule= module {
|
||||
}
|
||||
|
||||
single { LabRepositoryImpl(get(), get()) }
|
||||
|
||||
single { SearchTestsUseCase(get()) }
|
||||
//view model
|
||||
viewModel {
|
||||
HomeViewModel(get() , get() , get())
|
||||
}
|
||||
|
||||
viewModel {
|
||||
LabViewModel(get() , get())
|
||||
LabViewModel(get() , get() , get())
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,22 @@
|
||||
package com.approagency.drug.domain.model
|
||||
|
||||
import com.approagency.drug.data.local.entities.TestGroupEntity
|
||||
|
||||
data class TestGroup(
|
||||
val id: Int,
|
||||
val ename: String?,
|
||||
val fname: String?,
|
||||
val detail: String?,
|
||||
val isParent: Boolean
|
||||
val isParent: String?,
|
||||
)
|
||||
|
||||
|
||||
fun TestGroupEntity.toTestGroup(): TestGroup {
|
||||
return TestGroup(
|
||||
id = this.id,
|
||||
ename = this.ename,
|
||||
fname = this.fname,
|
||||
detail = this.detail,
|
||||
isParent = this.isParent
|
||||
)
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
package com.approagency.drug.domain.model
|
||||
|
||||
import com.approagency.drug.data.local.entities.TestItemEntity
|
||||
|
||||
data class TestItem(
|
||||
val id: Int,
|
||||
val groupId: Int,
|
||||
@@ -7,3 +9,12 @@ data class TestItem(
|
||||
val normalValue: String?,
|
||||
val detail: String?
|
||||
)
|
||||
fun TestItemEntity.toTestItem(): TestItem {
|
||||
return TestItem(
|
||||
id = this.id,
|
||||
groupId = this.groupId,
|
||||
title = this.title,
|
||||
normalValue = this.normalValue,
|
||||
detail = this.detail
|
||||
)
|
||||
}
|
||||
@@ -16,7 +16,7 @@ class GetTestGroupUseCase (
|
||||
ename = entity.ename,
|
||||
fname = entity.fname,
|
||||
detail = entity.detail,
|
||||
isParent = entity.isParent == "1"
|
||||
isParent = entity.isParent
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.approagency.drug.domain.usecase
|
||||
|
||||
|
||||
import com.approagency.drug.data.repository.LabRepositoryImpl
|
||||
import com.approagency.drug.domain.model.TestGroup
|
||||
import com.approagency.drug.domain.model.TestItem
|
||||
import com.approagency.drug.domain.model.toTestGroup
|
||||
import com.approagency.drug.domain.model.toTestItem
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
class SearchTestsUseCase(
|
||||
private val repository: LabRepositoryImpl
|
||||
) {
|
||||
operator fun invoke(query: String): Flow<Pair<List<TestGroup>, List<TestItem>>> {
|
||||
return repository.searchGroupsAndItems(query).map { (groups, items) ->
|
||||
Pair(
|
||||
groups.map { it.toTestGroup() },
|
||||
items.map { it.toTestItem() }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
package com.approagency.drug.presentation.common
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.text.BasicTextField
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material.icons.filled.Search
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.getValue
|
||||
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.focus.onFocusChanged
|
||||
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.VisualTransformation
|
||||
import androidx.compose.ui.unit.LayoutDirection
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.vada.caller.ui.theme.dime
|
||||
|
||||
@Composable
|
||||
fun CustomTextFilled(
|
||||
value: String = "",
|
||||
onValueChange: (String) -> Unit,
|
||||
onSearch: (String) -> Unit = {},
|
||||
placeholder: String = "جستجو...",
|
||||
modifier: Modifier = Modifier,
|
||||
enabled: Boolean = true,
|
||||
readOnly: Boolean = false,
|
||||
singleLine: Boolean = true,
|
||||
showClearButton: Boolean = true,
|
||||
showSearchButton: Boolean = true,
|
||||
autoSearch: Boolean = false,
|
||||
searchDelay: Long = 500L,
|
||||
keyboardOptions: KeyboardOptions = KeyboardOptions(
|
||||
imeAction = ImeAction.Search
|
||||
),
|
||||
keyboardActions: KeyboardActions = KeyboardActions(
|
||||
onSearch = {
|
||||
if (value.isNotBlank()) {
|
||||
onSearch(value)
|
||||
}
|
||||
}
|
||||
),
|
||||
textStyle: androidx.compose.ui.text.TextStyle = MaterialTheme.typography.bodyLarge,
|
||||
hintStyle: androidx.compose.ui.text.TextStyle = MaterialTheme.typography.bodyMedium.copy(
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
),
|
||||
containerColor: androidx.compose.ui.graphics.Color = MaterialTheme.colorScheme.surfaceVariant,
|
||||
borderColor: androidx.compose.ui.graphics.Color = MaterialTheme.colorScheme.primaryContainer,
|
||||
shape: androidx.compose.ui.graphics.Shape = MaterialTheme.shapes.medium,
|
||||
height: Int = 45,
|
||||
onFocusChange: ((Boolean) -> Unit)? = null
|
||||
) {
|
||||
var isFocused by remember { mutableStateOf(false) }
|
||||
var localValue by remember(value) { mutableStateOf(value) }
|
||||
|
||||
// Auto-search functionality
|
||||
androidx.compose.runtime.LaunchedEffect(autoSearch, searchDelay, localValue) {
|
||||
if (autoSearch && localValue.isNotBlank()) {
|
||||
kotlinx.coroutines.delay(searchDelay)
|
||||
onSearch(localValue)
|
||||
}
|
||||
}
|
||||
|
||||
CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.height(height.dp)
|
||||
.background(
|
||||
color = containerColor,
|
||||
shape = shape
|
||||
)
|
||||
.border(
|
||||
width = 1.dp,
|
||||
color = if (isFocused)
|
||||
MaterialTheme.colorScheme.primary
|
||||
else
|
||||
borderColor,
|
||||
shape = shape
|
||||
)
|
||||
.padding(horizontal = MaterialTheme.dime.md, vertical = MaterialTheme.dime.sm)
|
||||
.onFocusChanged { focusState ->
|
||||
isFocused = focusState.isFocused
|
||||
onFocusChange?.invoke(focusState.isFocused)
|
||||
}
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
// Clear button (shows when there's text and clear button is enabled)
|
||||
if (showClearButton && localValue.isNotEmpty()) {
|
||||
IconButton(
|
||||
onClick = {
|
||||
localValue = ""
|
||||
onValueChange("")
|
||||
},
|
||||
modifier = Modifier.size(28.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Close,
|
||||
contentDescription = "Clear",
|
||||
modifier = Modifier.size(16.dp),
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Text field
|
||||
BasicTextField(
|
||||
value = localValue,
|
||||
onValueChange = { newValue ->
|
||||
localValue = newValue
|
||||
onValueChange(newValue)
|
||||
},
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(horizontal = if (showClearButton && localValue.isNotEmpty()) 4.dp else 0.dp),
|
||||
enabled = enabled,
|
||||
readOnly = readOnly,
|
||||
singleLine = singleLine,
|
||||
textStyle = textStyle,
|
||||
keyboardOptions = keyboardOptions,
|
||||
keyboardActions = keyboardActions,
|
||||
decorationBox = { innerTextField ->
|
||||
Box(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
contentAlignment = Alignment.CenterStart
|
||||
) {
|
||||
if (localValue.isEmpty() && !isFocused) {
|
||||
Text(
|
||||
text = placeholder,
|
||||
style = hintStyle
|
||||
)
|
||||
}
|
||||
innerTextField()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// Search button
|
||||
if (showSearchButton) {
|
||||
IconButton(
|
||||
onClick = {
|
||||
if (localValue.isNotBlank()) {
|
||||
onSearch(localValue)
|
||||
}
|
||||
},
|
||||
modifier = Modifier.size(32.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Search,
|
||||
contentDescription = "Search",
|
||||
modifier = Modifier.size(18.dp),
|
||||
tint = if (localValue.isNotBlank())
|
||||
MaterialTheme.colorScheme.primary
|
||||
else
|
||||
MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -43,40 +43,15 @@ fun DarmanContent(
|
||||
){
|
||||
val drugs = darmanData?.getOrNull()?.data ?: emptyList()
|
||||
if (drugs.isEmpty()){
|
||||
CustomBox(
|
||||
child = {
|
||||
Column {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(all = MaterialTheme.dime.sm),
|
||||
) {
|
||||
Text(
|
||||
text = "مشکل در دریافت اطلاعات",
|
||||
fontWeight = FontWeight.W500
|
||||
)
|
||||
Spacer(modifier = Modifier.width(MaterialTheme.dime.xs))
|
||||
Icon(
|
||||
imageVector = Icons.Default.Search,
|
||||
contentDescription = "Search"
|
||||
)
|
||||
}
|
||||
PrimaryButton(
|
||||
text = "تلاش دوباره",
|
||||
height = 45,
|
||||
isLoading = false,
|
||||
RetryContent(
|
||||
modifier = Modifier,
|
||||
error = "مشکلی از سمت سرور پیش آمده",
|
||||
onClick = {
|
||||
onRetryClick()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}else {
|
||||
LazyColumn(
|
||||
|
||||
){
|
||||
} else {
|
||||
LazyColumn {
|
||||
items(drugs.count() , itemContent ={
|
||||
i -> DarmanItem(
|
||||
darman = darmanData?.getOrNull()!!.data[i] ,
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
package com.approagency.drug.presentation.components
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.approagency.drug.presentation.common.CustomBox
|
||||
import com.approagency.drug.presentation.common.PrimaryButton
|
||||
import com.vada.caller.ui.theme.dime
|
||||
|
||||
@Composable
|
||||
fun RetryContent(modifier: Modifier = Modifier , error:String ,onClick:()-> Unit ) {
|
||||
CustomBox(
|
||||
child = {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = Modifier.padding(MaterialTheme.dime.md)
|
||||
) {
|
||||
Text(
|
||||
text = "خطا در دریافت اطلاعات",
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.error
|
||||
)
|
||||
Text(
|
||||
text = error,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
modifier = Modifier.padding(vertical = 8.dp)
|
||||
)
|
||||
PrimaryButton(
|
||||
text = "تلاش مجدد",
|
||||
height = 40,
|
||||
isLoading = false,
|
||||
onClick = { onClick() }
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
+205
@@ -0,0 +1,205 @@
|
||||
package com.approagency.drug.presentation.components
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
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.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
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.approagency.drug.domain.model.TestGroup
|
||||
import com.approagency.drug.domain.model.TestItem
|
||||
import com.approagency.drug.presentation.common.CustomBox
|
||||
import com.approagency.drug.presentation.common.Loading
|
||||
import com.approagency.drug.presentation.viewModel.SearchResult
|
||||
import com.vada.caller.ui.theme.dime
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.ui.AbsoluteAlignment
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextDirection
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
|
||||
@Composable
|
||||
fun SearchResultsContent(
|
||||
searchResult: SearchResult?,
|
||||
isLoading: Boolean,
|
||||
error: String?,
|
||||
onGroupClick: (TestGroup) -> Unit,
|
||||
onItemClick: (TestItem) -> Unit,
|
||||
onClearSearch: () -> Unit
|
||||
) {
|
||||
when {
|
||||
isLoading -> {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
Loading(color = MaterialTheme.colorScheme.primary)
|
||||
Spacer(modifier = Modifier.height(MaterialTheme.dime.sm))
|
||||
Text("در حال جستجو...")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
error != null -> {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
Text(
|
||||
text = "خطا در جستجو",
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.error
|
||||
)
|
||||
Text(
|
||||
text = error,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
modifier = Modifier.padding(vertical = 8.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
searchResult != null -> {
|
||||
val groups = searchResult.groups
|
||||
val items = searchResult.items
|
||||
|
||||
if (groups.isEmpty() && items.isEmpty()) {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
Text(
|
||||
text = "نتیجهای یافت نشد",
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
)
|
||||
Spacer(modifier = Modifier.height(MaterialTheme.dime.sm))
|
||||
Text(
|
||||
text = "لطفاً عبارت دیگری را جستجو کنید",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(vertical = MaterialTheme.dime.md),
|
||||
horizontalAlignment = AbsoluteAlignment.Right
|
||||
|
||||
) {
|
||||
if (groups.isNotEmpty()) {
|
||||
item {
|
||||
Text(
|
||||
text = "گروههای آزمایشگاهی (${groups.size})",
|
||||
style = MaterialTheme.typography.titleMedium.copy(
|
||||
textAlign = TextAlign.Center
|
||||
),
|
||||
fontWeight = FontWeight.Bold,
|
||||
modifier = Modifier.padding(vertical = MaterialTheme.dime.md)
|
||||
)
|
||||
}
|
||||
|
||||
items(groups) { group ->
|
||||
TestGroupItemContent(
|
||||
group = group,
|
||||
onClick = { onGroupClick(group) }
|
||||
)
|
||||
}
|
||||
|
||||
if (items.isNotEmpty()) {
|
||||
item {
|
||||
HorizontalDivider(
|
||||
modifier = Modifier.padding(vertical = MaterialTheme.dime.md)
|
||||
)
|
||||
Text(
|
||||
text = "آیتمهای آزمایشگاهی (${items.size})",
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.Bold,
|
||||
modifier = Modifier.padding(vertical = MaterialTheme.dime.md)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
items(items) { item ->
|
||||
TestItemSearchResult(
|
||||
testItem = item,
|
||||
onClick = { onItemClick(item) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun TestItemSearchResult(
|
||||
testItem: TestItem,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
CustomBox(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = MaterialTheme.dime.sm)
|
||||
.clickable(
|
||||
indication = null,
|
||||
interactionSource = remember { MutableInteractionSource() }
|
||||
) {
|
||||
onClick()
|
||||
}
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(all = MaterialTheme.dime.lg),
|
||||
horizontalAlignment = AbsoluteAlignment.Right
|
||||
) {
|
||||
Text(
|
||||
text = testItem.title ?: "بدون عنوان",
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
|
||||
if (!testItem.normalValue.isNullOrBlank()) {
|
||||
Text(
|
||||
text = "مقدار نرمال: ${testItem.normalValue}",
|
||||
style = MaterialTheme.typography.bodySmall.copy(
|
||||
textDirection = TextDirection.Rtl,
|
||||
textAlign = TextAlign.Right
|
||||
),
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.padding(top = MaterialTheme.dime.xs)
|
||||
)
|
||||
}
|
||||
|
||||
if (!testItem.detail.isNullOrBlank()) {
|
||||
Text(
|
||||
text = testItem.detail,
|
||||
style = MaterialTheme.typography.bodySmall.copy(
|
||||
textDirection = TextDirection.Rtl,
|
||||
textAlign = TextAlign.Right
|
||||
),
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.padding(top = MaterialTheme.dime.sm)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
package com.approagency.drug.presentation.components
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.LayoutDirection
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.approagency.drug.domain.model.TestGroup
|
||||
import com.approagency.drug.presentation.common.CustomModalBottomSheet
|
||||
import com.approagency.drug.presentation.viewModel.LabViewModel
|
||||
import com.approagency.drug.presentation.viewModel.TestItemState
|
||||
import com.vada.caller.ui.theme.dime
|
||||
import kotlinx.coroutines.flow.collect
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun TestDetailSheet(state: TestItemState, testGroup: TestGroup, onDismiss: () -> Unit) {
|
||||
if (state.testItem != null) {
|
||||
val testItems by state.testItem.collectAsState(initial = emptyList())
|
||||
|
||||
CustomModalBottomSheet(
|
||||
onDismiss = onDismiss,
|
||||
content = {
|
||||
CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) {
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = MaterialTheme.dime.lg),
|
||||
contentPadding = PaddingValues(bottom = MaterialTheme.dime.lg)
|
||||
) {
|
||||
item {
|
||||
Text(
|
||||
text = testGroup.fname ?: "جزئیات آزمایش",
|
||||
style = MaterialTheme.typography.headlineMedium,
|
||||
fontWeight = FontWeight.Bold,
|
||||
modifier = Modifier.padding(bottom = MaterialTheme.dime.md)
|
||||
)
|
||||
Text(
|
||||
text = testGroup.ename ?: "N/A (English Name)",
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.padding(bottom = MaterialTheme.dime.sm)
|
||||
)
|
||||
if (testGroup.detail != null){
|
||||
Text(testGroup.detail , style = MaterialTheme.typography.bodySmall,modifier = Modifier.padding(bottom = MaterialTheme.dime.md) )
|
||||
}
|
||||
|
||||
|
||||
|
||||
HorizontalDivider(Modifier.padding(bottom = MaterialTheme.dime.md))
|
||||
}
|
||||
|
||||
if (testItems.isEmpty()) {
|
||||
item {
|
||||
|
||||
}
|
||||
} else {
|
||||
items(testItems) { testItem ->
|
||||
TestItemDetailCard(testItem = testItem)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun TestItemDetailCard(testItem: com.approagency.drug.domain.model.TestItem) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = MaterialTheme.dime.md)
|
||||
) {
|
||||
Text(
|
||||
text = testItem.title ?: "بدون عنوان",
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
|
||||
if (!testItem.normalValue.isNullOrBlank()) {
|
||||
Text(
|
||||
text = "مقدار نرمال: ${testItem.normalValue}",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.padding(top = MaterialTheme.dime.xs)
|
||||
)
|
||||
}
|
||||
|
||||
if (!testItem.detail.isNullOrBlank()) {
|
||||
Text(
|
||||
text = testItem.detail,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.padding(top = MaterialTheme.dime.sm)
|
||||
)
|
||||
}
|
||||
|
||||
HorizontalDivider(
|
||||
modifier = Modifier.padding(top = MaterialTheme.dime.md),
|
||||
thickness = 0.5.dp
|
||||
)
|
||||
}
|
||||
}
|
||||
+90
@@ -0,0 +1,90 @@
|
||||
package com.approagency.drug.presentation.components
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
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.padding
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.twotone.KeyboardArrowLeft
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.AbsoluteAlignment
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextDirection
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import com.approagency.drug.domain.model.TestGroup
|
||||
import com.approagency.drug.presentation.common.CustomBox
|
||||
import com.vada.caller.ui.theme.dime
|
||||
|
||||
|
||||
@Composable
|
||||
fun TestGroupItemContent(
|
||||
group: TestGroup,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
|
||||
CustomBox(
|
||||
modifier = Modifier
|
||||
.padding(bottom = MaterialTheme.dime.sm)
|
||||
.clickable(
|
||||
indication = null,
|
||||
interactionSource = remember { MutableInteractionSource() }
|
||||
) {
|
||||
onClick()
|
||||
},
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.padding(all = MaterialTheme.dime.lg),
|
||||
) {
|
||||
Column (
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.Start
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalAlignment = AbsoluteAlignment.Right
|
||||
// horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Text(group.fname ?: "" , style = MaterialTheme.typography.labelLarge , textAlign = TextAlign.Right )
|
||||
Text(group.ename ?: "" , style = MaterialTheme.typography.bodySmall , textAlign = TextAlign.Left)
|
||||
Spacer(modifier = Modifier.padding(vertical = MaterialTheme.dime.xs))
|
||||
Text(group.detail ?: "" , style = MaterialTheme.typography.bodySmall.copy(
|
||||
textDirection = TextDirection.Rtl
|
||||
) , textAlign = TextAlign.Right , maxLines = 2 , softWrap = true , overflow = TextOverflow.Ellipsis)
|
||||
}
|
||||
Row (
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.Center
|
||||
) {
|
||||
TextButton(
|
||||
onClick = {
|
||||
onClick()
|
||||
}
|
||||
) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.Center
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.TwoTone.KeyboardArrowLeft
|
||||
, contentDescription = "see more"
|
||||
)
|
||||
Text("مشاهده جزییات")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -69,6 +69,7 @@ import com.approagency.drug.domain.model.DrugSearchParams
|
||||
import com.approagency.drug.presentation.common.CustomBox
|
||||
import com.approagency.drug.presentation.common.CustomModalBottomSheet
|
||||
import com.approagency.drug.presentation.common.CustomModalDialog
|
||||
import com.approagency.drug.presentation.common.CustomTextFilled
|
||||
import com.approagency.drug.presentation.common.Loading
|
||||
import com.approagency.drug.presentation.common.PrimaryButton
|
||||
import com.approagency.drug.presentation.components.DarmanContent
|
||||
@@ -129,7 +130,7 @@ fun HomeContent(
|
||||
onDrugClick: (Int) -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
var search by remember { mutableStateOf("") }
|
||||
var searchText by remember { mutableStateOf("") }
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxSize().padding(horizontal = MaterialTheme.dime.md)
|
||||
@@ -139,70 +140,28 @@ fun HomeContent(
|
||||
Column (
|
||||
verticalArrangement = Arrangement.Center
|
||||
){
|
||||
CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(45.dp) // Your desired height
|
||||
.background(
|
||||
color = MaterialTheme.colorScheme.surfaceVariant,
|
||||
shape =MaterialTheme.shapes.medium
|
||||
)
|
||||
.border(
|
||||
width = 1.dp,
|
||||
color = MaterialTheme.colorScheme.primaryContainer,
|
||||
shape =MaterialTheme.shapes.medium
|
||||
)
|
||||
.padding(horizontal = MaterialTheme.dime.md, vertical = MaterialTheme.dime.sm)
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.fillMaxSize()
|
||||
) {
|
||||
BasicTextField(
|
||||
value = search,
|
||||
onValueChange = { search = it },
|
||||
modifier = Modifier.weight(1f),
|
||||
singleLine = true,
|
||||
textStyle = MaterialTheme.typography.bodyLarge,
|
||||
decorationBox = { innerTextField ->
|
||||
Box {
|
||||
if (search.isEmpty()) {
|
||||
Text(
|
||||
"جستجوی دارو",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
innerTextField()
|
||||
}
|
||||
}
|
||||
)
|
||||
IconButton(
|
||||
onClick = {
|
||||
if (search.isNotBlank()) {
|
||||
onSearch(search)
|
||||
CustomTextFilled(
|
||||
value = searchText,
|
||||
onValueChange = { searchText = it },
|
||||
onSearch = { query ->
|
||||
if (query.isNotBlank()) {
|
||||
onSearch(query)
|
||||
}
|
||||
},
|
||||
modifier = Modifier.size(32.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Search,
|
||||
contentDescription = "Search",
|
||||
modifier = Modifier.size(18.dp)
|
||||
placeholder = "جستجوی دارو",
|
||||
showClearButton = true,
|
||||
showSearchButton = true,
|
||||
autoSearch = false, // Set to true if you want search while typing
|
||||
height = 45
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.height(MaterialTheme.dime.xs))
|
||||
PrimaryButton(
|
||||
text = "جستجو",
|
||||
height = 40,
|
||||
isLoading = state.drugSearchState.isLoading,
|
||||
onClick = {
|
||||
if (search.isNotBlank()) {
|
||||
onSearch(search)
|
||||
if (searchText.isNotBlank()) {
|
||||
onSearch(searchText)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
@@ -1,33 +1,50 @@
|
||||
package com.approagency.drug.presentation.screens
|
||||
|
||||
import android.R.attr.onClick
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
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.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.twotone.KeyboardArrowLeft
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
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.runtime.setValue
|
||||
import androidx.compose.ui.AbsoluteAlignment
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextDirection
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.approagency.drug.domain.model.TestGroup
|
||||
import com.approagency.drug.domain.model.TestItem
|
||||
import com.approagency.drug.presentation.common.CustomBox
|
||||
import com.approagency.drug.presentation.common.CustomModalDialog
|
||||
import com.approagency.drug.presentation.common.CustomTextFilled
|
||||
import com.approagency.drug.presentation.common.Loading
|
||||
import com.approagency.drug.presentation.common.PrimaryButton
|
||||
import com.approagency.drug.presentation.components.RetryContent
|
||||
import com.approagency.drug.presentation.components.SearchResultsContent
|
||||
import com.approagency.drug.presentation.components.TestDetailSheet
|
||||
import com.approagency.drug.presentation.components.TestGroupItemContent
|
||||
import com.approagency.drug.presentation.viewModel.LabViewModel
|
||||
import com.vada.caller.ui.theme.dime
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
@@ -40,11 +57,94 @@ fun LabScreen(
|
||||
viewModel: LabViewModel = koinViewModel()
|
||||
) {
|
||||
val testGroupsState by viewModel.testGroups.collectAsState()
|
||||
val isLoading = testGroupsState.isLoading
|
||||
val error = testGroupsState.error
|
||||
val testItemsState by viewModel.testItems.collectAsState()
|
||||
val searchResultsState by viewModel.searchResults.collectAsState()
|
||||
|
||||
val isLoading = testGroupsState.isLoading || testItemsState.isLoading || searchResultsState.isLoading
|
||||
val error = testGroupsState.error ?: searchResultsState.error
|
||||
val testGroups = testGroupsState.testGroup
|
||||
|
||||
Column(modifier = modifier.fillMaxSize()) {
|
||||
var selectedGroup by remember { mutableStateOf<TestGroup?>(null) }
|
||||
var selectedItem by remember { mutableStateOf<TestItem?>(null) }
|
||||
var showSheet by remember { mutableStateOf(false) }
|
||||
var searchText by remember { mutableStateOf("") }
|
||||
Column(modifier = modifier.fillMaxSize().padding(horizontal = MaterialTheme.dime.md)
|
||||
.padding(top = MaterialTheme.dime.md),) {
|
||||
CustomTextFilled(
|
||||
value = searchText,
|
||||
onValueChange = {
|
||||
searchText = it
|
||||
viewModel.updateSearchQuery(it)
|
||||
},
|
||||
onSearch = { query ->
|
||||
if (query.isNotBlank()) {
|
||||
viewModel.updateSearchQuery(query)
|
||||
}
|
||||
},
|
||||
placeholder = "جستجو در گروهها و آیتمهای آزمایشگاهی",
|
||||
showClearButton = true,
|
||||
showSearchButton = true,
|
||||
autoSearch = true,
|
||||
height = 45
|
||||
)
|
||||
Spacer(modifier = Modifier.height(MaterialTheme.dime.xs))
|
||||
PrimaryButton(
|
||||
text = "جستجو",
|
||||
height = 40,
|
||||
isLoading =false,
|
||||
onClick = {}
|
||||
)
|
||||
Spacer(modifier = Modifier.height(MaterialTheme.dime.xs))
|
||||
if (searchResultsState.query.isNotBlank()) {
|
||||
// Search Results Section
|
||||
SearchResultsContent(
|
||||
searchResult = searchResultsState.searchResults,
|
||||
isLoading = searchResultsState.isLoading,
|
||||
error = searchResultsState.error,
|
||||
onGroupClick = { group ->
|
||||
selectedGroup = group
|
||||
viewModel.getItemByGroupId(group.id)
|
||||
showSheet = true
|
||||
},
|
||||
onItemClick = { item ->
|
||||
selectedItem = item
|
||||
// You can show item detail sheet here
|
||||
// For now, we'll show the parent group
|
||||
viewModel.getItemByGroupId(item.groupId)
|
||||
showSheet = true
|
||||
},
|
||||
onClearSearch = {
|
||||
searchText = ""
|
||||
viewModel.clearSearch()
|
||||
viewModel.loadTestGroups()
|
||||
}
|
||||
)
|
||||
} else if(error != null){
|
||||
RetryContent(modifier = modifier , error = error , onClick = {
|
||||
viewModel.loadTestGroups()
|
||||
})
|
||||
}
|
||||
else if (testGroups != null){
|
||||
TestGroupsList(
|
||||
testGroupsFlow = testGroups,
|
||||
viewModel = viewModel,
|
||||
onGroupClick = { group ->
|
||||
selectedGroup = group
|
||||
viewModel.getItemByGroupId(group.id)
|
||||
showSheet = true
|
||||
}
|
||||
)
|
||||
}
|
||||
else {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text("هیچ دادهای موجود نیست")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
if (isLoading) {
|
||||
CustomModalDialog(
|
||||
onDismissRequest = { /* maybe disable dismiss while loading */ },
|
||||
@@ -60,54 +160,20 @@ fun LabScreen(
|
||||
// .background(MaterialTheme.colorScheme.background.copy(alpha = 0.5f))
|
||||
)
|
||||
}
|
||||
else if(error != null){
|
||||
CustomBox(
|
||||
child = {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = Modifier.padding(MaterialTheme.dime.md)
|
||||
) {
|
||||
Text(
|
||||
text = "خطا در دریافت اطلاعات",
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.error
|
||||
if (showSheet && selectedGroup != null) {
|
||||
TestDetailSheet(
|
||||
state = testItemsState,
|
||||
testGroup = selectedGroup!!,
|
||||
onDismiss = { showSheet = false }
|
||||
)
|
||||
Text(
|
||||
text = error,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
modifier = Modifier.padding(vertical = 8.dp)
|
||||
)
|
||||
PrimaryButton(
|
||||
text = "تلاش مجدد",
|
||||
height = 40,
|
||||
isLoading = false,
|
||||
onClick = { viewModel.loadTestGroups() }
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
else if (testGroups != null){
|
||||
TestGroupsList(
|
||||
testGroupsFlow = testGroups,
|
||||
viewModel = viewModel
|
||||
)
|
||||
} else {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text("هیچ دادهای موجود نیست")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun TestGroupsList(
|
||||
testGroupsFlow: Flow<List<TestGroup>>,
|
||||
viewModel: LabViewModel
|
||||
viewModel: LabViewModel,
|
||||
onGroupClick: (TestGroup) -> Unit
|
||||
) {
|
||||
val testGroups by testGroupsFlow.collectAsState(initial = emptyList())
|
||||
|
||||
@@ -128,56 +194,12 @@ fun TestGroupsList(
|
||||
} else {
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize()
|
||||
.fillMaxSize().padding( vertical =MaterialTheme.dime.md )
|
||||
) {
|
||||
items(testGroups) { group ->
|
||||
TestGroupItem(
|
||||
TestGroupItemContent(
|
||||
group = group,
|
||||
onClick = {
|
||||
// Navigate to test items of this group
|
||||
// viewModel.loadTestItemsByGroup(group.id)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun TestGroupItem(
|
||||
group: TestGroup,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
CustomBox (
|
||||
modifier = Modifier
|
||||
.padding(bottom = MaterialTheme.dime.sm)
|
||||
.clickable(
|
||||
indication = null,
|
||||
interactionSource = remember { MutableInteractionSource() }
|
||||
) {
|
||||
onClick()
|
||||
},
|
||||
) {
|
||||
Column (
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.Start
|
||||
) {
|
||||
Text(
|
||||
text = group.fname ?: group.ename ?: "بدون نام",
|
||||
style = MaterialTheme.typography.titleMedium.copy(
|
||||
textAlign = TextAlign.Right,
|
||||
textDirection = TextDirection.Rtl
|
||||
),
|
||||
modifier = Modifier.padding(vertical = MaterialTheme.dime.xs)
|
||||
)
|
||||
group.detail?.let {
|
||||
Text(
|
||||
text = it,
|
||||
style = MaterialTheme.typography.bodySmall.copy(
|
||||
textAlign = TextAlign.Right ,
|
||||
textDirection = TextDirection.Rtl
|
||||
),
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.padding(horizontal = MaterialTheme.dime.lg)
|
||||
onClick = { onGroupClick(group) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,14 +69,12 @@ fun RootScreen(navHostController: NavHostController, modifier: Modifier){
|
||||
titleContentColor = MaterialTheme.colorScheme.primary,
|
||||
),
|
||||
title = {
|
||||
Text("جستجوی دارویی" , textAlign = TextAlign.Right , style = MaterialTheme.typography.titleLarge , fontWeight = FontWeight.W700 )
|
||||
Text("دستیار سلامت" , textAlign = TextAlign.Right , style = MaterialTheme.typography.titleLarge , fontWeight = FontWeight.W700 )
|
||||
}
|
||||
)
|
||||
}},
|
||||
// modifier = modifier.fillMaxSize(),
|
||||
bottomBar = {
|
||||
NavigationBar(
|
||||
|
||||
containerColor = MaterialTheme.colorScheme.surface,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
|
||||
@@ -12,6 +12,7 @@ import com.approagency.drug.domain.usecase.GetDarmanUseCase
|
||||
import com.approagency.drug.domain.usecase.GetDrugDetailUseCase
|
||||
import com.approagency.drug.domain.usecase.GetDrugSearchUseCase
|
||||
import com.approagency.drug.presentation.screens.HomeScreen
|
||||
import com.approagency.drug.utils.retryWithBackoff
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
@@ -33,13 +34,24 @@ class HomeViewModel (
|
||||
init {
|
||||
getDarmani()
|
||||
}
|
||||
fun getDarmani(){
|
||||
_uiState.update { it.copy(darmanState = it.darmanState.copy(
|
||||
isLoading = true
|
||||
) , showDarmanList = true ) }
|
||||
fun getDarmani() {
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
darmanState = it.darmanState.copy(isLoading = true),
|
||||
showDarmanList = true
|
||||
)
|
||||
}
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
val result = getDarmanUseCase.invoke()
|
||||
retryWithBackoff(
|
||||
maxRetries = 3,
|
||||
onRetry = { attempt, delay ->
|
||||
// Optional: log retry attempt
|
||||
println("Retrying getDarmani, attempt $attempt, delay $delay ms")
|
||||
}
|
||||
) {
|
||||
getDarmanUseCase.invoke()
|
||||
}.fold(
|
||||
onSuccess = { result ->
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
darmanState = it.darmanState.copy(
|
||||
@@ -48,10 +60,20 @@ class HomeViewModel (
|
||||
)
|
||||
)
|
||||
}
|
||||
} catch (e: HttpException){
|
||||
handleError(e.message)
|
||||
},
|
||||
onFailure = { error ->
|
||||
handleError(error.message ?: "Unknown error")
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
darmanState = it.darmanState.copy(
|
||||
isLoading = false,
|
||||
getDarmani = Result.failure(error)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun searchDrugs(drugSearchParams: DrugSearchParams) {
|
||||
|
||||
@@ -9,16 +9,20 @@ import com.approagency.drug.domain.model.TestGroup
|
||||
import com.approagency.drug.domain.model.TestItem
|
||||
import com.approagency.drug.domain.usecase.GetTestGroupUseCase
|
||||
import com.approagency.drug.domain.usecase.GetTestItemByGroupId
|
||||
import com.approagency.drug.domain.usecase.SearchTestsUseCase
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.debounce
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class LabViewModel (
|
||||
private val getTestGroupUseCase: GetTestGroupUseCase,
|
||||
private val getTestItemByGroupId: GetTestItemByGroupId
|
||||
private val getTestItemByGroupId: GetTestItemByGroupId,
|
||||
private val searchTestsUseCase: SearchTestsUseCase
|
||||
) : ViewModel(){
|
||||
private val _testGroups = MutableStateFlow(TestGroupItem())
|
||||
val testGroups: StateFlow<TestGroupItem> = _testGroups.asStateFlow()
|
||||
@@ -26,10 +30,71 @@ class LabViewModel (
|
||||
private val _testItems = MutableStateFlow(TestItemState())
|
||||
val testItems: StateFlow<TestItemState> = _testItems.asStateFlow()
|
||||
|
||||
|
||||
private val _searchResults = MutableStateFlow(SearchResultState())
|
||||
val searchResults: StateFlow<SearchResultState> = _searchResults.asStateFlow()
|
||||
|
||||
private val searchQuery = MutableStateFlow("")
|
||||
|
||||
init {
|
||||
loadTestGroups()
|
||||
setupSearch()
|
||||
}
|
||||
private fun setupSearch() {
|
||||
viewModelScope.launch {
|
||||
searchQuery
|
||||
.debounce(1000) // Wait 500ms after user stops typing
|
||||
.distinctUntilChanged()
|
||||
.collect { query ->
|
||||
if (query.isNotBlank()) {
|
||||
performSearch(query)
|
||||
} else {
|
||||
_searchResults.update {
|
||||
SearchResultState(
|
||||
isLoading = false,
|
||||
searchResults = null,
|
||||
query = ""
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun updateSearchQuery(query: String) {
|
||||
searchQuery.value = query
|
||||
_searchResults.update {
|
||||
it.copy(
|
||||
isLoading = query.isNotBlank(),
|
||||
query = query
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun performSearch(query: String) {
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
val result = searchTestsUseCase.invoke(query)
|
||||
result.collect { (groups, items) ->
|
||||
_searchResults.update {
|
||||
it.copy(
|
||||
isLoading = false,
|
||||
searchResults = SearchResult(groups = groups, items = items),
|
||||
error = null
|
||||
)
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
_searchResults.update {
|
||||
it.copy(
|
||||
isLoading = false,
|
||||
searchResults = null,
|
||||
error = e.message ?: "خطا در جستجو"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
fun loadTestGroups(){
|
||||
_testGroups.update {
|
||||
it.copy(
|
||||
@@ -51,7 +116,12 @@ class LabViewModel (
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
fun clearSearch() {
|
||||
searchQuery.value = ""
|
||||
_searchResults.update {
|
||||
SearchResultState()
|
||||
}
|
||||
}
|
||||
fun getItemByGroupId(id:Int){
|
||||
_testItems.update { it.copy(isLoading = true) }
|
||||
viewModelScope.launch {
|
||||
@@ -91,4 +161,14 @@ data class TestItemState(
|
||||
val testItem: Flow<List<TestItem>>? = null,
|
||||
val error:String? = null
|
||||
)
|
||||
data class SearchResultState(
|
||||
val isLoading: Boolean = false,
|
||||
val searchResults: SearchResult? = null,
|
||||
val error: String? = null,
|
||||
val query: String = ""
|
||||
)
|
||||
|
||||
data class SearchResult(
|
||||
val groups: List<TestGroup>,
|
||||
val items: List<TestItem>
|
||||
)
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.approagency.drug.utils
|
||||
|
||||
import kotlinx.coroutines.delay
|
||||
|
||||
suspend fun <T> retryWithBackoff(
|
||||
maxRetries: Int = 3,
|
||||
initialDelayMs: Long = 1000,
|
||||
maxDelayMs: Long = 5000,
|
||||
factor: Double = 2.0,
|
||||
onRetry: ((attempt: Int, delayMs: Long) -> Unit)? = null,
|
||||
block: suspend () -> T
|
||||
): Result<T> {
|
||||
var currentDelay = initialDelayMs
|
||||
repeat(maxRetries - 1) { attempt ->
|
||||
try {
|
||||
return Result.success(block())
|
||||
} catch (e: Exception) {
|
||||
onRetry?.invoke(attempt + 1, currentDelay)
|
||||
delay(currentDelay)
|
||||
currentDelay = (currentDelay * factor).toLong().coerceAtMost(maxDelayMs)
|
||||
}
|
||||
}
|
||||
// Last attempt
|
||||
return try {
|
||||
Result.success(block())
|
||||
} catch (e: Exception) {
|
||||
Result.failure(e)
|
||||
}
|
||||
}
|
||||
@@ -22,3 +22,9 @@ kotlin.code.style=official
|
||||
# thereby reducing the size of the R class for that library
|
||||
android.nonTransitiveRClass=true
|
||||
kotlin.compiler.execution.strategy=in-process
|
||||
# Disable downloading Gradle sources
|
||||
org.gradle.caching=false
|
||||
org.gradle.daemon=true
|
||||
|
||||
# Prevent Gradle from downloading sources
|
||||
org.gradle.internal.launcher.welcomeMessageDisplayed=false
|
||||
-1
@@ -1,4 +1,3 @@
|
||||
#Thu Feb 26 19:18:21 IRST 2026
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://maven.myket.ir/gradle/distributions/gradle-8.13-bin.zip
|
||||
|
||||
+8
-8
@@ -1,19 +1,19 @@
|
||||
pluginManagement {
|
||||
repositories {
|
||||
maven { url = uri("https://maven.myket.ir") }
|
||||
google {
|
||||
content {
|
||||
includeGroupByRegex("com\\.android.*")
|
||||
includeGroupByRegex("com\\.google.*")
|
||||
includeGroupByRegex("androidx.*")
|
||||
}
|
||||
}
|
||||
// google {
|
||||
// content {
|
||||
// includeGroupByRegex("com\\.android.*")
|
||||
// includeGroupByRegex("com\\.google.*")
|
||||
// includeGroupByRegex("androidx.*")
|
||||
// }
|
||||
// }
|
||||
// mavenCentral()
|
||||
// gradlePluginPortal()
|
||||
}
|
||||
}
|
||||
dependencyResolutionManagement {
|
||||
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
|
||||
repositoriesMode.set(RepositoriesMode.PREFER_PROJECT)
|
||||
repositories {
|
||||
//google()
|
||||
//mavenCentral()
|
||||
|
||||
Reference in New Issue
Block a user