feat: parse text so better

This commit is contained in:
2026-05-27 18:07:46 +03:30
parent 6d89abc56d
commit e02bc76ff8
3 changed files with 693 additions and 291 deletions
@@ -4,19 +4,51 @@ import com.approagency.drug.domain.model.*
import org.jsoup.Jsoup
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import org.jsoup.select.Evaluator
class DrugDetailParser {
fun parseDrugDetail(html: String): DrugDetail {
val document = Jsoup.parse(html)
// اطلاعات پایه
// تشخیص نوع صفحه: Generic (G) یا Brand (B)
val isGenericPage = detectPageType(document)
return if (isGenericPage) {
parseGenericPage(document)
} else {
parseBrandPage(document)
}
}
/**
* تشخیص نوع صفحه با بررسی URL canonical و ساختار DOM
*/
private fun detectPageType(document: Document): Boolean {
// روش 1: بررسی URL canonical
val canonicalUrl = document.select("link[rel=canonical]").attr("href")
if (canonicalUrl.contains("/G-")) {
return true
}
if (canonicalUrl.contains("/B-")) {
return false
}
// روش 2: بررسی ساختار DOM (Fallback)
val hasGenericLayout = document.select("#UL_GenericTabInfo").isNotEmpty()
val hasBrandLayout = document.select("#BrandInfoContainer").isNotEmpty()
return hasGenericLayout || !hasBrandLayout // Default to generic if ambiguous
}
// ============================================================
// Generic Page Parsing
// ============================================================
private fun parseGenericPage(document: Document): DrugDetail {
val genericId = extractGenericId(document)
val persianName = extractPersianName(document)
val englishName = extractEnglishName(document)
// استخراج تمام بخش‌ها به صورت داینامیک
val sections = extractAllSections(document)
val sections = extractSectionsFromGeneric(document)
return DrugDetail(
genericId = genericId,
@@ -24,27 +56,30 @@ class DrugDetailParser {
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, "توصیه"),
usage = sections["usage"],
mechanism = sections["mechanism"],
pharmacokinetics = sections["pharmacokinetics"],
contraindications = sections["contraindications"],
sideEffects = sections["sideEffects"],
interactions = sections["interactions"],
warnings = sections["warnings"],
recommendations = sections["recommendations"],
pregnancyCategory = extractPregnancyCategory(document),
pregnancyDescription = extractPregnancyDescription(document),
dosageForms = extractDosageForms(document),
brandNames = extractBrandNames(document),
similarDrugs = extractSimilarDrugs(document),
categories = extractCategories(document),
comments = extractComments(document)
comments = extractComments(document),
manufacturer = null,
isGeneric = true,
generalInfo = extractGeneralInfoGeneric(document),
specializedInfo = extractSpecializedInfoGeneric(document),
)
}
private fun extractGenericId(document: Document): String {
val urlElement = document.select("link[rel=canonical]").first()
val url = urlElement?.attr("href") ?: ""
val url = document.select("link[rel=canonical]").attr("href")
val regex = "/G-(\\d+)/".toRegex()
return regex.find(url)?.groupValues?.get(1) ?: ""
}
@@ -52,124 +87,283 @@ class DrugDetailParser {
private fun extractPersianName(document: Document): String {
val titleElement = document.select("h1.EnglishNumericFont").first()
val fullText = titleElement?.text() ?: ""
return fullText.replace("چیست و برای چه مواردی استفاده می شود؟", "").trim()
// Remove the suffix "چیست و برای چه مواردی استفاده می شود؟"
return fullText.replace(Regex("\\s*چیست و برای چه مواردی استفاده می شود\\?\\s*"), "").trim()
}
private fun extractEnglishName(document: Document): String {
val englishLabel = document.select("label.EnglishTopLabel").first()
return englishLabel?.text()?.trim() ?: ""
return document.select("label.EnglishTopLabel").text().trim()
}
private fun extractDrugClass(document: Document): String? {
val classElement = document.select("#divExtraInfo > div:first-child a.ahref_Generic").first()
return classElement?.text()?.trim()
return document.select("#divExtraInfo > div:first-child a.ahref_Generic").first()?.text()?.trim()
}
private fun extractTherapeuticClass(document: Document): String? {
val therapeuticElement = document.select("#divExtraInfo > div:last-child a.ahref_Generic").first()
return therapeuticElement?.text()?.trim()
// The therapeutic class can be a chain of links
val classLinks = document.select("#divExtraInfo > div:last-child a.ahref_Generic")
return if (classLinks.isNotEmpty()) classLinks.joinToString(" > ") { it.text().trim() } else null
}
/**
* استخراج بخش‌ها با استفاده از ID (روش قبلی)
* Generic pages store content in divs with IDs inside #EtelaatTakhasosiContent
* The titles are h2.h2_TabTitle, and content is everything until the next h2.
*/
private fun extractAllSections(document: Document): Map<String, String> {
private fun extractSectionsFromGeneric(document: Document): Map<String, String> {
val sections = mutableMapOf<String, String>()
val specializedContentDiv = document.getElementById("EtelaatTakhasosiContent") ?: return sections
// نقشه 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"
// Mapping of section title keywords to our data class keys
val titleToKey = mapOf(
"موارد مصرف" to "usage",
"مکانیسم اثر" to "mechanism",
"فارماکوکینتیک" to "pharmacokinetics",
"منع مصرف" to "contraindications",
"عوارض جانبی" to "sideEffects",
"تداخلات دارویی" to "interactions",
"هشدار" to "warnings",
"توصیه های دارویی" to "recommendations"
)
for ((id, key) in idToKey) {
val section = extractSectionById(document, id)
if (!section.isNullOrBlank()) {
sections[key] = section
val sectionHeaders = specializedContentDiv.select("h2.h2_TabTitle")
for (header in sectionHeaders) {
val headerText = header.text().trim()
val sectionKey = titleToKey.entries.find { headerText.contains(it.key) }?.value
if (sectionKey != null) {
var contentElement = header.nextElementSibling()
val contentBuilder = StringBuilder()
while (contentElement != null && !contentElement.select("h2.h2_TabTitle").hasText()) {
// Extract clean text from various elements
val text = cleanText(contentElement)
if (text.isNotBlank()) {
contentBuilder.append(text).append("\n\n")
}
contentElement = contentElement.nextElementSibling()
}
val content = contentBuilder.toString().trim()
if (content.isNotEmpty()) {
sections[sectionKey] = content
}
}
}
return sections
}
private fun extractSectionById(document: Document, sectionId: String): String? {
val sectionElement = document.select("h2.h2_TabTitle#${sectionId}").first()
if (sectionElement == null) return null
/**
* General info for generic pages is inside #EtelaatOmomiVaTakhasosi > #EtelaatOmomi
*/
private fun extractGeneralInfoGeneric(document: Document): String? {
val generalDiv = document.select("#EtelaatOmomiVaTakhasosi #EtelaatOmomi").first() ?: return null
// Clone the element to avoid modifying the original document
val clone = generalDiv.clone()
val content = StringBuilder()
var nextElement = sectionElement.nextElementSibling()
// Remove the accordion (table of contents) as it's not part of the main content
clone.select(".accordion").remove()
// Remove navigation boxes if any
clone.select("#nav_box_general").remove()
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() }
return cleanText(clone).trim().takeIf { it.isNotEmpty() }
}
/**
* استخراج بخش با جستجوی متن عنوان (روش جایگزین)
* Specialized info for generic pages is inside #EtelaatTakhasosiContent
*/
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
private fun extractSpecializedInfoGeneric(document: Document): String? {
val specializedDiv = document.getElementById("EtelaatTakhasosiContent") ?: return null
val clone = specializedDiv.clone()
val content = StringBuilder()
var nextElement = header.nextElementSibling()
// Remove the table of contents
clone.select(".accordion").remove()
// Remove the "دارو های هم گروه" section and sources if you don't want them in specializedInfo
clone.select("#Teammate").remove()
clone.select("#externalLinks").remove()
while (nextElement != null && !nextElement.select("h2.h2_TabTitle, h3").hasText()) {
if (nextElement.tagName() == "p" || nextElement.tagName() == "div") {
val text = cleanHtmlText(nextElement.text())
return cleanText(clone).trim().takeIf { it.isNotEmpty() }
}
// ============================================================
// Brand Page Parsing
// ============================================================
private fun parseBrandPage(document: Document): DrugDetail {
val brandId = extractBrandId(document)
val persianName = extractBrandPersianName(document)
val englishName = extractBrandEnglishName(document)
val manufacturer = extractManufacturer(document)
val genericInfo = extractGenericInfo(document)
// Extract all sections from .brandAttrPersDesc
val (introText, sections) = extractBrandSections(document)
return DrugDetail(
genericId = genericInfo?.genericId ?: "",
persianName = persianName,
englishName = englishName,
drugClass = genericInfo?.persianName, // Drug class is essentially the generic name
therapeuticClass = extractBrandTherapeuticClass(document),
usage = sections["usage"] ?: introText,
mechanism = sections["mechanism"],
pharmacokinetics = null,
contraindications = sections["contraindications"],
sideEffects = sections["sideEffects"],
interactions = sections["interactions"],
warnings = sections["warnings"],
recommendations = sections["recommendations"],
pregnancyCategory = null,
pregnancyDescription = null,
dosageForms = extractOtherBrandForms(document),
brandNames = listOf(), // This page itself is a brand
similarDrugs = emptyList(),
categories = null,
comments = extractComments(document),
manufacturer = manufacturer,
genericInfo = genericInfo,
otherBrandForms = extractOtherBrandForms(document),
isGeneric = false
)
}
private fun extractBrandId(document: Document): String {
val url = document.select("link[rel=canonical]").attr("href")
val regex = "/B-(\\d+)/".toRegex()
return regex.find(url)?.groupValues?.get(1) ?: ""
}
private fun extractBrandPersianName(document: Document): String {
return document.select("#h1PersianName").text().trim()
}
private fun extractBrandEnglishName(document: Document): String {
return document.select("#h2EnglishName").text().trim()
}
private fun extractManufacturer(document: Document): String? {
val manufacturerLink = document.select("#divProducer a.ahref_Generic").first()
return manufacturerLink?.text()?.trim()
}
private fun extractGenericInfo(document: Document): GenericInfo? {
val genericLink = document.select("#divAjzaContent a.ahref_Generic").first() ?: return null
val href = genericLink.attr("href")
val genericIdRegex = "/G-(\\d+)/".toRegex()
val genericId = genericIdRegex.find(href)?.groupValues?.get(1) ?: ""
return GenericInfo(
genericId = genericId,
persianName = genericLink.text().trim(),
detailUrl = "https://www.darooyab.ir$href"
)
}
private fun extractBrandTherapeuticClass(document: Document): String? {
val therapeuticContainer = document.select("#brand_desc > div:last-child").first()
val text = therapeuticContainer?.text() ?: ""
val regex = "طبقه بندی درمانی :\\s*(.+?)(?:\$|\\n)".toRegex()
return regex.find(text)?.groupValues?.get(1)?.trim()
}
/**
* Extract all content from .brandAttrPersDesc.
* Returns a Pair: first is the introductory text (before any h2), second is a map of section title to content.
*/
private fun extractBrandSections(document: Document): Pair<String?, Map<String, String>> {
val container = document.select(".brandAttrPersDesc").first() ?: return Pair(null, emptyMap())
val sections = mutableMapOf<String, String>()
val titleToKey = mapOf(
"موارد مصرف" to "usage",
"مکانیسم اثر" to "mechanism",
"منع مصرف" to "contraindications",
"عوارض جانبی" to "sideEffects",
"تداخلات دارویی" to "interactions",
"هشدار" to "warnings",
"توصیه های دارویی" to "recommendations"
)
var introText: String? = null
var currentSectionKey: String? = null
val contentBuilder = StringBuilder()
for (child in container.children()) {
if (child.tagName() == "h2" || child.tagName() == "h3") {
// If we were building a previous section, save it
if (currentSectionKey != null && contentBuilder.isNotEmpty()) {
sections[currentSectionKey] = contentBuilder.toString().trim()
contentBuilder.clear()
}
// Start a new section
val headerText = child.text().trim()
currentSectionKey = titleToKey.entries.find { headerText.contains(it.key) }?.value
} else {
val text = cleanText(child)
if (text.isNotBlank()) {
content.append(text).append("\n\n")
if (currentSectionKey == null) {
// This is introductory text before any h2
introText = introText?.let { "$it\n\n$text" } ?: text
} else {
contentBuilder.append(text).append("\n\n")
}
}
}
nextElement = nextElement.nextElementSibling()
}
return content.toString().trim().takeIf { it.isNotEmpty() }
// Save the last section if any
if (currentSectionKey != null && contentBuilder.isNotEmpty()) {
sections[currentSectionKey] = contentBuilder.toString().trim()
}
private fun cleanHtmlText(text: String): String {
return text
.replace(Regex("\\s+"), " ")
.trim()
return Pair(introText, sections)
}
private fun extractOtherBrandForms(document: Document): List<DosageForm> {
val forms = mutableListOf<DosageForm>()
val brandmateDiv = document.select(".brandmate").first() ?: return forms
val links = brandmateDiv.select("a.ahref_Brand")
for (link in links) {
val persianName = link.text().trim()
val detailUrl = "https://www.darooyab.ir${link.attr("href")}"
forms.add(
DosageForm(
code = "",
persianName = persianName,
englishName = "",
isHighRisk = false,
temperature = null,
isVital = false,
warningLabel = null,
detailUrl = detailUrl
)
)
}
return forms
}
// ============================================================
// Common Extractors (for both page types)
// ============================================================
private fun extractPregnancyCategory(document: Document): String? {
// On generic pages only
val categoryElement = document.select("#UseInPregnancy .EnglishNumericFont, #UseInPregnancy > div.EnglishNumericFont").first()
return categoryElement?.text()?.trim()
}
private fun extractPregnancyDescription(document: Document): String? {
// On generic pages only
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 table = document.select("#TBL_AshkalDarooyi").first() ?: return forms
val rows = table.select("tbody tr").filter { !it.hasClass("showMoreRow") && it.id() != "showMoreRow" }
@@ -178,17 +372,13 @@ class DrugDetailParser {
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
val persianName = persianNameElement?.text()?.trim() ?: continue
forms.add(
DosageForm(
code = cells[0].text().trim(),
persianName = persianName,
englishName = englishNameElement?.text()?.trim() ?: "",
englishName = cells[1].select("label.EnglishNumericFont").first()?.text()?.trim() ?: "",
isHighRisk = cells.getOrNull(2)?.hasText() == true,
temperature = cells.getOrNull(3)?.text()?.takeIf { it.isNotBlank() },
isVital = cells.getOrNull(4)?.hasText() == true,
@@ -200,34 +390,22 @@ class DrugDetailParser {
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
}
if (persianRows.isEmpty()) 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()
@@ -244,19 +422,12 @@ class DrugDetailParser {
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 table = document.select("table.tableGroups").first() ?: return drugs
val rows = table.select("tbody tr").filter {
!it.hasClass("hidden-row") && it.select("a#toggleButton").isEmpty()
@@ -264,8 +435,7 @@ class DrugDetailParser {
for (row in rows) {
try {
val cells = row.select("td")
for (cell in cells) {
for (cell in row.select("td")) {
val link = cell.select("a.ahref_Generic").first()
if (link != null && link.text().isNotBlank()) {
val href = link.attr("href")
@@ -286,7 +456,6 @@ class DrugDetailParser {
println("Error parsing similar drug: ${e.message}")
}
}
return drugs
}
@@ -295,7 +464,6 @@ class DrugDetailParser {
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()
@@ -320,15 +488,12 @@ class DrugDetailParser {
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()
@@ -346,7 +511,6 @@ class DrugDetailParser {
println("Error parsing comment: ${e.message}")
}
}
return comments
}
@@ -354,10 +518,8 @@ class DrugDetailParser {
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(
@@ -367,4 +529,29 @@ class DrugDetailParser {
doctorUrl = doctorUrl
)
}
/**
* Helper function to clean and normalize text from an HTML element.
* Converts block elements to newlines for better readability.
*/
private fun cleanText(element: Element): String {
// Clone to avoid affecting the original
val clone = element.clone()
// Replace block-level elements with newlines for structure
clone.select("p, div, h2, h3, h4, li, br").before("\n")
// Get the text and clean it up
var text = clone.text()
.replace(Regex("\\n\\s*\\n+"), "\n\n") // Collapse multiple newlines
.replace(Regex("[ \\t]+"), " ") // Collapse spaces/tabs
.trim()
// Ensure lists look decent
if (element.tagName() == "li") {
text = "$text"
}
return text
}
}
@@ -20,9 +20,19 @@ data class DrugDetail(
val brandNames: List<BrandName>,
val similarDrugs: List<SimilarDrug>,
val categories: DrugCategories?,
val comments: List<Comment>
val comments: List<Comment>,
val manufacturer: String? = null,
val genericInfo: GenericInfo? = null,
val otherBrandForms: List<DosageForm> = emptyList(),
val isGeneric: Boolean = true,
val generalInfo: String? = null,
val specializedInfo: String? = null,
)
data class GenericInfo(
val genericId: String,
val persianName: String,
val detailUrl: String
)
data class DosageForm(
val code: String,
val persianName: String,
@@ -30,7 +40,8 @@ data class DosageForm(
val isHighRisk: Boolean,
val temperature: String?,
val isVital: Boolean,
val warningLabel: String?
val warningLabel: String?,
val detailUrl: String? = null
)
data class BrandName(
@@ -12,12 +12,10 @@ 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.material.icons.filled.Info
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.CircularProgressIndicator
@@ -36,7 +34,6 @@ 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
@@ -66,6 +63,7 @@ fun DrugDetailScreen(
val dime = LocalDime.current
val detailState by viewModel.detailState.collectAsStateWithLifecycle()
var selectedTab by remember { mutableIntStateOf(0) }
val tabs = listOf("عمومی", "تخصصی", "اشکال دارویی", "اسامی تجاری")
LaunchedEffect(detailUrl) {
@@ -80,16 +78,16 @@ fun DrugDetailScreen(
is DrugDetailYabState.Success -> {
Column {
Text(
text = state.drugDetail.persianName.take(20),
text = state.drugDetail.persianName.take(25),
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),
text = state.drugDetail.englishName.take(30),
fontSize = 11.sp,
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f),
maxLines = 1
)
}
@@ -120,9 +118,7 @@ fun DrugDetailScreen(
.padding(paddingValues)
) {
when (val state = detailState) {
DrugDetailYabState.Idle -> {
// Initial state
}
DrugDetailYabState.Idle -> {}
DrugDetailYabState.Loading -> {
Box(
@@ -144,9 +140,18 @@ fun DrugDetailScreen(
}
is DrugDetailYabState.Success -> {
Column(
modifier = Modifier.fillMaxSize()
) {
val drugDetail = state.drugDetail
Column(modifier = Modifier.fillMaxSize()) {
// Manufacturer Info Card (برای صفحات برند)
if (!drugDetail.isGeneric && drugDetail.manufacturer != null) {
ManufacturerInfoCard(
manufacturer = drugDetail.manufacturer!!,
genericInfo = drugDetail.genericInfo,
modifier = Modifier.padding(horizontal = dime.md, vertical = dime.sm)
)
}
// Tab Row
TabRow(
selectedTabIndex = selectedTab,
@@ -160,8 +165,9 @@ fun DrugDetailScreen(
text = {
Text(
text = title,
fontSize = 14.sp,
fontWeight = if (selectedTab == index) FontWeight.Bold else FontWeight.Normal
fontSize = 13.sp,
fontWeight = if (selectedTab == index) FontWeight.Bold else FontWeight.Normal,
maxLines = 1
)
}
)
@@ -178,16 +184,16 @@ fun DrugDetailScreen(
) {
when (selectedTab) {
0 -> {
item { GeneralInfoTabContent(state.drugDetail) }
item { GeneralInfoTabContent(drugDetail) }
}
1 -> {
item { SpecializedInfoTabContent(state.drugDetail) }
item { SpecializedInfoTabContent(drugDetail) }
}
2 -> {
item { DosageFormsTabContent(state.drugDetail.dosageForms) }
item { DosageFormsTabContent(drugDetail) }
}
3 -> {
item { BrandNamesTabContent(state.drugDetail.brandNames) }
item { BrandNamesTabContent(drugDetail) }
}
}
}
@@ -210,11 +216,100 @@ fun DrugDetailScreen(
}
}
@Composable
fun ManufacturerInfoCard(
manufacturer: String,
genericInfo: com.approagency.drug.domain.model.GenericInfo?,
modifier: Modifier = Modifier
) {
val dime = LocalDime.current
Card(
modifier = modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.15f)
),
shape = RoundedCornerShape(dime.sm)
) {
Column(
modifier = Modifier.padding(dime.md),
verticalArrangement = Arrangement.spacedBy(dime.xs)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(dime.xs)
) {
Icon(
imageVector = Icons.Default.Info,
contentDescription = null,
modifier = Modifier.size(18.dp),
tint = MaterialTheme.colorScheme.primary
)
Text(
text = "سازنده:",
fontWeight = FontWeight.Bold,
fontSize = 13.sp
)
Text(
text = manufacturer,
fontSize = 13.sp,
color = MaterialTheme.colorScheme.primary
)
}
if (genericInfo != null) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(dime.xs)
) {
Icon(
imageVector = Icons.Default.Info,
contentDescription = null,
modifier = Modifier.size(18.dp),
tint = MaterialTheme.colorScheme.primary
)
Text(
text = "ماده موثره:",
fontWeight = FontWeight.Bold,
fontSize = 13.sp
)
Text(
text = genericInfo.persianName,
fontSize = 13.sp,
color = MaterialTheme.colorScheme.primary
)
}
}
}
}
}
// آپدیت GeneralInfoTabContent برای صفحات Generic
@Composable
fun GeneralInfoTabContent(drugDetail: DrugDetail) {
val dime = LocalDime.current
Column(verticalArrangement = Arrangement.spacedBy(dime.md)) {
if (drugDetail.isGeneric) {
// ========== صفحات ژنریک (Generic) ==========
// اگر generalInfo وجود دارد (از EtelaatOmomiVaTakhasosi یا EtelaatOmomi)
if (!drugDetail.generalInfo.isNullOrBlank()) {
InfoSectionCard(
title = "اطلاعات عمومی",
content = drugDetail.generalInfo
)
}
// اگر specializedInfo وجود دارد (از EtelaatTakhasosiContent)
else if (!drugDetail.specializedInfo.isNullOrBlank()) {
InfoSectionCard(
title = "اطلاعات تخصصی",
content = drugDetail.specializedInfo
)
}
// در غیر این صورت، اطلاعات پراکنده را نمایش بده
else {
// Categories Card
if (drugDetail.drugClass != null || drugDetail.therapeuticClass != null) {
Card(
@@ -233,11 +328,11 @@ fun GeneralInfoTabContent(drugDetail: DrugDetail) {
Text(
text = "طبقه بندی مارتیندل: ",
fontWeight = FontWeight.Bold,
fontSize = 14.sp
fontSize = 13.sp
)
Text(
text = it,
fontSize = 14.sp,
fontSize = 13.sp,
color = MaterialTheme.colorScheme.primary
)
}
@@ -247,11 +342,11 @@ fun GeneralInfoTabContent(drugDetail: DrugDetail) {
Text(
text = "طبقه درمانی: ",
fontWeight = FontWeight.Bold,
fontSize = 14.sp
fontSize = 13.sp
)
Text(
text = it,
fontSize = 14.sp,
fontSize = 13.sp,
color = MaterialTheme.colorScheme.primary
)
}
@@ -276,20 +371,22 @@ fun GeneralInfoTabContent(drugDetail: DrugDetail) {
Text(
text = "مصرف در بارداری",
fontWeight = FontWeight.Bold,
fontSize = 16.sp,
fontSize = 15.sp,
color = Color(0xFFE65100)
)
drugDetail.pregnancyCategory?.let {
Text(
text = "گروه: $it",
fontSize = 14.sp,
fontSize = 13.sp,
fontWeight = FontWeight.Medium
)
}
drugDetail.pregnancyDescription?.let {
Spacer(modifier = Modifier.height(dime.xs))
Text(
text = it,
fontSize = 13.sp,
fontSize = 12.sp,
lineHeight = 18.sp,
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.8f)
)
}
@@ -297,31 +394,109 @@ fun GeneralInfoTabContent(drugDetail: DrugDetail) {
}
}
// 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)
}
// توصیه‌های دارویی
drugDetail.recommendations?.let {
InfoSectionCard(title = "توصیه‌های دارویی", content = it)
}
}
// اگر هیچ اطلاعاتی وجود نداشت
if (drugDetail.drugClass == null &&
drugDetail.therapeuticClass == null &&
drugDetail.pregnancyCategory == null &&
drugDetail.usage == null &&
drugDetail.contraindications == null &&
drugDetail.sideEffects == null &&
drugDetail.warnings == null &&
drugDetail.generalInfo.isNullOrBlank() &&
drugDetail.specializedInfo.isNullOrBlank()
) {
Box(
modifier = Modifier
.fillMaxWidth()
.padding(32.dp),
contentAlignment = Alignment.Center
) {
Text(
text = "اطلاعات عمومی برای این دارو ثبت نشده است",
fontSize = 13.sp,
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f),
textAlign = TextAlign.Center
)
}
}
} else {
// ========== صفحات برند (Brand) ==========
// نمایش اطلاعات تخصصی برند که در بخش brandAttrPersDesc است
drugDetail.usage?.let {
InfoSectionCard(title = "موارد مصرف", content = it)
}
drugDetail.contraindications?.let {
InfoSectionCard(title = "موارد منع مصرف", content = it)
}
drugDetail.sideEffects?.let {
InfoSectionCard(title = "عوارض جانبی", content = it)
}
drugDetail.warnings?.let {
InfoSectionCard(title = "هشدارها", content = it)
}
drugDetail.recommendations?.let {
InfoSectionCard(title = "توصیه‌های دارویی", content = it)
}
if (drugDetail.usage == null &&
drugDetail.contraindications == null &&
drugDetail.sideEffects == null &&
drugDetail.warnings == null
) {
Box(
modifier = Modifier
.fillMaxWidth()
.padding(32.dp),
contentAlignment = Alignment.Center
) {
Text(
text = "اطلاعات عمومی برای این برند ثبت نشده است.\nبرای مشاهده اطلاعات کامل، به صفحه داروی ژنریک مراجعه کنید.",
fontSize = 13.sp,
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f),
textAlign = TextAlign.Center
)
}
}
}
}
}
@Composable
fun SpecializedInfoTabContent(drugDetail: DrugDetail) {
Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
val dime = LocalDime.current
Column(verticalArrangement = Arrangement.spacedBy(dime.md)) {
// برای صفحات برند، اطلاعات تخصصی معمولاً در قسمت brandAttrPersDesc است
// که در پارسر ما به صورت usage, contraindications, sideEffects, warnings, recommendations ذخیره شده
drugDetail.usage?.let {
InfoSectionCard(title = "موارد مصرف", content = it)
}
@@ -347,10 +522,12 @@ fun SpecializedInfoTabContent(drugDetail: DrugDetail) {
InfoSectionCard(title = "توصیه‌های دارویی", content = it)
}
// اگر هیچ اطلاعات تخصصی وجود نداشت
if (drugDetail.usage == null &&
drugDetail.mechanism == null &&
drugDetail.contraindications == null &&
drugDetail.sideEffects == null
drugDetail.sideEffects == null &&
drugDetail.warnings == null
) {
Box(
modifier = Modifier
@@ -359,8 +536,11 @@ fun SpecializedInfoTabContent(drugDetail: DrugDetail) {
contentAlignment = Alignment.Center
) {
Text(
text = "اطلاعات تخصصی برای این دارو ثبت نشده است",
fontSize = 14.sp,
text = if (drugDetail.isGeneric)
"اطلاعات تخصصی برای این دارو ثبت نشده است"
else
"اطلاعات تخصصی برای این برند ثبت نشده است.\nبرای مشاهده اطلاعات کامل، به صفحه داروی ژنریک مراجعه کنید.",
fontSize = 13.sp,
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f),
textAlign = TextAlign.Center
)
@@ -370,8 +550,9 @@ fun SpecializedInfoTabContent(drugDetail: DrugDetail) {
}
@Composable
fun DosageFormsTabContent(dosageForms: List<com.approagency.drug.domain.model.DosageForm>) {
fun DosageFormsTabContent(drugDetail: DrugDetail) {
val dime = LocalDime.current
val dosageForms = drugDetail.dosageForms
if (dosageForms.isEmpty()) {
Box(
@@ -381,18 +562,32 @@ fun DosageFormsTabContent(dosageForms: List<com.approagency.drug.domain.model.Do
contentAlignment = Alignment.Center
) {
Text(
text = "اشکال دارویی برای این دارو ثبت نشده است",
fontSize = 14.sp,
text = if (drugDetail.isGeneric)
"اشکال دارویی برای این دارو ثبت نشده است"
else
"سایر اشکال دارویی این برند ثبت نشده است",
fontSize = 13.sp,
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f),
textAlign = TextAlign.Center
)
}
} else {
Column(verticalArrangement = Arrangement.spacedBy(dime.sm)) {
// اگر صفحه برند است، عنوان متفاوت نشان بده
if (!drugDetail.isGeneric) {
Text(
text = "سایر محصولات این برند",
fontWeight = FontWeight.Bold,
fontSize = 14.sp,
color = MaterialTheme.colorScheme.primary,
modifier = Modifier.padding(bottom = dime.xs)
)
}
dosageForms.forEach { form ->
Card(
modifier = Modifier.fillMaxWidth(),
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp),
elevation = CardDefaults.cardElevation(defaultElevation = 1.dp),
shape = RoundedCornerShape(dime.sm)
) {
Column(
@@ -401,55 +596,49 @@ fun DosageFormsTabContent(dosageForms: List<com.approagency.drug.domain.model.Do
) {
Text(
text = form.persianName,
fontWeight = FontWeight.Bold,
fontSize = 15.sp
fontWeight = FontWeight.Medium,
fontSize = 14.sp
)
if (form.englishName.isNotBlank()) {
Text(
text = form.englishName,
fontSize = 12.sp,
fontSize = 11.sp,
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f)
)
}
// نشانگرهای خطر
if (form.isHighRisk || form.isVital) {
Row(
horizontalArrangement = Arrangement.spacedBy(dime.sm)
horizontalArrangement = Arrangement.spacedBy(dime.xs)
) {
if (form.isHighRisk) {
androidx.compose.material3.Surface(
shape = RoundedCornerShape(4.dp),
color = Color(0xFFFFEBEE),
modifier = Modifier.padding(vertical = 4.dp)
color = Color(0xFFFFEBEE)
) {
Text(
text = "پرخطر",
fontSize = 10.sp,
color = Color(0xFFC62828),
modifier = Modifier.padding(horizontal = 8.dp, vertical = 2.dp)
modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp)
)
}
}
if (form.isVital) {
androidx.compose.material3.Surface(
shape = RoundedCornerShape(4.dp),
color = Color(0xFFE8F5E9),
modifier = Modifier.padding(vertical = 4.dp)
color = Color(0xFFE8F5E9)
) {
Text(
text = "حیاتی",
fontSize = 10.sp,
color = Color(0xFF2E7D32),
modifier = Modifier.padding(horizontal = 8.dp, vertical = 2.dp)
modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp)
)
}
}
}
if (!form.warningLabel.isNullOrBlank()) {
Text(
text = "⚠️ ${form.warningLabel}",
fontSize = 11.sp,
color = Color(0xFFE65100),
modifier = Modifier.padding(top = dime.xs)
)
}
}
}
@@ -459,10 +648,10 @@ fun DosageFormsTabContent(dosageForms: List<com.approagency.drug.domain.model.Do
}
@Composable
fun BrandNamesTabContent(brandNames: List<com.approagency.drug.domain.model.BrandName>) {
fun BrandNamesTabContent(drugDetail: DrugDetail) {
val dime = LocalDime.current
if (brandNames.isEmpty()) {
if (drugDetail.isGeneric && drugDetail.brandNames.isEmpty()) {
Box(
modifier = Modifier
.fillMaxWidth()
@@ -471,14 +660,14 @@ fun BrandNamesTabContent(brandNames: List<com.approagency.drug.domain.model.Bran
) {
Text(
text = "اسامی تجاری برای این دارو ثبت نشده است",
fontSize = 14.sp,
fontSize = 13.sp,
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f),
textAlign = TextAlign.Center
)
}
} else {
} else if (drugDetail.isGeneric && drugDetail.brandNames.isNotEmpty()) {
Column(verticalArrangement = Arrangement.spacedBy(dime.sm)) {
brandNames.forEach { brand ->
drugDetail.brandNames.forEach { brand ->
Card(
modifier = Modifier.fillMaxWidth(),
elevation = CardDefaults.cardElevation(defaultElevation = 1.dp),
@@ -525,6 +714,21 @@ fun BrandNamesTabContent(brandNames: List<com.approagency.drug.domain.model.Bran
}
}
}
} else {
// صفحه برند است - اسامی تجاری برای برند معنی ندارد
Box(
modifier = Modifier
.fillMaxWidth()
.padding(32.dp),
contentAlignment = Alignment.Center
) {
Text(
text = "این صفحه مربوط به یک محصول تجاری است. برای مشاهده اسامی تجاری سایر برندها، به صفحه داروی ژنریک مراجعه کنید.",
fontSize = 13.sp,
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f),
textAlign = TextAlign.Center
)
}
}
}
@@ -548,14 +752,14 @@ fun InfoSectionCard(
Text(
text = title,
fontWeight = FontWeight.Bold,
fontSize = 16.sp,
fontSize = 15.sp,
color = MaterialTheme.colorScheme.primary
)
Divider(color = MaterialTheme.colorScheme.outline.copy(alpha = 0.2f))
Text(
text = content,
fontSize = 14.sp,
lineHeight = 22.sp,
fontSize = 13.sp,
lineHeight = 20.sp,
textAlign = TextAlign.Justify,
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.85f)
)