feat: add detail screen and nav fixed
This commit is contained in:
@@ -42,8 +42,8 @@ class MainActivity : ComponentActivity() {
|
||||
Scaffold(modifier = Modifier.fillMaxSize(),
|
||||
) { innerPadding ->
|
||||
AppNavGraph(
|
||||
navController = navController,
|
||||
modifier = Modifier.padding(innerPadding)
|
||||
// navController = navController,
|
||||
// modifier = Modifier.padding(innerPadding)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,370 @@
|
||||
package com.approagency.drug.data.remote
|
||||
|
||||
import com.approagency.drug.domain.model.*
|
||||
import org.jsoup.Jsoup
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
|
||||
class DrugDetailParser {
|
||||
|
||||
fun parseDrugDetail(html: String): DrugDetail {
|
||||
val document = Jsoup.parse(html)
|
||||
|
||||
// اطلاعات پایه
|
||||
val genericId = extractGenericId(document)
|
||||
val persianName = extractPersianName(document)
|
||||
val englishName = extractEnglishName(document)
|
||||
|
||||
// استخراج تمام بخشها به صورت داینامیک
|
||||
val sections = extractAllSections(document)
|
||||
|
||||
return DrugDetail(
|
||||
genericId = genericId,
|
||||
persianName = persianName,
|
||||
englishName = englishName,
|
||||
drugClass = extractDrugClass(document),
|
||||
therapeuticClass = extractTherapeuticClass(document),
|
||||
usage = sections["usage"] ?: extractSectionByText(document, "موارد مصرف"),
|
||||
mechanism = sections["mechanism"] ?: extractSectionByText(document, "مکانیسم اثر"),
|
||||
pharmacokinetics = sections["pharmacokinetics"] ?: extractSectionByText(document, "فارماکوکینتیک"),
|
||||
contraindications = sections["contraindications"] ?: extractSectionByText(document, "منع مصرف"),
|
||||
sideEffects = sections["sideEffects"] ?: extractSectionByText(document, "عوارض جانبی"),
|
||||
interactions = sections["interactions"] ?: extractSectionByText(document, "تداخلات دارویی"),
|
||||
warnings = sections["warnings"] ?: extractSectionByText(document, "هشدار"),
|
||||
recommendations = sections["recommendations"] ?: extractSectionByText(document, "توصیه"),
|
||||
pregnancyCategory = extractPregnancyCategory(document),
|
||||
pregnancyDescription = extractPregnancyDescription(document),
|
||||
dosageForms = extractDosageForms(document),
|
||||
brandNames = extractBrandNames(document),
|
||||
similarDrugs = extractSimilarDrugs(document),
|
||||
categories = extractCategories(document),
|
||||
comments = extractComments(document)
|
||||
)
|
||||
}
|
||||
|
||||
private fun extractGenericId(document: Document): String {
|
||||
val urlElement = document.select("link[rel=canonical]").first()
|
||||
val url = urlElement?.attr("href") ?: ""
|
||||
val regex = "/G-(\\d+)/".toRegex()
|
||||
return regex.find(url)?.groupValues?.get(1) ?: ""
|
||||
}
|
||||
|
||||
private fun extractPersianName(document: Document): String {
|
||||
val titleElement = document.select("h1.EnglishNumericFont").first()
|
||||
val fullText = titleElement?.text() ?: ""
|
||||
return fullText.replace("چیست و برای چه مواردی استفاده می شود؟", "").trim()
|
||||
}
|
||||
|
||||
private fun extractEnglishName(document: Document): String {
|
||||
val englishLabel = document.select("label.EnglishTopLabel").first()
|
||||
return englishLabel?.text()?.trim() ?: ""
|
||||
}
|
||||
|
||||
private fun extractDrugClass(document: Document): String? {
|
||||
val classElement = document.select("#divExtraInfo > div:first-child a.ahref_Generic").first()
|
||||
return classElement?.text()?.trim()
|
||||
}
|
||||
|
||||
private fun extractTherapeuticClass(document: Document): String? {
|
||||
val therapeuticElement = document.select("#divExtraInfo > div:last-child a.ahref_Generic").first()
|
||||
return therapeuticElement?.text()?.trim()
|
||||
}
|
||||
|
||||
/**
|
||||
* استخراج بخشها با استفاده از ID (روش قبلی)
|
||||
*/
|
||||
private fun extractAllSections(document: Document): Map<String, String> {
|
||||
val sections = mutableMapOf<String, String>()
|
||||
|
||||
// نقشه ID به کلید
|
||||
val idToKey = mapOf(
|
||||
"0" to "usage",
|
||||
"1" to "mechanism",
|
||||
"2" to "pharmacokinetics",
|
||||
"3" to "contraindications",
|
||||
"4" to "sideEffects",
|
||||
"5" to "interactions",
|
||||
"6" to "warnings",
|
||||
"7" to "recommendations"
|
||||
)
|
||||
|
||||
for ((id, key) in idToKey) {
|
||||
val section = extractSectionById(document, id)
|
||||
if (!section.isNullOrBlank()) {
|
||||
sections[key] = section
|
||||
}
|
||||
}
|
||||
|
||||
return sections
|
||||
}
|
||||
|
||||
private fun extractSectionById(document: Document, sectionId: String): String? {
|
||||
val sectionElement = document.select("h2.h2_TabTitle#${sectionId}").first()
|
||||
if (sectionElement == null) return null
|
||||
|
||||
val content = StringBuilder()
|
||||
var nextElement = sectionElement.nextElementSibling()
|
||||
|
||||
while (nextElement != null && !nextElement.select("h2.h2_TabTitle").hasText()) {
|
||||
if (nextElement.tagName() == "p" || nextElement.tagName() == "div") {
|
||||
val text = cleanHtmlText(nextElement.text())
|
||||
if (text.isNotBlank()) {
|
||||
content.append(text).append("\n\n")
|
||||
}
|
||||
}
|
||||
nextElement = nextElement.nextElementSibling()
|
||||
}
|
||||
|
||||
return content.toString().trim().takeIf { it.isNotEmpty() }
|
||||
}
|
||||
|
||||
/**
|
||||
* استخراج بخش با جستجوی متن عنوان (روش جایگزین)
|
||||
*/
|
||||
private fun extractSectionByText(document: Document, titleKeyword: String): String? {
|
||||
// جستجوی هدر حاوی کلمه کلیدی
|
||||
val header = document.select("h2.h2_TabTitle, h3").firstOrNull {
|
||||
it.text().contains(titleKeyword, ignoreCase = true)
|
||||
} ?: return null
|
||||
|
||||
val content = StringBuilder()
|
||||
var nextElement = header.nextElementSibling()
|
||||
|
||||
while (nextElement != null && !nextElement.select("h2.h2_TabTitle, h3").hasText()) {
|
||||
if (nextElement.tagName() == "p" || nextElement.tagName() == "div") {
|
||||
val text = cleanHtmlText(nextElement.text())
|
||||
if (text.isNotBlank()) {
|
||||
content.append(text).append("\n\n")
|
||||
}
|
||||
}
|
||||
nextElement = nextElement.nextElementSibling()
|
||||
}
|
||||
|
||||
return content.toString().trim().takeIf { it.isNotEmpty() }
|
||||
}
|
||||
|
||||
private fun cleanHtmlText(text: String): String {
|
||||
return text
|
||||
.replace(Regex("\\s+"), " ")
|
||||
.trim()
|
||||
}
|
||||
|
||||
private fun extractPregnancyCategory(document: Document): String? {
|
||||
val categoryElement = document.select("#UseInPregnancy .EnglishNumericFont, #UseInPregnancy > div.EnglishNumericFont").first()
|
||||
return categoryElement?.text()?.trim()
|
||||
}
|
||||
|
||||
private fun extractPregnancyDescription(document: Document): String? {
|
||||
val descElement = document.select("#UseInPregnancy p, #UseInPregnancy .alert").first()
|
||||
val text = descElement?.text()?.trim()
|
||||
// اگر متن "مصرف در بارداری ثبت نشده است" باشد، null برگردان
|
||||
return if (text.isNullOrBlank() || text.contains("ثبت نشده")) null else text
|
||||
}
|
||||
|
||||
private fun extractDosageForms(document: Document): List<DosageForm> {
|
||||
val forms = mutableListOf<DosageForm>()
|
||||
|
||||
// بررسی وجود جدول اشکال دارویی
|
||||
val table = document.select("#TBL_AshkalDarooyi").first()
|
||||
if (table == null) {
|
||||
println("No dosage forms table found")
|
||||
return forms
|
||||
}
|
||||
|
||||
val rows = table.select("tbody tr").filter { !it.hasClass("showMoreRow") && it.id() != "showMoreRow" }
|
||||
|
||||
for (row in rows) {
|
||||
try {
|
||||
val cells = row.select("td")
|
||||
if (cells.size >= 2) {
|
||||
val persianNameElement = cells[1].select("h3").first()
|
||||
val englishNameElement = cells[1].select("label.EnglishNumericFont").first()
|
||||
|
||||
// بررسی اینکه آیا داده معتبر است
|
||||
val persianName = persianNameElement?.text()?.trim()
|
||||
if (persianName.isNullOrBlank()) continue
|
||||
|
||||
forms.add(
|
||||
DosageForm(
|
||||
code = cells[0].text().trim(),
|
||||
persianName = persianName,
|
||||
englishName = englishNameElement?.text()?.trim() ?: "",
|
||||
isHighRisk = cells.getOrNull(2)?.hasText() == true,
|
||||
temperature = cells.getOrNull(3)?.text()?.takeIf { it.isNotBlank() },
|
||||
isVital = cells.getOrNull(4)?.hasText() == true,
|
||||
warningLabel = cells.getOrNull(5)?.text()?.takeIf { it.isNotBlank() }
|
||||
)
|
||||
)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
println("Error parsing dosage form: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
return forms
|
||||
}
|
||||
|
||||
private fun extractBrandNames(document: Document): List<BrandName> {
|
||||
val brands = mutableListOf<BrandName>()
|
||||
|
||||
// استخراج از بخش اسامی تجاری فارسی
|
||||
val persianRows = document.select("#PersCommertialDrugs .tableCommertial tbody tr.tr_persian")
|
||||
|
||||
// اگر ردیفی وجود نداشت، بررسی کن که آیا پیام "ثبت نشده" وجود دارد
|
||||
if (persianRows.isEmpty()) {
|
||||
val noDataMessage = document.select("#PersCommertialDrugs .alert")
|
||||
if (noDataMessage.isNotEmpty()) {
|
||||
println("No brand names available: ${noDataMessage.text()}")
|
||||
}
|
||||
return brands
|
||||
}
|
||||
|
||||
for (row in persianRows) {
|
||||
try {
|
||||
val linkElement = row.select("td:first-child a.ahref_Generic").first()
|
||||
val persianName = linkElement?.text()?.trim() ?: continue
|
||||
val detailUrl = "https://www.darooyab.ir${linkElement.attr("href")}"
|
||||
|
||||
val manufacturerElement = row.select("td:eq(1) a.ahref_Generic").first()
|
||||
val manufacturer = manufacturerElement?.text()?.trim()
|
||||
|
||||
val importerElement = row.select("td:eq(2) a.ahref_Generic").first()
|
||||
val importer = importerElement?.text()?.trim()
|
||||
|
||||
brands.add(
|
||||
BrandName(
|
||||
persianName = persianName,
|
||||
englishName = "",
|
||||
manufacturer = manufacturer,
|
||||
importer = importer,
|
||||
detailUrl = detailUrl
|
||||
)
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
println("Error parsing brand name: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
return brands
|
||||
}
|
||||
|
||||
private fun extractSimilarDrugs(document: Document): List<SimilarDrug> {
|
||||
val drugs = mutableListOf<SimilarDrug>()
|
||||
|
||||
// بررسی وجود جدول داروهای هم گروه
|
||||
val table = document.select("table.tableGroups").first()
|
||||
if (table == null) {
|
||||
println("No similar drugs table found")
|
||||
return drugs
|
||||
}
|
||||
|
||||
val rows = table.select("tbody tr").filter {
|
||||
!it.hasClass("hidden-row") && it.select("a#toggleButton").isEmpty()
|
||||
}
|
||||
|
||||
for (row in rows) {
|
||||
try {
|
||||
val cells = row.select("td")
|
||||
for (cell in cells) {
|
||||
val link = cell.select("a.ahref_Generic").first()
|
||||
if (link != null && link.text().isNotBlank()) {
|
||||
val href = link.attr("href")
|
||||
val genericIdRegex = "/G-(\\d+)/".toRegex()
|
||||
val genericId = genericIdRegex.find(href)?.groupValues?.get(1) ?: ""
|
||||
|
||||
drugs.add(
|
||||
SimilarDrug(
|
||||
persianName = link.text().trim(),
|
||||
englishName = null,
|
||||
genericId = genericId,
|
||||
detailUrl = "https://www.darooyab.ir$href"
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
println("Error parsing similar drug: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
return drugs
|
||||
}
|
||||
|
||||
private fun extractCategories(document: Document): DrugCategories? {
|
||||
val martindaleLink = document.select("#divExtraInfo > div:first-child a.ahref_Generic").first()
|
||||
val martindale = martindaleLink?.text()?.trim()
|
||||
val martindaleUrl = martindaleLink?.attr("href")?.let { "https://www.darooyab.ir$it" }
|
||||
|
||||
// بررسی طبقه بندی درمانی (ممکن است "بدون طبقه بندی درمانی" باشد)
|
||||
val therapeuticLinks = document.select("#divExtraInfo > div:last-child a.ahref_Generic")
|
||||
val therapeutic = therapeuticLinks.mapNotNull { it.text().trim().takeIf { text ->
|
||||
text != "بدون طبقه بندی درمانی" && text.isNotBlank()
|
||||
} }
|
||||
val therapeuticUrls = therapeuticLinks.map { "https://www.darooyab.ir${it.attr("href")}" }
|
||||
|
||||
return if (martindale != null || therapeutic.isNotEmpty()) {
|
||||
DrugCategories(
|
||||
martindale = martindale,
|
||||
martindaleUrl = martindaleUrl,
|
||||
therapeutic = therapeutic.ifEmpty { null },
|
||||
therapeuticUrls = therapeuticUrls.ifEmpty { null }
|
||||
)
|
||||
} else null
|
||||
}
|
||||
|
||||
private fun extractComments(document: Document): List<Comment> {
|
||||
val comments = mutableListOf<Comment>()
|
||||
val commentElements = document.select("#CommentContent .comment")
|
||||
|
||||
for (element in commentElements) {
|
||||
try {
|
||||
val authorElement = element.select("span").first()
|
||||
val author = authorElement?.text()?.replace("(", "")?.replace(")", "")?.trim() ?: "ناشناس"
|
||||
|
||||
val date = authorElement?.text()?.let {
|
||||
val regex = "\\((\\d{4}/\\d{1,2}/\\d{1,2})\\)".toRegex()
|
||||
regex.find(it)?.groupValues?.get(1) ?: ""
|
||||
} ?: ""
|
||||
|
||||
val textElement = element.select("p.commentText").first()
|
||||
val text = textElement?.text()?.trim() ?: ""
|
||||
|
||||
if (text.isBlank()) continue
|
||||
|
||||
val responseElement = element.select(".responseComment").first()
|
||||
val response = responseElement?.let { parseCommentResponse(it) }
|
||||
|
||||
comments.add(
|
||||
Comment(
|
||||
author = author,
|
||||
date = date,
|
||||
text = text,
|
||||
response = response
|
||||
)
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
println("Error parsing comment: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
return comments
|
||||
}
|
||||
|
||||
private fun parseCommentResponse(element: Element): CommentResponse {
|
||||
val doctorLink = element.select("a").first()
|
||||
val doctorName = doctorLink?.text()?.trim() ?: ""
|
||||
val doctorUrl = doctorLink?.attr("href")?.let { "https://www.darooyab.ir$it" }
|
||||
|
||||
val doctorText = element.select("span").first()?.text()?.trim() ?: ""
|
||||
val doctorTitle = doctorText.substringAfter(" - ").takeIf { it.isNotBlank() } ?: ""
|
||||
|
||||
val responseText = element.select("p.commentText").last()?.text()?.trim() ?: ""
|
||||
|
||||
return CommentResponse(
|
||||
doctorName = doctorName,
|
||||
doctorTitle = doctorTitle,
|
||||
text = responseText,
|
||||
doctorUrl = doctorUrl
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -5,9 +5,11 @@ import com.approagency.drug.data.dto.DrugListResponse
|
||||
import com.approagency.drug.data.dto.DrugModels
|
||||
import com.approagency.drug.data.remote.DarooyabApiService
|
||||
import com.approagency.drug.data.remote.DrugApiService
|
||||
import com.approagency.drug.data.remote.DrugDetailParser
|
||||
import com.approagency.drug.data.remote.DrugHtmlParser
|
||||
import com.approagency.drug.domain.model.DaroYabParams
|
||||
import com.approagency.drug.domain.model.DaroYabSearchResult
|
||||
import com.approagency.drug.domain.model.DrugDetail
|
||||
import com.approagency.drug.domain.model.DrugSearchParams
|
||||
import com.approagency.drug.domain.model.DrugSearchResult
|
||||
import com.approagency.drug.domain.repository.DrugRepository
|
||||
@@ -18,7 +20,8 @@ import java.io.IOException
|
||||
class DrugRepositoryImpl(
|
||||
private val apiService: DrugApiService,
|
||||
private val darooyabApiService: DarooyabApiService,
|
||||
private val parser: DrugHtmlParser
|
||||
private val parser: DrugHtmlParser,
|
||||
private val detailParser: DrugDetailParser,
|
||||
): DrugRepository {
|
||||
override suspend fun searchDrug(params: DrugSearchParams): Result<DrugListResponse> {
|
||||
return try {
|
||||
@@ -74,4 +77,16 @@ class DrugRepositoryImpl(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getDrugDetailFromYab(detailUrl: String): Result<DrugDetail> {
|
||||
return withContext(Dispatchers.IO) {
|
||||
try {
|
||||
val html = darooyabApiService.getDrugDetail(detailUrl)
|
||||
val detail = detailParser.parseDrugDetail(html)
|
||||
Result.success(detail)
|
||||
} catch (e: Exception) {
|
||||
Result.failure(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,10 +5,12 @@ import android.app.Application
|
||||
import com.approagency.drug.data.local.database.LabDatabase
|
||||
import com.approagency.drug.data.remote.DarooyabApiService
|
||||
import com.approagency.drug.data.remote.DrugApiService
|
||||
import com.approagency.drug.data.remote.DrugDetailParser
|
||||
import com.approagency.drug.data.remote.DrugHtmlParser
|
||||
import com.approagency.drug.data.repository.DrugRepositoryImpl
|
||||
import com.approagency.drug.data.repository.LabRepositoryImpl
|
||||
import com.approagency.drug.domain.repository.DrugRepository
|
||||
import com.approagency.drug.domain.usecase.DrugDetailYabUseCase
|
||||
import com.approagency.drug.domain.usecase.GetDarmanUseCase
|
||||
import com.approagency.drug.domain.usecase.GetDrugDetailUseCase
|
||||
import com.approagency.drug.domain.usecase.GetDrugSearchUseCase
|
||||
@@ -16,6 +18,7 @@ import com.approagency.drug.domain.usecase.GetTestGroupUseCase
|
||||
import com.approagency.drug.domain.usecase.GetTestItemByGroupId
|
||||
import com.approagency.drug.domain.usecase.SearchDrugsYabUseCase
|
||||
import com.approagency.drug.domain.usecase.SearchTestsUseCase
|
||||
import com.approagency.drug.presentation.viewModel.DrugDetailViewModel
|
||||
import com.approagency.drug.presentation.viewModel.HomeViewModel
|
||||
import com.approagency.drug.presentation.viewModel.LabViewModel
|
||||
import com.approagency.drug.utils.Config
|
||||
@@ -86,10 +89,10 @@ val appModule= module {
|
||||
|
||||
|
||||
factory { DrugHtmlParser() }
|
||||
|
||||
factory { DrugDetailParser() }
|
||||
//repo
|
||||
single<DrugRepository> {
|
||||
DrugRepositoryImpl(get<DrugApiService>() , get<DarooyabApiService>() , get())
|
||||
DrugRepositoryImpl(get<DrugApiService>() , get<DarooyabApiService>() , get() , get())
|
||||
}
|
||||
|
||||
|
||||
@@ -122,6 +125,14 @@ val appModule= module {
|
||||
|
||||
single { LabRepositoryImpl(get(), get()) }
|
||||
|
||||
single {
|
||||
DrugDetailYabUseCase(get())
|
||||
}
|
||||
|
||||
viewModel {
|
||||
DrugDetailViewModel(get())
|
||||
}
|
||||
|
||||
single { SearchTestsUseCase(get()) }
|
||||
//view model
|
||||
viewModel {
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
package com.approagency.drug.domain.model
|
||||
|
||||
data class DrugDetail(
|
||||
val genericId: String,
|
||||
val persianName: String,
|
||||
val englishName: String,
|
||||
val drugClass: String?,
|
||||
val therapeuticClass: String?,
|
||||
val usage: String?,
|
||||
val mechanism: String?,
|
||||
val pharmacokinetics: String?,
|
||||
val contraindications: String?,
|
||||
val sideEffects: String?,
|
||||
val interactions: String?,
|
||||
val warnings: String?,
|
||||
val recommendations: String?,
|
||||
val pregnancyCategory: String?,
|
||||
val pregnancyDescription: String?,
|
||||
val dosageForms: List<DosageForm>,
|
||||
val brandNames: List<BrandName>,
|
||||
val similarDrugs: List<SimilarDrug>,
|
||||
val categories: DrugCategories?,
|
||||
val comments: List<Comment>
|
||||
)
|
||||
|
||||
data class DosageForm(
|
||||
val code: String,
|
||||
val persianName: String,
|
||||
val englishName: String,
|
||||
val isHighRisk: Boolean,
|
||||
val temperature: String?,
|
||||
val isVital: Boolean,
|
||||
val warningLabel: String?
|
||||
)
|
||||
|
||||
data class BrandName(
|
||||
val persianName: String,
|
||||
val englishName: String,
|
||||
val manufacturer: String?,
|
||||
val importer: String?,
|
||||
val detailUrl: String
|
||||
)
|
||||
|
||||
data class SimilarDrug(
|
||||
val persianName: String,
|
||||
val englishName: String?,
|
||||
val genericId: String,
|
||||
val detailUrl: String
|
||||
)
|
||||
|
||||
data class DrugCategories(
|
||||
val martindale: String?,
|
||||
val martindaleUrl: String?,
|
||||
val therapeutic: List<String>?,
|
||||
val therapeuticUrls: List<String>?
|
||||
)
|
||||
|
||||
data class Comment(
|
||||
val author: String,
|
||||
val date: String,
|
||||
val text: String,
|
||||
val response: CommentResponse?
|
||||
)
|
||||
|
||||
data class CommentResponse(
|
||||
val doctorName: String,
|
||||
val doctorTitle: String,
|
||||
val text: String,
|
||||
val doctorUrl: String?
|
||||
)
|
||||
@@ -5,6 +5,7 @@ import com.approagency.drug.data.dto.DrugListResponse
|
||||
import com.approagency.drug.data.dto.DrugModels
|
||||
import com.approagency.drug.domain.model.DaroYabParams
|
||||
import com.approagency.drug.domain.model.DaroYabSearchResult
|
||||
import com.approagency.drug.domain.model.DrugDetail
|
||||
import com.approagency.drug.domain.model.DrugSearchParams
|
||||
import com.approagency.drug.domain.model.DrugSearchResult
|
||||
|
||||
@@ -13,4 +14,6 @@ interface DrugRepository {
|
||||
suspend fun drugDetail(cod:Int): Result<DrugModels>
|
||||
suspend fun getGorohDaroei(): Result<DarmanModel>
|
||||
suspend fun searchDrugs(params:DaroYabParams):Result<DaroYabSearchResult>
|
||||
suspend fun getDrugDetailFromYab(detailUrl: String):Result<DrugDetail>
|
||||
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.approagency.drug.domain.usecase
|
||||
|
||||
import com.approagency.drug.domain.model.DaroYabParams
|
||||
import com.approagency.drug.domain.model.DaroYabSearchResult
|
||||
import com.approagency.drug.domain.model.DrugDetail
|
||||
import com.approagency.drug.domain.repository.DrugRepository
|
||||
|
||||
class DrugDetailYabUseCase (
|
||||
private val repository: DrugRepository
|
||||
) {
|
||||
suspend operator fun invoke(detailUrl: String):Result<DrugDetail> {
|
||||
return repository.getDrugDetailFromYab(detailUrl)
|
||||
}
|
||||
}
|
||||
@@ -14,4 +14,4 @@ class SearchDrugsYabUseCase (
|
||||
val params = DaroYabParams(query = query, pageNumber = pageNumber)
|
||||
return repository.searchDrugs(params)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.approagency.drug.navigation
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
|
||||
@Composable
|
||||
fun AppNavGraph() {
|
||||
|
||||
val navController = rememberNavController()
|
||||
|
||||
NavHost(
|
||||
navController = navController,
|
||||
startDestination = "main_container"
|
||||
) {
|
||||
|
||||
composable("main_container") {
|
||||
|
||||
MainContainer()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
package com.approagency.drug.navigation
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Build
|
||||
import androidx.compose.material.icons.filled.Search
|
||||
import androidx.compose.material.icons.rounded.Refresh
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.NavigationBar
|
||||
import androidx.compose.material3.NavigationBarItem
|
||||
import androidx.compose.material3.NavigationBarItemDefaults
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.navigation.NavDestination.Companion.hierarchy
|
||||
import androidx.navigation.NavGraph.Companion.findStartDestination
|
||||
import androidx.navigation.NavHostController
|
||||
import androidx.navigation.compose.currentBackStackEntryAsState
|
||||
|
||||
@Composable
|
||||
fun BottomBar(
|
||||
navController: NavHostController
|
||||
) {
|
||||
val items = listOf(
|
||||
Triple(MainRoute.HomeGraph, "دارو", Icons.Default.Search),
|
||||
Triple(MainRoute.SearchGraph, "سرچ", Icons.Rounded.Refresh),
|
||||
Triple(MainRoute.LabGraph, "آزمایش", Icons.Default.Build)
|
||||
)
|
||||
|
||||
val navBackStackEntry by navController.currentBackStackEntryAsState()
|
||||
val currentDestination = navBackStackEntry?.destination
|
||||
Surface(
|
||||
tonalElevation = 2.dp,
|
||||
color = MaterialTheme.colorScheme.surface,
|
||||
modifier = Modifier
|
||||
.navigationBarsPadding()
|
||||
.height(50.dp)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
|
||||
items.forEach { item ->
|
||||
|
||||
val selected = currentDestination
|
||||
?.hierarchy
|
||||
?.any { it.route == item.first.route } == true
|
||||
|
||||
val color = if (selected)
|
||||
MaterialTheme.colorScheme.primary
|
||||
else
|
||||
MaterialTheme.colorScheme.onSurfaceVariant
|
||||
|
||||
val background = if (selected)
|
||||
MaterialTheme.colorScheme.primary.copy(alpha = 0.12f)
|
||||
else
|
||||
Color.Transparent
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(horizontal = 8.dp , vertical = 4.dp)
|
||||
.clip(MaterialTheme.shapes.large)
|
||||
.background(background)
|
||||
.clickable {
|
||||
navController.navigate(item.first.route) {
|
||||
popUpTo(navController.graph.findStartDestination().id) {
|
||||
saveState = true
|
||||
}
|
||||
launchSingleTop = true
|
||||
restoreState = true
|
||||
}
|
||||
}
|
||||
.padding(horizontal = 10.dp, vertical = 10.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
|
||||
Icon(
|
||||
imageVector = item.third,
|
||||
contentDescription = item.second,
|
||||
tint = color,
|
||||
modifier = Modifier.size(22.dp)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.width(6.dp))
|
||||
|
||||
Text(
|
||||
text = item.second,
|
||||
color = color,
|
||||
style = MaterialTheme.typography.labelMedium
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
package com.approagency.drug.navigation
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.NavHostController
|
||||
import androidx.navigation.compose.NavHost
|
||||
import com.approagency.drug.presentation.screens.HomeScreen
|
||||
import androidx.navigation.compose.composable
|
||||
import com.approagency.drug.presentation.screens.RootScreen
|
||||
|
||||
@Composable
|
||||
fun AppNavGraph(navController: NavHostController, modifier: Modifier = Modifier) {
|
||||
NavHost(
|
||||
navController = navController ,
|
||||
startDestination = NavRoutes.Home.route
|
||||
){
|
||||
composable(NavRoutes.Home.route) {
|
||||
RootScreen(
|
||||
navHostController = navController , modifier = Modifier
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package com.approagency.drug.navigation
|
||||
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import com.approagency.drug.navigation.graphs.homeGraph
|
||||
import com.approagency.drug.navigation.graphs.labGraph
|
||||
import com.approagency.drug.navigation.graphs.searchGraph
|
||||
|
||||
@Composable
|
||||
fun MainContainer() {
|
||||
|
||||
val navController = rememberNavController()
|
||||
|
||||
Scaffold(
|
||||
bottomBar = {
|
||||
BottomBar(navController)
|
||||
}
|
||||
) { padding ->
|
||||
|
||||
NavHost(
|
||||
navController = navController,
|
||||
startDestination = MainRoute.HomeGraph.route,
|
||||
modifier = Modifier.padding(padding)
|
||||
) {
|
||||
|
||||
homeGraph(navController)
|
||||
|
||||
searchGraph(navController)
|
||||
|
||||
labGraph(navController)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
package com.approagency.drug.navigation
|
||||
|
||||
abstract class NavRoutes (val route:String) {
|
||||
object Home: NavRoutes("home")
|
||||
object Detail: NavRoutes("detail/{cod}"){
|
||||
fun createRoute(cod: Int) = "detail/$cod"
|
||||
}
|
||||
object Lab: NavRoutes("lab")
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package com.approagency.drug.navigation
|
||||
|
||||
import android.net.Uri
|
||||
|
||||
sealed class MainRoute(
|
||||
val route: String
|
||||
) {
|
||||
|
||||
object HomeGraph : MainRoute("home_graph")
|
||||
|
||||
object SearchGraph : MainRoute("search_graph")
|
||||
|
||||
object LabGraph : MainRoute("lab_graph")
|
||||
}
|
||||
|
||||
sealed class Screen(
|
||||
val route: String
|
||||
) {
|
||||
|
||||
object Home : Screen("home")
|
||||
|
||||
object Search : Screen("search")
|
||||
|
||||
object Lab : Screen("lab")
|
||||
|
||||
object DrugDetail : Screen("drug_detail/{detailUrl}") {
|
||||
|
||||
fun createRoute(detailUrl: String): String {
|
||||
return "drug_detail/${Uri.encode(detailUrl)}"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.approagency.drug.navigation.graphs
|
||||
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.navigation.NavGraphBuilder
|
||||
import androidx.navigation.NavHostController
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.navigation
|
||||
import com.approagency.drug.navigation.MainRoute
|
||||
import com.approagency.drug.navigation.Screen
|
||||
import com.approagency.drug.presentation.screens.HomeScreen
|
||||
|
||||
fun NavGraphBuilder.homeGraph(
|
||||
navController: NavHostController
|
||||
) {
|
||||
|
||||
navigation(
|
||||
route = MainRoute.HomeGraph.route,
|
||||
startDestination = Screen.Home.route
|
||||
) {
|
||||
|
||||
composable(Screen.Home.route) {
|
||||
|
||||
HomeScreen(
|
||||
navController = navController , modifier = Modifier
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.approagency.drug.navigation.graphs
|
||||
|
||||
import androidx.navigation.NavGraphBuilder
|
||||
import androidx.navigation.NavHostController
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.navigation
|
||||
import com.approagency.drug.navigation.MainRoute
|
||||
import com.approagency.drug.navigation.Screen
|
||||
import com.approagency.drug.presentation.screens.LabScreen
|
||||
|
||||
fun NavGraphBuilder.labGraph(
|
||||
navController: NavHostController
|
||||
) {
|
||||
|
||||
navigation(
|
||||
route = MainRoute.LabGraph.route,
|
||||
startDestination = Screen.Lab.route
|
||||
) {
|
||||
|
||||
composable(Screen.Lab.route) {
|
||||
|
||||
LabScreen()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package com.approagency.drug.navigation.graphs
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.navigation.NavGraphBuilder
|
||||
import androidx.navigation.NavHostController
|
||||
import androidx.navigation.NavType
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.navArgument
|
||||
import androidx.navigation.navigation
|
||||
import com.approagency.drug.navigation.MainRoute
|
||||
import com.approagency.drug.navigation.Screen
|
||||
import com.approagency.drug.presentation.screens.DrugDetailScreen
|
||||
import com.approagency.drug.presentation.screens.SearchScreen
|
||||
|
||||
fun NavGraphBuilder.searchGraph(
|
||||
navController: NavHostController
|
||||
) {
|
||||
|
||||
navigation(
|
||||
route = MainRoute.SearchGraph.route,
|
||||
startDestination = Screen.Search.route
|
||||
) {
|
||||
|
||||
composable(Screen.Search.route) {
|
||||
|
||||
SearchScreen(
|
||||
navController = navController
|
||||
)
|
||||
}
|
||||
|
||||
composable(
|
||||
route = Screen.DrugDetail.route,
|
||||
arguments = listOf(
|
||||
navArgument("detailUrl") {
|
||||
type = NavType.StringType
|
||||
}
|
||||
)
|
||||
) {
|
||||
|
||||
val detailUrl =
|
||||
Uri.decode(
|
||||
it.arguments?.getString("detailUrl") ?: ""
|
||||
)
|
||||
|
||||
DrugDetailScreen(
|
||||
navController = navController,
|
||||
detailUrl = detailUrl
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,516 @@
|
||||
package com.approagency.drug.presentation.components
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.Divider
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.FilterChip
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
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.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.approagency.drug.R
|
||||
import com.approagency.drug.domain.model.DrugDetail
|
||||
import com.approagency.drug.presentation.common.CustomModalBottomSheet
|
||||
import com.approagency.drug.presentation.common.ErrorState
|
||||
import com.approagency.drug.presentation.viewModel.DrugDetailViewModel
|
||||
import com.approagency.drug.presentation.viewModel.DrugDetailYabState
|
||||
import com.vada.caller.ui.theme.LocalDime
|
||||
import com.vada.caller.ui.theme.dime
|
||||
import kotlinx.coroutines.delay
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun DrugDetailBottomSheet(
|
||||
detailUrl: String?,
|
||||
onDismiss: () -> Unit,
|
||||
viewModel: DrugDetailViewModel
|
||||
) {
|
||||
val dime = LocalDime.current
|
||||
val detailState by viewModel.detailState.collectAsStateWithLifecycle()
|
||||
|
||||
// Load detail when sheet opens with a valid URL
|
||||
LaunchedEffect(detailUrl) {
|
||||
if (detailUrl != null) {
|
||||
viewModel.loadDrugDetail(detailUrl)
|
||||
}
|
||||
}
|
||||
|
||||
// Reset when sheet is closed
|
||||
LaunchedEffect(detailUrl) {
|
||||
if (detailUrl == null) {
|
||||
viewModel.reset()
|
||||
}
|
||||
}
|
||||
|
||||
if (detailUrl != null) {
|
||||
CustomModalBottomSheet(
|
||||
onDismiss = {
|
||||
onDismiss()
|
||||
viewModel.reset()
|
||||
}
|
||||
) {
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
color = MaterialTheme.colorScheme.surface
|
||||
) {
|
||||
when (val state = detailState) {
|
||||
DrugDetailYabState.Idle -> {
|
||||
// Initial state, nothing to show yet
|
||||
}
|
||||
|
||||
DrugDetailYabState.Loading -> {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(400.dp)
|
||||
.padding(dime.xl),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
CircularProgressIndicator(
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
Spacer(modifier = Modifier.height(dime.md))
|
||||
Text(
|
||||
text = "در حال دریافت اطلاعات دارو...",
|
||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
is DrugDetailYabState.Success -> {
|
||||
DrugDetailContent(
|
||||
drugDetail = state.drugDetail,
|
||||
modifier = Modifier.padding(bottom = dime.lg)
|
||||
)
|
||||
}
|
||||
|
||||
is DrugDetailYabState.Error -> {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(300.dp)
|
||||
.padding(dime.xl),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
ErrorState(
|
||||
message = state.message,
|
||||
onRetry = { viewModel.loadDrugDetail(detailUrl) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DrugDetailContent(
|
||||
drugDetail: DrugDetail,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val dime = LocalDime.current
|
||||
var selectedTab by remember { mutableStateOf(0) }
|
||||
val tabs = listOf("عمومی", "تخصصی", "اشکال دارویی", "اسامی تجاری")
|
||||
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = dime.md)
|
||||
) {
|
||||
// Header with close button
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = dime.md),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.weight(1f)
|
||||
) {
|
||||
Text(
|
||||
text = drugDetail.persianName,
|
||||
fontSize = 18.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
if (drugDetail.englishName.isNotEmpty()) {
|
||||
Text(
|
||||
text = drugDetail.englishName,
|
||||
fontSize = 14.sp,
|
||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Categories
|
||||
if (drugDetail.drugClass != null || drugDetail.therapeuticClass != null) {
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = dime.sm),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.3f)
|
||||
),
|
||||
shape = RoundedCornerShape(dime.sm)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(dime.sm)
|
||||
) {
|
||||
drugDetail.drugClass?.let {
|
||||
Row {
|
||||
Text(
|
||||
text = "طبقه بندی: ",
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 13.sp
|
||||
)
|
||||
Text(
|
||||
text = it,
|
||||
fontSize = 13.sp,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
}
|
||||
drugDetail.therapeuticClass?.let {
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Row {
|
||||
Text(
|
||||
text = "طبقه درمانی: ",
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 13.sp
|
||||
)
|
||||
Text(
|
||||
text = it,
|
||||
fontSize = 13.sp,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Tabs
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = dime.sm)
|
||||
) {
|
||||
tabs.forEachIndexed { index, title ->
|
||||
androidx.compose.material3.Tab(
|
||||
selected = selectedTab == index,
|
||||
onClick = { selectedTab = index },
|
||||
modifier = Modifier.weight(1f),
|
||||
text = {
|
||||
Text(
|
||||
text = title,
|
||||
fontSize = 13.sp,
|
||||
fontWeight = if (selectedTab == index) FontWeight.Bold else FontWeight.Normal
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Divider(color = MaterialTheme.colorScheme.outline.copy(alpha = 0.3f))
|
||||
|
||||
// Tab content
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(500.dp)
|
||||
.padding(vertical = dime.md)
|
||||
) {
|
||||
when (selectedTab) {
|
||||
0 -> {
|
||||
// General Information
|
||||
item {
|
||||
GeneralInfoTab(drugDetail)
|
||||
}
|
||||
}
|
||||
1 -> {
|
||||
// Specialized Information
|
||||
drugDetail.usage?.let {
|
||||
item {
|
||||
InfoSection(title = "موارد مصرف", content = it)
|
||||
}
|
||||
}
|
||||
drugDetail.mechanism?.let {
|
||||
item {
|
||||
InfoSection(title = "مکانیسم اثر", content = it)
|
||||
}
|
||||
}
|
||||
drugDetail.contraindications?.let {
|
||||
item {
|
||||
InfoSection(title = "موارد منع مصرف", content = it)
|
||||
}
|
||||
}
|
||||
drugDetail.sideEffects?.let {
|
||||
item {
|
||||
InfoSection(title = "عوارض جانبی", content = it)
|
||||
}
|
||||
}
|
||||
drugDetail.interactions?.let {
|
||||
item {
|
||||
InfoSection(title = "تداخلات دارویی", content = it)
|
||||
}
|
||||
}
|
||||
drugDetail.warnings?.let {
|
||||
item {
|
||||
InfoSection(title = "هشدارها", content = it)
|
||||
}
|
||||
}
|
||||
drugDetail.recommendations?.let {
|
||||
item {
|
||||
InfoSection(title = "توصیههای دارویی", content = it)
|
||||
}
|
||||
}
|
||||
}
|
||||
2 -> {
|
||||
// Dosage Forms
|
||||
if (drugDetail.dosageForms.isNotEmpty()) {
|
||||
item {
|
||||
DosageFormsSection(dosageForms = drugDetail.dosageForms)
|
||||
}
|
||||
}
|
||||
}
|
||||
3 -> {
|
||||
// Brand Names
|
||||
if (drugDetail.brandNames.isNotEmpty()) {
|
||||
item {
|
||||
BrandNamesSection(brandNames = drugDetail.brandNames)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun GeneralInfoTab(drugDetail: DrugDetail) {
|
||||
val dime = LocalDime.current
|
||||
|
||||
Column {
|
||||
// Pregnancy Info
|
||||
if (drugDetail.pregnancyCategory != null || drugDetail.pregnancyDescription != null) {
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = dime.md),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = Color(0xFFFFF3E0)
|
||||
)
|
||||
) {
|
||||
Column(modifier = Modifier.padding(dime.md)) {
|
||||
Text(
|
||||
text = "مصرف در بارداری",
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 16.sp,
|
||||
color = Color(0xFFE65100)
|
||||
)
|
||||
Spacer(modifier = Modifier.height(dime.xs))
|
||||
drugDetail.pregnancyCategory?.let {
|
||||
Text(
|
||||
text = "گروه: $it",
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.Medium
|
||||
)
|
||||
}
|
||||
drugDetail.pregnancyDescription?.let {
|
||||
Spacer(modifier = Modifier.height(dime.xs))
|
||||
Text(
|
||||
text = it,
|
||||
fontSize = 13.sp,
|
||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.8f)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Usage
|
||||
drugDetail.usage?.let {
|
||||
InfoSection(title = "موارد مصرف", content = it)
|
||||
}
|
||||
|
||||
// Contraindications
|
||||
drugDetail.contraindications?.let {
|
||||
InfoSection(title = "موارد منع مصرف", content = it)
|
||||
}
|
||||
|
||||
// Side Effects
|
||||
drugDetail.sideEffects?.let {
|
||||
InfoSection(title = "عوارض جانبی", content = it)
|
||||
}
|
||||
|
||||
// Warnings
|
||||
drugDetail.warnings?.let {
|
||||
InfoSection(title = "هشدارها", content = it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun InfoSection(
|
||||
title: String,
|
||||
content: String,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val dime = LocalDime.current
|
||||
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = dime.md)
|
||||
) {
|
||||
Text(
|
||||
text = title,
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 16.sp,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
Spacer(modifier = Modifier.height(dime.xs))
|
||||
Divider(color = MaterialTheme.colorScheme.outline.copy(alpha = 0.3f))
|
||||
Spacer(modifier = Modifier.height(dime.xs))
|
||||
Text(
|
||||
text = content,
|
||||
fontSize = 14.sp,
|
||||
lineHeight = 22.sp,
|
||||
textAlign = TextAlign.Justify,
|
||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.9f)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DosageFormsSection(dosageForms: List<com.approagency.drug.domain.model.DosageForm>) {
|
||||
val dime = LocalDime.current
|
||||
|
||||
Column {
|
||||
Text(
|
||||
text = "اشکال دارویی",
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 16.sp,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.padding(bottom = dime.sm)
|
||||
)
|
||||
|
||||
dosageForms.forEach { form ->
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = dime.sm),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
|
||||
) {
|
||||
Column(modifier = Modifier.padding(dime.sm)) {
|
||||
Text(
|
||||
text = form.persianName,
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 14.sp
|
||||
)
|
||||
Text(
|
||||
text = form.englishName,
|
||||
fontSize = 12.sp,
|
||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f)
|
||||
)
|
||||
Row(
|
||||
modifier = Modifier.padding(top = dime.xs),
|
||||
horizontalArrangement = Arrangement.spacedBy(dime.sm)
|
||||
) {
|
||||
if (form.isHighRisk) {
|
||||
Card(
|
||||
onClick = { },
|
||||
content = { Text("پرخطر", fontSize = 10.sp) },
|
||||
|
||||
)
|
||||
}
|
||||
if (form.isVital) {
|
||||
Card(
|
||||
onClick = { },
|
||||
content = { Text("حیاتی", fontSize = 10.sp) },
|
||||
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun BrandNamesSection(brandNames: List<com.approagency.drug.domain.model.BrandName>) {
|
||||
val dime = LocalDime.current
|
||||
|
||||
Column {
|
||||
Text(
|
||||
text = "اسامی تجاری",
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 16.sp,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.padding(bottom = dime.sm)
|
||||
)
|
||||
|
||||
brandNames.forEach { brand ->
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = dime.sm),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = 1.dp)
|
||||
) {
|
||||
Column(modifier = Modifier.padding(dime.sm)) {
|
||||
Text(
|
||||
text = brand.persianName,
|
||||
fontWeight = FontWeight.Medium,
|
||||
fontSize = 14.sp
|
||||
)
|
||||
brand.manufacturer?.let {
|
||||
Text(
|
||||
text = "تولید کننده: $it",
|
||||
fontSize = 12.sp,
|
||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,564 @@
|
||||
package com.approagency.drug.presentation.screens
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Favorite
|
||||
import androidx.compose.material.icons.filled.FavoriteBorder
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.Divider
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Tab
|
||||
import androidx.compose.material3.TabRow
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.navigation.NavController
|
||||
import com.approagency.drug.domain.model.DrugDetail
|
||||
import com.approagency.drug.presentation.common.ErrorState
|
||||
import com.approagency.drug.presentation.viewModel.DrugDetailViewModel
|
||||
import com.approagency.drug.presentation.viewModel.DrugDetailYabState
|
||||
import com.vada.caller.ui.theme.LocalDime
|
||||
import com.vada.caller.ui.theme.dime
|
||||
import org.koin.androidx.compose.koinViewModel
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun DrugDetailScreen(
|
||||
navController: NavController,
|
||||
detailUrl: String,
|
||||
viewModel: DrugDetailViewModel = koinViewModel()
|
||||
) {
|
||||
val dime = LocalDime.current
|
||||
val detailState by viewModel.detailState.collectAsStateWithLifecycle()
|
||||
var selectedTab by remember { mutableIntStateOf(0) }
|
||||
val tabs = listOf("عمومی", "تخصصی", "اشکال دارویی", "اسامی تجاری")
|
||||
|
||||
LaunchedEffect(detailUrl) {
|
||||
viewModel.loadDrugDetail(detailUrl)
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = {
|
||||
when (val state = detailState) {
|
||||
is DrugDetailYabState.Success -> {
|
||||
Column {
|
||||
Text(
|
||||
text = state.drugDetail.persianName.take(20),
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
maxLines = 1
|
||||
)
|
||||
if (state.drugDetail.englishName.isNotEmpty()) {
|
||||
Text(
|
||||
text = state.drugDetail.englishName.take(25),
|
||||
fontSize = 12.sp,
|
||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f),
|
||||
maxLines = 1
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
Text("جزئیات دارو", fontSize = 16.sp)
|
||||
}
|
||||
}
|
||||
},
|
||||
navigationIcon = {
|
||||
IconButton(onClick = { navController.popBackStack() }) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||
contentDescription = "بازگشت"
|
||||
)
|
||||
}
|
||||
},
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = MaterialTheme.colorScheme.surface
|
||||
)
|
||||
)
|
||||
}
|
||||
) { paddingValues ->
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues)
|
||||
) {
|
||||
when (val state = detailState) {
|
||||
DrugDetailYabState.Idle -> {
|
||||
// Initial state
|
||||
}
|
||||
|
||||
DrugDetailYabState.Loading -> {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
CircularProgressIndicator(
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.size(48.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.height(dime.md))
|
||||
Text(
|
||||
text = "در حال دریافت اطلاعات دارو...",
|
||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
is DrugDetailYabState.Success -> {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxSize()
|
||||
) {
|
||||
// Tab Row
|
||||
TabRow(
|
||||
selectedTabIndex = selectedTab,
|
||||
containerColor = MaterialTheme.colorScheme.surface,
|
||||
contentColor = MaterialTheme.colorScheme.primary
|
||||
) {
|
||||
tabs.forEachIndexed { index, title ->
|
||||
Tab(
|
||||
selected = selectedTab == index,
|
||||
onClick = { selectedTab = index },
|
||||
text = {
|
||||
Text(
|
||||
text = title,
|
||||
fontSize = 14.sp,
|
||||
fontWeight = if (selectedTab == index) FontWeight.Bold else FontWeight.Normal
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Tab Content
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(horizontal = dime.md)
|
||||
.padding(top = dime.md),
|
||||
verticalArrangement = Arrangement.spacedBy(dime.md)
|
||||
) {
|
||||
when (selectedTab) {
|
||||
0 -> {
|
||||
item { GeneralInfoTabContent(state.drugDetail) }
|
||||
}
|
||||
1 -> {
|
||||
item { SpecializedInfoTabContent(state.drugDetail) }
|
||||
}
|
||||
2 -> {
|
||||
item { DosageFormsTabContent(state.drugDetail.dosageForms) }
|
||||
}
|
||||
3 -> {
|
||||
item { BrandNamesTabContent(state.drugDetail.brandNames) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
is DrugDetailYabState.Error -> {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
ErrorState(
|
||||
message = state.message,
|
||||
onRetry = { viewModel.loadDrugDetail(detailUrl) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun GeneralInfoTabContent(drugDetail: DrugDetail) {
|
||||
val dime = LocalDime.current
|
||||
|
||||
Column(verticalArrangement = Arrangement.spacedBy(dime.md)) {
|
||||
// Categories Card
|
||||
if (drugDetail.drugClass != null || drugDetail.therapeuticClass != null) {
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.2f)
|
||||
),
|
||||
shape = RoundedCornerShape(dime.sm)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(dime.md),
|
||||
verticalArrangement = Arrangement.spacedBy(dime.xs)
|
||||
) {
|
||||
drugDetail.drugClass?.let {
|
||||
Row {
|
||||
Text(
|
||||
text = "طبقه بندی مارتیندل: ",
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 14.sp
|
||||
)
|
||||
Text(
|
||||
text = it,
|
||||
fontSize = 14.sp,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
}
|
||||
drugDetail.therapeuticClass?.let {
|
||||
Row {
|
||||
Text(
|
||||
text = "طبقه درمانی: ",
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 14.sp
|
||||
)
|
||||
Text(
|
||||
text = it,
|
||||
fontSize = 14.sp,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Pregnancy Card
|
||||
if (drugDetail.pregnancyCategory != null || drugDetail.pregnancyDescription != null) {
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = Color(0xFFFFF3E0)
|
||||
),
|
||||
shape = RoundedCornerShape(dime.sm)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(dime.md),
|
||||
verticalArrangement = Arrangement.spacedBy(dime.xs)
|
||||
) {
|
||||
Text(
|
||||
text = "مصرف در بارداری",
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 16.sp,
|
||||
color = Color(0xFFE65100)
|
||||
)
|
||||
drugDetail.pregnancyCategory?.let {
|
||||
Text(
|
||||
text = "گروه: $it",
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.Medium
|
||||
)
|
||||
}
|
||||
drugDetail.pregnancyDescription?.let {
|
||||
Text(
|
||||
text = it,
|
||||
fontSize = 13.sp,
|
||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.8f)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Usage Section
|
||||
drugDetail.usage?.let {
|
||||
InfoSectionCard(title = "موارد مصرف", content = it)
|
||||
}
|
||||
|
||||
// Contraindications Section
|
||||
drugDetail.contraindications?.let {
|
||||
InfoSectionCard(title = "موارد منع مصرف", content = it)
|
||||
}
|
||||
|
||||
// Side Effects Section
|
||||
drugDetail.sideEffects?.let {
|
||||
InfoSectionCard(title = "عوارض جانبی", content = it)
|
||||
}
|
||||
|
||||
// Warnings Section
|
||||
drugDetail.warnings?.let {
|
||||
InfoSectionCard(title = "هشدارها", content = it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SpecializedInfoTabContent(drugDetail: DrugDetail) {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
drugDetail.usage?.let {
|
||||
InfoSectionCard(title = "موارد مصرف", content = it)
|
||||
}
|
||||
drugDetail.mechanism?.let {
|
||||
InfoSectionCard(title = "مکانیسم اثر", content = it)
|
||||
}
|
||||
drugDetail.pharmacokinetics?.let {
|
||||
InfoSectionCard(title = "فارماکوکینتیک", content = it)
|
||||
}
|
||||
drugDetail.contraindications?.let {
|
||||
InfoSectionCard(title = "موارد منع مصرف", content = it)
|
||||
}
|
||||
drugDetail.sideEffects?.let {
|
||||
InfoSectionCard(title = "عوارض جانبی", content = it)
|
||||
}
|
||||
drugDetail.interactions?.let {
|
||||
InfoSectionCard(title = "تداخلات دارویی", content = it)
|
||||
}
|
||||
drugDetail.warnings?.let {
|
||||
InfoSectionCard(title = "هشدارها", content = it)
|
||||
}
|
||||
drugDetail.recommendations?.let {
|
||||
InfoSectionCard(title = "توصیههای دارویی", content = it)
|
||||
}
|
||||
|
||||
if (drugDetail.usage == null &&
|
||||
drugDetail.mechanism == null &&
|
||||
drugDetail.contraindications == null &&
|
||||
drugDetail.sideEffects == null
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(32.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
text = "اطلاعات تخصصی برای این دارو ثبت نشده است",
|
||||
fontSize = 14.sp,
|
||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f),
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DosageFormsTabContent(dosageForms: List<com.approagency.drug.domain.model.DosageForm>) {
|
||||
val dime = LocalDime.current
|
||||
|
||||
if (dosageForms.isEmpty()) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(32.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
text = "اشکال دارویی برای این دارو ثبت نشده است",
|
||||
fontSize = 14.sp,
|
||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f),
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
}
|
||||
} else {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(dime.sm)) {
|
||||
dosageForms.forEach { form ->
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp),
|
||||
shape = RoundedCornerShape(dime.sm)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(dime.md),
|
||||
verticalArrangement = Arrangement.spacedBy(dime.xs)
|
||||
) {
|
||||
Text(
|
||||
text = form.persianName,
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 15.sp
|
||||
)
|
||||
if (form.englishName.isNotBlank()) {
|
||||
Text(
|
||||
text = form.englishName,
|
||||
fontSize = 12.sp,
|
||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f)
|
||||
)
|
||||
}
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(dime.sm)
|
||||
) {
|
||||
if (form.isHighRisk) {
|
||||
androidx.compose.material3.Surface(
|
||||
shape = RoundedCornerShape(4.dp),
|
||||
color = Color(0xFFFFEBEE),
|
||||
modifier = Modifier.padding(vertical = 4.dp)
|
||||
) {
|
||||
Text(
|
||||
text = "پرخطر",
|
||||
fontSize = 10.sp,
|
||||
color = Color(0xFFC62828),
|
||||
modifier = Modifier.padding(horizontal = 8.dp, vertical = 2.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
if (form.isVital) {
|
||||
androidx.compose.material3.Surface(
|
||||
shape = RoundedCornerShape(4.dp),
|
||||
color = Color(0xFFE8F5E9),
|
||||
modifier = Modifier.padding(vertical = 4.dp)
|
||||
) {
|
||||
Text(
|
||||
text = "حیاتی",
|
||||
fontSize = 10.sp,
|
||||
color = Color(0xFF2E7D32),
|
||||
modifier = Modifier.padding(horizontal = 8.dp, vertical = 2.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!form.warningLabel.isNullOrBlank()) {
|
||||
Text(
|
||||
text = "⚠️ ${form.warningLabel}",
|
||||
fontSize = 11.sp,
|
||||
color = Color(0xFFE65100),
|
||||
modifier = Modifier.padding(top = dime.xs)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun BrandNamesTabContent(brandNames: List<com.approagency.drug.domain.model.BrandName>) {
|
||||
val dime = LocalDime.current
|
||||
|
||||
if (brandNames.isEmpty()) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(32.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
text = "اسامی تجاری برای این دارو ثبت نشده است",
|
||||
fontSize = 14.sp,
|
||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f),
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
}
|
||||
} else {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(dime.sm)) {
|
||||
brandNames.forEach { brand ->
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = 1.dp),
|
||||
shape = RoundedCornerShape(dime.sm)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(dime.md),
|
||||
verticalArrangement = Arrangement.spacedBy(dime.xs)
|
||||
) {
|
||||
Text(
|
||||
text = brand.persianName,
|
||||
fontWeight = FontWeight.Medium,
|
||||
fontSize = 14.sp
|
||||
)
|
||||
brand.manufacturer?.let {
|
||||
Row {
|
||||
Text(
|
||||
text = "تولید کننده: ",
|
||||
fontSize = 12.sp,
|
||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f)
|
||||
)
|
||||
Text(
|
||||
text = it,
|
||||
fontSize = 12.sp,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
}
|
||||
brand.importer?.let {
|
||||
Row {
|
||||
Text(
|
||||
text = "وارد کننده: ",
|
||||
fontSize = 12.sp,
|
||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f)
|
||||
)
|
||||
Text(
|
||||
text = it,
|
||||
fontSize = 12.sp,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun InfoSectionCard(
|
||||
title: String,
|
||||
content: String,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val dime = LocalDime.current
|
||||
|
||||
Card(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = 1.dp),
|
||||
shape = RoundedCornerShape(dime.sm)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(dime.md),
|
||||
verticalArrangement = Arrangement.spacedBy(dime.xs)
|
||||
) {
|
||||
Text(
|
||||
text = title,
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 16.sp,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
Divider(color = MaterialTheme.colorScheme.outline.copy(alpha = 0.2f))
|
||||
Text(
|
||||
text = content,
|
||||
fontSize = 14.sp,
|
||||
lineHeight = 22.sp,
|
||||
textAlign = TextAlign.Justify,
|
||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.85f)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,21 +1,16 @@
|
||||
package com.approagency.drug.presentation.screens
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
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.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
@@ -27,14 +22,9 @@ import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.snapshotFlow
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.navigation.NavController
|
||||
import com.approagency.drug.domain.model.DaroYabSearchResult
|
||||
import com.approagency.drug.domain.model.DrugSearchResult
|
||||
import com.approagency.drug.navigation.Screen
|
||||
import com.approagency.drug.presentation.common.CustomTextFilled
|
||||
import com.approagency.drug.presentation.common.EmptySearchState
|
||||
import com.approagency.drug.presentation.common.EndOfListIndicator
|
||||
@@ -42,12 +32,12 @@ import com.approagency.drug.presentation.common.ErrorState
|
||||
import com.approagency.drug.presentation.common.LoadingMoreIndicator
|
||||
import com.approagency.drug.presentation.common.PrimaryButton
|
||||
import com.approagency.drug.presentation.components.DaroYabSearchResult
|
||||
import com.approagency.drug.presentation.components.PaginationControls
|
||||
import com.approagency.drug.presentation.components.DrugDetailBottomSheet
|
||||
import com.approagency.drug.presentation.viewModel.DrugDetailViewModel
|
||||
import com.approgency.drug.presentation.viewModel.SearchState
|
||||
import com.approgency.drug.presentation.viewModel.SearchViewModel
|
||||
import com.vada.caller.ui.theme.LocalDime
|
||||
import com.vada.caller.ui.theme.dime
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import org.koin.androidx.compose.koinViewModel
|
||||
|
||||
|
||||
@@ -55,22 +45,29 @@ import org.koin.androidx.compose.koinViewModel
|
||||
fun SearchScreen(
|
||||
navController: NavController,
|
||||
modifier: Modifier = Modifier,
|
||||
viewModel: SearchViewModel = koinViewModel()
|
||||
viewModel: SearchViewModel = koinViewModel(),
|
||||
drugDetailViewModel: DrugDetailViewModel = koinViewModel() // اضافه کنید
|
||||
) {
|
||||
val dime = LocalDime.current
|
||||
val keyboardController = LocalSoftwareKeyboardController.current
|
||||
var searchText by remember { mutableStateOf("") }
|
||||
val searchText = viewModel.searchText
|
||||
val state by viewModel.searchState.collectAsState()
|
||||
val lazyListState = rememberLazyListState()
|
||||
|
||||
var selectedDrugUrl by remember { mutableStateOf<String?>(null) }
|
||||
// Auto-search for testing (remove in production)
|
||||
LaunchedEffect(Unit) {
|
||||
if (searchText.isEmpty()) {
|
||||
searchText = "انتی"
|
||||
viewModel.searchDrugs(searchText)
|
||||
}
|
||||
// LaunchedEffect(Unit) {
|
||||
// if (searchText.isEmpty()) {
|
||||
// searchText = "انتی"
|
||||
// viewModel.searchDrugs(searchText)
|
||||
// }
|
||||
// }
|
||||
if (selectedDrugUrl != null) {
|
||||
DrugDetailBottomSheet(
|
||||
detailUrl = selectedDrugUrl,
|
||||
onDismiss = { selectedDrugUrl = null },
|
||||
viewModel = drugDetailViewModel
|
||||
)
|
||||
}
|
||||
|
||||
// Detect when user scrolls to the bottom to load more
|
||||
LaunchedEffect(lazyListState) {
|
||||
snapshotFlow { lazyListState.layoutInfo.visibleItemsInfo.lastOrNull()?.index }
|
||||
@@ -95,7 +92,7 @@ fun SearchScreen(
|
||||
// Search input
|
||||
CustomTextFilled(
|
||||
value = searchText,
|
||||
onValueChange = { searchText = it },
|
||||
onValueChange = { viewModel.updateSearchText(it) },
|
||||
onSearch = { query ->
|
||||
if (query.isNotBlank()) {
|
||||
keyboardController?.hide()
|
||||
@@ -105,7 +102,7 @@ fun SearchScreen(
|
||||
placeholder = "جستجوی دارو",
|
||||
showClearButton = true,
|
||||
showSearchButton = true,
|
||||
autoSearch = true, // Set to true if you want search while typing
|
||||
autoSearch = false,
|
||||
height = 45
|
||||
)
|
||||
Spacer(modifier = Modifier.height(MaterialTheme.dime.xs))
|
||||
@@ -144,9 +141,12 @@ fun SearchScreen(
|
||||
items(currentState.currentItems) { drug ->
|
||||
DaroYabSearchResult(
|
||||
drug = drug,
|
||||
onClickDetail = { selectedDrug ->println(selectedDrug.detailPageUrl)
|
||||
// Navigate to drug detail
|
||||
// navController.navigate("drug_detail/${selectedDrug.genericId}")
|
||||
onClickDetail = { selectedDrug ->
|
||||
navController.navigate(
|
||||
Screen.DrugDetail.createRoute(
|
||||
selectedDrug.detailPageUrl
|
||||
)
|
||||
)
|
||||
},
|
||||
onClickDrugStore = {}
|
||||
)
|
||||
@@ -165,9 +165,12 @@ fun SearchScreen(
|
||||
items(currentState.drugs) { drug ->
|
||||
DaroYabSearchResult(
|
||||
drug = drug,
|
||||
onClickDetail = { selectedDrug -> println(selectedDrug.detailPageUrl)
|
||||
// Navigate to drug detail
|
||||
// navController.navigate("drug_detail/${selectedDrug.genericId}")
|
||||
onClickDetail = { selectedDrug ->
|
||||
navController.navigate(
|
||||
Screen.DrugDetail.createRoute(
|
||||
selectedDrug.detailPageUrl
|
||||
)
|
||||
)
|
||||
},
|
||||
onClickDrugStore = {}
|
||||
)
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
package com.approagency.drug.presentation.viewModel
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.approagency.drug.domain.model.DrugDetail
|
||||
import com.approagency.drug.domain.usecase.DrugDetailYabUseCase
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class DrugDetailViewModel(
|
||||
private val drugDetailUseCase: DrugDetailYabUseCase
|
||||
) : ViewModel() {
|
||||
|
||||
private val _detailState = MutableStateFlow<DrugDetailYabState>(DrugDetailYabState.Idle)
|
||||
val detailState: StateFlow<DrugDetailYabState> = _detailState.asStateFlow()
|
||||
|
||||
fun loadDrugDetail(detailUrl: String) {
|
||||
viewModelScope.launch {
|
||||
_detailState.value = DrugDetailYabState.Loading
|
||||
val result = drugDetailUseCase(detailUrl)
|
||||
_detailState.value = when {
|
||||
result.isSuccess -> DrugDetailYabState.Success(result.getOrNull()!!)
|
||||
else -> DrugDetailYabState.Error(result.exceptionOrNull()?.message ?: "خطا در دریافت اطلاعات")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun reset() {
|
||||
_detailState.value = DrugDetailYabState.Idle
|
||||
}
|
||||
}
|
||||
|
||||
sealed class DrugDetailYabState {
|
||||
object Idle : DrugDetailYabState()
|
||||
object Loading : DrugDetailYabState()
|
||||
data class Success(val drugDetail: DrugDetail) : DrugDetailYabState()
|
||||
data class Error(val message: String) : DrugDetailYabState()
|
||||
}
|
||||
@@ -1,5 +1,8 @@
|
||||
package com.approgency.drug.presentation.viewModel
|
||||
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.approagency.drug.domain.model.DrugSearchResult
|
||||
@@ -16,7 +19,12 @@ class SearchViewModel(
|
||||
|
||||
private val _searchState = MutableStateFlow<SearchState>(SearchState.Idle)
|
||||
val searchState: StateFlow<SearchState> = _searchState.asStateFlow()
|
||||
var searchText by mutableStateOf("")
|
||||
private set
|
||||
|
||||
fun updateSearchText(value: String) {
|
||||
searchText = value
|
||||
}
|
||||
private var currentQuery = ""
|
||||
private var currentPage = 1
|
||||
private var totalPages = 1
|
||||
|
||||
Reference in New Issue
Block a user