diff --git a/.idea/misc.xml b/.idea/misc.xml index 74dd639..b2c751a 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,4 +1,3 @@ - diff --git a/app/src/androidTest/java/com/approagency/pharmacy/ExampleInstrumentedTest.kt b/app/src/androidTest/java/com/approagency/pharmacy/ExampleInstrumentedTest.kt index ea737b0..b4de82d 100644 --- a/app/src/androidTest/java/com/approagency/pharmacy/ExampleInstrumentedTest.kt +++ b/app/src/androidTest/java/com/approagency/pharmacy/ExampleInstrumentedTest.kt @@ -19,6 +19,6 @@ class ExampleInstrumentedTest { fun useAppContext() { // Context of the app under test. val appContext = InstrumentationRegistry.getInstrumentation().targetContext - assertEquals("com.approagency.drug", appContext.packageName) + assertEquals("com.approagency.pharmacy", appContext.packageName) } } \ No newline at end of file diff --git a/app/src/main/java/com/approagency/pharmacy/data/remote/PharmacyHtmlParser.kt b/app/src/main/java/com/approagency/pharmacy/data/remote/PharmacyHtmlParser.kt index b2c4319..2a38801 100644 --- a/app/src/main/java/com/approagency/pharmacy/data/remote/PharmacyHtmlParser.kt +++ b/app/src/main/java/com/approagency/pharmacy/data/remote/PharmacyHtmlParser.kt @@ -14,64 +14,78 @@ object PharmacyHtmlParser { val doc = Jsoup.parse(html) val table = doc.select("table#TBL_patientReferral").first() ?: return emptyList() - // تشخیص ساختار بر اساس سربرگ جدول (یک بار برای کل جدول) - val hasGuaranteeColumn = hasGuaranteeColumnInHeader(table) + // ساختار جدول متغیر است: + // - صفحات ژنریک: [نام برند, نام داروخانه, شهرستان] (گاهی با ستون «تضمین موجودی») + // - صفحات برند: [نام داروخانه, شهرستان] (بدون ستون برند) + // بنابراین ایندکس ستون‌ها را از روی متن سربرگ تشخیص می‌دهیم نه موقعیت ثابت + val headers = table.select("thead th") + var brandIndex = -1 + var pharmacyIndex = -1 + var locationIndex = -1 + headers.forEachIndexed { index, th -> + val text = th.text().trim() + when { + brandIndex == -1 && text.contains("برند") -> brandIndex = index + pharmacyIndex == -1 && text.contains("داروخانه") -> pharmacyIndex = index + locationIndex == -1 && (text.contains("شهرستان") || text.contains("استان")) -> locationIndex = index + } + } val rows = table.select("tbody tr") for (row in rows) { val cells = row.select("td") - if (cells.size < 3) continue + if (cells.isEmpty()) continue - // ایندکس ستون‌ها بر اساس وجود ستون تضمین - val brandIndex = if (hasGuaranteeColumn) 1 else 0 - val pharmacyIndex = if (hasGuaranteeColumn) 2 else 1 - val locationIndex = if (hasGuaranteeColumn) 3 else 2 + // ستون داروخانه (ضروری) — اگر از سربرگ پیدا نشد، سلولی که لینک ph- دارد + val pIdx = if (pharmacyIndex in cells.indices) { + pharmacyIndex + } else { + cells.indexOfFirst { cell -> + cell.select("a").any { it.attr("href").contains("ph-") } + } + } + if (pIdx < 0) continue - // ستون برند - val brandCell = cells[brandIndex] - val brandLink = brandCell.select("a").first() + val pharmacyCell = cells[pIdx] + // پیدا کردن لینک داروخانه (لینکی که "اطلاعات تماس" نباشد) + val pharmacyLink = pharmacyCell.select("a").firstOrNull { + !it.text().contains("اطلاعات تماس") + } + val pharmacyName = pharmacyLink?.text()?.trim() ?: "" + val pharmacyUrl = normalizeUrl(pharmacyLink?.attr("href")?.trim() ?: "") + val pharmacyId = extractIdFromUrl(pharmacyUrl, "/ph-(\\d+)/?") + if (pharmacyName.isBlank()) continue + + // ستون برند (در صفحات برند وجود ندارد) + val brandCell = brandIndex.takeIf { it in cells.indices }?.let { cells[it] } + val brandLink = brandCell?.select("a")?.first() val brandName = brandLink?.text()?.trim() ?: "" val brandUrl = normalizeUrl(brandLink?.attr("href")?.trim() ?: "") val brandId = extractIdFromUrl(brandUrl, "/B-(\\d+)/?") - // ستون داروخانه - val pharmacyCell = cells[pharmacyIndex] - val pharmacyLinks = pharmacyCell.select("a") - - // پیدا کردن لینک داروخانه (لینکی که "اطلاعات تماس" نباشد) - val pharmacyLink = pharmacyLinks.firstOrNull { - !it.text().contains("اطلاعات تماس") + // ستون موقعیت — اگر از سربرگ پیدا نشد، سلولی که «استان» دارد + val locIdx = if (locationIndex in cells.indices) { + locationIndex + } else { + cells.indexOfFirst { it.text().contains("استان") } } - val pharmacyName = pharmacyLink?.text()?.trim() ?: "" - var pharmacyUrl = pharmacyLink?.attr("href")?.trim() ?: "" - - // نرمال کردن URL داروخانه - pharmacyUrl = normalizeUrl(pharmacyUrl) - val pharmacyId = extractIdFromUrl(pharmacyUrl, "/ph-(\\d+)/?") - - // ستون موقعیت - val locationCell = cells[locationIndex] - val locationText = locationCell.text().trim() - - // استخراج استان و شهر + val locationText = locIdx.takeIf { it in cells.indices }?.let { cells[it].text().trim() } ?: "" val province = extractProvince(locationText) val city = extractCity(locationText) - if (brandName.isNotBlank() && pharmacyName.isNotBlank()) { - items.add( - PharmacyItem( - brandName = brandName, - brandUrl = brandUrl, - brandId = brandId, - pharmacyName = pharmacyName, - pharmacyUrl = pharmacyUrl, - pharmacyId = pharmacyId, - province = province, - city = city - ) + items.add( + PharmacyItem( + brandName = brandName, + brandUrl = brandUrl, + brandId = brandId, + pharmacyName = pharmacyName, + pharmacyUrl = pharmacyUrl, + pharmacyId = pharmacyId, + province = province, + city = city ) - } + ) } return items @@ -108,18 +122,6 @@ object PharmacyHtmlParser { } } - /** - * تشخیص وجود ستون تضمین موجودی با بررسی سربرگ جدول - */ - private fun hasGuaranteeColumnInHeader(table: org.jsoup.nodes.Element): Boolean { - val headers = table.select("thead th") - if (headers.isEmpty()) return false - - // بررسی می‌کنیم که آیا اولین ستون حاوی متن "تضمین" است - val firstHeaderText = headers.firstOrNull()?.text()?.trim() ?: "" - return firstHeaderText.contains("تضمین") || firstHeaderText.contains("موجودی") - } - private fun extractProvince(locationText: String): String { // الگو: "استان تهران" یا "استان اصفهان" val provincePattern = "استان\\s*(\\S+)".toRegex() diff --git a/app/src/main/java/com/approagency/pharmacy/data/repository/DrugRepositoryImpl.kt b/app/src/main/java/com/approagency/pharmacy/data/repository/DrugRepositoryImpl.kt index 096088e..17bbb89 100644 --- a/app/src/main/java/com/approagency/pharmacy/data/repository/DrugRepositoryImpl.kt +++ b/app/src/main/java/com/approagency/pharmacy/data/repository/DrugRepositoryImpl.kt @@ -94,12 +94,13 @@ class DrugRepositoryImpl( override suspend fun searchPharmacies( genericDrugId: String, + brandIrc: String, provId: String ): List { return withContext(Dispatchers.IO) { try { val html = darooyabApiService.getPharmacies( - brandIrc = "0", + brandIrc = brandIrc, genericDrugId = genericDrugId, provId = provId, cityId = "0" diff --git a/app/src/main/java/com/approagency/pharmacy/domain/repository/DrugRepository.kt b/app/src/main/java/com/approagency/pharmacy/domain/repository/DrugRepository.kt index 35d5f4a..b1f00e4 100644 --- a/app/src/main/java/com/approagency/pharmacy/domain/repository/DrugRepository.kt +++ b/app/src/main/java/com/approagency/pharmacy/domain/repository/DrugRepository.kt @@ -16,7 +16,7 @@ interface DrugRepository { suspend fun getGorohDaroei(): Result suspend fun searchDrugs(params:DaroYabParams):Result suspend fun getDrugDetailFromYab(detailUrl: String):Result - suspend fun searchPharmacies(genericDrugId: String,provId: String): List + suspend fun searchPharmacies(genericDrugId: String, brandIrc: String, provId: String): List suspend fun getPharmacyDetail(pharmacyUrl: String): PharmacyDetail diff --git a/app/src/main/java/com/approagency/pharmacy/domain/usecase/GetPharmaciesUseCase.kt b/app/src/main/java/com/approagency/pharmacy/domain/usecase/GetPharmaciesUseCase.kt index 4cfab2d..60c4da8 100644 --- a/app/src/main/java/com/approagency/pharmacy/domain/usecase/GetPharmaciesUseCase.kt +++ b/app/src/main/java/com/approagency/pharmacy/domain/usecase/GetPharmaciesUseCase.kt @@ -8,8 +8,9 @@ class GetPharmaciesUseCase( ) { suspend operator fun invoke( genericDrugId: String, + brandIrc: String, provinceId: String ): List { - return repository.searchPharmacies(genericDrugId, provinceId) + return repository.searchPharmacies(genericDrugId, brandIrc, provinceId) } } \ No newline at end of file diff --git a/app/src/main/java/com/approagency/pharmacy/presentation/components/PharmacyBottomSheet.kt b/app/src/main/java/com/approagency/pharmacy/presentation/components/PharmacyBottomSheet.kt index fce5f55..1ff0480 100644 --- a/app/src/main/java/com/approagency/pharmacy/presentation/components/PharmacyBottomSheet.kt +++ b/app/src/main/java/com/approagency/pharmacy/presentation/components/PharmacyBottomSheet.kt @@ -33,7 +33,11 @@ import com.vada.caller.ui.theme.dime @OptIn(ExperimentalMaterial3Api::class) @Composable fun PharmacyBottomSheet( - viewModel: PharmacyViewModel, genericDrugId: String, isOpen: Boolean, onDismiss: () -> Unit + viewModel: PharmacyViewModel, + genericDrugId: String, + brandIrc: String, + isOpen: Boolean, + onDismiss: () -> Unit ) { val scope = rememberCoroutineScope() val sheetState = rememberModalBottomSheetState( @@ -45,11 +49,10 @@ fun PharmacyBottomSheet( val pharmacyState by viewModel.state.collectAsState() - // جستجوی اولیه با استان تهران - LaunchedEffect(Unit) { - if (genericDrugId.isNotBlank()) { - viewModel.search(genericDrugId, "40") - } + // جستجوی اولیه با استان تهران — با تغییر دارو دوباره اجرا می‌شود + // (در صورت خالی بودن شناسه، ViewModel وضعیت قبلی را پاک و خطا نمایش می‌دهد) + LaunchedEffect(genericDrugId, brandIrc) { + viewModel.search(genericDrugId, brandIrc, "40") } if (isOpen) { @@ -98,7 +101,7 @@ fun PharmacyBottomSheet( selectedProvinceId = province.id showProvinceDropdown = false // جستجو با استان جدید - viewModel.search(genericDrugId, province.id) + viewModel.search(genericDrugId, brandIrc, province.id) }) } } diff --git a/app/src/main/java/com/approagency/pharmacy/presentation/screens/SearchScreen.kt b/app/src/main/java/com/approagency/pharmacy/presentation/screens/SearchScreen.kt index db1267c..b8f0b57 100644 --- a/app/src/main/java/com/approagency/pharmacy/presentation/screens/SearchScreen.kt +++ b/app/src/main/java/com/approagency/pharmacy/presentation/screens/SearchScreen.kt @@ -128,13 +128,19 @@ fun SearchScreen( Spacer(modifier = Modifier.height(MaterialTheme.dime.md)) if (showPharmacyBottomSheet && selectedDrugForPharmacy != null) { - // استخراج genericDrugId از URL - // مثال: /G-799/Fexofenadine -> 799 - val genericId = extractGenericIdFromUrl(selectedDrugForPharmacy?.detailPageUrl ?: "") + // درخواست بر اساس نوع صفحه فرق می‌کند: + // - صفحات برند (/B-...): شناسه باید به‌عنوان brandIrc ارسال شود + // - صفحات ژنریک (/G-...): شناسه به‌عنوان genericDrugId ارسال می‌شود + // (genericId از قبل پارس شده و در هر دو حالت همان عدد را نگه می‌دارد) + val drug = selectedDrugForPharmacy!! + val isBrand = drug.detailPageUrl.contains("/B-") + val genericId = if (isBrand) "0" else drug.genericId + val brandIrc = if (isBrand) drug.genericId else "0" PharmacyBottomSheet( viewModel = pharmacyViewModel, genericDrugId = genericId, + brandIrc = brandIrc, isOpen = showPharmacyBottomSheet, onDismiss = { showPharmacyBottomSheet = false @@ -235,10 +241,4 @@ fun SearchScreen( } } } -} - -// تابع کمکی برای استخراج ID از URL -private fun extractGenericIdFromUrl(url: String): String { - val pattern = "/G-(\\d+)/".toRegex() - return pattern.find(url)?.groupValues?.get(1) ?: "" } \ No newline at end of file diff --git a/app/src/main/java/com/approagency/pharmacy/presentation/viewModel/PharmacyViewModel.kt b/app/src/main/java/com/approagency/pharmacy/presentation/viewModel/PharmacyViewModel.kt index fb67f88..fec554c 100644 --- a/app/src/main/java/com/approagency/pharmacy/presentation/viewModel/PharmacyViewModel.kt +++ b/app/src/main/java/com/approagency/pharmacy/presentation/viewModel/PharmacyViewModel.kt @@ -38,6 +38,7 @@ class PharmacyViewModel( val pharmacyDetailStates: StateFlow> = _pharmacyDetailStates.asStateFlow() private var currentGenericDrugId: String = "" + private var currentBrandIrc: String = "0" private var currentProvinceId: String = "0" fun loadPharmacyDetail(pharmacyUrl: String) { @@ -67,16 +68,26 @@ class PharmacyViewModel( } - fun search(genericDrugId: String, provinceId: String) { - if (genericDrugId.isBlank()) return - + fun search(genericDrugId: String, brandIrc: String, provinceId: String) { currentGenericDrugId = genericDrugId + currentBrandIrc = brandIrc currentProvinceId = provinceId + // پاک کردن نتایج جستجوی قبلی تا داده‌ی قبلی نمایش داده نشود + _pharmacyDetailStates.value = emptyMap() + + // حداقل یکی از شناسه‌ها (ژنریک یا برند) باید معتبر باشد + val hasGeneric = genericDrugId.isNotBlank() && genericDrugId != "0" + val hasBrand = brandIrc.isNotBlank() && brandIrc != "0" + if (!hasGeneric && !hasBrand) { + _state.value = PharmacyState.Error("شناسه دارو یافت نشد") + return + } + viewModelScope.launch { _state.value = PharmacyState.Loading try { - val result = getPharmaciesUseCase(genericDrugId, provinceId) + val result = getPharmaciesUseCase(genericDrugId, brandIrc, provinceId) _state.value = PharmacyState.Success(result) } catch (e: Exception) { _state.value = PharmacyState.Error(e.message ?: "خطا در دریافت اطلاعات") @@ -85,8 +96,6 @@ class PharmacyViewModel( } fun retry() { - if (currentGenericDrugId.isNotBlank()) { - search(currentGenericDrugId, currentProvinceId) - } + search(currentGenericDrugId, currentBrandIrc, currentProvinceId) } } \ No newline at end of file