feat: add detail screen and nav fixed
This commit is contained in:
@@ -42,8 +42,8 @@ class MainActivity : ComponentActivity() {
|
|||||||
Scaffold(modifier = Modifier.fillMaxSize(),
|
Scaffold(modifier = Modifier.fillMaxSize(),
|
||||||
) { innerPadding ->
|
) { innerPadding ->
|
||||||
AppNavGraph(
|
AppNavGraph(
|
||||||
navController = navController,
|
// navController = navController,
|
||||||
modifier = Modifier.padding(innerPadding)
|
// 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.dto.DrugModels
|
||||||
import com.approagency.drug.data.remote.DarooyabApiService
|
import com.approagency.drug.data.remote.DarooyabApiService
|
||||||
import com.approagency.drug.data.remote.DrugApiService
|
import com.approagency.drug.data.remote.DrugApiService
|
||||||
|
import com.approagency.drug.data.remote.DrugDetailParser
|
||||||
import com.approagency.drug.data.remote.DrugHtmlParser
|
import com.approagency.drug.data.remote.DrugHtmlParser
|
||||||
import com.approagency.drug.domain.model.DaroYabParams
|
import com.approagency.drug.domain.model.DaroYabParams
|
||||||
import com.approagency.drug.domain.model.DaroYabSearchResult
|
import com.approagency.drug.domain.model.DaroYabSearchResult
|
||||||
|
import com.approagency.drug.domain.model.DrugDetail
|
||||||
import com.approagency.drug.domain.model.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.repository.DrugRepository
|
import com.approagency.drug.domain.repository.DrugRepository
|
||||||
@@ -18,7 +20,8 @@ import java.io.IOException
|
|||||||
class DrugRepositoryImpl(
|
class DrugRepositoryImpl(
|
||||||
private val apiService: DrugApiService,
|
private val apiService: DrugApiService,
|
||||||
private val darooyabApiService: DarooyabApiService,
|
private val darooyabApiService: DarooyabApiService,
|
||||||
private val parser: DrugHtmlParser
|
private val parser: DrugHtmlParser,
|
||||||
|
private val detailParser: DrugDetailParser,
|
||||||
): DrugRepository {
|
): DrugRepository {
|
||||||
override suspend fun searchDrug(params: DrugSearchParams): Result<DrugListResponse> {
|
override suspend fun searchDrug(params: DrugSearchParams): Result<DrugListResponse> {
|
||||||
return try {
|
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.local.database.LabDatabase
|
||||||
import com.approagency.drug.data.remote.DarooyabApiService
|
import com.approagency.drug.data.remote.DarooyabApiService
|
||||||
import com.approagency.drug.data.remote.DrugApiService
|
import com.approagency.drug.data.remote.DrugApiService
|
||||||
|
import com.approagency.drug.data.remote.DrugDetailParser
|
||||||
import com.approagency.drug.data.remote.DrugHtmlParser
|
import com.approagency.drug.data.remote.DrugHtmlParser
|
||||||
import com.approagency.drug.data.repository.DrugRepositoryImpl
|
import com.approagency.drug.data.repository.DrugRepositoryImpl
|
||||||
import com.approagency.drug.data.repository.LabRepositoryImpl
|
import com.approagency.drug.data.repository.LabRepositoryImpl
|
||||||
import com.approagency.drug.domain.repository.DrugRepository
|
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.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
|
||||||
@@ -16,6 +18,7 @@ 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
|
||||||
import com.approagency.drug.domain.usecase.SearchTestsUseCase
|
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.HomeViewModel
|
||||||
import com.approagency.drug.presentation.viewModel.LabViewModel
|
import com.approagency.drug.presentation.viewModel.LabViewModel
|
||||||
import com.approagency.drug.utils.Config
|
import com.approagency.drug.utils.Config
|
||||||
@@ -86,10 +89,10 @@ val appModule= module {
|
|||||||
|
|
||||||
|
|
||||||
factory { DrugHtmlParser() }
|
factory { DrugHtmlParser() }
|
||||||
|
factory { DrugDetailParser() }
|
||||||
//repo
|
//repo
|
||||||
single<DrugRepository> {
|
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 { LabRepositoryImpl(get(), get()) }
|
||||||
|
|
||||||
|
single {
|
||||||
|
DrugDetailYabUseCase(get())
|
||||||
|
}
|
||||||
|
|
||||||
|
viewModel {
|
||||||
|
DrugDetailViewModel(get())
|
||||||
|
}
|
||||||
|
|
||||||
single { SearchTestsUseCase(get()) }
|
single { SearchTestsUseCase(get()) }
|
||||||
//view model
|
//view model
|
||||||
viewModel {
|
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.data.dto.DrugModels
|
||||||
import com.approagency.drug.domain.model.DaroYabParams
|
import com.approagency.drug.domain.model.DaroYabParams
|
||||||
import com.approagency.drug.domain.model.DaroYabSearchResult
|
import com.approagency.drug.domain.model.DaroYabSearchResult
|
||||||
|
import com.approagency.drug.domain.model.DrugDetail
|
||||||
import com.approagency.drug.domain.model.DrugSearchParams
|
import com.approagency.drug.domain.model.DrugSearchParams
|
||||||
import com.approagency.drug.domain.model.DrugSearchResult
|
import com.approagency.drug.domain.model.DrugSearchResult
|
||||||
|
|
||||||
@@ -13,4 +14,6 @@ interface DrugRepository {
|
|||||||
suspend fun drugDetail(cod:Int): Result<DrugModels>
|
suspend fun drugDetail(cod:Int): Result<DrugModels>
|
||||||
suspend fun getGorohDaroei(): Result<DarmanModel>
|
suspend fun getGorohDaroei(): Result<DarmanModel>
|
||||||
suspend fun searchDrugs(params:DaroYabParams):Result<DaroYabSearchResult>
|
suspend fun searchDrugs(params:DaroYabParams):Result<DaroYabSearchResult>
|
||||||
|
suspend fun getDrugDetailFromYab(detailUrl: String):Result<DrugDetail>
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
package com.approagency.drug.presentation.screens
|
||||||
|
|
||||||
import androidx.compose.foundation.clickable
|
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
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.foundation.lazy.rememberLazyListState
|
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.CircularProgressIndicator
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.OutlinedTextField
|
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
@@ -27,14 +22,9 @@ import androidx.compose.runtime.setValue
|
|||||||
import androidx.compose.runtime.snapshotFlow
|
import androidx.compose.runtime.snapshotFlow
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
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 androidx.navigation.NavController
|
||||||
import com.approagency.drug.domain.model.DaroYabSearchResult
|
import com.approagency.drug.navigation.Screen
|
||||||
import com.approagency.drug.domain.model.DrugSearchResult
|
|
||||||
import com.approagency.drug.presentation.common.CustomTextFilled
|
import com.approagency.drug.presentation.common.CustomTextFilled
|
||||||
import com.approagency.drug.presentation.common.EmptySearchState
|
import com.approagency.drug.presentation.common.EmptySearchState
|
||||||
import com.approagency.drug.presentation.common.EndOfListIndicator
|
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.LoadingMoreIndicator
|
||||||
import com.approagency.drug.presentation.common.PrimaryButton
|
import com.approagency.drug.presentation.common.PrimaryButton
|
||||||
import com.approagency.drug.presentation.components.DaroYabSearchResult
|
import com.approagency.drug.presentation.components.DaroYabSearchResult
|
||||||
import com.approagency.drug.presentation.components.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.SearchState
|
||||||
import com.approgency.drug.presentation.viewModel.SearchViewModel
|
import com.approgency.drug.presentation.viewModel.SearchViewModel
|
||||||
import com.vada.caller.ui.theme.LocalDime
|
import com.vada.caller.ui.theme.LocalDime
|
||||||
import com.vada.caller.ui.theme.dime
|
import com.vada.caller.ui.theme.dime
|
||||||
import kotlinx.coroutines.flow.collect
|
|
||||||
import org.koin.androidx.compose.koinViewModel
|
import org.koin.androidx.compose.koinViewModel
|
||||||
|
|
||||||
|
|
||||||
@@ -55,22 +45,29 @@ import org.koin.androidx.compose.koinViewModel
|
|||||||
fun SearchScreen(
|
fun SearchScreen(
|
||||||
navController: NavController,
|
navController: NavController,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
viewModel: SearchViewModel = koinViewModel()
|
viewModel: SearchViewModel = koinViewModel(),
|
||||||
|
drugDetailViewModel: DrugDetailViewModel = koinViewModel() // اضافه کنید
|
||||||
) {
|
) {
|
||||||
val dime = LocalDime.current
|
val dime = LocalDime.current
|
||||||
val keyboardController = LocalSoftwareKeyboardController.current
|
val keyboardController = LocalSoftwareKeyboardController.current
|
||||||
var searchText by remember { mutableStateOf("") }
|
val searchText = viewModel.searchText
|
||||||
val state by viewModel.searchState.collectAsState()
|
val state by viewModel.searchState.collectAsState()
|
||||||
val lazyListState = rememberLazyListState()
|
val lazyListState = rememberLazyListState()
|
||||||
|
var selectedDrugUrl by remember { mutableStateOf<String?>(null) }
|
||||||
// Auto-search for testing (remove in production)
|
// Auto-search for testing (remove in production)
|
||||||
LaunchedEffect(Unit) {
|
// LaunchedEffect(Unit) {
|
||||||
if (searchText.isEmpty()) {
|
// if (searchText.isEmpty()) {
|
||||||
searchText = "انتی"
|
// searchText = "انتی"
|
||||||
viewModel.searchDrugs(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
|
// Detect when user scrolls to the bottom to load more
|
||||||
LaunchedEffect(lazyListState) {
|
LaunchedEffect(lazyListState) {
|
||||||
snapshotFlow { lazyListState.layoutInfo.visibleItemsInfo.lastOrNull()?.index }
|
snapshotFlow { lazyListState.layoutInfo.visibleItemsInfo.lastOrNull()?.index }
|
||||||
@@ -95,7 +92,7 @@ fun SearchScreen(
|
|||||||
// Search input
|
// Search input
|
||||||
CustomTextFilled(
|
CustomTextFilled(
|
||||||
value = searchText,
|
value = searchText,
|
||||||
onValueChange = { searchText = it },
|
onValueChange = { viewModel.updateSearchText(it) },
|
||||||
onSearch = { query ->
|
onSearch = { query ->
|
||||||
if (query.isNotBlank()) {
|
if (query.isNotBlank()) {
|
||||||
keyboardController?.hide()
|
keyboardController?.hide()
|
||||||
@@ -105,7 +102,7 @@ fun SearchScreen(
|
|||||||
placeholder = "جستجوی دارو",
|
placeholder = "جستجوی دارو",
|
||||||
showClearButton = true,
|
showClearButton = true,
|
||||||
showSearchButton = true,
|
showSearchButton = true,
|
||||||
autoSearch = true, // Set to true if you want search while typing
|
autoSearch = false,
|
||||||
height = 45
|
height = 45
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(MaterialTheme.dime.xs))
|
Spacer(modifier = Modifier.height(MaterialTheme.dime.xs))
|
||||||
@@ -144,9 +141,12 @@ fun SearchScreen(
|
|||||||
items(currentState.currentItems) { drug ->
|
items(currentState.currentItems) { drug ->
|
||||||
DaroYabSearchResult(
|
DaroYabSearchResult(
|
||||||
drug = drug,
|
drug = drug,
|
||||||
onClickDetail = { selectedDrug ->println(selectedDrug.detailPageUrl)
|
onClickDetail = { selectedDrug ->
|
||||||
// Navigate to drug detail
|
navController.navigate(
|
||||||
// navController.navigate("drug_detail/${selectedDrug.genericId}")
|
Screen.DrugDetail.createRoute(
|
||||||
|
selectedDrug.detailPageUrl
|
||||||
|
)
|
||||||
|
)
|
||||||
},
|
},
|
||||||
onClickDrugStore = {}
|
onClickDrugStore = {}
|
||||||
)
|
)
|
||||||
@@ -165,9 +165,12 @@ fun SearchScreen(
|
|||||||
items(currentState.drugs) { drug ->
|
items(currentState.drugs) { drug ->
|
||||||
DaroYabSearchResult(
|
DaroYabSearchResult(
|
||||||
drug = drug,
|
drug = drug,
|
||||||
onClickDetail = { selectedDrug -> println(selectedDrug.detailPageUrl)
|
onClickDetail = { selectedDrug ->
|
||||||
// Navigate to drug detail
|
navController.navigate(
|
||||||
// navController.navigate("drug_detail/${selectedDrug.genericId}")
|
Screen.DrugDetail.createRoute(
|
||||||
|
selectedDrug.detailPageUrl
|
||||||
|
)
|
||||||
|
)
|
||||||
},
|
},
|
||||||
onClickDrugStore = {}
|
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
|
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.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.approagency.drug.domain.model.DrugSearchResult
|
import com.approagency.drug.domain.model.DrugSearchResult
|
||||||
@@ -16,7 +19,12 @@ class SearchViewModel(
|
|||||||
|
|
||||||
private val _searchState = MutableStateFlow<SearchState>(SearchState.Idle)
|
private val _searchState = MutableStateFlow<SearchState>(SearchState.Idle)
|
||||||
val searchState: StateFlow<SearchState> = _searchState.asStateFlow()
|
val searchState: StateFlow<SearchState> = _searchState.asStateFlow()
|
||||||
|
var searchText by mutableStateOf("")
|
||||||
|
private set
|
||||||
|
|
||||||
|
fun updateSearchText(value: String) {
|
||||||
|
searchText = value
|
||||||
|
}
|
||||||
private var currentQuery = ""
|
private var currentQuery = ""
|
||||||
private var currentPage = 1
|
private var currentPage = 1
|
||||||
private var totalPages = 1
|
private var totalPages = 1
|
||||||
|
|||||||
Reference in New Issue
Block a user