diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 6a794d7..5ce0012 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -29,6 +29,9 @@ android:theme="@style/Theme.Ecommerce_serang" android:usesCleartextTraffic="true" tools:targetApi="31"> + @@ -64,7 +67,7 @@ + android:exported="false" /> @@ -157,6 +160,7 @@ + @@ -168,6 +172,4 @@ android:value="fcm_default_channel" /> - - \ No newline at end of file diff --git a/app/src/main/java/com/alya/ecommerce_serang/app/App.kt b/app/src/main/java/com/alya/ecommerce_serang/app/App.kt index 55e0321..fa3dd4f 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/app/App.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/app/App.kt @@ -1,50 +1,17 @@ package com.alya.ecommerce_serang.app import android.app.Application -import android.content.Context -import android.util.Log -import com.google.firebase.FirebaseApp -import com.google.firebase.messaging.FirebaseMessaging import dagger.hilt.android.HiltAndroidApp @HiltAndroidApp class App : Application(){ - private val TAG = "AppSerang" +// private val TAG = "AppSerang" +// +//// var tokenTes: String? = null +// +// override fun onCreate() { +// +// } -// var tokenTes: String? = null - override fun onCreate() { - super.onCreate() - - // Initialize Firebase - FirebaseApp.initializeApp(this) - - // Request FCM token at app startup - retrieveFCMToken() - } - - private fun retrieveFCMToken() { - FirebaseMessaging.getInstance().token - .addOnCompleteListener { task -> - if (!task.isSuccessful) { - Log.e(TAG, "Failed to get FCM token", task.exception) - return@addOnCompleteListener - } - - val token = task.result -// tokenTes = token - Log.d(TAG, "FCM token retrieved: $token") - - // Save token locally - val sharedPreferences = getSharedPreferences("FCM_PREFS", Context.MODE_PRIVATE) - sharedPreferences.edit().putString("FCM_TOKEN", token).apply() - - // Send to your server - sendTokenToServer(token) - } - } - - private fun sendTokenToServer(token: String) { - Log.d(TAG, "Would send token to server: $token") - } } \ No newline at end of file diff --git a/app/src/main/java/com/alya/ecommerce_serang/data/api/dto/CancelOrderReq.kt b/app/src/main/java/com/alya/ecommerce_serang/data/api/dto/CancelOrderReq.kt new file mode 100644 index 0000000..1009818 --- /dev/null +++ b/app/src/main/java/com/alya/ecommerce_serang/data/api/dto/CancelOrderReq.kt @@ -0,0 +1,11 @@ +package com.alya.ecommerce_serang.data.api.dto + +import com.google.gson.annotations.SerializedName + +data class CancelOrderReq ( + @SerializedName("order_id") + val orderId: Int, + + @SerializedName("reason") + val reason: String +) \ No newline at end of file diff --git a/app/src/main/java/com/alya/ecommerce_serang/data/api/dto/FcmReq.kt b/app/src/main/java/com/alya/ecommerce_serang/data/api/dto/FcmReq.kt new file mode 100644 index 0000000..0f64faf --- /dev/null +++ b/app/src/main/java/com/alya/ecommerce_serang/data/api/dto/FcmReq.kt @@ -0,0 +1,8 @@ +package com.alya.ecommerce_serang.data.api.dto + +import com.google.gson.annotations.SerializedName + +data class FcmReq ( + @SerializedName("fcm_req") + val fcmToken: String?= null +) \ No newline at end of file diff --git a/app/src/main/java/com/alya/ecommerce_serang/data/api/dto/VerifRegisReq.kt b/app/src/main/java/com/alya/ecommerce_serang/data/api/dto/VerifRegisReq.kt new file mode 100644 index 0000000..3b1054d --- /dev/null +++ b/app/src/main/java/com/alya/ecommerce_serang/data/api/dto/VerifRegisReq.kt @@ -0,0 +1,11 @@ +package com.alya.ecommerce_serang.data.api.dto + +import com.google.gson.annotations.SerializedName + +data class VerifRegisReq ( + @SerializedName("field") + val fieldRegis: String, + + @SerializedName("value") + val valueRegis: String +) \ No newline at end of file diff --git a/app/src/main/java/com/alya/ecommerce_serang/data/api/response/auth/FcmTokenResponse.kt b/app/src/main/java/com/alya/ecommerce_serang/data/api/response/auth/FcmTokenResponse.kt new file mode 100644 index 0000000..0790e8c --- /dev/null +++ b/app/src/main/java/com/alya/ecommerce_serang/data/api/response/auth/FcmTokenResponse.kt @@ -0,0 +1,9 @@ +package com.alya.ecommerce_serang.data.api.response.auth + +import com.google.gson.annotations.SerializedName + +data class FcmTokenResponse( + + @field:SerializedName("message") + val message: String? = null +) diff --git a/app/src/main/java/com/alya/ecommerce_serang/data/api/response/auth/HasStoreResponse.kt b/app/src/main/java/com/alya/ecommerce_serang/data/api/response/auth/HasStoreResponse.kt new file mode 100644 index 0000000..cd0a881 --- /dev/null +++ b/app/src/main/java/com/alya/ecommerce_serang/data/api/response/auth/HasStoreResponse.kt @@ -0,0 +1,9 @@ +package com.alya.ecommerce_serang.data.api.response.auth + +import com.google.gson.annotations.SerializedName + +data class HasStoreResponse( + + @field:SerializedName("hasStore") + val hasStore: Boolean +) diff --git a/app/src/main/java/com/alya/ecommerce_serang/data/api/response/auth/ListStoreTypeResponse.kt b/app/src/main/java/com/alya/ecommerce_serang/data/api/response/auth/ListStoreTypeResponse.kt new file mode 100644 index 0000000..c30af1f --- /dev/null +++ b/app/src/main/java/com/alya/ecommerce_serang/data/api/response/auth/ListStoreTypeResponse.kt @@ -0,0 +1,21 @@ +package com.alya.ecommerce_serang.data.api.response.auth + +import com.google.gson.annotations.SerializedName + +data class ListStoreTypeResponse( + + @field:SerializedName("storeTypes") + val storeTypes: List, + + @field:SerializedName("message") + val message: String +) + +data class StoreTypesItem( + + @field:SerializedName("name") + val name: String, + + @field:SerializedName("id") + val id: Int +) diff --git a/app/src/main/java/com/alya/ecommerce_serang/data/api/response/auth/RegisterStoreResponse.kt b/app/src/main/java/com/alya/ecommerce_serang/data/api/response/auth/RegisterStoreResponse.kt new file mode 100644 index 0000000..973e594 --- /dev/null +++ b/app/src/main/java/com/alya/ecommerce_serang/data/api/response/auth/RegisterStoreResponse.kt @@ -0,0 +1,57 @@ +package com.alya.ecommerce_serang.data.api.response.auth + +import com.google.gson.annotations.SerializedName + +data class RegisterStoreResponse( + + @field:SerializedName("store") + val store: Store, + + @field:SerializedName("message") + val message: String +) + +data class Store( + + @field:SerializedName("image") + val image: String, + + @field:SerializedName("ktp") + val ktp: String, + + @field:SerializedName("nib") + val nib: String, + + @field:SerializedName("npwp") + val npwp: String, + + @field:SerializedName("address_id") + val addressId: Int, + + @field:SerializedName("description") + val description: String, + + @field:SerializedName("store_type_id") + val storeTypeId: Int, + + @field:SerializedName("is_on_leave") + val isOnLeave: Boolean, + + @field:SerializedName("balance") + val balance: String, + + @field:SerializedName("user_id") + val userId: Int, + + @field:SerializedName("name") + val name: String, + + @field:SerializedName("persetujuan") + val persetujuan: String, + + @field:SerializedName("id") + val id: Int, + + @field:SerializedName("status") + val status: String +) diff --git a/app/src/main/java/com/alya/ecommerce_serang/data/api/response/customer/order/CancelOrderResponse.kt b/app/src/main/java/com/alya/ecommerce_serang/data/api/response/customer/order/CancelOrderResponse.kt new file mode 100644 index 0000000..24adeed --- /dev/null +++ b/app/src/main/java/com/alya/ecommerce_serang/data/api/response/customer/order/CancelOrderResponse.kt @@ -0,0 +1,14 @@ +package com.alya.ecommerce_serang.data.api.response.customer.order + +data class CancelOrderResponse( + val data: DataCancel, + val message: String +) + +data class DataCancel( + val reason: String, + val createdAt: String, + val id: Int, + val orderId: Int +) + diff --git a/app/src/main/java/com/alya/ecommerce_serang/data/api/retrofit/ApiConfig.kt b/app/src/main/java/com/alya/ecommerce_serang/data/api/retrofit/ApiConfig.kt index 026c995..78e0394 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/data/api/retrofit/ApiConfig.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/data/api/retrofit/ApiConfig.kt @@ -22,6 +22,9 @@ class ApiConfig { val client = OkHttpClient.Builder() .addInterceptor(loggingInterceptor) .addInterceptor(authInterceptor) + .connectTimeout(60, TimeUnit.SECONDS) // Increase to 60 seconds + .readTimeout(60, TimeUnit.SECONDS) // Increase to 60 seconds + .writeTimeout(60, TimeUnit.SECONDS) .build() val retrofit = Retrofit.Builder() diff --git a/app/src/main/java/com/alya/ecommerce_serang/data/api/retrofit/ApiService.kt b/app/src/main/java/com/alya/ecommerce_serang/data/api/retrofit/ApiService.kt index 05322c5..195ef18 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/data/api/retrofit/ApiService.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/data/api/retrofit/ApiService.kt @@ -3,11 +3,13 @@ package com.alya.ecommerce_serang.data.api.retrofit import com.alya.ecommerce_serang.data.api.dto.AddEvidenceRequest import com.alya.ecommerce_serang.data.api.dto.AddPaymentInfoResponse +import com.alya.ecommerce_serang.data.api.dto.CancelOrderReq import com.alya.ecommerce_serang.data.api.dto.CartItem import com.alya.ecommerce_serang.data.api.dto.CityResponse import com.alya.ecommerce_serang.data.api.dto.CompletedOrderRequest import com.alya.ecommerce_serang.data.api.dto.CourierCostRequest import com.alya.ecommerce_serang.data.api.dto.CreateAddressRequest +import com.alya.ecommerce_serang.data.api.dto.FcmReq import com.alya.ecommerce_serang.data.api.dto.LoginRequest import com.alya.ecommerce_serang.data.api.dto.OrderRequest import com.alya.ecommerce_serang.data.api.dto.OrderRequestBuy @@ -20,10 +22,16 @@ import com.alya.ecommerce_serang.data.api.dto.ShippingServiceRequest import com.alya.ecommerce_serang.data.api.dto.StoreAddressResponse import com.alya.ecommerce_serang.data.api.dto.UpdateCart import com.alya.ecommerce_serang.data.api.dto.UpdateChatRequest +import com.alya.ecommerce_serang.data.api.dto.VerifRegisReq import com.alya.ecommerce_serang.data.api.response.auth.CheckStoreResponse +import com.alya.ecommerce_serang.data.api.response.auth.FcmTokenResponse +import com.alya.ecommerce_serang.data.api.response.auth.HasStoreResponse +import com.alya.ecommerce_serang.data.api.response.auth.ListStoreTypeResponse import com.alya.ecommerce_serang.data.api.response.auth.LoginResponse import com.alya.ecommerce_serang.data.api.response.auth.OtpResponse import com.alya.ecommerce_serang.data.api.response.auth.RegisterResponse +import com.alya.ecommerce_serang.data.api.response.auth.RegisterStoreResponse +import com.alya.ecommerce_serang.data.api.response.auth.VerifRegisterResponse import com.alya.ecommerce_serang.data.api.response.chat.ChatHistoryResponse import com.alya.ecommerce_serang.data.api.response.chat.ChatListResponse import com.alya.ecommerce_serang.data.api.response.chat.SendChatResponse @@ -32,6 +40,7 @@ import com.alya.ecommerce_serang.data.api.response.customer.cart.AddCartResponse import com.alya.ecommerce_serang.data.api.response.customer.cart.DeleteCartResponse import com.alya.ecommerce_serang.data.api.response.customer.cart.ListCartResponse import com.alya.ecommerce_serang.data.api.response.customer.cart.UpdateCartResponse +import com.alya.ecommerce_serang.data.api.response.customer.order.CancelOrderResponse import com.alya.ecommerce_serang.data.api.response.customer.order.CourierCostResponse import com.alya.ecommerce_serang.data.api.response.customer.order.CreateOrderResponse import com.alya.ecommerce_serang.data.api.response.customer.order.CreateReviewResponse @@ -73,6 +82,7 @@ import retrofit2.http.Multipart import retrofit2.http.POST import retrofit2.http.PUT import retrofit2.http.Part +import retrofit2.http.PartMap import retrofit2.http.Path import retrofit2.http.Query @@ -82,20 +92,54 @@ interface ApiService { @Body registerRequest: RegisterRequest ): Response + @POST("verif") + suspend fun verifValue ( + @Body verifRegisReq: VerifRegisReq + ):VerifRegisterResponse + @GET("checkstore") suspend fun checkStore (): Response -// @Multipart -// @POST("registerstore") -// suspend fun registerStore( -// -// ): Response<> + @Multipart + @POST("registerstore") + suspend fun registerStore( + @Part("description") description: RequestBody, + @Part("store_type_id") storeTypeId: RequestBody, + @Part("latitude") latitude: RequestBody, + @Part("longitude") longitude: RequestBody, + @Part("street") street: RequestBody, + @Part("subdistrict") subdistrict: RequestBody, + @Part("city_id") cityId: RequestBody, + @Part("province_id") provinceId: RequestBody, + @Part("postal_code") postalCode: RequestBody, + @Part("detail") detail: RequestBody, + @Part("bank_name") bankName: RequestBody, + @Part("bank_num") bankNum: RequestBody, + @Part("store_name") storeName: RequestBody, + @Part storeimg: MultipartBody.Part?, + @Part ktp: MultipartBody.Part?, + @Part npwp: MultipartBody.Part?, + @Part nib: MultipartBody.Part?, + @Part persetujuan: MultipartBody.Part?, + @PartMap couriers: Map, + @Part qris: MultipartBody.Part?, + @Part("account_name") accountName: RequestBody, + ): Response @POST("otp") suspend fun getOTP( @Body otpRequest: OtpRequest ):OtpResponse + @PUT("updatefcm") + suspend fun updateFcm( + @Body fcmReq: FcmReq + ): FcmTokenResponse + + @GET("checkstore") + suspend fun checkStoreUser( + ): HasStoreResponse + @POST("login") suspend fun login( @Body loginRequest: LoginRequest @@ -105,6 +149,10 @@ interface ApiService { suspend fun allCategory( ): Response + @GET("storetype") + suspend fun listTypeStore( + ): Response + @GET("product") suspend fun getAllProduct(): Response @@ -131,6 +179,11 @@ interface ApiService { @Body request: OrderRequest ): Response + @POST("order/cancel") + suspend fun cancelOrder( + @Body cancelReq: CancelOrderReq + ): Response + @GET("order/detail/{id}") suspend fun getDetailOrder( @Path("id") orderId: Int diff --git a/app/src/main/java/com/alya/ecommerce_serang/data/repository/OrderRepository.kt b/app/src/main/java/com/alya/ecommerce_serang/data/repository/OrderRepository.kt index 080c3cb..e162141 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/data/repository/OrderRepository.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/data/repository/OrderRepository.kt @@ -2,16 +2,17 @@ package com.alya.ecommerce_serang.data.repository import android.util.Log import com.alya.ecommerce_serang.data.api.dto.AddEvidenceMultipartRequest +import com.alya.ecommerce_serang.data.api.dto.CancelOrderReq import com.alya.ecommerce_serang.data.api.dto.CompletedOrderRequest import com.alya.ecommerce_serang.data.api.dto.CourierCostRequest import com.alya.ecommerce_serang.data.api.dto.CreateAddressRequest import com.alya.ecommerce_serang.data.api.dto.OrderRequest import com.alya.ecommerce_serang.data.api.dto.OrderRequestBuy -import com.alya.ecommerce_serang.data.api.dto.OrdersItem import com.alya.ecommerce_serang.data.api.dto.ReviewProductItem import com.alya.ecommerce_serang.data.api.dto.UpdateCart import com.alya.ecommerce_serang.data.api.dto.UserProfile import com.alya.ecommerce_serang.data.api.response.customer.cart.DataItemCart +import com.alya.ecommerce_serang.data.api.response.customer.order.CancelOrderResponse import com.alya.ecommerce_serang.data.api.response.customer.order.CourierCostResponse import com.alya.ecommerce_serang.data.api.response.customer.order.CreateOrderResponse import com.alya.ecommerce_serang.data.api.response.customer.order.CreateReviewResponse @@ -491,4 +492,23 @@ class OrderRepository(private val apiService: ApiService) { } + suspend fun cancelOrder(cancelReq: CancelOrderReq): Result{ + return try{ + val response= apiService.cancelOrder(cancelReq) + + if (response.isSuccessful){ + response.body()?.let { cancelOrderResponse -> + Result.Success(cancelOrderResponse) + } ?: run { + Result.Error(Exception("Failed to cancel order")) + } + } else { + val errorMsg = response.errorBody()?.string() ?: "Unknown Error" + Result.Error(Exception(errorMsg)) + } + }catch (e: Exception){ + Result.Error(e) + } + } + } \ No newline at end of file diff --git a/app/src/main/java/com/alya/ecommerce_serang/data/repository/UserRepository.kt b/app/src/main/java/com/alya/ecommerce_serang/data/repository/UserRepository.kt index ff202ee..cdffe08 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/data/repository/UserRepository.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/data/repository/UserRepository.kt @@ -3,17 +3,30 @@ package com.alya.ecommerce_serang.data.repository import android.content.Context import android.net.Uri import android.util.Log +import com.alya.ecommerce_serang.data.api.dto.FcmReq import com.alya.ecommerce_serang.data.api.dto.LoginRequest import com.alya.ecommerce_serang.data.api.dto.OtpRequest import com.alya.ecommerce_serang.data.api.dto.RegisterRequest import com.alya.ecommerce_serang.data.api.dto.UserProfile +import com.alya.ecommerce_serang.data.api.dto.VerifRegisReq +import com.alya.ecommerce_serang.data.api.response.auth.FcmTokenResponse +import com.alya.ecommerce_serang.data.api.response.auth.HasStoreResponse +import com.alya.ecommerce_serang.data.api.response.auth.ListStoreTypeResponse import com.alya.ecommerce_serang.data.api.response.auth.LoginResponse import com.alya.ecommerce_serang.data.api.response.auth.OtpResponse +import com.alya.ecommerce_serang.data.api.response.auth.RegisterStoreResponse +import com.alya.ecommerce_serang.data.api.response.auth.VerifRegisterResponse +import com.alya.ecommerce_serang.data.api.response.customer.order.ListCityResponse +import com.alya.ecommerce_serang.data.api.response.customer.order.ListProvinceResponse import com.alya.ecommerce_serang.data.api.response.customer.profile.EditProfileResponse import com.alya.ecommerce_serang.data.api.retrofit.ApiService import com.alya.ecommerce_serang.utils.FileUtils import okhttp3.MediaType.Companion.toMediaTypeOrNull +import okhttp3.MultipartBody +import okhttp3.RequestBody +import okhttp3.RequestBody.Companion.asRequestBody import okhttp3.RequestBody.Companion.toRequestBody +import java.io.File class UserRepository(private val apiService: ApiService) { //post data without message/response @@ -21,6 +34,31 @@ class UserRepository(private val apiService: ApiService) { return apiService.getOTP(OtpRequest(email)) } + suspend fun listStoreType(): Result{ + return try{ + val response = apiService.listTypeStore() + if (response.isSuccessful) { + response.body()?.let { + Result.Success(it) + } ?: Result.Error(Exception("No store type")) + } else { + throw Exception("No response ${response.errorBody()?.string()}") + } + } catch (e:Exception){ + Result.Error(e) + } + } + + suspend fun getListProvinces(): ListProvinceResponse? { + val response = apiService.getListProv() + return if (response.isSuccessful) response.body() else null + } + + suspend fun getListCities(provId : Int): ListCityResponse? { + val response = apiService.getCityProvId(provId) + return if (response.isSuccessful) response.body() else null + } + suspend fun registerUser(request: RegisterRequest): String { val response = apiService.register(request) // API call @@ -32,6 +70,169 @@ class UserRepository(private val apiService: ApiService) { } } + suspend fun registerStoreUser( + context: Context, + description: String, + storeTypeId: Int, + latitude: String, + longitude: String, + street: String, + subdistrict: String, + cityId: Int, + provinceId: Int, + postalCode: Int, + detail: String?, + bankName: String, + bankNum: Int, + storeName: String, + storeImg: Uri?, + ktp: Uri?, + npwp: Uri?, + nib: Uri?, + persetujuan: Uri?, + couriers: List, + qris: Uri?, + accountName: String + ): Result { + return try { + val descriptionPart = description.toRequestBody("text/plain".toMediaTypeOrNull()) + val storeTypeIdPart = storeTypeId.toString().toRequestBody("text/plain".toMediaTypeOrNull()) + val latitudePart = latitude.toRequestBody("text/plain".toMediaTypeOrNull()) + val longitudePart = longitude.toRequestBody("text/plain".toMediaTypeOrNull()) + val streetPart = street.toRequestBody("text/plain".toMediaTypeOrNull()) + val subdistrictPart = subdistrict.toRequestBody("text/plain".toMediaTypeOrNull()) + val cityIdPart = cityId.toString().toRequestBody("text/plain".toMediaTypeOrNull()) + val provinceIdPart = provinceId.toString().toRequestBody("text/plain".toMediaTypeOrNull()) + val postalCodePart = postalCode.toString().toRequestBody("text/plain".toMediaTypeOrNull()) + val detailPart = detail?.toRequestBody("text/plain".toMediaTypeOrNull()) + val bankNamePart = bankName.toRequestBody("text/plain".toMediaTypeOrNull()) + val bankNumPart = bankNum.toString().toRequestBody("text/plain".toMediaTypeOrNull()) + val storeNamePart = storeName.toRequestBody("text/plain".toMediaTypeOrNull()) + val accountNamePart = accountName.toRequestBody("text/plain".toMediaTypeOrNull()) + + + // Create a Map for courier values + val courierMap = HashMap() + couriers.forEach { courier -> + courierMap["couriers[]"] = courier.toRequestBody("text/plain".toMediaTypeOrNull()) + } + + // Convert URIs to MultipartBody.Part + val storeImgPart = storeImg?.let { + val inputStream = context.contentResolver.openInputStream(it) + val file = File(context.cacheDir, "store_img_${System.currentTimeMillis()}") + inputStream?.use { input -> + file.outputStream().use { output -> + input.copyTo(output) + } + } + val mimeType = context.contentResolver.getType(it) ?: "application/octet-stream" + val requestFile = file.asRequestBody(mimeType.toMediaTypeOrNull()) + MultipartBody.Part.createFormData("storeimg", file.name, requestFile) + } + + val ktpPart = ktp?.let { + val inputStream = context.contentResolver.openInputStream(it) + val file = File(context.cacheDir, "ktp_${System.currentTimeMillis()}") + inputStream?.use { input -> + file.outputStream().use { output -> + input.copyTo(output) + } + } + val mimeType = context.contentResolver.getType(it) ?: "application/octet-stream" + val requestFile = file.asRequestBody(mimeType.toMediaTypeOrNull()) + MultipartBody.Part.createFormData("ktp", file.name, requestFile) + } + + val npwpPart = npwp?.let { + val inputStream = context.contentResolver.openInputStream(it) + val file = File(context.cacheDir, "npwp_${System.currentTimeMillis()}") + inputStream?.use { input -> + file.outputStream().use { output -> + input.copyTo(output) + } + } + val mimeType = context.contentResolver.getType(it) ?: "application/octet-stream" + val requestFile = file.asRequestBody(mimeType.toMediaTypeOrNull()) + MultipartBody.Part.createFormData("npwp", file.name, requestFile) + } + + val nibPart = nib?.let { + val inputStream = context.contentResolver.openInputStream(it) + val file = File(context.cacheDir, "nib_${System.currentTimeMillis()}") + inputStream?.use { input -> + file.outputStream().use { output -> + input.copyTo(output) + } + } + val mimeType = context.contentResolver.getType(it) ?: "application/octet-stream" + val requestFile = file.asRequestBody(mimeType.toMediaTypeOrNull()) + MultipartBody.Part.createFormData("nib", file.name, requestFile) + } + + val persetujuanPart = persetujuan?.let { + val inputStream = context.contentResolver.openInputStream(it) + val file = File(context.cacheDir, "persetujuan_${System.currentTimeMillis()}") + inputStream?.use { input -> + file.outputStream().use { output -> + input.copyTo(output) + } + } + val mimeType = context.contentResolver.getType(it) ?: "application/octet-stream" + val requestFile = file.asRequestBody(mimeType.toMediaTypeOrNull()) + MultipartBody.Part.createFormData("persetujuan", file.name, requestFile) + } + + val qrisPart = qris?.let { + val inputStream = context.contentResolver.openInputStream(it) + val file = File(context.cacheDir, "qris_${System.currentTimeMillis()}") + inputStream?.use { input -> + file.outputStream().use { output -> + input.copyTo(output) + } + } + val mimeType = context.contentResolver.getType(it) ?: "application/octet-stream" + val requestFile = file.asRequestBody(mimeType.toMediaTypeOrNull()) + MultipartBody.Part.createFormData("qris", file.name, requestFile) + } + + // Make the API call + val response = apiService.registerStore( + descriptionPart, + storeTypeIdPart, + latitudePart, + longitudePart, + streetPart, + subdistrictPart, + cityIdPart, + provinceIdPart, + postalCodePart, + detailPart ?: "".toRequestBody("text/plain".toMediaTypeOrNull()), + bankNamePart, + bankNumPart, + storeNamePart, + storeImgPart, + ktpPart, + npwpPart, + nibPart, + persetujuanPart, + courierMap, + qrisPart, + accountNamePart + ) + + // Check if response is successful + if (response.isSuccessful) { + Result.Success(response.body() ?: throw Exception("Response body is null")) + } else { + Result.Error(Exception("Registration failed with code: ${response.code()}")) + } + + } catch (e: Exception) { + Result.Error(e) + } + } + suspend fun login(email: String, password: String): Result { return try { val response = apiService.login(LoginRequest(email, password)) @@ -130,6 +331,18 @@ class UserRepository(private val apiService: ApiService) { Result.Error(e) } } + + suspend fun checkStore(): HasStoreResponse{ + return apiService.checkStoreUser() + } + + suspend fun checkValue(request: VerifRegisReq): VerifRegisterResponse{ + return apiService.verifValue(request) + } + + suspend fun sendFcm(request: FcmReq): FcmTokenResponse{ + return apiService.updateFcm(request) + } companion object{ private const val TAG = "UserRepository" diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/MainActivity.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/MainActivity.kt index 35a85a4..12d3b44 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/ui/MainActivity.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/ui/MainActivity.kt @@ -1,8 +1,10 @@ package com.alya.ecommerce_serang.ui +import android.content.Context import android.content.pm.PackageManager import android.os.Build import android.os.Bundle +import android.util.Log import android.widget.Toast import androidx.activity.enableEdgeToEdge import androidx.appcompat.app.AppCompatActivity @@ -20,11 +22,15 @@ import com.alya.ecommerce_serang.data.api.retrofit.ApiService import com.alya.ecommerce_serang.databinding.ActivityMainBinding import com.alya.ecommerce_serang.ui.notif.WebSocketManager import com.alya.ecommerce_serang.utils.SessionManager +import com.google.firebase.FirebaseApp +import com.google.firebase.messaging.FirebaseMessaging import dagger.hilt.android.AndroidEntryPoint import javax.inject.Inject @AndroidEntryPoint class MainActivity : AppCompatActivity() { + private val TAG = "MainActivity" + private lateinit var binding: ActivityMainBinding private lateinit var apiService: ApiService private lateinit var sessionManager: SessionManager @@ -65,6 +71,11 @@ class MainActivity : AppCompatActivity() { ) windowInsets } + // Initialize Firebase + FirebaseApp.initializeApp(this) + + // Request FCM token at app startup + retrieveFCMToken() requestNotificationPermissionIfNeeded() @@ -151,4 +162,31 @@ class MainActivity : AppCompatActivity() { } } } + + private fun retrieveFCMToken() { + FirebaseMessaging.getInstance().token + .addOnCompleteListener { task -> + if (!task.isSuccessful) { + Log.e(TAG, "Failed to get FCM token", task.exception) + return@addOnCompleteListener + } + + val token = task.result +// tokenTes = token + Log.d(TAG, "FCM token retrieved: $token") + + // Save token locally + val sharedPreferences = getSharedPreferences("FCM_PREFS", Context.MODE_PRIVATE) + sharedPreferences.edit().putString("FCM_TOKEN", token).apply() + + // Send to your server + sendTokenToServer(token) + } + } + + private fun sendTokenToServer(token: String) { + Log.d(TAG, "Would send token to server: $token") + } + + } \ No newline at end of file diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/auth/LoginActivity.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/auth/LoginActivity.kt index e2c4839..2b2a48f 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/ui/auth/LoginActivity.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/ui/auth/LoginActivity.kt @@ -1,11 +1,14 @@ package com.alya.ecommerce_serang.ui.auth +import android.content.Context import android.content.Intent import android.os.Bundle +import android.util.Log import android.widget.Toast import androidx.activity.enableEdgeToEdge import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity +import com.alya.ecommerce_serang.data.api.dto.FcmReq import com.alya.ecommerce_serang.data.api.retrofit.ApiConfig import com.alya.ecommerce_serang.data.repository.Result import com.alya.ecommerce_serang.data.repository.UserRepository @@ -14,9 +17,13 @@ import com.alya.ecommerce_serang.ui.MainActivity import com.alya.ecommerce_serang.utils.BaseViewModelFactory import com.alya.ecommerce_serang.utils.SessionManager import com.alya.ecommerce_serang.utils.viewmodel.LoginViewModel +import com.google.firebase.FirebaseApp +import com.google.firebase.messaging.FirebaseMessaging class LoginActivity : AppCompatActivity() { + private val TAG = "LoginActivity" + private lateinit var binding: ActivityLoginBinding private val loginViewModel: LoginViewModel by viewModels{ BaseViewModelFactory { @@ -35,6 +42,11 @@ class LoginActivity : AppCompatActivity() { setupListeners() observeLoginState() + + FirebaseApp.initializeApp(this) + + // Request FCM token at app startup + retrieveFCMToken() } private fun setupListeners() { @@ -74,4 +86,35 @@ class LoginActivity : AppCompatActivity() { } } } + + private fun retrieveFCMToken() { + FirebaseMessaging.getInstance().token + .addOnCompleteListener { task -> + if (!task.isSuccessful) { + Log.e(TAG, "Failed to get FCM token", task.exception) + return@addOnCompleteListener + } + + val token = task.result +// tokenTes = token + Log.d(TAG, "FCM token retrieved: $token") + + // Save token locally + val sharedPreferences = getSharedPreferences("FCM_PREFS", Context.MODE_PRIVATE) + sharedPreferences.edit().putString("FCM_TOKEN", token).apply() + + // Send to your server + sendTokenToServer(token) + } + } + + private fun sendTokenToServer(token: String) { + Log.d(TAG, "Would send token to server: $token") + val tokenFcm=FcmReq( + fcmToken = token + ) + loginViewModel.sendFcm(tokenFcm) + Log.d(TAG, "Sent token fcm: $token") + + } } \ No newline at end of file diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/auth/RegisterActivity.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/auth/RegisterActivity.kt index 72d1e1a..f07dcd4 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/ui/auth/RegisterActivity.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/ui/auth/RegisterActivity.kt @@ -12,6 +12,7 @@ import androidx.core.view.ViewCompat import androidx.core.view.WindowCompat import androidx.core.view.WindowInsetsCompat import com.alya.ecommerce_serang.data.api.dto.RegisterRequest +import com.alya.ecommerce_serang.data.api.dto.VerifRegisReq import com.alya.ecommerce_serang.data.api.retrofit.ApiConfig import com.alya.ecommerce_serang.data.repository.Result import com.alya.ecommerce_serang.data.repository.UserRepository @@ -27,6 +28,17 @@ import java.util.Locale class RegisterActivity : AppCompatActivity() { private lateinit var binding: ActivityRegisterBinding private lateinit var sessionManager: SessionManager + + private var isEmailValid = false + private var isPhoneValid = false + + // Track which validation was last performed + private var lastCheckField = "" + + // Counter for signup validation + private var signupValidationsComplete = 0 + private var signupInProgress = false + private val registerViewModel: RegisterViewModel by viewModels{ BaseViewModelFactory { val apiService = ApiConfig.getUnauthenticatedApiService() @@ -76,107 +88,248 @@ class RegisterActivity : AppCompatActivity() { windowInsets } + setupObservers() - // Observe OTP state - observeOtpState() + // Set up field validations + setupFieldValidations() binding.btnSignup.setOnClickListener { - // Retrieve values inside the click listener (so we get latest input) - val birthDate = binding.etBirthDate.text.toString() - val email = binding.etEmail.text.toString() - val password = binding.etPassword.text.toString() - val phone = binding.etNumberPhone.text.toString() - val username = binding.etUsername.text.toString() - val name = binding.etFullname.text.toString() - val image = null - - val userData = RegisterRequest(name, email, password, username, phone, birthDate, image) - - Log.d("RegisterActivity", "Requesting OTP for email: $email") - - // Request OTP and wait for success before showing dialog - registerViewModel.requestOtp(userData.email.toString()) - - // Observe OTP state and show OTP dialog only when successful - registerViewModel.otpState.observe(this) { result -> - when (result) { - is Result.Success -> { - Log.d("RegisterActivity", "OTP sent successfully. Showing OTP dialog.") - // Show OTP dialog after OTP is successfully sent - val otpBottomSheet = OtpBottomSheetDialog(userData) { fullUserData -> - Log.d("RegisterActivity", "OTP entered successfully. Proceeding with registration.") - registerViewModel.registerUser(fullUserData) // Send complete data - } - otpBottomSheet.show(supportFragmentManager, "OtpBottomSheet") - } - is Result.Error -> { - // Show error message if OTP request fails - Log.e("RegisterActivity", "Failed to request OTP: ${result.exception.message}") - Toast.makeText(this, "Failed to request OTP: ${result.exception.message}", Toast.LENGTH_LONG).show() - } - is Result.Loading -> { - // Optional: Show loading indicator - } - } - } - - // Observe Register state - observeRegisterState() + handleSignUp() } - binding.tvLoginAlt.setOnClickListener{ + binding.tvLoginAlt.setOnClickListener { val intent = Intent(this, LoginActivity::class.java) startActivity(intent) } - binding.etBirthDate.setOnClickListener{ + binding.etBirthDate.setOnClickListener { showDatePicker() } } - private fun observeOtpState() { - registerViewModel.otpState.observe(this) { result -> - when (result) { - is Result.Loading -> { - // Show loading indicator - binding.progressBarOtp.visibility = android.view.View.VISIBLE - } - is Result.Success -> { - // Hide loading indicator and show success message - binding.progressBarOtp.visibility = android.view.View.GONE -// Toast.makeText(this@RegisterActivity, result.data, Toast.LENGTH_SHORT).show() - } - is Result.Error -> { - // Hide loading indicator and show error message - binding.progressBarOtp.visibility = android.view.View.GONE - Toast.makeText(this, "OTP Request Failed: ${result.exception.message}", Toast.LENGTH_SHORT).show() - } + private fun setupFieldValidations() { + // Validate email when focus changes + binding.etEmail.setOnFocusChangeListener { _, hasFocus -> + if (!hasFocus) { + val email = binding.etEmail.text.toString() + if (email.isNotEmpty()) { + validateEmail(email, false) } } + } + + // Validate phone when focus changes + binding.etNumberPhone.setOnFocusChangeListener { _, hasFocus -> + if (!hasFocus) { + val phone = binding.etNumberPhone.text.toString() + if (phone.isNotEmpty()) { + validatePhone(phone, false) + } + } + } } - private fun observeRegisterState() { - registerViewModel.registerState.observe(this) { result -> - when (result) { - is Result.Loading -> { - // Show loading indicator for registration - binding.progressBarRegister.visibility = android.view.View.VISIBLE + private fun validateEmail(email: String, isSignup: Boolean) { + lastCheckField = "email" + Log.d("RegisterActivity", "Validating email: $email (signup: $isSignup)") + + val checkValueEmail = VerifRegisReq( + fieldRegis = "email", + valueRegis = email + ) + registerViewModel.checkValueReg(checkValueEmail) + } + + private fun validatePhone(phone: String, isSignup: Boolean) { + lastCheckField = "phone" + Log.d("RegisterActivity", "Validating phone: $phone (signup: $isSignup)") + + val checkValuePhone = VerifRegisReq( + fieldRegis = "phone", + valueRegis = phone + ) + registerViewModel.checkValueReg(checkValuePhone) + } + + private fun setupObservers() { + + registerViewModel.checkValue.observe(this) { result -> + when (result) { + is Result.Loading -> { + // Show loading if needed + } + is Result.Success -> { + val isValid = (result.data as? Boolean) ?: false + + when (lastCheckField) { + "email" -> { + isEmailValid = isValid + if (!isValid) { + Toast.makeText(this, "Email is already registered", Toast.LENGTH_SHORT).show() + } else { + Log.d("RegisterActivity", "Email is valid") + } + } + "phone" -> { + isPhoneValid = isValid + if (!isValid) { + Toast.makeText(this, "Phone number is already registered", Toast.LENGTH_SHORT).show() + } else { + Log.d("RegisterActivity", "Phone is valid") + } + } } - is Result.Success -> { - // Hide loading indicator and show success message - binding.progressBarRegister.visibility = android.view.View.GONE - Toast.makeText(this, result.data, Toast.LENGTH_SHORT).show() - val intent = Intent(this, LoginActivity::class.java) - startActivity(intent) - // Navigate to another screen if needed - } - is com.alya.ecommerce_serang.data.repository.Result.Error -> { - // Hide loading indicator and show error message - binding.progressBarRegister.visibility = android.view.View.GONE - Toast.makeText(this, "Registration Failed: ${result.exception.message}", Toast.LENGTH_SHORT).show() + + // Check if we're in signup process + if (signupInProgress) { + signupValidationsComplete++ + + // Check if both validations completed + if (signupValidationsComplete >= 2) { + signupInProgress = false + signupValidationsComplete = 0 + + // If both validations passed, request OTP + if (isEmailValid && isPhoneValid) { + requestOtp() + } + } } } + is Result.Error -> { + val fieldType = if (lastCheckField == "email") "Email" else "Phone" + Toast.makeText(this, "$fieldType validation failed: ${result.exception.message}", Toast.LENGTH_SHORT).show() + + // Mark validation as invalid + if (lastCheckField == "email") { + isEmailValid = false + } else if (lastCheckField == "phone") { + isPhoneValid = false + } + + // Update signup validation counter if in signup process + if (signupInProgress) { + signupValidationsComplete++ + + // Check if both validations completed + if (signupValidationsComplete >= 2) { + signupInProgress = false + signupValidationsComplete = 0 + } + } + } + else -> { + Log.e("RegisterActivity", "Unexpected result type: $result") + } } + } + registerViewModel.otpState.observe(this) { result -> + when (result) { + is Result.Loading -> { + binding.progressBarOtp.visibility = android.view.View.VISIBLE + } + is Result.Success -> { + binding.progressBarOtp.visibility = android.view.View.GONE + Log.d("RegisterActivity", "OTP sent successfully. Showing OTP dialog.") + + // Create user data before showing OTP dialog + val userData = createUserData() + + // Show OTP dialog + val otpBottomSheet = OtpBottomSheetDialog(userData) { fullUserData -> + Log.d("RegisterActivity", "OTP entered successfully. Proceeding with registration.") + registerViewModel.registerUser(fullUserData) + } + otpBottomSheet.show(supportFragmentManager, "OtpBottomSheet") + } + is Result.Error -> { + binding.progressBarOtp.visibility = android.view.View.GONE + Toast.makeText(this, "OTP Request Failed: ${result.exception.message}", Toast.LENGTH_SHORT).show() + } + else -> { + Log.e("RegisterActivity", "Unexpected result type: $result") + } + } + } + registerViewModel.registerState.observe(this) { result -> + when (result) { + is Result.Loading -> { + // Show loading indicator for registration + binding.progressBarRegister.visibility = android.view.View.VISIBLE + } + is Result.Success -> { + // Hide loading indicator and show success message + binding.progressBarRegister.visibility = android.view.View.GONE + Toast.makeText(this, result.data, Toast.LENGTH_SHORT).show() + val intent = Intent(this, LoginActivity::class.java) + startActivity(intent) + // Navigate to another screen if needed + } + is com.alya.ecommerce_serang.data.repository.Result.Error -> { + // Hide loading indicator and show error message + binding.progressBarRegister.visibility = android.view.View.GONE + Toast.makeText(this, "Registration Failed: ${result.exception.message}", Toast.LENGTH_SHORT).show() + } + } + } + } + + private fun handleSignUp() { + // Basic validation first + val email = binding.etEmail.text.toString() + val password = binding.etPassword.text.toString() + val confirmPassword = binding.etConfirmPassword.text.toString() + val phone = binding.etNumberPhone.text.toString() + val username = binding.etUsername.text.toString() + val name = binding.etFullname.text.toString() + val birthDate = binding.etBirthDate.text.toString() + + // Check if fields are filled + if (email.isEmpty() || password.isEmpty() || confirmPassword.isEmpty() || + phone.isEmpty() || username.isEmpty() || name.isEmpty() || birthDate.isEmpty()) { + Toast.makeText(this, "Please fill all required fields", Toast.LENGTH_SHORT).show() + return + } + + // Check if passwords match + if (password != confirmPassword) { + Toast.makeText(this, "Passwords do not match", Toast.LENGTH_SHORT).show() + return + } + + // If both validations are already done and successful, just request OTP + if (isEmailValid && isPhoneValid) { + requestOtp() + return + } + + // Reset validation counters + signupInProgress = true + signupValidationsComplete = 0 + + // Start validations in parallel + validateEmail(email, true) + validatePhone(phone, true) + } + + private fun requestOtp() { + val email = binding.etEmail.text.toString() + Log.d("RegisterActivity", "Requesting OTP for email: $email") + registerViewModel.requestOtp(email) + } + + private fun createUserData(): RegisterRequest { + // Get all form values + val birthDate = binding.etBirthDate.text.toString() + val email = binding.etEmail.text.toString() + val password = binding.etPassword.text.toString() + val phone = binding.etNumberPhone.text.toString() + val username = binding.etUsername.text.toString() + val name = binding.etFullname.text.toString() + val image = null + + // Create and return user data object + return RegisterRequest(name, email, password, username, phone, birthDate, image) } private fun showDatePicker() { @@ -189,7 +342,7 @@ class RegisterActivity : AppCompatActivity() { this, { _, selectedYear, selectedMonth, selectedDay -> calendar.set(selectedYear, selectedMonth, selectedDay) - val sdf = SimpleDateFormat("dd-MM-yyyy", Locale.getDefault()) + val sdf = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()) binding.etBirthDate.setText(sdf.format(calendar.time)) }, year, month, day diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/auth/RegisterStoreActivity.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/auth/RegisterStoreActivity.kt new file mode 100644 index 0000000..46a91b0 --- /dev/null +++ b/app/src/main/java/com/alya/ecommerce_serang/ui/auth/RegisterStoreActivity.kt @@ -0,0 +1,609 @@ +package com.alya.ecommerce_serang.ui.auth + +import android.Manifest +import android.app.Activity +import android.content.Intent +import android.content.pm.PackageManager +import android.net.Uri +import android.os.Bundle +import android.provider.MediaStore +import android.text.Editable +import android.text.TextWatcher +import android.util.Log +import android.view.View +import android.view.ViewGroup +import android.widget.AdapterView +import android.widget.ArrayAdapter +import android.widget.ImageView +import android.widget.LinearLayout +import android.widget.TextView +import android.widget.Toast +import androidx.activity.enableEdgeToEdge +import androidx.activity.viewModels +import androidx.appcompat.app.AppCompatActivity +import androidx.core.app.ActivityCompat +import androidx.core.content.ContextCompat +import androidx.core.view.ViewCompat +import androidx.core.view.WindowCompat +import androidx.core.view.WindowInsetsCompat +import com.alya.ecommerce_serang.data.api.response.auth.StoreTypesItem +import com.alya.ecommerce_serang.data.api.retrofit.ApiConfig +import com.alya.ecommerce_serang.data.repository.Result +import com.alya.ecommerce_serang.data.repository.UserRepository +import com.alya.ecommerce_serang.databinding.ActivityRegisterStoreBinding +import com.alya.ecommerce_serang.ui.order.address.CityAdapter +import com.alya.ecommerce_serang.ui.order.address.ProvinceAdapter +import com.alya.ecommerce_serang.utils.BaseViewModelFactory +import com.alya.ecommerce_serang.utils.SessionManager + +class RegisterStoreActivity : AppCompatActivity() { + + private lateinit var binding: ActivityRegisterStoreBinding + private lateinit var sessionManager: SessionManager + + private lateinit var provinceAdapter: ProvinceAdapter + private lateinit var cityAdapter: CityAdapter + // Request codes for file picking + private val PICK_STORE_IMAGE_REQUEST = 1001 + private val PICK_KTP_REQUEST = 1002 + private val PICK_NPWP_REQUEST = 1003 + private val PICK_NIB_REQUEST = 1004 + private val PICK_PERSETUJUAN_REQUEST = 1005 + private val PICK_QRIS_REQUEST = 1006 + + // Location request code + private val LOCATION_PERMISSION_REQUEST = 2001 + + private val viewModel: RegisterStoreViewModel by viewModels { + BaseViewModelFactory { + val apiService = ApiConfig.getApiService(sessionManager) + val orderRepository = UserRepository(apiService) + RegisterStoreViewModel(orderRepository) + } + } + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = ActivityRegisterStoreBinding.inflate(layoutInflater) + setContentView(binding.root) + + sessionManager = SessionManager(this) + + WindowCompat.setDecorFitsSystemWindows(window, false) + + enableEdgeToEdge() + + // Apply insets to your root layout + ViewCompat.setOnApplyWindowInsetsListener(binding.root) { view, windowInsets -> + val systemBars = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()) + view.setPadding( + systemBars.left, + systemBars.top, + systemBars.right, + systemBars.bottom + ) + windowInsets + } + + provinceAdapter = ProvinceAdapter(this) + cityAdapter = CityAdapter(this) + + setupDataBinding() + setupSpinners() // Location spinners + + // Setup observers + setupStoreTypesObserver() // Store type observer + setupObservers() + + setupMap() + setupDocumentUploads() + setupCourierSelection() + + viewModel.fetchStoreTypes() + viewModel.getProvinces() + + + // Setup register button + binding.btnRegister.setOnClickListener { + if (viewModel.validateForm()) { + viewModel.registerStore(this) + } else { + Toast.makeText(this, "Harap lengkapi semua field yang wajib diisi", Toast.LENGTH_SHORT).show() + } + } + } + + private fun setupObservers() { + // Observe province state + viewModel.provincesState.observe(this) { state -> + when (state) { + is Result.Loading -> { + Log.d(TAG, "Loading provinces...") + binding.provinceProgressBar?.visibility = View.VISIBLE + binding.spinnerProvince.isEnabled = false + } + is Result.Success -> { + Log.d(TAG, "Provinces loaded: ${state.data.size}") + binding.provinceProgressBar?.visibility = View.GONE + binding.spinnerProvince.isEnabled = true + + // Update adapter with data + provinceAdapter.updateData(state.data) + } + is Result.Error -> { +// Log.e(TAG, "Error loading provinces: ${state.}") + binding.provinceProgressBar?.visibility = View.GONE + binding.spinnerProvince.isEnabled = true + +// Toast.makeText(this, "Gagal memuat provinsi: ${state.message}", Toast.LENGTH_SHORT).show() + } + } + } + + // Observe city state + viewModel.citiesState.observe(this) { state -> + when (state) { + is Result.Loading -> { + Log.d(TAG, "Loading cities...") + binding.cityProgressBar?.visibility = View.VISIBLE + binding.spinnerCity.isEnabled = false + } + is Result.Success -> { + Log.d(TAG, "Cities loaded: ${state.data.size}") + binding.cityProgressBar?.visibility = View.GONE + binding.spinnerCity.isEnabled = true + + // Update adapter with data + cityAdapter.updateData(state.data) + } + is Result.Error -> { +// Log.e(TAG, "Error loading cities: ${state.message}") + binding.cityProgressBar?.visibility = View.GONE + binding.spinnerCity.isEnabled = true + +// Toast.makeText(this, "Gagal memuat kota: ${state.message}", Toast.LENGTH_SHORT).show() + } + } + } + + // Observe registration state + viewModel.registerState.observe(this) { result -> + when (result) { + is Result.Loading -> { + showLoading(true) + } + is Result.Success -> { + showLoading(false) + Toast.makeText(this, "Toko berhasil didaftarkan", Toast.LENGTH_SHORT).show() + finish() // Return to previous screen + } + is Result.Error -> { + showLoading(false) + Toast.makeText(this, "Gagal mendaftarkan toko: ${result.exception.message}", Toast.LENGTH_SHORT).show() + } + } + } + } + + private fun setupStoreTypesObserver() { + // Observe loading state + viewModel.isLoadingType.observe(this) { isLoading -> + if (isLoading) { + // Show loading indicator for store types spinner + binding.spinnerStoreType.isEnabled = false + binding.storeTypeProgressBar?.visibility = View.VISIBLE + } else { + binding.spinnerStoreType.isEnabled = true + binding.storeTypeProgressBar?.visibility = View.GONE + } + } + + // Observe error messages + viewModel.errorMessage.observe(this) { errorMsg -> + if (errorMsg.isNotEmpty()) { + Toast.makeText(this, "Error loading store types: $errorMsg", Toast.LENGTH_SHORT).show() + } + } + + // Observe store types data + viewModel.storeTypes.observe(this) { storeTypes -> + Log.d(TAG, "Store types loaded: ${storeTypes.size}") + if (storeTypes.isNotEmpty()) { + // Add "Pilih Jenis UMKM" as the first item if it's not already there + val displayList = if (storeTypes.any { it.name == "Pilih Jenis UMKM" || it.id == 0 }) { + storeTypes + } else { + val defaultItem = StoreTypesItem(name = "Pilih Jenis UMKM", id = 0) + listOf(defaultItem) + storeTypes + } + + // Setup spinner with API data + setupStoreTypeSpinner(displayList) + } + } + } + + private fun setupStoreTypeSpinner(storeTypes: List) { + Log.d(TAG, "Setting up store type spinner with ${storeTypes.size} items") + + // Create a custom adapter to display just the name but hold the whole object + val adapter = object : ArrayAdapter( + this, + android.R.layout.simple_spinner_item, + storeTypes + ) { + override fun getView(position: Int, convertView: View?, parent: ViewGroup): View { + val view = super.getView(position, convertView, parent) + (view as TextView).text = getItem(position)?.name ?: "" + return view + } + + override fun getDropDownView(position: Int, convertView: View?, parent: ViewGroup): View { + val view = super.getDropDownView(position, convertView, parent) + (view as TextView).text = getItem(position)?.name ?: "" + return view + } + + // Override toString to ensure proper display + override fun getItem(position: Int): StoreTypesItem? { + return super.getItem(position) + } + } + + adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) + + // Set adapter to spinner + binding.spinnerStoreType.adapter = adapter + + // Set item selection listener + binding.spinnerStoreType.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { + override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) { + val selectedItem = adapter.getItem(position) + Log.d(TAG, "Store type selected: position=$position, item=${selectedItem?.name}, id=${selectedItem?.id}") + + if (selectedItem != null && selectedItem.id > 0) { + // Store the actual ID from the API, not just position + viewModel.storeTypeId.value = selectedItem.id + Log.d(TAG, "Set storeTypeId to ${selectedItem.id}") + } + } + + override fun onNothingSelected(parent: AdapterView<*>?) { + Log.d(TAG, "No store type selected") + } + } + + // Hide progress bar after setup + binding.storeTypeProgressBar?.visibility = View.GONE + } + + private fun setupSpinners() { + // Setup province spinner + binding.spinnerProvince.adapter = provinceAdapter + binding.spinnerProvince.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { + override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) { + Log.d(TAG, "Province selected at position: $position") + val provinceId = provinceAdapter.getProvinceId(position) + if (provinceId != null) { + Log.d(TAG, "Setting province ID: $provinceId") + viewModel.provinceId.value = provinceId + viewModel.getCities(provinceId) + + // Reset city selection when province changes + cityAdapter.clear() + binding.spinnerCity.setSelection(0) + } else { + Log.e(TAG, "Invalid province ID for position: $position") + } + } + + override fun onNothingSelected(parent: AdapterView<*>?) { + // Do nothing + } + } + + // Setup city spinner + binding.spinnerCity.adapter = cityAdapter + binding.spinnerCity.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { + override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) { + Log.d(TAG, "City selected at position: $position") + val cityId = cityAdapter.getCityId(position) + if (cityId != null) { + Log.d(TAG, "Setting city ID: $cityId") + viewModel.cityId.value = cityId + viewModel.selectedCityId = cityId + } else { + Log.e(TAG, "Invalid city ID for position: $position") + } + } + + override fun onNothingSelected(parent: AdapterView<*>?) { + // Do nothing + } + } + + // Add initial hints to the spinners + if (provinceAdapter.isEmpty) { + provinceAdapter.add("Pilih Provinsi") + } + + if (cityAdapter.isEmpty) { + cityAdapter.add("Pilih Kabupaten/Kota") + } + } + +// private fun setupSubdistrictSpinner(cityId: Int) { +// // This would typically be populated from API based on cityId +// val subdistricts = listOf("Pilih Kecamatan", "Kecamatan 1", "Kecamatan 2", "Kecamatan 3") +// val subdistrictAdapter = ArrayAdapter(this, R.layout.simple_spinner_dropdown_item, subdistricts) +// binding.spinnerSubdistrict.adapter = subdistrictAdapter +// binding.spinnerSubdistrict.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { +// override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) { +// if (position > 0) { +// viewModel.subdistrict.value = subdistricts[position] +// } +// } +// override fun onNothingSelected(parent: AdapterView<*>?) {} +// } +// } + + private fun setupDocumentUploads() { + // Store Image + binding.containerStoreImg.setOnClickListener { + pickImage(PICK_STORE_IMAGE_REQUEST) + } + + // KTP + binding.containerKtp.setOnClickListener { + pickImage(PICK_KTP_REQUEST) + } + + // NIB + binding.containerNib.setOnClickListener { + pickDocument(PICK_NIB_REQUEST) + } + + // NPWP + binding.containerNpwp?.setOnClickListener { + pickImage(PICK_NPWP_REQUEST) + } + + // SPPIRT + binding.containerSppirt.setOnClickListener { + pickDocument(PICK_PERSETUJUAN_REQUEST) + } + + // Halal + binding.containerHalal.setOnClickListener { + pickDocument(PICK_QRIS_REQUEST) + } + } + + private fun pickImage(requestCode: Int) { + val intent = Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI) + startActivityForResult(intent, requestCode) + } + + private fun pickDocument(requestCode: Int) { + val intent = Intent(Intent.ACTION_OPEN_DOCUMENT) + intent.addCategory(Intent.CATEGORY_OPENABLE) + intent.type = "*/*" + val mimeTypes = arrayOf("application/pdf", "image/jpeg", "image/png") + intent.putExtra(Intent.EXTRA_MIME_TYPES, mimeTypes) + startActivityForResult(intent, requestCode) + } + + private fun setupCourierSelection() { + binding.checkboxJne.setOnCheckedChangeListener { _, isChecked -> + handleCourierSelection("jne", isChecked) + } + + binding.checkboxJnt.setOnCheckedChangeListener { _, isChecked -> + handleCourierSelection("tiki", isChecked) + } + + binding.checkboxPos.setOnCheckedChangeListener { _, isChecked -> + handleCourierSelection("pos", isChecked) + } + } + + private fun handleCourierSelection(courier: String, isSelected: Boolean) { + if (isSelected) { + if (!viewModel.selectedCouriers.contains(courier)) { + viewModel.selectedCouriers.add(courier) + } + } else { + viewModel.selectedCouriers.remove(courier) + } + } + + private fun setupMap() { + // This would typically integrate with Google Maps SDK + // For simplicity, we're just using a placeholder + binding.mapContainer.setOnClickListener { + // Request location permission if not granted + if (ContextCompat.checkSelfPermission( + this, + Manifest.permission.ACCESS_FINE_LOCATION + ) != PackageManager.PERMISSION_GRANTED + ) { + ActivityCompat.requestPermissions( + this, + arrayOf(Manifest.permission.ACCESS_FINE_LOCATION), + LOCATION_PERMISSION_REQUEST + + ) + viewModel.latitude.value = "-6.2088" + viewModel.longitude.value = "106.8456" + Toast.makeText(this, "Lokasi dipilih", Toast.LENGTH_SHORT).show() + } else { + // Show map selection UI + // This would typically launch Maps UI for location selection + // For now, we'll just set some dummy coordinates + viewModel.latitude.value = "-6.2088" + viewModel.longitude.value = "106.8456" + Toast.makeText(this, "Lokasi dipilih", Toast.LENGTH_SHORT).show() + } + } + } + + private fun setupDataBinding() { + // Two-way data binding for text fields + binding.etStoreName.addTextChangedListener(object : TextWatcher { + override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {} + override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {} + override fun afterTextChanged(s: Editable?) { + viewModel.storeName.value = s.toString() + } + }) + + binding.etStoreDescription.addTextChangedListener(object : TextWatcher { + override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {} + override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {} + override fun afterTextChanged(s: Editable?) { + viewModel.storeDescription.value = s.toString() + } + }) + + binding.etStreet.addTextChangedListener(object : TextWatcher { + override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {} + override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {} + override fun afterTextChanged(s: Editable?) { + viewModel.street.value = s.toString() + } + }) + + binding.etPostalCode.addTextChangedListener(object : TextWatcher { + override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {} + override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {} + override fun afterTextChanged(s: Editable?) { + try { + viewModel.postalCode.value = s.toString().toInt() + } catch (e: NumberFormatException) { + // Handle invalid input + //show toast + } + } + }) + + binding.etAddressDetail.addTextChangedListener(object : TextWatcher { + override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {} + override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {} + override fun afterTextChanged(s: Editable?) { + viewModel.addressDetail.value = s.toString() + } + }) + + binding.etBankNumber.addTextChangedListener(object : TextWatcher { + override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {} + override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {} + override fun afterTextChanged(s: Editable?) { + viewModel.bankNumber.value = s.toString().toInt() + } + }) + + binding.etSubdistrict.addTextChangedListener(object : TextWatcher { + override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {} + override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {} + override fun afterTextChanged(s: Editable?) { + viewModel.subdistrict.value = s.toString() + } + }) + + binding.etBankName.addTextChangedListener(object: TextWatcher { + override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {} + override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {} + override fun afterTextChanged(s: Editable?) { + viewModel.subdistrict.value = s.toString() + } + }) + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + if (resultCode == Activity.RESULT_OK && data != null) { + val uri = data.data + when (requestCode) { + PICK_STORE_IMAGE_REQUEST -> { + viewModel.storeImageUri = uri + updateImagePreview(uri, binding.imgStore, binding.layoutUploadStoreImg) + } + PICK_KTP_REQUEST -> { + viewModel.ktpUri = uri + updateImagePreview(uri, binding.imgKtp, binding.layoutUploadKtp) + } + PICK_NPWP_REQUEST -> { + viewModel.npwpUri = uri + updateDocumentPreview(binding.layoutUploadNpwp) + } + PICK_NIB_REQUEST -> { + viewModel.nibUri = uri + updateDocumentPreview(binding.layoutUploadNib) + } + PICK_PERSETUJUAN_REQUEST -> { + viewModel.persetujuanUri = uri + updateDocumentPreview(binding.layoutUploadSppirt) + } + PICK_QRIS_REQUEST -> { + viewModel.qrisUri = uri + updateDocumentPreview(binding.layoutUploadHalal) + } + } + } + } + + private fun updateImagePreview(uri: Uri?, imageView: ImageView, uploadLayout: LinearLayout) { + uri?.let { + imageView.setImageURI(it) + imageView.visibility = View.VISIBLE + uploadLayout.visibility = View.GONE + } + } + + private fun updateDocumentPreview(uploadLayout: LinearLayout) { + // For documents, we just show a success indicator + val checkIcon = ImageView(this) + checkIcon.setImageResource(android.R.drawable.ic_menu_gallery) + val successText = TextView(this) + successText.text = "Dokumen berhasil diunggah" + + uploadLayout.removeAllViews() + uploadLayout.addView(checkIcon) + uploadLayout.addView(successText) + } + + //later implement get location form gps + override fun onRequestPermissionsResult( + requestCode: Int, + permissions: Array, + grantResults: IntArray + ) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults) + if (requestCode == LOCATION_PERMISSION_REQUEST) { + if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + // Permission granted, proceed with location selection + viewModel.latitude.value = "-6.2088" + viewModel.longitude.value = "106.8456" + Toast.makeText(this, "Lokasi dipilih", Toast.LENGTH_SHORT).show() + } else { + viewModel.latitude.value = "-6.2088" + viewModel.longitude.value = "106.8456" + } + } + } + + private fun showLoading(isLoading: Boolean) { + if (isLoading) { + // Show loading indicator + binding.btnRegister.isEnabled = false + binding.btnRegister.text = "Mendaftar..." + } else { + // Hide loading indicator + binding.btnRegister.isEnabled = true + binding.btnRegister.text = "Daftar" + } + } + + companion object { + private const val TAG = "RegisterStoreActivity" + } +} \ No newline at end of file diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/auth/RegisterStoreViewModel.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/auth/RegisterStoreViewModel.kt new file mode 100644 index 0000000..41bfbf1 --- /dev/null +++ b/app/src/main/java/com/alya/ecommerce_serang/ui/auth/RegisterStoreViewModel.kt @@ -0,0 +1,202 @@ +package com.alya.ecommerce_serang.ui.auth + +import android.content.Context +import android.net.Uri +import android.util.Log +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.alya.ecommerce_serang.data.api.response.auth.RegisterStoreResponse +import com.alya.ecommerce_serang.data.api.response.auth.StoreTypesItem +import com.alya.ecommerce_serang.data.api.response.customer.order.CitiesItem +import com.alya.ecommerce_serang.data.api.response.customer.order.ProvincesItem +import com.alya.ecommerce_serang.data.repository.Result +import com.alya.ecommerce_serang.data.repository.UserRepository +import kotlinx.coroutines.launch + +class RegisterStoreViewModel( + private val repository: UserRepository +) : ViewModel() { + + // LiveData for UI state + private val _registerState = MutableLiveData>() + val registerState: LiveData> = _registerState + + private val _storeTypes = MutableLiveData>() + val storeTypes: LiveData> = _storeTypes + + // LiveData for error messages + private val _errorMessage = MutableLiveData() + val errorMessage: LiveData = _errorMessage + + // LiveData for loading state + private val _isLoadingType = MutableLiveData() + val isLoadingType: LiveData = _isLoadingType + + private val _provincesState = MutableLiveData>>() + val provincesState: LiveData>> = _provincesState + + private val _citiesState = MutableLiveData>>() + val citiesState: LiveData>> = _citiesState + + var selectedProvinceId: Int? = null + var selectedCityId: Int? = null + + // Form fields + val storeName = MutableLiveData() + val storeDescription = MutableLiveData() + val storeTypeId = MutableLiveData() + val latitude = MutableLiveData() + val longitude = MutableLiveData() + val street = MutableLiveData() + val subdistrict = MutableLiveData() + val cityId = MutableLiveData() + val provinceId = MutableLiveData() + val postalCode = MutableLiveData() + val addressDetail = MutableLiveData() + val bankName = MutableLiveData() + val bankNumber = MutableLiveData() + val accountName = MutableLiveData() + + // Files + var storeImageUri: Uri? = null + var ktpUri: Uri? = null + var npwpUri: Uri? = null + var nibUri: Uri? = null + var persetujuanUri: Uri? = null + var qrisUri: Uri? = null + + // Selected couriers + val selectedCouriers = mutableListOf() + + fun registerStore(context: Context) { + viewModelScope.launch { + try { + _registerState.value = Result.Loading + + val result = repository.registerStoreUser( + context = context, + description = storeDescription.value ?: "", + storeTypeId = storeTypeId.value ?: 0, + latitude = latitude.value ?: "", + longitude = longitude.value ?: "", + street = street.value ?: "", + subdistrict = subdistrict.value ?: "", + cityId = cityId.value ?: 0, + provinceId = provinceId.value ?: 0, + postalCode = postalCode.value ?: 0, + detail = addressDetail.value, + bankName = bankName.value ?: "", + bankNum = bankNumber.value ?: 0, + storeName = storeName.value ?: "", + storeImg = storeImageUri, + ktp = ktpUri, + npwp = npwpUri, + nib = nibUri, + persetujuan = persetujuanUri, + couriers = selectedCouriers, + qris = qrisUri, + accountName = accountName.value ?: "" + ) + + _registerState.value = result + } catch (e: Exception) { + _registerState.value = com.alya.ecommerce_serang.data.repository.Result.Error(e) + } + } + } + +// // Helper function to convert Uri to File +// private fun getFileFromUri(context: Context, uri: Uri): File { +// val inputStream = context.contentResolver.openInputStream(uri) +// val tempFile = File(context.cacheDir, "temp_file_${System.currentTimeMillis()}") +// inputStream?.use { input -> +// tempFile.outputStream().use { output -> +// input.copyTo(output) +// } +// } +// return tempFile +// } + + fun validateForm(): Boolean { + // Implement form validation logic + return !(storeName.value.isNullOrEmpty() || + storeTypeId.value == null || + street.value.isNullOrEmpty() || + subdistrict.value.isNullOrEmpty() || + cityId.value == null || + provinceId.value == null || + postalCode.value == null || + bankName.value.isNullOrEmpty() || + bankNumber.value == null || + selectedCouriers.isEmpty() || + ktpUri == null || + nibUri == null) + } + + + + // Function to fetch store types + fun fetchStoreTypes() { + _isLoadingType.value = true + viewModelScope.launch { + when (val result = repository.listStoreType()) { + is Result.Success -> { + _storeTypes.value = result.data.storeTypes + _isLoadingType.value = false + } + is Result.Error -> { + _errorMessage.value = result.exception.message ?: "Unknown error occurred" + _isLoadingType.value = false + } + is Result.Loading -> { + _isLoadingType.value = true + } + } + } + } + + fun getProvinces() { + _provincesState.value = Result.Loading + viewModelScope.launch { + try { + val result = repository.getListProvinces() + if (result?.provinces != null) { + _provincesState.postValue(Result.Success(result.provinces)) + Log.d(TAG, "Provinces loaded: ${result.provinces.size}") + } else { + _provincesState.postValue(Result.Error(Exception("Failed to load provinces"))) + Log.e(TAG, "Province result was null or empty") + } + } catch (e: Exception) { + _provincesState.postValue(Result.Error(Exception(e.message ?: "Error loading provinces"))) + Log.e(TAG, "Error fetching provinces", e) + } + } + } + + fun getCities(provinceId: Int){ + _citiesState.value = Result.Loading + viewModelScope.launch { + try { + selectedProvinceId = provinceId + val result = repository.getListCities(provinceId) + result?.let { + _citiesState.postValue(Result.Success(it.cities)) + Log.d(TAG, "Cities loaded for province $provinceId: ${it.cities.size}") + } ?: run { + _citiesState.postValue(Result.Error(Exception("Failed to load cities"))) + Log.e(TAG, "City result was null for province $provinceId") + } + } catch (e: Exception) { + _citiesState.postValue(Result.Error(Exception(e.message ?: "Error loading cities"))) + Log.e(TAG, "Error fetching cities for province $provinceId", e) + } + } + } + + companion object { + private const val TAG = "RegisterStoreUserViewModel" + } +} \ No newline at end of file diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/chat/ChatActivity.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/chat/ChatActivity.kt index 907d4bb..0d61401 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/ui/chat/ChatActivity.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/ui/chat/ChatActivity.kt @@ -125,6 +125,7 @@ class ChatActivity : AppCompatActivity() { val productRating = intent.getFloatExtra(Constants.EXTRA_PRODUCT_RATING, 0f) val storeName = intent.getStringExtra(Constants.EXTRA_STORE_NAME) ?: "" val chatRoomId = intent.getIntExtra(Constants.EXTRA_CHAT_ROOM_ID, 0) + val storeImg = intent.getStringExtra(Constants.EXTRA_STORE_IMAGE) ?: "" // Check if user is logged in val token = sessionManager.getToken() @@ -137,7 +138,20 @@ class ChatActivity : AppCompatActivity() { return } - // Set chat parameters to ViewModel + binding.tvStoreName.text = storeName + val fullImageUrl = when (val img = storeImg) { + is String -> { + if (img.startsWith("/")) BASE_URL + img.substring(1) else img + } + else -> R.drawable.placeholder_image + } + + Glide.with(this) + .load(fullImageUrl) + .placeholder(R.drawable.placeholder_image) + .into(binding.imgProfile) + + // Set chat parameters to ViewModel viewModel.setChatParameters( storeId = storeId, productId = productId, @@ -227,6 +241,7 @@ class ChatActivity : AppCompatActivity() { } }) + // Observe state changes using LiveData viewModel.state.observe(this, Observer { state -> // Update messages @@ -244,6 +259,7 @@ class ChatActivity : AppCompatActivity() { binding.ratingBar.rating = state.productRating binding.tvRating.text = state.productRating.toString() binding.tvSellerName.text = state.storeName + binding.tvStoreName.text=state.storeName // Load product image if (!state.productImageUrl.isNullOrEmpty()) { @@ -270,6 +286,7 @@ class ChatActivity : AppCompatActivity() { binding.editTextMessage.hint = getString(R.string.write_message) } + // Show typing indicator binding.tvTypingIndicator.visibility = if (state.isOtherUserTyping) View.VISIBLE else View.GONE @@ -467,7 +484,8 @@ class ChatActivity : AppCompatActivity() { productImage: String? = null, productRating: String? = null, storeName: String? = null, - chatRoomId: Int = 0 + chatRoomId: Int = 0, + storeImage: String? = null ) { val intent = Intent(context, ChatActivity::class.java).apply { putExtra(Constants.EXTRA_STORE_ID, storeId) @@ -475,6 +493,7 @@ class ChatActivity : AppCompatActivity() { putExtra(Constants.EXTRA_PRODUCT_NAME, productName) putExtra(Constants.EXTRA_PRODUCT_PRICE, productPrice) putExtra(Constants.EXTRA_PRODUCT_IMAGE, productImage) + putExtra(Constants.EXTRA_STORE_IMAGE, storeImage) // Convert productRating string to float if provided if (productRating != null) { diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/chat/ChatFragment.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/chat/ChatFragment.kt deleted file mode 100644 index 4bd12ae..0000000 --- a/app/src/main/java/com/alya/ecommerce_serang/ui/chat/ChatFragment.kt +++ /dev/null @@ -1,337 +0,0 @@ -//package com.alya.ecommerce_serang.ui.chat -// -//import android.Manifest -//import android.app.Activity -//import android.content.Intent -//import android.content.pm.PackageManager -//import android.net.Uri -//import android.os.Bundle -//import android.provider.MediaStore -//import android.text.Editable -//import android.text.TextWatcher -//import androidx.fragment.app.Fragment -//import android.view.LayoutInflater -//import android.view.View -//import android.view.ViewGroup -//import android.widget.Toast -//import androidx.activity.result.contract.ActivityResultContracts -//import androidx.core.app.ActivityCompat -//import androidx.core.content.ContextCompat -//import androidx.core.content.FileProvider -//import androidx.fragment.app.viewModels -//import androidx.lifecycle.lifecycleScope -//import androidx.navigation.fragment.navArgs -//import androidx.recyclerview.widget.LinearLayoutManager -//import com.alya.ecommerce_serang.BuildConfig.BASE_URL -//import com.alya.ecommerce_serang.R -//import com.alya.ecommerce_serang.databinding.FragmentChatBinding -//import com.alya.ecommerce_serang.utils.Constants -//import com.bumptech.glide.Glide -//import dagger.hilt.android.AndroidEntryPoint -//import kotlinx.coroutines.launch -//import java.io.File -//import java.text.SimpleDateFormat -//import java.util.Locale -// -//@AndroidEntryPoint -//class ChatFragment : Fragment() { -// -// private var _binding: FragmentChatBinding? = null -// private val binding get() = _binding!! -// -// private val viewModel: ChatViewModel by viewModels() -//// private val args: ChatFragmentArgs by navArgs() -// -// private lateinit var chatAdapter: ChatAdapter -// -// // For image attachment -// private var tempImageUri: Uri? = null -// -// // Typing indicator handler -// private val typingHandler = android.os.Handler(android.os.Looper.getMainLooper()) -// private val stopTypingRunnable = Runnable { -// viewModel.sendTypingStatus(false) -// } -// -// // Activity Result Launchers -// private val pickImageLauncher = registerForActivityResult( -// ActivityResultContracts.StartActivityForResult() -// ) { result -> -// if (result.resultCode == Activity.RESULT_OK) { -// result.data?.data?.let { uri -> -// handleSelectedImage(uri) -// } -// } -// } -// -// private val takePictureLauncher = registerForActivityResult( -// ActivityResultContracts.StartActivityForResult() -// ) { result -> -// if (result.resultCode == Activity.RESULT_OK) { -// tempImageUri?.let { uri -> -// handleSelectedImage(uri) -// } -// } -// } -// -// override fun onCreateView( -// inflater: LayoutInflater, -// container: ViewGroup?, -// savedInstanceState: Bundle? -// ): View { -// _binding = FragmentChatBinding.inflate(inflater, container, false) -// return binding.root -// } -// -// override fun onViewCreated(view: View, savedInstanceState: Bundle?) { -// super.onViewCreated(view, savedInstanceState) -// -// setupRecyclerView() -// setupListeners() -// setupTypingIndicator() -// observeViewModel() -// } -// -// private fun setupRecyclerView() { -// chatAdapter = ChatAdapter() -// binding.recyclerChat.apply { -// adapter = chatAdapter -// layoutManager = LinearLayoutManager(requireContext()).apply { -// stackFromEnd = true -// } -// } -// } -// -// private fun setupListeners() { -// // Back button -// binding.btnBack.setOnClickListener { -// requireActivity().onBackPressed() -// } -// -// // Options button -// binding.btnOptions.setOnClickListener { -// showOptionsMenu() -// } -// -// // Send button -// binding.btnSend.setOnClickListener { -// val message = binding.editTextMessage.text.toString().trim() -// if (message.isNotEmpty() || viewModel.state.value.hasAttachment) { -// viewModel.sendMessage(message) -// binding.editTextMessage.text.clear() -// } -// } -// -// // Attachment button -// binding.btnAttachment.setOnClickListener { -// checkPermissionsAndShowImagePicker() -// } -// } -// -// private fun setupTypingIndicator() { -// binding.editTextMessage.addTextChangedListener(object : TextWatcher { -// override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {} -// -// override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { -// viewModel.sendTypingStatus(true) -// -// // Reset the timer -// typingHandler.removeCallbacks(stopTypingRunnable) -// typingHandler.postDelayed(stopTypingRunnable, 1000) -// } -// -// override fun afterTextChanged(s: Editable?) {} -// }) -// } -// -// private fun observeViewModel() { -// viewLifecycleOwner.lifecycleScope.launch { -// viewModel.state.collectLatest { state -> -// // Update messages -// chatAdapter.submitList(state.messages) -// -// // Scroll to bottom if new message -// if (state.messages.isNotEmpty()) { -// binding.recyclerChat.scrollToPosition(state.messages.size - 1) -// } -// -// // Update product info -// binding.tvProductName.text = state.productName -// binding.tvProductPrice.text = state.productPrice -// binding.ratingBar.rating = state.productRating -// binding.tvRating.text = state.productRating.toString() -// binding.tvSellerName.text = state.storeName -// -// // Load product image -// if (state.productImageUrl.isNotEmpty()) { -// Glide.with(requireContext()) -// .load(BASE_URL + state.productImageUrl) -// .centerCrop() -// .placeholder(R.drawable.placeholder_image) -// .error(R.drawable.placeholder_image) -// .into(binding.imgProduct) -// } -// -// // Show/hide loading indicators -// binding.progressBar.visibility = if (state.isLoading) View.VISIBLE else View.GONE -// binding.btnSend.isEnabled = !state.isSending -// -// // Update attachment hint -// if (state.hasAttachment) { -// binding.editTextMessage.hint = getString(R.string.image_attached) -// } else { -// binding.editTextMessage.hint = getString(R.string.write_message) -// } -// -// // Show typing indicator -// binding.tvTypingIndicator.visibility = -// if (state.isOtherUserTyping) View.VISIBLE else View.GONE -// -// // Handle connection state -// handleConnectionState(state.connectionState) -// -// // Show error if any -// state.error?.let { error -> -// Toast.makeText(requireContext(), error, Toast.LENGTH_SHORT).show() -// viewModel.clearError() -// } -// } -// } -// } -// -// private fun handleConnectionState(state: ConnectionState) { -// when (state) { -// is ConnectionState.Connected -> { -// binding.tvConnectionStatus.visibility = View.GONE -// } -// is ConnectionState.Connecting -> { -// binding.tvConnectionStatus.visibility = View.VISIBLE -// binding.tvConnectionStatus.text = getString(R.string.connecting) -// } -// is ConnectionState.Disconnected -> { -// binding.tvConnectionStatus.visibility = View.VISIBLE -// binding.tvConnectionStatus.text = getString(R.string.disconnected_reconnecting) -// } -// is ConnectionState.Error -> { -// binding.tvConnectionStatus.visibility = View.VISIBLE -// binding.tvConnectionStatus.text = getString(R.string.connection_error, state.message) -// } -// } -// } -// -// private fun showOptionsMenu() { -// val options = arrayOf( -// getString(R.string.block_user), -// getString(R.string.report), -// getString(R.string.clear_chat), -// getString(R.string.cancel) -// ) -// -// androidx.appcompat.app.AlertDialog.Builder(requireContext()) -// .setTitle(getString(R.string.options)) -// .setItems(options) { dialog, which -> -// when (which) { -// 0 -> Toast.makeText(requireContext(), R.string.block_user_selected, Toast.LENGTH_SHORT).show() -// 1 -> Toast.makeText(requireContext(), R.string.report_selected, Toast.LENGTH_SHORT).show() -// 2 -> Toast.makeText(requireContext(), R.string.clear_chat_selected, Toast.LENGTH_SHORT).show() -// } -// dialog.dismiss() -// } -// .show() -// } -// -// private fun checkPermissionsAndShowImagePicker() { -// if (ContextCompat.checkSelfPermission( -// requireContext(), -// Manifest.permission.READ_EXTERNAL_STORAGE -// ) != PackageManager.PERMISSION_GRANTED -// ) { -// ActivityCompat.requestPermissions( -// requireActivity(), -// arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.CAMERA), -// Constants.REQUEST_STORAGE_PERMISSION -// ) -// } else { -// showImagePickerOptions() -// } -// } -// -// private fun showImagePickerOptions() { -// val options = arrayOf( -// getString(R.string.take_photo), -// getString(R.string.choose_from_gallery), -// getString(R.string.cancel) -// ) -// -// androidx.appcompat.app.AlertDialog.Builder(requireContext()) -// .setTitle(getString(R.string.select_attachment)) -// .setItems(options) { dialog, which -> -// when (which) { -// 0 -> openCamera() -// 1 -> openGallery() -// } -// dialog.dismiss() -// } -// .show() -// } -// -// private fun openCamera() { -// val timeStamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(Date()) -// val imageFileName = "IMG_${timeStamp}.jpg" -// val storageDir = requireContext().getExternalFilesDir(null) -// val imageFile = File(storageDir, imageFileName) -// -// tempImageUri = FileProvider.getUriForFile( -// requireContext(), -// "${requireContext().packageName}.fileprovider", -// imageFile -// ) -// -// val intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE).apply { -// putExtra(MediaStore.EXTRA_OUTPUT, tempImageUri) -// } -// -// takePictureLauncher.launch(intent) -// } -// -// private fun openGallery() { -// val intent = Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI) -// pickImageLauncher.launch(intent) -// } -// -// private fun handleSelectedImage(uri: Uri) { -// // Get the file from Uri -// val filePathColumn = arrayOf(MediaStore.Images.Media.DATA) -// val cursor = requireContext().contentResolver.query(uri, filePathColumn, null, null, null) -// cursor?.moveToFirst() -// val columnIndex = cursor?.getColumnIndex(filePathColumn[0]) -// val filePath = cursor?.getString(columnIndex ?: 0) -// cursor?.close() -// -// if (filePath != null) { -// viewModel.setSelectedImageFile(File(filePath)) -// Toast.makeText(requireContext(), R.string.image_selected, Toast.LENGTH_SHORT).show() -// } -// } -// -// override fun onRequestPermissionsResult( -// requestCode: Int, -// permissions: Array, -// grantResults: IntArray -// ) { -// super.onRequestPermissionsResult(requestCode, permissions, grantResults) -// if (requestCode == Constants.REQUEST_STORAGE_PERMISSION) { -// if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) { -// showImagePickerOptions() -// } else { -// Toast.makeText(requireContext(), R.string.permission_denied, Toast.LENGTH_SHORT).show() -// } -// } -// } -// -// override fun onDestroyView() { -// super.onDestroyView() -// typingHandler.removeCallbacks(stopTypingRunnable) -// _binding = null -// } -//} \ No newline at end of file diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/chat/ChatListFragment.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/chat/ChatListFragment.kt index 4d395a2..ed2aa87 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/ui/chat/ChatListFragment.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/ui/chat/ChatListFragment.kt @@ -21,6 +21,7 @@ class ChatListFragment : Fragment() { private val binding get() = _binding!! private lateinit var socketService: SocketIOService private lateinit var sessionManager: SessionManager + private val viewModel: com.alya.ecommerce_serang.ui.chat.ChatViewModel by viewModels { BaseViewModelFactory { val apiService = ApiConfig.getApiService(sessionManager) @@ -65,7 +66,8 @@ class ChatListFragment : Fragment() { productImage = null, productRating = null, storeName = chatItem.storeName, - chatRoomId = chatItem.chatRoomId + chatRoomId = chatItem.chatRoomId, + storeImage = chatItem.storeImage ) } binding.chatListRecyclerView.adapter = adapter @@ -85,4 +87,8 @@ class ChatListFragment : Fragment() { super.onDestroyView() _binding = null } + + companion object{ + + } } \ No newline at end of file diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/chat/ChatViewModel.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/chat/ChatViewModel.kt index b5db71a..989dedd 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/ui/chat/ChatViewModel.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/ui/chat/ChatViewModel.kt @@ -8,6 +8,7 @@ import androidx.lifecycle.viewModelScope import com.alya.ecommerce_serang.data.api.response.chat.ChatItem import com.alya.ecommerce_serang.data.api.response.chat.ChatItemList import com.alya.ecommerce_serang.data.api.response.chat.ChatLine +import com.alya.ecommerce_serang.data.api.response.customer.product.StoreProduct import com.alya.ecommerce_serang.data.repository.ChatRepository import com.alya.ecommerce_serang.data.repository.Result import com.alya.ecommerce_serang.utils.Constants @@ -23,6 +24,8 @@ import javax.inject.Inject class ChatViewModel @Inject constructor( private val chatRepository: ChatRepository, private val socketService: SocketIOService, + + private val sessionManager: SessionManager ) : ViewModel() { @@ -38,6 +41,9 @@ class ChatViewModel @Inject constructor( private val _chatList = MutableLiveData>>() val chatList: LiveData>> = _chatList + private val _storeDetail = MutableLiveData>() + val storeDetail : LiveData> get() = _storeDetail + // Store and product parameters private var storeId: Int = 0 private var productId: Int? = 0 diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/order/ShippingViewModel.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/order/ShippingViewModel.kt index 1252f3d..9793977 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/ui/order/ShippingViewModel.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/ui/order/ShippingViewModel.kt @@ -1,5 +1,6 @@ package com.alya.ecommerce_serang.ui.order +import android.util.Log import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel @@ -9,6 +10,7 @@ import com.alya.ecommerce_serang.data.api.dto.CourierCostRequest import com.alya.ecommerce_serang.data.api.response.customer.order.CourierCostsItem import com.alya.ecommerce_serang.data.repository.OrderRepository import com.alya.ecommerce_serang.data.repository.Result +import kotlinx.coroutines.delay import kotlinx.coroutines.launch class ShippingViewModel( @@ -30,12 +32,71 @@ class ShippingViewModel( /** * Load shipping options based on address, product, and quantity */ +// fun loadShippingOptions(addressId: Int, productId: Int, quantity: Int) { +// _isLoading.value = true +// _errorMessage.value = "" +// +// val costProduct = CostProduct( +// productId = productId, +// quantity = quantity +// ) +// +// viewModelScope.launch { +// // Define the courier services to try +// val courierServices = listOf("pos", "jne", "tiki") +// +// // Create a mutable list to collect successful courier options +// val availableCourierOptions = mutableListOf() +// +// // Try each courier service +// for (courier in courierServices) { +// try { +// // Create a request for this specific courier +// val courierRequest = CourierCostRequest( +// addressId = addressId, +// itemCost = listOf(costProduct), +// courier = courier // Add the courier to the request +// ) +// +// // Make a separate API call for each courier +// val result = repository.getCountCourierCost(courierRequest) +// +// when (result) { +// is Result.Success -> { +// // Add this courier's options to our collection +// result.data.courierCosts?.let { costs -> +// availableCourierOptions.addAll(costs) +// } +// // Update UI with what we have so far +// _shippingOptions.value = availableCourierOptions +// } +// is Result.Error -> { +// // Log the error but continue with next courier +// Log.e("ShippingViewModel", "Error fetching cost for courier $courier: ${result.exception.message}") +// } +// is Result.Loading -> { +// // Handle loading state +// } +// } +// } catch (e: Exception) { +// // Log the exception but continue with next courier +// Log.e("ShippingViewModel", "Exception for courier $courier: ${e.message}") +// } +// } +// +// // Show error only if we couldn't get any shipping options +// if (availableCourierOptions.isEmpty()) { +// _errorMessage.value = "No shipping options available. Please try again later." +// } +// +// _isLoading.value = false +// } +// } + fun loadShippingOptions(addressId: Int, productId: Int, quantity: Int) { - // Reset previous state _isLoading.value = true _errorMessage.value = "" - // Prepare the request val costProduct = CostProduct( productId = productId, quantity = quantity @@ -43,34 +104,47 @@ class ShippingViewModel( val request = CourierCostRequest( addressId = addressId, - itemCost = listOf(costProduct) // Wrap in a list + itemCost = listOf(costProduct) ) viewModelScope.launch { - try { - // Fetch courier costs - val result = repository.getCountCourierCost(request) + var success = false + var attempt = 0 + val maxAttempts = 3 - when (result) { - is Result.Success -> { - // Update shipping options directly with courier costs - _shippingOptions.value = result.data.courierCosts - } - is Result.Error -> { - // Handle error case - _errorMessage.value = result.exception.message ?: "Unknown error occurred" - } - is Result.Loading -> { - // Typically handled by the loading state + while (!success && attempt < maxAttempts) { + attempt++ + + try { + val result = repository.getCountCourierCost(request) + + when (result) { + is Result.Success -> { + _shippingOptions.value = result.data.courierCosts + success = true + } + is com.alya.ecommerce_serang.data.repository.Result.Error -> { + Log.e("ShippingViewModel", "Attempt $attempt failed: ${result.exception.message}") + // Wait before retrying + delay(120000) + } + is com.alya.ecommerce_serang.data.repository.Result.Loading -> { + // Handle loading state + } } + } catch (e: Exception) { + Log.e("ShippingViewModel", "Attempt $attempt exception: ${e.message}") + // Wait before retrying + delay(1000) } - } catch (e: Exception) { - // Catch any unexpected exceptions - _errorMessage.value = e.localizedMessage ?: "An unexpected error occurred" - } finally { - // Always set loading to false - _isLoading.value = false } + + // After all attempts, check if we have any shipping options + if (!success || _shippingOptions.value.isNullOrEmpty()) { + _errorMessage.value = "No shipping options available. Please try again later." + } + + _isLoading.value = false } } } \ No newline at end of file diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/order/address/AddAddressActivity.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/order/address/AddAddressActivity.kt index c955672..4de34df 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/ui/order/address/AddAddressActivity.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/ui/order/address/AddAddressActivity.kt @@ -398,7 +398,7 @@ class AddAddressActivity : AppCompatActivity() { isRequestingLocation = false Toast.makeText(this, "Timeout lokasi, menggunakan lokasi default", Toast.LENGTH_SHORT).show() } - }, 15000) // 15 seconds timeout + }, 60000) // 15 seconds timeout // Try getting last known location first try { diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/order/history/HistoryViewModel.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/order/history/HistoryViewModel.kt index 4f02163..9c36c28 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/ui/order/history/HistoryViewModel.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/ui/order/history/HistoryViewModel.kt @@ -5,8 +5,10 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.alya.ecommerce_serang.data.api.dto.CancelOrderReq import com.alya.ecommerce_serang.data.api.dto.CompletedOrderRequest import com.alya.ecommerce_serang.data.api.dto.OrdersItem +import com.alya.ecommerce_serang.data.api.response.customer.order.CancelOrderResponse import com.alya.ecommerce_serang.data.api.response.customer.order.OrderListItemsItem import com.alya.ecommerce_serang.data.api.response.customer.order.Orders import com.alya.ecommerce_serang.data.api.response.order.CompletedOrderResponse @@ -31,6 +33,11 @@ class HistoryViewModel(private val repository: OrderRepository) : ViewModel() { private val _orderDetails = MutableLiveData() val orderDetails: LiveData get() = _orderDetails + private val _cancelOrderStatus = MutableLiveData>() + val cancelOrderStatus: LiveData> = _cancelOrderStatus + private val _isCancellingOrder = MutableLiveData() + val isCancellingOrder: LiveData = _isCancellingOrder + // LiveData untuk OrderItems private val _orderItems = MutableLiveData>() val orderItems: LiveData> get() = _orderItems @@ -131,4 +138,26 @@ class HistoryViewModel(private val repository: OrderRepository) : ViewModel() { } } } + + fun cancelOrder(cancelReq: CancelOrderReq) { + viewModelScope.launch { + try { + _cancelOrderStatus.value = Result.Loading + val result = repository.cancelOrder(cancelReq) + _cancelOrderStatus.value = result + } catch (e: Exception) { + Log.e("HistoryViewModel", "Error cancelling order: ${e.message}") + _cancelOrderStatus.value = Result.Error(e) + } + } + } + + fun refreshOrders(status: String = "all") { + Log.d(TAG, "Refreshing orders with status: $status") + // Clear current orders before fetching new ones + _orders.value = ViewState.Loading + + // Re-fetch the orders with the current status + getOrderList(status) + } } \ No newline at end of file diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/order/history/OrderHistoryAdapter.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/order/history/OrderHistoryAdapter.kt index c420ae8..5432f78 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/ui/order/history/OrderHistoryAdapter.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/ui/order/history/OrderHistoryAdapter.kt @@ -2,6 +2,7 @@ package com.alya.ecommerce_serang.ui.order.history import android.app.Activity import android.app.Dialog +import android.content.ContextWrapper import android.content.Intent import android.graphics.Color import android.net.Uri @@ -17,6 +18,7 @@ import android.widget.ImageView import android.widget.ProgressBar import android.widget.TextView import android.widget.Toast +import androidx.fragment.app.FragmentActivity import androidx.lifecycle.findViewTreeLifecycleOwner import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView @@ -24,6 +26,7 @@ import com.alya.ecommerce_serang.R import com.alya.ecommerce_serang.data.api.dto.OrdersItem import com.alya.ecommerce_serang.data.api.dto.ReviewUIItem import com.alya.ecommerce_serang.ui.order.detail.PaymentActivity +import com.alya.ecommerce_serang.ui.order.history.cancelorder.CancelOrderBottomSheet import com.alya.ecommerce_serang.ui.order.review.CreateReviewActivity import com.alya.ecommerce_serang.ui.product.ReviewProductActivity import com.google.android.material.button.MaterialButton @@ -150,7 +153,8 @@ class OrderHistoryAdapter( visibility = View.VISIBLE text = itemView.context.getString(R.string.canceled_order_btn) setOnClickListener { - showCancelOrderDialog(order.orderId.toString()) + showCancelOrderBottomSheet(order.orderId) + viewModel.refreshOrders() } } deadlineDate.apply { @@ -171,7 +175,8 @@ class OrderHistoryAdapter( visibility = View.VISIBLE text = itemView.context.getString(R.string.canceled_order_btn) setOnClickListener { - showCancelOrderDialog(order.orderId.toString()) + showCancelOrderBottomSheet(order.orderId) + viewModel.refreshOrders() } } @@ -208,6 +213,7 @@ class OrderHistoryAdapter( text = itemView.context.getString(R.string.canceled_order_btn) setOnClickListener { showCancelOrderDialog(order.orderId.toString()) + viewModel.refreshOrders() } } } @@ -226,7 +232,7 @@ class OrderHistoryAdapter( text = itemView.context.getString(R.string.claim_complaint) setOnClickListener { showCancelOrderDialog(order.orderId.toString()) - // Handle click event + viewModel.refreshOrders() } } btnRight.apply { @@ -235,6 +241,7 @@ class OrderHistoryAdapter( setOnClickListener { // Handle click event viewModel.confirmOrderCompleted(order.orderId, "completed") + viewModel.refreshOrders() } } @@ -243,16 +250,6 @@ class OrderHistoryAdapter( text = formatShipmentDate(order.updatedAt, order.etd ?: "0") } } - "delivered" -> { - // Untuk status delivered, tampilkan "Beri Ulasan" - btnRight.apply { - visibility = View.VISIBLE - text = itemView.context.getString(R.string.add_review) - setOnClickListener { - // Handle click event - } - } - } "completed" -> { statusOrder.apply { visibility = View.VISIBLE @@ -267,6 +264,7 @@ class OrderHistoryAdapter( text = itemView.context.getString(R.string.add_review) setOnClickListener { addReviewProduct(order) + viewModel.refreshOrders() // Handle click event } } @@ -492,6 +490,48 @@ class OrderHistoryAdapter( dialog.show() } + private fun showCancelOrderBottomSheet(orderId : Int) { + val context = itemView.context + + // We need a FragmentManager to show the bottom sheet + // Try to get it from the context + val fragmentActivity = when (context) { + is FragmentActivity -> context + is ContextWrapper -> { + val baseContext = context.baseContext + if (baseContext is FragmentActivity) { + baseContext + } else { + // Log error and show a Toast instead if we can't get a FragmentManager + Log.e("OrderHistoryAdapter", "Cannot show bottom sheet: Context is not a FragmentActivity") + Toast.makeText(context, "Cannot show cancel order dialog", Toast.LENGTH_SHORT).show() + return + } + } + else -> { + // Log error and show a Toast instead if we can't get a FragmentManager + Log.e("OrderHistoryAdapter", "Cannot show bottom sheet: Context is not a FragmentActivity") + Toast.makeText(context, "Cannot show cancel order dialog", Toast.LENGTH_SHORT).show() + return + } + } + + // Create and show the bottom sheet using the obtained FragmentManager + val bottomSheet = CancelOrderBottomSheet( + orderId = orderId, + onOrderCancelled = { + // Handle the successful cancellation + // Refresh the data + viewModel.refreshOrders() // Assuming there's a method to refresh orders + + // Show a success message + Toast.makeText(context, "Order cancelled successfully", Toast.LENGTH_SHORT).show() + } + ) + + bottomSheet.show(fragmentActivity.supportFragmentManager, CancelOrderBottomSheet.TAG) + } + private fun addReviewProduct(order: OrdersItem) { // Use ViewModel to fetch order details viewModel.getOrderDetails(order.orderId) diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/order/history/OrderHistoryFragment.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/order/history/OrderHistoryFragment.kt index b7d0f6e..267eb28 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/ui/order/history/OrderHistoryFragment.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/ui/order/history/OrderHistoryFragment.kt @@ -47,11 +47,9 @@ class OrderHistoryFragment : Fragment() { 1 -> getString(R.string.pending_orders) 2 -> getString(R.string.unpaid_orders) 3 -> getString(R.string.processed_orders) - 4 -> getString(R.string.paid_orders) - 5 -> getString(R.string.shipped_orders) - 6 -> getString(R.string.delivered_orders) - 7 -> getString(R.string.completed_orders) - 8 -> getString(R.string.canceled_orders) + 4 -> getString(R.string.shipped_orders) + 5 -> getString(R.string.completed_orders) + 6 -> getString(R.string.canceled_orders) else -> "Tab $position" } }.attach() diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/order/history/OrderViewPageAdapter.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/order/history/OrderViewPageAdapter.kt index a759240..615f7a1 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/ui/order/history/OrderViewPageAdapter.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/ui/order/history/OrderViewPageAdapter.kt @@ -14,9 +14,7 @@ class OrderViewPagerAdapter( "pending", // Menunggu Tagihan "unpaid", // Belum Dibayar "processed", // Diproses - "paid", // Dibayar "shipped", // Dikirim - "delivered", // Diterima "completed", // Selesai "canceled" // Dibatalkan ) diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/order/history/cancelorder/CancelOrderBottomSheet.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/order/history/cancelorder/CancelOrderBottomSheet.kt new file mode 100644 index 0000000..99f25de --- /dev/null +++ b/app/src/main/java/com/alya/ecommerce_serang/ui/order/history/cancelorder/CancelOrderBottomSheet.kt @@ -0,0 +1,173 @@ +package com.alya.ecommerce_serang.ui.order.history.cancelorder + +import android.os.Bundle +import android.util.Log +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.AdapterView +import android.widget.Button +import android.widget.Spinner +import android.widget.TextView +import android.widget.Toast +import androidx.fragment.app.viewModels +import com.alya.ecommerce_serang.R +import com.alya.ecommerce_serang.data.api.dto.CancelOrderReq +import com.alya.ecommerce_serang.data.api.retrofit.ApiConfig +import com.alya.ecommerce_serang.data.repository.OrderRepository +import com.alya.ecommerce_serang.data.repository.Result +import com.alya.ecommerce_serang.ui.order.history.HistoryViewModel +import com.alya.ecommerce_serang.utils.BaseViewModelFactory +import com.alya.ecommerce_serang.utils.SessionManager +import com.google.android.material.bottomsheet.BottomSheetDialogFragment + +class CancelOrderBottomSheet( + private val orderId: Int, + private val onOrderCancelled: () -> Unit +) : BottomSheetDialogFragment() { + private lateinit var sessionManager: SessionManager + + private val viewModel: HistoryViewModel by viewModels { + BaseViewModelFactory { + val apiService = ApiConfig.getApiService(sessionManager) + val orderRepository = OrderRepository(apiService) + HistoryViewModel(orderRepository) + } + } + private var selectedReason: CancelOrderReq? = null + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + return inflater.inflate(R.layout.layout_cancel_order_bottom, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + sessionManager = SessionManager(requireContext()) + + val tvTitle = view.findViewById(R.id.tv_title) + val spinnerReason = view.findViewById(R.id.spinner_reason) + val btnCancel = view.findViewById