Compare commits
2
Commits
1ed75fb823
...
1971708b3f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1971708b3f | ||
|
|
27dc08fa68 |
Generated
-1
@@ -1,4 +1,3 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
||||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" project-jdk-name="17" project-jdk-type="JavaSDK">
|
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" project-jdk-name="17" project-jdk-type="JavaSDK">
|
||||||
|
|||||||
Binary file not shown.
@@ -0,0 +1,185 @@
|
|||||||
|
package com.approagency.pharmacy.data.local
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.database.Cursor
|
||||||
|
import android.database.sqlite.SQLiteDatabase
|
||||||
|
import com.approagency.pharmacy.data.dto.DarmanList
|
||||||
|
import com.approagency.pharmacy.data.dto.DarmanModel
|
||||||
|
import com.approagency.pharmacy.data.dto.DrugDetail
|
||||||
|
import com.approagency.pharmacy.data.dto.DrugListResponse
|
||||||
|
import com.approagency.pharmacy.data.dto.DrugModels
|
||||||
|
import com.approagency.pharmacy.data.dto.GorohDaroei
|
||||||
|
import com.approagency.pharmacy.data.dto.GorohDarmani
|
||||||
|
import com.approagency.pharmacy.data.dto.Meta
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import java.io.FileOutputStream
|
||||||
|
import kotlin.math.ceil
|
||||||
|
|
||||||
|
/**
|
||||||
|
* منبعِ دادهٔ دارو روی پایگاهدادهٔ همراهِ اپ (`assets/DrugStore.db`).
|
||||||
|
*
|
||||||
|
* تنها منبعِ اطلاعاتِ دارو (جستوجو، جزئیات، گروههای درمانی) است و فقط خواندنی
|
||||||
|
* است. از SQLite خام استفاده میکند (نه Room) تا اعتبارسنجیِ سختگیرانهٔ شِمای
|
||||||
|
* Room روی این دیتابیسِ ثابتِ ازپیشساخته (با کلیدهای خارجی) دردسر نسازد.
|
||||||
|
*/
|
||||||
|
class LocalDrugDataSource(private val context: Context) {
|
||||||
|
|
||||||
|
@Volatile
|
||||||
|
private var db: SQLiteDatabase? = null
|
||||||
|
|
||||||
|
private fun database(): SQLiteDatabase {
|
||||||
|
db?.let { return it }
|
||||||
|
return synchronized(this) {
|
||||||
|
db ?: openReadOnly().also { db = it }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun openReadOnly(): SQLiteDatabase {
|
||||||
|
val out = context.getDatabasePath(DB_NAME)
|
||||||
|
if (!out.exists()) {
|
||||||
|
out.parentFile?.mkdirs()
|
||||||
|
context.assets.open(DB_NAME).use { input ->
|
||||||
|
FileOutputStream(out).use { output -> input.copyTo(output) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return SQLiteDatabase.openDatabase(out.path, null, SQLiteDatabase.OPEN_READONLY)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** جستوجوی دارو با فیلترهای نام، گروه دارویی و گروه درمانی. */
|
||||||
|
suspend fun searchDrugs(
|
||||||
|
query: String?,
|
||||||
|
drugGroup: Int?,
|
||||||
|
healGroup: Int?,
|
||||||
|
perPage: Int
|
||||||
|
): DrugListResponse = withContext(Dispatchers.IO) {
|
||||||
|
val where = StringBuilder("1=1")
|
||||||
|
val args = mutableListOf<String>()
|
||||||
|
if (!query.isNullOrBlank()) {
|
||||||
|
where.append(" AND (d.nam_fa LIKE ? OR d.nam_en LIKE ?)")
|
||||||
|
args.add("%$query%"); args.add("%$query%")
|
||||||
|
}
|
||||||
|
if (drugGroup != null) { where.append(" AND d.goroh_daroei_cod = ?"); args.add(drugGroup.toString()) }
|
||||||
|
if (healGroup != null) { where.append(" AND d.goroh_darmani_cod = ?"); args.add(healGroup.toString()) }
|
||||||
|
|
||||||
|
val total = database().rawQuery(
|
||||||
|
"SELECT COUNT(*) FROM DrugInfo d WHERE $where", args.toTypedArray()
|
||||||
|
).use { c -> if (c.moveToFirst()) c.getInt(0) else 0 }
|
||||||
|
|
||||||
|
val items = database().rawQuery(
|
||||||
|
"$SELECT_WITH_RELATIONS WHERE $where ORDER BY d.nam_fa LIMIT ?",
|
||||||
|
(args + perPage.toString()).toTypedArray()
|
||||||
|
).use { c -> buildList { while (c.moveToNext()) add(c.toDrugDetail()) } }
|
||||||
|
|
||||||
|
DrugListResponse(
|
||||||
|
success = true,
|
||||||
|
message = null,
|
||||||
|
data = items,
|
||||||
|
meta = Meta(
|
||||||
|
current_page = 1,
|
||||||
|
last_page = if (perPage > 0) ceil(total.toDouble() / perPage).toInt() else 1,
|
||||||
|
per_page = perPage,
|
||||||
|
total = total
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** جزئیات یک دارو بر اساس [cod]. اگر یافت نشود استثناء پرتاب میشود. */
|
||||||
|
suspend fun drugDetail(cod: Int): DrugModels = withContext(Dispatchers.IO) {
|
||||||
|
val detail = database().rawQuery(
|
||||||
|
"$SELECT_WITH_RELATIONS WHERE d.cod = ?", arrayOf(cod.toString())
|
||||||
|
).use { c -> if (c.moveToFirst()) c.toDrugDetail() else null }
|
||||||
|
?: throw NoSuchElementException("دارویی با شناسهٔ $cod در پایگاهدادهٔ محلی یافت نشد.")
|
||||||
|
|
||||||
|
DrugModels(success = true, message = null, data = detail, meta = null)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** فهرست گروههای درمانی (goroh_darmani). */
|
||||||
|
suspend fun darmanGroups(): DarmanModel = withContext(Dispatchers.IO) {
|
||||||
|
val groups = database().rawQuery(
|
||||||
|
"SELECT cod, nam_fa, nam_en FROM goroh_darmani ORDER BY nam_fa", null
|
||||||
|
).use { c ->
|
||||||
|
buildList {
|
||||||
|
while (c.moveToNext()) {
|
||||||
|
add(
|
||||||
|
DarmanList(
|
||||||
|
cod = c.getInt(0),
|
||||||
|
nam_fa = c.getStringOrEmpty(1),
|
||||||
|
nam_en = c.getStringOrEmpty(2)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DarmanModel(success = "true", message = "", data = groups)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Cursor.toDrugDetail(): DrugDetail {
|
||||||
|
val daroeiCod = getIntOrNull("goroh_daroei_cod")
|
||||||
|
val darmaniCod = getIntOrNull("goroh_darmani_cod")
|
||||||
|
return DrugDetail(
|
||||||
|
cod = getIntOrNull("cod"),
|
||||||
|
goroh_darmani_detail_cod = getIntOrNull("goroh_darmani_detail_cod"),
|
||||||
|
goroh_daroei_cod = daroeiCod,
|
||||||
|
goroh_farmakologic_cod = getIntOrNull("goroh_farmakologic_cod"),
|
||||||
|
goroh_darmani_cod = darmaniCod,
|
||||||
|
nam_fa = getStringOrNull("nam_fa"),
|
||||||
|
nam_en = getStringOrNull("nam_en"),
|
||||||
|
mavaredmasraf = getStringOrNull("mavaredmasraf"),
|
||||||
|
meghdarmasraf = getStringOrNull("meghdarmasraf"),
|
||||||
|
masrafdarhamelegi = getStringOrNull("masrafdarhamelegi"),
|
||||||
|
masrafdarshirdehi = getStringOrNull("masrafdarshirdehi"),
|
||||||
|
manemasraf = getStringOrNull("manemasraf"),
|
||||||
|
avarez = getStringOrNull("avarez"),
|
||||||
|
tadakhol = getStringOrNull("tadakhol"),
|
||||||
|
mekanismtasir = getStringOrNull("mekanismtasir"),
|
||||||
|
nokte = getStringOrNull("nokte"),
|
||||||
|
hoshdar = getStringOrNull("hoshdar"),
|
||||||
|
sharayetnegahdari = getStringOrNull("sharayetnegahdari"),
|
||||||
|
ashkal_daroei = getStringOrNull("ashkal_daroei"),
|
||||||
|
created_at = null,
|
||||||
|
updated_at = null,
|
||||||
|
goroh_daroei = getStringOrNull("gd_nam")?.let { GorohDaroei(cod = daroeiCod, nam = it, created_at = null, updated_at = null) },
|
||||||
|
goroh_darmani = getStringOrNull("gm_nam_fa")?.let {
|
||||||
|
GorohDarmani(
|
||||||
|
cod = darmaniCod,
|
||||||
|
nam_fa = it,
|
||||||
|
nam_en = getStringOrNull("gm_nam_en"),
|
||||||
|
giyahi_ya_shimiyaei = getStringOrNull("gm_giyahi"),
|
||||||
|
image = getStringOrNull("gm_image"),
|
||||||
|
created_at = null,
|
||||||
|
updated_at = null
|
||||||
|
)
|
||||||
|
},
|
||||||
|
goroh_darmani_detail = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Cursor.getStringOrNull(name: String): String? {
|
||||||
|
val i = getColumnIndex(name)
|
||||||
|
return if (i >= 0 && !isNull(i)) getString(i) else null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Cursor.getStringOrEmpty(index: Int): String =
|
||||||
|
if (!isNull(index)) getString(index) else ""
|
||||||
|
|
||||||
|
private fun Cursor.getIntOrNull(name: String): Int? {
|
||||||
|
val i = getColumnIndex(name)
|
||||||
|
return if (i >= 0 && !isNull(i)) getInt(i) else null
|
||||||
|
}
|
||||||
|
|
||||||
|
private companion object {
|
||||||
|
const val DB_NAME = "DrugStore.db"
|
||||||
|
const val SELECT_WITH_RELATIONS = """
|
||||||
|
SELECT d.*,
|
||||||
|
gd.nam AS gd_nam,
|
||||||
|
gm.nam_fa AS gm_nam_fa,
|
||||||
|
gm.nam_en AS gm_nam_en,
|
||||||
|
gm.giyahi_ya_shimiyaei AS gm_giyahi,
|
||||||
|
gm.image AS gm_image
|
||||||
|
FROM DrugInfo d
|
||||||
|
LEFT JOIN goroh_daroei gd ON gd.cod = d.goroh_daroei_cod
|
||||||
|
LEFT JOIN goroh_darmani gm ON gm.cod = d.goroh_darmani_cod
|
||||||
|
"""
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
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
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -3,8 +3,8 @@ package com.approagency.pharmacy.data.repository
|
|||||||
import com.approagency.pharmacy.data.dto.DarmanModel
|
import com.approagency.pharmacy.data.dto.DarmanModel
|
||||||
import com.approagency.pharmacy.data.dto.DrugListResponse
|
import com.approagency.pharmacy.data.dto.DrugListResponse
|
||||||
import com.approagency.pharmacy.data.dto.DrugModels
|
import com.approagency.pharmacy.data.dto.DrugModels
|
||||||
|
import com.approagency.pharmacy.data.local.LocalDrugDataSource
|
||||||
import com.approagency.pharmacy.data.remote.DarooyabApiService
|
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.DrugDetailParser
|
||||||
import com.approagency.pharmacy.data.remote.DrugHtmlParser
|
import com.approagency.pharmacy.data.remote.DrugHtmlParser
|
||||||
import com.approagency.pharmacy.data.remote.PharmacyHtmlParser
|
import com.approagency.pharmacy.data.remote.PharmacyHtmlParser
|
||||||
@@ -20,34 +20,39 @@ import kotlinx.coroutines.withContext
|
|||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
|
|
||||||
class DrugRepositoryImpl(
|
class DrugRepositoryImpl(
|
||||||
private val apiService: DrugApiService,
|
|
||||||
private val darooyabApiService: DarooyabApiService,
|
private val darooyabApiService: DarooyabApiService,
|
||||||
private val parser: DrugHtmlParser,
|
private val parser: DrugHtmlParser,
|
||||||
private val detailParser: DrugDetailParser,
|
private val detailParser: DrugDetailParser,
|
||||||
|
private val localDrugDataSource: LocalDrugDataSource,
|
||||||
): DrugRepository {
|
): DrugRepository {
|
||||||
|
// اطلاعات دارو همیشه از پایگاهدادهٔ محلیِ همراهِ اپ (assets/DrugStore.db) خوانده میشود.
|
||||||
override suspend fun searchDrug(params: DrugSearchParams): Result<DrugListResponse> {
|
override suspend fun searchDrug(params: DrugSearchParams): Result<DrugListResponse> {
|
||||||
return try {
|
return try {
|
||||||
val response = apiService.getDrugs(query = params.query , perPage = params.perPage , withRelations = params.withRelations , healGroup = params.healGroup , drugGroup = params.drugGroup )
|
Result.success(
|
||||||
return Result.success(response)
|
localDrugDataSource.searchDrugs(
|
||||||
}catch (e: Exception){
|
query = params.query,
|
||||||
|
drugGroup = params.drugGroup,
|
||||||
|
healGroup = params.healGroup,
|
||||||
|
perPage = params.perPage
|
||||||
|
)
|
||||||
|
)
|
||||||
|
} catch (e: Exception) {
|
||||||
Result.failure(e)
|
Result.failure(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun drugDetail(cod: Int): Result<DrugModels> {
|
override suspend fun drugDetail(cod: Int): Result<DrugModels> {
|
||||||
return try {
|
return try {
|
||||||
val response= apiService.getDrugDetail(cod = cod)
|
Result.success(localDrugDataSource.drugDetail(cod))
|
||||||
return Result.success(response)
|
} catch (e: Exception) {
|
||||||
}catch (e: Exception){
|
|
||||||
Result.failure(e)
|
Result.failure(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getGorohDaroei(): Result<DarmanModel> {
|
override suspend fun getGorohDaroei(): Result<DarmanModel> {
|
||||||
return try {
|
return try {
|
||||||
val response = apiService.getGorohDaroei()
|
Result.success(localDrugDataSource.darmanGroups())
|
||||||
return Result.success(response)
|
} catch (e: Exception) {
|
||||||
}catch (e: Exception){
|
|
||||||
Result.failure(e)
|
Result.failure(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,12 +2,12 @@ package com.approagency.pharmacy.di
|
|||||||
|
|
||||||
|
|
||||||
import com.approagency.pharmacy.data.billing.StubPurchaseGateway
|
import com.approagency.pharmacy.data.billing.StubPurchaseGateway
|
||||||
|
import com.approagency.pharmacy.data.local.LocalDrugDataSource
|
||||||
import com.approagency.pharmacy.data.local.SessionManager
|
import com.approagency.pharmacy.data.local.SessionManager
|
||||||
import com.approagency.pharmacy.data.local.database.LabDatabase
|
import com.approagency.pharmacy.data.local.database.LabDatabase
|
||||||
import com.approagency.pharmacy.data.remote.ApproApiService
|
import com.approagency.pharmacy.data.remote.ApproApiService
|
||||||
import com.approagency.pharmacy.data.remote.AuthInterceptor
|
import com.approagency.pharmacy.data.remote.AuthInterceptor
|
||||||
import com.approagency.pharmacy.data.remote.DarooyabApiService
|
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.DrugDetailParser
|
||||||
import com.approagency.pharmacy.data.remote.DrugHtmlParser
|
import com.approagency.pharmacy.data.remote.DrugHtmlParser
|
||||||
import com.approagency.pharmacy.data.repository.AuthRepositoryImpl
|
import com.approagency.pharmacy.data.repository.AuthRepositoryImpl
|
||||||
@@ -47,7 +47,6 @@ import retrofit2.converter.scalars.ScalarsConverterFactory
|
|||||||
import java.net.CookieManager
|
import java.net.CookieManager
|
||||||
import java.net.CookiePolicy
|
import java.net.CookiePolicy
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
val jsonRetrofitQualifier = qualifier("jsonRetrofit")
|
|
||||||
val scalarRetrofitQualifier = qualifier("scalarRetrofit")
|
val scalarRetrofitQualifier = qualifier("scalarRetrofit")
|
||||||
val authRetrofitQualifier = qualifier("authRetrofit")
|
val authRetrofitQualifier = qualifier("authRetrofit")
|
||||||
val appModule= module {
|
val appModule= module {
|
||||||
@@ -76,19 +75,6 @@ val appModule= module {
|
|||||||
.build()
|
.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 ==========
|
// ========== Retrofit for DAROOYAB Website ==========
|
||||||
single<Retrofit>(scalarRetrofitQualifier) { // Add qualifier
|
single<Retrofit>(scalarRetrofitQualifier) { // Add qualifier
|
||||||
Retrofit.Builder()
|
Retrofit.Builder()
|
||||||
@@ -135,9 +121,12 @@ val appModule= module {
|
|||||||
|
|
||||||
factory { DrugHtmlParser() }
|
factory { DrugHtmlParser() }
|
||||||
factory { DrugDetailParser() }
|
factory { DrugDetailParser() }
|
||||||
|
// منبع آفلاین دارو (assets/DrugStore.db) برای fallback هنگام خطای شبکه
|
||||||
|
single { LocalDrugDataSource(androidContext()) }
|
||||||
|
|
||||||
//repo
|
//repo
|
||||||
single<DrugRepository> {
|
single<DrugRepository> {
|
||||||
DrugRepositoryImpl(get<DrugApiService>() , get<DarooyabApiService>() , get() , get())
|
DrugRepositoryImpl(get<DarooyabApiService>() , get() , get() , get())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ fun AppNavGraph() {
|
|||||||
|
|
||||||
NavHost(
|
NavHost(
|
||||||
navController = navController,
|
navController = navController,
|
||||||
startDestination = "home"
|
startDestination = "search"
|
||||||
) {
|
) {
|
||||||
|
|
||||||
// ───────────────
|
// ───────────────
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.navigation.NavGraph.Companion.findStartDestination
|
||||||
import androidx.navigation.NavHostController
|
import androidx.navigation.NavHostController
|
||||||
import androidx.navigation.compose.currentBackStackEntryAsState
|
import androidx.navigation.compose.currentBackStackEntryAsState
|
||||||
|
|
||||||
@@ -70,7 +71,7 @@ fun BottomBar(navController: NavHostController) {
|
|||||||
.clickable {
|
.clickable {
|
||||||
if (!selected) {
|
if (!selected) {
|
||||||
navController.navigate(item.first) {
|
navController.navigate(item.first) {
|
||||||
popUpTo("home") {
|
popUpTo(navController.graph.findStartDestination().id) {
|
||||||
saveState = true
|
saveState = true
|
||||||
}
|
}
|
||||||
launchSingleTop = true
|
launchSingleTop = true
|
||||||
|
|||||||
+58
-35
@@ -1,74 +1,97 @@
|
|||||||
package com.approagency.pharmacy.presentation.components
|
package com.approagency.pharmacy.presentation.components
|
||||||
|
|
||||||
import androidx.compose.foundation.clickable
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.LocalPharmacy
|
||||||
|
import androidx.compose.material.icons.filled.Medication
|
||||||
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.OutlinedButton
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.unit.sp
|
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.DrugSearchResult
|
import com.approagency.pharmacy.domain.model.DrugSearchResult
|
||||||
import com.approagency.pharmacy.presentation.common.CustomBox
|
import com.approagency.pharmacy.presentation.common.CustomBox
|
||||||
import com.vada.caller.ui.theme.LocalDime
|
|
||||||
import com.vada.caller.ui.theme.dime
|
import com.vada.caller.ui.theme.dime
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun DaroYabSearchResult(
|
fun DaroYabSearchResult(
|
||||||
drug: DrugSearchResult,
|
drug: DrugSearchResult,
|
||||||
onClickDetail: (DrugSearchResult) -> Unit,
|
onClickDetail: (DrugSearchResult) -> Unit,
|
||||||
onClickDrugStore:(DrugSearchResult) ->Unit,
|
onClickDrugStore: (DrugSearchResult) -> Unit,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
val dime = LocalDime.current
|
CustomBox(
|
||||||
|
|
||||||
CustomBox (
|
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.padding(bottom = MaterialTheme.dime.sm)
|
.padding(bottom = MaterialTheme.dime.sm)
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.clickable { onClickDetail(drug) },
|
|
||||||
|
|
||||||
) {
|
|
||||||
Row (
|
|
||||||
modifier = modifier.fillMaxWidth(),
|
|
||||||
horizontalArrangement = Arrangement.End
|
|
||||||
) {
|
) {
|
||||||
|
CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.padding(dime.lg),
|
modifier = Modifier
|
||||||
verticalArrangement = Arrangement.Center,
|
.fillMaxWidth()
|
||||||
horizontalAlignment = Alignment.End
|
.padding(MaterialTheme.dime.lg),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(MaterialTheme.dime.xs)
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = drug.persianName,
|
text = drug.persianName,
|
||||||
style = MaterialTheme.typography.labelLarge
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
fontWeight = FontWeight.Bold
|
||||||
)
|
)
|
||||||
drug.englishName?.let {
|
drug.englishName?.takeIf { it.isNotBlank() }?.let {
|
||||||
Text(
|
Text(
|
||||||
text = it,
|
text = it,
|
||||||
fontSize = 12.sp,
|
style = MaterialTheme.typography.bodySmall,
|
||||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f)
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Text(
|
|
||||||
text = "کد: ${drug.genericId}",
|
Spacer(Modifier.height(MaterialTheme.dime.sm))
|
||||||
fontSize = 10.sp,
|
|
||||||
color = MaterialTheme.colorScheme.primary
|
|
||||||
)
|
|
||||||
Row(
|
Row(
|
||||||
modifier = modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
horizontalArrangement = Arrangement.SpaceBetween
|
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.dime.sm)
|
||||||
) {
|
) {
|
||||||
TextButton(onClick = {onClickDetail(drug)} , content = {
|
Button(
|
||||||
Text("اطلاعات دارویی")
|
onClick = { onClickDrugStore(drug) },
|
||||||
})
|
modifier = Modifier
|
||||||
TextButton(onClick = {onClickDrugStore(drug)} , content = {
|
.weight(1f)
|
||||||
Text("موجودی داروخانه")
|
.height(44.dp)
|
||||||
})
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Filled.LocalPharmacy,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(18.dp)
|
||||||
|
)
|
||||||
|
Spacer(Modifier.size(MaterialTheme.dime.xs))
|
||||||
|
Text("موجودی داروخانه", style = MaterialTheme.typography.labelLarge)
|
||||||
|
}
|
||||||
|
OutlinedButton(
|
||||||
|
onClick = { onClickDetail(drug) },
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.height(44.dp)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Filled.Medication,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(18.dp)
|
||||||
|
)
|
||||||
|
Spacer(Modifier.size(MaterialTheme.dime.xs))
|
||||||
|
Text("اطلاعات دارویی", style = MaterialTheme.typography.labelLarge)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+94
-103
@@ -3,13 +3,13 @@ package com.approagency.pharmacy.presentation.components
|
|||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalConfiguration
|
||||||
import androidx.compose.ui.platform.LocalLayoutDirection
|
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.LayoutDirection
|
import androidx.compose.ui.unit.LayoutDirection
|
||||||
@@ -33,155 +33,99 @@ fun PharmacyBottomSheet(
|
|||||||
) {
|
) {
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
val sheetState = rememberModalBottomSheetState(
|
val sheetState = rememberModalBottomSheetState(
|
||||||
skipPartiallyExpanded = false, confirmValueChange = { true })
|
skipPartiallyExpanded = true, confirmValueChange = { true }
|
||||||
|
)
|
||||||
|
|
||||||
var selectedProvinceId by remember { mutableStateOf("40") } // پیشفرض تهران
|
var selectedProvinceId by remember { mutableStateOf("40") } // پیشفرض تهران
|
||||||
var showProvinceDropdown by remember { mutableStateOf(false) }
|
var showProvinceDropdown by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
val pharmacyState by viewModel.state.collectAsState()
|
val pharmacyState by viewModel.state.collectAsState()
|
||||||
|
|
||||||
// جستجوی اولیه با استان تهران — با تغییر دارو دوباره اجرا میشود
|
// شیت بلند ثابت میشود تا لیست داخلی بهدرستی اسکرول شود (نه اینکه درگِ شیت
|
||||||
// (در صورت خالی بودن شناسه، ViewModel وضعیت قبلی را پاک و خطا نمایش میدهد)
|
// جای اسکرولِ لیست را بگیرد) و حالتهای لودینگ/خالی/خطا وسط دیده شوند.
|
||||||
|
val sheetHeight = (LocalConfiguration.current.screenHeightDp * 0.85f).dp
|
||||||
|
|
||||||
LaunchedEffect(genericDrugId, brandIrc) {
|
LaunchedEffect(genericDrugId, brandIrc) {
|
||||||
viewModel.search(genericDrugId, brandIrc, "40")
|
viewModel.search(genericDrugId, brandIrc, "40")
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isOpen) {
|
if (isOpen) {
|
||||||
CustomModalBottomSheet(
|
CustomModalBottomSheet(sheetState = sheetState, onDismiss = onDismiss, scope = scope) {
|
||||||
sheetState = sheetState, onDismiss = onDismiss, scope = scope
|
|
||||||
) {
|
|
||||||
CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) {
|
CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) {
|
||||||
LazyColumn(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxHeight()
|
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.heightIn(min = 400.dp)
|
.height(sheetHeight)
|
||||||
.padding(MaterialTheme.dime.md),
|
.padding(MaterialTheme.dime.lg)
|
||||||
verticalArrangement = Arrangement.spacedBy(MaterialTheme.dime.xl)
|
|
||||||
) {
|
) {
|
||||||
item {
|
// سربرگ ثابت
|
||||||
Text(
|
Text(
|
||||||
text = "جستجوی دارو در داروخانهها",
|
text = "موجودی دارو در داروخانهها",
|
||||||
fontWeight = FontWeight.Bold,
|
style = MaterialTheme.typography.titleMedium,
|
||||||
fontSize = MaterialTheme.typography.titleMedium.fontSize
|
fontWeight = FontWeight.Bold
|
||||||
)
|
|
||||||
}
|
|
||||||
// عنوان
|
|
||||||
|
|
||||||
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(
|
Spacer(Modifier.height(MaterialTheme.dime.md))
|
||||||
|
|
||||||
|
ProvinceSelector(
|
||||||
|
selectedProvinceId = selectedProvinceId,
|
||||||
expanded = showProvinceDropdown,
|
expanded = showProvinceDropdown,
|
||||||
onDismissRequest = { showProvinceDropdown = false }) {
|
onExpandedChange = { showProvinceDropdown = it },
|
||||||
provinces.forEach { province ->
|
onProvinceSelected = { id ->
|
||||||
DropdownMenuItem(text = { Text(province.name) }, onClick = {
|
selectedProvinceId = id
|
||||||
selectedProvinceId = province.id
|
|
||||||
showProvinceDropdown = false
|
showProvinceDropdown = false
|
||||||
// جستجو با استان جدید
|
viewModel.search(genericDrugId, brandIrc, id)
|
||||||
viewModel.search(genericDrugId, brandIrc, province.id)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// انتخاب استان
|
|
||||||
|
|
||||||
|
|
||||||
// نمایش نتایج
|
|
||||||
when (val state = pharmacyState) {
|
|
||||||
PharmacyState.Loading -> {
|
|
||||||
item {
|
|
||||||
Loading(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.height(400.dp),
|
|
||||||
message = "در حال جستجو..."
|
|
||||||
)
|
)
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
is PharmacyState.Success -> {
|
Spacer(Modifier.height(MaterialTheme.dime.md))
|
||||||
if (state.items.isEmpty()) {
|
|
||||||
item {
|
// ناحیهی محتوا فضای باقیمانده را پر میکند؛ سربرگ بالا ثابت میماند.
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.height(400.dp),
|
.weight(1f),
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
Text(
|
when (val state = pharmacyState) {
|
||||||
text = "هیچ داروخانهای یافت نشد",
|
PharmacyState.Loading -> Loading(
|
||||||
fontSize = MaterialTheme.typography.bodyMedium.fontSize,
|
modifier = Modifier.fillMaxSize(),
|
||||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f)
|
message = "در حال جستجو..."
|
||||||
)
|
)
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
is PharmacyState.Success -> {
|
||||||
|
if (state.items.isEmpty()) {
|
||||||
|
EmptyMessage("هیچ داروخانهای یافت نشد")
|
||||||
} else {
|
} else {
|
||||||
|
LazyColumn(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(MaterialTheme.dime.sm)
|
||||||
|
) {
|
||||||
items(state.items) { pharmacy ->
|
items(state.items) { pharmacy ->
|
||||||
PharmacyResultItem(pharmacy = pharmacy, viewModel)
|
PharmacyResultItem(pharmacy = pharmacy, viewModel)
|
||||||
}
|
}
|
||||||
item {
|
item {
|
||||||
Spacer(modifier = Modifier.height(MaterialTheme.dime.md))
|
|
||||||
Text(
|
Text(
|
||||||
text = "تعداد کل: ${state.items.size} داروخانه",
|
text = "تعداد کل: ${state.items.size} داروخانه",
|
||||||
fontSize = MaterialTheme.typography.bodySmall.fontSize,
|
style = MaterialTheme.typography.bodySmall,
|
||||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.5f),
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
modifier = Modifier.padding(bottom = MaterialTheme.dime.xs)
|
modifier = Modifier.padding(vertical = MaterialTheme.dime.sm)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
// LazyColumn(
|
}
|
||||||
// verticalArrangement = Arrangement.spacedBy(8.dp),
|
|
||||||
// modifier = Modifier,
|
|
||||||
// ) {
|
|
||||||
// items(state.items) { pharmacy ->
|
|
||||||
// PharmacyResultItem(pharmacy = pharmacy)
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
//
|
|
||||||
// }
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
is PharmacyState.Error -> {
|
is PharmacyState.Error -> Column(
|
||||||
item {
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.height(200.dp),
|
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
) {
|
||||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
|
||||||
Text(
|
Text(
|
||||||
text = state.message,
|
text = state.message,
|
||||||
color = MaterialTheme.colorScheme.error,
|
color = MaterialTheme.colorScheme.error,
|
||||||
fontSize = MaterialTheme.typography.bodyMedium.fontSize
|
style = MaterialTheme.typography.bodyMedium
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(MaterialTheme.dime.sm))
|
Spacer(Modifier.height(MaterialTheme.dime.sm))
|
||||||
TextButton(
|
TextButton(onClick = { viewModel.retry() }) { Text("تلاش مجدد") }
|
||||||
onClick = { viewModel.retry() }) {
|
|
||||||
Text("تلاش مجدد")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
PharmacyState.Idle -> {}
|
PharmacyState.Idle -> {}
|
||||||
@@ -190,4 +134,51 @@ fun PharmacyBottomSheet(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
private fun ProvinceSelector(
|
||||||
|
selectedProvinceId: String,
|
||||||
|
expanded: Boolean,
|
||||||
|
onExpandedChange: (Boolean) -> Unit,
|
||||||
|
onProvinceSelected: (String) -> Unit
|
||||||
|
) {
|
||||||
|
ExposedDropdownMenuBox(
|
||||||
|
expanded = expanded,
|
||||||
|
onExpandedChange = onExpandedChange
|
||||||
|
) {
|
||||||
|
OutlinedTextField(
|
||||||
|
value = provinces.find { it.id == selectedProvinceId }?.name ?: "انتخاب استان",
|
||||||
|
onValueChange = {},
|
||||||
|
readOnly = true,
|
||||||
|
label = { Text("استان") },
|
||||||
|
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) },
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.menuAnchor(),
|
||||||
|
shape = MaterialTheme.shapes.large
|
||||||
|
)
|
||||||
|
ExposedDropdownMenu(
|
||||||
|
expanded = expanded,
|
||||||
|
onDismissRequest = { onExpandedChange(false) }
|
||||||
|
) {
|
||||||
|
provinces.forEach { province ->
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text(province.name) },
|
||||||
|
onClick = { onProvinceSelected(province.id) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun EmptyMessage(message: String) {
|
||||||
|
Text(
|
||||||
|
text = message,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package com.approagency.pharmacy.utils
|
package com.approagency.pharmacy.utils
|
||||||
|
|
||||||
object Config {
|
object Config {
|
||||||
const val BASE_URL = "https://drug.approagency.ir/api/"
|
|
||||||
const val Darro_Url = "https://www.darooyab.ir/"
|
const val Darro_Url = "https://www.darooyab.ir/"
|
||||||
|
|
||||||
/** بکاند اشتراک/احراز هویت آپرواجنسی (لاگین، وضعیت، محصولات، خرید). */
|
/** بکاند اشتراک/احراز هویت آپرواجنسی (لاگین، وضعیت، محصولات، خرید). */
|
||||||
|
|||||||
Reference in New Issue
Block a user