diff --git a/app/src/main/java/com/approagency/drug/data/remote/DrugDetailParser.kt b/app/src/main/java/com/approagency/drug/data/remote/DrugDetailParser.kt index c862449..6499b69 100644 --- a/app/src/main/java/com/approagency/drug/data/remote/DrugDetailParser.kt +++ b/app/src/main/java/com/approagency/drug/data/remote/DrugDetailParser.kt @@ -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 { + private fun extractSectionsFromGeneric(document: Document): Map { val sections = mutableMapOf() + 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> { + val container = document.select(".brandAttrPersDesc").first() ?: return Pair(null, emptyMap()) + val sections = mutableMapOf() + 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() + } + + return Pair(introText, sections) } - private fun cleanHtmlText(text: String): String { - return text - .replace(Regex("\\s+"), " ") - .trim() + private fun extractOtherBrandForms(document: Document): List { + val forms = mutableListOf() + 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 { val forms = mutableListOf() - - // بررسی وجود جدول اشکال دارویی - 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 { val brands = mutableListOf() - - // استخراج از بخش اسامی تجاری فارسی 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 { val drugs = mutableListOf() - - // بررسی وجود جدول داروهای هم گروه - 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 + } } \ No newline at end of file diff --git a/app/src/main/java/com/approagency/drug/domain/model/DrugDetail.kt b/app/src/main/java/com/approagency/drug/domain/model/DrugDetail.kt index bdb5415..ae296f6 100644 --- a/app/src/main/java/com/approagency/drug/domain/model/DrugDetail.kt +++ b/app/src/main/java/com/approagency/drug/domain/model/DrugDetail.kt @@ -20,9 +20,19 @@ data class DrugDetail( val brandNames: List, val similarDrugs: List, val categories: DrugCategories?, - val comments: List + val comments: List, + val manufacturer: String? = null, + val genericInfo: GenericInfo? = null, + val otherBrandForms: List = 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( diff --git a/app/src/main/java/com/approagency/drug/presentation/screens/DrugDetailScreen.kt b/app/src/main/java/com/approagency/drug/presentation/screens/DrugDetailScreen.kt index f657d5d..68b173b 100644 --- a/app/src/main/java/com/approagency/drug/presentation/screens/DrugDetailScreen.kt +++ b/app/src/main/java/com/approagency/drug/presentation/screens/DrugDetailScreen.kt @@ -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) } } } } @@ -211,117 +217,286 @@ fun DrugDetailScreen( } @Composable -fun GeneralInfoTabContent(drugDetail: DrugDetail) { +fun ManufacturerInfoCard( + manufacturer: String, + genericInfo: com.approagency.drug.domain.model.GenericInfo?, + modifier: Modifier = Modifier +) { 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) + 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) ) { - 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 - ) - } - } - } + 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 + ) } - } - // 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) + if (genericInfo != null) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(dime.xs) ) { - Text( - text = "مصرف در بارداری", - fontWeight = FontWeight.Bold, - fontSize = 16.sp, - color = Color(0xFFE65100) + 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 ) - 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 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) { +fun DosageFormsTabContent(drugDetail: DrugDetail) { val dime = LocalDime.current + val dosageForms = drugDetail.dosageForms if (dosageForms.isEmpty()) { Box( @@ -381,18 +562,32 @@ fun DosageFormsTabContent(dosageForms: List 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) { +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 + drugDetail.brandNames.forEach { brand -> Card( modifier = Modifier.fillMaxWidth(), elevation = CardDefaults.cardElevation(defaultElevation = 1.dp), @@ -525,6 +714,21 @@ fun BrandNamesTabContent(brandNames: List