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.Jsoup
import org.jsoup.nodes.Document import org.jsoup.nodes.Document
import org.jsoup.nodes.Element import org.jsoup.nodes.Element
import org.jsoup.select.Evaluator
class DrugDetailParser { class DrugDetailParser {
fun parseDrugDetail(html: String): DrugDetail { fun parseDrugDetail(html: String): DrugDetail {
val document = Jsoup.parse(html) 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 genericId = extractGenericId(document)
val persianName = extractPersianName(document) val persianName = extractPersianName(document)
val englishName = extractEnglishName(document) val englishName = extractEnglishName(document)
val sections = extractSectionsFromGeneric(document)
// استخراج تمام بخش‌ها به صورت داینامیک
val sections = extractAllSections(document)
return DrugDetail( return DrugDetail(
genericId = genericId, genericId = genericId,
@@ -24,27 +56,30 @@ class DrugDetailParser {
englishName = englishName, englishName = englishName,
drugClass = extractDrugClass(document), drugClass = extractDrugClass(document),
therapeuticClass = extractTherapeuticClass(document), therapeuticClass = extractTherapeuticClass(document),
usage = sections["usage"] ?: extractSectionByText(document, "موارد مصرف"), usage = sections["usage"],
mechanism = sections["mechanism"] ?: extractSectionByText(document, "مکانیسم اثر"), mechanism = sections["mechanism"],
pharmacokinetics = sections["pharmacokinetics"] ?: extractSectionByText(document, "فارماکوکینتیک"), pharmacokinetics = sections["pharmacokinetics"],
contraindications = sections["contraindications"] ?: extractSectionByText(document, "منع مصرف"), contraindications = sections["contraindications"],
sideEffects = sections["sideEffects"] ?: extractSectionByText(document, "عوارض جانبی"), sideEffects = sections["sideEffects"],
interactions = sections["interactions"] ?: extractSectionByText(document, "تداخلات دارویی"), interactions = sections["interactions"],
warnings = sections["warnings"] ?: extractSectionByText(document, "هشدار"), warnings = sections["warnings"],
recommendations = sections["recommendations"] ?: extractSectionByText(document, "توصیه"), recommendations = sections["recommendations"],
pregnancyCategory = extractPregnancyCategory(document), pregnancyCategory = extractPregnancyCategory(document),
pregnancyDescription = extractPregnancyDescription(document), pregnancyDescription = extractPregnancyDescription(document),
dosageForms = extractDosageForms(document), dosageForms = extractDosageForms(document),
brandNames = extractBrandNames(document), brandNames = extractBrandNames(document),
similarDrugs = extractSimilarDrugs(document), similarDrugs = extractSimilarDrugs(document),
categories = extractCategories(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 { private fun extractGenericId(document: Document): String {
val urlElement = document.select("link[rel=canonical]").first() val url = document.select("link[rel=canonical]").attr("href")
val url = urlElement?.attr("href") ?: ""
val regex = "/G-(\\d+)/".toRegex() val regex = "/G-(\\d+)/".toRegex()
return regex.find(url)?.groupValues?.get(1) ?: "" return regex.find(url)?.groupValues?.get(1) ?: ""
} }
@@ -52,124 +87,283 @@ class DrugDetailParser {
private fun extractPersianName(document: Document): String { private fun extractPersianName(document: Document): String {
val titleElement = document.select("h1.EnglishNumericFont").first() val titleElement = document.select("h1.EnglishNumericFont").first()
val fullText = titleElement?.text() ?: "" val fullText = titleElement?.text() ?: ""
return fullText.replace("چیست و برای چه مواردی استفاده می شود؟", "").trim() // Remove the suffix "چیست و برای چه مواردی استفاده می شود؟"
return fullText.replace(Regex("\\s*چیست و برای چه مواردی استفاده می شود\\?\\s*"), "").trim()
} }
private fun extractEnglishName(document: Document): String { private fun extractEnglishName(document: Document): String {
val englishLabel = document.select("label.EnglishTopLabel").first() return document.select("label.EnglishTopLabel").text().trim()
return englishLabel?.text()?.trim() ?: ""
} }
private fun extractDrugClass(document: Document): String? { private fun extractDrugClass(document: Document): String? {
val classElement = document.select("#divExtraInfo > div:first-child a.ahref_Generic").first() return document.select("#divExtraInfo > div:first-child a.ahref_Generic").first()?.text()?.trim()
return classElement?.text()?.trim()
} }
private fun extractTherapeuticClass(document: Document): String? { private fun extractTherapeuticClass(document: Document): String? {
val therapeuticElement = document.select("#divExtraInfo > div:last-child a.ahref_Generic").first() // The therapeutic class can be a chain of links
return therapeuticElement?.text()?.trim() 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 sections = mutableMapOf<String, String>()
val specializedContentDiv = document.getElementById("EtelaatTakhasosiContent") ?: return sections
// نقشه ID به کلید // Mapping of section title keywords to our data class keys
val idToKey = mapOf( val titleToKey = mapOf(
"0" to "usage", "موارد مصرف" to "usage",
"1" to "mechanism", "مکانیسم اثر" to "mechanism",
"2" to "pharmacokinetics", "فارماکوکینتیک" to "pharmacokinetics",
"3" to "contraindications", "منع مصرف" to "contraindications",
"4" to "sideEffects", "عوارض جانبی" to "sideEffects",
"5" to "interactions", "تداخلات دارویی" to "interactions",
"6" to "warnings", "هشدار" to "warnings",
"7" to "recommendations" "توصیه های دارویی" to "recommendations"
) )
for ((id, key) in idToKey) { val sectionHeaders = specializedContentDiv.select("h2.h2_TabTitle")
val section = extractSectionById(document, id)
if (!section.isNullOrBlank()) { for (header in sectionHeaders) {
sections[key] = section 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 return sections
} }
private fun extractSectionById(document: Document, sectionId: String): String? { /**
val sectionElement = document.select("h2.h2_TabTitle#${sectionId}").first() * General info for generic pages is inside #EtelaatOmomiVaTakhasosi > #EtelaatOmomi
if (sectionElement == null) return null */
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() // Remove the accordion (table of contents) as it's not part of the main content
var nextElement = sectionElement.nextElementSibling() clone.select(".accordion").remove()
// Remove navigation boxes if any
clone.select("#nav_box_general").remove()
while (nextElement != null && !nextElement.select("h2.h2_TabTitle").hasText()) { return cleanText(clone).trim().takeIf { it.isNotEmpty() }
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() }
} }
/** /**
* استخراج بخش با جستجوی متن عنوان (روش جایگزین) * Specialized info for generic pages is inside #EtelaatTakhasosiContent
*/ */
private fun extractSectionByText(document: Document, titleKeyword: String): String? { private fun extractSpecializedInfoGeneric(document: Document): String? {
// جستجوی هدر حاوی کلمه کلیدی val specializedDiv = document.getElementById("EtelaatTakhasosiContent") ?: return null
val header = document.select("h2.h2_TabTitle, h3").firstOrNull { val clone = specializedDiv.clone()
it.text().contains(titleKeyword, ignoreCase = true)
} ?: return null
val content = StringBuilder() // Remove the table of contents
var nextElement = header.nextElementSibling() 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()) { return cleanText(clone).trim().takeIf { it.isNotEmpty() }
if (nextElement.tagName() == "p" || nextElement.tagName() == "div") { }
val text = cleanHtmlText(nextElement.text())
// ============================================================
// 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()) { 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()
}
return Pair(introText, sections)
} }
private fun cleanHtmlText(text: String): String { private fun extractOtherBrandForms(document: Document): List<DosageForm> {
return text val forms = mutableListOf<DosageForm>()
.replace(Regex("\\s+"), " ") val brandmateDiv = document.select(".brandmate").first() ?: return forms
.trim()
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? { private fun extractPregnancyCategory(document: Document): String? {
// On generic pages only
val categoryElement = document.select("#UseInPregnancy .EnglishNumericFont, #UseInPregnancy > div.EnglishNumericFont").first() val categoryElement = document.select("#UseInPregnancy .EnglishNumericFont, #UseInPregnancy > div.EnglishNumericFont").first()
return categoryElement?.text()?.trim() return categoryElement?.text()?.trim()
} }
private fun extractPregnancyDescription(document: Document): String? { private fun extractPregnancyDescription(document: Document): String? {
// On generic pages only
val descElement = document.select("#UseInPregnancy p, #UseInPregnancy .alert").first() val descElement = document.select("#UseInPregnancy p, #UseInPregnancy .alert").first()
val text = descElement?.text()?.trim() val text = descElement?.text()?.trim()
// اگر متن "مصرف در بارداری ثبت نشده است" باشد، null برگردان
return if (text.isNullOrBlank() || text.contains("ثبت نشده")) null else text return if (text.isNullOrBlank() || text.contains("ثبت نشده")) null else text
} }
private fun extractDosageForms(document: Document): List<DosageForm> { private fun extractDosageForms(document: Document): List<DosageForm> {
val forms = mutableListOf<DosageForm>() val forms = mutableListOf<DosageForm>()
val table = document.select("#TBL_AshkalDarooyi").first() ?: return forms
// بررسی وجود جدول اشکال دارویی
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" } val rows = table.select("tbody tr").filter { !it.hasClass("showMoreRow") && it.id() != "showMoreRow" }
@@ -178,17 +372,13 @@ class DrugDetailParser {
val cells = row.select("td") val cells = row.select("td")
if (cells.size >= 2) { if (cells.size >= 2) {
val persianNameElement = cells[1].select("h3").first() val persianNameElement = cells[1].select("h3").first()
val englishNameElement = cells[1].select("label.EnglishNumericFont").first() val persianName = persianNameElement?.text()?.trim() ?: continue
// بررسی اینکه آیا داده معتبر است
val persianName = persianNameElement?.text()?.trim()
if (persianName.isNullOrBlank()) continue
forms.add( forms.add(
DosageForm( DosageForm(
code = cells[0].text().trim(), code = cells[0].text().trim(),
persianName = persianName, persianName = persianName,
englishName = englishNameElement?.text()?.trim() ?: "", englishName = cells[1].select("label.EnglishNumericFont").first()?.text()?.trim() ?: "",
isHighRisk = cells.getOrNull(2)?.hasText() == true, isHighRisk = cells.getOrNull(2)?.hasText() == true,
temperature = cells.getOrNull(3)?.text()?.takeIf { it.isNotBlank() }, temperature = cells.getOrNull(3)?.text()?.takeIf { it.isNotBlank() },
isVital = cells.getOrNull(4)?.hasText() == true, isVital = cells.getOrNull(4)?.hasText() == true,
@@ -200,34 +390,22 @@ class DrugDetailParser {
println("Error parsing dosage form: ${e.message}") println("Error parsing dosage form: ${e.message}")
} }
} }
return forms return forms
} }
private fun extractBrandNames(document: Document): List<BrandName> { private fun extractBrandNames(document: Document): List<BrandName> {
val brands = mutableListOf<BrandName>() val brands = mutableListOf<BrandName>()
// استخراج از بخش اسامی تجاری فارسی
val persianRows = document.select("#PersCommertialDrugs .tableCommertial tbody tr.tr_persian") val persianRows = document.select("#PersCommertialDrugs .tableCommertial tbody tr.tr_persian")
// اگر ردیفی وجود نداشت، بررسی کن که آیا پیام "ثبت نشده" وجود دارد if (persianRows.isEmpty()) return brands
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) { for (row in persianRows) {
try { try {
val linkElement = row.select("td:first-child a.ahref_Generic").first() val linkElement = row.select("td:first-child a.ahref_Generic").first()
val persianName = linkElement?.text()?.trim() ?: continue val persianName = linkElement?.text()?.trim() ?: continue
val detailUrl = "https://www.darooyab.ir${linkElement.attr("href")}" val detailUrl = "https://www.darooyab.ir${linkElement.attr("href")}"
val manufacturerElement = row.select("td:eq(1) a.ahref_Generic").first() val manufacturerElement = row.select("td:eq(1) a.ahref_Generic").first()
val manufacturer = manufacturerElement?.text()?.trim() val manufacturer = manufacturerElement?.text()?.trim()
val importerElement = row.select("td:eq(2) a.ahref_Generic").first() val importerElement = row.select("td:eq(2) a.ahref_Generic").first()
val importer = importerElement?.text()?.trim() val importer = importerElement?.text()?.trim()
@@ -244,19 +422,12 @@ class DrugDetailParser {
println("Error parsing brand name: ${e.message}") println("Error parsing brand name: ${e.message}")
} }
} }
return brands return brands
} }
private fun extractSimilarDrugs(document: Document): List<SimilarDrug> { private fun extractSimilarDrugs(document: Document): List<SimilarDrug> {
val drugs = mutableListOf<SimilarDrug>() val drugs = mutableListOf<SimilarDrug>()
val table = document.select("table.tableGroups").first() ?: return drugs
// بررسی وجود جدول داروهای هم گروه
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 { val rows = table.select("tbody tr").filter {
!it.hasClass("hidden-row") && it.select("a#toggleButton").isEmpty() !it.hasClass("hidden-row") && it.select("a#toggleButton").isEmpty()
@@ -264,8 +435,7 @@ class DrugDetailParser {
for (row in rows) { for (row in rows) {
try { try {
val cells = row.select("td") for (cell in row.select("td")) {
for (cell in cells) {
val link = cell.select("a.ahref_Generic").first() val link = cell.select("a.ahref_Generic").first()
if (link != null && link.text().isNotBlank()) { if (link != null && link.text().isNotBlank()) {
val href = link.attr("href") val href = link.attr("href")
@@ -286,7 +456,6 @@ class DrugDetailParser {
println("Error parsing similar drug: ${e.message}") println("Error parsing similar drug: ${e.message}")
} }
} }
return drugs return drugs
} }
@@ -295,7 +464,6 @@ class DrugDetailParser {
val martindale = martindaleLink?.text()?.trim() val martindale = martindaleLink?.text()?.trim()
val martindaleUrl = martindaleLink?.attr("href")?.let { "https://www.darooyab.ir$it" } val martindaleUrl = martindaleLink?.attr("href")?.let { "https://www.darooyab.ir$it" }
// بررسی طبقه بندی درمانی (ممکن است "بدون طبقه بندی درمانی" باشد)
val therapeuticLinks = document.select("#divExtraInfo > div:last-child a.ahref_Generic") val therapeuticLinks = document.select("#divExtraInfo > div:last-child a.ahref_Generic")
val therapeutic = therapeuticLinks.mapNotNull { it.text().trim().takeIf { text -> val therapeutic = therapeuticLinks.mapNotNull { it.text().trim().takeIf { text ->
text != "بدون طبقه بندی درمانی" && text.isNotBlank() text != "بدون طبقه بندی درمانی" && text.isNotBlank()
@@ -320,15 +488,12 @@ class DrugDetailParser {
try { try {
val authorElement = element.select("span").first() val authorElement = element.select("span").first()
val author = authorElement?.text()?.replace("(", "")?.replace(")", "")?.trim() ?: "ناشناس" val author = authorElement?.text()?.replace("(", "")?.replace(")", "")?.trim() ?: "ناشناس"
val date = authorElement?.text()?.let { val date = authorElement?.text()?.let {
val regex = "\\((\\d{4}/\\d{1,2}/\\d{1,2})\\)".toRegex() val regex = "\\((\\d{4}/\\d{1,2}/\\d{1,2})\\)".toRegex()
regex.find(it)?.groupValues?.get(1) ?: "" regex.find(it)?.groupValues?.get(1) ?: ""
} ?: "" } ?: ""
val textElement = element.select("p.commentText").first() val textElement = element.select("p.commentText").first()
val text = textElement?.text()?.trim() ?: "" val text = textElement?.text()?.trim() ?: ""
if (text.isBlank()) continue if (text.isBlank()) continue
val responseElement = element.select(".responseComment").first() val responseElement = element.select(".responseComment").first()
@@ -346,7 +511,6 @@ class DrugDetailParser {
println("Error parsing comment: ${e.message}") println("Error parsing comment: ${e.message}")
} }
} }
return comments return comments
} }
@@ -354,10 +518,8 @@ class DrugDetailParser {
val doctorLink = element.select("a").first() val doctorLink = element.select("a").first()
val doctorName = doctorLink?.text()?.trim() ?: "" val doctorName = doctorLink?.text()?.trim() ?: ""
val doctorUrl = doctorLink?.attr("href")?.let { "https://www.darooyab.ir$it" } val doctorUrl = doctorLink?.attr("href")?.let { "https://www.darooyab.ir$it" }
val doctorText = element.select("span").first()?.text()?.trim() ?: "" val doctorText = element.select("span").first()?.text()?.trim() ?: ""
val doctorTitle = doctorText.substringAfter(" - ").takeIf { it.isNotBlank() } ?: "" val doctorTitle = doctorText.substringAfter(" - ").takeIf { it.isNotBlank() } ?: ""
val responseText = element.select("p.commentText").last()?.text()?.trim() ?: "" val responseText = element.select("p.commentText").last()?.text()?.trim() ?: ""
return CommentResponse( return CommentResponse(
@@ -367,4 +529,29 @@ class DrugDetailParser {
doctorUrl = doctorUrl 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 brandNames: List<BrandName>,
val similarDrugs: List<SimilarDrug>, val similarDrugs: List<SimilarDrug>,
val categories: DrugCategories?, 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( data class DosageForm(
val code: String, val code: String,
val persianName: String, val persianName: String,
@@ -30,7 +40,8 @@ data class DosageForm(
val isHighRisk: Boolean, val isHighRisk: Boolean,
val temperature: String?, val temperature: String?,
val isVital: Boolean, val isVital: Boolean,
val warningLabel: String? val warningLabel: String?,
val detailUrl: String? = null
) )
data class BrandName( data class BrandName(
@@ -12,12 +12,10 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.Favorite import androidx.compose.material.icons.filled.Info
import androidx.compose.material.icons.filled.FavoriteBorder
import androidx.compose.material3.Card import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults import androidx.compose.material3.CardDefaults
import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.CircularProgressIndicator
@@ -36,7 +34,6 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
@@ -66,6 +63,7 @@ fun DrugDetailScreen(
val dime = LocalDime.current val dime = LocalDime.current
val detailState by viewModel.detailState.collectAsStateWithLifecycle() val detailState by viewModel.detailState.collectAsStateWithLifecycle()
var selectedTab by remember { mutableIntStateOf(0) } var selectedTab by remember { mutableIntStateOf(0) }
val tabs = listOf("عمومی", "تخصصی", "اشکال دارویی", "اسامی تجاری") val tabs = listOf("عمومی", "تخصصی", "اشکال دارویی", "اسامی تجاری")
LaunchedEffect(detailUrl) { LaunchedEffect(detailUrl) {
@@ -80,16 +78,16 @@ fun DrugDetailScreen(
is DrugDetailYabState.Success -> { is DrugDetailYabState.Success -> {
Column { Column {
Text( Text(
text = state.drugDetail.persianName.take(20), text = state.drugDetail.persianName.take(25),
fontSize = 16.sp, fontSize = 16.sp,
fontWeight = FontWeight.Medium, fontWeight = FontWeight.Medium,
maxLines = 1 maxLines = 1
) )
if (state.drugDetail.englishName.isNotEmpty()) { if (state.drugDetail.englishName.isNotEmpty()) {
Text( Text(
text = state.drugDetail.englishName.take(25), text = state.drugDetail.englishName.take(30),
fontSize = 12.sp, fontSize = 11.sp,
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f), color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f),
maxLines = 1 maxLines = 1
) )
} }
@@ -120,9 +118,7 @@ fun DrugDetailScreen(
.padding(paddingValues) .padding(paddingValues)
) { ) {
when (val state = detailState) { when (val state = detailState) {
DrugDetailYabState.Idle -> { DrugDetailYabState.Idle -> {}
// Initial state
}
DrugDetailYabState.Loading -> { DrugDetailYabState.Loading -> {
Box( Box(
@@ -144,9 +140,18 @@ fun DrugDetailScreen(
} }
is DrugDetailYabState.Success -> { is DrugDetailYabState.Success -> {
Column( val drugDetail = state.drugDetail
modifier = Modifier.fillMaxSize()
) { 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 // Tab Row
TabRow( TabRow(
selectedTabIndex = selectedTab, selectedTabIndex = selectedTab,
@@ -160,8 +165,9 @@ fun DrugDetailScreen(
text = { text = {
Text( Text(
text = title, text = title,
fontSize = 14.sp, fontSize = 13.sp,
fontWeight = if (selectedTab == index) FontWeight.Bold else FontWeight.Normal fontWeight = if (selectedTab == index) FontWeight.Bold else FontWeight.Normal,
maxLines = 1
) )
} }
) )
@@ -178,16 +184,16 @@ fun DrugDetailScreen(
) { ) {
when (selectedTab) { when (selectedTab) {
0 -> { 0 -> {
item { GeneralInfoTabContent(state.drugDetail) } item { GeneralInfoTabContent(drugDetail) }
} }
1 -> { 1 -> {
item { SpecializedInfoTabContent(state.drugDetail) } item { SpecializedInfoTabContent(drugDetail) }
} }
2 -> { 2 -> {
item { DosageFormsTabContent(state.drugDetail.dosageForms) } item { DosageFormsTabContent(drugDetail) }
} }
3 -> { 3 -> {
item { BrandNamesTabContent(state.drugDetail.brandNames) } item { BrandNamesTabContent(drugDetail) }
} }
} }
} }
@@ -211,117 +217,286 @@ fun DrugDetailScreen(
} }
@Composable @Composable
fun GeneralInfoTabContent(drugDetail: DrugDetail) { fun ManufacturerInfoCard(
manufacturer: String,
genericInfo: com.approagency.drug.domain.model.GenericInfo?,
modifier: Modifier = Modifier
) {
val dime = LocalDime.current val dime = LocalDime.current
Column(verticalArrangement = Arrangement.spacedBy(dime.md)) { Card(
// Categories Card modifier = modifier.fillMaxWidth(),
if (drugDetail.drugClass != null || drugDetail.therapeuticClass != null) { colors = CardDefaults.cardColors(
Card( containerColor = MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.15f)
modifier = Modifier.fillMaxWidth(), ),
colors = CardDefaults.cardColors( shape = RoundedCornerShape(dime.sm)
containerColor = MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.2f) ) {
), Column(
shape = RoundedCornerShape(dime.sm) modifier = Modifier.padding(dime.md),
verticalArrangement = Arrangement.spacedBy(dime.xs)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(dime.xs)
) { ) {
Column( Icon(
modifier = Modifier.padding(dime.md), imageVector = Icons.Default.Info,
verticalArrangement = Arrangement.spacedBy(dime.xs) contentDescription = null,
) { modifier = Modifier.size(18.dp),
drugDetail.drugClass?.let { tint = MaterialTheme.colorScheme.primary
Row { )
Text( Text(
text = "طبقه بندی مارتیندل: ", text = "سازنده:",
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
fontSize = 14.sp fontSize = 13.sp
) )
Text( Text(
text = it, text = manufacturer,
fontSize = 14.sp, fontSize = 13.sp,
color = MaterialTheme.colorScheme.primary 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 (genericInfo != null) {
if (drugDetail.pregnancyCategory != null || drugDetail.pregnancyDescription != null) { Row(
Card( verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(dime.xs)
colors = CardDefaults.cardColors(
containerColor = Color(0xFFFFF3E0)
),
shape = RoundedCornerShape(dime.sm)
) {
Column(
modifier = Modifier.padding(dime.md),
verticalArrangement = Arrangement.spacedBy(dime.xs)
) { ) {
Text( Icon(
text = "مصرف در بارداری", imageVector = Icons.Default.Info,
fontWeight = FontWeight.Bold, contentDescription = null,
fontSize = 16.sp, modifier = Modifier.size(18.dp),
color = Color(0xFFE65100) tint = MaterialTheme.colorScheme.primary
)
Text(
text = "ماده موثره:",
fontWeight = FontWeight.Bold,
fontSize = 13.sp
)
Text(
text = genericInfo.persianName,
fontSize = 13.sp,
color = MaterialTheme.colorScheme.primary
) )
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)
}
} }
} }
// آپدیت 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(
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 = 13.sp
)
Text(
text = it,
fontSize = 13.sp,
color = MaterialTheme.colorScheme.primary
)
}
}
drugDetail.therapeuticClass?.let {
Row {
Text(
text = "طبقه درمانی: ",
fontWeight = FontWeight.Bold,
fontSize = 13.sp
)
Text(
text = it,
fontSize = 13.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 = 15.sp,
color = Color(0xFFE65100)
)
drugDetail.pregnancyCategory?.let {
Text(
text = "گروه: $it",
fontSize = 13.sp,
fontWeight = FontWeight.Medium
)
}
drugDetail.pregnancyDescription?.let {
Spacer(modifier = Modifier.height(dime.xs))
Text(
text = it,
fontSize = 12.sp,
lineHeight = 18.sp,
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.8f)
)
}
}
}
}
// موارد مصرف
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.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 @Composable
fun SpecializedInfoTabContent(drugDetail: DrugDetail) { 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 { drugDetail.usage?.let {
InfoSectionCard(title = "موارد مصرف", content = it) InfoSectionCard(title = "موارد مصرف", content = it)
} }
@@ -347,10 +522,12 @@ fun SpecializedInfoTabContent(drugDetail: DrugDetail) {
InfoSectionCard(title = "توصیه‌های دارویی", content = it) InfoSectionCard(title = "توصیه‌های دارویی", content = it)
} }
// اگر هیچ اطلاعات تخصصی وجود نداشت
if (drugDetail.usage == null && if (drugDetail.usage == null &&
drugDetail.mechanism == null && drugDetail.mechanism == null &&
drugDetail.contraindications == null && drugDetail.contraindications == null &&
drugDetail.sideEffects == null drugDetail.sideEffects == null &&
drugDetail.warnings == null
) { ) {
Box( Box(
modifier = Modifier modifier = Modifier
@@ -359,8 +536,11 @@ fun SpecializedInfoTabContent(drugDetail: DrugDetail) {
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
Text( Text(
text = "اطلاعات تخصصی برای این دارو ثبت نشده است", text = if (drugDetail.isGeneric)
fontSize = 14.sp, "اطلاعات تخصصی برای این دارو ثبت نشده است"
else
"اطلاعات تخصصی برای این برند ثبت نشده است.\nبرای مشاهده اطلاعات کامل، به صفحه داروی ژنریک مراجعه کنید.",
fontSize = 13.sp,
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f), color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f),
textAlign = TextAlign.Center textAlign = TextAlign.Center
) )
@@ -370,8 +550,9 @@ fun SpecializedInfoTabContent(drugDetail: DrugDetail) {
} }
@Composable @Composable
fun DosageFormsTabContent(dosageForms: List<com.approagency.drug.domain.model.DosageForm>) { fun DosageFormsTabContent(drugDetail: DrugDetail) {
val dime = LocalDime.current val dime = LocalDime.current
val dosageForms = drugDetail.dosageForms
if (dosageForms.isEmpty()) { if (dosageForms.isEmpty()) {
Box( Box(
@@ -381,18 +562,32 @@ fun DosageFormsTabContent(dosageForms: List<com.approagency.drug.domain.model.Do
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
Text( Text(
text = "اشکال دارویی برای این دارو ثبت نشده است", text = if (drugDetail.isGeneric)
fontSize = 14.sp, "اشکال دارویی برای این دارو ثبت نشده است"
else
"سایر اشکال دارویی این برند ثبت نشده است",
fontSize = 13.sp,
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f), color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f),
textAlign = TextAlign.Center textAlign = TextAlign.Center
) )
} }
} else { } else {
Column(verticalArrangement = Arrangement.spacedBy(dime.sm)) { 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 -> dosageForms.forEach { form ->
Card( Card(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp), elevation = CardDefaults.cardElevation(defaultElevation = 1.dp),
shape = RoundedCornerShape(dime.sm) shape = RoundedCornerShape(dime.sm)
) { ) {
Column( Column(
@@ -401,55 +596,49 @@ fun DosageFormsTabContent(dosageForms: List<com.approagency.drug.domain.model.Do
) { ) {
Text( Text(
text = form.persianName, text = form.persianName,
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Medium,
fontSize = 15.sp fontSize = 14.sp
) )
if (form.englishName.isNotBlank()) { if (form.englishName.isNotBlank()) {
Text( Text(
text = form.englishName, text = form.englishName,
fontSize = 12.sp, fontSize = 11.sp,
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f) color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f)
) )
} }
Row(
horizontalArrangement = Arrangement.spacedBy(dime.sm) // نشانگرهای خطر
) { if (form.isHighRisk || form.isVital) {
if (form.isHighRisk) { Row(
androidx.compose.material3.Surface( horizontalArrangement = Arrangement.spacedBy(dime.xs)
shape = RoundedCornerShape(4.dp), ) {
color = Color(0xFFFFEBEE), if (form.isHighRisk) {
modifier = Modifier.padding(vertical = 4.dp) androidx.compose.material3.Surface(
) { shape = RoundedCornerShape(4.dp),
Text( color = Color(0xFFFFEBEE)
text = "پرخطر", ) {
fontSize = 10.sp, Text(
color = Color(0xFFC62828), text = "پرخطر",
modifier = Modifier.padding(horizontal = 8.dp, vertical = 2.dp) fontSize = 10.sp,
) color = Color(0xFFC62828),
modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp)
)
}
}
if (form.isVital) {
androidx.compose.material3.Surface(
shape = RoundedCornerShape(4.dp),
color = Color(0xFFE8F5E9)
) {
Text(
text = "حیاتی",
fontSize = 10.sp,
color = Color(0xFF2E7D32),
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)
) {
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)
)
} }
} }
} }
@@ -459,10 +648,10 @@ fun DosageFormsTabContent(dosageForms: List<com.approagency.drug.domain.model.Do
} }
@Composable @Composable
fun BrandNamesTabContent(brandNames: List<com.approagency.drug.domain.model.BrandName>) { fun BrandNamesTabContent(drugDetail: DrugDetail) {
val dime = LocalDime.current val dime = LocalDime.current
if (brandNames.isEmpty()) { if (drugDetail.isGeneric && drugDetail.brandNames.isEmpty()) {
Box( Box(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
@@ -471,14 +660,14 @@ fun BrandNamesTabContent(brandNames: List<com.approagency.drug.domain.model.Bran
) { ) {
Text( Text(
text = "اسامی تجاری برای این دارو ثبت نشده است", text = "اسامی تجاری برای این دارو ثبت نشده است",
fontSize = 14.sp, fontSize = 13.sp,
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f), color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f),
textAlign = TextAlign.Center textAlign = TextAlign.Center
) )
} }
} else { } else if (drugDetail.isGeneric && drugDetail.brandNames.isNotEmpty()) {
Column(verticalArrangement = Arrangement.spacedBy(dime.sm)) { Column(verticalArrangement = Arrangement.spacedBy(dime.sm)) {
brandNames.forEach { brand -> drugDetail.brandNames.forEach { brand ->
Card( Card(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
elevation = CardDefaults.cardElevation(defaultElevation = 1.dp), 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(
text = title, text = title,
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
fontSize = 16.sp, fontSize = 15.sp,
color = MaterialTheme.colorScheme.primary color = MaterialTheme.colorScheme.primary
) )
Divider(color = MaterialTheme.colorScheme.outline.copy(alpha = 0.2f)) Divider(color = MaterialTheme.colorScheme.outline.copy(alpha = 0.2f))
Text( Text(
text = content, text = content,
fontSize = 14.sp, fontSize = 13.sp,
lineHeight = 22.sp, lineHeight = 20.sp,
textAlign = TextAlign.Justify, textAlign = TextAlign.Justify,
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.85f) color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.85f)
) )