diff --git a/.idea/misc.xml b/.idea/misc.xml index 1896758..08ffc7e 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,4 +1,3 @@ - diff --git a/app/src/main/assets/DrugStore.db b/app/src/main/assets/DrugStore.db new file mode 100644 index 0000000..875e15d Binary files /dev/null and b/app/src/main/assets/DrugStore.db differ diff --git a/app/src/main/java/com/approagency/pharmacy/data/local/LocalDrugDataSource.kt b/app/src/main/java/com/approagency/pharmacy/data/local/LocalDrugDataSource.kt new file mode 100644 index 0000000..2d4cee3 --- /dev/null +++ b/app/src/main/java/com/approagency/pharmacy/data/local/LocalDrugDataSource.kt @@ -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() + 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 + """ + } +} diff --git a/app/src/main/java/com/approagency/pharmacy/data/remote/DrugApiService.kt b/app/src/main/java/com/approagency/pharmacy/data/remote/DrugApiService.kt deleted file mode 100644 index 7cea760..0000000 --- a/app/src/main/java/com/approagency/pharmacy/data/remote/DrugApiService.kt +++ /dev/null @@ -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 - -} \ No newline at end of file diff --git a/app/src/main/java/com/approagency/pharmacy/data/repository/DrugRepositoryImpl.kt b/app/src/main/java/com/approagency/pharmacy/data/repository/DrugRepositoryImpl.kt index cbadbe1..7b4f98d 100644 --- a/app/src/main/java/com/approagency/pharmacy/data/repository/DrugRepositoryImpl.kt +++ b/app/src/main/java/com/approagency/pharmacy/data/repository/DrugRepositoryImpl.kt @@ -3,8 +3,8 @@ 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.local.LocalDrugDataSource 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 @@ -20,34 +20,39 @@ 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, + private val localDrugDataSource: LocalDrugDataSource, ): DrugRepository { + // اطلاعات دارو همیشه از پایگاه‌دادهٔ محلیِ همراهِ اپ (assets/DrugStore.db) خوانده می‌شود. override suspend fun searchDrug(params: DrugSearchParams): Result { 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.success( + localDrugDataSource.searchDrugs( + query = params.query, + drugGroup = params.drugGroup, + healGroup = params.healGroup, + perPage = params.perPage + ) + ) + } catch (e: Exception) { Result.failure(e) } } override suspend fun drugDetail(cod: Int): Result { return try { - val response= apiService.getDrugDetail(cod = cod) - return Result.success(response) - }catch (e: Exception){ + Result.success(localDrugDataSource.drugDetail(cod)) + } catch (e: Exception) { Result.failure(e) } } override suspend fun getGorohDaroei(): Result { - return try { - val response = apiService.getGorohDaroei() - return Result.success(response) - }catch (e: Exception){ + return try { + Result.success(localDrugDataSource.darmanGroups()) + } catch (e: Exception) { Result.failure(e) } } diff --git a/app/src/main/java/com/approagency/pharmacy/di/AppModule.kt b/app/src/main/java/com/approagency/pharmacy/di/AppModule.kt index 40d5027..6c8159e 100644 --- a/app/src/main/java/com/approagency/pharmacy/di/AppModule.kt +++ b/app/src/main/java/com/approagency/pharmacy/di/AppModule.kt @@ -2,12 +2,12 @@ package com.approagency.pharmacy.di 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.database.LabDatabase import com.approagency.pharmacy.data.remote.ApproApiService import com.approagency.pharmacy.data.remote.AuthInterceptor 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.AuthRepositoryImpl @@ -47,7 +47,6 @@ 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 authRetrofitQualifier = qualifier("authRetrofit") val appModule= module { @@ -76,19 +75,6 @@ val appModule= module { .build() } - single(jsonRetrofitQualifier) { // Add qualifier - Retrofit.Builder() - .baseUrl(Config.BASE_URL) - .client(get()) - .addConverterFactory(GsonConverterFactory.create()) - .build() - } - - single { - val retrofit: Retrofit = get(jsonRetrofitQualifier) // Get qualified instance - retrofit.create(DrugApiService::class.java) - } - // ========== Retrofit for DAROOYAB Website ========== single(scalarRetrofitQualifier) { // Add qualifier Retrofit.Builder() @@ -135,9 +121,12 @@ val appModule= module { factory { DrugHtmlParser() } factory { DrugDetailParser() } + // منبع آفلاین دارو (assets/DrugStore.db) برای fallback هنگام خطای شبکه + single { LocalDrugDataSource(androidContext()) } + //repo single { - DrugRepositoryImpl(get() , get() , get() , get()) + DrugRepositoryImpl(get() , get() , get() , get()) } diff --git a/app/src/main/java/com/approagency/pharmacy/navigation/AppNavGraph.kt b/app/src/main/java/com/approagency/pharmacy/navigation/AppNavGraph.kt index d368d63..15bf0ba 100644 --- a/app/src/main/java/com/approagency/pharmacy/navigation/AppNavGraph.kt +++ b/app/src/main/java/com/approagency/pharmacy/navigation/AppNavGraph.kt @@ -20,7 +20,7 @@ fun AppNavGraph() { NavHost( navController = navController, - startDestination = "home" + startDestination = "search" ) { // ─────────────── diff --git a/app/src/main/java/com/approagency/pharmacy/navigation/BottomBar.kt b/app/src/main/java/com/approagency/pharmacy/navigation/BottomBar.kt index e37c6f2..dd99395 100644 --- a/app/src/main/java/com/approagency/pharmacy/navigation/BottomBar.kt +++ b/app/src/main/java/com/approagency/pharmacy/navigation/BottomBar.kt @@ -26,6 +26,7 @@ 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.NavGraph.Companion.findStartDestination import androidx.navigation.NavHostController import androidx.navigation.compose.currentBackStackEntryAsState @@ -70,7 +71,7 @@ fun BottomBar(navController: NavHostController) { .clickable { if (!selected) { navController.navigate(item.first) { - popUpTo("home") { + popUpTo(navController.graph.findStartDestination().id) { saveState = true } launchSingleTop = true diff --git a/app/src/main/java/com/approagency/pharmacy/utils/Config.kt b/app/src/main/java/com/approagency/pharmacy/utils/Config.kt index 163a516..3f308c6 100644 --- a/app/src/main/java/com/approagency/pharmacy/utils/Config.kt +++ b/app/src/main/java/com/approagency/pharmacy/utils/Config.kt @@ -1,7 +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/" /** بک‌اند اشتراک/احراز هویت آپرواجنسی (لاگین، وضعیت، محصولات، خرید). */