feat: add detail screen and nav fixed

This commit is contained in:
2026-05-27 15:34:45 +03:30
parent bf2d1ad211
commit 6d89abc56d
22 changed files with 1969 additions and 69 deletions
@@ -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