feat: add pharmacy detail
This commit is contained in:
@@ -7,6 +7,7 @@ import retrofit2.http.Headers
|
|||||||
import retrofit2.http.POST
|
import retrofit2.http.POST
|
||||||
import retrofit2.http.Path
|
import retrofit2.http.Path
|
||||||
import retrofit2.http.Query
|
import retrofit2.http.Query
|
||||||
|
import retrofit2.http.Url
|
||||||
|
|
||||||
interface DarooyabApiService {
|
interface DarooyabApiService {
|
||||||
@FormUrlEncoded
|
@FormUrlEncoded
|
||||||
@@ -33,7 +34,16 @@ interface DarooyabApiService {
|
|||||||
@Path(value = "detailUrl", encoded = true) detailUrl: String
|
@Path(value = "detailUrl", encoded = true) detailUrl: String
|
||||||
): String
|
): String
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Headers(
|
||||||
|
"User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
|
||||||
|
"Accept: text/html,application/xhtml+xml,application/xml;q=0.9",
|
||||||
|
"Accept-Language: en-US,en;q=0.9,fa;q=0.8"
|
||||||
|
)
|
||||||
|
// @GET("{pharmacyPath}")
|
||||||
|
suspend fun getPharmacyDetail(
|
||||||
|
@Url url: String
|
||||||
|
): String
|
||||||
@POST("Home/partialPatientReferralList")
|
@POST("Home/partialPatientReferralList")
|
||||||
@FormUrlEncoded
|
@FormUrlEncoded
|
||||||
@Headers(
|
@Headers(
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
|
// data/remote/PharmacyHtmlParser.kt
|
||||||
package com.approagency.drug.data.remote
|
package com.approagency.drug.data.remote
|
||||||
|
|
||||||
|
import com.approagency.drug.domain.model.PharmacyDetail
|
||||||
import com.approagency.drug.domain.model.PharmacyItem
|
import com.approagency.drug.domain.model.PharmacyItem
|
||||||
import org.jsoup.Jsoup
|
import org.jsoup.Jsoup
|
||||||
|
|
||||||
object PharmacyHtmlParser {
|
object PharmacyHtmlParser {
|
||||||
|
|
||||||
|
// private const val BASE_URL = "https://www.darooyab.ir"
|
||||||
|
|
||||||
fun parse(html: String): List<PharmacyItem> {
|
fun parse(html: String): List<PharmacyItem> {
|
||||||
val items = mutableListOf<PharmacyItem>()
|
val items = mutableListOf<PharmacyItem>()
|
||||||
val doc = Jsoup.parse(html)
|
val doc = Jsoup.parse(html)
|
||||||
@@ -28,7 +32,7 @@ object PharmacyHtmlParser {
|
|||||||
val brandCell = cells[brandIndex]
|
val brandCell = cells[brandIndex]
|
||||||
val brandLink = brandCell.select("a").first()
|
val brandLink = brandCell.select("a").first()
|
||||||
val brandName = brandLink?.text()?.trim() ?: ""
|
val brandName = brandLink?.text()?.trim() ?: ""
|
||||||
val brandUrl = brandLink?.attr("href")?.trim() ?: ""
|
val brandUrl = normalizeUrl(brandLink?.attr("href")?.trim() ?: "")
|
||||||
val brandId = extractIdFromUrl(brandUrl, "/B-(\\d+)/?")
|
val brandId = extractIdFromUrl(brandUrl, "/B-(\\d+)/?")
|
||||||
|
|
||||||
// ستون داروخانه
|
// ستون داروخانه
|
||||||
@@ -40,7 +44,10 @@ object PharmacyHtmlParser {
|
|||||||
!it.text().contains("اطلاعات تماس")
|
!it.text().contains("اطلاعات تماس")
|
||||||
}
|
}
|
||||||
val pharmacyName = pharmacyLink?.text()?.trim() ?: ""
|
val pharmacyName = pharmacyLink?.text()?.trim() ?: ""
|
||||||
val pharmacyUrl = pharmacyLink?.attr("href")?.trim() ?: ""
|
var pharmacyUrl = pharmacyLink?.attr("href")?.trim() ?: ""
|
||||||
|
|
||||||
|
// نرمال کردن URL داروخانه
|
||||||
|
pharmacyUrl = normalizeUrl(pharmacyUrl)
|
||||||
val pharmacyId = extractIdFromUrl(pharmacyUrl, "/ph-(\\d+)/?")
|
val pharmacyId = extractIdFromUrl(pharmacyUrl, "/ph-(\\d+)/?")
|
||||||
|
|
||||||
// ستون موقعیت
|
// ستون موقعیت
|
||||||
@@ -70,6 +77,37 @@ object PharmacyHtmlParser {
|
|||||||
return items
|
return items
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* نرمال کردن URL: تبدیل مسیرهای نسبی به URL کامل
|
||||||
|
* مثال: ~/../../../../../ph-12012/... -> https://www.darooyab.ir/ph-12012/...
|
||||||
|
*/
|
||||||
|
private fun normalizeUrl(url: String): String {
|
||||||
|
if (url.isBlank()) return ""
|
||||||
|
|
||||||
|
// اگر absolute بود domain را حذف کن
|
||||||
|
if (url.startsWith("http://") || url.startsWith("https://")) {
|
||||||
|
return url
|
||||||
|
.replace("https://www.darooyab.ir", "")
|
||||||
|
.replace("http://www.darooyab.ir", "")
|
||||||
|
}
|
||||||
|
|
||||||
|
var cleanUrl = url
|
||||||
|
|
||||||
|
if (cleanUrl.startsWith("~/")) {
|
||||||
|
cleanUrl = cleanUrl.removePrefix("~/")
|
||||||
|
}
|
||||||
|
|
||||||
|
while (cleanUrl.startsWith("../")) {
|
||||||
|
cleanUrl = cleanUrl.removePrefix("../")
|
||||||
|
}
|
||||||
|
|
||||||
|
return if (cleanUrl.startsWith("/")) {
|
||||||
|
cleanUrl
|
||||||
|
} else {
|
||||||
|
"/$cleanUrl"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* تشخیص وجود ستون تضمین موجودی با بررسی سربرگ جدول
|
* تشخیص وجود ستون تضمین موجودی با بررسی سربرگ جدول
|
||||||
*/
|
*/
|
||||||
@@ -100,4 +138,29 @@ object PharmacyHtmlParser {
|
|||||||
val regex = pattern.toRegex()
|
val regex = pattern.toRegex()
|
||||||
return regex.find(url)?.groupValues?.get(1) ?: ""
|
return regex.find(url)?.groupValues?.get(1) ?: ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun parsePharmacyDetail(html: String): PharmacyDetail {
|
||||||
|
val doc = Jsoup.parse(html)
|
||||||
|
|
||||||
|
// استخراج نام داروخانه
|
||||||
|
val pharmacyTitle = doc.select("h2#pharmacyTitle").first()?.text()?.trim() ?: ""
|
||||||
|
|
||||||
|
// استخراج آدرس
|
||||||
|
val addressElement = doc.select("h2#h2Address").first()
|
||||||
|
val address = addressElement?.text()?.trim() ?: ""
|
||||||
|
|
||||||
|
// استخراج تلفن
|
||||||
|
val phoneElement = doc.select("h3#h3Phone a").first()
|
||||||
|
val phone = phoneElement?.text()?.trim() ?: ""
|
||||||
|
|
||||||
|
// استخراج دانشگاه (اختیاری)
|
||||||
|
val university = doc.select("div.alert.alert-success > span").first()?.text()?.trim()
|
||||||
|
|
||||||
|
return PharmacyDetail(
|
||||||
|
name = pharmacyTitle,
|
||||||
|
address = address,
|
||||||
|
phone = phone,
|
||||||
|
university = university
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -13,6 +13,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.PharmacyDetail
|
||||||
import com.approagency.drug.domain.model.PharmacyItem
|
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
|
||||||
@@ -110,4 +111,15 @@ class DrugRepositoryImpl(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun getPharmacyDetail(pharmacyUrl: String): PharmacyDetail {
|
||||||
|
return withContext(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
|
val html = darooyabApiService.getPharmacyDetail(pharmacyUrl)
|
||||||
|
PharmacyHtmlParser.parsePharmacyDetail(html)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
PharmacyDetail("", "", "")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -15,6 +15,7 @@ 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.GetPharmaciesUseCase
|
||||||
|
import com.approagency.drug.domain.usecase.GetPharmacyDetailUseCase
|
||||||
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
|
||||||
@@ -135,6 +136,10 @@ val appModule= module {
|
|||||||
DrugDetailYabUseCase(get())
|
DrugDetailYabUseCase(get())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
single {
|
||||||
|
GetPharmacyDetailUseCase(get())
|
||||||
|
}
|
||||||
|
|
||||||
viewModel {
|
viewModel {
|
||||||
DrugDetailViewModel(get())
|
DrugDetailViewModel(get())
|
||||||
}
|
}
|
||||||
@@ -149,7 +154,7 @@ val appModule= module {
|
|||||||
SearchViewModel(get())
|
SearchViewModel(get())
|
||||||
}
|
}
|
||||||
viewModel {
|
viewModel {
|
||||||
PharmacyViewModel(get())
|
PharmacyViewModel(get() , get ())
|
||||||
}
|
}
|
||||||
|
|
||||||
viewModel {
|
viewModel {
|
||||||
|
|||||||
@@ -17,5 +17,15 @@ data class PharmacyItem(
|
|||||||
val pharmacyId: String, // ID داروخانه
|
val pharmacyId: String, // ID داروخانه
|
||||||
|
|
||||||
val province: String, // نام استان
|
val province: String, // نام استان
|
||||||
val city: String // نام شهر
|
val city: String , // نام شهر
|
||||||
|
val address: String? = null,
|
||||||
|
val phone: String? = null
|
||||||
|
)
|
||||||
|
|
||||||
|
// مدل برای اطلاعات کامل داروخانه
|
||||||
|
data class PharmacyDetail(
|
||||||
|
val name: String,
|
||||||
|
val address: String,
|
||||||
|
val phone: String,
|
||||||
|
val university: String? = null
|
||||||
)
|
)
|
||||||
@@ -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.PharmacyDetail
|
||||||
import com.approagency.drug.domain.model.PharmacyItem
|
import com.approagency.drug.domain.model.PharmacyItem
|
||||||
|
|
||||||
interface DrugRepository {
|
interface DrugRepository {
|
||||||
@@ -18,4 +19,6 @@ interface DrugRepository {
|
|||||||
suspend fun getDrugDetailFromYab(detailUrl: String):Result<DrugDetail>
|
suspend fun getDrugDetailFromYab(detailUrl: String):Result<DrugDetail>
|
||||||
suspend fun searchPharmacies(genericDrugId: String,provId: String): List<PharmacyItem>
|
suspend fun searchPharmacies(genericDrugId: String,provId: String): List<PharmacyItem>
|
||||||
|
|
||||||
|
suspend fun getPharmacyDetail(pharmacyUrl: String): PharmacyDetail
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package com.approagency.drug.domain.usecase
|
||||||
|
|
||||||
|
import com.approagency.drug.domain.model.PharmacyDetail
|
||||||
|
import com.approagency.drug.domain.repository.DrugRepository
|
||||||
|
|
||||||
|
class GetPharmacyDetailUseCase(
|
||||||
|
private val repository: DrugRepository
|
||||||
|
) {
|
||||||
|
suspend operator fun invoke(pharmacyUrl: String): PharmacyDetail {
|
||||||
|
return repository.getPharmacyDetail(pharmacyUrl)
|
||||||
|
}
|
||||||
|
}
|
||||||
+146
-36
@@ -3,8 +3,12 @@ package com.approagency.drug.presentation.components
|
|||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.LocationOn
|
||||||
|
import androidx.compose.material.icons.filled.Phone
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
@@ -14,10 +18,12 @@ import androidx.compose.ui.platform.LocalLayoutDirection
|
|||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.LayoutDirection
|
import androidx.compose.ui.unit.LayoutDirection
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
import com.approagency.drug.domain.model.PharmacyItem
|
import com.approagency.drug.domain.model.PharmacyItem
|
||||||
import com.approagency.drug.presentation.common.CustomBox
|
import com.approagency.drug.presentation.common.CustomBox
|
||||||
import com.approagency.drug.presentation.common.CustomModalBottomSheet
|
import com.approagency.drug.presentation.common.CustomModalBottomSheet
|
||||||
import com.approagency.drug.presentation.common.Loading
|
import com.approagency.drug.presentation.common.Loading
|
||||||
|
import com.approagency.drug.presentation.viewModel.PharmacyDetailState
|
||||||
import com.approagency.drug.presentation.viewModel.PharmacyState
|
import com.approagency.drug.presentation.viewModel.PharmacyState
|
||||||
import com.approagency.drug.presentation.viewModel.PharmacyViewModel
|
import com.approagency.drug.presentation.viewModel.PharmacyViewModel
|
||||||
import com.approagency.drug.utils.provinces
|
import com.approagency.drug.utils.provinces
|
||||||
@@ -29,16 +35,11 @@ import kotlinx.coroutines.launch
|
|||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun PharmacyBottomSheet(
|
fun PharmacyBottomSheet(
|
||||||
viewModel: PharmacyViewModel,
|
viewModel: PharmacyViewModel, genericDrugId: String, isOpen: Boolean, onDismiss: () -> Unit
|
||||||
genericDrugId: String,
|
|
||||||
isOpen: Boolean,
|
|
||||||
onDismiss: () -> Unit
|
|
||||||
) {
|
) {
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
val sheetState = rememberModalBottomSheetState(
|
val sheetState = rememberModalBottomSheetState(
|
||||||
skipPartiallyExpanded = false,
|
skipPartiallyExpanded = false, confirmValueChange = { true })
|
||||||
confirmValueChange = { true }
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
var selectedProvinceId by remember { mutableStateOf("40") } // پیشفرض تهران
|
var selectedProvinceId by remember { mutableStateOf("40") } // پیشفرض تهران
|
||||||
@@ -55,14 +56,14 @@ fun PharmacyBottomSheet(
|
|||||||
|
|
||||||
if (isOpen) {
|
if (isOpen) {
|
||||||
CustomModalBottomSheet(
|
CustomModalBottomSheet(
|
||||||
sheetState = sheetState,
|
sheetState = sheetState, onDismiss = onDismiss, scope = scope
|
||||||
onDismiss = onDismiss,
|
|
||||||
scope = scope
|
|
||||||
) {
|
) {
|
||||||
CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) {
|
CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) {
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
modifier = Modifier.fillMaxHeight()
|
modifier = Modifier
|
||||||
.fillMaxWidth().heightIn(min = 400.dp)
|
.fillMaxHeight()
|
||||||
|
.fillMaxWidth()
|
||||||
|
.heightIn(min = 400.dp)
|
||||||
.padding(MaterialTheme.dime.md),
|
.padding(MaterialTheme.dime.md),
|
||||||
verticalArrangement = Arrangement.spacedBy(MaterialTheme.dime.xl)
|
verticalArrangement = Arrangement.spacedBy(MaterialTheme.dime.xl)
|
||||||
) {
|
) {
|
||||||
@@ -78,11 +79,10 @@ fun PharmacyBottomSheet(
|
|||||||
item {
|
item {
|
||||||
ExposedDropdownMenuBox(
|
ExposedDropdownMenuBox(
|
||||||
expanded = showProvinceDropdown,
|
expanded = showProvinceDropdown,
|
||||||
onExpandedChange = { showProvinceDropdown = it }
|
onExpandedChange = { showProvinceDropdown = it }) {
|
||||||
) {
|
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
value = provinces.find { it.id == selectedProvinceId }?.name
|
value = provinces.find { it.id == selectedProvinceId }?.name
|
||||||
?: "انتخاب استان",
|
?: "انتخاب استان",
|
||||||
onValueChange = {},
|
onValueChange = {},
|
||||||
readOnly = true,
|
readOnly = true,
|
||||||
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = showProvinceDropdown) },
|
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = showProvinceDropdown) },
|
||||||
@@ -94,18 +94,14 @@ fun PharmacyBottomSheet(
|
|||||||
|
|
||||||
ExposedDropdownMenu(
|
ExposedDropdownMenu(
|
||||||
expanded = showProvinceDropdown,
|
expanded = showProvinceDropdown,
|
||||||
onDismissRequest = { showProvinceDropdown = false }
|
onDismissRequest = { showProvinceDropdown = false }) {
|
||||||
) {
|
|
||||||
provinces.forEach { province ->
|
provinces.forEach { province ->
|
||||||
DropdownMenuItem(
|
DropdownMenuItem(text = { Text(province.name) }, onClick = {
|
||||||
text = { Text(province.name) },
|
selectedProvinceId = province.id
|
||||||
onClick = {
|
showProvinceDropdown = false
|
||||||
selectedProvinceId = province.id
|
// جستجو با استان جدید
|
||||||
showProvinceDropdown = false
|
viewModel.search(genericDrugId, province.id)
|
||||||
// جستجو با استان جدید
|
})
|
||||||
viewModel.search(genericDrugId, province.id)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -113,8 +109,6 @@ fun PharmacyBottomSheet(
|
|||||||
// انتخاب استان
|
// انتخاب استان
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// نمایش نتایج
|
// نمایش نتایج
|
||||||
when (val state = pharmacyState) {
|
when (val state = pharmacyState) {
|
||||||
PharmacyState.Loading -> {
|
PharmacyState.Loading -> {
|
||||||
@@ -156,9 +150,8 @@ fun PharmacyBottomSheet(
|
|||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
items(state.items)
|
items(state.items) { pharmacy ->
|
||||||
{ pharmacy ->
|
PharmacyResultItem(pharmacy = pharmacy, viewModel)
|
||||||
PharmacyResultItem(pharmacy = pharmacy)
|
|
||||||
}
|
}
|
||||||
item {
|
item {
|
||||||
Spacer(modifier = Modifier.height(MaterialTheme.dime.md))
|
Spacer(modifier = Modifier.height(MaterialTheme.dime.md))
|
||||||
@@ -199,8 +192,7 @@ fun PharmacyBottomSheet(
|
|||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
TextButton(
|
TextButton(
|
||||||
onClick = { viewModel.retry() }
|
onClick = { viewModel.retry() }) {
|
||||||
) {
|
|
||||||
Text("تلاش مجدد")
|
Text("تلاش مجدد")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -219,11 +211,30 @@ fun PharmacyBottomSheet(
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun PharmacyResultItem(
|
fun PharmacyResultItem(
|
||||||
pharmacy: PharmacyItem,
|
pharmacy: PharmacyItem, viewModel: PharmacyViewModel, modifier: Modifier = Modifier
|
||||||
modifier: Modifier = Modifier
|
|
||||||
) {
|
) {
|
||||||
val dime = LocalDime.current
|
val dime = LocalDime.current
|
||||||
|
var showDetails by remember { mutableStateOf(false) }
|
||||||
|
// گرفتن State مخصوص این داروخانه از Map
|
||||||
|
val detailStates by viewModel.pharmacyDetailStates.collectAsState()
|
||||||
|
val detailState = detailStates[pharmacy.pharmacyUrl] ?: PharmacyDetailState.Idle
|
||||||
|
|
||||||
|
// Reset detail state when item changes
|
||||||
|
LaunchedEffect(pharmacy.pharmacyUrl) {
|
||||||
|
println(pharmacy.pharmacyUrl)
|
||||||
|
showDetails = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load detail when expanded
|
||||||
|
LaunchedEffect(showDetails) {
|
||||||
|
if (showDetails) {
|
||||||
|
// فقط اگر Idle هست (هنوز لود نشده) درخواست بده
|
||||||
|
if (detailState is PharmacyDetailState.Idle) {
|
||||||
|
println(pharmacy.pharmacyUrl)
|
||||||
|
viewModel.loadPharmacyDetail(pharmacy.pharmacyUrl)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
CustomBox(
|
CustomBox(
|
||||||
modifier = modifier.fillMaxWidth(),
|
modifier = modifier.fillMaxWidth(),
|
||||||
) {
|
) {
|
||||||
@@ -248,8 +259,107 @@ fun PharmacyResultItem(
|
|||||||
)
|
)
|
||||||
|
|
||||||
// موقعیت
|
// موقعیت
|
||||||
|
// موقعیت و دکمه اطلاعات تماس
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "${pharmacy.province} - ${pharmacy.city}",
|
||||||
|
fontSize = MaterialTheme.typography.bodySmall.fontSize,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f)
|
||||||
|
)
|
||||||
|
|
||||||
|
TextButton(
|
||||||
|
onClick = { showDetails = !showDetails }) {
|
||||||
|
Text(
|
||||||
|
text = if (showDetails) "بستن" else "اطلاعات تماس",
|
||||||
|
fontSize = MaterialTheme.typography.bodySmall.fontSize
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (showDetails) {
|
||||||
|
Divider(modifier = Modifier.padding(vertical = 4.dp))
|
||||||
|
|
||||||
|
when (detailState) {
|
||||||
|
is PharmacyDetailState.Loading -> {
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.Center,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
CircularProgressIndicator(modifier = Modifier.size(24.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
is PharmacyDetailState.Success -> {
|
||||||
|
Column(
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
// آدرس
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.Top,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.LocationOn,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(16.dp),
|
||||||
|
tint = MaterialTheme.colorScheme.primary
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = detailState.detail.address,
|
||||||
|
fontSize = MaterialTheme.typography.bodySmall.fontSize,
|
||||||
|
lineHeight = 18.sp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// تلفن
|
||||||
|
if (detailState.detail.phone.isNotBlank()) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Phone,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(16.dp),
|
||||||
|
tint = MaterialTheme.colorScheme.primary
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = detailState.detail.phone,
|
||||||
|
fontSize = MaterialTheme.typography.bodyMedium.fontSize,
|
||||||
|
fontWeight = FontWeight.Medium
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// هشدار (اطلاعات از HTML)
|
||||||
|
Text(
|
||||||
|
text = "⚠️ دریافت دارو فقط با نسخه کامل و بصورت حضوری در محل داروخانه امکان پذیر است",
|
||||||
|
fontSize = MaterialTheme.typography.bodySmall.fontSize,
|
||||||
|
color = MaterialTheme.colorScheme.error,
|
||||||
|
modifier = Modifier.padding(top = 4.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
is PharmacyDetailState.Error -> {
|
||||||
|
Text(
|
||||||
|
text = "خطا در دریافت اطلاعات تماس",
|
||||||
|
fontSize = MaterialTheme.typography.bodySmall.fontSize,
|
||||||
|
color = MaterialTheme.colorScheme.error
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = "${pharmacy.province} - ${pharmacy.city}",
|
text = "${pharmacy.pharmacyUrl}",
|
||||||
fontSize = MaterialTheme.typography.bodySmall.fontSize,
|
fontSize = MaterialTheme.typography.bodySmall.fontSize,
|
||||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f)
|
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -3,13 +3,20 @@ package com.approagency.drug.presentation.viewModel
|
|||||||
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.approagency.drug.domain.model.PharmacyDetail
|
||||||
import com.approagency.drug.domain.model.PharmacyItem
|
import com.approagency.drug.domain.model.PharmacyItem
|
||||||
import com.approagency.drug.domain.usecase.GetPharmaciesUseCase
|
import com.approagency.drug.domain.usecase.GetPharmaciesUseCase
|
||||||
|
import com.approagency.drug.domain.usecase.GetPharmacyDetailUseCase
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
sealed class PharmacyDetailState {
|
||||||
|
object Idle : PharmacyDetailState()
|
||||||
|
object Loading : PharmacyDetailState()
|
||||||
|
data class Success(val detail: PharmacyDetail) : PharmacyDetailState()
|
||||||
|
data class Error(val message: String) : PharmacyDetailState()
|
||||||
|
}
|
||||||
sealed class PharmacyState {
|
sealed class PharmacyState {
|
||||||
object Idle : PharmacyState()
|
object Idle : PharmacyState()
|
||||||
object Loading : PharmacyState()
|
object Loading : PharmacyState()
|
||||||
@@ -18,15 +25,48 @@ sealed class PharmacyState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class PharmacyViewModel(
|
class PharmacyViewModel(
|
||||||
private val getPharmaciesUseCase: GetPharmaciesUseCase
|
private val getPharmaciesUseCase: GetPharmaciesUseCase,
|
||||||
|
private val getPharmacyDetailUseCase: GetPharmacyDetailUseCase
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
private val _state = MutableStateFlow<PharmacyState>(PharmacyState.Idle)
|
private val _state = MutableStateFlow<PharmacyState>(PharmacyState.Idle)
|
||||||
val state: StateFlow<PharmacyState> = _state.asStateFlow()
|
val state: StateFlow<PharmacyState> = _state.asStateFlow()
|
||||||
|
|
||||||
|
|
||||||
|
// استفاده از Map برای نگهداری State هر داروخانه به صورت جداگانه
|
||||||
|
private val _pharmacyDetailStates = MutableStateFlow<Map<String, PharmacyDetailState>>(emptyMap())
|
||||||
|
val pharmacyDetailStates: StateFlow<Map<String, PharmacyDetailState>> = _pharmacyDetailStates.asStateFlow()
|
||||||
|
|
||||||
private var currentGenericDrugId: String = ""
|
private var currentGenericDrugId: String = ""
|
||||||
private var currentProvinceId: String = "0"
|
private var currentProvinceId: String = "0"
|
||||||
|
|
||||||
|
fun loadPharmacyDetail(pharmacyUrl: String) {
|
||||||
|
// اگر در حال حاضر لود میشود یا موفقیت آمیز بوده، دوباره درخواست نده
|
||||||
|
val currentState = _pharmacyDetailStates.value[pharmacyUrl]
|
||||||
|
if (currentState is PharmacyDetailState.Loading || currentState is PharmacyDetailState.Success) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
viewModelScope.launch {
|
||||||
|
// تنظیم Loading برای این داروخانه خاص
|
||||||
|
_pharmacyDetailStates.value = _pharmacyDetailStates.value.toMutableMap().apply {
|
||||||
|
put(pharmacyUrl, PharmacyDetailState.Loading)
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
val detail = getPharmacyDetailUseCase(pharmacyUrl)
|
||||||
|
_pharmacyDetailStates.value = _pharmacyDetailStates.value.toMutableMap().apply {
|
||||||
|
put(pharmacyUrl, PharmacyDetailState.Success(detail))
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
_pharmacyDetailStates.value = _pharmacyDetailStates.value.toMutableMap().apply {
|
||||||
|
put(pharmacyUrl, PharmacyDetailState.Error(e.message ?: "خطا در دریافت اطلاعات"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
fun search(genericDrugId: String, provinceId: String) {
|
fun search(genericDrugId: String, provinceId: String) {
|
||||||
if (genericDrugId.isBlank()) return
|
if (genericDrugId.isBlank()) return
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user