feat: add pharmacy feature
This commit is contained in:
@@ -32,4 +32,20 @@ interface DarooyabApiService {
|
|||||||
suspend fun getDrugDetail(
|
suspend fun getDrugDetail(
|
||||||
@Path(value = "detailUrl", encoded = true) detailUrl: String
|
@Path(value = "detailUrl", encoded = true) detailUrl: String
|
||||||
): String
|
): String
|
||||||
|
|
||||||
|
|
||||||
|
@POST("Home/partialPatientReferralList")
|
||||||
|
@FormUrlEncoded
|
||||||
|
@Headers(
|
||||||
|
"User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36",
|
||||||
|
"X-Requested-With: XMLHttpRequest",
|
||||||
|
"Accept: */*",
|
||||||
|
"Accept-Language: en-US,en;q=0.9,fa;q=0.8"
|
||||||
|
)
|
||||||
|
suspend fun getPharmacies(
|
||||||
|
@Field("brandIrc") brandIrc: String,
|
||||||
|
@Field("genericDrugId") genericDrugId: String,
|
||||||
|
@Field("provId") provId: String,
|
||||||
|
@Field("cityId") cityId: String
|
||||||
|
): String // returns HTML
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
package com.approagency.drug.data.remote
|
||||||
|
import com.approagency.drug.domain.model.PharmacyItem
|
||||||
|
import org.jsoup.Jsoup
|
||||||
|
|
||||||
|
object PharmacyHtmlParser {
|
||||||
|
|
||||||
|
fun parse(html: String): List<PharmacyItem> {
|
||||||
|
val items = mutableListOf<PharmacyItem>()
|
||||||
|
val doc = Jsoup.parse(html)
|
||||||
|
val rows = doc.select("table#TBL_patientReferral tbody tr")
|
||||||
|
|
||||||
|
for (row in rows) {
|
||||||
|
val cells = row.select("td")
|
||||||
|
if (cells.size < 3) continue
|
||||||
|
|
||||||
|
// ستون 0: اطلاعات برند
|
||||||
|
val brandCell = cells[0]
|
||||||
|
val brandLink = brandCell.select("a").first()
|
||||||
|
val brandName = brandLink?.text()?.trim() ?: ""
|
||||||
|
val brandUrl = brandLink?.attr("href")?.trim() ?: ""
|
||||||
|
val brandId = extractIdFromUrl(brandUrl, "/B-(\\d+)/?")
|
||||||
|
|
||||||
|
// ستون 1: اطلاعات داروخانه
|
||||||
|
val pharmacyCell = cells[1]
|
||||||
|
val pharmacyLink = pharmacyCell.select("a").first()
|
||||||
|
val pharmacyName = pharmacyLink?.text()?.trim() ?: ""
|
||||||
|
val pharmacyUrl = pharmacyLink?.attr("href")?.trim() ?: ""
|
||||||
|
val pharmacyId = extractIdFromUrl(pharmacyUrl, "/ph-(\\d+)/?")
|
||||||
|
|
||||||
|
// ستون 2: موقعیت
|
||||||
|
val locationText = cells[2].text().trim()
|
||||||
|
val parts = locationText.split("شهر")
|
||||||
|
val province = parts.getOrNull(0)?.replace("استان", "")?.trim() ?: ""
|
||||||
|
val city = parts.getOrNull(1)?.trim() ?: ""
|
||||||
|
|
||||||
|
if (brandName.isNotBlank() && pharmacyName.isNotBlank()) {
|
||||||
|
items.add(
|
||||||
|
PharmacyItem(
|
||||||
|
brandName = brandName,
|
||||||
|
brandUrl = brandUrl,
|
||||||
|
brandId = brandId,
|
||||||
|
pharmacyName = pharmacyName,
|
||||||
|
pharmacyUrl = pharmacyUrl,
|
||||||
|
pharmacyId = pharmacyId,
|
||||||
|
province = province,
|
||||||
|
city = city
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return items
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun extractIdFromUrl(url: String, pattern: String): String {
|
||||||
|
val regex = pattern.toRegex()
|
||||||
|
return regex.find(url)?.groupValues?.get(1) ?: ""
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,11 +7,13 @@ import com.approagency.drug.data.remote.DarooyabApiService
|
|||||||
import com.approagency.drug.data.remote.DrugApiService
|
import com.approagency.drug.data.remote.DrugApiService
|
||||||
import com.approagency.drug.data.remote.DrugDetailParser
|
import com.approagency.drug.data.remote.DrugDetailParser
|
||||||
import com.approagency.drug.data.remote.DrugHtmlParser
|
import com.approagency.drug.data.remote.DrugHtmlParser
|
||||||
|
import com.approagency.drug.data.remote.PharmacyHtmlParser
|
||||||
import com.approagency.drug.domain.model.DaroYabParams
|
import com.approagency.drug.domain.model.DaroYabParams
|
||||||
import com.approagency.drug.domain.model.DaroYabSearchResult
|
import com.approagency.drug.domain.model.DaroYabSearchResult
|
||||||
import com.approagency.drug.domain.model.DrugDetail
|
import com.approagency.drug.domain.model.DrugDetail
|
||||||
import com.approagency.drug.domain.model.DrugSearchParams
|
import com.approagency.drug.domain.model.DrugSearchParams
|
||||||
import com.approagency.drug.domain.model.DrugSearchResult
|
import com.approagency.drug.domain.model.DrugSearchResult
|
||||||
|
import com.approagency.drug.domain.model.PharmacyItem
|
||||||
import com.approagency.drug.domain.repository.DrugRepository
|
import com.approagency.drug.domain.repository.DrugRepository
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
@@ -89,4 +91,23 @@ class DrugRepositoryImpl(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun searchPharmacies(
|
||||||
|
genericDrugId: String,
|
||||||
|
provId: String
|
||||||
|
): List<PharmacyItem> {
|
||||||
|
return withContext(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
|
val html = darooyabApiService.getPharmacies(
|
||||||
|
brandIrc = "0",
|
||||||
|
genericDrugId = genericDrugId,
|
||||||
|
provId = provId,
|
||||||
|
cityId = "0"
|
||||||
|
)
|
||||||
|
PharmacyHtmlParser.parse(html)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
emptyList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -14,6 +14,7 @@ import com.approagency.drug.domain.usecase.DrugDetailYabUseCase
|
|||||||
import com.approagency.drug.domain.usecase.GetDarmanUseCase
|
import com.approagency.drug.domain.usecase.GetDarmanUseCase
|
||||||
import com.approagency.drug.domain.usecase.GetDrugDetailUseCase
|
import com.approagency.drug.domain.usecase.GetDrugDetailUseCase
|
||||||
import com.approagency.drug.domain.usecase.GetDrugSearchUseCase
|
import com.approagency.drug.domain.usecase.GetDrugSearchUseCase
|
||||||
|
import com.approagency.drug.domain.usecase.GetPharmaciesUseCase
|
||||||
import com.approagency.drug.domain.usecase.GetTestGroupUseCase
|
import com.approagency.drug.domain.usecase.GetTestGroupUseCase
|
||||||
import com.approagency.drug.domain.usecase.GetTestItemByGroupId
|
import com.approagency.drug.domain.usecase.GetTestItemByGroupId
|
||||||
import com.approagency.drug.domain.usecase.SearchDrugsYabUseCase
|
import com.approagency.drug.domain.usecase.SearchDrugsYabUseCase
|
||||||
@@ -21,6 +22,7 @@ import com.approagency.drug.domain.usecase.SearchTestsUseCase
|
|||||||
import com.approagency.drug.presentation.viewModel.DrugDetailViewModel
|
import com.approagency.drug.presentation.viewModel.DrugDetailViewModel
|
||||||
import com.approagency.drug.presentation.viewModel.HomeViewModel
|
import com.approagency.drug.presentation.viewModel.HomeViewModel
|
||||||
import com.approagency.drug.presentation.viewModel.LabViewModel
|
import com.approagency.drug.presentation.viewModel.LabViewModel
|
||||||
|
import com.approagency.drug.presentation.viewModel.PharmacyViewModel
|
||||||
import com.approagency.drug.utils.Config
|
import com.approagency.drug.utils.Config
|
||||||
import com.approgency.drug.presentation.viewModel.SearchViewModel
|
import com.approgency.drug.presentation.viewModel.SearchViewModel
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
@@ -119,6 +121,10 @@ val appModule= module {
|
|||||||
GetTestGroupUseCase(get())
|
GetTestGroupUseCase(get())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
single {
|
||||||
|
GetPharmaciesUseCase(get())
|
||||||
|
}
|
||||||
|
|
||||||
single {
|
single {
|
||||||
GetTestItemByGroupId(get())
|
GetTestItemByGroupId(get())
|
||||||
}
|
}
|
||||||
@@ -142,6 +148,9 @@ val appModule= module {
|
|||||||
viewModel {
|
viewModel {
|
||||||
SearchViewModel(get())
|
SearchViewModel(get())
|
||||||
}
|
}
|
||||||
|
viewModel {
|
||||||
|
PharmacyViewModel(get())
|
||||||
|
}
|
||||||
|
|
||||||
viewModel {
|
viewModel {
|
||||||
LabViewModel(get() , get() , get())
|
LabViewModel(get() , get() , get())
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package com.approagency.drug.domain.model
|
||||||
|
|
||||||
|
data class PharmacySearchRequest(
|
||||||
|
val brandIrc: String = "0", // برای صفحات برند
|
||||||
|
val genericDrugId: String, // برای صفحات ژنریک
|
||||||
|
val provId: String, // ID استان (0 برای همه)
|
||||||
|
val cityId: String = "0" // ID شهر (0 برای همه)
|
||||||
|
)
|
||||||
|
|
||||||
|
data class PharmacyItem(
|
||||||
|
val brandName: String, // نام برند دارو
|
||||||
|
val brandUrl: String, // لینک صفحه برند
|
||||||
|
val brandId: String, // IRC برند
|
||||||
|
|
||||||
|
val pharmacyName: String, // نام داروخانه
|
||||||
|
val pharmacyUrl: String, // لینک داروخانه
|
||||||
|
val pharmacyId: String, // ID داروخانه
|
||||||
|
|
||||||
|
val province: String, // نام استان
|
||||||
|
val city: String // نام شهر
|
||||||
|
)
|
||||||
@@ -8,6 +8,7 @@ import com.approagency.drug.domain.model.DaroYabSearchResult
|
|||||||
import com.approagency.drug.domain.model.DrugDetail
|
import com.approagency.drug.domain.model.DrugDetail
|
||||||
import com.approagency.drug.domain.model.DrugSearchParams
|
import com.approagency.drug.domain.model.DrugSearchParams
|
||||||
import com.approagency.drug.domain.model.DrugSearchResult
|
import com.approagency.drug.domain.model.DrugSearchResult
|
||||||
|
import com.approagency.drug.domain.model.PharmacyItem
|
||||||
|
|
||||||
interface DrugRepository {
|
interface DrugRepository {
|
||||||
suspend fun searchDrug(params: DrugSearchParams): Result<DrugListResponse>
|
suspend fun searchDrug(params: DrugSearchParams): Result<DrugListResponse>
|
||||||
@@ -15,5 +16,6 @@ interface DrugRepository {
|
|||||||
suspend fun getGorohDaroei(): Result<DarmanModel>
|
suspend fun getGorohDaroei(): Result<DarmanModel>
|
||||||
suspend fun searchDrugs(params:DaroYabParams):Result<DaroYabSearchResult>
|
suspend fun searchDrugs(params:DaroYabParams):Result<DaroYabSearchResult>
|
||||||
suspend fun getDrugDetailFromYab(detailUrl: String):Result<DrugDetail>
|
suspend fun getDrugDetailFromYab(detailUrl: String):Result<DrugDetail>
|
||||||
|
suspend fun searchPharmacies(genericDrugId: String,provId: String): List<PharmacyItem>
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package com.approagency.drug.domain.usecase
|
||||||
|
|
||||||
|
import com.approagency.drug.domain.model.PharmacyItem
|
||||||
|
import com.approagency.drug.domain.repository.DrugRepository
|
||||||
|
|
||||||
|
class GetPharmaciesUseCase(
|
||||||
|
private val repository: DrugRepository
|
||||||
|
) {
|
||||||
|
suspend operator fun invoke(
|
||||||
|
genericDrugId: String,
|
||||||
|
provinceId: String
|
||||||
|
): List<PharmacyItem> {
|
||||||
|
return repository.searchPharmacies(genericDrugId, provinceId)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,7 +6,6 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.navigation.NavType
|
import androidx.navigation.NavType
|
||||||
import androidx.navigation.compose.NavHost
|
import androidx.navigation.compose.NavHost
|
||||||
import androidx.navigation.compose.composable
|
import androidx.navigation.compose.composable
|
||||||
import androidx.navigation.compose.navigation
|
|
||||||
import androidx.navigation.compose.rememberNavController
|
import androidx.navigation.compose.rememberNavController
|
||||||
import androidx.navigation.navArgument
|
import androidx.navigation.navArgument
|
||||||
import com.approagency.drug.presentation.screens.DrugDetailScreen
|
import com.approagency.drug.presentation.screens.DrugDetailScreen
|
||||||
@@ -36,7 +35,7 @@ fun AppNavGraph() {
|
|||||||
|
|
||||||
composable("search") {
|
composable("search") {
|
||||||
MainContainer(navController) {
|
MainContainer(navController) {
|
||||||
SearchScreen(navController)
|
SearchScreen(navController,)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -27,7 +27,7 @@ import com.vada.caller.ui.theme.dime
|
|||||||
fun DaroYabSearchResult(
|
fun DaroYabSearchResult(
|
||||||
drug: DrugSearchResult,
|
drug: DrugSearchResult,
|
||||||
onClickDetail: (DrugSearchResult) -> Unit,
|
onClickDetail: (DrugSearchResult) -> Unit,
|
||||||
onClickDrugStore:() ->Unit,
|
onClickDrugStore:(DrugSearchResult) ->Unit,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
val dime = LocalDime.current
|
val dime = LocalDime.current
|
||||||
@@ -71,7 +71,7 @@ fun DaroYabSearchResult(
|
|||||||
TextButton(onClick = {onClickDetail(drug)} , content = {
|
TextButton(onClick = {onClickDetail(drug)} , content = {
|
||||||
Text("اطلاعات دارویی")
|
Text("اطلاعات دارویی")
|
||||||
})
|
})
|
||||||
TextButton(onClick = {onClickDetail(drug)} , content = {
|
TextButton(onClick = {onClickDrugStore(drug)} , content = {
|
||||||
Text("موجودی داروخانه")
|
Text("موجودی داروخانه")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
+269
@@ -0,0 +1,269 @@
|
|||||||
|
package com.approagency.drug.presentation.components
|
||||||
|
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.material3.*
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
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.PharmacyItem
|
||||||
|
import com.approagency.drug.presentation.common.CustomModalBottomSheet
|
||||||
|
import com.approagency.drug.presentation.viewModel.PharmacyState
|
||||||
|
import com.approagency.drug.presentation.viewModel.PharmacyViewModel
|
||||||
|
import com.vada.caller.ui.theme.LocalDime
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun PharmacyBottomSheet(
|
||||||
|
viewModel: PharmacyViewModel,
|
||||||
|
genericDrugId: String,
|
||||||
|
isOpen: Boolean,
|
||||||
|
onDismiss: () -> Unit
|
||||||
|
) {
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
val sheetState = rememberModalBottomSheetState(
|
||||||
|
skipPartiallyExpanded = true,
|
||||||
|
confirmValueChange = { true }
|
||||||
|
)
|
||||||
|
|
||||||
|
// لیست استانها (همان لیست HTML)
|
||||||
|
val provinces = listOf(
|
||||||
|
Province("0", "همه استان ها"),
|
||||||
|
Province("1", "اصفهان"),
|
||||||
|
Province("40", "تهران"),
|
||||||
|
Province("137", "آذربایجان شرقی"),
|
||||||
|
Province("138", "آذربایجان غربی"),
|
||||||
|
Province("139", "اردبیل"),
|
||||||
|
Province("140", "البرز"),
|
||||||
|
Province("141", "ایلام"),
|
||||||
|
Province("142", "بوشهر"),
|
||||||
|
Province("143", "چهارمحال و بختیاری"),
|
||||||
|
Province("144", "خراسان جنوبی"),
|
||||||
|
Province("145", "خراسان رضوی"),
|
||||||
|
Province("146", "خراسان شمالی"),
|
||||||
|
Province("147", "خوزستان"),
|
||||||
|
Province("148", "زنجان"),
|
||||||
|
Province("149", "سمنان"),
|
||||||
|
Province("150", "سیستان و بلوچستان"),
|
||||||
|
Province("151", "فارس"),
|
||||||
|
Province("152", "قزوین"),
|
||||||
|
Province("153", "قم"),
|
||||||
|
Province("154", "کردستان"),
|
||||||
|
Province("155", "کرمان"),
|
||||||
|
Province("156", "کرمانشاه"),
|
||||||
|
Province("157", "کهگیلویه و بویراحمد"),
|
||||||
|
Province("158", "گلستان"),
|
||||||
|
Province("159", "گیلان"),
|
||||||
|
Province("160", "لرستان"),
|
||||||
|
Province("161", "مازندران"),
|
||||||
|
Province("162", "مرکزی"),
|
||||||
|
Province("163", "هرمزگان"),
|
||||||
|
Province("164", "همدان"),
|
||||||
|
Province("165", "یزد")
|
||||||
|
)
|
||||||
|
|
||||||
|
var selectedProvinceId by remember { mutableStateOf("40") } // پیشفرض تهران
|
||||||
|
var showProvinceDropdown by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
val pharmacyState by viewModel.state.collectAsState()
|
||||||
|
|
||||||
|
// جستجوی اولیه با استان تهران
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
if (genericDrugId.isNotBlank()) {
|
||||||
|
viewModel.search(genericDrugId, "40")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isOpen) {
|
||||||
|
CustomModalBottomSheet(
|
||||||
|
sheetState = sheetState,
|
||||||
|
onDismiss = onDismiss,
|
||||||
|
scope = scope
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(16.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
|
) {
|
||||||
|
// عنوان
|
||||||
|
Text(
|
||||||
|
text = "جستجوی دارو در داروخانهها",
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
fontSize = MaterialTheme.typography.titleMedium.fontSize
|
||||||
|
)
|
||||||
|
|
||||||
|
// انتخاب استان
|
||||||
|
ExposedDropdownMenuBox(
|
||||||
|
expanded = showProvinceDropdown,
|
||||||
|
onExpandedChange = { showProvinceDropdown = it }
|
||||||
|
) {
|
||||||
|
OutlinedTextField(
|
||||||
|
value = provinces.find { it.id == selectedProvinceId }?.name ?: "انتخاب استان",
|
||||||
|
onValueChange = {},
|
||||||
|
readOnly = true,
|
||||||
|
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = showProvinceDropdown) },
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.menuAnchor(),
|
||||||
|
shape = MaterialTheme.shapes.small
|
||||||
|
)
|
||||||
|
|
||||||
|
ExposedDropdownMenu(
|
||||||
|
expanded = showProvinceDropdown,
|
||||||
|
onDismissRequest = { showProvinceDropdown = false }
|
||||||
|
) {
|
||||||
|
provinces.forEach { province ->
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text(province.name) },
|
||||||
|
onClick = {
|
||||||
|
selectedProvinceId = province.id
|
||||||
|
showProvinceDropdown = false
|
||||||
|
// جستجو با استان جدید
|
||||||
|
viewModel.search(genericDrugId, province.id)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|
||||||
|
// نمایش نتایج
|
||||||
|
when (val state = pharmacyState) {
|
||||||
|
PharmacyState.Loading -> {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(200.dp),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||||
|
CircularProgressIndicator(
|
||||||
|
modifier = Modifier.size(32.dp)
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
Text(
|
||||||
|
text = "در حال جستجو...",
|
||||||
|
fontSize = MaterialTheme.typography.bodySmall.fontSize
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
is PharmacyState.Success -> {
|
||||||
|
if (state.items.isEmpty()) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(200.dp),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "هیچ داروخانهای یافت نشد",
|
||||||
|
fontSize = MaterialTheme.typography.bodyMedium.fontSize,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
LazyColumn(
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
modifier = Modifier.heightIn(max = 400.dp)
|
||||||
|
) {
|
||||||
|
items(state.items) { pharmacy ->
|
||||||
|
PharmacyResultItem(pharmacy = pharmacy)
|
||||||
|
}
|
||||||
|
|
||||||
|
item {
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
Text(
|
||||||
|
text = "تعداد کل: ${state.items.size} داروخانه",
|
||||||
|
fontSize = MaterialTheme.typography.bodySmall.fontSize,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.5f),
|
||||||
|
modifier = Modifier.padding(bottom = 8.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
is PharmacyState.Error -> {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(200.dp),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||||
|
Text(
|
||||||
|
text = state.message,
|
||||||
|
color = MaterialTheme.colorScheme.error,
|
||||||
|
fontSize = MaterialTheme.typography.bodyMedium.fontSize
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
TextButton(
|
||||||
|
onClick = { viewModel.retry() }
|
||||||
|
) {
|
||||||
|
Text("تلاش مجدد")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
PharmacyState.Idle -> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun PharmacyResultItem(
|
||||||
|
pharmacy: PharmacyItem,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
val dime = LocalDime.current
|
||||||
|
|
||||||
|
Card(
|
||||||
|
modifier = modifier.fillMaxWidth(),
|
||||||
|
elevation = CardDefaults.cardElevation(defaultElevation = 1.dp)
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(dime.md),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(4.dp)
|
||||||
|
) {
|
||||||
|
// نام برند
|
||||||
|
Text(
|
||||||
|
text = pharmacy.brandName,
|
||||||
|
fontSize = MaterialTheme.typography.bodyMedium.fontSize,
|
||||||
|
color = MaterialTheme.colorScheme.primary
|
||||||
|
)
|
||||||
|
|
||||||
|
// نام داروخانه
|
||||||
|
Text(
|
||||||
|
text = pharmacy.pharmacyName,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
fontSize = MaterialTheme.typography.bodyLarge.fontSize
|
||||||
|
)
|
||||||
|
|
||||||
|
// موقعیت
|
||||||
|
Text(
|
||||||
|
text = "${pharmacy.province} - ${pharmacy.city}",
|
||||||
|
fontSize = MaterialTheme.typography.bodySmall.fontSize,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class Province(
|
||||||
|
val id: String,
|
||||||
|
val name: String
|
||||||
|
)
|
||||||
@@ -2,19 +2,14 @@ package com.approagency.drug.presentation.screens
|
|||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.graphics.drawable.Icon
|
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.WindowInsets
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
import androidx.compose.foundation.layout.WindowInsetsSides
|
import androidx.compose.foundation.layout.WindowInsetsSides
|
||||||
import androidx.compose.foundation.layout.consumeWindowInsets
|
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.navigationBars
|
import androidx.compose.foundation.layout.navigationBars
|
||||||
import androidx.compose.foundation.layout.only
|
import androidx.compose.foundation.layout.only
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.safeDrawingPadding
|
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.Build
|
import androidx.compose.material.icons.filled.Build
|
||||||
@@ -24,12 +19,10 @@ import androidx.compose.material3.ExperimentalMaterial3Api
|
|||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.NavigationBar
|
import androidx.compose.material3.NavigationBar
|
||||||
import androidx.compose.material3.NavigationBarDefaults
|
|
||||||
import androidx.compose.material3.NavigationBarItem
|
import androidx.compose.material3.NavigationBarItem
|
||||||
import androidx.compose.material3.NavigationBarItemDefaults
|
import androidx.compose.material3.NavigationBarItemDefaults
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.ScaffoldDefaults
|
import androidx.compose.material3.ScaffoldDefaults
|
||||||
import androidx.compose.material3.SegmentedButtonDefaults.Icon
|
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TopAppBar
|
import androidx.compose.material3.TopAppBar
|
||||||
import androidx.compose.material3.TopAppBarDefaults
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
@@ -41,7 +34,6 @@ import androidx.compose.runtime.remember
|
|||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.platform.LocalConfiguration
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalLayoutDirection
|
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
@@ -123,7 +115,10 @@ fun RootScreen(navHostController: NavHostController, modifier: Modifier){
|
|||||||
padding -> when (seletedTab) {
|
padding -> when (seletedTab) {
|
||||||
0 -> HomeScreen( navController = navHostController,Modifier.padding(padding).consumeWindowInsets(padding))
|
0 -> HomeScreen( navController = navHostController,Modifier.padding(padding).consumeWindowInsets(padding))
|
||||||
1 -> LabScreen(Modifier.padding(padding))
|
1 -> LabScreen(Modifier.padding(padding))
|
||||||
2 -> SearchScreen(navHostController,modifier = Modifier.padding(padding).consumeWindowInsets(padding))
|
2 -> SearchScreen(
|
||||||
|
navHostController,
|
||||||
|
modifier = Modifier.padding(padding).consumeWindowInsets(padding),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import androidx.compose.ui.Alignment
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
|
import com.approagency.drug.domain.model.DrugSearchResult
|
||||||
import com.approagency.drug.navigation.Screen
|
import com.approagency.drug.navigation.Screen
|
||||||
import com.approagency.drug.presentation.common.CustomTextFilled
|
import com.approagency.drug.presentation.common.CustomTextFilled
|
||||||
import com.approagency.drug.presentation.common.EmptySearchState
|
import com.approagency.drug.presentation.common.EmptySearchState
|
||||||
@@ -33,7 +34,9 @@ import com.approagency.drug.presentation.common.LoadingMoreIndicator
|
|||||||
import com.approagency.drug.presentation.common.PrimaryButton
|
import com.approagency.drug.presentation.common.PrimaryButton
|
||||||
import com.approagency.drug.presentation.components.DaroYabSearchResult
|
import com.approagency.drug.presentation.components.DaroYabSearchResult
|
||||||
import com.approagency.drug.presentation.components.DrugDetailBottomSheet
|
import com.approagency.drug.presentation.components.DrugDetailBottomSheet
|
||||||
|
import com.approagency.drug.presentation.components.PharmacyBottomSheet
|
||||||
import com.approagency.drug.presentation.viewModel.DrugDetailViewModel
|
import com.approagency.drug.presentation.viewModel.DrugDetailViewModel
|
||||||
|
import com.approagency.drug.presentation.viewModel.PharmacyViewModel
|
||||||
import com.approgency.drug.presentation.viewModel.SearchState
|
import com.approgency.drug.presentation.viewModel.SearchState
|
||||||
import com.approgency.drug.presentation.viewModel.SearchViewModel
|
import com.approgency.drug.presentation.viewModel.SearchViewModel
|
||||||
import com.vada.caller.ui.theme.LocalDime
|
import com.vada.caller.ui.theme.LocalDime
|
||||||
@@ -46,7 +49,8 @@ fun SearchScreen(
|
|||||||
navController: NavController,
|
navController: NavController,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
viewModel: SearchViewModel = koinViewModel(),
|
viewModel: SearchViewModel = koinViewModel(),
|
||||||
drugDetailViewModel: DrugDetailViewModel = koinViewModel() // اضافه کنید
|
drugDetailViewModel: DrugDetailViewModel = koinViewModel(),
|
||||||
|
pharmacyViewModel: PharmacyViewModel = koinViewModel(),
|
||||||
) {
|
) {
|
||||||
val dime = LocalDime.current
|
val dime = LocalDime.current
|
||||||
val keyboardController = LocalSoftwareKeyboardController.current
|
val keyboardController = LocalSoftwareKeyboardController.current
|
||||||
@@ -54,6 +58,10 @@ fun SearchScreen(
|
|||||||
val state by viewModel.searchState.collectAsState()
|
val state by viewModel.searchState.collectAsState()
|
||||||
val lazyListState = rememberLazyListState()
|
val lazyListState = rememberLazyListState()
|
||||||
var selectedDrugUrl by remember { mutableStateOf<String?>(null) }
|
var selectedDrugUrl by remember { mutableStateOf<String?>(null) }
|
||||||
|
|
||||||
|
var showPharmacyBottomSheet by remember { mutableStateOf(false) }
|
||||||
|
var selectedDrugForPharmacy by remember { mutableStateOf<DrugSearchResult?>(null) }
|
||||||
|
|
||||||
// Auto-search for testing (remove in production)
|
// Auto-search for testing (remove in production)
|
||||||
// LaunchedEffect(Unit) {
|
// LaunchedEffect(Unit) {
|
||||||
// if (searchText.isEmpty()) {
|
// if (searchText.isEmpty()) {
|
||||||
@@ -118,6 +126,22 @@ fun SearchScreen(
|
|||||||
)
|
)
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(MaterialTheme.dime.md))
|
Spacer(modifier = Modifier.height(MaterialTheme.dime.md))
|
||||||
|
|
||||||
|
if (showPharmacyBottomSheet && selectedDrugForPharmacy != null) {
|
||||||
|
// استخراج genericDrugId از URL
|
||||||
|
// مثال: /G-799/Fexofenadine -> 799
|
||||||
|
val genericId = extractGenericIdFromUrl(selectedDrugForPharmacy?.detailPageUrl ?: "")
|
||||||
|
|
||||||
|
PharmacyBottomSheet(
|
||||||
|
viewModel = pharmacyViewModel,
|
||||||
|
genericDrugId = genericId,
|
||||||
|
isOpen = showPharmacyBottomSheet,
|
||||||
|
onDismiss = {
|
||||||
|
showPharmacyBottomSheet = false
|
||||||
|
selectedDrugForPharmacy = null
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
// Results area
|
// Results area
|
||||||
when (val currentState = state) {
|
when (val currentState = state) {
|
||||||
is SearchState.Loading -> {
|
is SearchState.Loading -> {
|
||||||
@@ -148,7 +172,10 @@ fun SearchScreen(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
onClickDrugStore = {}
|
onClickDrugStore = { selectedDrug ->
|
||||||
|
selectedDrugForPharmacy = selectedDrug
|
||||||
|
showPharmacyBottomSheet = true
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
item {
|
item {
|
||||||
@@ -172,7 +199,10 @@ fun SearchScreen(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
onClickDrugStore = {}
|
onClickDrugStore = { selectedDrug ->
|
||||||
|
selectedDrugForPharmacy = selectedDrug
|
||||||
|
showPharmacyBottomSheet = true
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -206,3 +236,9 @@ fun SearchScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// تابع کمکی برای استخراج ID از URL
|
||||||
|
private fun extractGenericIdFromUrl(url: String): String {
|
||||||
|
val pattern = "/G-(\\d+)/".toRegex()
|
||||||
|
return pattern.find(url)?.groupValues?.get(1) ?: ""
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
package com.approagency.drug.presentation.viewModel
|
||||||
|
|
||||||
|
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.approagency.drug.domain.model.PharmacyItem
|
||||||
|
import com.approagency.drug.domain.usecase.GetPharmaciesUseCase
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
sealed class PharmacyState {
|
||||||
|
object Idle : PharmacyState()
|
||||||
|
object Loading : PharmacyState()
|
||||||
|
data class Success(val items: List<PharmacyItem>) : PharmacyState()
|
||||||
|
data class Error(val message: String) : PharmacyState()
|
||||||
|
}
|
||||||
|
|
||||||
|
class PharmacyViewModel(
|
||||||
|
private val getPharmaciesUseCase: GetPharmaciesUseCase
|
||||||
|
) : ViewModel() {
|
||||||
|
|
||||||
|
private val _state = MutableStateFlow<PharmacyState>(PharmacyState.Idle)
|
||||||
|
val state: StateFlow<PharmacyState> = _state.asStateFlow()
|
||||||
|
|
||||||
|
private var currentGenericDrugId: String = ""
|
||||||
|
private var currentProvinceId: String = "0"
|
||||||
|
|
||||||
|
fun search(genericDrugId: String, provinceId: String) {
|
||||||
|
if (genericDrugId.isBlank()) return
|
||||||
|
|
||||||
|
currentGenericDrugId = genericDrugId
|
||||||
|
currentProvinceId = provinceId
|
||||||
|
|
||||||
|
viewModelScope.launch {
|
||||||
|
_state.value = PharmacyState.Loading
|
||||||
|
try {
|
||||||
|
val result = getPharmaciesUseCase(genericDrugId, provinceId)
|
||||||
|
_state.value = PharmacyState.Success(result)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
_state.value = PharmacyState.Error(e.message ?: "خطا در دریافت اطلاعات")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun retry() {
|
||||||
|
if (currentGenericDrugId.isNotBlank()) {
|
||||||
|
search(currentGenericDrugId, currentProvinceId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user