feat: change package name
This commit is contained in:
@@ -0,0 +1,25 @@
|
||||
package com.approagency.pharmacy
|
||||
|
||||
import android.app.Application
|
||||
import com.approagency.pharmacy.data.local.database.LabDatabase
|
||||
import com.approagency.pharmacy.di.appModule
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koin.android.ext.koin.androidContext
|
||||
import org.koin.core.context.startKoin
|
||||
|
||||
class DrugApp : Application(){
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
startKoin {
|
||||
androidContext(this@DrugApp)
|
||||
modules(appModule)
|
||||
printLogger()
|
||||
}
|
||||
|
||||
// Preload database (optional, for faster access)
|
||||
GlobalScope.launch {
|
||||
LabDatabase.getInstance(this@DrugApp)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package com.approagency.pharmacy
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import com.approagency.pharmacy.navigation.AppNavGraph
|
||||
import com.approagency.pharmacy.ui.theme.DrugTheme
|
||||
|
||||
class MainActivity : ComponentActivity() {
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
enableEdgeToEdge()
|
||||
setContent {
|
||||
DrugTheme {
|
||||
|
||||
AppNavGraph(
|
||||
// navController = navController,
|
||||
// modifier = Modifier.padding(innerPadding)
|
||||
)
|
||||
// Scaffold(modifier = Modifier.fillMaxSize(),
|
||||
// ) { innerPadding ->
|
||||
// AppNavGraph(
|
||||
//// navController = navController,
|
||||
//// modifier = Modifier.padding(innerPadding)
|
||||
// )
|
||||
// }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.approagency.pharmacy.data.dto
|
||||
|
||||
data class DarmanModel(
|
||||
val success : String,
|
||||
val message:String,
|
||||
val data: List<DarmanList>
|
||||
)
|
||||
|
||||
data class DarmanList(
|
||||
val cod : Int,
|
||||
val nam_fa:String,
|
||||
val nam_en:String,
|
||||
)
|
||||
@@ -0,0 +1,74 @@
|
||||
package com.approagency.pharmacy.data.dto
|
||||
|
||||
data class DrugModels(
|
||||
val success: Boolean,
|
||||
val message: String?,
|
||||
val data: DrugDetail?,
|
||||
val meta: Meta?
|
||||
)
|
||||
|
||||
data class DrugDetail(
|
||||
val cod: Int?,
|
||||
val goroh_darmani_detail_cod: Int?,
|
||||
val goroh_daroei_cod: Int?,
|
||||
val goroh_farmakologic_cod: Int?,
|
||||
val goroh_darmani_cod: Int?,
|
||||
val nam_fa: String?,
|
||||
val nam_en: String?,
|
||||
val mavaredmasraf: String?,
|
||||
val meghdarmasraf: String?,
|
||||
val masrafdarhamelegi: String?,
|
||||
val masrafdarshirdehi: String?,
|
||||
val manemasraf: String?,
|
||||
val avarez: String?,
|
||||
val tadakhol: String?,
|
||||
val mekanismtasir: String?,
|
||||
val nokte: String?,
|
||||
val hoshdar: String?,
|
||||
val sharayetnegahdari: String?,
|
||||
val ashkal_daroei: String?,
|
||||
val created_at: String?,
|
||||
val updated_at: String?,
|
||||
val goroh_daroei: GorohDaroei?,
|
||||
val goroh_darmani: GorohDarmani?,
|
||||
val goroh_darmani_detail: GorohDarmaniDetail?
|
||||
)
|
||||
|
||||
data class GorohDaroei(
|
||||
val cod: Int?,
|
||||
val nam: String?,
|
||||
val created_at: String?,
|
||||
val updated_at: String?
|
||||
)
|
||||
|
||||
data class GorohDarmani(
|
||||
val cod: Int?,
|
||||
val nam_fa: String?,
|
||||
val nam_en: String?,
|
||||
val giyahi_ya_shimiyaei: String?,
|
||||
val image: String?,
|
||||
val created_at: String?,
|
||||
val updated_at: String?
|
||||
)
|
||||
|
||||
data class GorohDarmaniDetail(
|
||||
val cod: Int?,
|
||||
val nam: String?,
|
||||
val created_at: String?,
|
||||
val updated_at: String?
|
||||
)
|
||||
|
||||
data class Meta(
|
||||
val current_page: Int?,
|
||||
val last_page: Int?,
|
||||
val per_page: Int?,
|
||||
val total: Int?
|
||||
)
|
||||
|
||||
// For list response compatibility
|
||||
data class DrugListResponse(
|
||||
val success: Boolean,
|
||||
val message: String?,
|
||||
val data: List<DrugDetail> = emptyList(),
|
||||
val meta: Meta?
|
||||
)
|
||||
@@ -0,0 +1,46 @@
|
||||
package com.approagency.pharmacy.data.local.dao
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import com.approagency.pharmacy.data.local.entities.TestGroupEntity
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
@Dao
|
||||
interface TestGroupDao {
|
||||
@Query("SELECT * FROM testGroup ORDER BY Id")
|
||||
fun getAllGroups(): Flow<List<TestGroupEntity>>
|
||||
|
||||
@Query("SELECT * FROM testGroup WHERE Id = :groupId")
|
||||
suspend fun getGroupById(groupId: Int): TestGroupEntity?
|
||||
|
||||
@Query("SELECT * FROM testGroup WHERE Isparent = '1'")
|
||||
fun getParentGroups(): Flow<List<TestGroupEntity>>
|
||||
|
||||
@Query("SELECT * FROM testGroup WHERE Isparent = '0'")
|
||||
fun getChildGroups(): Flow<List<TestGroupEntity>>
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun insertGroup(group: TestGroupEntity)
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun insertAllGroups(groups: List<TestGroupEntity>)
|
||||
|
||||
@Query("DELETE FROM testGroup")
|
||||
suspend fun deleteAllGroups()
|
||||
|
||||
@Query("""
|
||||
SELECT * FROM testGroup
|
||||
WHERE Fname LIKE '%' || :query || '%'
|
||||
OR Ename LIKE '%' || :query || '%'
|
||||
OR Detail LIKE '%' || :query || '%'
|
||||
ORDER BY
|
||||
CASE
|
||||
WHEN Fname LIKE '%' || :query || '%' THEN 1
|
||||
WHEN Ename LIKE '%' || :query || '%' THEN 2
|
||||
ELSE 3
|
||||
END
|
||||
""")
|
||||
fun searchGroups(query: String): Flow<List<TestGroupEntity>>
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package com.approagency.pharmacy.data.local.dao
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import com.approagency.pharmacy.data.local.entities.TestItemEntity
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
@Dao
|
||||
interface TestItemDao {
|
||||
@Query("SELECT * FROM testItem ORDER BY Id")
|
||||
fun getAllItems(): Flow<List<TestItemEntity>>
|
||||
|
||||
@Query("SELECT * FROM testItem WHERE Group_Id = :groupId ORDER BY Id")
|
||||
fun getItemsByGroupId(groupId: Int): Flow<List<TestItemEntity>>
|
||||
|
||||
@Query("SELECT * FROM testItem WHERE Id = :itemId")
|
||||
suspend fun getItemById(itemId: Int): TestItemEntity?
|
||||
|
||||
@Query("""
|
||||
SELECT ti.* FROM testItem ti
|
||||
LEFT JOIN testGroup tg ON ti.Group_Id = tg.Id
|
||||
WHERE ti.Title LIKE '%' || :searchQuery || '%'
|
||||
OR ti.Normal_Value LIKE '%' || :searchQuery || '%'
|
||||
OR ti.Detail LIKE '%' || :searchQuery || '%'
|
||||
OR tg.Fname LIKE '%' || :searchQuery || '%'
|
||||
OR tg.Ename LIKE '%' || :searchQuery || '%'
|
||||
ORDER BY
|
||||
CASE
|
||||
WHEN ti.Title LIKE '%' || :searchQuery || '%' THEN 1
|
||||
WHEN tg.Fname LIKE '%' || :searchQuery || '%' THEN 2
|
||||
ELSE 3
|
||||
END
|
||||
""")
|
||||
fun searchTestItems(searchQuery: String): Flow<List<TestItemEntity>>
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun insertItem(item: TestItemEntity)
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun insertAllItems(items: List<TestItemEntity>)
|
||||
|
||||
@Query("DELETE FROM testItem")
|
||||
suspend fun deleteAllItems()
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package com.approagency.pharmacy.data.local.database
|
||||
|
||||
import android.content.Context
|
||||
import androidx.room.Database
|
||||
import androidx.room.Room
|
||||
import androidx.room.RoomDatabase
|
||||
import com.approagency.pharmacy.data.local.dao.TestGroupDao
|
||||
import com.approagency.pharmacy.data.local.dao.TestItemDao
|
||||
import com.approagency.pharmacy.data.local.entities.TestGroupEntity
|
||||
import com.approagency.pharmacy.data.local.entities.TestItemEntity
|
||||
|
||||
@Database(
|
||||
entities = [
|
||||
TestGroupEntity::class,
|
||||
TestItemEntity::class
|
||||
],
|
||||
version = 1,
|
||||
exportSchema = false
|
||||
)
|
||||
abstract class LabDatabase : RoomDatabase() {
|
||||
abstract fun testGroupDao(): TestGroupDao
|
||||
abstract fun testItemDao(): TestItemDao
|
||||
|
||||
companion object {
|
||||
@Volatile
|
||||
private var INSTANCE: LabDatabase? = null
|
||||
|
||||
fun getInstance(context: Context): LabDatabase {
|
||||
return INSTANCE ?: synchronized(this) {
|
||||
val instance = Room.databaseBuilder(
|
||||
context.applicationContext,
|
||||
LabDatabase::class.java,
|
||||
"lab_tests.db" // This will use your existing database
|
||||
)
|
||||
.createFromAsset("lab_tests.db") // IMPORTANT: Copy from assets
|
||||
.fallbackToDestructiveMigration() // For development only
|
||||
.build()
|
||||
INSTANCE = instance
|
||||
instance
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.approagency.pharmacy.data.local.entities
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
|
||||
@Entity(tableName = "testGroup")
|
||||
data class TestGroupEntity(
|
||||
@PrimaryKey
|
||||
@ColumnInfo(name = "Id")
|
||||
val id: Int,
|
||||
|
||||
@ColumnInfo(name = "Ename")
|
||||
val ename: String?,
|
||||
|
||||
@ColumnInfo(name = "Fname")
|
||||
val fname: String?,
|
||||
|
||||
@ColumnInfo(name = "Detail")
|
||||
val detail: String?,
|
||||
|
||||
@ColumnInfo(name = "Isparent")
|
||||
val isParent: String?
|
||||
)
|
||||
@@ -0,0 +1,36 @@
|
||||
package com.approagency.pharmacy.data.local.entities
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.Index
|
||||
import androidx.room.PrimaryKey
|
||||
|
||||
@Entity(
|
||||
tableName = "testItem",
|
||||
// foreignKeys = [
|
||||
// ForeignKey(
|
||||
// entity = TestGroupEntity::class,
|
||||
// parentColumns = ["Id"],
|
||||
// childColumns = ["Group_Id"],
|
||||
// onDelete = ForeignKey.CASCADE
|
||||
// )
|
||||
// ],
|
||||
indices = [Index(value = ["Group_Id"])]
|
||||
)
|
||||
data class TestItemEntity(
|
||||
@PrimaryKey
|
||||
@ColumnInfo(name = "Id")
|
||||
val id: Int,
|
||||
|
||||
@ColumnInfo(name = "Group_Id")
|
||||
val groupId: Int,
|
||||
|
||||
@ColumnInfo(name = "Title")
|
||||
val title: String?,
|
||||
|
||||
@ColumnInfo(name = "Normal_Value")
|
||||
val normalValue: String?,
|
||||
|
||||
@ColumnInfo(name = "Detail")
|
||||
val detail: String?
|
||||
)
|
||||
@@ -0,0 +1,60 @@
|
||||
package com.approagency.pharmacy.data.remote
|
||||
|
||||
import retrofit2.http.Field
|
||||
import retrofit2.http.FormUrlEncoded
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.Headers
|
||||
import retrofit2.http.POST
|
||||
import retrofit2.http.Path
|
||||
import retrofit2.http.Url
|
||||
|
||||
interface DarooyabApiService {
|
||||
@FormUrlEncoded
|
||||
@POST("Home/PartialNewSearch")
|
||||
@Headers(
|
||||
"User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36",
|
||||
"X-Requested-With: XMLHttpRequest",
|
||||
"Accept: */*",
|
||||
"Accept-Language: en-US,en;q=0.9,fa;q=0.8"
|
||||
)
|
||||
suspend fun searchDrugs(
|
||||
@Field("autocomplete") searchText: String,
|
||||
@Field("DrugName_pageNumber") pageNumber: Int = 1
|
||||
): String
|
||||
|
||||
|
||||
@GET("{detailUrl}")
|
||||
@Headers(
|
||||
"User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
|
||||
"Accept: text/html,application/xhtml+xml,application/xml;q=0.9",
|
||||
"Accept-Language: en-US,en;q=0.9,fa;q=0.8"
|
||||
)
|
||||
suspend fun getDrugDetail(
|
||||
@Path(value = "detailUrl", encoded = true) detailUrl: String
|
||||
): String
|
||||
|
||||
@GET
|
||||
@Headers(
|
||||
"User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
|
||||
"Accept: text/html,application/xhtml+xml,application/xml;q=0.9",
|
||||
"Accept-Language: en-US,en;q=0.9,fa;q=0.8"
|
||||
)
|
||||
// @GET("{pharmacyPath}")
|
||||
suspend fun getPharmacyDetail(
|
||||
@Url url: String
|
||||
): String
|
||||
@POST("Home/partialPatientReferralList")
|
||||
@FormUrlEncoded
|
||||
@Headers(
|
||||
"User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36",
|
||||
"X-Requested-With: XMLHttpRequest",
|
||||
"Accept: */*",
|
||||
"Accept-Language: en-US,en;q=0.9,fa;q=0.8"
|
||||
)
|
||||
suspend fun getPharmacies(
|
||||
@Field("brandIrc") brandIrc: String,
|
||||
@Field("genericDrugId") genericDrugId: String,
|
||||
@Field("provId") provId: String,
|
||||
@Field("cityId") cityId: String
|
||||
): String // returns HTML
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.approagency.pharmacy.data.remote
|
||||
|
||||
import com.approagency.pharmacy.data.dto.DarmanModel
|
||||
import com.approagency.pharmacy.data.dto.DrugListResponse
|
||||
import com.approagency.pharmacy.data.dto.DrugModels
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.Path
|
||||
import retrofit2.http.Query
|
||||
|
||||
interface DrugApiService {
|
||||
@GET("drugs/search")
|
||||
suspend fun getDrugs(
|
||||
@Query("q")query: String?,
|
||||
@Query("per_page")perPage:Int? = 20,
|
||||
@Query("with_relations")withRelations: Boolean? =true,
|
||||
@Query("goroh_daroei_cod")drugGroup:Int?,
|
||||
@Query("goroh_darmani_cod")healGroup:Int?,
|
||||
): DrugListResponse
|
||||
|
||||
|
||||
@GET("drugs/{cod}")
|
||||
suspend fun getDrugDetail(
|
||||
@Path("cod") cod: Int
|
||||
): DrugModels
|
||||
|
||||
|
||||
@GET("drugs/goroh-darmani")
|
||||
suspend fun getGorohDaroei(): DarmanModel
|
||||
|
||||
}
|
||||
@@ -0,0 +1,628 @@
|
||||
package com.approagency.pharmacy.data.remote
|
||||
|
||||
import com.approagency.pharmacy.domain.model.*
|
||||
import org.jsoup.Jsoup
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
|
||||
class DrugDetailParser {
|
||||
|
||||
fun parseDrugDetail(html: String): DrugDetail {
|
||||
val document = Jsoup.parse(html)
|
||||
|
||||
// تشخیص نوع صفحه: 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 = extractSectionsFromGeneric(document)
|
||||
|
||||
return DrugDetail(
|
||||
genericId = genericId,
|
||||
persianName = persianName,
|
||||
englishName = englishName,
|
||||
drugClass = extractDrugClass(document),
|
||||
therapeuticClass = extractTherapeuticClass(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),
|
||||
manufacturer = null,
|
||||
isGeneric = true,
|
||||
generalInfo = extractGeneralInfoGeneric(document),
|
||||
specializedInfo = extractSpecializedInfoGeneric(document),
|
||||
)
|
||||
}
|
||||
|
||||
private fun extractGenericId(document: Document): String {
|
||||
val url = document.select("link[rel=canonical]").attr("href")
|
||||
val regex = "/G-(\\d+)/".toRegex()
|
||||
return regex.find(url)?.groupValues?.get(1) ?: ""
|
||||
}
|
||||
|
||||
private fun extractPersianName(document: Document): String {
|
||||
val titleElement = document.select("h1.EnglishNumericFont").first()
|
||||
val fullText = titleElement?.text() ?: ""
|
||||
val regexFullText = fullText.replace(Regex("\\s*چیست و برای چه مواردی استفاده می شود؟\\s*"), "").trim()
|
||||
println("regexFullText: $regexFullText")
|
||||
return regexFullText
|
||||
}
|
||||
|
||||
private fun extractEnglishName(document: Document): String {
|
||||
return document.select("label.EnglishTopLabel").text().trim()
|
||||
}
|
||||
|
||||
private fun extractDrugClass(document: Document): String? {
|
||||
return document.select("#divExtraInfo > div:first-child a.ahref_Generic").first()?.text()?.trim()
|
||||
}
|
||||
|
||||
private fun extractTherapeuticClass(document: Document): String? {
|
||||
// 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
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 extractSectionsFromGeneric(document: Document): Map<String, String> {
|
||||
val sections = mutableMapOf<String, String>()
|
||||
val specializedContentDiv = document.getElementById("EtelaatTakhasosiContent") ?: return sections
|
||||
|
||||
// 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"
|
||||
)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
/**
|
||||
* 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()
|
||||
|
||||
// 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()
|
||||
|
||||
return cleanText(clone).trim().takeIf { it.isNotEmpty() }
|
||||
}
|
||||
|
||||
/**
|
||||
* Specialized info for generic pages is inside #EtelaatTakhasosiContent
|
||||
*/
|
||||
private fun extractSpecializedInfoGeneric(document: Document): String? {
|
||||
val specializedDiv = document.getElementById("EtelaatTakhasosiContent") ?: return null
|
||||
val clone = specializedDiv.clone()
|
||||
|
||||
// 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()
|
||||
|
||||
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()) {
|
||||
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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Save the last section if any
|
||||
if (currentSectionKey != null && contentBuilder.isNotEmpty()) {
|
||||
sections[currentSectionKey] = contentBuilder.toString().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()
|
||||
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() ?: return forms
|
||||
|
||||
val rows = table.select("tbody tr").filter { !it.hasClass("showMoreRow") && it.id() != "showMoreRow" }
|
||||
|
||||
for (row in rows) {
|
||||
try {
|
||||
val cells = row.select("td")
|
||||
if (cells.size >= 2) {
|
||||
val persianNameElement = cells[1].select("h3").first()
|
||||
val persianName = persianNameElement?.text()?.trim() ?: continue
|
||||
|
||||
forms.add(
|
||||
DosageForm(
|
||||
code = cells[0].text().trim(),
|
||||
persianName = persianName,
|
||||
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,
|
||||
warningLabel = cells.getOrNull(5)?.text()?.takeIf { it.isNotBlank() }
|
||||
)
|
||||
)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
println("Error parsing dosage form: ${e.message}")
|
||||
}
|
||||
}
|
||||
return forms
|
||||
}
|
||||
|
||||
private fun extractBrandNames(document: Document): List<BrandName> {
|
||||
val brands = mutableListOf<BrandName>()
|
||||
val persianRows = document.select("#PersCommertialDrugs .tableCommertial tbody tr.tr_persian")
|
||||
|
||||
if (persianRows.isEmpty()) return brands
|
||||
|
||||
for (row in persianRows) {
|
||||
try {
|
||||
val linkElement = row.select("td:first-child a.ahref_Generic").first()
|
||||
val persianName = linkElement?.text()?.trim() ?: continue
|
||||
val detailUrl = "https://www.darooyab.ir${linkElement.attr("href")}"
|
||||
val manufacturerElement = row.select("td:eq(1) a.ahref_Generic").first()
|
||||
val manufacturer = manufacturerElement?.text()?.trim()
|
||||
val importerElement = row.select("td:eq(2) a.ahref_Generic").first()
|
||||
val importer = importerElement?.text()?.trim()
|
||||
|
||||
brands.add(
|
||||
BrandName(
|
||||
persianName = persianName,
|
||||
englishName = "",
|
||||
manufacturer = manufacturer,
|
||||
importer = importer,
|
||||
detailUrl = detailUrl
|
||||
)
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
println("Error parsing brand name: ${e.message}")
|
||||
}
|
||||
}
|
||||
return brands
|
||||
}
|
||||
|
||||
private fun extractSimilarDrugs(document: Document): List<SimilarDrug> {
|
||||
val drugs = mutableListOf<SimilarDrug>()
|
||||
val table = document.select("table.tableGroups").first() ?: return drugs
|
||||
|
||||
val rows = table.select("tbody tr").filter {
|
||||
!it.hasClass("hidden-row") && it.select("a#toggleButton").isEmpty()
|
||||
}
|
||||
|
||||
for (row in rows) {
|
||||
try {
|
||||
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")
|
||||
val genericIdRegex = "/G-(\\d+)/".toRegex()
|
||||
val genericId = genericIdRegex.find(href)?.groupValues?.get(1) ?: ""
|
||||
|
||||
drugs.add(
|
||||
SimilarDrug(
|
||||
persianName = link.text().trim(),
|
||||
englishName = null,
|
||||
genericId = genericId,
|
||||
detailUrl = "https://www.darooyab.ir$href"
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
println("Error parsing similar drug: ${e.message}")
|
||||
}
|
||||
}
|
||||
return drugs
|
||||
}
|
||||
|
||||
private fun extractCategories(document: Document): DrugCategories? {
|
||||
val martindaleLink = document.select("#divExtraInfo > div:first-child a.ahref_Generic").first()
|
||||
val martindale = martindaleLink?.text()?.trim()
|
||||
val martindaleUrl = martindaleLink?.attr("href")?.let { "https://www.darooyab.ir$it" }
|
||||
|
||||
val therapeuticLinks = document.select("#divExtraInfo > div:last-child a.ahref_Generic")
|
||||
val therapeutic = therapeuticLinks.mapNotNull { it.text().trim().takeIf { text ->
|
||||
text != "بدون طبقه بندی درمانی" && text.isNotBlank()
|
||||
} }
|
||||
val therapeuticUrls = therapeuticLinks.map { "https://www.darooyab.ir${it.attr("href")}" }
|
||||
|
||||
return if (martindale != null || therapeutic.isNotEmpty()) {
|
||||
DrugCategories(
|
||||
martindale = martindale,
|
||||
martindaleUrl = martindaleUrl,
|
||||
therapeutic = therapeutic.ifEmpty { null },
|
||||
therapeuticUrls = therapeuticUrls.ifEmpty { null }
|
||||
)
|
||||
} else null
|
||||
}
|
||||
|
||||
private fun extractComments(document: Document): List<Comment> {
|
||||
val comments = mutableListOf<Comment>()
|
||||
val commentElements = document.select("#CommentContent .comment")
|
||||
|
||||
for (element in commentElements) {
|
||||
try {
|
||||
val authorElement = element.select("span").first()
|
||||
val author = authorElement?.text()?.replace("(", "")?.replace(")", "")?.trim() ?: "ناشناس"
|
||||
val date = authorElement?.text()?.let {
|
||||
val regex = "\\((\\d{4}/\\d{1,2}/\\d{1,2})\\)".toRegex()
|
||||
regex.find(it)?.groupValues?.get(1) ?: ""
|
||||
} ?: ""
|
||||
val textElement = element.select("p.commentText").first()
|
||||
val text = textElement?.text()?.trim() ?: ""
|
||||
if (text.isBlank()) continue
|
||||
|
||||
val responseElement = element.select(".responseComment").first()
|
||||
val response = responseElement?.let { parseCommentResponse(it) }
|
||||
|
||||
comments.add(
|
||||
Comment(
|
||||
author = author,
|
||||
date = date,
|
||||
text = text,
|
||||
response = response
|
||||
)
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
println("Error parsing comment: ${e.message}")
|
||||
}
|
||||
}
|
||||
return comments
|
||||
}
|
||||
|
||||
private fun parseCommentResponse(element: Element): CommentResponse {
|
||||
val doctorLink = element.select("a").first()
|
||||
val doctorName = doctorLink?.text()?.trim() ?: ""
|
||||
val doctorUrl = doctorLink?.attr("href")?.let { "https://www.darooyab.ir$it" }
|
||||
val doctorText = element.select("span").first()?.text()?.trim() ?: ""
|
||||
val doctorTitle = doctorText.substringAfter(" - ").takeIf { it.isNotBlank() } ?: ""
|
||||
val responseText = element.select("p.commentText").last()?.text()?.trim() ?: ""
|
||||
|
||||
return CommentResponse(
|
||||
doctorName = doctorName,
|
||||
doctorTitle = doctorTitle,
|
||||
text = responseText,
|
||||
doctorUrl = doctorUrl
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
val clone = element.clone()
|
||||
val output = StringBuilder()
|
||||
|
||||
// Process all nodes in order (both elements and text nodes)
|
||||
processNode(clone, output)
|
||||
|
||||
return output.toString()
|
||||
.replace(Regex("\\n{3,}"), "\n\n") // Collapse 3+ newlines to 2
|
||||
.trim()
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively process nodes to build formatted text
|
||||
*/
|
||||
private fun processNode(node: org.jsoup.nodes.Node, output: StringBuilder) {
|
||||
when (node) {
|
||||
is Element -> {
|
||||
when (node.tagName()) {
|
||||
"h2" -> {
|
||||
val text = node.text().trim()
|
||||
if (text.isNotEmpty()) {
|
||||
output.append("\n\n📌 $text\n\n")
|
||||
output.append("─".repeat(minOf(30, text.length))).append("\n\n")
|
||||
}
|
||||
}
|
||||
"h3" -> {
|
||||
val text = node.text().trim()
|
||||
if (text.isNotEmpty()) {
|
||||
output.append("\n\n🔹 $text\n\n")
|
||||
}
|
||||
}
|
||||
"h4" -> {
|
||||
val text = node.text().trim()
|
||||
if (text.isNotEmpty()) {
|
||||
output.append("\n▸ $text\n")
|
||||
}
|
||||
}
|
||||
"p" -> {
|
||||
val text = node.text().trim()
|
||||
if (text.isNotEmpty()) {
|
||||
output.append(text).append("\n\n")
|
||||
}
|
||||
}
|
||||
"ul", "ol" -> {
|
||||
for (li in node.select("li")) {
|
||||
val liText = li.text().trim()
|
||||
if (liText.isNotEmpty()) {
|
||||
output.append(" • $liText\n")
|
||||
}
|
||||
}
|
||||
output.append("\n")
|
||||
}
|
||||
"li" -> {
|
||||
// Handled by ul/ol processing, but if standalone:
|
||||
val text = node.text().trim()
|
||||
if (text.isNotEmpty()) {
|
||||
output.append(" • $text\n")
|
||||
}
|
||||
}
|
||||
"br" -> {
|
||||
output.append("\n")
|
||||
}
|
||||
"strong", "b" -> {
|
||||
val text = node.text().trim()
|
||||
if (text.isNotEmpty()) {
|
||||
output.append("**$text**")
|
||||
}
|
||||
}
|
||||
"div", "section", "article" -> {
|
||||
// Process children of container elements
|
||||
for (child in node.childNodes()) {
|
||||
processNode(child, output)
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
// For other elements, just process their children
|
||||
for (child in node.childNodes()) {
|
||||
processNode(child, output)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
is org.jsoup.nodes.TextNode -> {
|
||||
val text = node.text().trim()
|
||||
if (text.isNotEmpty()) {
|
||||
output.append(text)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
package com.approagency.pharmacy.data.remote
|
||||
|
||||
import com.approagency.pharmacy.domain.model.DaroYabSearchResult
|
||||
import com.approagency.pharmacy.domain.model.DrugSearchResult
|
||||
import org.jsoup.Jsoup
|
||||
import org.jsoup.nodes.Document
|
||||
|
||||
class DrugHtmlParser {
|
||||
|
||||
fun parseSearchResultsWithPagination(html: String): DaroYabSearchResult {
|
||||
val document: Document = Jsoup.parse(html)
|
||||
val drugs = parseDrugRows(document)
|
||||
val paginationInfo = extractPaginationInfo(document)
|
||||
|
||||
return DaroYabSearchResult(
|
||||
drugs = drugs,
|
||||
currentPage = paginationInfo.currentPage,
|
||||
totalPages = paginationInfo.totalPages,
|
||||
hasNextPage = paginationInfo.currentPage < paginationInfo.totalPages,
|
||||
hasPreviousPage = paginationInfo.currentPage > 1
|
||||
)
|
||||
}
|
||||
|
||||
private fun parseDrugRows(document: Document): List<DrugSearchResult> {
|
||||
val results = mutableListOf<DrugSearchResult>()
|
||||
val rows = document.select("#tbody_DrugList tr")
|
||||
|
||||
println("Found ${rows.size} rows in the response")
|
||||
|
||||
if (rows.isEmpty()) {
|
||||
println("No rows found. HTML snippet: ${document.html().take(500)}")
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
for (row in rows) {
|
||||
try {
|
||||
val nameCell = row.selectFirst("td:eq(0)")
|
||||
val drugLink = nameCell?.selectFirst("a.ahref_Generic")
|
||||
val persianName = drugLink?.text()?.trim() ?: continue
|
||||
val detailPageRelativeUrl = drugLink?.attr("href") ?: continue
|
||||
val detailPageUrl = "https://www.darooyab.ir$detailPageRelativeUrl"
|
||||
|
||||
val genericIdRegex = "/G-(\\d+)/?".toRegex()
|
||||
val genericId = genericIdRegex.find(detailPageRelativeUrl)?.groupValues?.get(1) ?: ""
|
||||
|
||||
val brandIdRegex = "/B-(\\d+)/?".toRegex()
|
||||
val brandId = brandIdRegex.find(detailPageRelativeUrl)?.groupValues?.get(1) ?: ""
|
||||
|
||||
val lastCell = row.select("td").last()
|
||||
val englishLink = lastCell?.selectFirst("a.ahref_Generic")
|
||||
val englishName = englishLink?.text()?.trim()
|
||||
|
||||
val drugResult = DrugSearchResult(
|
||||
genericId = if (genericId.isNotEmpty()) genericId else brandId,
|
||||
persianName = persianName,
|
||||
englishName = englishName,
|
||||
detailPageUrl = detailPageUrl
|
||||
)
|
||||
results.add(drugResult)
|
||||
println("Parsed: $persianName -> $englishName")
|
||||
|
||||
} catch (e: Exception) {
|
||||
println("Error parsing row: ${e.message}")
|
||||
}
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
private fun extractPaginationInfo(document: Document): PaginationInfo {
|
||||
var currentPage = 1
|
||||
var totalPages = 1
|
||||
|
||||
try {
|
||||
// Get current page from hidden input
|
||||
val currentPageInput = document.select("#CurrentPager_Number")
|
||||
if (currentPageInput.isNotEmpty()) {
|
||||
currentPage = currentPageInput.`val`()?.toIntOrNull() ?: 1
|
||||
}
|
||||
|
||||
// Get total pages from pagination links
|
||||
val pageLinks = document.select(".pagination li .PagerBtn_DrugName")
|
||||
if (pageLinks.isNotEmpty()) {
|
||||
val pageNumbers = pageLinks.mapNotNull { it.text().toIntOrNull() }
|
||||
if (pageNumbers.isNotEmpty()) {
|
||||
totalPages = pageNumbers.maxOrNull() ?: 1
|
||||
}
|
||||
}
|
||||
|
||||
println("Pagination: Current page=$currentPage, Total pages=$totalPages")
|
||||
|
||||
} catch (e: Exception) {
|
||||
println("Error extracting pagination: ${e.message}")
|
||||
}
|
||||
|
||||
return PaginationInfo(currentPage, totalPages)
|
||||
}
|
||||
|
||||
private data class PaginationInfo(
|
||||
val currentPage: Int,
|
||||
val totalPages: Int
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
// data/remote/PharmacyHtmlParser.kt
|
||||
package com.approagency.pharmacy.data.remote
|
||||
|
||||
import com.approagency.pharmacy.domain.model.PharmacyDetail
|
||||
import com.approagency.pharmacy.domain.model.PharmacyItem
|
||||
import org.jsoup.Jsoup
|
||||
|
||||
object PharmacyHtmlParser {
|
||||
|
||||
// private const val BASE_URL = "https://www.darooyab.ir"
|
||||
|
||||
fun parse(html: String): List<PharmacyItem> {
|
||||
val items = mutableListOf<PharmacyItem>()
|
||||
val doc = Jsoup.parse(html)
|
||||
val table = doc.select("table#TBL_patientReferral").first() ?: return emptyList()
|
||||
|
||||
// تشخیص ساختار بر اساس سربرگ جدول (یک بار برای کل جدول)
|
||||
val hasGuaranteeColumn = hasGuaranteeColumnInHeader(table)
|
||||
|
||||
val rows = table.select("tbody tr")
|
||||
|
||||
for (row in rows) {
|
||||
val cells = row.select("td")
|
||||
if (cells.size < 3) continue
|
||||
|
||||
// ایندکس ستونها بر اساس وجود ستون تضمین
|
||||
val brandIndex = if (hasGuaranteeColumn) 1 else 0
|
||||
val pharmacyIndex = if (hasGuaranteeColumn) 2 else 1
|
||||
val locationIndex = if (hasGuaranteeColumn) 3 else 2
|
||||
|
||||
// ستون برند
|
||||
val brandCell = cells[brandIndex]
|
||||
val brandLink = brandCell.select("a").first()
|
||||
val brandName = brandLink?.text()?.trim() ?: ""
|
||||
val brandUrl = normalizeUrl(brandLink?.attr("href")?.trim() ?: "")
|
||||
val brandId = extractIdFromUrl(brandUrl, "/B-(\\d+)/?")
|
||||
|
||||
// ستون داروخانه
|
||||
val pharmacyCell = cells[pharmacyIndex]
|
||||
val pharmacyLinks = pharmacyCell.select("a")
|
||||
|
||||
// پیدا کردن لینک داروخانه (لینکی که "اطلاعات تماس" نباشد)
|
||||
val pharmacyLink = pharmacyLinks.firstOrNull {
|
||||
!it.text().contains("اطلاعات تماس")
|
||||
}
|
||||
val pharmacyName = pharmacyLink?.text()?.trim() ?: ""
|
||||
var pharmacyUrl = pharmacyLink?.attr("href")?.trim() ?: ""
|
||||
|
||||
// نرمال کردن URL داروخانه
|
||||
pharmacyUrl = normalizeUrl(pharmacyUrl)
|
||||
val pharmacyId = extractIdFromUrl(pharmacyUrl, "/ph-(\\d+)/?")
|
||||
|
||||
// ستون موقعیت
|
||||
val locationCell = cells[locationIndex]
|
||||
val locationText = locationCell.text().trim()
|
||||
|
||||
// استخراج استان و شهر
|
||||
val province = extractProvince(locationText)
|
||||
val city = extractCity(locationText)
|
||||
|
||||
if (brandName.isNotBlank() && pharmacyName.isNotBlank()) {
|
||||
items.add(
|
||||
PharmacyItem(
|
||||
brandName = brandName,
|
||||
brandUrl = brandUrl,
|
||||
brandId = brandId,
|
||||
pharmacyName = pharmacyName,
|
||||
pharmacyUrl = pharmacyUrl,
|
||||
pharmacyId = pharmacyId,
|
||||
province = province,
|
||||
city = city
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return items
|
||||
}
|
||||
|
||||
/**
|
||||
* نرمال کردن URL: تبدیل مسیرهای نسبی به URL کامل
|
||||
* مثال: ~/../../../../../ph-12012/... -> https://www.darooyab.ir/ph-12012/...
|
||||
*/
|
||||
private fun normalizeUrl(url: String): String {
|
||||
if (url.isBlank()) return ""
|
||||
|
||||
// اگر absolute بود domain را حذف کن
|
||||
if (url.startsWith("http://") || url.startsWith("https://")) {
|
||||
return url
|
||||
.replace("https://www.darooyab.ir", "")
|
||||
.replace("http://www.darooyab.ir", "")
|
||||
}
|
||||
|
||||
var cleanUrl = url
|
||||
|
||||
if (cleanUrl.startsWith("~/")) {
|
||||
cleanUrl = cleanUrl.removePrefix("~/")
|
||||
}
|
||||
|
||||
while (cleanUrl.startsWith("../")) {
|
||||
cleanUrl = cleanUrl.removePrefix("../")
|
||||
}
|
||||
|
||||
return if (cleanUrl.startsWith("/")) {
|
||||
cleanUrl
|
||||
} else {
|
||||
"/$cleanUrl"
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* تشخیص وجود ستون تضمین موجودی با بررسی سربرگ جدول
|
||||
*/
|
||||
private fun hasGuaranteeColumnInHeader(table: org.jsoup.nodes.Element): Boolean {
|
||||
val headers = table.select("thead th")
|
||||
if (headers.isEmpty()) return false
|
||||
|
||||
// بررسی میکنیم که آیا اولین ستون حاوی متن "تضمین" است
|
||||
val firstHeaderText = headers.firstOrNull()?.text()?.trim() ?: ""
|
||||
return firstHeaderText.contains("تضمین") || firstHeaderText.contains("موجودی")
|
||||
}
|
||||
|
||||
private fun extractProvince(locationText: String): String {
|
||||
// الگو: "استان تهران" یا "استان اصفهان"
|
||||
val provincePattern = "استان\\s*(\\S+)".toRegex()
|
||||
val match = provincePattern.find(locationText)
|
||||
return match?.groupValues?.get(1)?.trim() ?: ""
|
||||
}
|
||||
|
||||
private fun extractCity(locationText: String): String {
|
||||
// الگو: "شهر تهران" یا "شهر اصفهان" یا "شهر پردیس"
|
||||
val cityPattern = "شهر\\s*(.+)".toRegex()
|
||||
val match = cityPattern.find(locationText)
|
||||
return match?.groupValues?.get(1)?.trim() ?: ""
|
||||
}
|
||||
|
||||
private fun extractIdFromUrl(url: String, pattern: String): String {
|
||||
val regex = pattern.toRegex()
|
||||
return regex.find(url)?.groupValues?.get(1) ?: ""
|
||||
}
|
||||
|
||||
fun parsePharmacyDetail(html: String): PharmacyDetail {
|
||||
val doc = Jsoup.parse(html)
|
||||
|
||||
// استخراج نام داروخانه
|
||||
val pharmacyTitle = doc.select("h2#pharmacyTitle").first()?.text()?.trim() ?: ""
|
||||
|
||||
// استخراج آدرس
|
||||
val addressElement = doc.select("h2#h2Address").first()
|
||||
val address = addressElement?.text()?.trim() ?: ""
|
||||
|
||||
// استخراج تلفن
|
||||
val phoneElement = doc.select("h3#h3Phone a").first()
|
||||
val phone = phoneElement?.text()?.trim() ?: ""
|
||||
|
||||
// استخراج دانشگاه (اختیاری)
|
||||
val university = doc.select("div.alert.alert-success > span").first()?.text()?.trim()
|
||||
|
||||
return PharmacyDetail(
|
||||
name = pharmacyTitle,
|
||||
address = address,
|
||||
phone = phone,
|
||||
university = university
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.approagency.pharmacy.data.remote
|
||||
|
||||
import okhttp3.ResponseBody
|
||||
import retrofit2.Converter
|
||||
import retrofit2.Retrofit
|
||||
import java.lang.reflect.Type
|
||||
|
||||
class StringConverterFactory : Converter.Factory() {
|
||||
override fun responseBodyConverter(
|
||||
type: Type,
|
||||
annotations: Array<out Annotation>,
|
||||
retrofit: Retrofit
|
||||
): Converter<ResponseBody, *>? {
|
||||
return if (type == String::class.java) {
|
||||
Converter<ResponseBody, String> { value -> value.string() }
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
package com.approagency.pharmacy.data.repository
|
||||
|
||||
import com.approagency.pharmacy.data.dto.DarmanModel
|
||||
import com.approagency.pharmacy.data.dto.DrugListResponse
|
||||
import com.approagency.pharmacy.data.dto.DrugModels
|
||||
import com.approagency.pharmacy.data.remote.DarooyabApiService
|
||||
import com.approagency.pharmacy.data.remote.DrugApiService
|
||||
import com.approagency.pharmacy.data.remote.DrugDetailParser
|
||||
import com.approagency.pharmacy.data.remote.DrugHtmlParser
|
||||
import com.approagency.pharmacy.data.remote.PharmacyHtmlParser
|
||||
import com.approagency.pharmacy.domain.model.DaroYabParams
|
||||
import com.approagency.pharmacy.domain.model.DaroYabSearchResult
|
||||
import com.approagency.pharmacy.domain.model.DrugDetail
|
||||
import com.approagency.pharmacy.domain.model.DrugSearchParams
|
||||
import com.approagency.pharmacy.domain.model.PharmacyDetail
|
||||
import com.approagency.pharmacy.domain.model.PharmacyItem
|
||||
import com.approagency.pharmacy.domain.repository.DrugRepository
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.IOException
|
||||
|
||||
class DrugRepositoryImpl(
|
||||
private val apiService: DrugApiService,
|
||||
private val darooyabApiService: DarooyabApiService,
|
||||
private val parser: DrugHtmlParser,
|
||||
private val detailParser: DrugDetailParser,
|
||||
): DrugRepository {
|
||||
override suspend fun searchDrug(params: DrugSearchParams): Result<DrugListResponse> {
|
||||
return try {
|
||||
val response = apiService.getDrugs(query = params.query , perPage = params.perPage , withRelations = params.withRelations , healGroup = params.healGroup , drugGroup = params.drugGroup )
|
||||
return Result.success(response)
|
||||
}catch (e: Exception){
|
||||
Result.failure(e)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun drugDetail(cod: Int): Result<DrugModels> {
|
||||
return try {
|
||||
val response= apiService.getDrugDetail(cod = cod)
|
||||
println(response.message)
|
||||
return Result.success(response)
|
||||
}catch (e: Exception){
|
||||
println(e.message)
|
||||
Result.failure(e)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getGorohDaroei(): Result<DarmanModel> {
|
||||
return try {
|
||||
val response = apiService.getGorohDaroei()
|
||||
println(response)
|
||||
return Result.success(response)
|
||||
}catch (e: Exception){
|
||||
Result.failure(e)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun searchDrugs(params: DaroYabParams): Result<DaroYabSearchResult> {
|
||||
return withContext(Dispatchers.IO) {
|
||||
try {
|
||||
val htmlResponse = darooyabApiService.searchDrugs(
|
||||
searchText = params.query ?: "",
|
||||
pageNumber = params.pageNumber
|
||||
)
|
||||
|
||||
println("HTML Response length: ${htmlResponse.length}")
|
||||
|
||||
val searchResult = parser.parseSearchResultsWithPagination(htmlResponse)
|
||||
|
||||
if (searchResult.drugs.isNotEmpty()) {
|
||||
Result.success(searchResult)
|
||||
} else {
|
||||
Result.failure(Exception("No drugs found for query: '${params.query}'"))
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
Result.failure(Exception("Network error: ${e.message}", e))
|
||||
} catch (e: Exception) {
|
||||
Result.failure(Exception("An error occurred: ${e.message}", e))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getDrugDetailFromYab(detailUrl: String): Result<DrugDetail> {
|
||||
return withContext(Dispatchers.IO) {
|
||||
try {
|
||||
val html = darooyabApiService.getDrugDetail(detailUrl)
|
||||
val detail = detailParser.parseDrugDetail(html)
|
||||
Result.success(detail)
|
||||
} catch (e: Exception) {
|
||||
Result.failure(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun searchPharmacies(
|
||||
genericDrugId: String,
|
||||
provId: String
|
||||
): List<PharmacyItem> {
|
||||
return withContext(Dispatchers.IO) {
|
||||
try {
|
||||
val html = darooyabApiService.getPharmacies(
|
||||
brandIrc = "0",
|
||||
genericDrugId = genericDrugId,
|
||||
provId = provId,
|
||||
cityId = "0"
|
||||
)
|
||||
PharmacyHtmlParser.parse(html)
|
||||
} catch (e: Exception) {
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getPharmacyDetail(pharmacyUrl: String): PharmacyDetail {
|
||||
return withContext(Dispatchers.IO) {
|
||||
try {
|
||||
val html = darooyabApiService.getPharmacyDetail(pharmacyUrl)
|
||||
PharmacyHtmlParser.parsePharmacyDetail(html)
|
||||
} catch (e: Exception) {
|
||||
PharmacyDetail("", "", "")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package com.approagency.pharmacy.data.repository
|
||||
|
||||
import com.approagency.pharmacy.data.local.dao.TestGroupDao
|
||||
import com.approagency.pharmacy.data.local.dao.TestItemDao
|
||||
import com.approagency.pharmacy.data.local.entities.TestGroupEntity
|
||||
import com.approagency.pharmacy.data.local.entities.TestItemEntity
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
|
||||
class LabRepositoryImpl (
|
||||
private val testGroupDao: TestGroupDao,
|
||||
private val testItemDao: TestItemDao
|
||||
) {
|
||||
// Test Group Operations
|
||||
fun getAllGroups(): Flow<List<TestGroupEntity>> = testGroupDao.getAllGroups()
|
||||
fun getParentGroups(): Flow<List<TestGroupEntity>> = testGroupDao.getParentGroups()
|
||||
fun getChildGroups(): Flow<List<TestGroupEntity>> = testGroupDao.getChildGroups()
|
||||
suspend fun getGroupById(groupId: Int): TestGroupEntity? = testGroupDao.getGroupById(groupId)
|
||||
|
||||
// Test Item Operations
|
||||
fun getAllItems(): Flow<List<TestItemEntity>> = testItemDao.getAllItems()
|
||||
fun getItemsByGroupId(groupId: Int): Flow<List<TestItemEntity>> = testItemDao.getItemsByGroupId(groupId)
|
||||
suspend fun getItemById(itemId: Int): TestItemEntity? = testItemDao.getItemById(itemId)
|
||||
fun searchTestItems(query: String): Flow<List<TestItemEntity>> = testItemDao.searchTestItems(query)
|
||||
|
||||
|
||||
fun searchGroupsAndItems(searchQuery: String): Flow<Pair<List<TestGroupEntity>, List<TestItemEntity>>> {
|
||||
return combine(
|
||||
testGroupDao.searchGroups(searchQuery),
|
||||
testItemDao.searchTestItems(searchQuery)
|
||||
) { groups, items ->
|
||||
Pair(groups, items)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
package com.approagency.pharmacy.di
|
||||
|
||||
|
||||
import com.approagency.pharmacy.data.local.database.LabDatabase
|
||||
import com.approagency.pharmacy.data.remote.DarooyabApiService
|
||||
import com.approagency.pharmacy.data.remote.DrugApiService
|
||||
import com.approagency.pharmacy.data.remote.DrugDetailParser
|
||||
import com.approagency.pharmacy.data.remote.DrugHtmlParser
|
||||
import com.approagency.pharmacy.data.repository.DrugRepositoryImpl
|
||||
import com.approagency.pharmacy.data.repository.LabRepositoryImpl
|
||||
import com.approagency.pharmacy.domain.repository.DrugRepository
|
||||
import com.approagency.pharmacy.domain.usecase.DrugDetailYabUseCase
|
||||
import com.approagency.pharmacy.domain.usecase.GetDarmanUseCase
|
||||
import com.approagency.pharmacy.domain.usecase.GetDrugDetailUseCase
|
||||
import com.approagency.pharmacy.domain.usecase.GetDrugSearchUseCase
|
||||
import com.approagency.pharmacy.domain.usecase.GetPharmaciesUseCase
|
||||
import com.approagency.pharmacy.domain.usecase.GetPharmacyDetailUseCase
|
||||
import com.approagency.pharmacy.domain.usecase.GetTestGroupUseCase
|
||||
import com.approagency.pharmacy.domain.usecase.GetTestItemByGroupId
|
||||
import com.approagency.pharmacy.domain.usecase.SearchDrugsYabUseCase
|
||||
import com.approagency.pharmacy.domain.usecase.SearchTestsUseCase
|
||||
import com.approagency.pharmacy.presentation.viewModel.DrugDetailViewModel
|
||||
import com.approagency.pharmacy.presentation.viewModel.HomeViewModel
|
||||
import com.approagency.pharmacy.presentation.viewModel.LabViewModel
|
||||
import com.approagency.pharmacy.presentation.viewModel.PharmacyViewModel
|
||||
import com.approagency.pharmacy.utils.Config
|
||||
import com.approgency.drug.presentation.viewModel.SearchViewModel
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.logging.HttpLoggingInterceptor
|
||||
import org.koin.android.ext.koin.androidContext
|
||||
import org.koin.core.module.dsl.viewModel
|
||||
import org.koin.core.qualifier.qualifier
|
||||
import org.koin.dsl.module
|
||||
import retrofit2.Retrofit
|
||||
import retrofit2.converter.gson.GsonConverterFactory
|
||||
import retrofit2.converter.scalars.ScalarsConverterFactory
|
||||
import java.net.CookieManager
|
||||
import java.net.CookiePolicy
|
||||
import java.util.concurrent.TimeUnit
|
||||
val jsonRetrofitQualifier = qualifier("jsonRetrofit")
|
||||
val scalarRetrofitQualifier = qualifier("scalarRetrofit")
|
||||
val appModule= module {
|
||||
|
||||
single { LabDatabase.getInstance(androidContext()) }
|
||||
|
||||
single { get<LabDatabase>().testGroupDao() }
|
||||
single { get<LabDatabase>().testItemDao() }
|
||||
|
||||
// Shared OkHttpClient for both APIs
|
||||
single {
|
||||
val cookieManager = CookieManager()
|
||||
cookieManager.setCookiePolicy(CookiePolicy.ACCEPT_ALL)
|
||||
|
||||
OkHttpClient.Builder()
|
||||
.addInterceptor(HttpLoggingInterceptor().apply {
|
||||
level = HttpLoggingInterceptor.Level.BODY // Change to BODY for debugging
|
||||
})
|
||||
// Add cookie support
|
||||
.connectTimeout(30, TimeUnit.SECONDS)
|
||||
.readTimeout(30, TimeUnit.SECONDS)
|
||||
.followRedirects(true)
|
||||
.build()
|
||||
}
|
||||
|
||||
single<Retrofit>(jsonRetrofitQualifier) { // Add qualifier
|
||||
Retrofit.Builder()
|
||||
.baseUrl(Config.BASE_URL)
|
||||
.client(get<OkHttpClient>())
|
||||
.addConverterFactory(GsonConverterFactory.create())
|
||||
.build()
|
||||
}
|
||||
|
||||
single<DrugApiService> {
|
||||
val retrofit: Retrofit = get(jsonRetrofitQualifier) // Get qualified instance
|
||||
retrofit.create(DrugApiService::class.java)
|
||||
}
|
||||
|
||||
// ========== Retrofit for DAROOYAB Website ==========
|
||||
single<Retrofit>(scalarRetrofitQualifier) { // Add qualifier
|
||||
Retrofit.Builder()
|
||||
.baseUrl(Config.Darro_Url)
|
||||
.client(get<OkHttpClient>())
|
||||
.addConverterFactory(ScalarsConverterFactory.create())
|
||||
.build()
|
||||
}
|
||||
|
||||
single<DarooyabApiService> {
|
||||
val retrofit: Retrofit = get(scalarRetrofitQualifier) // Get qualified instance
|
||||
retrofit.create(DarooyabApiService::class.java)
|
||||
}
|
||||
|
||||
|
||||
factory { DrugHtmlParser() }
|
||||
factory { DrugDetailParser() }
|
||||
//repo
|
||||
single<DrugRepository> {
|
||||
DrugRepositoryImpl(get<DrugApiService>() , get<DarooyabApiService>() , get() , get())
|
||||
}
|
||||
|
||||
|
||||
|
||||
//UseCase
|
||||
single {
|
||||
GetDrugSearchUseCase(get())
|
||||
|
||||
}
|
||||
|
||||
single {
|
||||
SearchDrugsYabUseCase(get())
|
||||
}
|
||||
|
||||
single{
|
||||
GetDrugDetailUseCase(get())
|
||||
}
|
||||
|
||||
single {
|
||||
GetDarmanUseCase(get())
|
||||
}
|
||||
|
||||
single {
|
||||
GetTestGroupUseCase(get())
|
||||
}
|
||||
|
||||
single {
|
||||
GetPharmaciesUseCase(get())
|
||||
}
|
||||
|
||||
single {
|
||||
GetTestItemByGroupId(get())
|
||||
}
|
||||
|
||||
single { LabRepositoryImpl(get(), get()) }
|
||||
|
||||
single {
|
||||
DrugDetailYabUseCase(get())
|
||||
}
|
||||
|
||||
single {
|
||||
GetPharmacyDetailUseCase(get())
|
||||
}
|
||||
|
||||
viewModel {
|
||||
DrugDetailViewModel(get())
|
||||
}
|
||||
|
||||
single { SearchTestsUseCase(get()) }
|
||||
//view model
|
||||
viewModel {
|
||||
HomeViewModel(get() , get() , get())
|
||||
}
|
||||
|
||||
viewModel {
|
||||
SearchViewModel(get())
|
||||
}
|
||||
viewModel {
|
||||
PharmacyViewModel(get() , get ())
|
||||
}
|
||||
|
||||
viewModel {
|
||||
LabViewModel(get() , get() , get())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package com.approagency.pharmacy.domain.model
|
||||
|
||||
data class DaroYabParams(
|
||||
val query: String? = null,
|
||||
val pageNumber: Int = 1,
|
||||
)
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.approagency.pharmacy.domain.model
|
||||
|
||||
data class DaroYabSearchResult(
|
||||
val drugs: List<DrugSearchResult>,
|
||||
val currentPage: Int,
|
||||
val totalPages: Int,
|
||||
val hasNextPage: Boolean,
|
||||
val hasPreviousPage: Boolean
|
||||
)
|
||||
@@ -0,0 +1,81 @@
|
||||
package com.approagency.pharmacy.domain.model
|
||||
|
||||
data class DrugDetail(
|
||||
val genericId: String,
|
||||
val persianName: String,
|
||||
val englishName: String,
|
||||
val drugClass: String?,
|
||||
val therapeuticClass: String?,
|
||||
val usage: String?,
|
||||
val mechanism: String?,
|
||||
val pharmacokinetics: String?,
|
||||
val contraindications: String?,
|
||||
val sideEffects: String?,
|
||||
val interactions: String?,
|
||||
val warnings: String?,
|
||||
val recommendations: String?,
|
||||
val pregnancyCategory: String?,
|
||||
val pregnancyDescription: String?,
|
||||
val dosageForms: List<DosageForm>,
|
||||
val brandNames: List<BrandName>,
|
||||
val similarDrugs: List<SimilarDrug>,
|
||||
val categories: DrugCategories?,
|
||||
val comments: List<Comment>,
|
||||
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,
|
||||
val englishName: String,
|
||||
val isHighRisk: Boolean,
|
||||
val temperature: String?,
|
||||
val isVital: Boolean,
|
||||
val warningLabel: String?,
|
||||
val detailUrl: String? = null
|
||||
)
|
||||
|
||||
data class BrandName(
|
||||
val persianName: String,
|
||||
val englishName: String,
|
||||
val manufacturer: String?,
|
||||
val importer: String?,
|
||||
val detailUrl: String
|
||||
)
|
||||
|
||||
data class SimilarDrug(
|
||||
val persianName: String,
|
||||
val englishName: String?,
|
||||
val genericId: String,
|
||||
val detailUrl: String
|
||||
)
|
||||
|
||||
data class DrugCategories(
|
||||
val martindale: String?,
|
||||
val martindaleUrl: String?,
|
||||
val therapeutic: List<String>?,
|
||||
val therapeuticUrls: List<String>?
|
||||
)
|
||||
|
||||
data class Comment(
|
||||
val author: String,
|
||||
val date: String,
|
||||
val text: String,
|
||||
val response: CommentResponse?
|
||||
)
|
||||
|
||||
data class CommentResponse(
|
||||
val doctorName: String,
|
||||
val doctorTitle: String,
|
||||
val text: String,
|
||||
val doctorUrl: String?
|
||||
)
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.approagency.pharmacy.domain.model
|
||||
|
||||
data class DrugSearchParams(
|
||||
val query: String? = null,
|
||||
val perPage: Int = 20,
|
||||
val withRelations: Boolean = true,
|
||||
val drugGroup: Int? = null,
|
||||
val healGroup: Int? = null,
|
||||
|
||||
)
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.approagency.pharmacy.domain.model
|
||||
|
||||
data class DrugSearchResult(
|
||||
val genericId: String, // e.g., "2556" from "/G-2556/Casanthranol"
|
||||
val persianName: String,
|
||||
val englishName: String? = null, // "Casanthranol", might be null for some entries
|
||||
val detailPageUrl: String // e.g., "/G-2556/Casanthranol" or "https://www.darooyab.ir/G-2556/Casanthranol"
|
||||
)
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.approagency.pharmacy.domain.model
|
||||
|
||||
data class PharmacySearchRequest(
|
||||
val brandIrc: String = "0", // برای صفحات برند
|
||||
val genericDrugId: String, // برای صفحات ژنریک
|
||||
val provId: String, // ID استان (0 برای همه)
|
||||
val cityId: String = "0" // ID شهر (0 برای همه)
|
||||
)
|
||||
|
||||
data class PharmacyItem(
|
||||
val brandName: String, // نام برند دارو
|
||||
val brandUrl: String, // لینک صفحه برند
|
||||
val brandId: String, // IRC برند
|
||||
|
||||
val pharmacyName: String, // نام داروخانه
|
||||
val pharmacyUrl: String, // لینک داروخانه
|
||||
val pharmacyId: String, // ID داروخانه
|
||||
|
||||
val province: String, // نام استان
|
||||
val city: String , // نام شهر
|
||||
val address: String? = null,
|
||||
val phone: String? = null
|
||||
)
|
||||
|
||||
// مدل برای اطلاعات کامل داروخانه
|
||||
data class PharmacyDetail(
|
||||
val name: String,
|
||||
val address: String,
|
||||
val phone: String,
|
||||
val university: String? = null
|
||||
)
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.approagency.pharmacy.domain.model
|
||||
|
||||
import com.approagency.pharmacy.data.local.entities.TestGroupEntity
|
||||
|
||||
data class TestGroup(
|
||||
val id: Int,
|
||||
val ename: String?,
|
||||
val fname: String?,
|
||||
val detail: String?,
|
||||
val isParent: String?,
|
||||
)
|
||||
|
||||
|
||||
fun TestGroupEntity.toTestGroup(): TestGroup {
|
||||
return TestGroup(
|
||||
id = this.id,
|
||||
ename = this.ename,
|
||||
fname = this.fname,
|
||||
detail = this.detail,
|
||||
isParent = this.isParent
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.approagency.pharmacy.domain.model
|
||||
|
||||
import com.approagency.pharmacy.data.local.entities.TestItemEntity
|
||||
|
||||
data class TestItem(
|
||||
val id: Int,
|
||||
val groupId: Int,
|
||||
val title: String?,
|
||||
val normalValue: String?,
|
||||
val detail: String?
|
||||
)
|
||||
fun TestItemEntity.toTestItem(): TestItem {
|
||||
return TestItem(
|
||||
id = this.id,
|
||||
groupId = this.groupId,
|
||||
title = this.title,
|
||||
normalValue = this.normalValue,
|
||||
detail = this.detail
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.approagency.pharmacy.domain.repository
|
||||
|
||||
import com.approagency.pharmacy.data.dto.DarmanModel
|
||||
import com.approagency.pharmacy.data.dto.DrugListResponse
|
||||
import com.approagency.pharmacy.data.dto.DrugModels
|
||||
import com.approagency.pharmacy.domain.model.DaroYabParams
|
||||
import com.approagency.pharmacy.domain.model.DaroYabSearchResult
|
||||
import com.approagency.pharmacy.domain.model.DrugDetail
|
||||
import com.approagency.pharmacy.domain.model.DrugSearchParams
|
||||
import com.approagency.pharmacy.domain.model.PharmacyDetail
|
||||
import com.approagency.pharmacy.domain.model.PharmacyItem
|
||||
|
||||
interface DrugRepository {
|
||||
suspend fun searchDrug(params: DrugSearchParams): Result<DrugListResponse>
|
||||
suspend fun drugDetail(cod:Int): Result<DrugModels>
|
||||
suspend fun getGorohDaroei(): Result<DarmanModel>
|
||||
suspend fun searchDrugs(params:DaroYabParams):Result<DaroYabSearchResult>
|
||||
suspend fun getDrugDetailFromYab(detailUrl: String):Result<DrugDetail>
|
||||
suspend fun searchPharmacies(genericDrugId: String,provId: String): List<PharmacyItem>
|
||||
|
||||
suspend fun getPharmacyDetail(pharmacyUrl: String): PharmacyDetail
|
||||
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.approagency.pharmacy.domain.usecase
|
||||
|
||||
import com.approagency.pharmacy.domain.model.DrugDetail
|
||||
import com.approagency.pharmacy.domain.repository.DrugRepository
|
||||
|
||||
class DrugDetailYabUseCase (
|
||||
private val repository: DrugRepository
|
||||
) {
|
||||
suspend operator fun invoke(detailUrl: String):Result<DrugDetail> {
|
||||
return repository.getDrugDetailFromYab(detailUrl)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.approagency.pharmacy.domain.usecase
|
||||
|
||||
import com.approagency.pharmacy.data.dto.DarmanModel
|
||||
import com.approagency.pharmacy.domain.repository.DrugRepository
|
||||
|
||||
class GetDarmanUseCase (
|
||||
private val repository: DrugRepository
|
||||
){
|
||||
suspend operator fun invoke (): Result<DarmanModel>{
|
||||
return repository.getGorohDaroei()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.approagency.pharmacy.domain.usecase
|
||||
|
||||
import com.approagency.pharmacy.data.dto.DrugModels
|
||||
import com.approagency.pharmacy.domain.repository.DrugRepository
|
||||
|
||||
class GetDrugDetailUseCase
|
||||
(
|
||||
private val repository: DrugRepository){
|
||||
suspend operator fun invoke(cod:Int): Result<DrugModels>{
|
||||
return repository.drugDetail(cod)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.approagency.pharmacy.domain.usecase
|
||||
|
||||
import com.approagency.pharmacy.data.dto.DrugListResponse
|
||||
import com.approagency.pharmacy.domain.model.DrugSearchParams
|
||||
import com.approagency.pharmacy.domain.repository.DrugRepository
|
||||
|
||||
class GetDrugSearchUseCase (
|
||||
private val repository: DrugRepository
|
||||
){
|
||||
suspend operator fun invoke(params: DrugSearchParams): Result<DrugListResponse> {
|
||||
return repository.searchDrug(params)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.approagency.pharmacy.domain.usecase
|
||||
|
||||
import com.approagency.pharmacy.domain.model.PharmacyItem
|
||||
import com.approagency.pharmacy.domain.repository.DrugRepository
|
||||
|
||||
class GetPharmaciesUseCase(
|
||||
private val repository: DrugRepository
|
||||
) {
|
||||
suspend operator fun invoke(
|
||||
genericDrugId: String,
|
||||
provinceId: String
|
||||
): List<PharmacyItem> {
|
||||
return repository.searchPharmacies(genericDrugId, provinceId)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.approagency.pharmacy.domain.usecase
|
||||
|
||||
import com.approagency.pharmacy.domain.model.PharmacyDetail
|
||||
import com.approagency.pharmacy.domain.repository.DrugRepository
|
||||
|
||||
class GetPharmacyDetailUseCase(
|
||||
private val repository: DrugRepository
|
||||
) {
|
||||
suspend operator fun invoke(pharmacyUrl: String): PharmacyDetail {
|
||||
return repository.getPharmacyDetail(pharmacyUrl)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.approagency.pharmacy.domain.usecase
|
||||
|
||||
import com.approagency.pharmacy.data.repository.LabRepositoryImpl
|
||||
import com.approagency.pharmacy.domain.model.TestGroup
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
class GetTestGroupUseCase (
|
||||
private val repository: LabRepositoryImpl
|
||||
) {
|
||||
operator fun invoke(): Flow<List<TestGroup>> {
|
||||
return repository.getAllGroups().map { entities ->
|
||||
entities.map { entity ->
|
||||
TestGroup(
|
||||
id = entity.id,
|
||||
ename = entity.ename,
|
||||
fname = entity.fname,
|
||||
detail = entity.detail,
|
||||
isParent = entity.isParent
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.approagency.pharmacy.domain.usecase
|
||||
|
||||
import com.approagency.pharmacy.data.repository.LabRepositoryImpl
|
||||
import com.approagency.pharmacy.domain.model.TestItem
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
class GetTestItemByGroupId (
|
||||
private val repository: LabRepositoryImpl
|
||||
) {
|
||||
operator fun invoke(id:Int): Flow<List<TestItem>> {
|
||||
return repository.getItemsByGroupId(groupId = id).map {
|
||||
entities -> entities.map {
|
||||
entity ->
|
||||
TestItem(
|
||||
id = entity.id,
|
||||
groupId = entity.groupId,
|
||||
title = entity.title,
|
||||
normalValue = entity.normalValue,
|
||||
detail = entity.detail,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.approagency.pharmacy.domain.usecase
|
||||
|
||||
import com.approagency.pharmacy.domain.model.DaroYabParams
|
||||
import com.approagency.pharmacy.domain.model.DaroYabSearchResult
|
||||
import com.approagency.pharmacy.domain.repository.DrugRepository
|
||||
|
||||
class SearchDrugsYabUseCase (
|
||||
private val repository: DrugRepository
|
||||
) {
|
||||
suspend operator fun invoke(query: String, pageNumber: Int = 1): Result<DaroYabSearchResult> {
|
||||
val params = DaroYabParams(query = query, pageNumber = pageNumber)
|
||||
return repository.searchDrugs(params)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.approagency.pharmacy.domain.usecase
|
||||
|
||||
|
||||
import com.approagency.pharmacy.data.repository.LabRepositoryImpl
|
||||
import com.approagency.pharmacy.domain.model.TestGroup
|
||||
import com.approagency.pharmacy.domain.model.TestItem
|
||||
import com.approagency.pharmacy.domain.model.toTestGroup
|
||||
import com.approagency.pharmacy.domain.model.toTestItem
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
class SearchTestsUseCase(
|
||||
private val repository: LabRepositoryImpl
|
||||
) {
|
||||
operator fun invoke(query: String): Flow<Pair<List<TestGroup>, List<TestItem>>> {
|
||||
return repository.searchGroupsAndItems(query).map { (groups, items) ->
|
||||
Pair(
|
||||
groups.map { it.toTestGroup() },
|
||||
items.map { it.toTestItem() }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package com.approagency.pharmacy.navigation
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.navigation.NavType
|
||||
import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import androidx.navigation.navArgument
|
||||
import com.approagency.pharmacy.presentation.screens.DrugDetailScreen
|
||||
import com.approagency.pharmacy.presentation.screens.HomeScreen
|
||||
import com.approagency.pharmacy.presentation.screens.LabScreen
|
||||
import com.approagency.pharmacy.presentation.screens.SearchScreen
|
||||
|
||||
@Composable
|
||||
fun AppNavGraph() {
|
||||
|
||||
val navController = rememberNavController()
|
||||
|
||||
NavHost(
|
||||
navController = navController,
|
||||
startDestination = "home"
|
||||
) {
|
||||
|
||||
// ───────────────
|
||||
// BOTTOM TABS
|
||||
// ───────────────
|
||||
|
||||
composable("home") {
|
||||
MainContainer(navController) {
|
||||
HomeScreen(navController , modifier = Modifier)
|
||||
}
|
||||
}
|
||||
|
||||
composable("search") {
|
||||
MainContainer(navController) {
|
||||
SearchScreen(navController,)
|
||||
}
|
||||
}
|
||||
|
||||
composable("lab") {
|
||||
MainContainer(navController) {
|
||||
LabScreen()
|
||||
}
|
||||
}
|
||||
|
||||
// ───────────────
|
||||
// GLOBAL SCREEN
|
||||
// ───────────────
|
||||
|
||||
composable(
|
||||
route = Screen.DrugDetail.route,
|
||||
arguments = listOf(
|
||||
navArgument("detailUrl") {
|
||||
type = NavType.StringType
|
||||
}
|
||||
)
|
||||
) {
|
||||
|
||||
val detailUrl =
|
||||
Uri.decode(it.arguments?.getString("detailUrl") ?: "")
|
||||
|
||||
DrugDetailScreen(
|
||||
navController = navController,
|
||||
detailUrl = detailUrl
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
package com.approagency.pharmacy.navigation
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.LocalPharmacy
|
||||
import androidx.compose.material.icons.filled.Science
|
||||
import androidx.compose.material.icons.filled.Search
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.navigation.NavHostController
|
||||
import androidx.navigation.compose.currentBackStackEntryAsState
|
||||
|
||||
@Composable
|
||||
fun BottomBar(navController: NavHostController) {
|
||||
|
||||
val items = listOf(
|
||||
Triple("home", "عمومی", Icons.Filled.LocalPharmacy),
|
||||
Triple("search", "جستجو",Icons.Filled.Search),
|
||||
Triple("lab", "آزمایش", Icons.Filled.Science)
|
||||
)
|
||||
|
||||
val navBackStackEntry by navController.currentBackStackEntryAsState()
|
||||
val currentRoute = navBackStackEntry?.destination?.route
|
||||
|
||||
Surface(
|
||||
tonalElevation = 2.dp,
|
||||
modifier = Modifier
|
||||
.navigationBarsPadding()
|
||||
.height(56.dp)
|
||||
) {
|
||||
Row(modifier = Modifier.fillMaxSize(),
|
||||
verticalAlignment = Alignment.CenterVertically) {
|
||||
|
||||
items.forEach { item ->
|
||||
|
||||
val selected = currentRoute == item.first
|
||||
|
||||
val color = if (selected)
|
||||
MaterialTheme.colorScheme.primary
|
||||
else
|
||||
MaterialTheme.colorScheme.onSurfaceVariant
|
||||
val background = if (selected)
|
||||
MaterialTheme.colorScheme.primary.copy(alpha = 0.12f)
|
||||
else
|
||||
Color.Transparent
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.weight(1f).padding(horizontal = 8.dp , vertical = 4.dp)
|
||||
.clip(MaterialTheme.shapes.large)
|
||||
.background(background)
|
||||
.clickable {
|
||||
if (!selected) {
|
||||
navController.navigate(item.first) {
|
||||
popUpTo("home") {
|
||||
saveState = true
|
||||
}
|
||||
launchSingleTop = true
|
||||
restoreState = true
|
||||
}
|
||||
}
|
||||
}.padding(horizontal = 10.dp, vertical = 10.dp),
|
||||
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
|
||||
Icon(
|
||||
imageVector = item.third,
|
||||
contentDescription = item.second,
|
||||
tint = color,
|
||||
modifier = Modifier.size(22.dp)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.width(6.dp))
|
||||
|
||||
Text(
|
||||
text = item.second,
|
||||
color = color,
|
||||
style = MaterialTheme.typography.labelMedium
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.approagency.pharmacy.navigation
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.navigation.NavHostController
|
||||
|
||||
@Composable
|
||||
fun MainContainer(
|
||||
navController: NavHostController,
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
|
||||
Scaffold(
|
||||
bottomBar = {
|
||||
BottomBar(navController)
|
||||
}
|
||||
) { padding ->
|
||||
|
||||
Box(modifier = Modifier.padding(padding)) {
|
||||
content()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package com.approagency.pharmacy.navigation
|
||||
|
||||
import android.net.Uri
|
||||
|
||||
sealed class MainRoute(
|
||||
val route: String
|
||||
) {
|
||||
|
||||
object HomeGraph : MainRoute("home_graph")
|
||||
|
||||
object SearchGraph : MainRoute("search_graph")
|
||||
|
||||
object LabGraph : MainRoute("lab_graph")
|
||||
}
|
||||
|
||||
sealed class Screen(
|
||||
val route: String
|
||||
) {
|
||||
|
||||
object Home : Screen("home")
|
||||
|
||||
object Search : Screen("search")
|
||||
|
||||
object Lab : Screen("lab")
|
||||
|
||||
object DrugDetail : Screen("drug_detail/{detailUrl}") {
|
||||
|
||||
fun createRoute(detailUrl: String): String {
|
||||
return "drug_detail/${Uri.encode(detailUrl)}"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
//package com.approagency.drug.navigation.graphs
|
||||
//
|
||||
//import androidx.compose.ui.Modifier
|
||||
//import androidx.navigation.NavGraphBuilder
|
||||
//import androidx.navigation.NavHostController
|
||||
//import androidx.navigation.compose.composable
|
||||
//import androidx.navigation.navigation
|
||||
//import com.approagency.drug.navigation.MainRoute
|
||||
//import com.approagency.drug.navigation.Screen
|
||||
//import com.approagency.drug.presentation.screens.HomeScreen
|
||||
//
|
||||
//fun NavGraphBuilder.homeGraph(navController: NavHostController) {
|
||||
//
|
||||
// composable(MainRoute.HomeGraph.route) {
|
||||
// HomeScreen(
|
||||
// navController = navController , modifier = Modifier
|
||||
// )
|
||||
// }
|
||||
//}
|
||||
@@ -0,0 +1,16 @@
|
||||
//package com.approagency.drug.navigation.graphs
|
||||
//
|
||||
//import androidx.navigation.NavGraphBuilder
|
||||
//import androidx.navigation.NavHostController
|
||||
//import androidx.navigation.compose.composable
|
||||
//import androidx.navigation.navigation
|
||||
//import com.approagency.drug.navigation.MainRoute
|
||||
//import com.approagency.drug.navigation.Screen
|
||||
//import com.approagency.drug.presentation.screens.LabScreen
|
||||
//
|
||||
//fun NavGraphBuilder.labGraph(navController: NavHostController) {
|
||||
//
|
||||
// composable(MainRoute.LabGraph.route) {
|
||||
// LabScreen()
|
||||
// }
|
||||
//}
|
||||
@@ -0,0 +1,20 @@
|
||||
//package com.approagency.drug.navigation.graphs
|
||||
//
|
||||
//import android.net.Uri
|
||||
//import androidx.navigation.NavGraphBuilder
|
||||
//import androidx.navigation.NavHostController
|
||||
//import androidx.navigation.NavType
|
||||
//import androidx.navigation.compose.composable
|
||||
//import androidx.navigation.navArgument
|
||||
//import androidx.navigation.navigation
|
||||
//import com.approagency.drug.navigation.MainRoute
|
||||
//import com.approagency.drug.navigation.Screen
|
||||
//import com.approagency.drug.presentation.screens.DrugDetailScreen
|
||||
//import com.approagency.drug.presentation.screens.SearchScreen
|
||||
//
|
||||
//fun NavGraphBuilder.searchGraph(navController: NavHostController) {
|
||||
//
|
||||
// composable(MainRoute.SearchGraph.route) {
|
||||
// SearchScreen(navController)
|
||||
// }
|
||||
//}
|
||||
@@ -0,0 +1,37 @@
|
||||
package com.approagency.pharmacy.presentation.common
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.vada.caller.ui.theme.dime
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun CustomModalBottomSheet(
|
||||
modifier: Modifier = Modifier,
|
||||
sheetState: SheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true),
|
||||
scope: CoroutineScope = rememberCoroutineScope(),
|
||||
onDismiss: (() -> Unit)? = null,
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
ModalBottomSheet(
|
||||
onDismissRequest = {
|
||||
onDismiss?.invoke()
|
||||
},
|
||||
sheetState = sheetState,
|
||||
modifier = modifier,
|
||||
shape = RoundedCornerShape(topStart = MaterialTheme.dime.lg, topEnd = MaterialTheme.dime.lg)
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
) {
|
||||
content()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.approagency.pharmacy.presentation.common
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
|
||||
|
||||
@Composable
|
||||
|
||||
fun CustomBox(
|
||||
modifier: Modifier? = Modifier,
|
||||
child: @Composable ()-> Unit
|
||||
) {
|
||||
if (modifier != null) {
|
||||
Box(
|
||||
modifier = modifier.clip(shape = MaterialTheme.shapes.large).background(
|
||||
color = MaterialTheme.colorScheme.surfaceContainer
|
||||
)
|
||||
) {
|
||||
child()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package com.approagency.pharmacy.presentation.common
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.wrapContentHeight
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import com.vada.caller.ui.theme.dime
|
||||
|
||||
@Composable
|
||||
fun CustomModalDialog(
|
||||
onDismissRequest: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
content: @Composable ColumnScope.() -> Unit
|
||||
) {
|
||||
Dialog(
|
||||
onDismissRequest = onDismissRequest
|
||||
){
|
||||
Surface(
|
||||
modifier = modifier
|
||||
.wrapContentHeight(),
|
||||
shape = MaterialTheme.shapes.medium,
|
||||
color = MaterialTheme.colorScheme.surface,
|
||||
tonalElevation = MaterialTheme.dime.sm
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(MaterialTheme.dime.lg),
|
||||
verticalArrangement = Arrangement.spacedBy(MaterialTheme.dime.lg),
|
||||
content = content
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
package com.approagency.pharmacy.presentation.common
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.text.BasicTextField
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material.icons.filled.Search
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.onFocusChanged
|
||||
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.unit.LayoutDirection
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.vada.caller.ui.theme.dime
|
||||
|
||||
@Composable
|
||||
fun CustomTextFilled(
|
||||
value: String = "",
|
||||
onValueChange: (String) -> Unit,
|
||||
onSearch: (String) -> Unit = {},
|
||||
placeholder: String = "جستجو...",
|
||||
modifier: Modifier = Modifier,
|
||||
enabled: Boolean = true,
|
||||
readOnly: Boolean = false,
|
||||
singleLine: Boolean = true,
|
||||
showClearButton: Boolean = true,
|
||||
showSearchButton: Boolean = true,
|
||||
autoSearch: Boolean = false,
|
||||
searchDelay: Long = 1200L,
|
||||
keyboardOptions: KeyboardOptions = KeyboardOptions(
|
||||
imeAction = ImeAction.Search
|
||||
),
|
||||
keyboardActions: KeyboardActions = KeyboardActions(
|
||||
onSearch = {
|
||||
if (value.isNotBlank()) {
|
||||
onSearch(value)
|
||||
}
|
||||
}
|
||||
),
|
||||
textStyle: androidx.compose.ui.text.TextStyle = MaterialTheme.typography.bodyLarge,
|
||||
hintStyle: androidx.compose.ui.text.TextStyle = MaterialTheme.typography.bodyMedium.copy(
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
),
|
||||
containerColor: androidx.compose.ui.graphics.Color = MaterialTheme.colorScheme.surfaceVariant,
|
||||
borderColor: androidx.compose.ui.graphics.Color = MaterialTheme.colorScheme.primaryContainer,
|
||||
shape: androidx.compose.ui.graphics.Shape = MaterialTheme.shapes.medium,
|
||||
height: Int = 45,
|
||||
onFocusChange: ((Boolean) -> Unit)? = null
|
||||
) {
|
||||
var isFocused by remember { mutableStateOf(false) }
|
||||
var localValue by remember(value) { mutableStateOf(value) }
|
||||
|
||||
// Auto-search functionality
|
||||
androidx.compose.runtime.LaunchedEffect(autoSearch, searchDelay, localValue) {
|
||||
if (autoSearch && localValue.isNotBlank()) {
|
||||
kotlinx.coroutines.delay(searchDelay)
|
||||
onSearch(localValue)
|
||||
}
|
||||
}
|
||||
|
||||
CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.height(height.dp)
|
||||
.background(
|
||||
color = containerColor,
|
||||
shape = shape
|
||||
)
|
||||
.border(
|
||||
width = 1.dp,
|
||||
color = if (isFocused)
|
||||
MaterialTheme.colorScheme.primary
|
||||
else
|
||||
borderColor,
|
||||
shape = shape
|
||||
)
|
||||
.padding(horizontal = MaterialTheme.dime.md, vertical = MaterialTheme.dime.sm)
|
||||
.onFocusChanged { focusState ->
|
||||
isFocused = focusState.isFocused
|
||||
onFocusChange?.invoke(focusState.isFocused)
|
||||
}
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
// Clear button (shows when there's text and clear button is enabled)
|
||||
if (showClearButton && localValue.isNotEmpty()) {
|
||||
IconButton(
|
||||
onClick = {
|
||||
localValue = ""
|
||||
onValueChange("")
|
||||
},
|
||||
modifier = Modifier.size(28.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Close,
|
||||
contentDescription = "Clear",
|
||||
modifier = Modifier.size(16.dp),
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Text field
|
||||
BasicTextField(
|
||||
value = localValue,
|
||||
onValueChange = { newValue ->
|
||||
localValue = newValue
|
||||
onValueChange(newValue)
|
||||
},
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(horizontal = if (showClearButton && localValue.isNotEmpty()) 4.dp else 0.dp),
|
||||
enabled = enabled,
|
||||
readOnly = readOnly,
|
||||
singleLine = singleLine,
|
||||
textStyle = textStyle,
|
||||
keyboardOptions = keyboardOptions,
|
||||
keyboardActions = keyboardActions,
|
||||
decorationBox = { innerTextField ->
|
||||
Box(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
contentAlignment = Alignment.CenterStart
|
||||
) {
|
||||
if (localValue.isEmpty() && !isFocused) {
|
||||
Text(
|
||||
text = placeholder,
|
||||
style = hintStyle
|
||||
)
|
||||
}
|
||||
innerTextField()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// Search button
|
||||
if (showSearchButton) {
|
||||
IconButton(
|
||||
onClick = {
|
||||
if (localValue.isNotBlank()) {
|
||||
onSearch(localValue)
|
||||
}
|
||||
},
|
||||
modifier = Modifier.size(32.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Search,
|
||||
contentDescription = "Search",
|
||||
modifier = Modifier.size(18.dp),
|
||||
tint = if (localValue.isNotBlank())
|
||||
MaterialTheme.colorScheme.primary
|
||||
else
|
||||
MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
package com.approagency.pharmacy.presentation.common
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.vada.caller.ui.theme.LocalDime
|
||||
|
||||
@Composable
|
||||
fun EmptySearchState(
|
||||
onRetry: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val dime = LocalDime.current
|
||||
|
||||
Column(
|
||||
modifier = modifier.fillMaxSize(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
Text(
|
||||
text = "جستجوی داروها",
|
||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f),
|
||||
fontSize = 16.sp
|
||||
)
|
||||
Text(
|
||||
text = "نام دارو را وارد کنید",
|
||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.4f),
|
||||
fontSize = 14.sp,
|
||||
modifier = Modifier.padding(top = dime.sm)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ErrorState(
|
||||
message: String,
|
||||
onRetry: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val dime = LocalDime.current
|
||||
|
||||
Column(
|
||||
modifier = modifier.fillMaxSize(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
Text(
|
||||
text = "خطا",
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
fontSize = 18.sp,
|
||||
fontWeight = androidx.compose.ui.text.font.FontWeight.Bold
|
||||
)
|
||||
Text(
|
||||
text = message,
|
||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f),
|
||||
fontSize = 14.sp,
|
||||
modifier = Modifier.padding(top = dime.sm, bottom = dime.lg),
|
||||
textAlign = androidx.compose.ui.text.style.TextAlign.Center
|
||||
)
|
||||
Button(
|
||||
onClick = onRetry,
|
||||
colors = androidx.compose.material3.ButtonDefaults.buttonColors(
|
||||
containerColor = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
) {
|
||||
Text("تلاش مجدد")
|
||||
}
|
||||
}
|
||||
}
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
package com.approagency.pharmacy.presentation.common
|
||||
|
||||
import androidx.compose.ui.unit.sp
|
||||
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.vada.caller.ui.theme.LocalDime
|
||||
|
||||
@Composable
|
||||
fun LoadingMoreIndicator(
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val dime = LocalDime.current
|
||||
|
||||
Box(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.padding(dime.lg),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.size(32.dp),
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun EndOfListIndicator(
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val dime = LocalDime.current
|
||||
|
||||
Box(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.padding(dime.xl),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
text = "پایان نتایج",
|
||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.5f),
|
||||
fontSize = 12.sp
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.approagency.pharmacy.presentation.common
|
||||
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
object SnackbarManager {
|
||||
private var snackbarHostState: SnackbarHostState? = null
|
||||
private var coroutineScope: CoroutineScope? = null
|
||||
|
||||
fun init(scope: CoroutineScope, hostState: SnackbarHostState) {
|
||||
coroutineScope = scope
|
||||
snackbarHostState = hostState
|
||||
}
|
||||
|
||||
fun showMessage(message: String) {
|
||||
coroutineScope?.launch {
|
||||
snackbarHostState?.showSnackbar(message)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.vada.caller.presentation.common
|
||||
|
||||
sealed interface UiState<out T> {
|
||||
object Loading : UiState<Nothing>
|
||||
data class Success<T>(val data: T) : UiState<T>
|
||||
data class Error(val message: String) : UiState<Nothing>
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.approagency.pharmacy.presentation.common
|
||||
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@Composable
|
||||
fun Loading(
|
||||
color: Color = MaterialTheme.colorScheme.onPrimary,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
CircularProgressIndicator(
|
||||
color = color,
|
||||
modifier = modifier,
|
||||
strokeWidth = 2.dp
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package com.approagency.pharmacy.presentation.common
|
||||
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@Composable
|
||||
fun PrimaryButton(
|
||||
text: String,
|
||||
isLoading: Boolean?,
|
||||
onClick: () -> Unit,
|
||||
height: Int? = null
|
||||
) {
|
||||
Button(
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = MaterialTheme.colorScheme.primary,
|
||||
contentColor = MaterialTheme.colorScheme.onPrimary
|
||||
),
|
||||
shape = MaterialTheme.shapes.medium,
|
||||
modifier = Modifier.fillMaxWidth().height(height?.dp ?: 50.dp),
|
||||
onClick = {
|
||||
onClick()
|
||||
},
|
||||
) {
|
||||
if (isLoading == true) {
|
||||
Text("... در حال جستجو" , textAlign = TextAlign.Right)
|
||||
} else {
|
||||
Text(text)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
package com.approagency.pharmacy.presentation.components
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.twotone.KeyboardArrowLeft
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.AbsoluteAlignment
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import com.approagency.pharmacy.data.dto.DarmanList
|
||||
import com.approagency.pharmacy.data.dto.DarmanModel
|
||||
import com.approagency.pharmacy.presentation.common.CustomBox
|
||||
import com.vada.caller.ui.theme.dime
|
||||
import kotlin.collections.emptyList
|
||||
|
||||
@Composable
|
||||
fun DarmanContent(
|
||||
darmanData: Result<DarmanModel?>?,
|
||||
onDarmanClick: (Int) -> Unit,
|
||||
onRetryClick:()-> Unit,
|
||||
){
|
||||
val drugs = darmanData?.getOrNull()?.data ?: emptyList()
|
||||
if (drugs.isEmpty()){
|
||||
RetryContent(
|
||||
modifier = Modifier,
|
||||
error = "مشکلی از سمت سرور پیش آمده",
|
||||
onClick = {
|
||||
onRetryClick()
|
||||
}
|
||||
)
|
||||
} else {
|
||||
LazyColumn {
|
||||
items(drugs.count() , itemContent ={
|
||||
i -> DarmanItem(
|
||||
darman = darmanData?.getOrNull()!!.data[i] ,
|
||||
onClick = { onDarmanClick(drugs[i].cod!!) }
|
||||
)
|
||||
} )
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DarmanItem(
|
||||
darman: DarmanList, // Adjust this based on your actual data class
|
||||
onClick: (Int?) -> Unit
|
||||
) {
|
||||
CustomBox(
|
||||
modifier = Modifier
|
||||
.padding(bottom = MaterialTheme.dime.sm)
|
||||
.clickable(
|
||||
indication = null,
|
||||
interactionSource = remember { MutableInteractionSource() }
|
||||
) {
|
||||
onClick(darman.cod)
|
||||
},
|
||||
child = {
|
||||
Row(
|
||||
modifier = Modifier.padding(all = MaterialTheme.dime.lg),
|
||||
) {
|
||||
Column (
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.Start
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalAlignment = AbsoluteAlignment.Right
|
||||
// horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Text(darman.nam_fa.toString() , style = MaterialTheme.typography.labelLarge , textAlign = TextAlign.Right )
|
||||
Text(darman.nam_en.toString() , style = MaterialTheme.typography.bodySmall)
|
||||
}
|
||||
Row (
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.Center
|
||||
) {
|
||||
TextButton(
|
||||
onClick = {
|
||||
onClick(darman.cod)
|
||||
}
|
||||
) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.Center
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.TwoTone.KeyboardArrowLeft
|
||||
, contentDescription = "see more"
|
||||
)
|
||||
Text("مشاهده لیست")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
)
|
||||
// Button(
|
||||
// onClick = onClick,
|
||||
// modifier = Modifier
|
||||
// .fillMaxWidth()
|
||||
// .padding(vertical = 4.dp)
|
||||
// ) {
|
||||
// drug.nam_fa?.let { Text(text = it) } // Adjust based on your drug model
|
||||
// }
|
||||
}
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
package com.approagency.pharmacy.presentation.components
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.approagency.pharmacy.domain.model.DrugSearchResult
|
||||
import com.approagency.pharmacy.presentation.common.CustomBox
|
||||
import com.vada.caller.ui.theme.LocalDime
|
||||
import com.vada.caller.ui.theme.dime
|
||||
|
||||
@Composable
|
||||
fun DaroYabSearchResult(
|
||||
drug: DrugSearchResult,
|
||||
onClickDetail: (DrugSearchResult) -> Unit,
|
||||
onClickDrugStore:(DrugSearchResult) ->Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val dime = LocalDime.current
|
||||
|
||||
CustomBox (
|
||||
modifier = modifier
|
||||
.padding(bottom = MaterialTheme.dime.sm)
|
||||
.fillMaxWidth()
|
||||
.clickable { onClickDetail(drug) },
|
||||
|
||||
) {
|
||||
Row (
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.End
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(dime.lg),
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.End
|
||||
) {
|
||||
Text(
|
||||
text = drug.persianName,
|
||||
style = MaterialTheme.typography.labelLarge
|
||||
)
|
||||
drug.englishName?.let {
|
||||
Text(
|
||||
text = it,
|
||||
fontSize = 12.sp,
|
||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f)
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text = "کد: ${drug.genericId}",
|
||||
fontSize = 10.sp,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
Row(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
TextButton(onClick = {onClickDetail(drug)} , content = {
|
||||
Text("اطلاعات دارویی")
|
||||
})
|
||||
TextButton(onClick = {onClickDrugStore(drug)} , content = {
|
||||
Text("موجودی داروخانه")
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+152
@@ -0,0 +1,152 @@
|
||||
package com.approagency.pharmacy.presentation.components
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.twotone.KeyboardArrowLeft
|
||||
import androidx.compose.material.icons.filled.Search
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import com.approagency.pharmacy.data.dto.DrugDetail
|
||||
import com.approagency.pharmacy.data.dto.DrugListResponse
|
||||
import com.approagency.pharmacy.presentation.common.CustomBox
|
||||
import com.vada.caller.ui.theme.dime
|
||||
|
||||
@Composable
|
||||
fun DrugListContent(
|
||||
drugsData: Result<DrugListResponse?>?,
|
||||
onDrugClick: (Int) -> Unit
|
||||
) {
|
||||
val drugs = drugsData?.getOrNull()?.data ?: emptyList()
|
||||
|
||||
if (drugs.isEmpty()) {
|
||||
CustomBox(
|
||||
child = {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(all = MaterialTheme.dime.sm),
|
||||
) {
|
||||
Text(
|
||||
text = "نتیجهای یافت نشد",
|
||||
fontWeight = FontWeight.W500
|
||||
)
|
||||
Spacer(modifier = Modifier.width(MaterialTheme.dime.xs))
|
||||
Icon(
|
||||
imageVector = Icons.Default.Search,
|
||||
contentDescription = "Search"
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
} else {
|
||||
LazyColumn(
|
||||
|
||||
){
|
||||
items(drugs.count() , itemContent ={
|
||||
i -> DrugItem(
|
||||
drug = drugs[i],
|
||||
onClick = { onDrugClick(drugs[i].cod!!) }
|
||||
)
|
||||
} )
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Composable
|
||||
fun DrugItem(
|
||||
drug: DrugDetail, // Adjust this based on your actual data class
|
||||
onClick: (Int?) -> Unit
|
||||
) {
|
||||
CustomBox(
|
||||
modifier = Modifier
|
||||
.padding(bottom = MaterialTheme.dime.sm)
|
||||
.fillMaxWidth()
|
||||
.clickable(
|
||||
indication = null,
|
||||
interactionSource = remember { MutableInteractionSource() }
|
||||
) {
|
||||
onClick(drug.cod)
|
||||
},
|
||||
child = {
|
||||
Row(
|
||||
modifier = Modifier.padding(all = MaterialTheme.dime.lg),
|
||||
) {
|
||||
Column (
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.Start
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Text(drug.nam_en.toString() , style = MaterialTheme.typography.bodySmall)
|
||||
Text(drug.nam_fa.toString() , style = MaterialTheme.typography.labelLarge,textAlign = TextAlign.Right)
|
||||
}
|
||||
if (drug.goroh_darmani != null )
|
||||
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
, horizontalArrangement = Arrangement.End
|
||||
) {Text(
|
||||
"گروه دارویی: ${drug.goroh_darmani.nam_fa.toString()}",
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
textAlign = TextAlign.Right
|
||||
) }
|
||||
|
||||
|
||||
|
||||
Row (
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.Center
|
||||
) {
|
||||
TextButton(
|
||||
onClick = {
|
||||
onClick(drug.cod)
|
||||
}
|
||||
) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.Center
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.TwoTone.KeyboardArrowLeft
|
||||
, contentDescription = "see more"
|
||||
)
|
||||
Text("مشاهده جزییات")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
)
|
||||
// Button(
|
||||
// onClick = onClick,
|
||||
// modifier = Modifier
|
||||
// .fillMaxWidth()
|
||||
// .padding(vertical = 4.dp)
|
||||
// ) {
|
||||
// drug.nam_fa?.let { Text(text = it) } // Adjust based on your drug model
|
||||
// }
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
package com.approagency.pharmacy.presentation.components
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Search
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.approagency.pharmacy.presentation.common.CustomBox
|
||||
import com.vada.caller.ui.theme.dime
|
||||
|
||||
@Composable
|
||||
fun EmptyStateContent() {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
, verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Search,
|
||||
contentDescription = "Search",
|
||||
modifier = Modifier.size(MaterialTheme.dime.iconLg)
|
||||
)
|
||||
CustomBox(
|
||||
child = {
|
||||
Text(
|
||||
text = "برای جستجو، نام دارو را وارد کنید",
|
||||
modifier = Modifier.padding(MaterialTheme.dime.sm)
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
+141
@@ -0,0 +1,141 @@
|
||||
package com.approagency.pharmacy.presentation.components
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.ArrowForward
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
|
||||
@Composable
|
||||
fun PaginationControls(
|
||||
currentPage: Int,
|
||||
totalPages: Int,
|
||||
onPageSelected: (Int) -> Unit,
|
||||
onNextPage: () -> Unit,
|
||||
onPreviousPage: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
if (totalPages <= 1) return
|
||||
|
||||
Row(
|
||||
modifier = modifier.padding(vertical = 16.dp),
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
// Previous button
|
||||
PaginationButton(
|
||||
enabled = currentPage > 1,
|
||||
onClick = onPreviousPage,
|
||||
isPrevious = true
|
||||
)
|
||||
|
||||
// Page numbers
|
||||
val pageRange = getVisiblePageRange(currentPage, totalPages)
|
||||
|
||||
if (pageRange.first > 1) {
|
||||
PageNumberButton(page = 1, isSelected = false, onClick = onPageSelected)
|
||||
if (pageRange.first > 2) {
|
||||
Text("...", modifier = Modifier.padding(horizontal = 4.dp))
|
||||
}
|
||||
}
|
||||
|
||||
pageRange.forEach { page ->
|
||||
PageNumberButton(
|
||||
page = page,
|
||||
isSelected = page == currentPage,
|
||||
onClick = onPageSelected
|
||||
)
|
||||
}
|
||||
|
||||
if (pageRange.last < totalPages) {
|
||||
if (pageRange.last < totalPages - 1) {
|
||||
Text("...", modifier = Modifier.padding(horizontal = 4.dp))
|
||||
}
|
||||
PageNumberButton(page = totalPages, isSelected = false, onClick = onPageSelected)
|
||||
}
|
||||
|
||||
// Next button
|
||||
PaginationButton(
|
||||
enabled = currentPage < totalPages,
|
||||
onClick = onNextPage,
|
||||
isPrevious = false
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PageNumberButton(
|
||||
page: Int,
|
||||
isSelected: Boolean,
|
||||
onClick: (Int) -> Unit
|
||||
) {
|
||||
val backgroundColor = if (isSelected) Color(0xFF7D64BA) else Color.Transparent
|
||||
val textColor = if (isSelected) Color.White else Color(0xFF7D64BA)
|
||||
|
||||
Text(
|
||||
text = page.toString(),
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 4.dp)
|
||||
.clip(RoundedCornerShape(4.dp))
|
||||
.background(backgroundColor)
|
||||
.clickable { onClick(page) }
|
||||
.padding(horizontal = 12.dp, vertical = 8.dp),
|
||||
color = textColor,
|
||||
fontSize = 14.sp
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PaginationButton(
|
||||
enabled: Boolean,
|
||||
onClick: () -> Unit,
|
||||
isPrevious: Boolean
|
||||
) {
|
||||
val icon = if (isPrevious) Icons.Default.ArrowBack else Icons.Default.ArrowForward
|
||||
val color = if (enabled) Color(0xFF7D64BA) else Color.Gray
|
||||
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = if (isPrevious) "Previous" else "Next",
|
||||
modifier = Modifier
|
||||
.size(36.dp)
|
||||
.clip(RoundedCornerShape(4.dp))
|
||||
.then(if (enabled) Modifier.clickable(onClick = onClick) else Modifier)
|
||||
.padding(8.dp),
|
||||
tint = color
|
||||
)
|
||||
}
|
||||
|
||||
private fun getVisiblePageRange(currentPage: Int, totalPages: Int): IntRange {
|
||||
val maxVisible = 5
|
||||
val halfVisible = maxVisible / 2
|
||||
|
||||
var start = currentPage - halfVisible
|
||||
var end = currentPage + halfVisible
|
||||
|
||||
if (start < 1) {
|
||||
end += (1 - start)
|
||||
start = 1
|
||||
}
|
||||
if (end > totalPages) {
|
||||
start -= (end - totalPages)
|
||||
end = totalPages
|
||||
}
|
||||
if (start < 1) start = 1
|
||||
|
||||
return start..end
|
||||
}
|
||||
+350
@@ -0,0 +1,350 @@
|
||||
package com.approagency.pharmacy.presentation.components
|
||||
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.LocationOn
|
||||
import androidx.compose.material.icons.filled.Phone
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.LayoutDirection
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.approagency.pharmacy.domain.model.PharmacyItem
|
||||
import com.approagency.pharmacy.presentation.common.CustomBox
|
||||
import com.approagency.pharmacy.presentation.common.CustomModalBottomSheet
|
||||
import com.approagency.pharmacy.presentation.common.Loading
|
||||
import com.approagency.pharmacy.presentation.viewModel.PharmacyDetailState
|
||||
import com.approagency.pharmacy.presentation.viewModel.PharmacyState
|
||||
import com.approagency.pharmacy.presentation.viewModel.PharmacyViewModel
|
||||
import com.approagency.pharmacy.utils.provinces
|
||||
import com.vada.caller.ui.theme.LocalDime
|
||||
import com.vada.caller.ui.theme.dime
|
||||
|
||||
@SuppressLint("ConfigurationScreenWidthHeight")
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun PharmacyBottomSheet(
|
||||
viewModel: PharmacyViewModel, genericDrugId: String, isOpen: Boolean, onDismiss: () -> Unit
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
val sheetState = rememberModalBottomSheetState(
|
||||
skipPartiallyExpanded = false, confirmValueChange = { true })
|
||||
|
||||
|
||||
var selectedProvinceId by remember { mutableStateOf("40") } // پیشفرض تهران
|
||||
var showProvinceDropdown by remember { mutableStateOf(false) }
|
||||
|
||||
val pharmacyState by viewModel.state.collectAsState()
|
||||
|
||||
// جستجوی اولیه با استان تهران
|
||||
LaunchedEffect(Unit) {
|
||||
if (genericDrugId.isNotBlank()) {
|
||||
viewModel.search(genericDrugId, "40")
|
||||
}
|
||||
}
|
||||
|
||||
if (isOpen) {
|
||||
CustomModalBottomSheet(
|
||||
sheetState = sheetState, onDismiss = onDismiss, scope = scope
|
||||
) {
|
||||
CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) {
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxHeight()
|
||||
.fillMaxWidth()
|
||||
.heightIn(min = 400.dp)
|
||||
.padding(MaterialTheme.dime.md),
|
||||
verticalArrangement = Arrangement.spacedBy(MaterialTheme.dime.xl)
|
||||
) {
|
||||
item {
|
||||
Text(
|
||||
text = "جستجوی دارو در داروخانهها",
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = MaterialTheme.typography.titleMedium.fontSize
|
||||
)
|
||||
}
|
||||
// عنوان
|
||||
|
||||
item {
|
||||
ExposedDropdownMenuBox(
|
||||
expanded = showProvinceDropdown,
|
||||
onExpandedChange = { showProvinceDropdown = it }) {
|
||||
OutlinedTextField(
|
||||
value = provinces.find { it.id == selectedProvinceId }?.name
|
||||
?: "انتخاب استان",
|
||||
onValueChange = {},
|
||||
readOnly = true,
|
||||
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = showProvinceDropdown) },
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.menuAnchor(),
|
||||
shape = MaterialTheme.shapes.large
|
||||
)
|
||||
|
||||
ExposedDropdownMenu(
|
||||
expanded = showProvinceDropdown,
|
||||
onDismissRequest = { showProvinceDropdown = false }) {
|
||||
provinces.forEach { province ->
|
||||
DropdownMenuItem(text = { Text(province.name) }, onClick = {
|
||||
selectedProvinceId = province.id
|
||||
showProvinceDropdown = false
|
||||
// جستجو با استان جدید
|
||||
viewModel.search(genericDrugId, province.id)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// انتخاب استان
|
||||
|
||||
|
||||
// نمایش نتایج
|
||||
when (val state = pharmacyState) {
|
||||
PharmacyState.Loading -> {
|
||||
item {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(400.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
Loading()
|
||||
Spacer(modifier = Modifier.height(MaterialTheme.dime.xl))
|
||||
Text(
|
||||
text = "در حال جستجو...",
|
||||
fontSize = MaterialTheme.typography.bodySmall.fontSize
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
is PharmacyState.Success -> {
|
||||
if (state.items.isEmpty()) {
|
||||
item {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(400.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
text = "هیچ داروخانهای یافت نشد",
|
||||
fontSize = MaterialTheme.typography.bodyMedium.fontSize,
|
||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
items(state.items) { pharmacy ->
|
||||
PharmacyResultItem(pharmacy = pharmacy, viewModel)
|
||||
}
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(MaterialTheme.dime.md))
|
||||
Text(
|
||||
text = "تعداد کل: ${state.items.size} داروخانه",
|
||||
fontSize = MaterialTheme.typography.bodySmall.fontSize,
|
||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.5f),
|
||||
modifier = Modifier.padding(bottom = MaterialTheme.dime.xs)
|
||||
)
|
||||
}
|
||||
// LazyColumn(
|
||||
// verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
// modifier = Modifier,
|
||||
// ) {
|
||||
// items(state.items) { pharmacy ->
|
||||
// PharmacyResultItem(pharmacy = pharmacy)
|
||||
// }
|
||||
//
|
||||
//
|
||||
// }
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
is PharmacyState.Error -> {
|
||||
item {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(200.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
Text(
|
||||
text = state.message,
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
fontSize = MaterialTheme.typography.bodyMedium.fontSize
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
TextButton(
|
||||
onClick = { viewModel.retry() }) {
|
||||
Text("تلاش مجدد")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
PharmacyState.Idle -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun PharmacyResultItem(
|
||||
pharmacy: PharmacyItem, viewModel: PharmacyViewModel, modifier: Modifier = Modifier
|
||||
) {
|
||||
val dime = LocalDime.current
|
||||
var showDetails by remember { mutableStateOf(false) }
|
||||
// گرفتن State مخصوص این داروخانه از Map
|
||||
val detailStates by viewModel.pharmacyDetailStates.collectAsState()
|
||||
val detailState = detailStates[pharmacy.pharmacyUrl] ?: PharmacyDetailState.Idle
|
||||
|
||||
// Reset detail state when item changes
|
||||
LaunchedEffect(pharmacy.pharmacyUrl) {
|
||||
println(pharmacy.pharmacyUrl)
|
||||
showDetails = false
|
||||
}
|
||||
|
||||
// Load detail when expanded
|
||||
LaunchedEffect(showDetails) {
|
||||
if (showDetails) {
|
||||
// فقط اگر Idle هست (هنوز لود نشده) درخواست بده
|
||||
if (detailState is PharmacyDetailState.Idle) {
|
||||
println(pharmacy.pharmacyUrl)
|
||||
viewModel.loadPharmacyDetail(pharmacy.pharmacyUrl)
|
||||
}
|
||||
}
|
||||
}
|
||||
CustomBox(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(dime.md),
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
// نام برند
|
||||
Text(
|
||||
text = pharmacy.brandName,
|
||||
fontSize = MaterialTheme.typography.labelSmall.fontSize,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
|
||||
// نام داروخانه
|
||||
Text(
|
||||
text = pharmacy.pharmacyName,
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = MaterialTheme.typography.labelLarge.fontSize
|
||||
)
|
||||
|
||||
// موقعیت
|
||||
// موقعیت و دکمه اطلاعات تماس
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Text(
|
||||
text = "${pharmacy.province} - ${pharmacy.city}",
|
||||
fontSize = MaterialTheme.typography.bodySmall.fontSize,
|
||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f)
|
||||
)
|
||||
|
||||
TextButton(
|
||||
onClick = { showDetails = !showDetails }) {
|
||||
Text(
|
||||
text = if (showDetails) "بستن" else "اطلاعات تماس",
|
||||
fontSize = MaterialTheme.typography.bodySmall.fontSize
|
||||
)
|
||||
}
|
||||
}
|
||||
if (showDetails) {
|
||||
|
||||
when (detailState) {
|
||||
is PharmacyDetailState.Loading -> {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
CircularProgressIndicator(modifier = Modifier.size(24.dp))
|
||||
}
|
||||
}
|
||||
|
||||
is PharmacyDetailState.Success -> {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
HorizontalDivider(modifier = Modifier.padding(vertical = MaterialTheme.dime.xs))
|
||||
// آدرس
|
||||
Row(
|
||||
verticalAlignment = Alignment.Top,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.LocationOn,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(16.dp),
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
Text(
|
||||
text = detailState.detail.address,
|
||||
fontSize = MaterialTheme.typography.bodySmall.fontSize,
|
||||
lineHeight = 18.sp
|
||||
)
|
||||
}
|
||||
|
||||
// تلفن
|
||||
if (detailState.detail.phone.isNotBlank()) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Phone,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(16.dp),
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
Text(
|
||||
text = detailState.detail.phone,
|
||||
fontSize = MaterialTheme.typography.bodyMedium.fontSize,
|
||||
fontWeight = FontWeight.Medium
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
is PharmacyDetailState.Error -> {
|
||||
Text(
|
||||
text = "خطا در دریافت اطلاعات تماس",
|
||||
fontSize = MaterialTheme.typography.bodySmall.fontSize,
|
||||
color = MaterialTheme.colorScheme.error
|
||||
)
|
||||
}
|
||||
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package com.approagency.pharmacy.presentation.components
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.approagency.pharmacy.presentation.common.CustomBox
|
||||
import com.approagency.pharmacy.presentation.common.PrimaryButton
|
||||
import com.vada.caller.ui.theme.dime
|
||||
|
||||
@Composable
|
||||
fun RetryContent(modifier: Modifier = Modifier , error:String ,onClick:()-> Unit ) {
|
||||
CustomBox(
|
||||
child = {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = Modifier.padding(MaterialTheme.dime.md)
|
||||
) {
|
||||
Text(
|
||||
text = "خطا در دریافت اطلاعات",
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.error
|
||||
)
|
||||
Text(
|
||||
text = error,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
modifier = Modifier.padding(vertical = 8.dp)
|
||||
)
|
||||
PrimaryButton(
|
||||
text = "تلاش مجدد",
|
||||
height = 40,
|
||||
isLoading = false,
|
||||
onClick = { onClick() }
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
+204
@@ -0,0 +1,204 @@
|
||||
package com.approagency.pharmacy.presentation.components
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.approagency.pharmacy.domain.model.TestGroup
|
||||
import com.approagency.pharmacy.domain.model.TestItem
|
||||
import com.approagency.pharmacy.presentation.common.CustomBox
|
||||
import com.approagency.pharmacy.presentation.common.Loading
|
||||
import com.approagency.pharmacy.presentation.viewModel.SearchResult
|
||||
import com.vada.caller.ui.theme.dime
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.ui.AbsoluteAlignment
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextDirection
|
||||
|
||||
@Composable
|
||||
fun SearchResultsContent(
|
||||
searchResult: SearchResult?,
|
||||
isLoading: Boolean,
|
||||
error: String?,
|
||||
onGroupClick: (TestGroup) -> Unit,
|
||||
onItemClick: (TestItem) -> Unit,
|
||||
onClearSearch: () -> Unit
|
||||
) {
|
||||
when {
|
||||
isLoading -> {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
Loading(color = MaterialTheme.colorScheme.primary)
|
||||
Spacer(modifier = Modifier.height(MaterialTheme.dime.sm))
|
||||
Text("در حال جستجو...")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
error != null -> {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
Text(
|
||||
text = "خطا در جستجو",
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.error
|
||||
)
|
||||
Text(
|
||||
text = error,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
modifier = Modifier.padding(vertical = 8.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
searchResult != null -> {
|
||||
val groups = searchResult.groups
|
||||
val items = searchResult.items
|
||||
|
||||
if (groups.isEmpty() && items.isEmpty()) {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
Text(
|
||||
text = "نتیجهای یافت نشد",
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
)
|
||||
Spacer(modifier = Modifier.height(MaterialTheme.dime.sm))
|
||||
Text(
|
||||
text = "لطفاً عبارت دیگری را جستجو کنید",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(vertical = MaterialTheme.dime.md),
|
||||
horizontalAlignment = AbsoluteAlignment.Right
|
||||
|
||||
) {
|
||||
if (groups.isNotEmpty()) {
|
||||
item {
|
||||
Text(
|
||||
text = "گروههای آزمایشگاهی (${groups.size})",
|
||||
style = MaterialTheme.typography.titleMedium.copy(
|
||||
textAlign = TextAlign.Center
|
||||
),
|
||||
fontWeight = FontWeight.Bold,
|
||||
modifier = Modifier.padding(vertical = MaterialTheme.dime.md)
|
||||
)
|
||||
}
|
||||
|
||||
items(groups) { group ->
|
||||
TestGroupItemContent(
|
||||
group = group,
|
||||
onClick = { onGroupClick(group) }
|
||||
)
|
||||
}
|
||||
|
||||
if (items.isNotEmpty()) {
|
||||
item {
|
||||
HorizontalDivider(
|
||||
modifier = Modifier.padding(vertical = MaterialTheme.dime.md)
|
||||
)
|
||||
Text(
|
||||
text = "آیتمهای آزمایشگاهی (${items.size})",
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.Bold,
|
||||
modifier = Modifier.padding(vertical = MaterialTheme.dime.md)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
items(items) { item ->
|
||||
TestItemSearchResult(
|
||||
testItem = item,
|
||||
onClick = { onItemClick(item) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun TestItemSearchResult(
|
||||
testItem: TestItem,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
CustomBox(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = MaterialTheme.dime.sm)
|
||||
.clickable(
|
||||
indication = null,
|
||||
interactionSource = remember { MutableInteractionSource() }
|
||||
) {
|
||||
onClick()
|
||||
}
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(all = MaterialTheme.dime.lg),
|
||||
horizontalAlignment = AbsoluteAlignment.Right
|
||||
) {
|
||||
Text(
|
||||
text = testItem.title ?: "بدون عنوان",
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
|
||||
if (!testItem.normalValue.isNullOrBlank()) {
|
||||
Text(
|
||||
text = "مقدار نرمال: ${testItem.normalValue}",
|
||||
style = MaterialTheme.typography.bodySmall.copy(
|
||||
textDirection = TextDirection.Rtl,
|
||||
textAlign = TextAlign.Right
|
||||
),
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.padding(top = MaterialTheme.dime.xs)
|
||||
)
|
||||
}
|
||||
|
||||
if (!testItem.detail.isNullOrBlank()) {
|
||||
Text(
|
||||
text = testItem.detail,
|
||||
style = MaterialTheme.typography.bodySmall.copy(
|
||||
textDirection = TextDirection.Rtl,
|
||||
textAlign = TextAlign.Right
|
||||
),
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.padding(top = MaterialTheme.dime.sm)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,222 @@
|
||||
package com.approagency.pharmacy.presentation.components
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.LayoutDirection
|
||||
import com.approagency.pharmacy.data.dto.DrugDetail // Import DrugDetail directly
|
||||
import com.approagency.pharmacy.presentation.common.CustomModalBottomSheet
|
||||
import com.approagency.pharmacy.presentation.viewModel.DrugDetailState
|
||||
import com.approagency.pharmacy.presentation.viewModel.HomeViewModel
|
||||
import com.vada.caller.ui.theme.dime // Assuming this is your custom spacing/dimension object
|
||||
|
||||
@Composable
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
fun DrugDetailSheet(
|
||||
viewModel: HomeViewModel,
|
||||
state: DrugDetailState // Assuming you pass the state containing the detail
|
||||
) {
|
||||
val detail: DrugDetail? = state.drugDetail?.getOrNull()?.data
|
||||
|
||||
// Only show the sheet if detail is not null
|
||||
if (detail != null) {
|
||||
CustomModalBottomSheet(
|
||||
onDismiss = {
|
||||
viewModel.closeDetail()
|
||||
},
|
||||
content = {
|
||||
// Use a LazyColumn for scrollable content inside the sheet
|
||||
|
||||
CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) {
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = MaterialTheme.dime.lg),
|
||||
contentPadding = PaddingValues(bottom = MaterialTheme.dime.lg) // Add padding to the bottom of the content for better scrolling
|
||||
) {
|
||||
// --- 1. Main Title ---
|
||||
item { // Use item {} for single composables within LazyColumn
|
||||
Text(
|
||||
text = detail.nam_fa ?: "جزئیات دارو",
|
||||
style = MaterialTheme.typography.headlineMedium,
|
||||
fontWeight = FontWeight.Bold,
|
||||
modifier = Modifier.padding(bottom = MaterialTheme.dime.md)
|
||||
)
|
||||
Text(
|
||||
text = detail.nam_en ?: "N/A (English Name)",
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.padding(bottom = MaterialTheme.dime.lg)
|
||||
)
|
||||
|
||||
// Divider for visual separation
|
||||
HorizontalDivider(Modifier.padding(bottom = MaterialTheme.dime.md))
|
||||
}
|
||||
|
||||
// --- 2. Key Groupings ---
|
||||
// Goroh Darmani (Pharmacological Group)
|
||||
detail.goroh_darmani?.let { group ->
|
||||
item {
|
||||
DetailRow(
|
||||
title = "گروه درمانی",
|
||||
value = group.nam_fa ?: "نامشخص"
|
||||
)
|
||||
}
|
||||
group.nam_en?.let { enName ->
|
||||
item {
|
||||
DetailRow(
|
||||
title = "Group (EN)",
|
||||
value = enName
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Goroh Darmanei (Therapeutic Group)
|
||||
detail.goroh_daroei?.let { group ->
|
||||
item {
|
||||
DetailRow(
|
||||
title = "گروه دارویی",
|
||||
value = group.nam ?: "نامشخص"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Goroh Farmakologic (Pharmacological Class)
|
||||
detail.goroh_farmakologic_cod?.let { cod ->
|
||||
// NOTE: You might need a separate API call or mapping here
|
||||
// if cod needs to be translated to a name. For now, we show the code.
|
||||
item {
|
||||
DetailRow(
|
||||
title = "کلاس فارماکولوژیک",
|
||||
value = cod.toString()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
item { // Add a divider as a separate item
|
||||
HorizontalDivider(Modifier.padding(vertical = MaterialTheme.dime.md))
|
||||
}
|
||||
|
||||
// --- 3. Usage & Mechanism xw---
|
||||
detail.mavaredmasraf?.let { usage ->
|
||||
item {
|
||||
DetailSection(title = "موارد مصرف", content = usage)
|
||||
}
|
||||
}
|
||||
|
||||
detail.mekanismtasir?.let { mechanism ->
|
||||
item {
|
||||
DetailSection(title = "مکانیسم اثر", content = mechanism)
|
||||
}
|
||||
}
|
||||
|
||||
// --- 4. Warnings & Storage ---
|
||||
detail.hoshdar?.let { warning ->
|
||||
item {
|
||||
DetailSection(
|
||||
title = "هشدارها",
|
||||
content = warning,
|
||||
isWarning = true
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
detail.sharayetnegahdari?.let { storage ->
|
||||
item {
|
||||
DetailSection(
|
||||
title = "شرایط نگهداری",
|
||||
content = storage
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// --- Helper Composables for Clean Code ---
|
||||
|
||||
@Composable
|
||||
fun DetailRow(
|
||||
title: String,
|
||||
value: String
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = MaterialTheme.dime.sm),
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Text(
|
||||
text = title,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
textAlign = TextAlign.Right
|
||||
)
|
||||
Text(
|
||||
text = value,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
textAlign = TextAlign.End,
|
||||
// Ensure text takes up available space and doesn't push other elements.
|
||||
// Using weight(1f, fill = false) is good for aligning to the end if there's space.
|
||||
modifier = Modifier.weight(1f, fill = false)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DetailSection(
|
||||
title: String,
|
||||
content: String,
|
||||
isWarning: Boolean = false
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = MaterialTheme.dime.md)
|
||||
) {
|
||||
Text(
|
||||
text = title,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = if (isWarning) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.primary,
|
||||
fontWeight = FontWeight.Bold,
|
||||
modifier = Modifier.padding(bottom = MaterialTheme.dime.sm)
|
||||
)
|
||||
// Use a text block that respects line breaks (usually done with \n)
|
||||
Text(
|
||||
text = content,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
// Consider adding softWrap = true if content might overflow
|
||||
// softWrap = true
|
||||
)
|
||||
}
|
||||
// Add divider after each section for better visual separation
|
||||
HorizontalDivider(Modifier.padding(top = MaterialTheme.dime.sm))
|
||||
}
|
||||
|
||||
// Make sure your DrugDetailState in ViewModel looks something like this:
|
||||
// data class DrugDetailState(
|
||||
// val isLoading: Boolean = false,
|
||||
// val drugDetail: DrugModels? = null, // Changed to DrugModels? to match data access
|
||||
// val isDetailVisible: Boolean = false,
|
||||
// val error: String? = null
|
||||
// )
|
||||
+119
@@ -0,0 +1,119 @@
|
||||
package com.approagency.pharmacy.presentation.components
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.LayoutDirection
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.approagency.pharmacy.domain.model.TestGroup
|
||||
import com.approagency.pharmacy.presentation.common.CustomModalBottomSheet
|
||||
import com.approagency.pharmacy.presentation.viewModel.TestItemState
|
||||
import com.vada.caller.ui.theme.dime
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun TestDetailSheet(state: TestItemState, testGroup: TestGroup, onDismiss: () -> Unit) {
|
||||
if (state.testItem != null) {
|
||||
val testItems by state.testItem.collectAsState(initial = emptyList())
|
||||
|
||||
CustomModalBottomSheet(
|
||||
onDismiss = onDismiss,
|
||||
content = {
|
||||
CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) {
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = MaterialTheme.dime.lg),
|
||||
contentPadding = PaddingValues(bottom = MaterialTheme.dime.lg)
|
||||
) {
|
||||
item {
|
||||
Text(
|
||||
text = testGroup.fname ?: "جزئیات آزمایش",
|
||||
style = MaterialTheme.typography.headlineMedium,
|
||||
fontWeight = FontWeight.Bold,
|
||||
modifier = Modifier.padding(bottom = MaterialTheme.dime.md)
|
||||
)
|
||||
Text(
|
||||
text = testGroup.ename ?: "N/A (English Name)",
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.padding(bottom = MaterialTheme.dime.sm)
|
||||
)
|
||||
if (testGroup.detail != null){
|
||||
Text(testGroup.detail , style = MaterialTheme.typography.bodySmall,modifier = Modifier.padding(bottom = MaterialTheme.dime.md) )
|
||||
}
|
||||
|
||||
|
||||
|
||||
HorizontalDivider(Modifier.padding(bottom = MaterialTheme.dime.md))
|
||||
}
|
||||
|
||||
if (testItems.isEmpty()) {
|
||||
item {
|
||||
|
||||
}
|
||||
} else {
|
||||
items(testItems) { testItem ->
|
||||
TestItemDetailCard(testItem = testItem)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun TestItemDetailCard(testItem: com.approagency.pharmacy.domain.model.TestItem) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = MaterialTheme.dime.md)
|
||||
) {
|
||||
Text(
|
||||
text = testItem.title ?: "بدون عنوان",
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
|
||||
if (!testItem.normalValue.isNullOrBlank()) {
|
||||
Text(
|
||||
text = "مقدار نرمال: ${testItem.normalValue}",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.padding(top = MaterialTheme.dime.xs)
|
||||
)
|
||||
}
|
||||
|
||||
if (!testItem.detail.isNullOrBlank()) {
|
||||
Text(
|
||||
text = testItem.detail,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.padding(top = MaterialTheme.dime.sm)
|
||||
)
|
||||
}
|
||||
|
||||
HorizontalDivider(
|
||||
modifier = Modifier.padding(top = MaterialTheme.dime.md),
|
||||
thickness = 0.5.dp
|
||||
)
|
||||
}
|
||||
}
|
||||
+90
@@ -0,0 +1,90 @@
|
||||
package com.approagency.pharmacy.presentation.components
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.twotone.KeyboardArrowLeft
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.AbsoluteAlignment
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextDirection
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import com.approagency.pharmacy.domain.model.TestGroup
|
||||
import com.approagency.pharmacy.presentation.common.CustomBox
|
||||
import com.vada.caller.ui.theme.dime
|
||||
|
||||
|
||||
@Composable
|
||||
fun TestGroupItemContent(
|
||||
group: TestGroup,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
|
||||
CustomBox(
|
||||
modifier = Modifier
|
||||
.padding(bottom = MaterialTheme.dime.sm)
|
||||
.clickable(
|
||||
indication = null,
|
||||
interactionSource = remember { MutableInteractionSource() }
|
||||
) {
|
||||
onClick()
|
||||
},
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.padding(all = MaterialTheme.dime.lg),
|
||||
) {
|
||||
Column (
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.Start
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalAlignment = AbsoluteAlignment.Right
|
||||
// horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Text(group.fname ?: "" , style = MaterialTheme.typography.labelLarge , textAlign = TextAlign.Right )
|
||||
Text(group.ename ?: "" , style = MaterialTheme.typography.bodySmall , textAlign = TextAlign.Left)
|
||||
Spacer(modifier = Modifier.padding(vertical = MaterialTheme.dime.xs))
|
||||
Text(group.detail ?: "" , style = MaterialTheme.typography.bodySmall.copy(
|
||||
textDirection = TextDirection.Rtl
|
||||
) , textAlign = TextAlign.Right , maxLines = 2 , softWrap = true , overflow = TextOverflow.Ellipsis)
|
||||
}
|
||||
Row (
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.Center
|
||||
) {
|
||||
TextButton(
|
||||
onClick = {
|
||||
onClick()
|
||||
}
|
||||
) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.Center
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.TwoTone.KeyboardArrowLeft
|
||||
, contentDescription = "see more"
|
||||
)
|
||||
Text("مشاهده جزییات")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,504 @@
|
||||
package com.approagency.pharmacy.presentation.components
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.Divider
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.approagency.pharmacy.domain.model.DrugDetail
|
||||
import com.approagency.pharmacy.presentation.common.CustomModalBottomSheet
|
||||
import com.approagency.pharmacy.presentation.common.ErrorState
|
||||
import com.approagency.pharmacy.presentation.viewModel.DrugDetailViewModel
|
||||
import com.approagency.pharmacy.presentation.viewModel.DrugDetailYabState
|
||||
import com.vada.caller.ui.theme.LocalDime
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun DrugDetailBottomSheet(
|
||||
detailUrl: String?,
|
||||
onDismiss: () -> Unit,
|
||||
viewModel: DrugDetailViewModel
|
||||
) {
|
||||
val dime = LocalDime.current
|
||||
val detailState by viewModel.detailState.collectAsStateWithLifecycle()
|
||||
|
||||
// Load detail when sheet opens with a valid URL
|
||||
LaunchedEffect(detailUrl) {
|
||||
if (detailUrl != null) {
|
||||
viewModel.loadDrugDetail(detailUrl)
|
||||
}
|
||||
}
|
||||
|
||||
// Reset when sheet is closed
|
||||
LaunchedEffect(detailUrl) {
|
||||
if (detailUrl == null) {
|
||||
viewModel.reset()
|
||||
}
|
||||
}
|
||||
|
||||
if (detailUrl != null) {
|
||||
CustomModalBottomSheet(
|
||||
onDismiss = {
|
||||
onDismiss()
|
||||
viewModel.reset()
|
||||
}
|
||||
) {
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
color = MaterialTheme.colorScheme.surface
|
||||
) {
|
||||
when (val state = detailState) {
|
||||
DrugDetailYabState.Idle -> {
|
||||
// Initial state, nothing to show yet
|
||||
}
|
||||
|
||||
DrugDetailYabState.Loading -> {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(400.dp)
|
||||
.padding(dime.xl),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
CircularProgressIndicator(
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
Spacer(modifier = Modifier.height(dime.md))
|
||||
Text(
|
||||
text = "در حال دریافت اطلاعات دارو...",
|
||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
is DrugDetailYabState.Success -> {
|
||||
DrugDetailContent(
|
||||
drugDetail = state.drugDetail,
|
||||
modifier = Modifier.padding(bottom = dime.lg)
|
||||
)
|
||||
}
|
||||
|
||||
is DrugDetailYabState.Error -> {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(300.dp)
|
||||
.padding(dime.xl),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
ErrorState(
|
||||
message = state.message,
|
||||
onRetry = { viewModel.loadDrugDetail(detailUrl) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DrugDetailContent(
|
||||
drugDetail: DrugDetail,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val dime = LocalDime.current
|
||||
var selectedTab by remember { mutableStateOf(0) }
|
||||
val tabs = listOf("عمومی", "تخصصی", "اشکال دارویی", "اسامی تجاری")
|
||||
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = dime.md)
|
||||
) {
|
||||
// Header with close button
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = dime.md),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.weight(1f)
|
||||
) {
|
||||
Text(
|
||||
text = drugDetail.persianName,
|
||||
fontSize = 18.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
if (drugDetail.englishName.isNotEmpty()) {
|
||||
Text(
|
||||
text = drugDetail.englishName,
|
||||
fontSize = 14.sp,
|
||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Categories
|
||||
if (drugDetail.drugClass != null || drugDetail.therapeuticClass != null) {
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = dime.sm),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.3f)
|
||||
),
|
||||
shape = RoundedCornerShape(dime.sm)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(dime.sm)
|
||||
) {
|
||||
drugDetail.drugClass?.let {
|
||||
Row {
|
||||
Text(
|
||||
text = "طبقه بندی: ",
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 13.sp
|
||||
)
|
||||
Text(
|
||||
text = it,
|
||||
fontSize = 13.sp,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
}
|
||||
drugDetail.therapeuticClass?.let {
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Row {
|
||||
Text(
|
||||
text = "طبقه درمانی: ",
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 13.sp
|
||||
)
|
||||
Text(
|
||||
text = it,
|
||||
fontSize = 13.sp,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Tabs
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = dime.sm)
|
||||
) {
|
||||
tabs.forEachIndexed { index, title ->
|
||||
androidx.compose.material3.Tab(
|
||||
selected = selectedTab == index,
|
||||
onClick = { selectedTab = index },
|
||||
modifier = Modifier.weight(1f),
|
||||
text = {
|
||||
Text(
|
||||
text = title,
|
||||
fontSize = 13.sp,
|
||||
fontWeight = if (selectedTab == index) FontWeight.Bold else FontWeight.Normal
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Divider(color = MaterialTheme.colorScheme.outline.copy(alpha = 0.3f))
|
||||
|
||||
// Tab content
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(500.dp)
|
||||
.padding(vertical = dime.md)
|
||||
) {
|
||||
when (selectedTab) {
|
||||
0 -> {
|
||||
// General Information
|
||||
item {
|
||||
GeneralInfoTab(drugDetail)
|
||||
}
|
||||
}
|
||||
1 -> {
|
||||
// Specialized Information
|
||||
drugDetail.usage?.let {
|
||||
item {
|
||||
InfoSection(title = "موارد مصرف", content = it)
|
||||
}
|
||||
}
|
||||
drugDetail.mechanism?.let {
|
||||
item {
|
||||
InfoSection(title = "مکانیسم اثر", content = it)
|
||||
}
|
||||
}
|
||||
drugDetail.contraindications?.let {
|
||||
item {
|
||||
InfoSection(title = "موارد منع مصرف", content = it)
|
||||
}
|
||||
}
|
||||
drugDetail.sideEffects?.let {
|
||||
item {
|
||||
InfoSection(title = "عوارض جانبی", content = it)
|
||||
}
|
||||
}
|
||||
drugDetail.interactions?.let {
|
||||
item {
|
||||
InfoSection(title = "تداخلات دارویی", content = it)
|
||||
}
|
||||
}
|
||||
drugDetail.warnings?.let {
|
||||
item {
|
||||
InfoSection(title = "هشدارها", content = it)
|
||||
}
|
||||
}
|
||||
drugDetail.recommendations?.let {
|
||||
item {
|
||||
InfoSection(title = "توصیههای دارویی", content = it)
|
||||
}
|
||||
}
|
||||
}
|
||||
2 -> {
|
||||
// Dosage Forms
|
||||
if (drugDetail.dosageForms.isNotEmpty()) {
|
||||
item {
|
||||
DosageFormsSection(dosageForms = drugDetail.dosageForms)
|
||||
}
|
||||
}
|
||||
}
|
||||
3 -> {
|
||||
// Brand Names
|
||||
if (drugDetail.brandNames.isNotEmpty()) {
|
||||
item {
|
||||
BrandNamesSection(brandNames = drugDetail.brandNames)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun GeneralInfoTab(drugDetail: DrugDetail) {
|
||||
val dime = LocalDime.current
|
||||
|
||||
Column {
|
||||
// Pregnancy Info
|
||||
if (drugDetail.pregnancyCategory != null || drugDetail.pregnancyDescription != null) {
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = dime.md),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = Color(0xFFFFF3E0)
|
||||
)
|
||||
) {
|
||||
Column(modifier = Modifier.padding(dime.md)) {
|
||||
Text(
|
||||
text = "مصرف در بارداری",
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 16.sp,
|
||||
color = Color(0xFFE65100)
|
||||
)
|
||||
Spacer(modifier = Modifier.height(dime.xs))
|
||||
drugDetail.pregnancyCategory?.let {
|
||||
Text(
|
||||
text = "گروه: $it",
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.Medium
|
||||
)
|
||||
}
|
||||
drugDetail.pregnancyDescription?.let {
|
||||
Spacer(modifier = Modifier.height(dime.xs))
|
||||
Text(
|
||||
text = it,
|
||||
fontSize = 13.sp,
|
||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.8f)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Usage
|
||||
drugDetail.usage?.let {
|
||||
InfoSection(title = "موارد مصرف", content = it)
|
||||
}
|
||||
|
||||
// Contraindications
|
||||
drugDetail.contraindications?.let {
|
||||
InfoSection(title = "موارد منع مصرف", content = it)
|
||||
}
|
||||
|
||||
// Side Effects
|
||||
drugDetail.sideEffects?.let {
|
||||
InfoSection(title = "عوارض جانبی", content = it)
|
||||
}
|
||||
|
||||
// Warnings
|
||||
drugDetail.warnings?.let {
|
||||
InfoSection(title = "هشدارها", content = it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun InfoSection(
|
||||
title: String,
|
||||
content: String,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val dime = LocalDime.current
|
||||
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = dime.md)
|
||||
) {
|
||||
Text(
|
||||
text = title,
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 16.sp,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
Spacer(modifier = Modifier.height(dime.xs))
|
||||
Divider(color = MaterialTheme.colorScheme.outline.copy(alpha = 0.3f))
|
||||
Spacer(modifier = Modifier.height(dime.xs))
|
||||
Text(
|
||||
text = content,
|
||||
fontSize = 14.sp,
|
||||
lineHeight = 22.sp,
|
||||
textAlign = TextAlign.Justify,
|
||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.9f)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DosageFormsSection(dosageForms: List<com.approagency.pharmacy.domain.model.DosageForm>) {
|
||||
val dime = LocalDime.current
|
||||
|
||||
Column {
|
||||
Text(
|
||||
text = "اشکال دارویی",
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 16.sp,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.padding(bottom = dime.sm)
|
||||
)
|
||||
|
||||
dosageForms.forEach { form ->
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = dime.sm),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
|
||||
) {
|
||||
Column(modifier = Modifier.padding(dime.sm)) {
|
||||
Text(
|
||||
text = form.persianName,
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 14.sp
|
||||
)
|
||||
Text(
|
||||
text = form.englishName,
|
||||
fontSize = 12.sp,
|
||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f)
|
||||
)
|
||||
Row(
|
||||
modifier = Modifier.padding(top = dime.xs),
|
||||
horizontalArrangement = Arrangement.spacedBy(dime.sm)
|
||||
) {
|
||||
if (form.isHighRisk) {
|
||||
Card(
|
||||
onClick = { },
|
||||
content = { Text("پرخطر", fontSize = 10.sp) },
|
||||
|
||||
)
|
||||
}
|
||||
if (form.isVital) {
|
||||
Card(
|
||||
onClick = { },
|
||||
content = { Text("حیاتی", fontSize = 10.sp) },
|
||||
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun BrandNamesSection(brandNames: List<com.approagency.pharmacy.domain.model.BrandName>) {
|
||||
val dime = LocalDime.current
|
||||
|
||||
Column {
|
||||
Text(
|
||||
text = "اسامی تجاری",
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 16.sp,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.padding(bottom = dime.sm)
|
||||
)
|
||||
|
||||
brandNames.forEach { brand ->
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = dime.sm),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = 1.dp)
|
||||
) {
|
||||
Column(modifier = Modifier.padding(dime.sm)) {
|
||||
Text(
|
||||
text = brand.persianName,
|
||||
fontWeight = FontWeight.Medium,
|
||||
fontSize = 14.sp
|
||||
)
|
||||
brand.manufacturer?.let {
|
||||
Text(
|
||||
text = "تولید کننده: $it",
|
||||
fontSize = 12.sp,
|
||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,776 @@
|
||||
package com.approagency.pharmacy.presentation.screens
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Info
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.Divider
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Tab
|
||||
import androidx.compose.material3.TabRow
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.LayoutDirection
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.navigation.NavController
|
||||
import com.approagency.pharmacy.domain.model.DrugDetail
|
||||
import com.approagency.pharmacy.presentation.common.ErrorState
|
||||
import com.approagency.pharmacy.presentation.viewModel.DrugDetailViewModel
|
||||
import com.approagency.pharmacy.presentation.viewModel.DrugDetailYabState
|
||||
import com.vada.caller.ui.theme.LocalDime
|
||||
import org.koin.androidx.compose.koinViewModel
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun DrugDetailScreen(
|
||||
navController: NavController,
|
||||
detailUrl: String,
|
||||
viewModel: DrugDetailViewModel = koinViewModel()
|
||||
) {
|
||||
val dime = LocalDime.current
|
||||
val detailState by viewModel.detailState.collectAsStateWithLifecycle()
|
||||
var selectedTab by remember { mutableIntStateOf(0) }
|
||||
|
||||
val tabs = listOf("عمومی", "تخصصی", "اشکال دارویی", "اسامی تجاری")
|
||||
|
||||
LaunchedEffect(detailUrl) {
|
||||
viewModel.loadDrugDetail(detailUrl)
|
||||
}
|
||||
|
||||
CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) {
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = {
|
||||
when (val state = detailState) {
|
||||
is DrugDetailYabState.Success -> {
|
||||
Column {
|
||||
Text(
|
||||
text = state.drugDetail.persianName,
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
)
|
||||
if (state.drugDetail.englishName.isNotEmpty()) {
|
||||
Text(
|
||||
text = state.drugDetail.englishName.take(30),
|
||||
fontSize = 11.sp,
|
||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f),
|
||||
maxLines = 1
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
else -> {
|
||||
Text("جزئیات دارو", fontSize = 16.sp)
|
||||
}
|
||||
}
|
||||
},
|
||||
navigationIcon = {
|
||||
IconButton(onClick = { navController.popBackStack() }) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||
contentDescription = "بازگشت"
|
||||
)
|
||||
}
|
||||
},
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = MaterialTheme.colorScheme.surface
|
||||
)
|
||||
)
|
||||
}
|
||||
) { paddingValues ->
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues)
|
||||
) {
|
||||
when (val state = detailState) {
|
||||
DrugDetailYabState.Idle -> {}
|
||||
|
||||
DrugDetailYabState.Loading -> {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
CircularProgressIndicator(
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.size(48.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.height(dime.md))
|
||||
Text(
|
||||
text = "در حال دریافت اطلاعات دارو...",
|
||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
is DrugDetailYabState.Success -> {
|
||||
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,
|
||||
containerColor = MaterialTheme.colorScheme.surface,
|
||||
contentColor = MaterialTheme.colorScheme.primary
|
||||
) {
|
||||
tabs.forEachIndexed { index, title ->
|
||||
Tab(
|
||||
selected = selectedTab == index,
|
||||
onClick = { selectedTab = index },
|
||||
text = {
|
||||
Text(
|
||||
text = title,
|
||||
fontSize = 13.sp,
|
||||
fontWeight = if (selectedTab == index) FontWeight.Bold else FontWeight.Normal,
|
||||
maxLines = 1
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Tab Content
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(horizontal = dime.md)
|
||||
.padding(top = dime.md),
|
||||
verticalArrangement = Arrangement.spacedBy(dime.md)
|
||||
) {
|
||||
when (selectedTab) {
|
||||
0 -> {
|
||||
item { GeneralInfoTabContent(drugDetail) }
|
||||
}
|
||||
|
||||
1 -> {
|
||||
item { SpecializedInfoTabContent(drugDetail) }
|
||||
}
|
||||
|
||||
2 -> {
|
||||
item { DosageFormsTabContent(drugDetail) }
|
||||
}
|
||||
|
||||
3 -> {
|
||||
item { BrandNamesTabContent(drugDetail) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
is DrugDetailYabState.Error -> {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
ErrorState(
|
||||
message = state.message,
|
||||
onRetry = { viewModel.loadDrugDetail(detailUrl) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ManufacturerInfoCard(
|
||||
manufacturer: String,
|
||||
genericInfo: com.approagency.pharmacy.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(
|
||||
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) {
|
||||
val dime = LocalDime.current
|
||||
|
||||
Column(verticalArrangement = Arrangement.spacedBy(dime.md)) {
|
||||
|
||||
// برای صفحات برند، اطلاعات تخصصی معمولاً در قسمت brandAttrPersDesc است
|
||||
// که در پارسر ما به صورت usage, contraindications, sideEffects, warnings, recommendations ذخیره شده
|
||||
|
||||
drugDetail.usage?.let {
|
||||
InfoSectionCard(title = "موارد مصرف", content = it)
|
||||
}
|
||||
drugDetail.mechanism?.let {
|
||||
InfoSectionCard(title = "مکانیسم اثر", content = it)
|
||||
}
|
||||
drugDetail.pharmacokinetics?.let {
|
||||
InfoSectionCard(title = "فارماکوکینتیک", content = it)
|
||||
}
|
||||
drugDetail.contraindications?.let {
|
||||
InfoSectionCard(title = "موارد منع مصرف", content = it)
|
||||
}
|
||||
drugDetail.sideEffects?.let {
|
||||
InfoSectionCard(title = "عوارض جانبی", content = it)
|
||||
}
|
||||
drugDetail.interactions?.let {
|
||||
InfoSectionCard(title = "تداخلات دارویی", content = it)
|
||||
}
|
||||
drugDetail.warnings?.let {
|
||||
InfoSectionCard(title = "هشدارها", content = it)
|
||||
}
|
||||
drugDetail.recommendations?.let {
|
||||
InfoSectionCard(title = "توصیههای دارویی", content = it)
|
||||
}
|
||||
|
||||
// اگر هیچ اطلاعات تخصصی وجود نداشت
|
||||
if (drugDetail.usage == null &&
|
||||
drugDetail.mechanism == null &&
|
||||
drugDetail.contraindications == null &&
|
||||
drugDetail.sideEffects == null &&
|
||||
drugDetail.warnings == null
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(32.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
text = if (drugDetail.isGeneric)
|
||||
"اطلاعات تخصصی برای این دارو ثبت نشده است"
|
||||
else
|
||||
"اطلاعات تخصصی برای این برند ثبت نشده است.\nبرای مشاهده اطلاعات کامل، به صفحه داروی ژنریک مراجعه کنید.",
|
||||
fontSize = 13.sp,
|
||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f),
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DosageFormsTabContent(drugDetail: DrugDetail) {
|
||||
val dime = LocalDime.current
|
||||
val dosageForms = drugDetail.dosageForms
|
||||
|
||||
if (dosageForms.isEmpty()) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(32.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
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 = 1.dp),
|
||||
shape = RoundedCornerShape(dime.sm)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(dime.md),
|
||||
verticalArrangement = Arrangement.spacedBy(dime.xs)
|
||||
) {
|
||||
Text(
|
||||
text = form.persianName,
|
||||
fontWeight = FontWeight.Medium,
|
||||
fontSize = 14.sp
|
||||
)
|
||||
if (form.englishName.isNotBlank()) {
|
||||
Text(
|
||||
text = form.englishName,
|
||||
fontSize = 11.sp,
|
||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f)
|
||||
)
|
||||
}
|
||||
|
||||
// نشانگرهای خطر
|
||||
if (form.isHighRisk || form.isVital) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(dime.xs)
|
||||
) {
|
||||
if (form.isHighRisk) {
|
||||
androidx.compose.material3.Surface(
|
||||
shape = RoundedCornerShape(4.dp),
|
||||
color = Color(0xFFFFEBEE)
|
||||
) {
|
||||
Text(
|
||||
text = "پرخطر",
|
||||
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)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun BrandNamesTabContent(drugDetail: DrugDetail) {
|
||||
val dime = LocalDime.current
|
||||
|
||||
if (drugDetail.isGeneric && drugDetail.brandNames.isEmpty()) {
|
||||
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 if (drugDetail.isGeneric && drugDetail.brandNames.isNotEmpty()) {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(dime.sm)) {
|
||||
drugDetail.brandNames.forEach { brand ->
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = 1.dp),
|
||||
shape = RoundedCornerShape(dime.sm)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(dime.md),
|
||||
verticalArrangement = Arrangement.spacedBy(dime.xs)
|
||||
) {
|
||||
Text(
|
||||
text = brand.persianName,
|
||||
fontWeight = FontWeight.Medium,
|
||||
fontSize = 14.sp
|
||||
)
|
||||
brand.manufacturer?.let {
|
||||
Row {
|
||||
Text(
|
||||
text = "تولید کننده: ",
|
||||
fontSize = 12.sp,
|
||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f)
|
||||
)
|
||||
Text(
|
||||
text = it,
|
||||
fontSize = 12.sp,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
}
|
||||
brand.importer?.let {
|
||||
Row {
|
||||
Text(
|
||||
text = "وارد کننده: ",
|
||||
fontSize = 12.sp,
|
||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f)
|
||||
)
|
||||
Text(
|
||||
text = it,
|
||||
fontSize = 12.sp,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} 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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun InfoSectionCard(
|
||||
title: String,
|
||||
content: String,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val dime = LocalDime.current
|
||||
|
||||
Card(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = 1.dp),
|
||||
shape = RoundedCornerShape(dime.sm)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(dime.md),
|
||||
verticalArrangement = Arrangement.spacedBy(dime.xs)
|
||||
) {
|
||||
Text(
|
||||
text = title,
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 15.sp,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
Divider(color = MaterialTheme.colorScheme.outline.copy(alpha = 0.2f))
|
||||
Text(
|
||||
text = content,
|
||||
fontSize = 13.sp,
|
||||
lineHeight = 20.sp,
|
||||
textAlign = TextAlign.Justify,
|
||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.85f)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,210 @@
|
||||
package com.approagency.pharmacy.presentation.screens
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.KeyboardArrowRight
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.navigation.NavController
|
||||
import com.approagency.pharmacy.domain.model.DrugSearchParams
|
||||
import com.approagency.pharmacy.presentation.common.CustomModalDialog
|
||||
import com.approagency.pharmacy.presentation.common.Loading
|
||||
import com.approagency.pharmacy.presentation.components.DarmanContent
|
||||
import com.approagency.pharmacy.presentation.components.DrugDetailSheet
|
||||
import com.approagency.pharmacy.presentation.components.DrugListContent
|
||||
import com.approagency.pharmacy.presentation.components.EmptyStateContent
|
||||
import com.approagency.pharmacy.presentation.viewModel.CombineState
|
||||
import com.approagency.pharmacy.presentation.viewModel.HomeEvent
|
||||
import com.approagency.pharmacy.presentation.viewModel.HomeViewModel
|
||||
import com.vada.caller.ui.theme.dime
|
||||
import kotlinx.coroutines.flow.SharedFlow
|
||||
import org.koin.androidx.compose.koinViewModel
|
||||
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun HomeScreen(
|
||||
navController : NavController,
|
||||
modifier: Modifier,
|
||||
viewModel: HomeViewModel = koinViewModel()
|
||||
) {
|
||||
val state by viewModel.uiState.collectAsState()
|
||||
val focusManager = LocalFocusManager.current
|
||||
val keyboardController = LocalSoftwareKeyboardController.current
|
||||
if (state.drugDetailState.isDetailVisible) {
|
||||
DrugDetailSheet(
|
||||
viewModel = viewModel,
|
||||
state = state.drugDetailState
|
||||
)
|
||||
}
|
||||
HomeContent(
|
||||
modifier = modifier,
|
||||
state = state,
|
||||
viewModelEvent = viewModel.event,
|
||||
onSearch = { value ->
|
||||
if (value is Int) {
|
||||
viewModel.searchDrugs(DrugSearchParams(healGroup = value))
|
||||
} else if (value is String) {
|
||||
viewModel.searchDrugs(DrugSearchParams(query = value))
|
||||
}
|
||||
keyboardController?.hide()
|
||||
focusManager.clearFocus()},
|
||||
onRetry = {
|
||||
viewModel.getDarmani()
|
||||
},
|
||||
onDrugClick = { drugId -> viewModel.drugDetail(drugId , allDrugs = state.drugSearchState.drugsData) }
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun HomeContent(
|
||||
state: CombineState,
|
||||
viewModelEvent: SharedFlow<HomeEvent>,
|
||||
onSearch: (Any) -> Unit,
|
||||
onRetry: () -> Unit,
|
||||
onDrugClick: (Int) -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
var searchText by remember { mutableStateOf("") }
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxSize().padding(horizontal = MaterialTheme.dime.md)
|
||||
.padding(top = MaterialTheme.dime.md),
|
||||
horizontalAlignment = Alignment.Start,
|
||||
) {
|
||||
Column (
|
||||
verticalArrangement = Arrangement.Center
|
||||
){
|
||||
// CustomTextFilled(
|
||||
// value = searchText,
|
||||
// onValueChange = { searchText = it },
|
||||
// onSearch = { query ->
|
||||
// if (query.isNotBlank()) {
|
||||
// onSearch(query)
|
||||
// }
|
||||
// },
|
||||
// placeholder = "جستجوی دارو",
|
||||
// showClearButton = true,
|
||||
// showSearchButton = true,
|
||||
// autoSearch = false, // Set to true if you want search while typing
|
||||
// height = 45
|
||||
// )
|
||||
// Spacer(modifier = Modifier.height(MaterialTheme.dime.xs))
|
||||
// PrimaryButton(
|
||||
// text = "جستجو",
|
||||
// height = 40,
|
||||
// isLoading = state.drugSearchState.isLoading,
|
||||
// onClick = {
|
||||
// if (searchText.isNotBlank()) {
|
||||
// onSearch(searchText)
|
||||
// }
|
||||
// }
|
||||
// )
|
||||
// Spacer(modifier = Modifier.height(MaterialTheme.dime.xs))
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
|
||||
// Always draw the data if available
|
||||
if (state.drugSearchState.drugsData != null && !state.showDarmanList) {
|
||||
Column {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.End,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
TextButton(
|
||||
onClick = {
|
||||
onRetry()
|
||||
}
|
||||
) {
|
||||
Text("مشاهده تمامی گروه های دارویی")
|
||||
Icon(
|
||||
imageVector = Icons.Default.KeyboardArrowRight,
|
||||
contentDescription = "back"
|
||||
)
|
||||
Spacer(modifier = Modifier.width(MaterialTheme.dime.sm))
|
||||
}
|
||||
}
|
||||
DrugListContent(
|
||||
drugsData = state.drugSearchState.drugsData,
|
||||
onDrugClick = onDrugClick
|
||||
)
|
||||
}
|
||||
}
|
||||
else if (state.darmanState.getDarmani != null ){
|
||||
Column {
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.End
|
||||
) {
|
||||
Text("لیست گروه های دارویی" , style = MaterialTheme.typography.labelLarge.copy(
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
) ,modifier= Modifier.padding(MaterialTheme.dime.md))
|
||||
}
|
||||
|
||||
DarmanContent(
|
||||
darmanData = state.darmanState.getDarmani,
|
||||
onDarmanClick = { value ->
|
||||
onSearch(value)
|
||||
},
|
||||
onRetryClick = {
|
||||
onRetry()
|
||||
}
|
||||
)
|
||||
}
|
||||
} else {
|
||||
EmptyStateContent()
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Draw the loading dialog on top if needed
|
||||
if (state.drugSearchState.isLoading || state.drugDetailState.isLoading || state.darmanState.isLoading) {
|
||||
CustomModalDialog(
|
||||
onDismissRequest = { /* maybe disable dismiss while loading */ },
|
||||
content = {
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
Loading(color = MaterialTheme.colorScheme.primary)
|
||||
Spacer(modifier = Modifier.height(MaterialTheme.dime.sm))
|
||||
Text("در حال جستجو..." , textAlign = TextAlign.Right)
|
||||
}
|
||||
},
|
||||
// modifier = Modifier
|
||||
// .matchParentSize()
|
||||
// .background(MaterialTheme.colorScheme.background.copy(alpha = 0.5f))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,190 @@
|
||||
package com.approagency.pharmacy.presentation.screens
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import com.approagency.pharmacy.domain.model.TestGroup
|
||||
import com.approagency.pharmacy.domain.model.TestItem
|
||||
import com.approagency.pharmacy.presentation.common.CustomModalDialog
|
||||
import com.approagency.pharmacy.presentation.common.CustomTextFilled
|
||||
import com.approagency.pharmacy.presentation.common.Loading
|
||||
import com.approagency.pharmacy.presentation.common.PrimaryButton
|
||||
import com.approagency.pharmacy.presentation.components.RetryContent
|
||||
import com.approagency.pharmacy.presentation.components.SearchResultsContent
|
||||
import com.approagency.pharmacy.presentation.components.TestDetailSheet
|
||||
import com.approagency.pharmacy.presentation.components.TestGroupItemContent
|
||||
import com.approagency.pharmacy.presentation.viewModel.LabViewModel
|
||||
import com.vada.caller.ui.theme.dime
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import org.koin.androidx.compose.koinViewModel
|
||||
|
||||
@Composable
|
||||
fun LabScreen(
|
||||
modifier: Modifier = Modifier,
|
||||
viewModel: LabViewModel = koinViewModel()
|
||||
) {
|
||||
val testGroupsState by viewModel.testGroups.collectAsState()
|
||||
val testItemsState by viewModel.testItems.collectAsState()
|
||||
val searchResultsState by viewModel.searchResults.collectAsState()
|
||||
|
||||
val isLoading = testGroupsState.isLoading || testItemsState.isLoading || searchResultsState.isLoading
|
||||
val error = testGroupsState.error ?: searchResultsState.error
|
||||
val testGroups = testGroupsState.testGroup
|
||||
|
||||
var selectedGroup by remember { mutableStateOf<TestGroup?>(null) }
|
||||
var selectedItem by remember { mutableStateOf<TestItem?>(null) }
|
||||
var showSheet by remember { mutableStateOf(false) }
|
||||
var searchText by remember { mutableStateOf("") }
|
||||
Column(modifier = modifier.fillMaxSize().padding(horizontal = MaterialTheme.dime.md)
|
||||
.padding(top = MaterialTheme.dime.md),) {
|
||||
CustomTextFilled(
|
||||
value = searchText,
|
||||
onValueChange = {
|
||||
searchText = it
|
||||
viewModel.updateSearchQuery(it)
|
||||
},
|
||||
onSearch = { query ->
|
||||
if (query.isNotBlank()) {
|
||||
viewModel.updateSearchQuery(query)
|
||||
}
|
||||
},
|
||||
placeholder = "جستجو در گروهها و آیتمهای آزمایشگاهی",
|
||||
showClearButton = true,
|
||||
showSearchButton = true,
|
||||
autoSearch = true,
|
||||
height = 45
|
||||
)
|
||||
Spacer(modifier = Modifier.height(MaterialTheme.dime.xs))
|
||||
PrimaryButton(
|
||||
text = "جستجو",
|
||||
height = 40,
|
||||
isLoading =false,
|
||||
onClick = {}
|
||||
)
|
||||
Spacer(modifier = Modifier.height(MaterialTheme.dime.xs))
|
||||
if (searchResultsState.query.isNotBlank()) {
|
||||
// Search Results Section
|
||||
SearchResultsContent(
|
||||
searchResult = searchResultsState.searchResults,
|
||||
isLoading = searchResultsState.isLoading,
|
||||
error = searchResultsState.error,
|
||||
onGroupClick = { group ->
|
||||
selectedGroup = group
|
||||
viewModel.getItemByGroupId(group.id)
|
||||
showSheet = true
|
||||
},
|
||||
onItemClick = { item ->
|
||||
selectedItem = item
|
||||
// You can show item detail sheet here
|
||||
// For now, we'll show the parent group
|
||||
viewModel.getItemByGroupId(item.groupId)
|
||||
showSheet = true
|
||||
},
|
||||
onClearSearch = {
|
||||
searchText = ""
|
||||
viewModel.clearSearch()
|
||||
viewModel.loadTestGroups()
|
||||
}
|
||||
)
|
||||
} else if(error != null){
|
||||
RetryContent(modifier = modifier , error = error , onClick = {
|
||||
viewModel.loadTestGroups()
|
||||
})
|
||||
}
|
||||
else if (testGroups != null){
|
||||
TestGroupsList(
|
||||
testGroupsFlow = testGroups,
|
||||
viewModel = viewModel,
|
||||
onGroupClick = { group ->
|
||||
selectedGroup = group
|
||||
viewModel.getItemByGroupId(group.id)
|
||||
showSheet = true
|
||||
}
|
||||
)
|
||||
}
|
||||
else {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text("هیچ دادهای موجود نیست")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
if (isLoading) {
|
||||
CustomModalDialog(
|
||||
onDismissRequest = { /* maybe disable dismiss while loading */ },
|
||||
content = {
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
Loading(color = MaterialTheme.colorScheme.primary)
|
||||
Spacer(modifier = Modifier.height(MaterialTheme.dime.sm))
|
||||
Text("در حال جستجو..." , textAlign = TextAlign.Right)
|
||||
}
|
||||
},
|
||||
// modifier = Modifier
|
||||
// .matchParentSize()
|
||||
// .background(MaterialTheme.colorScheme.background.copy(alpha = 0.5f))
|
||||
)
|
||||
}
|
||||
if (showSheet && selectedGroup != null) {
|
||||
TestDetailSheet(
|
||||
state = testItemsState,
|
||||
testGroup = selectedGroup!!,
|
||||
onDismiss = { showSheet = false }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun TestGroupsList(
|
||||
testGroupsFlow: Flow<List<TestGroup>>,
|
||||
viewModel: LabViewModel,
|
||||
onGroupClick: (TestGroup) -> Unit
|
||||
) {
|
||||
val testGroups by testGroupsFlow.collectAsState(initial = emptyList())
|
||||
|
||||
if (testGroups.isEmpty()) {
|
||||
CustomModalDialog(
|
||||
onDismissRequest = { /* maybe disable dismiss while loading */ },
|
||||
content = {
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
Loading(color = MaterialTheme.colorScheme.primary)
|
||||
Spacer(modifier = Modifier.height(MaterialTheme.dime.sm))
|
||||
Text("در حال جستجو..." , textAlign = TextAlign.Right)
|
||||
}
|
||||
},
|
||||
// modifier = Modifier
|
||||
// .matchParentSize()
|
||||
// .background(MaterialTheme.colorScheme.background.copy(alpha = 0.5f))
|
||||
)
|
||||
} else {
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize()
|
||||
.fillMaxSize().padding( vertical =MaterialTheme.dime.md )
|
||||
) {
|
||||
items(testGroups) { group ->
|
||||
TestGroupItemContent(
|
||||
group = group,
|
||||
onClick = { onGroupClick(group) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
package com.approagency.pharmacy.presentation.screens
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.WindowInsetsSides
|
||||
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.navigationBars
|
||||
import androidx.compose.foundation.layout.only
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Build
|
||||
import androidx.compose.material.icons.filled.Search
|
||||
import androidx.compose.material.icons.rounded.Refresh
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.NavigationBar
|
||||
import androidx.compose.material3.NavigationBarItem
|
||||
import androidx.compose.material3.NavigationBarItemDefaults
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.ScaffoldDefaults
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.LayoutDirection
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.navigation.NavHostController
|
||||
@SuppressLint("ConfigurationScreenWidthHeight")
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun RootScreen(navHostController: NavHostController, modifier: Modifier){
|
||||
val pages = listOf(
|
||||
"دارو" to Icons.Default.Search,
|
||||
"آزمایش" to Icons.Default.Build,
|
||||
"سرچ" to Icons.Rounded.Refresh
|
||||
)
|
||||
var seletedTab by remember { mutableStateOf(0) }
|
||||
val context = LocalContext.current
|
||||
val activity = context as Activity
|
||||
|
||||
Scaffold (
|
||||
contentWindowInsets = ScaffoldDefaults.contentWindowInsets,
|
||||
topBar = { CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl){
|
||||
TopAppBar(
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = MaterialTheme.colorScheme.primaryContainer,
|
||||
titleContentColor = MaterialTheme.colorScheme.primary,
|
||||
),
|
||||
title = {
|
||||
Text("دستیار سلامت" , textAlign = TextAlign.Right , style = MaterialTheme.typography.titleLarge , fontWeight = FontWeight.W700 )
|
||||
}
|
||||
)
|
||||
}},
|
||||
bottomBar = {
|
||||
NavigationBar(
|
||||
containerColor = MaterialTheme.colorScheme.surface,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(110.dp).padding(bottom = 2.dp).clip(MaterialTheme.shapes.extraLarge),
|
||||
tonalElevation = 8.dp, // subtle shadow
|
||||
windowInsets = WindowInsets.navigationBars
|
||||
.only(WindowInsetsSides.Bottom)
|
||||
) {
|
||||
pages.forEachIndexed { index, item ->
|
||||
val selected = seletedTab == index
|
||||
|
||||
NavigationBarItem(
|
||||
colors = NavigationBarItemDefaults.colors(
|
||||
selectedIconColor = MaterialTheme.colorScheme.onPrimary,
|
||||
selectedTextColor = MaterialTheme.colorScheme.primary,
|
||||
indicatorColor = MaterialTheme.colorScheme.primary,
|
||||
unselectedIconColor = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
unselectedTextColor = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
),
|
||||
selected = selected,
|
||||
onClick = { seletedTab = index },
|
||||
|
||||
icon = {
|
||||
Icon(
|
||||
imageVector = item.second,
|
||||
contentDescription = item.first,
|
||||
modifier = Modifier.size(16.dp)
|
||||
)
|
||||
},
|
||||
|
||||
label = {
|
||||
Text(
|
||||
item.first,
|
||||
style = MaterialTheme.typography.labelMedium
|
||||
)
|
||||
},
|
||||
|
||||
alwaysShowLabel = true
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
) {
|
||||
padding -> when (seletedTab) {
|
||||
0 -> HomeScreen( navController = navHostController,Modifier.padding(padding).consumeWindowInsets(padding))
|
||||
1 -> LabScreen(Modifier.padding(padding))
|
||||
2 -> SearchScreen(
|
||||
navHostController,
|
||||
modifier = Modifier.padding(padding).consumeWindowInsets(padding),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,244 @@
|
||||
package com.approagency.pharmacy.presentation.screens
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.snapshotFlow
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||
import androidx.navigation.NavController
|
||||
import com.approagency.pharmacy.domain.model.DrugSearchResult
|
||||
import com.approagency.pharmacy.navigation.Screen
|
||||
import com.approagency.pharmacy.presentation.common.CustomTextFilled
|
||||
import com.approagency.pharmacy.presentation.common.EmptySearchState
|
||||
import com.approagency.pharmacy.presentation.common.EndOfListIndicator
|
||||
import com.approagency.pharmacy.presentation.common.ErrorState
|
||||
import com.approagency.pharmacy.presentation.common.LoadingMoreIndicator
|
||||
import com.approagency.pharmacy.presentation.common.PrimaryButton
|
||||
import com.approagency.pharmacy.presentation.components.DaroYabSearchResult
|
||||
import com.approagency.pharmacy.presentation.components.DrugDetailBottomSheet
|
||||
import com.approagency.pharmacy.presentation.components.PharmacyBottomSheet
|
||||
import com.approagency.pharmacy.presentation.viewModel.DrugDetailViewModel
|
||||
import com.approagency.pharmacy.presentation.viewModel.PharmacyViewModel
|
||||
import com.approgency.drug.presentation.viewModel.SearchState
|
||||
import com.approgency.drug.presentation.viewModel.SearchViewModel
|
||||
import com.vada.caller.ui.theme.LocalDime
|
||||
import com.vada.caller.ui.theme.dime
|
||||
import org.koin.androidx.compose.koinViewModel
|
||||
|
||||
|
||||
@Composable
|
||||
fun SearchScreen(
|
||||
navController: NavController,
|
||||
modifier: Modifier = Modifier,
|
||||
viewModel: SearchViewModel = koinViewModel(),
|
||||
drugDetailViewModel: DrugDetailViewModel = koinViewModel(),
|
||||
pharmacyViewModel: PharmacyViewModel = koinViewModel(),
|
||||
) {
|
||||
val dime = LocalDime.current
|
||||
val keyboardController = LocalSoftwareKeyboardController.current
|
||||
val searchText = viewModel.searchText
|
||||
val state by viewModel.searchState.collectAsState()
|
||||
val lazyListState = rememberLazyListState()
|
||||
var selectedDrugUrl by remember { mutableStateOf<String?>(null) }
|
||||
|
||||
var showPharmacyBottomSheet by remember { mutableStateOf(false) }
|
||||
var selectedDrugForPharmacy by remember { mutableStateOf<DrugSearchResult?>(null) }
|
||||
|
||||
// Auto-search for testing (remove in production)
|
||||
// LaunchedEffect(Unit) {
|
||||
// if (searchText.isEmpty()) {
|
||||
// searchText = "انتی"
|
||||
// viewModel.searchDrugs(searchText)
|
||||
// }
|
||||
// }
|
||||
if (selectedDrugUrl != null) {
|
||||
DrugDetailBottomSheet(
|
||||
detailUrl = selectedDrugUrl,
|
||||
onDismiss = { selectedDrugUrl = null },
|
||||
viewModel = drugDetailViewModel
|
||||
)
|
||||
}
|
||||
// Detect when user scrolls to the bottom to load more
|
||||
LaunchedEffect(lazyListState) {
|
||||
snapshotFlow { lazyListState.layoutInfo.visibleItemsInfo.lastOrNull()?.index }
|
||||
.collect { lastVisibleIndex ->
|
||||
if (lastVisibleIndex != null && state is SearchState.Success) {
|
||||
val successState = state as SearchState.Success
|
||||
val totalItems = successState.drugs.size
|
||||
// Load more when user is 3 items from the end
|
||||
if (lastVisibleIndex >= totalItems - 3 &&
|
||||
successState.hasMorePages &&
|
||||
!successState.isLoadingMore) {
|
||||
viewModel.loadNextPage()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column(modifier = modifier
|
||||
.fillMaxSize()
|
||||
.padding(horizontal = MaterialTheme.dime.md)
|
||||
.padding(top = MaterialTheme.dime.md)) {
|
||||
// Search input
|
||||
CustomTextFilled(
|
||||
value = searchText,
|
||||
onValueChange = { viewModel.updateSearchText(it) },
|
||||
onSearch = { query ->
|
||||
if (query.isNotBlank()) {
|
||||
keyboardController?.hide()
|
||||
viewModel.searchDrugs(searchText)
|
||||
}
|
||||
},
|
||||
placeholder = "جستجوی دارو",
|
||||
showClearButton = true,
|
||||
showSearchButton = true,
|
||||
autoSearch = true,
|
||||
height = 45
|
||||
)
|
||||
Spacer(modifier = Modifier.height(MaterialTheme.dime.xs))
|
||||
PrimaryButton(
|
||||
text = "جستجو",
|
||||
height = 40,
|
||||
isLoading = state is SearchState.Loading,
|
||||
onClick = {
|
||||
if (searchText.isNotBlank()) {
|
||||
viewModel.searchDrugs(searchText)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(MaterialTheme.dime.md))
|
||||
|
||||
if (showPharmacyBottomSheet && selectedDrugForPharmacy != null) {
|
||||
// استخراج genericDrugId از URL
|
||||
// مثال: /G-799/Fexofenadine -> 799
|
||||
val genericId = extractGenericIdFromUrl(selectedDrugForPharmacy?.detailPageUrl ?: "")
|
||||
|
||||
PharmacyBottomSheet(
|
||||
viewModel = pharmacyViewModel,
|
||||
genericDrugId = genericId,
|
||||
isOpen = showPharmacyBottomSheet,
|
||||
onDismiss = {
|
||||
showPharmacyBottomSheet = false
|
||||
selectedDrugForPharmacy = null
|
||||
}
|
||||
)
|
||||
}
|
||||
// Results area
|
||||
when (val currentState = state) {
|
||||
is SearchState.Loading -> {
|
||||
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
CircularProgressIndicator(
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
Text(
|
||||
text = "در حال جستجو...",
|
||||
modifier = Modifier.padding(top = dime.md),
|
||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
is SearchState.LoadingMore -> {
|
||||
// Show existing items with loading indicator at bottom
|
||||
LazyColumn(state = lazyListState , modifier = modifier.padding(vertical = MaterialTheme.dime.xs)) {
|
||||
items(currentState.currentItems) { drug ->
|
||||
DaroYabSearchResult(
|
||||
drug = drug,
|
||||
onClickDetail = { selectedDrug ->
|
||||
navController.navigate(
|
||||
Screen.DrugDetail.createRoute(
|
||||
selectedDrug.detailPageUrl
|
||||
)
|
||||
)
|
||||
},
|
||||
onClickDrugStore = { selectedDrug ->
|
||||
selectedDrugForPharmacy = selectedDrug
|
||||
showPharmacyBottomSheet = true
|
||||
}
|
||||
)
|
||||
}
|
||||
item {
|
||||
LoadingMoreIndicator()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
is SearchState.Success -> {
|
||||
if (currentState.drugs.isEmpty()) {
|
||||
EmptySearchState(onRetry = { viewModel.retryLastSearch() })
|
||||
} else {
|
||||
LazyColumn(state = lazyListState) {
|
||||
items(currentState.drugs) { drug ->
|
||||
DaroYabSearchResult(
|
||||
drug = drug,
|
||||
onClickDetail = { selectedDrug ->
|
||||
navController.navigate(
|
||||
Screen.DrugDetail.createRoute(
|
||||
selectedDrug.detailPageUrl
|
||||
)
|
||||
)
|
||||
},
|
||||
onClickDrugStore = { selectedDrug ->
|
||||
selectedDrugForPharmacy = selectedDrug
|
||||
showPharmacyBottomSheet = true
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// Show loading indicator at bottom when loading more
|
||||
if (currentState.isLoadingMore) {
|
||||
item {
|
||||
LoadingMoreIndicator()
|
||||
}
|
||||
}
|
||||
|
||||
// Show end of list indicator
|
||||
if (!currentState.hasMorePages && currentState.drugs.isNotEmpty()) {
|
||||
item {
|
||||
EndOfListIndicator()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
is SearchState.Error -> {
|
||||
ErrorState(
|
||||
message = currentState.message,
|
||||
onRetry = { viewModel.retryLastSearch() }
|
||||
)
|
||||
}
|
||||
|
||||
SearchState.Idle -> {
|
||||
EmptySearchState(onRetry = {})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// تابع کمکی برای استخراج ID از URL
|
||||
private fun extractGenericIdFromUrl(url: String): String {
|
||||
val pattern = "/G-(\\d+)/".toRegex()
|
||||
return pattern.find(url)?.groupValues?.get(1) ?: ""
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
package com.approagency.pharmacy.presentation.viewModel
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.approagency.pharmacy.domain.model.DrugDetail
|
||||
import com.approagency.pharmacy.domain.usecase.DrugDetailYabUseCase
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class DrugDetailViewModel(
|
||||
private val drugDetailUseCase: DrugDetailYabUseCase
|
||||
) : ViewModel() {
|
||||
|
||||
private val _detailState = MutableStateFlow<DrugDetailYabState>(DrugDetailYabState.Idle)
|
||||
val detailState: StateFlow<DrugDetailYabState> = _detailState.asStateFlow()
|
||||
|
||||
fun loadDrugDetail(detailUrl: String) {
|
||||
viewModelScope.launch {
|
||||
_detailState.value = DrugDetailYabState.Loading
|
||||
val result = drugDetailUseCase(detailUrl)
|
||||
_detailState.value = when {
|
||||
result.isSuccess -> DrugDetailYabState.Success(result.getOrNull()!!)
|
||||
else -> DrugDetailYabState.Error(result.exceptionOrNull()?.message ?: "خطا در دریافت اطلاعات")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun reset() {
|
||||
_detailState.value = DrugDetailYabState.Idle
|
||||
}
|
||||
}
|
||||
|
||||
sealed class DrugDetailYabState {
|
||||
object Idle : DrugDetailYabState()
|
||||
object Loading : DrugDetailYabState()
|
||||
data class Success(val drugDetail: DrugDetail) : DrugDetailYabState()
|
||||
data class Error(val message: String) : DrugDetailYabState()
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
package com.approagency.pharmacy.presentation.viewModel
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.approagency.pharmacy.data.dto.DarmanModel
|
||||
import com.approagency.pharmacy.data.dto.DrugListResponse
|
||||
import com.approagency.pharmacy.data.dto.DrugModels
|
||||
import com.approagency.pharmacy.domain.model.DrugSearchParams
|
||||
import com.approagency.pharmacy.domain.usecase.GetDarmanUseCase
|
||||
import com.approagency.pharmacy.domain.usecase.GetDrugDetailUseCase
|
||||
import com.approagency.pharmacy.domain.usecase.GetDrugSearchUseCase
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asSharedFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class HomeViewModel (
|
||||
private val getDrugSearchUseCase: GetDrugSearchUseCase,
|
||||
private val getDrugDetailUseCase: GetDrugDetailUseCase,
|
||||
private val getDarmanUseCase: GetDarmanUseCase
|
||||
): ViewModel() {
|
||||
private val _uiState = MutableStateFlow(CombineState())
|
||||
val uiState : StateFlow<CombineState> = _uiState
|
||||
|
||||
// Add SharedFlow for one-time events
|
||||
private val _event = MutableSharedFlow<HomeEvent>()
|
||||
val event = _event.asSharedFlow()
|
||||
init {
|
||||
getDarmani()
|
||||
}
|
||||
fun getDarmani() {
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
darmanState = it.darmanState.copy(isLoading = true),
|
||||
showDarmanList = true
|
||||
)
|
||||
}
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
val result = getDarmanUseCase.invoke()
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
darmanState = it.darmanState.copy(
|
||||
isLoading = false,
|
||||
getDarmani = result
|
||||
)
|
||||
)
|
||||
}
|
||||
}catch (e: Exception) {
|
||||
handleError(e.message)
|
||||
}catch (e: Exception) {
|
||||
handleError(e.localizedMessage ?: "Unknown error")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun searchDrugs(drugSearchParams: DrugSearchParams) {
|
||||
_uiState.update { it.copy(drugSearchState = it.drugSearchState.copy(
|
||||
isLoading = true
|
||||
) , showDarmanList = false) }
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
val result = getDrugSearchUseCase.invoke(drugSearchParams)
|
||||
println(result)
|
||||
_uiState.update { it.copy(
|
||||
drugSearchState = it.drugSearchState.copy(
|
||||
isLoading = false,
|
||||
drugsData = result
|
||||
)
|
||||
|
||||
) }
|
||||
} catch (e: Exception) {
|
||||
println(e.message)
|
||||
handleError(e.message)
|
||||
}catch (e: Exception) {
|
||||
println(e.message)
|
||||
handleError(e.localizedMessage ?: "Unknown error")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun drugDetail(cod: Int , allDrugs:Result<DrugListResponse?>?) {
|
||||
_uiState.update { it.copy(drugDetailState = it.drugDetailState.copy(
|
||||
isLoading = true
|
||||
) , showDarmanList = false) }
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
val result = getDrugDetailUseCase.invoke(cod)
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
drugDetailState = it.drugDetailState.copy(
|
||||
isLoading = false,
|
||||
drugDetail = result,
|
||||
isDetailVisible = true
|
||||
),
|
||||
drugSearchState = it.drugSearchState.copy(
|
||||
isLoading = false,
|
||||
drugsData = allDrugs
|
||||
)
|
||||
)
|
||||
}
|
||||
println(result)
|
||||
} catch (e: Exception) {
|
||||
handleError(e.message)
|
||||
}catch (e: Exception) {
|
||||
handleError(e.localizedMessage ?: "Unknown error")
|
||||
}
|
||||
}
|
||||
}
|
||||
fun closeDetail() {
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
drugDetailState = it.drugDetailState.copy(
|
||||
isDetailVisible = false,
|
||||
drugDetail = null // optional, depends if you want to keep last detail cached
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun handleError(message: String?) {
|
||||
_uiState.update { it.copy(drugSearchState = it.drugSearchState.copy(
|
||||
isLoading = false
|
||||
) , drugDetailState = it.drugDetailState.copy(
|
||||
isLoading = false
|
||||
)) }
|
||||
_event.emit(HomeEvent.ShowError(message ?: "Error occurred"))
|
||||
}
|
||||
|
||||
// Clear error when consumed
|
||||
fun clearError() {
|
||||
// _uiState.update { it.copy(error = null) }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
sealed class HomeEvent {
|
||||
data class ShowError(val message: String) : HomeEvent()
|
||||
data class ShowDetailDrug(val drugDetail:Result<DrugModels?>?): HomeEvent()
|
||||
}
|
||||
|
||||
data class CombineState(
|
||||
val drugSearchState: DrugSearchState = DrugSearchState() ,
|
||||
val drugDetailState: DrugDetailState =DrugDetailState(),
|
||||
val darmanState: GetDarmaniState = GetDarmaniState(),
|
||||
val showDarmanList: Boolean = true
|
||||
)
|
||||
|
||||
data class DrugSearchState(
|
||||
val isLoading: Boolean = false,
|
||||
val drugsData: Result<DrugListResponse?>? = null,
|
||||
val drugDetail: Result<DrugModels?>? = null,
|
||||
val error:String? = null
|
||||
)
|
||||
|
||||
data class DrugDetailState(
|
||||
val isLoading: Boolean = false,
|
||||
val drugDetail:Result<DrugModels?>? = null,
|
||||
val isDetailVisible: Boolean = false,
|
||||
val error:String? = null
|
||||
)
|
||||
|
||||
data class GetDarmaniState(
|
||||
val isLoading: Boolean = false,
|
||||
val getDarmani:Result<DarmanModel?>? = null,
|
||||
val error:String? = null
|
||||
)
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
package com.approagency.pharmacy.presentation.viewModel
|
||||
|
||||
import android.net.http.HttpException
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.approagency.pharmacy.domain.model.TestGroup
|
||||
import com.approagency.pharmacy.domain.model.TestItem
|
||||
import com.approagency.pharmacy.domain.usecase.GetTestGroupUseCase
|
||||
import com.approagency.pharmacy.domain.usecase.GetTestItemByGroupId
|
||||
import com.approagency.pharmacy.domain.usecase.SearchTestsUseCase
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.debounce
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class LabViewModel (
|
||||
private val getTestGroupUseCase: GetTestGroupUseCase,
|
||||
private val getTestItemByGroupId: GetTestItemByGroupId,
|
||||
private val searchTestsUseCase: SearchTestsUseCase
|
||||
) : ViewModel(){
|
||||
private val _testGroups = MutableStateFlow(TestGroupItem())
|
||||
val testGroups: StateFlow<TestGroupItem> = _testGroups.asStateFlow()
|
||||
|
||||
private val _testItems = MutableStateFlow(TestItemState())
|
||||
val testItems: StateFlow<TestItemState> = _testItems.asStateFlow()
|
||||
|
||||
|
||||
private val _searchResults = MutableStateFlow(SearchResultState())
|
||||
val searchResults: StateFlow<SearchResultState> = _searchResults.asStateFlow()
|
||||
|
||||
private val searchQuery = MutableStateFlow("")
|
||||
|
||||
init {
|
||||
loadTestGroups()
|
||||
setupSearch()
|
||||
}
|
||||
private fun setupSearch() {
|
||||
viewModelScope.launch {
|
||||
searchQuery
|
||||
.debounce(1000) // Wait 500ms after user stops typing
|
||||
.distinctUntilChanged()
|
||||
.collect { query ->
|
||||
if (query.isNotBlank()) {
|
||||
performSearch(query)
|
||||
} else {
|
||||
_searchResults.update {
|
||||
SearchResultState(
|
||||
isLoading = false,
|
||||
searchResults = null,
|
||||
query = ""
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun updateSearchQuery(query: String) {
|
||||
searchQuery.value = query
|
||||
_searchResults.update {
|
||||
it.copy(
|
||||
isLoading = query.isNotBlank(),
|
||||
query = query
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun performSearch(query: String) {
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
val result = searchTestsUseCase.invoke(query)
|
||||
result.collect { (groups, items) ->
|
||||
_searchResults.update {
|
||||
it.copy(
|
||||
isLoading = false,
|
||||
searchResults = SearchResult(groups = groups, items = items),
|
||||
error = null
|
||||
)
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
_searchResults.update {
|
||||
it.copy(
|
||||
isLoading = false,
|
||||
searchResults = null,
|
||||
error = e.message ?: "خطا در جستجو"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
fun loadTestGroups(){
|
||||
_testGroups.update {
|
||||
it.copy(
|
||||
isLoading = true
|
||||
)
|
||||
}
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
val result = getTestGroupUseCase.invoke();
|
||||
_testGroups.update {
|
||||
it.copy(
|
||||
isLoading = false,
|
||||
testGroup = result,
|
||||
)
|
||||
}
|
||||
} catch (e:HttpException){
|
||||
handleError(e.message)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
fun clearSearch() {
|
||||
searchQuery.value = ""
|
||||
_searchResults.update {
|
||||
SearchResultState()
|
||||
}
|
||||
}
|
||||
fun getItemByGroupId(id:Int){
|
||||
_testItems.update { it.copy(isLoading = true) }
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
val result = getTestItemByGroupId.invoke(id);
|
||||
_testItems.update {
|
||||
it.copy(
|
||||
isLoading = false,
|
||||
testItem = result
|
||||
)
|
||||
}
|
||||
} catch (e:HttpException){
|
||||
handleError(e.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
private suspend fun handleError(message: String?) {
|
||||
_testGroups.update {
|
||||
it.copy(
|
||||
isLoading = false,
|
||||
testGroup = null,
|
||||
error = message
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
data class TestGroupItem(
|
||||
val isLoading: Boolean = false,
|
||||
val testGroup: Flow<List<TestGroup>>? = null,
|
||||
val error:String? = null
|
||||
)
|
||||
|
||||
data class TestItemState(
|
||||
val isLoading: Boolean = false,
|
||||
val testItem: Flow<List<TestItem>>? = null,
|
||||
val error:String? = null
|
||||
)
|
||||
data class SearchResultState(
|
||||
val isLoading: Boolean = false,
|
||||
val searchResults: SearchResult? = null,
|
||||
val error: String? = null,
|
||||
val query: String = ""
|
||||
)
|
||||
|
||||
data class SearchResult(
|
||||
val groups: List<TestGroup>,
|
||||
val items: List<TestItem>
|
||||
)
|
||||
+92
@@ -0,0 +1,92 @@
|
||||
package com.approagency.pharmacy.presentation.viewModel
|
||||
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.approagency.pharmacy.domain.model.PharmacyDetail
|
||||
import com.approagency.pharmacy.domain.model.PharmacyItem
|
||||
import com.approagency.pharmacy.domain.usecase.GetPharmaciesUseCase
|
||||
import com.approagency.pharmacy.domain.usecase.GetPharmacyDetailUseCase
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
sealed class PharmacyDetailState {
|
||||
object Idle : PharmacyDetailState()
|
||||
object Loading : PharmacyDetailState()
|
||||
data class Success(val detail: PharmacyDetail) : PharmacyDetailState()
|
||||
data class Error(val message: String) : PharmacyDetailState()
|
||||
}
|
||||
sealed class PharmacyState {
|
||||
object Idle : PharmacyState()
|
||||
object Loading : PharmacyState()
|
||||
data class Success(val items: List<PharmacyItem>) : PharmacyState()
|
||||
data class Error(val message: String) : PharmacyState()
|
||||
}
|
||||
|
||||
class PharmacyViewModel(
|
||||
private val getPharmaciesUseCase: GetPharmaciesUseCase,
|
||||
private val getPharmacyDetailUseCase: GetPharmacyDetailUseCase
|
||||
) : ViewModel() {
|
||||
|
||||
private val _state = MutableStateFlow<PharmacyState>(PharmacyState.Idle)
|
||||
val state: StateFlow<PharmacyState> = _state.asStateFlow()
|
||||
|
||||
|
||||
// استفاده از Map برای نگهداری State هر داروخانه به صورت جداگانه
|
||||
private val _pharmacyDetailStates = MutableStateFlow<Map<String, PharmacyDetailState>>(emptyMap())
|
||||
val pharmacyDetailStates: StateFlow<Map<String, PharmacyDetailState>> = _pharmacyDetailStates.asStateFlow()
|
||||
|
||||
private var currentGenericDrugId: String = ""
|
||||
private var currentProvinceId: String = "0"
|
||||
|
||||
fun loadPharmacyDetail(pharmacyUrl: String) {
|
||||
// اگر در حال حاضر لود میشود یا موفقیت آمیز بوده، دوباره درخواست نده
|
||||
val currentState = _pharmacyDetailStates.value[pharmacyUrl]
|
||||
if (currentState is PharmacyDetailState.Loading || currentState is PharmacyDetailState.Success) {
|
||||
return
|
||||
}
|
||||
|
||||
viewModelScope.launch {
|
||||
// تنظیم Loading برای این داروخانه خاص
|
||||
_pharmacyDetailStates.value = _pharmacyDetailStates.value.toMutableMap().apply {
|
||||
put(pharmacyUrl, PharmacyDetailState.Loading)
|
||||
}
|
||||
|
||||
try {
|
||||
val detail = getPharmacyDetailUseCase(pharmacyUrl)
|
||||
_pharmacyDetailStates.value = _pharmacyDetailStates.value.toMutableMap().apply {
|
||||
put(pharmacyUrl, PharmacyDetailState.Success(detail))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
_pharmacyDetailStates.value = _pharmacyDetailStates.value.toMutableMap().apply {
|
||||
put(pharmacyUrl, PharmacyDetailState.Error(e.message ?: "خطا در دریافت اطلاعات"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun search(genericDrugId: String, provinceId: String) {
|
||||
if (genericDrugId.isBlank()) return
|
||||
|
||||
currentGenericDrugId = genericDrugId
|
||||
currentProvinceId = provinceId
|
||||
|
||||
viewModelScope.launch {
|
||||
_state.value = PharmacyState.Loading
|
||||
try {
|
||||
val result = getPharmaciesUseCase(genericDrugId, provinceId)
|
||||
_state.value = PharmacyState.Success(result)
|
||||
} catch (e: Exception) {
|
||||
_state.value = PharmacyState.Error(e.message ?: "خطا در دریافت اطلاعات")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun retry() {
|
||||
if (currentGenericDrugId.isNotBlank()) {
|
||||
search(currentGenericDrugId, currentProvinceId)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
package com.approgency.drug.presentation.viewModel
|
||||
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.approagency.pharmacy.domain.model.DrugSearchResult
|
||||
import com.approagency.pharmacy.domain.usecase.SearchDrugsYabUseCase
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class SearchViewModel(
|
||||
private val searchDrugsUseCase: SearchDrugsYabUseCase
|
||||
) : ViewModel() {
|
||||
|
||||
private val _searchState = MutableStateFlow<SearchState>(SearchState.Idle)
|
||||
val searchState: StateFlow<SearchState> = _searchState.asStateFlow()
|
||||
var searchText by mutableStateOf("")
|
||||
private set
|
||||
|
||||
fun updateSearchText(value: String) {
|
||||
searchText = value
|
||||
}
|
||||
private var currentQuery = ""
|
||||
private var currentPage = 1
|
||||
private var totalPages = 1
|
||||
private val allDrugs = mutableListOf<DrugSearchResult>()
|
||||
private var isLoadingMore = false
|
||||
private var hasMorePages = true
|
||||
|
||||
fun searchDrugs(query: String, isNewSearch: Boolean = true) {
|
||||
if (query.length < 3) {
|
||||
_searchState.value = SearchState.Error("لطفاً حداقل ۳ حرف وارد کنید")
|
||||
return
|
||||
}
|
||||
|
||||
if (isNewSearch) {
|
||||
// Reset everything for new search
|
||||
currentQuery = query
|
||||
currentPage = 1
|
||||
allDrugs.clear()
|
||||
isLoadingMore = false
|
||||
hasMorePages = true
|
||||
_searchState.value = SearchState.Loading(isNew = true)
|
||||
} else {
|
||||
// Don't load if already loading or no more pages
|
||||
if (isLoadingMore || !hasMorePages) return
|
||||
isLoadingMore = true
|
||||
_searchState.value = SearchState.LoadingMore(
|
||||
currentItems = allDrugs.toList(),
|
||||
currentPage = currentPage
|
||||
)
|
||||
}
|
||||
|
||||
viewModelScope.launch {
|
||||
val result = searchDrugsUseCase(currentQuery, currentPage)
|
||||
|
||||
_searchState.value = when {
|
||||
result.isSuccess -> {
|
||||
val searchResult = result.getOrNull()!!
|
||||
totalPages = searchResult.totalPages
|
||||
hasMorePages = currentPage < totalPages
|
||||
|
||||
allDrugs.addAll(searchResult.drugs)
|
||||
isLoadingMore = false
|
||||
|
||||
SearchState.Success(
|
||||
drugs = allDrugs.toList(),
|
||||
currentPage = searchResult.currentPage,
|
||||
totalPages = searchResult.totalPages,
|
||||
isLoadingMore = false,
|
||||
hasMorePages = hasMorePages
|
||||
)
|
||||
}
|
||||
else -> {
|
||||
isLoadingMore = false
|
||||
SearchState.Error(result.exceptionOrNull()?.message ?: "خطا در جستجو")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun loadNextPage() {
|
||||
if (hasMorePages && !isLoadingMore && currentPage < totalPages) {
|
||||
currentPage++
|
||||
searchDrugs(currentQuery, isNewSearch = false)
|
||||
}
|
||||
}
|
||||
|
||||
fun resetSearch() {
|
||||
currentQuery = ""
|
||||
currentPage = 1
|
||||
totalPages = 1
|
||||
allDrugs.clear()
|
||||
isLoadingMore = false
|
||||
hasMorePages = true
|
||||
_searchState.value = SearchState.Idle
|
||||
}
|
||||
|
||||
fun retryLastSearch() {
|
||||
if (currentQuery.isNotEmpty()) {
|
||||
searchDrugs(currentQuery, isNewSearch = true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sealed class SearchState {
|
||||
object Idle : SearchState()
|
||||
data class Loading(val isNew: Boolean) : SearchState()
|
||||
data class LoadingMore(
|
||||
val currentItems: List<DrugSearchResult>,
|
||||
val currentPage: Int
|
||||
) : SearchState()
|
||||
data class Success(
|
||||
val drugs: List<DrugSearchResult>,
|
||||
val currentPage: Int,
|
||||
val totalPages: Int,
|
||||
val isLoadingMore: Boolean = false,
|
||||
val hasMorePages: Boolean = true
|
||||
) : SearchState()
|
||||
data class Error(val message: String) : SearchState()
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.approagency.pharmacy.ui.theme
|
||||
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
val Purple80 = Color(0xFFD0BCFF)
|
||||
val PurpleGrey80 = Color(0xFFCCC2DC)
|
||||
val Pink80 = Color(0xFFEFB8C8)
|
||||
|
||||
val Purple40 = Color(0xFF6650a4)
|
||||
val PurpleGrey40 = Color(0xFF625b71)
|
||||
val Pink40 = Color(0xFF7D5260)
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.vada.caller.ui.theme
|
||||
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
|
||||
data class Dime (
|
||||
val xxs: Dp = 2.dp,
|
||||
val xs: Dp = 4.dp,
|
||||
val sm: Dp = 8.dp,
|
||||
val md: Dp = 12.dp,
|
||||
val lg: Dp = 16.dp,
|
||||
val xl: Dp = 20.dp,
|
||||
val xxl: Dp = 24.dp,
|
||||
|
||||
|
||||
val iconSm: Dp = 20.dp,
|
||||
val iconMd: Dp = 24.dp,
|
||||
val iconLg: Dp = 32.dp,
|
||||
)
|
||||
@@ -0,0 +1,5 @@
|
||||
package com.vada.caller.ui.theme
|
||||
|
||||
import androidx.compose.runtime.staticCompositionLocalOf
|
||||
|
||||
val LocalDime = staticCompositionLocalOf { Dime() }
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.vada.caller.ui.theme
|
||||
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
|
||||
val MaterialTheme.dime: Dime
|
||||
@Composable
|
||||
get() =LocalDime.current
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.fabirt.podcastapp.ui.theme
|
||||
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Shapes
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
val Shapes = Shapes(
|
||||
small = RoundedCornerShape(4.dp),
|
||||
medium = RoundedCornerShape(12.dp),
|
||||
large = RoundedCornerShape(16.dp),
|
||||
extraLarge = RoundedCornerShape(20.dp)
|
||||
)
|
||||
@@ -0,0 +1,112 @@
|
||||
package com.approagency.pharmacy.ui.theme
|
||||
|
||||
import android.os.Build
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.darkColorScheme
|
||||
import androidx.compose.material3.dynamicDarkColorScheme
|
||||
import androidx.compose.material3.dynamicLightColorScheme
|
||||
import androidx.compose.material3.lightColorScheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import com.fabirt.podcastapp.ui.theme.Shapes
|
||||
import com.vada.caller.ui.theme.Dime
|
||||
import com.vada.caller.ui.theme.LocalDime
|
||||
object MedicalColors {
|
||||
|
||||
val Primary = Color(0xFF0F766E)
|
||||
val PrimaryContainer = Color(0xFFD7F3EF)
|
||||
|
||||
val Secondary = Color(0xFF5B8E8A)
|
||||
val SecondaryContainer = Color(0xFFE3F3F1)
|
||||
|
||||
val Tertiary = Color(0xFF8FBFB8)
|
||||
|
||||
val Background = Color(0xFFF8FCFB)
|
||||
val Surface = Color(0xFFFFFFFF)
|
||||
|
||||
val DarkBackground = Color(0xFF0F1720)
|
||||
val DarkSurface = Color(0xFF17232B)
|
||||
|
||||
val OnDark = Color(0xFFF5FAF9)
|
||||
val OnLight = Color(0xFF102A2A)
|
||||
|
||||
val Error = Color(0xFFD64545)
|
||||
|
||||
val Outline = Color(0xFFD0E2DF)
|
||||
}
|
||||
private val DarkColorScheme = darkColorScheme(
|
||||
primary = Color(0xFF5DD4C3),
|
||||
onPrimary = Color(0xFF003733),
|
||||
|
||||
secondary = Color(0xFF8CC9C1),
|
||||
onSecondary = Color(0xFF0D2B29),
|
||||
|
||||
tertiary = Color(0xFFA7DCD4),
|
||||
onTertiary = Color(0xFF102A2A),
|
||||
|
||||
background = MedicalColors.DarkBackground,
|
||||
onBackground = MedicalColors.OnDark,
|
||||
|
||||
surface = MedicalColors.DarkSurface,
|
||||
onSurface = MedicalColors.OnDark,
|
||||
|
||||
primaryContainer = Color(0xFF124A45),
|
||||
secondaryContainer = Color(0xFF1D3D3A),
|
||||
|
||||
error = Color(0xFFFF8A80)
|
||||
)
|
||||
|
||||
private val LightColorScheme = lightColorScheme(
|
||||
primary = MedicalColors.Primary,
|
||||
onPrimary = Color.White,
|
||||
|
||||
secondary = MedicalColors.Secondary,
|
||||
onSecondary = Color.White,
|
||||
|
||||
tertiary = MedicalColors.Tertiary,
|
||||
onTertiary = MedicalColors.OnLight,
|
||||
|
||||
background = MedicalColors.Background,
|
||||
onBackground = MedicalColors.OnLight,
|
||||
|
||||
surface = MedicalColors.Surface,
|
||||
onSurface = MedicalColors.OnLight,
|
||||
|
||||
primaryContainer = MedicalColors.PrimaryContainer,
|
||||
secondaryContainer = MedicalColors.SecondaryContainer,
|
||||
|
||||
outline = MedicalColors.Outline,
|
||||
error = MedicalColors.Error
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun DrugTheme(
|
||||
darkTheme: Boolean = isSystemInDarkTheme(),
|
||||
// Dynamic color is available on Android 12+
|
||||
dynamicColor: Boolean = false,
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
val colorScheme = when {
|
||||
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
|
||||
val context = LocalContext.current
|
||||
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
|
||||
}
|
||||
|
||||
darkTheme -> DarkColorScheme
|
||||
else -> LightColorScheme
|
||||
}
|
||||
|
||||
CompositionLocalProvider(
|
||||
LocalDime provides Dime()
|
||||
) {
|
||||
MaterialTheme(
|
||||
colorScheme = colorScheme,
|
||||
typography = AppTypography,
|
||||
content = content,
|
||||
shapes = Shapes,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
package com.approagency.pharmacy.ui.theme
|
||||
import com.approagency.pharmacy.R
|
||||
import androidx.compose.material3.Typography
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.Font
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.sp
|
||||
val Dana = FontFamily(
|
||||
Font(R.font.dana_regular),
|
||||
Font(R.font.dana_regular, weight = FontWeight.Light),
|
||||
Font(R.font.dana_medium, weight = FontWeight.Medium),
|
||||
Font(R.font.dana_bold, weight = FontWeight.Bold),
|
||||
)
|
||||
// Set of Material typography styles to start with
|
||||
val Typography = Typography(
|
||||
bodyLarge = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 16.sp,
|
||||
lineHeight = 24.sp,
|
||||
letterSpacing = 0.5.sp
|
||||
)
|
||||
)
|
||||
|
||||
val AppTypography = Typography(
|
||||
displayLarge = TextStyle(
|
||||
fontFamily = Dana,
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 57.sp,
|
||||
lineHeight = 64.sp,
|
||||
letterSpacing = (-0.25).sp
|
||||
),
|
||||
displayMedium = TextStyle(
|
||||
fontFamily = Dana,
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 45.sp,
|
||||
lineHeight = 52.sp
|
||||
),
|
||||
displaySmall = TextStyle(
|
||||
fontFamily = Dana,
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 36.sp,
|
||||
lineHeight = 44.sp
|
||||
),
|
||||
headlineLarge = TextStyle(
|
||||
fontFamily = Dana,
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 32.sp,
|
||||
lineHeight = 40.sp
|
||||
),
|
||||
headlineMedium = TextStyle(
|
||||
fontFamily = Dana,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
fontSize = 28.sp,
|
||||
lineHeight = 36.sp
|
||||
),
|
||||
headlineSmall = TextStyle(
|
||||
fontFamily = Dana,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
fontSize = 24.sp,
|
||||
lineHeight = 32.sp
|
||||
),
|
||||
titleLarge = TextStyle(
|
||||
fontFamily = Dana,
|
||||
fontWeight = FontWeight.Medium,
|
||||
fontSize = 22.sp,
|
||||
lineHeight = 28.sp
|
||||
),
|
||||
titleMedium = TextStyle(
|
||||
fontFamily = Dana,
|
||||
fontWeight = FontWeight.Medium,
|
||||
fontSize = 16.sp,
|
||||
lineHeight = 24.sp
|
||||
),
|
||||
titleSmall = TextStyle(
|
||||
fontFamily = Dana,
|
||||
fontWeight = FontWeight.Medium,
|
||||
fontSize = 14.sp,
|
||||
lineHeight = 20.sp
|
||||
),
|
||||
bodyLarge = TextStyle(
|
||||
fontFamily = Dana,
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 16.sp,
|
||||
lineHeight = 24.sp
|
||||
),
|
||||
bodyMedium = TextStyle(
|
||||
fontFamily = Dana,
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 14.sp,
|
||||
lineHeight = 20.sp
|
||||
),
|
||||
bodySmall = TextStyle(
|
||||
fontFamily = Dana,
|
||||
fontWeight = FontWeight.Light,
|
||||
fontSize = 12.sp,
|
||||
lineHeight = 16.sp
|
||||
),
|
||||
labelLarge = TextStyle(
|
||||
fontFamily = Dana,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
fontSize = 14.sp,
|
||||
lineHeight = 20.sp
|
||||
),
|
||||
labelMedium = TextStyle(
|
||||
fontFamily = Dana,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
fontSize = 12.sp,
|
||||
lineHeight = 16.sp
|
||||
),
|
||||
labelSmall = TextStyle(
|
||||
fontFamily = Dana,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
fontSize = 11.sp,
|
||||
lineHeight = 16.sp
|
||||
)
|
||||
)
|
||||
@@ -0,0 +1,6 @@
|
||||
package com.approagency.pharmacy.utils
|
||||
|
||||
object Config {
|
||||
const val BASE_URL = "https://drug.approagency.ir/api/"
|
||||
const val Darro_Url = "https://www.darooyab.ir/"
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package com.approagency.pharmacy.utils
|
||||
|
||||
|
||||
val provinces = listOf(
|
||||
Province("0", "همه استان ها"),
|
||||
Province("1", "اصفهان"),
|
||||
Province("40", "تهران"),
|
||||
Province("137", "آذربایجان شرقی"),
|
||||
Province("138", "آذربایجان غربی"),
|
||||
Province("139", "اردبیل"),
|
||||
Province("140", "البرز"),
|
||||
Province("141", "ایلام"),
|
||||
Province("142", "بوشهر"),
|
||||
Province("143", "چهارمحال و بختیاری"),
|
||||
Province("144", "خراسان جنوبی"),
|
||||
Province("145", "خراسان رضوی"),
|
||||
Province("146", "خراسان شمالی"),
|
||||
Province("147", "خوزستان"),
|
||||
Province("148", "زنجان"),
|
||||
Province("149", "سمنان"),
|
||||
Province("150", "سیستان و بلوچستان"),
|
||||
Province("151", "فارس"),
|
||||
Province("152", "قزوین"),
|
||||
Province("153", "قم"),
|
||||
Province("154", "کردستان"),
|
||||
Province("155", "کرمان"),
|
||||
Province("156", "کرمانشاه"),
|
||||
Province("157", "کهگیلویه و بویراحمد"),
|
||||
Province("158", "گلستان"),
|
||||
Province("159", "گیلان"),
|
||||
Province("160", "لرستان"),
|
||||
Province("161", "مازندران"),
|
||||
Province("162", "مرکزی"),
|
||||
Province("163", "هرمزگان"),
|
||||
Province("164", "همدان"),
|
||||
Province("165", "یزد")
|
||||
)
|
||||
|
||||
data class Province(
|
||||
val id: String,
|
||||
val name: String
|
||||
)
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.approagency.pharmacy.utils
|
||||
|
||||
import kotlinx.coroutines.delay
|
||||
|
||||
suspend fun <T> retryWithBackoff(
|
||||
maxRetries: Int = 3,
|
||||
initialDelayMs: Long = 1000,
|
||||
maxDelayMs: Long = 5000,
|
||||
factor: Double = 2.0,
|
||||
onRetry: ((attempt: Int, delayMs: Long) -> Unit)? = null,
|
||||
block: suspend () -> T
|
||||
): Result<T> {
|
||||
var currentDelay = initialDelayMs
|
||||
repeat(maxRetries - 1) { attempt ->
|
||||
try {
|
||||
return Result.success(block())
|
||||
} catch (e: Exception) {
|
||||
onRetry?.invoke(attempt + 1, currentDelay)
|
||||
delay(currentDelay)
|
||||
currentDelay = (currentDelay * factor).toLong().coerceAtMost(maxDelayMs)
|
||||
}
|
||||
}
|
||||
// Last attempt
|
||||
return try {
|
||||
Result.success(block())
|
||||
} catch (e: Exception) {
|
||||
Result.failure(e)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user