feat: remove drug.approagncy api
This commit is contained in:
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
|
||||||
|
|||||||
@@ -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