Compare commits

..
2 Commits
Author SHA1 Message Date
Amirmahdi 1971708b3f feat: remove code 2026-06-14 15:31:13 +03:30
Amirmahdi 27dc08fa68 feat: remove drug.approagncy api 2026-06-14 08:18:01 +03:30
11 changed files with 391 additions and 229 deletions
-1
View File
@@ -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
@@ -1,76 +1,99 @@
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 ( CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) {
modifier = modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.End
) {
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, Row(
color = MaterialTheme.colorScheme.primary modifier = Modifier.fillMaxWidth(),
) horizontalArrangement = Arrangement.spacedBy(MaterialTheme.dime.sm)
Row( ) {
modifier = modifier.fillMaxWidth(), Button(
horizontalArrangement = Arrangement.SpaceBetween onClick = { onClickDrugStore(drug) },
) { modifier = Modifier
TextButton(onClick = {onClickDetail(drug)} , content = { .weight(1f)
Text("اطلاعات دارویی") .height(44.dp)
}) ) {
TextButton(onClick = {onClickDrugStore(drug)} , content = { Icon(
Text("موجودی داروخانه") 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)
}
}
} }
} }
} }
} }
@@ -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,161 +33,152 @@ 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 { Spacer(Modifier.height(MaterialTheme.dime.md))
ExposedDropdownMenuBox(
expanded = showProvinceDropdown, ProvinceSelector(
onExpandedChange = { showProvinceDropdown = it }) { selectedProvinceId = selectedProvinceId,
OutlinedTextField( expanded = showProvinceDropdown,
value = provinces.find { it.id == selectedProvinceId }?.name onExpandedChange = { showProvinceDropdown = it },
?: "انتخاب استان", onProvinceSelected = { id ->
onValueChange = {}, selectedProvinceId = id
readOnly = true, showProvinceDropdown = false
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = showProvinceDropdown) }, viewModel.search(genericDrugId, brandIrc, id)
modifier = Modifier }
.fillMaxWidth() )
.menuAnchor(),
shape = MaterialTheme.shapes.large Spacer(Modifier.height(MaterialTheme.dime.md))
// ناحیه‌ی محتوا فضای باقی‌مانده را پر می‌کند؛ سربرگ بالا ثابت می‌ماند.
Box(
modifier = Modifier
.fillMaxWidth()
.weight(1f),
contentAlignment = Alignment.Center
) {
when (val state = pharmacyState) {
PharmacyState.Loading -> Loading(
modifier = Modifier.fillMaxSize(),
message = "در حال جستجو..."
) )
ExposedDropdownMenu( is PharmacyState.Success -> {
expanded = showProvinceDropdown, if (state.items.isEmpty()) {
onDismissRequest = { showProvinceDropdown = false }) { EmptyMessage("هیچ داروخانه‌ای یافت نشد")
provinces.forEach { province -> } else {
DropdownMenuItem(text = { Text(province.name) }, onClick = { LazyColumn(
selectedProvinceId = province.id modifier = Modifier.fillMaxSize(),
showProvinceDropdown = false verticalArrangement = Arrangement.spacedBy(MaterialTheme.dime.sm)
// جستجو با استان جدید
viewModel.search(genericDrugId, brandIrc, province.id)
})
}
}
}
}
// انتخاب استان
// نمایش نتایج
when (val state = pharmacyState) {
PharmacyState.Loading -> {
item {
Loading(
modifier = Modifier
.fillMaxWidth()
.height(400.dp),
message = "در حال جستجو..."
)
}
}
is PharmacyState.Success -> {
if (state.items.isEmpty()) {
item {
Box(
modifier = Modifier
.fillMaxWidth()
.height(400.dp),
contentAlignment = Alignment.Center
) { ) {
Text( items(state.items) { pharmacy ->
text = "هیچ داروخانه‌ای یافت نشد", PharmacyResultItem(pharmacy = pharmacy, viewModel)
fontSize = MaterialTheme.typography.bodyMedium.fontSize, }
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f) item {
) Text(
} text = "تعداد کل: ${state.items.size} داروخانه",
} style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
} else { modifier = Modifier.padding(vertical = MaterialTheme.dime.sm)
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(MaterialTheme.dime.sm))
TextButton(
onClick = { viewModel.retry() }) {
Text("تلاش مجدد")
} }
} }
} }
} }
} is PharmacyState.Error -> Column(
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = state.message,
color = MaterialTheme.colorScheme.error,
style = MaterialTheme.typography.bodyMedium
)
Spacer(Modifier.height(MaterialTheme.dime.sm))
TextButton(onClick = { viewModel.retry() }) { Text("تلاش مجدد") }
}
PharmacyState.Idle -> {} PharmacyState.Idle -> {}
}
} }
} }
} }
} }
} }
} }
@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/"
/** بک‌اند اشتراک/احراز هویت آپرواجنسی (لاگین، وضعیت، محصولات، خرید). */ /** بک‌اند اشتراک/احراز هویت آپرواجنسی (لاگین، وضعیت، محصولات، خرید). */