Merge branch 'screen-features'

This commit is contained in:
shaulascr
2025-05-19 18:50:08 +07:00
52 changed files with 3111 additions and 627 deletions

View File

@ -29,6 +29,9 @@
android:theme="@style/Theme.Ecommerce_serang" android:theme="@style/Theme.Ecommerce_serang"
android:usesCleartextTraffic="true" android:usesCleartextTraffic="true"
tools:targetApi="31"> tools:targetApi="31">
<activity
android:name=".ui.auth.RegisterStoreActivity"
android:exported="false" />
<activity <activity
android:name=".ui.profile.editprofile.EditProfileCustActivity" android:name=".ui.profile.editprofile.EditProfileCustActivity"
android:exported="false" /> android:exported="false" />
@ -157,6 +160,7 @@
<action android:name="com.google.firebase.MESSAGING_EVENT" /> <action android:name="com.google.firebase.MESSAGING_EVENT" />
</intent-filter> </intent-filter>
</service> </service>
<meta-data <meta-data
android:name="com.google.firebase.messaging.default_notification_icon" android:name="com.google.firebase.messaging.default_notification_icon"
android:resource="@drawable/outline_notifications_24" /> android:resource="@drawable/outline_notifications_24" />
@ -168,6 +172,4 @@
android:value="fcm_default_channel" /> android:value="fcm_default_channel" />
</application> </application>
</manifest> </manifest>

View File

@ -1,50 +1,17 @@
package com.alya.ecommerce_serang.app package com.alya.ecommerce_serang.app
import android.app.Application 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 import dagger.hilt.android.HiltAndroidApp
@HiltAndroidApp @HiltAndroidApp
class App : Application(){ 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")
}
} }

View File

@ -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
)

View File

@ -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
)

View File

@ -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
)

View File

@ -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
)

View File

@ -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
)

View File

@ -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<StoreTypesItem>,
@field:SerializedName("message")
val message: String
)
data class StoreTypesItem(
@field:SerializedName("name")
val name: String,
@field:SerializedName("id")
val id: Int
)

View File

@ -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
)

View File

@ -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
)

View File

@ -22,6 +22,9 @@ class ApiConfig {
val client = OkHttpClient.Builder() val client = OkHttpClient.Builder()
.addInterceptor(loggingInterceptor) .addInterceptor(loggingInterceptor)
.addInterceptor(authInterceptor) .addInterceptor(authInterceptor)
.connectTimeout(60, TimeUnit.SECONDS) // Increase to 60 seconds
.readTimeout(60, TimeUnit.SECONDS) // Increase to 60 seconds
.writeTimeout(60, TimeUnit.SECONDS)
.build() .build()
val retrofit = Retrofit.Builder() val retrofit = Retrofit.Builder()

View File

@ -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.AddEvidenceRequest
import com.alya.ecommerce_serang.data.api.dto.AddPaymentInfoResponse 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.CartItem
import com.alya.ecommerce_serang.data.api.dto.CityResponse 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.CompletedOrderRequest
import com.alya.ecommerce_serang.data.api.dto.CourierCostRequest 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.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.LoginRequest
import com.alya.ecommerce_serang.data.api.dto.OrderRequest 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.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.StoreAddressResponse
import com.alya.ecommerce_serang.data.api.dto.UpdateCart 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.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.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.LoginResponse
import com.alya.ecommerce_serang.data.api.response.auth.OtpResponse 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.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.ChatHistoryResponse
import com.alya.ecommerce_serang.data.api.response.chat.ChatListResponse import com.alya.ecommerce_serang.data.api.response.chat.ChatListResponse
import com.alya.ecommerce_serang.data.api.response.chat.SendChatResponse 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.DeleteCartResponse
import com.alya.ecommerce_serang.data.api.response.customer.cart.ListCartResponse 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.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.CourierCostResponse
import com.alya.ecommerce_serang.data.api.response.customer.order.CreateOrderResponse import com.alya.ecommerce_serang.data.api.response.customer.order.CreateOrderResponse
import com.alya.ecommerce_serang.data.api.response.customer.order.CreateReviewResponse 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.POST
import retrofit2.http.PUT import retrofit2.http.PUT
import retrofit2.http.Part import retrofit2.http.Part
import retrofit2.http.PartMap
import retrofit2.http.Path import retrofit2.http.Path
import retrofit2.http.Query import retrofit2.http.Query
@ -82,20 +92,54 @@ interface ApiService {
@Body registerRequest: RegisterRequest @Body registerRequest: RegisterRequest
): Response<RegisterResponse> ): Response<RegisterResponse>
@POST("verif")
suspend fun verifValue (
@Body verifRegisReq: VerifRegisReq
):VerifRegisterResponse
@GET("checkstore") @GET("checkstore")
suspend fun checkStore (): Response<CheckStoreResponse> suspend fun checkStore (): Response<CheckStoreResponse>
// @Multipart @Multipart
// @POST("registerstore") @POST("registerstore")
// suspend fun registerStore( suspend fun registerStore(
// @Part("description") description: RequestBody,
// ): Response<> @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<String, @JvmSuppressWildcards RequestBody>,
@Part qris: MultipartBody.Part?,
@Part("account_name") accountName: RequestBody,
): Response<RegisterStoreResponse>
@POST("otp") @POST("otp")
suspend fun getOTP( suspend fun getOTP(
@Body otpRequest: OtpRequest @Body otpRequest: OtpRequest
):OtpResponse ):OtpResponse
@PUT("updatefcm")
suspend fun updateFcm(
@Body fcmReq: FcmReq
): FcmTokenResponse
@GET("checkstore")
suspend fun checkStoreUser(
): HasStoreResponse
@POST("login") @POST("login")
suspend fun login( suspend fun login(
@Body loginRequest: LoginRequest @Body loginRequest: LoginRequest
@ -105,6 +149,10 @@ interface ApiService {
suspend fun allCategory( suspend fun allCategory(
): Response<CategoryResponse> ): Response<CategoryResponse>
@GET("storetype")
suspend fun listTypeStore(
): Response<ListStoreTypeResponse>
@GET("product") @GET("product")
suspend fun getAllProduct(): Response<AllProductResponse> suspend fun getAllProduct(): Response<AllProductResponse>
@ -131,6 +179,11 @@ interface ApiService {
@Body request: OrderRequest @Body request: OrderRequest
): Response<CreateOrderResponse> ): Response<CreateOrderResponse>
@POST("order/cancel")
suspend fun cancelOrder(
@Body cancelReq: CancelOrderReq
): Response<CancelOrderResponse>
@GET("order/detail/{id}") @GET("order/detail/{id}")
suspend fun getDetailOrder( suspend fun getDetailOrder(
@Path("id") orderId: Int @Path("id") orderId: Int

View File

@ -2,16 +2,17 @@ package com.alya.ecommerce_serang.data.repository
import android.util.Log import android.util.Log
import com.alya.ecommerce_serang.data.api.dto.AddEvidenceMultipartRequest 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.CompletedOrderRequest
import com.alya.ecommerce_serang.data.api.dto.CourierCostRequest 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.CreateAddressRequest
import com.alya.ecommerce_serang.data.api.dto.OrderRequest 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.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.ReviewProductItem
import com.alya.ecommerce_serang.data.api.dto.UpdateCart 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.dto.UserProfile
import com.alya.ecommerce_serang.data.api.response.customer.cart.DataItemCart 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.CourierCostResponse
import com.alya.ecommerce_serang.data.api.response.customer.order.CreateOrderResponse import com.alya.ecommerce_serang.data.api.response.customer.order.CreateOrderResponse
import com.alya.ecommerce_serang.data.api.response.customer.order.CreateReviewResponse 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<CancelOrderResponse>{
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)
}
}
} }

View File

@ -3,17 +3,30 @@ package com.alya.ecommerce_serang.data.repository
import android.content.Context import android.content.Context
import android.net.Uri import android.net.Uri
import android.util.Log 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.LoginRequest
import com.alya.ecommerce_serang.data.api.dto.OtpRequest 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.RegisterRequest
import com.alya.ecommerce_serang.data.api.dto.UserProfile 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.LoginResponse
import com.alya.ecommerce_serang.data.api.response.auth.OtpResponse 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.response.customer.profile.EditProfileResponse
import com.alya.ecommerce_serang.data.api.retrofit.ApiService import com.alya.ecommerce_serang.data.api.retrofit.ApiService
import com.alya.ecommerce_serang.utils.FileUtils import com.alya.ecommerce_serang.utils.FileUtils
import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.MultipartBody
import okhttp3.RequestBody
import okhttp3.RequestBody.Companion.asRequestBody
import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.RequestBody.Companion.toRequestBody
import java.io.File
class UserRepository(private val apiService: ApiService) { class UserRepository(private val apiService: ApiService) {
//post data without message/response //post data without message/response
@ -21,6 +34,31 @@ class UserRepository(private val apiService: ApiService) {
return apiService.getOTP(OtpRequest(email)) return apiService.getOTP(OtpRequest(email))
} }
suspend fun listStoreType(): Result<ListStoreTypeResponse>{
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 { suspend fun registerUser(request: RegisterRequest): String {
val response = apiService.register(request) // API call 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<String>,
qris: Uri?,
accountName: String
): Result<RegisterStoreResponse> {
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<String, RequestBody>()
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<LoginResponse> { suspend fun login(email: String, password: String): Result<LoginResponse> {
return try { return try {
val response = apiService.login(LoginRequest(email, password)) val response = apiService.login(LoginRequest(email, password))
@ -131,6 +332,18 @@ class UserRepository(private val apiService: ApiService) {
} }
} }
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{ companion object{
private const val TAG = "UserRepository" private const val TAG = "UserRepository"
} }

View File

@ -1,8 +1,10 @@
package com.alya.ecommerce_serang.ui package com.alya.ecommerce_serang.ui
import android.content.Context
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.util.Log
import android.widget.Toast import android.widget.Toast
import androidx.activity.enableEdgeToEdge import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity 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.databinding.ActivityMainBinding
import com.alya.ecommerce_serang.ui.notif.WebSocketManager import com.alya.ecommerce_serang.ui.notif.WebSocketManager
import com.alya.ecommerce_serang.utils.SessionManager import com.alya.ecommerce_serang.utils.SessionManager
import com.google.firebase.FirebaseApp
import com.google.firebase.messaging.FirebaseMessaging
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
class MainActivity : AppCompatActivity() { class MainActivity : AppCompatActivity() {
private val TAG = "MainActivity"
private lateinit var binding: ActivityMainBinding private lateinit var binding: ActivityMainBinding
private lateinit var apiService: ApiService private lateinit var apiService: ApiService
private lateinit var sessionManager: SessionManager private lateinit var sessionManager: SessionManager
@ -65,6 +71,11 @@ class MainActivity : AppCompatActivity() {
) )
windowInsets windowInsets
} }
// Initialize Firebase
FirebaseApp.initializeApp(this)
// Request FCM token at app startup
retrieveFCMToken()
requestNotificationPermissionIfNeeded() 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")
}
} }

View File

@ -1,11 +1,14 @@
package com.alya.ecommerce_serang.ui.auth package com.alya.ecommerce_serang.ui.auth
import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.util.Log
import android.widget.Toast import android.widget.Toast
import androidx.activity.enableEdgeToEdge import androidx.activity.enableEdgeToEdge
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity 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.api.retrofit.ApiConfig
import com.alya.ecommerce_serang.data.repository.Result import com.alya.ecommerce_serang.data.repository.Result
import com.alya.ecommerce_serang.data.repository.UserRepository 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.BaseViewModelFactory
import com.alya.ecommerce_serang.utils.SessionManager import com.alya.ecommerce_serang.utils.SessionManager
import com.alya.ecommerce_serang.utils.viewmodel.LoginViewModel import com.alya.ecommerce_serang.utils.viewmodel.LoginViewModel
import com.google.firebase.FirebaseApp
import com.google.firebase.messaging.FirebaseMessaging
class LoginActivity : AppCompatActivity() { class LoginActivity : AppCompatActivity() {
private val TAG = "LoginActivity"
private lateinit var binding: ActivityLoginBinding private lateinit var binding: ActivityLoginBinding
private val loginViewModel: LoginViewModel by viewModels{ private val loginViewModel: LoginViewModel by viewModels{
BaseViewModelFactory { BaseViewModelFactory {
@ -35,6 +42,11 @@ class LoginActivity : AppCompatActivity() {
setupListeners() setupListeners()
observeLoginState() observeLoginState()
FirebaseApp.initializeApp(this)
// Request FCM token at app startup
retrieveFCMToken()
} }
private fun setupListeners() { 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")
}
} }

View File

@ -12,6 +12,7 @@ import androidx.core.view.ViewCompat
import androidx.core.view.WindowCompat import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsCompat
import com.alya.ecommerce_serang.data.api.dto.RegisterRequest 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.api.retrofit.ApiConfig
import com.alya.ecommerce_serang.data.repository.Result import com.alya.ecommerce_serang.data.repository.Result
import com.alya.ecommerce_serang.data.repository.UserRepository import com.alya.ecommerce_serang.data.repository.UserRepository
@ -27,6 +28,17 @@ import java.util.Locale
class RegisterActivity : AppCompatActivity() { class RegisterActivity : AppCompatActivity() {
private lateinit var binding: ActivityRegisterBinding private lateinit var binding: ActivityRegisterBinding
private lateinit var sessionManager: SessionManager 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{ private val registerViewModel: RegisterViewModel by viewModels{
BaseViewModelFactory { BaseViewModelFactory {
val apiService = ApiConfig.getUnauthenticatedApiService() val apiService = ApiConfig.getUnauthenticatedApiService()
@ -76,52 +88,13 @@ class RegisterActivity : AppCompatActivity() {
windowInsets windowInsets
} }
setupObservers()
// Observe OTP state // Set up field validations
observeOtpState() setupFieldValidations()
binding.btnSignup.setOnClickListener { binding.btnSignup.setOnClickListener {
// Retrieve values inside the click listener (so we get latest input) handleSignUp()
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()
} }
binding.tvLoginAlt.setOnClickListener { binding.tvLoginAlt.setOnClickListener {
@ -134,28 +107,150 @@ class RegisterActivity : AppCompatActivity() {
} }
} }
private fun observeOtpState() { private fun setupFieldValidations() {
registerViewModel.otpState.observe(this) { result -> // Validate email when focus changes
when (result) { binding.etEmail.setOnFocusChangeListener { _, hasFocus ->
is Result.Loading -> { if (!hasFocus) {
// Show loading indicator val email = binding.etEmail.text.toString()
binding.progressBarOtp.visibility = android.view.View.VISIBLE if (email.isNotEmpty()) {
validateEmail(email, false)
} }
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 // Validate phone when focus changes
Toast.makeText(this, "OTP Request Failed: ${result.exception.message}", Toast.LENGTH_SHORT).show() binding.etNumberPhone.setOnFocusChangeListener { _, hasFocus ->
if (!hasFocus) {
val phone = binding.etNumberPhone.text.toString()
if (phone.isNotEmpty()) {
validatePhone(phone, false)
} }
} }
} }
} }
private fun observeRegisterState() { 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")
}
}
}
// 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 -> registerViewModel.registerState.observe(this) { result ->
when (result) { when (result) {
is Result.Loading -> { is Result.Loading -> {
@ -179,6 +274,64 @@ class RegisterActivity : AppCompatActivity() {
} }
} }
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() { private fun showDatePicker() {
val calendar = Calendar.getInstance() val calendar = Calendar.getInstance()
val year = calendar.get(Calendar.YEAR) val year = calendar.get(Calendar.YEAR)
@ -189,7 +342,7 @@ class RegisterActivity : AppCompatActivity() {
this, this,
{ _, selectedYear, selectedMonth, selectedDay -> { _, selectedYear, selectedMonth, selectedDay ->
calendar.set(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)) binding.etBirthDate.setText(sdf.format(calendar.time))
}, },
year, month, day year, month, day

View File

@ -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<StoreTypesItem>) {
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<StoreTypesItem>(
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<out String>,
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"
}
}

View File

@ -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<com.alya.ecommerce_serang.data.repository.Result<RegisterStoreResponse>>()
val registerState: LiveData<com.alya.ecommerce_serang.data.repository.Result<RegisterStoreResponse>> = _registerState
private val _storeTypes = MutableLiveData<List<StoreTypesItem>>()
val storeTypes: LiveData<List<StoreTypesItem>> = _storeTypes
// LiveData for error messages
private val _errorMessage = MutableLiveData<String>()
val errorMessage: LiveData<String> = _errorMessage
// LiveData for loading state
private val _isLoadingType = MutableLiveData<Boolean>()
val isLoadingType: LiveData<Boolean> = _isLoadingType
private val _provincesState = MutableLiveData<Result<List<ProvincesItem>>>()
val provincesState: LiveData<Result<List<ProvincesItem>>> = _provincesState
private val _citiesState = MutableLiveData<Result<List<CitiesItem>>>()
val citiesState: LiveData<Result<List<CitiesItem>>> = _citiesState
var selectedProvinceId: Int? = null
var selectedCityId: Int? = null
// Form fields
val storeName = MutableLiveData<String>()
val storeDescription = MutableLiveData<String>()
val storeTypeId = MutableLiveData<Int>()
val latitude = MutableLiveData<String>()
val longitude = MutableLiveData<String>()
val street = MutableLiveData<String>()
val subdistrict = MutableLiveData<String>()
val cityId = MutableLiveData<Int>()
val provinceId = MutableLiveData<Int>()
val postalCode = MutableLiveData<Int>()
val addressDetail = MutableLiveData<String>()
val bankName = MutableLiveData<String>()
val bankNumber = MutableLiveData<Int>()
val accountName = MutableLiveData<String>()
// 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<String>()
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"
}
}

View File

@ -125,6 +125,7 @@ class ChatActivity : AppCompatActivity() {
val productRating = intent.getFloatExtra(Constants.EXTRA_PRODUCT_RATING, 0f) val productRating = intent.getFloatExtra(Constants.EXTRA_PRODUCT_RATING, 0f)
val storeName = intent.getStringExtra(Constants.EXTRA_STORE_NAME) ?: "" val storeName = intent.getStringExtra(Constants.EXTRA_STORE_NAME) ?: ""
val chatRoomId = intent.getIntExtra(Constants.EXTRA_CHAT_ROOM_ID, 0) val chatRoomId = intent.getIntExtra(Constants.EXTRA_CHAT_ROOM_ID, 0)
val storeImg = intent.getStringExtra(Constants.EXTRA_STORE_IMAGE) ?: ""
// Check if user is logged in // Check if user is logged in
val token = sessionManager.getToken() val token = sessionManager.getToken()
@ -137,6 +138,19 @@ class ChatActivity : AppCompatActivity() {
return return
} }
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 // Set chat parameters to ViewModel
viewModel.setChatParameters( viewModel.setChatParameters(
storeId = storeId, storeId = storeId,
@ -227,6 +241,7 @@ class ChatActivity : AppCompatActivity() {
} }
}) })
// Observe state changes using LiveData // Observe state changes using LiveData
viewModel.state.observe(this, Observer { state -> viewModel.state.observe(this, Observer { state ->
// Update messages // Update messages
@ -244,6 +259,7 @@ class ChatActivity : AppCompatActivity() {
binding.ratingBar.rating = state.productRating binding.ratingBar.rating = state.productRating
binding.tvRating.text = state.productRating.toString() binding.tvRating.text = state.productRating.toString()
binding.tvSellerName.text = state.storeName binding.tvSellerName.text = state.storeName
binding.tvStoreName.text=state.storeName
// Load product image // Load product image
if (!state.productImageUrl.isNullOrEmpty()) { if (!state.productImageUrl.isNullOrEmpty()) {
@ -270,6 +286,7 @@ class ChatActivity : AppCompatActivity() {
binding.editTextMessage.hint = getString(R.string.write_message) binding.editTextMessage.hint = getString(R.string.write_message)
} }
// Show typing indicator // Show typing indicator
binding.tvTypingIndicator.visibility = binding.tvTypingIndicator.visibility =
if (state.isOtherUserTyping) View.VISIBLE else View.GONE if (state.isOtherUserTyping) View.VISIBLE else View.GONE
@ -467,7 +484,8 @@ class ChatActivity : AppCompatActivity() {
productImage: String? = null, productImage: String? = null,
productRating: String? = null, productRating: String? = null,
storeName: String? = null, storeName: String? = null,
chatRoomId: Int = 0 chatRoomId: Int = 0,
storeImage: String? = null
) { ) {
val intent = Intent(context, ChatActivity::class.java).apply { val intent = Intent(context, ChatActivity::class.java).apply {
putExtra(Constants.EXTRA_STORE_ID, storeId) putExtra(Constants.EXTRA_STORE_ID, storeId)
@ -475,6 +493,7 @@ class ChatActivity : AppCompatActivity() {
putExtra(Constants.EXTRA_PRODUCT_NAME, productName) putExtra(Constants.EXTRA_PRODUCT_NAME, productName)
putExtra(Constants.EXTRA_PRODUCT_PRICE, productPrice) putExtra(Constants.EXTRA_PRODUCT_PRICE, productPrice)
putExtra(Constants.EXTRA_PRODUCT_IMAGE, productImage) putExtra(Constants.EXTRA_PRODUCT_IMAGE, productImage)
putExtra(Constants.EXTRA_STORE_IMAGE, storeImage)
// Convert productRating string to float if provided // Convert productRating string to float if provided
if (productRating != null) { if (productRating != null) {

View File

@ -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<out String>,
// 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
// }
//}

View File

@ -21,6 +21,7 @@ class ChatListFragment : Fragment() {
private val binding get() = _binding!! private val binding get() = _binding!!
private lateinit var socketService: SocketIOService private lateinit var socketService: SocketIOService
private lateinit var sessionManager: SessionManager private lateinit var sessionManager: SessionManager
private val viewModel: com.alya.ecommerce_serang.ui.chat.ChatViewModel by viewModels { private val viewModel: com.alya.ecommerce_serang.ui.chat.ChatViewModel by viewModels {
BaseViewModelFactory { BaseViewModelFactory {
val apiService = ApiConfig.getApiService(sessionManager) val apiService = ApiConfig.getApiService(sessionManager)
@ -65,7 +66,8 @@ class ChatListFragment : Fragment() {
productImage = null, productImage = null,
productRating = null, productRating = null,
storeName = chatItem.storeName, storeName = chatItem.storeName,
chatRoomId = chatItem.chatRoomId chatRoomId = chatItem.chatRoomId,
storeImage = chatItem.storeImage
) )
} }
binding.chatListRecyclerView.adapter = adapter binding.chatListRecyclerView.adapter = adapter
@ -85,4 +87,8 @@ class ChatListFragment : Fragment() {
super.onDestroyView() super.onDestroyView()
_binding = null _binding = null
} }
companion object{
}
} }

View File

@ -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.ChatItem
import com.alya.ecommerce_serang.data.api.response.chat.ChatItemList 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.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.ChatRepository
import com.alya.ecommerce_serang.data.repository.Result import com.alya.ecommerce_serang.data.repository.Result
import com.alya.ecommerce_serang.utils.Constants import com.alya.ecommerce_serang.utils.Constants
@ -23,6 +24,8 @@ import javax.inject.Inject
class ChatViewModel @Inject constructor( class ChatViewModel @Inject constructor(
private val chatRepository: ChatRepository, private val chatRepository: ChatRepository,
private val socketService: SocketIOService, private val socketService: SocketIOService,
private val sessionManager: SessionManager private val sessionManager: SessionManager
) : ViewModel() { ) : ViewModel() {
@ -38,6 +41,9 @@ class ChatViewModel @Inject constructor(
private val _chatList = MutableLiveData<Result<List<ChatItemList>>>() private val _chatList = MutableLiveData<Result<List<ChatItemList>>>()
val chatList: LiveData<Result<List<ChatItemList>>> = _chatList val chatList: LiveData<Result<List<ChatItemList>>> = _chatList
private val _storeDetail = MutableLiveData<Result<StoreProduct?>>()
val storeDetail : LiveData<Result<StoreProduct?>> get() = _storeDetail
// Store and product parameters // Store and product parameters
private var storeId: Int = 0 private var storeId: Int = 0
private var productId: Int? = 0 private var productId: Int? = 0

View File

@ -1,5 +1,6 @@
package com.alya.ecommerce_serang.ui.order package com.alya.ecommerce_serang.ui.order
import android.util.Log
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel 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.api.response.customer.order.CourierCostsItem
import com.alya.ecommerce_serang.data.repository.OrderRepository import com.alya.ecommerce_serang.data.repository.OrderRepository
import com.alya.ecommerce_serang.data.repository.Result import com.alya.ecommerce_serang.data.repository.Result
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
class ShippingViewModel( class ShippingViewModel(
@ -30,12 +32,71 @@ class ShippingViewModel(
/** /**
* Load shipping options based on address, product, and quantity * 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<CourierCostsItem>()
//
// // 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) { fun loadShippingOptions(addressId: Int, productId: Int, quantity: Int) {
// Reset previous state
_isLoading.value = true _isLoading.value = true
_errorMessage.value = "" _errorMessage.value = ""
// Prepare the request
val costProduct = CostProduct( val costProduct = CostProduct(
productId = productId, productId = productId,
quantity = quantity quantity = quantity
@ -43,34 +104,47 @@ class ShippingViewModel(
val request = CourierCostRequest( val request = CourierCostRequest(
addressId = addressId, addressId = addressId,
itemCost = listOf(costProduct) // Wrap in a list itemCost = listOf(costProduct)
) )
viewModelScope.launch { viewModelScope.launch {
var success = false
var attempt = 0
val maxAttempts = 3
while (!success && attempt < maxAttempts) {
attempt++
try { try {
// Fetch courier costs
val result = repository.getCountCourierCost(request) val result = repository.getCountCourierCost(request)
when (result) { when (result) {
is Result.Success -> { is Result.Success -> {
// Update shipping options directly with courier costs
_shippingOptions.value = result.data.courierCosts _shippingOptions.value = result.data.courierCosts
success = true
} }
is Result.Error -> { is com.alya.ecommerce_serang.data.repository.Result.Error -> {
// Handle error case Log.e("ShippingViewModel", "Attempt $attempt failed: ${result.exception.message}")
_errorMessage.value = result.exception.message ?: "Unknown error occurred" // Wait before retrying
delay(120000)
} }
is Result.Loading -> { is com.alya.ecommerce_serang.data.repository.Result.Loading -> {
// Typically handled by the loading state // Handle loading state
} }
} }
} catch (e: Exception) { } catch (e: Exception) {
// Catch any unexpected exceptions Log.e("ShippingViewModel", "Attempt $attempt exception: ${e.message}")
_errorMessage.value = e.localizedMessage ?: "An unexpected error occurred" // Wait before retrying
} finally { delay(1000)
// Always set loading to 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 _isLoading.value = false
} }
} }
} }
}

View File

@ -398,7 +398,7 @@ class AddAddressActivity : AppCompatActivity() {
isRequestingLocation = false isRequestingLocation = false
Toast.makeText(this, "Timeout lokasi, menggunakan lokasi default", Toast.LENGTH_SHORT).show() 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 getting last known location first
try { try {

View File

@ -5,8 +5,10 @@ import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope 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.CompletedOrderRequest
import com.alya.ecommerce_serang.data.api.dto.OrdersItem 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.OrderListItemsItem
import com.alya.ecommerce_serang.data.api.response.customer.order.Orders import com.alya.ecommerce_serang.data.api.response.customer.order.Orders
import com.alya.ecommerce_serang.data.api.response.order.CompletedOrderResponse 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<Orders>() private val _orderDetails = MutableLiveData<Orders>()
val orderDetails: LiveData<Orders> get() = _orderDetails val orderDetails: LiveData<Orders> get() = _orderDetails
private val _cancelOrderStatus = MutableLiveData<Result<CancelOrderResponse>>()
val cancelOrderStatus: LiveData<Result<CancelOrderResponse>> = _cancelOrderStatus
private val _isCancellingOrder = MutableLiveData<Boolean>()
val isCancellingOrder: LiveData<Boolean> = _isCancellingOrder
// LiveData untuk OrderItems // LiveData untuk OrderItems
private val _orderItems = MutableLiveData<List<OrderListItemsItem>>() private val _orderItems = MutableLiveData<List<OrderListItemsItem>>()
val orderItems: LiveData<List<OrderListItemsItem>> get() = _orderItems val orderItems: LiveData<List<OrderListItemsItem>> 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)
}
} }

View File

@ -2,6 +2,7 @@ package com.alya.ecommerce_serang.ui.order.history
import android.app.Activity import android.app.Activity
import android.app.Dialog import android.app.Dialog
import android.content.ContextWrapper
import android.content.Intent import android.content.Intent
import android.graphics.Color import android.graphics.Color
import android.net.Uri import android.net.Uri
@ -17,6 +18,7 @@ import android.widget.ImageView
import android.widget.ProgressBar import android.widget.ProgressBar
import android.widget.TextView import android.widget.TextView
import android.widget.Toast import android.widget.Toast
import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.findViewTreeLifecycleOwner import androidx.lifecycle.findViewTreeLifecycleOwner
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView 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.OrdersItem
import com.alya.ecommerce_serang.data.api.dto.ReviewUIItem 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.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.order.review.CreateReviewActivity
import com.alya.ecommerce_serang.ui.product.ReviewProductActivity import com.alya.ecommerce_serang.ui.product.ReviewProductActivity
import com.google.android.material.button.MaterialButton import com.google.android.material.button.MaterialButton
@ -150,7 +153,8 @@ class OrderHistoryAdapter(
visibility = View.VISIBLE visibility = View.VISIBLE
text = itemView.context.getString(R.string.canceled_order_btn) text = itemView.context.getString(R.string.canceled_order_btn)
setOnClickListener { setOnClickListener {
showCancelOrderDialog(order.orderId.toString()) showCancelOrderBottomSheet(order.orderId)
viewModel.refreshOrders()
} }
} }
deadlineDate.apply { deadlineDate.apply {
@ -171,7 +175,8 @@ class OrderHistoryAdapter(
visibility = View.VISIBLE visibility = View.VISIBLE
text = itemView.context.getString(R.string.canceled_order_btn) text = itemView.context.getString(R.string.canceled_order_btn)
setOnClickListener { 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) text = itemView.context.getString(R.string.canceled_order_btn)
setOnClickListener { setOnClickListener {
showCancelOrderDialog(order.orderId.toString()) showCancelOrderDialog(order.orderId.toString())
viewModel.refreshOrders()
} }
} }
} }
@ -226,7 +232,7 @@ class OrderHistoryAdapter(
text = itemView.context.getString(R.string.claim_complaint) text = itemView.context.getString(R.string.claim_complaint)
setOnClickListener { setOnClickListener {
showCancelOrderDialog(order.orderId.toString()) showCancelOrderDialog(order.orderId.toString())
// Handle click event viewModel.refreshOrders()
} }
} }
btnRight.apply { btnRight.apply {
@ -235,6 +241,7 @@ class OrderHistoryAdapter(
setOnClickListener { setOnClickListener {
// Handle click event // Handle click event
viewModel.confirmOrderCompleted(order.orderId, "completed") viewModel.confirmOrderCompleted(order.orderId, "completed")
viewModel.refreshOrders()
} }
} }
@ -243,16 +250,6 @@ class OrderHistoryAdapter(
text = formatShipmentDate(order.updatedAt, order.etd ?: "0") 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" -> { "completed" -> {
statusOrder.apply { statusOrder.apply {
visibility = View.VISIBLE visibility = View.VISIBLE
@ -267,6 +264,7 @@ class OrderHistoryAdapter(
text = itemView.context.getString(R.string.add_review) text = itemView.context.getString(R.string.add_review)
setOnClickListener { setOnClickListener {
addReviewProduct(order) addReviewProduct(order)
viewModel.refreshOrders()
// Handle click event // Handle click event
} }
} }
@ -492,6 +490,48 @@ class OrderHistoryAdapter(
dialog.show() 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) { private fun addReviewProduct(order: OrdersItem) {
// Use ViewModel to fetch order details // Use ViewModel to fetch order details
viewModel.getOrderDetails(order.orderId) viewModel.getOrderDetails(order.orderId)

View File

@ -47,11 +47,9 @@ class OrderHistoryFragment : Fragment() {
1 -> getString(R.string.pending_orders) 1 -> getString(R.string.pending_orders)
2 -> getString(R.string.unpaid_orders) 2 -> getString(R.string.unpaid_orders)
3 -> getString(R.string.processed_orders) 3 -> getString(R.string.processed_orders)
4 -> getString(R.string.paid_orders) 4 -> getString(R.string.shipped_orders)
5 -> getString(R.string.shipped_orders) 5 -> getString(R.string.completed_orders)
6 -> getString(R.string.delivered_orders) 6 -> getString(R.string.canceled_orders)
7 -> getString(R.string.completed_orders)
8 -> getString(R.string.canceled_orders)
else -> "Tab $position" else -> "Tab $position"
} }
}.attach() }.attach()

View File

@ -14,9 +14,7 @@ class OrderViewPagerAdapter(
"pending", // Menunggu Tagihan "pending", // Menunggu Tagihan
"unpaid", // Belum Dibayar "unpaid", // Belum Dibayar
"processed", // Diproses "processed", // Diproses
"paid", // Dibayar
"shipped", // Dikirim "shipped", // Dikirim
"delivered", // Diterima
"completed", // Selesai "completed", // Selesai
"canceled" // Dibatalkan "canceled" // Dibatalkan
) )

View File

@ -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<TextView>(R.id.tv_title)
val spinnerReason = view.findViewById<Spinner>(R.id.spinner_reason)
val btnCancel = view.findViewById<Button>(R.id.btn_cancel)
val btnConfirm = view.findViewById<Button>(R.id.btn_confirm)
// Set the title
tvTitle.text = "Cancel Order #$orderId"
// Set up the spinner with cancellation reasons
setupReasonSpinner(spinnerReason)
// Handle button clicks
btnCancel.setOnClickListener {
dismiss()
}
btnConfirm.setOnClickListener {
if (selectedReason == null) {
Toast.makeText(context, "Please select a reason", Toast.LENGTH_SHORT).show()
return@setOnClickListener
}
cancelOrder()
}
}
private fun setupReasonSpinner(spinner: Spinner) {
val reasons = getCancellationReasons()
val adapter = CancelReasonAdapter(requireContext(), reasons)
spinner.adapter = adapter
spinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
selectedReason = reasons[position]
}
override fun onNothingSelected(parent: AdapterView<*>?) {
selectedReason = null
}
}
}
private fun getCancellationReasons(): List<CancelOrderReq> {
// These should ideally come from the server or a configuration
return listOf(
CancelOrderReq(1, "Changed my mind"),
CancelOrderReq(2, "Found a better option"),
CancelOrderReq(3, "Ordered by mistake"),
CancelOrderReq(4, "Delivery time too long"),
CancelOrderReq(5, "Other reason")
)
}
private fun cancelOrder() {
// Validate reason selection
if (selectedReason == null) {
Toast.makeText(context, "Mohon pilih alasan pembatalan", Toast.LENGTH_SHORT).show()
return
}
// Create cancel request
val cancelRequest = CancelOrderReq(
orderId = orderId,
reason = selectedReason!!.reason
)
Log.d(TAG, "Sending cancel request to ViewModel: orderId=${cancelRequest.orderId}, reason='${cancelRequest.reason}'")
// Submit the cancellation
viewModel.cancelOrder(cancelRequest)
// Observe the status
viewModel.cancelOrderStatus.observe(viewLifecycleOwner) { result ->
when (result) {
is Result.Loading -> {
// Show loading indicator
// showLoading(true)
}
is Result.Success -> {
// Hide loading indicator
showLoading(false)
// Show success message
Toast.makeText(
context,
"Pesanan berhasil dibatalkan",
Toast.LENGTH_SHORT
).show()
Log.d(TAG, "Cancel order status: SUCCESS, message: ${result.data.message}")
// Notify callback and close dialog
onOrderCancelled()
dismiss()
}
is Result.Error -> {
// Hide loading indicator
showLoading(false)
Log.e(TAG, "Cancel order status: ERROR", result.exception)
// Show error message
val errorMsg = result.exception.message ?: "Gagal membatalkan pesanan"
Toast.makeText(context, errorMsg, Toast.LENGTH_SHORT).show()
}
}
}
}
// private fun showLoading(isLoading: Boolean) {
// binding.progressBar.isVisible = isLoading
// binding.btnCancel.isEnabled = !isLoading
// binding.btnConfirm.isEnabled = !isLoading
// }
private fun showLoading(isLoading: Boolean) {
// Implement loading indicator if needed
}
companion object {
const val TAG = "CancelOrderBottomSheet"
}
}

View File

@ -0,0 +1,36 @@
package com.alya.ecommerce_serang.ui.order.history.cancelorder
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ArrayAdapter
import android.widget.TextView
import com.alya.ecommerce_serang.R
import com.alya.ecommerce_serang.data.api.dto.CancelOrderReq
class CancelReasonAdapter(
context: Context,
private val reasons: List<CancelOrderReq>
) : ArrayAdapter<CancelOrderReq>(context, 0, reasons) {
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
return createItemView(position, convertView, parent)
}
override fun getDropDownView(position: Int, convertView: View?, parent: ViewGroup): View {
return createItemView(position, convertView, parent)
}
private fun createItemView(position: Int, recycledView: View?, parent: ViewGroup): View {
val reason = getItem(position) ?: return recycledView ?: View(context)
val view = recycledView ?: LayoutInflater.from(context)
.inflate(R.layout.item_cancel_order, parent, false)
val tvReason = view.findViewById<TextView>(R.id.tv_reason)
tvReason.text = reason.reason
return view
}
}

View File

@ -33,6 +33,7 @@ import com.alya.ecommerce_serang.data.api.retrofit.ApiConfig
import com.alya.ecommerce_serang.data.repository.OrderRepository import com.alya.ecommerce_serang.data.repository.OrderRepository
import com.alya.ecommerce_serang.databinding.ActivityDetailOrderStatusBinding import com.alya.ecommerce_serang.databinding.ActivityDetailOrderStatusBinding
import com.alya.ecommerce_serang.ui.order.detail.PaymentActivity 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.order.review.CreateReviewActivity
import com.alya.ecommerce_serang.ui.product.ReviewProductActivity import com.alya.ecommerce_serang.ui.product.ReviewProductActivity
import com.alya.ecommerce_serang.utils.BaseViewModelFactory import com.alya.ecommerce_serang.utils.BaseViewModelFactory
@ -50,6 +51,7 @@ class DetailOrderStatusActivity : AppCompatActivity() {
private lateinit var binding: ActivityDetailOrderStatusBinding private lateinit var binding: ActivityDetailOrderStatusBinding
private lateinit var sessionManager: SessionManager private lateinit var sessionManager: SessionManager
private var orderId: Int = -1 private var orderId: Int = -1
private var orderStatus: String = "" private var orderStatus: String = ""
private val orders = mutableListOf<OrdersItem>() private val orders = mutableListOf<OrdersItem>()
@ -181,9 +183,9 @@ class DetailOrderStatusActivity : AppCompatActivity() {
setupProductsRecyclerView(orders.orderItems) setupProductsRecyclerView(orders.orderItems)
// Set payment method // Set payment method
binding.tvPaymentMethod.text = "Bank Transfer - ${orders.payInfoName ?: "BCA"}" binding.tvPaymentMethod.text = "Bank Transfer - ${orders.payInfoName ?: "Tidak tersedia"}"
Log.d(TAG, "populateOrderDetails: Payment method=${orders.payInfoName ?: "BCA"}") Log.d(TAG, "populateOrderDetails: Payment method=${orders.payInfoName ?: "Tidak tersedia"}")
// Set subtotal, shipping cost, and total // Set subtotal, shipping cost, and total
val subtotal = orders.totalAmount?.toIntOrNull()?.minus(orders.shipmentPrice.toIntOrNull() ?: 0) ?: 0 val subtotal = orders.totalAmount?.toIntOrNull()?.minus(orders.shipmentPrice.toIntOrNull() ?: 0) ?: 0
@ -237,10 +239,8 @@ class DetailOrderStatusActivity : AppCompatActivity() {
Log.d(TAG, "adjustButtonsBasedOnStatus: Status header set to '$statusText'") Log.d(TAG, "adjustButtonsBasedOnStatus: Status header set to '$statusText'")
when (status) { when (status) {
"pending", "unpaid" -> { "pending"->{
Log.d(TAG, "adjustButtonsBasedOnStatus: Setting up UI for pending/unpaid order") binding.tvStatusHeader.text = "Menunggu Tagihan"
// Show status note
binding.tvStatusNote.visibility = View.VISIBLE binding.tvStatusNote.visibility = View.VISIBLE
binding.tvStatusNote.text = "Pesanan ini harus dibayar sebelum ${formatDatePay(orders.updatedAt)}" binding.tvStatusNote.text = "Pesanan ini harus dibayar sebelum ${formatDatePay(orders.updatedAt)}"
@ -250,7 +250,27 @@ class DetailOrderStatusActivity : AppCompatActivity() {
text = "Batalkan Pesanan" text = "Batalkan Pesanan"
setOnClickListener { setOnClickListener {
Log.d(TAG, "Cancel Order button clicked") Log.d(TAG, "Cancel Order button clicked")
showCancelOrderDialog(orders.orderId.toString()) showCancelOrderBottomSheet(orders.orderId)
viewModel.getOrderDetails(orders.orderId)
}
}
}
"unpaid" -> {
Log.d(TAG, "adjustButtonsBasedOnStatus: Setting up UI for pending/unpaid order")
// Show status note
binding.tvStatusHeader.text = "Belum Dibayar"
binding.tvStatusNote.visibility = View.VISIBLE
binding.tvStatusNote.text = "Pesanan ini harus dibayar sebelum ${formatDatePay(orders.updatedAt)}"
// Set buttons
binding.btnSecondary.apply {
visibility = View.VISIBLE
text = "Batalkan Pesanan"
setOnClickListener {
Log.d(TAG, "Cancel Order button clicked")
showCancelOrderBottomSheet(orders.orderId)
viewModel.getOrderDetails(orders.orderId)
} }
} }
@ -270,6 +290,7 @@ class DetailOrderStatusActivity : AppCompatActivity() {
"processed" -> { "processed" -> {
Log.d(TAG, "adjustButtonsBasedOnStatus: Setting up UI for processed order") Log.d(TAG, "adjustButtonsBasedOnStatus: Setting up UI for processed order")
binding.tvStatusHeader.text = "Sedang Diproses"
binding.tvStatusNote.visibility = View.VISIBLE binding.tvStatusNote.visibility = View.VISIBLE
binding.tvStatusNote.text = "Penjual sedang memproses pesanan Anda" binding.tvStatusNote.text = "Penjual sedang memproses pesanan Anda"
@ -279,13 +300,19 @@ class DetailOrderStatusActivity : AppCompatActivity() {
setOnClickListener { setOnClickListener {
Log.d(TAG, "Cancel Order button clicked for processed order") Log.d(TAG, "Cancel Order button clicked for processed order")
showCancelOrderDialog(orders.orderId.toString()) showCancelOrderDialog(orders.orderId.toString())
viewModel.getOrderDetails(orders.orderId)
} }
} }
binding.btnPrimary.apply {
visibility = View.GONE
}
} }
"shipped" -> { "shipped" -> {
Log.d(TAG, "adjustButtonsBasedOnStatus: Setting up UI for shipped order") Log.d(TAG, "adjustButtonsBasedOnStatus: Setting up UI for shipped order")
binding.tvStatusHeader.text = "Sudah Dikirim"
binding.tvStatusNote.visibility = View.VISIBLE binding.tvStatusNote.visibility = View.VISIBLE
binding.tvStatusNote.text = "Pesanan Anda sedang dalam perjalanan. Akan sampai sekitar ${formatShipmentDate(orders.updatedAt, orders.etd ?: "0")}" binding.tvStatusNote.text = "Pesanan Anda sedang dalam perjalanan. Akan sampai sekitar ${formatShipmentDate(orders.updatedAt, orders.etd ?: "0")}"
@ -294,7 +321,8 @@ class DetailOrderStatusActivity : AppCompatActivity() {
text = "Ajukan Komplain" text = "Ajukan Komplain"
setOnClickListener { setOnClickListener {
Log.d(TAG, "Complaint button clicked") Log.d(TAG, "Complaint button clicked")
showCancelOrderDialog(orders.orderId.toString()) // For now, reuse the cancel dialog showCancelOrderDialog(orders.orderId.toString())
viewModel.getOrderDetails(orders.orderId)
} }
} }
@ -314,11 +342,11 @@ class DetailOrderStatusActivity : AppCompatActivity() {
} }
} }
"delivered", "completed" -> { "completed" -> {
Log.d(TAG, "adjustButtonsBasedOnStatus: Setting up UI for delivered/completed order") Log.d(TAG, "adjustButtonsBasedOnStatus: Setting up UI for delivered/completed order")
binding.tvStatusNote.visibility = View.VISIBLE binding.tvStatusHeader.text = "Pesanan Selesai"
binding.tvStatusNote.text = "Pesanan telah selesai" binding.tvStatusNote.visibility = View.GONE
binding.btnPrimary.apply { binding.btnPrimary.apply {
visibility = View.VISIBLE visibility = View.VISIBLE
@ -326,15 +354,27 @@ class DetailOrderStatusActivity : AppCompatActivity() {
setOnClickListener { setOnClickListener {
Log.d(TAG, "Review button clicked") Log.d(TAG, "Review button clicked")
addReviewForOrder(orders) addReviewForOrder(orders)
viewModel.getOrderDetails(orders.orderId)
} }
} }
binding.btnSecondary.apply {
visibility = View.GONE
}
} }
"canceled" -> { "canceled" -> {
Log.d(TAG, "adjustButtonsBasedOnStatus: Setting up UI for canceled order") Log.d(TAG, "adjustButtonsBasedOnStatus: Setting up UI for canceled order")
binding.tvStatusHeader.text = "Pesanan Selesai"
binding.tvStatusNote.visibility = View.VISIBLE binding.tvStatusNote.visibility = View.VISIBLE
binding.tvStatusNote.text = "Pesanan dibatalkan: ${orders.cancelReason ?: "Alasan tidak diberikan"}" binding.tvStatusNote.text = "Pesanan dibatalkan: ${orders.cancelReason ?: "Alasan tidak diberikan"}"
binding.btnSecondary.apply {
visibility = View.GONE
}
binding.btnPrimary.apply {
visibility = View.GONE
}
} }
} }
} }
@ -530,6 +570,22 @@ class DetailOrderStatusActivity : AppCompatActivity() {
dialog.show() dialog.show()
} }
private fun showCancelOrderBottomSheet(orderId: Int) {
// Create and show the bottom sheet directly since we're already in an Activity
val bottomSheet = CancelOrderBottomSheet(
orderId = orderId,
onOrderCancelled = {
// Handle the successful cancellation
// Refresh the data
// Show a success message
Toast.makeText(this, "Order cancelled successfully", Toast.LENGTH_SHORT).show()
}
)
bottomSheet.show(supportFragmentManager, CancelOrderBottomSheet.TAG)
}
private fun formatDate(dateString: String): String { private fun formatDate(dateString: String): String {
Log.d(TAG, "formatDate: Formatting date: $dateString") Log.d(TAG, "formatDate: Formatting date: $dateString")

View File

@ -414,7 +414,8 @@ class DetailProductActivity : AppCompatActivity() {
productImage = productDetail.image, productImage = productDetail.image,
productRating = productDetail.rating, productRating = productDetail.rating,
storeName = storeDetail.data.storeName, storeName = storeDetail.data.storeName,
chatRoomId = 0 chatRoomId = 0,
storeImage = storeDetail.data.storeImage
) )
} }

View File

@ -1,5 +1,7 @@
package com.alya.ecommerce_serang.ui.profile package com.alya.ecommerce_serang.ui.profile
import android.app.AlertDialog
import android.app.ProgressDialog
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.util.Log import android.util.Log
@ -9,18 +11,24 @@ import android.view.ViewGroup
import android.widget.Toast import android.widget.Toast
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope
import com.alya.ecommerce_serang.BuildConfig.BASE_URL import com.alya.ecommerce_serang.BuildConfig.BASE_URL
import com.alya.ecommerce_serang.R import com.alya.ecommerce_serang.R
import com.alya.ecommerce_serang.data.api.dto.UserProfile import com.alya.ecommerce_serang.data.api.dto.UserProfile
import com.alya.ecommerce_serang.data.api.retrofit.ApiConfig import com.alya.ecommerce_serang.data.api.retrofit.ApiConfig
import com.alya.ecommerce_serang.data.repository.UserRepository import com.alya.ecommerce_serang.data.repository.UserRepository
import com.alya.ecommerce_serang.databinding.FragmentProfileBinding import com.alya.ecommerce_serang.databinding.FragmentProfileBinding
import com.alya.ecommerce_serang.ui.auth.LoginActivity
import com.alya.ecommerce_serang.ui.auth.RegisterStoreActivity
import com.alya.ecommerce_serang.ui.order.address.AddressActivity
import com.alya.ecommerce_serang.ui.order.history.HistoryActivity import com.alya.ecommerce_serang.ui.order.history.HistoryActivity
import com.alya.ecommerce_serang.ui.profile.mystore.MyStoreActivity import com.alya.ecommerce_serang.ui.profile.mystore.MyStoreActivity
import com.alya.ecommerce_serang.utils.BaseViewModelFactory import com.alya.ecommerce_serang.utils.BaseViewModelFactory
import com.alya.ecommerce_serang.utils.SessionManager import com.alya.ecommerce_serang.utils.SessionManager
import com.alya.ecommerce_serang.utils.viewmodel.ProfileViewModel import com.alya.ecommerce_serang.utils.viewmodel.ProfileViewModel
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
class ProfileFragment : Fragment() { class ProfileFragment : Fragment() {
@ -53,10 +61,21 @@ class ProfileFragment : Fragment() {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
observeUserProfile() observeUserProfile()
viewModel.loadUserProfile() viewModel.loadUserProfile()
viewModel.checkStoreUser()
binding.cardBukaToko.setOnClickListener{ binding.cardBukaToko.setOnClickListener{
val hasStore = viewModel.checkStore.value
// val hasStore = false
Log.d("Profile Fragment", "Check store $hasStore")
if (hasStore == true){
val intentBuka = Intent(requireContext(), MyStoreActivity::class.java) val intentBuka = Intent(requireContext(), MyStoreActivity::class.java)
startActivity(intentBuka) startActivity(intentBuka)
} else {
val intentBuka = Intent(requireContext(), RegisterStoreActivity::class.java)
startActivity(intentBuka)
}
} }
binding.btnDetailProfile.setOnClickListener{ binding.btnDetailProfile.setOnClickListener{
@ -73,6 +92,16 @@ class ProfileFragment : Fragment() {
val intent = Intent(requireContext(), HistoryActivity::class.java) val intent = Intent(requireContext(), HistoryActivity::class.java)
startActivity(intent) startActivity(intent)
} }
binding.cardLogout.setOnClickListener({
logout()
})
binding.cardAddress.setOnClickListener({
val intent = Intent(requireContext(), AddressActivity::class.java)
startActivity(intent)
})
} }
private fun observeUserProfile() { private fun observeUserProfile() {
@ -103,4 +132,41 @@ class ProfileFragment : Fragment() {
.placeholder(R.drawable.placeholder_image) .placeholder(R.drawable.placeholder_image)
.into(profileImage) .into(profileImage)
} }
private fun logout(){
AlertDialog.Builder(requireContext())
.setTitle("Konfirmasi")
.setMessage("Apakah anda yakin ingin keluar?")
.setPositiveButton("Ya") { _, _ ->
actionLogout()
}
.setNegativeButton("Tidak", null)
.show()
}
private fun actionLogout(){
val loadingDialog = ProgressDialog(requireContext()).apply {
setMessage("Mohon ditunggu")
setCancelable(false)
show()
}
lifecycleScope.launch {
try {
delay(500)
loadingDialog.dismiss()
sessionManager.clearAll()
val intent = Intent(requireContext(), LoginActivity::class.java)
startActivity(intent)
} catch (e: Exception) {
Toast.makeText(
requireContext(),
"Gagal keluar: ${e.message}",
Toast.LENGTH_SHORT
).show()
}
}
}
} }

View File

@ -20,6 +20,7 @@ object Constants {
const val EXTRA_PRODUCT_PRICE = "product_price" const val EXTRA_PRODUCT_PRICE = "product_price"
const val EXTRA_PRODUCT_IMAGE = "product_image" const val EXTRA_PRODUCT_IMAGE = "product_image"
const val EXTRA_PRODUCT_RATING = "product_rating" const val EXTRA_PRODUCT_RATING = "product_rating"
const val EXTRA_STORE_IMAGE = "store_image"
// Request codes // Request codes
const val REQUEST_IMAGE_PICK = 1001 const val REQUEST_IMAGE_PICK = 1001

View File

@ -57,8 +57,6 @@ class SessionManager(context: Context) {
} }
} }
//clear data when log out //clear data when log out
fun clearAll() { fun clearAll() {
sharedPreferences.edit() { sharedPreferences.edit() {

View File

@ -1,9 +1,12 @@
package com.alya.ecommerce_serang.utils.viewmodel package com.alya.ecommerce_serang.utils.viewmodel
import android.util.Log
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.alya.ecommerce_serang.data.api.dto.FcmReq
import com.alya.ecommerce_serang.data.api.response.auth.FcmTokenResponse
import com.alya.ecommerce_serang.data.api.response.auth.LoginResponse import com.alya.ecommerce_serang.data.api.response.auth.LoginResponse
import com.alya.ecommerce_serang.data.repository.Result import com.alya.ecommerce_serang.data.repository.Result
import com.alya.ecommerce_serang.data.repository.UserRepository import com.alya.ecommerce_serang.data.repository.UserRepository
@ -13,6 +16,13 @@ class LoginViewModel(private val repository: UserRepository) : ViewModel() {
private val _loginState = MutableLiveData<Result<LoginResponse>>() private val _loginState = MutableLiveData<Result<LoginResponse>>()
val loginState: LiveData<Result<LoginResponse>> get() = _loginState val loginState: LiveData<Result<LoginResponse>> get() = _loginState
private val _otpState = MutableLiveData<Result<Unit>>()
val otpState: LiveData<Result<Unit>> = _otpState
// MutableLiveData to store messages from API responses
private val _message = MutableLiveData<String>()
val message: LiveData<String> = _message
fun login(email: String, password: String) { fun login(email: String, password: String) {
viewModelScope.launch { viewModelScope.launch {
_loginState.value = Result.Loading _loginState.value = Result.Loading
@ -20,4 +30,31 @@ class LoginViewModel(private val repository: UserRepository) : ViewModel() {
_loginState.value = result _loginState.value = result
} }
} }
fun sendFcm(token: FcmReq) {
viewModelScope.launch {
_otpState.value = Result.Loading // Indicating API call in progress
try {
// Call the repository function to request OTP
val response: FcmTokenResponse = repository.sendFcm(token)
// Log and store success message
Log.d("RegisterViewModel", "OTP Response: ${response.message}")
_message.value = response.message ?: "berhasil" // Store the message for UI feedback
// Update state to indicate success
_otpState.value = Result.Success(Unit)
} catch (exception: Exception) {
// Handle any errors and update state
_otpState.value = Result.Error(exception)
_message.value = exception.localizedMessage ?: "Failed to request OTP"
// Log the error for debugging
Log.e("RegisterViewModel", "OTP request failed for: $token", exception)
}
}
}
} }

View File

@ -8,6 +8,7 @@ import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.alya.ecommerce_serang.data.api.dto.UserProfile import com.alya.ecommerce_serang.data.api.dto.UserProfile
import com.alya.ecommerce_serang.data.api.response.auth.HasStoreResponse
import com.alya.ecommerce_serang.data.api.response.customer.profile.EditProfileResponse import com.alya.ecommerce_serang.data.api.response.customer.profile.EditProfileResponse
import com.alya.ecommerce_serang.data.repository.Result import com.alya.ecommerce_serang.data.repository.Result
import com.alya.ecommerce_serang.data.repository.UserRepository import com.alya.ecommerce_serang.data.repository.UserRepository
@ -23,6 +24,12 @@ class ProfileViewModel(private val userRepository: UserRepository) : ViewModel()
private val _editProfileResult = MutableLiveData<Result<EditProfileResponse>>() private val _editProfileResult = MutableLiveData<Result<EditProfileResponse>>()
val editProfileResult: LiveData<Result<EditProfileResponse>> = _editProfileResult val editProfileResult: LiveData<Result<EditProfileResponse>> = _editProfileResult
private val _checkStore = MutableLiveData<Boolean>()
val checkStore: LiveData<Boolean> = _checkStore
private val _logout = MutableLiveData<Boolean>()
val logout : LiveData<Boolean> = _checkStore
fun loadUserProfile(){ fun loadUserProfile(){
viewModelScope.launch { viewModelScope.launch {
when (val result = userRepository.fetchUserProfile()){ when (val result = userRepository.fetchUserProfile()){
@ -33,6 +40,28 @@ class ProfileViewModel(private val userRepository: UserRepository) : ViewModel()
} }
} }
fun checkStoreUser(){
viewModelScope.launch {
try {
// Call the repository function to request OTP
val response: HasStoreResponse = userRepository.checkStore()
// Log and store success message
Log.d("RegisterViewModel", "OTP Response: ${response.hasStore}")
_checkStore.value = response.hasStore // Store the message for UI feedback
} catch (exception: Exception) {
// Handle any errors and update state
_checkStore.value = false
// Log the error for debugging
Log.e("RegisterViewModel", "Error:", exception)
}
}
}
fun editProfileDirect( fun editProfileDirect(
context: Context, context: Context,
username: String, username: String,
@ -71,6 +100,17 @@ class ProfileViewModel(private val userRepository: UserRepository) : ViewModel()
} }
} }
fun logout(){
viewModelScope.launch {
try{
} catch (e: Exception){
}
}
}
companion object { companion object {
private const val TAG = "ProfileViewModel" private const val TAG = "ProfileViewModel"
} }

View File

@ -6,7 +6,9 @@ import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.alya.ecommerce_serang.data.api.dto.RegisterRequest 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.response.auth.OtpResponse import com.alya.ecommerce_serang.data.api.response.auth.OtpResponse
import com.alya.ecommerce_serang.data.api.response.auth.VerifRegisterResponse
import com.alya.ecommerce_serang.data.repository.Result import com.alya.ecommerce_serang.data.repository.Result
import com.alya.ecommerce_serang.data.repository.UserRepository import com.alya.ecommerce_serang.data.repository.UserRepository
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -21,6 +23,9 @@ class RegisterViewModel(private val repository: UserRepository) : ViewModel() {
private val _otpState = MutableLiveData<Result<Unit>>() private val _otpState = MutableLiveData<Result<Unit>>()
val otpState: LiveData<Result<Unit>> = _otpState val otpState: LiveData<Result<Unit>> = _otpState
private val _checkValue = MutableLiveData<Result<Boolean>>()
val checkValue: LiveData<Result<Boolean>> = _checkValue
// MutableLiveData to store messages from API responses // MutableLiveData to store messages from API responses
private val _message = MutableLiveData<String>() private val _message = MutableLiveData<String>()
val message: LiveData<String> = _message val message: LiveData<String> = _message
@ -86,4 +91,24 @@ class RegisterViewModel(private val repository: UserRepository) : ViewModel() {
} }
} }
} }
fun checkValueReg(request: VerifRegisReq){
viewModelScope.launch {
try {
// Call the repository function to request OTP
val response: VerifRegisterResponse = repository.checkValue(request)
// Log and store success message
Log.d("RegisterViewModel", "OTP Response: ${response.available}")
_checkValue.value = Result.Success(response.available)// Store the message for UI feedback
} catch (exception: Exception) {
// Handle any errors and update state
_checkValue.value = Result.Error(exception)
// Log the error for debugging
Log.e("RegisterViewModel", "Error:", exception)
}
}
}
} }

View File

@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#211E1E" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM13,17h-2v-6h2v6zM13,9h-2L11,7h2v2z"/>
</vector>

View File

@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:autoMirrored="true" android:height="24dp" android:tint="#211E1E" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M17,7l-1.41,1.41L18.17,11H8v2h10.17l-2.58,2.58L17,17l5,-5zM4,5h8V3H4c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h8v-2H4V5z"/>
</vector>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@color/white" />
<corners
android:topLeftRadius="16dp"
android:topRightRadius="16dp" />
</shape>

View File

@ -0,0 +1,8 @@
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@color/white" />
<corners android:radius="8dp" />
<stroke
android:width="1dp"
android:color="@color/light_gray" />
</shape>

View File

@ -0,0 +1,579 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".ui.auth.RegisterStoreActivity">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#334583"
android:padding="16dp"
android:text="Buka Toko"
android:textColor="@android:color/white"
android:textSize="20sp"
android:textStyle="bold" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#334583"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:paddingBottom="16dp"
android:text="Mohon untuk melengkapi formulir pendaftaran ini agar dapat mengakses fitur penjual pada aplikasi."
android:textColor="@android:color/white" />
</LinearLayout>
<!-- Main content in ScrollView -->
<ScrollView
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="* Wajib diisi"
android:textColor="#777777" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="1. Nama Toko *"
android:textColor="@android:color/black" />
<EditText
android:id="@+id/et_store_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:background="@android:drawable/editbox_background"
android:hint="Isi jawaban Anda di sini"
android:inputType="text"
android:padding="12dp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="2. Deskripsi Toko"
android:textColor="@android:color/black" />
<EditText
android:id="@+id/et_store_description"
android:layout_width="match_parent"
android:layout_height="100dp"
android:layout_marginTop="8dp"
android:background="@android:drawable/editbox_background"
android:gravity="top"
android:hint="Isi jawaban Anda di sini"
android:inputType="textMultiLine"
android:padding="12dp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="3. Jenis UMKM *"
android:textColor="@android:color/black" />
<Spinner
android:id="@+id/spinner_store_type"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:background="@android:drawable/btn_dropdown"
android:hint="Pilih jawaban Anda di sini"
android:padding="12dp" />
<ProgressBar
android:id="@+id/storeTypeProgressBar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="4dp"
android:visibility="gone" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="4. Provinsi *"
android:textColor="@android:color/black" />
<Spinner
android:id="@+id/spinner_province"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:background="@android:drawable/btn_dropdown"
android:hint="Pilih jawaban Anda di sini"
android:padding="12dp" />
<ProgressBar
android:id="@+id/provinceProgressBar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="4dp"
android:visibility="gone" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="5. Kabupaten/Kota *"
android:textColor="@android:color/black" />
<Spinner
android:id="@+id/spinner_city"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:background="@android:drawable/btn_dropdown"
android:hint="Pilih jawaban Anda di sini"
android:padding="12dp" />
<ProgressBar
android:id="@+id/cityProgressBar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="4dp"
android:visibility="gone" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="6. Kecamatan *"
android:textColor="@android:color/black" />
<EditText
android:id="@+id/et_subdistrict"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:background="@android:drawable/editbox_background"
android:hint="Isi jawaban Anda di sini"
android:inputType="text"
android:padding="12dp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="7. Jalan *"
android:textColor="@android:color/black" />
<EditText
android:id="@+id/et_street"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:background="@android:drawable/editbox_background"
android:hint="Isi jawaban Anda di sini"
android:inputType="text"
android:padding="12dp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="8. Kode Pos *"
android:textColor="@android:color/black" />
<EditText
android:id="@+id/et_postal_code"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:background="@android:drawable/editbox_background"
android:hint="Isi jawaban Anda di sini"
android:inputType="number"
android:padding="12dp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="9. Detail Alamat"
android:textColor="@android:color/black" />
<EditText
android:id="@+id/et_address_detail"
android:layout_width="match_parent"
android:layout_height="100dp"
android:layout_marginTop="8dp"
android:background="@android:drawable/editbox_background"
android:gravity="top"
android:hint="Isi jawaban Anda di sini"
android:inputType="textMultiLine"
android:padding="12dp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="10. Bank *"
android:textColor="@android:color/black" />
<EditText
android:id="@+id/et_bank_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:background="@android:drawable/editbox_background"
android:hint="Isi jawaban Anda di sini"
android:inputType="text"
android:padding="12dp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="11. Nomor Rekening *"
android:textColor="@android:color/black" />
<EditText
android:id="@+id/et_bank_number"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:background="@android:drawable/editbox_background"
android:hint="Isi jawaban Anda di sini"
android:inputType="number"
android:padding="12dp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="12. Pilih Kurir *"
android:textColor="@android:color/black" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<CheckBox
android:id="@+id/checkbox_jne"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="JNE" />
<CheckBox
android:id="@+id/checkbox_jnt"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:text="J&amp;T" />
<CheckBox
android:id="@+id/checkbox_pos"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:text="POS" />
</LinearLayout>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="13. Foto Profil Toko"
android:textColor="@android:color/black" />
<FrameLayout
android:id="@+id/container_store_img"
android:layout_width="match_parent"
android:layout_height="100dp"
android:layout_marginTop="8dp"
android:background="@android:drawable/editbox_background">
<ImageView
android:id="@+id/img_store"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="centerInside"
android:visibility="gone" />
<LinearLayout
android:id="@+id/layout_upload_store_img"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical">
<ImageView
android:layout_width="32dp"
android:layout_height="32dp"
android:src="@android:drawable/ic_menu_upload" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="Unggah dokumen Anda di sini"
android:textColor="#777777" />
</LinearLayout>
</FrameLayout>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="14. Dokumen KTP *"
android:textColor="@android:color/black" />
<FrameLayout
android:id="@+id/container_ktp"
android:layout_width="match_parent"
android:layout_height="100dp"
android:layout_marginTop="8dp"
android:background="@android:drawable/editbox_background">
<ImageView
android:id="@+id/img_ktp"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="centerInside"
android:visibility="gone" />
<LinearLayout
android:id="@+id/layout_upload_ktp"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical">
<ImageView
android:layout_width="32dp"
android:layout_height="32dp"
android:src="@android:drawable/ic_menu_upload" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="Unggah dokumen Anda di sini"
android:textColor="#777777" />
</LinearLayout>
</FrameLayout>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="15. Dokumen NIB *"
android:textColor="@android:color/black" />
<FrameLayout
android:id="@+id/container_nib"
android:layout_width="match_parent"
android:layout_height="100dp"
android:layout_marginTop="8dp"
android:background="@android:drawable/editbox_background">
<ImageView
android:id="@+id/img_nib"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="centerInside"
android:visibility="gone" />
<LinearLayout
android:id="@+id/layout_upload_nib"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical">
<ImageView
android:layout_width="32dp"
android:layout_height="32dp"
android:src="@android:drawable/ic_menu_upload" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="Unggah dokumen Anda di sini"
android:textColor="#777777" />
</LinearLayout>
</FrameLayout>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="16. Dokumen NPWP *"
android:textColor="@android:color/black" />
<FrameLayout
android:id="@+id/container_npwp"
android:layout_width="match_parent"
android:layout_height="100dp"
android:layout_marginTop="8dp"
android:background="@android:drawable/editbox_background">
<ImageView
android:id="@+id/img_npwp"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="centerInside"
android:visibility="gone" />
<LinearLayout
android:id="@+id/layout_upload_npwp"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical">
<ImageView
android:layout_width="32dp"
android:layout_height="32dp"
android:src="@android:drawable/ic_menu_upload" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="Unggah dokumen Anda di sini"
android:textColor="#777777" />
</LinearLayout>
</FrameLayout>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="16. Dokumen SPPIRT"
android:textColor="@android:color/black" />
<FrameLayout
android:id="@+id/container_sppirt"
android:layout_width="match_parent"
android:layout_height="100dp"
android:layout_marginTop="8dp"
android:background="@android:drawable/editbox_background">
<ImageView
android:id="@+id/img_sppirt"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="centerInside"
android:visibility="gone" />
<LinearLayout
android:id="@+id/layout_upload_sppirt"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical">
<ImageView
android:layout_width="32dp"
android:layout_height="32dp"
android:src="@android:drawable/ic_menu_upload" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="Unggah dokumen Anda di sini"
android:textColor="#777777" />
</LinearLayout>
</FrameLayout>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="17. Dokumen Sertifikat Halal"
android:textColor="@android:color/black" />
<FrameLayout
android:id="@+id/container_halal"
android:layout_width="match_parent"
android:layout_height="100dp"
android:layout_marginTop="8dp"
android:background="@android:drawable/editbox_background">
<ImageView
android:id="@+id/img_halal"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="centerInside"
android:visibility="gone" />
<LinearLayout
android:id="@+id/layout_upload_halal"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical">
<ImageView
android:layout_width="32dp"
android:layout_height="32dp"
android:src="@android:drawable/ic_menu_upload" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="Unggah dokumen Anda di sini"
android:textColor="#777777" />
</LinearLayout>
</FrameLayout>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="18. Pilih Titik Lokasi Usaha *"
android:textColor="@android:color/black" />
<FrameLayout
android:id="@+id/map_container"
android:layout_width="match_parent"
android:layout_height="200dp"
android:layout_marginTop="8dp"
android:background="@android:color/darker_gray">
<ImageView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="centerCrop"
android:src="@drawable/placeholder_image" />
</FrameLayout>
<Button
android:id="@+id/btn_register"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:layout_marginBottom="16dp"
android:background="#3F9CE8"
android:text="Daftar"
android:textColor="@android:color/white" />
</LinearLayout>
</ScrollView>
</LinearLayout>

View File

@ -49,7 +49,7 @@
android:text="@string/fragment_home_categories" android:text="@string/fragment_home_categories"
android:textColor="@color/blue_500" android:textColor="@color/blue_500"
android:fontFamily="@font/dmsans_bold" android:fontFamily="@font/dmsans_bold"
android:textSize="22sp" android:textSize="18sp"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/banners" /> app:layout_constraintTop_toBottomOf="@id/banners" />
@ -89,7 +89,7 @@
android:text="@string/sold_product_text" android:text="@string/sold_product_text"
android:textColor="@color/blue_500" android:textColor="@color/blue_500"
android:fontFamily="@font/dmsans_bold" android:fontFamily="@font/dmsans_bold"
android:textSize="22sp" android:textSize="18sp"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/categories" /> app:layout_constraintTop_toBottomOf="@id/categories" />
<com.google.android.material.button.MaterialButton <com.google.android.material.button.MaterialButton
@ -101,7 +101,7 @@
android:text="@string/show_all" android:text="@string/show_all"
android:textAllCaps="false" android:textAllCaps="false"
android:textColor="@color/blue_600" android:textColor="@color/blue_600"
android:textSize="16sp" android:textSize="14sp"
app:layout_constraintBaseline_toBaselineOf="@id/new_products_text" app:layout_constraintBaseline_toBaselineOf="@id/new_products_text"
app:layout_constraintEnd_toEndOf="parent" /> app:layout_constraintEnd_toEndOf="parent" />

View File

@ -16,6 +16,7 @@
android:layout_marginStart="16dp" android:layout_marginStart="16dp"
android:layout_marginTop="16dp" android:layout_marginTop="16dp"
android:src="@drawable/outline_account_circle_24" android:src="@drawable/outline_account_circle_24"
android:scaleType="centerCrop"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" /> app:layout_constraintTop_toTopOf="parent" />
@ -118,6 +119,7 @@
android:layout_marginStart="16dp" android:layout_marginStart="16dp"
android:layout_marginTop="8dp" android:layout_marginTop="8dp"
android:paddingVertical="8dp" android:paddingVertical="8dp"
android:textSize="16sp"
android:fontFamily="@font/dmsans_medium" android:fontFamily="@font/dmsans_medium"
android:text="Pesanan Saya" android:text="Pesanan Saya"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
@ -129,7 +131,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:background="?attr/selectableItemBackgroundBorderless" android:background="?attr/selectableItemBackgroundBorderless"
android:layout_marginEnd="16dp" android:layout_marginEnd="16dp"
android:textSize="12sp" android:textSize="14sp"
android:padding="0dp" android:padding="0dp"
android:fontFamily="@font/dmsans_light" android:fontFamily="@font/dmsans_light"
android:text="Lihat Riwayat Pesanan" android:text="Lihat Riwayat Pesanan"
@ -164,6 +166,8 @@
<TextView <TextView
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:textSize="12sp"
android:fontFamily="@font/dmsans_regular"
android:layout_marginTop="8dp" android:layout_marginTop="8dp"
android:text="@string/waiting_payment" /> android:text="@string/waiting_payment" />
</LinearLayout> </LinearLayout>
@ -185,6 +189,8 @@
<TextView <TextView
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:fontFamily="@font/dmsans_regular"
android:textSize="12sp"
android:layout_marginTop="8dp" android:layout_marginTop="8dp"
android:text="@string/packages" /> android:text="@string/packages" />
</LinearLayout> </LinearLayout>
@ -206,6 +212,8 @@
<TextView <TextView
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:fontFamily="@font/dmsans_regular"
android:textSize="12sp"
android:layout_marginTop="8dp" android:layout_marginTop="8dp"
android:text="@string/delivery" /> android:text="@string/delivery" />
</LinearLayout> </LinearLayout>
@ -223,103 +231,176 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="16dp" android:layout_marginTop="16dp"
android:padding="16dp" android:padding="16dp"
android:textSize="16sp"
android:text="Pengaturan Akun" android:text="Pengaturan Akun"
android:fontFamily="@font/dmsans_medium" android:fontFamily="@font/dmsans_medium"
app:layout_constraintTop_toBottomOf="@id/cardPesanan" /> app:layout_constraintTop_toBottomOf="@id/cardPesanan" />
<!-- Address --> <!-- Address -->
<LinearLayout
android:id="@+id/container_settings"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp"
app:layout_constraintTop_toBottomOf="@id/tvPengaturanAkun"
app:layout_constraintStart_toStartOf="parent">
<!-- Address Card -->
<androidx.cardview.widget.CardView
android:id="@+id/card_address"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:foreground="?attr/selectableItemBackground"
android:clickable="true"
android:focusable="true"
app:cardCornerRadius="8dp"
app:cardElevation="2dp">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp">
<ImageView <ImageView
android:id="@+id/ivAddress" android:id="@+id/ivAddress"
android:layout_width="24dp" android:layout_width="24dp"
android:layout_height="24dp" android:layout_height="24dp"
android:layout_marginStart="16dp"
android:src="@drawable/ic_address" android:src="@drawable/ic_address"
app:layout_constraintBottom_toBottomOf="@id/tvAddress"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@id/tvAddress" /> app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent" />
<TextView <TextView
android:id="@+id/tvAddress" android:id="@+id/tvAddress"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="16dp" android:layout_marginStart="16dp"
android:padding="16dp"
android:text="Alamat" android:text="Alamat"
app:layout_constraintEnd_toStartOf="@id/ivAddressArrow" android:textSize="14sp"
android:fontFamily="@font/dmsans_regular"
app:layout_constraintStart_toEndOf="@id/ivAddress" app:layout_constraintStart_toEndOf="@id/ivAddress"
app:layout_constraintTop_toBottomOf="@id/tvPengaturanAkun" /> app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/ivAddressArrow" />
<ImageView <ImageView
android:id="@+id/ivAddressArrow" android:id="@+id/ivAddressArrow"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:src="@drawable/ic_arrow_right" android:src="@drawable/ic_arrow_right"
app:layout_constraintBottom_toBottomOf="@id/tvAddress"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/tvAddress" /> app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.cardview.widget.CardView>
<!-- About Card -->
<androidx.cardview.widget.CardView
android:id="@+id/card_about"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:foreground="?attr/selectableItemBackground"
android:clickable="true"
android:focusable="true"
app:cardCornerRadius="8dp"
app:cardElevation="2dp">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp">
<!-- About -->
<ImageView <ImageView
android:id="@+id/ivAbout" android:id="@+id/ivAbout"
android:layout_width="24dp" android:layout_width="24dp"
android:layout_height="24dp" android:layout_height="24dp"
android:layout_marginStart="16dp" android:src="@drawable/baseline_info_24"
app:layout_constraintBottom_toBottomOf="@id/tvAbout"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@id/tvAbout" /> app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent" />
<TextView <TextView
android:id="@+id/tvAbout" android:id="@+id/tvAbout"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="16dp" android:layout_marginStart="16dp"
android:padding="16dp"
android:text="Tentang" android:text="Tentang"
app:layout_constraintEnd_toStartOf="@id/ivAboutArrow" android:textSize="14sp"
app:layout_constraintStart_toEndOf="@id/ivAbout" app:layout_constraintStart_toEndOf="@id/ivAbout"
app:layout_constraintTop_toBottomOf="@id/tvAddress" /> app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/ivAboutArrow" />
<ImageView <ImageView
android:id="@+id/ivAboutArrow" android:id="@+id/ivAboutArrow"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:src="@drawable/ic_arrow_right" android:src="@drawable/ic_arrow_right"
app:layout_constraintBottom_toBottomOf="@id/tvAbout"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/tvAbout" /> app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.cardview.widget.CardView>
<!-- Logout Card -->
<androidx.cardview.widget.CardView
android:id="@+id/card_logout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:foreground="?attr/selectableItemBackground"
android:clickable="true"
android:focusable="true"
app:cardCornerRadius="8dp"
app:cardElevation="2dp">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp">
<!-- Logout -->
<ImageView <ImageView
android:id="@+id/ivLogout" android:id="@+id/ivLogout"
android:layout_width="24dp" android:layout_width="24dp"
android:layout_height="24dp" android:layout_height="24dp"
android:layout_marginStart="16dp" android:src="@drawable/baseline_logout_24"
app:layout_constraintBottom_toBottomOf="@id/tvLogout"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@id/tvLogout" /> app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent" />
<TextView <TextView
android:id="@+id/tvLogout" android:id="@+id/tvLogout"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="16dp" android:layout_marginStart="16dp"
android:padding="16dp"
android:text="Keluar" android:text="Keluar"
app:layout_constraintEnd_toStartOf="@id/ivLogoutArrow" android:textSize="14sp"
app:layout_constraintStart_toEndOf="@id/ivLogout" app:layout_constraintStart_toEndOf="@id/ivLogout"
app:layout_constraintTop_toBottomOf="@id/tvAbout" /> app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/ivLogoutArrow" />
<ImageView <ImageView
android:id="@+id/ivLogoutArrow" android:id="@+id/ivLogoutArrow"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:src="@drawable/ic_arrow_right" android:src="@drawable/ic_arrow_right"
app:layout_constraintBottom_toBottomOf="@id/tvLogout"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/tvLogout" /> app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.cardview.widget.CardView>
</LinearLayout>
<!-- About -->
<!-- Logout -->
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/tv_reason"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="12dp"
android:textColor="@color/black"
android:textSize="14sp" />

View File

@ -7,11 +7,12 @@
<com.google.android.material.card.MaterialCardView <com.google.android.material.card.MaterialCardView
android:id="@+id/imageLayout" android:id="@+id/imageLayout"
android:layout_width="match_parent" android:layout_width="150dp"
android:layout_height="0dp" android:layout_height="0dp"
app:cardCornerRadius="14dp" app:cardCornerRadius="14dp"
app:layout_constraintDimensionRatio="272:218" app:layout_constraintDimensionRatio="272:218"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:cardElevation="0dp" app:cardElevation="0dp"
app:strokeColor="@color/gray_1" app:strokeColor="@color/gray_1"
app:strokeWidth="1dp"> app:strokeWidth="1dp">
@ -32,7 +33,7 @@
android:text="Banana" android:text="Banana"
android:textColor="@color/black" android:textColor="@color/black"
android:fontFamily="@font/dmsans_medium" android:fontFamily="@font/dmsans_medium"
android:textSize="18sp" android:textSize="16sp"
app:layout_constraintTop_toBottomOf="@id/imageLayout" /> app:layout_constraintTop_toBottomOf="@id/imageLayout" />
<TextView <TextView
@ -42,7 +43,7 @@
android:text="@string/item_price_txt" android:text="@string/item_price_txt"
android:textColor="@color/black" android:textColor="@color/black"
android:textStyle="bold" android:textStyle="bold"
android:textSize="18sp" android:textSize="16sp"
app:layout_constraintTop_toBottomOf="@id/item_name" /> app:layout_constraintTop_toBottomOf="@id/item_name" />
<TextView <TextView
@ -58,7 +59,7 @@
android:text="@string/rating" android:text="@string/rating"
android:textColor="@color/black" android:textColor="@color/black"
android:fontFamily="@font/dmsans_regular" android:fontFamily="@font/dmsans_regular"
android:textSize="14sp" android:textSize="12sp"
android:textAlignment="center" android:textAlignment="center"
android:gravity="center" android:gravity="center"
app:drawableStartCompat="@drawable/baseline_star_24" app:drawableStartCompat="@drawable/baseline_star_24"

View File

@ -1,16 +1,18 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android" <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center_horizontal" android:layout_gravity="center_horizontal"
xmlns:app="http://schemas.android.com/apk/res-auto"> xmlns:app="http://schemas.android.com/apk/res-auto">
<LinearLayout <LinearLayout
android:id="@+id/content"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:gravity="start" android:gravity="start"
android:layout_marginStart="16dp" android:layout_marginStart="16dp"
android:layout_marginVertical="8dp" android:layout_marginVertical="8dp"
android:layout_marginBottom="4dp"
android:layout_gravity="center_horizontal" android:layout_gravity="center_horizontal"
android:orientation="horizontal"> android:orientation="horizontal">
<RadioButton <RadioButton
@ -27,7 +29,7 @@
android:orientation="vertical"> android:orientation="vertical">
<TextView <TextView
android:id="@+id/courier_name_cost" android:id="@+id/courier_name_cost"
android:fontFamily="@font/dmsans_bold" android:fontFamily="@font/dmsans_semibold"
android:textSize="20sp" android:textSize="20sp"
android:padding="4dp" android:padding="4dp"
android:layout_width="wrap_content" android:layout_width="wrap_content"
@ -40,8 +42,8 @@
android:textSize="16sp" android:textSize="16sp"
android:paddingHorizontal="8dp" android:paddingHorizontal="8dp"
android:text="Estimasi 3-4 hari"/> android:text="Estimasi 3-4 hari"/>
</LinearLayout>
</LinearLayout>
<TextView <TextView
android:id="@+id/cost_price" android:id="@+id/cost_price"
@ -53,12 +55,16 @@
android:layout_height="match_parent" android:layout_height="match_parent"
android:text="Rp15.0000"/> android:text="Rp15.0000"/>
</LinearLayout> </LinearLayout>
<View <View
android:id="@+id/divider" android:id="@+id/divider"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="1dp" android:layout_height="1dp"
android:layout_marginTop="4dp"
android:background="#E0E0E0" android:background="#E0E0E0"
app:layout_constraintTop_toBottomOf="@id/linear_toolbar" /> app:layout_constraintTop_toBottomOf="@id/content" />
</androidx.cardview.widget.CardView> </androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,73 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/bg_bottom_sheet"
android:padding="16dp">
<TextView
android:id="@+id/tv_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:fontFamily="@font/dmsans_semibold"
android:text="Batalkan Pesanan"
android:textAlignment="center"
android:textColor="@color/black"
android:textSize="18sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/tv_reason_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="Alasan batalkan pesanan:"
android:fontFamily="@font/dmsans_regular"
android:textSize="14sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tv_title" />
<Spinner
android:id="@+id/spinner_reason"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:background="@drawable/bg_spinner_reason"
android:padding="12dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tv_reason_label" />
<Button
android:id="@+id/btn_cancel"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:layout_marginEnd="8dp"
android:backgroundTint="@color/black_200"
android:text="Kembali"
android:fontFamily="@font/dmsans_medium"
android:textColor="@color/white"
app:layout_constraintEnd_toStartOf="@+id/btn_confirm"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/spinner_reason" />
<Button
android:id="@+id/btn_confirm"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:backgroundTint="@color/blue_500"
android:text="Batal"
android:fontFamily="@font/dmsans_medium"
android:textColor="@color/white"
app:layout_constraintBottom_toBottomOf="@+id/btn_cancel"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/btn_cancel"
app:layout_constraintTop_toTopOf="@+id/btn_cancel" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -30,6 +30,7 @@
android:backgroundTint="@color/white" android:backgroundTint="@color/white"
android:src="@drawable/outline_notifications_24" android:src="@drawable/outline_notifications_24"
app:layout_constraintDimensionRatio="1:1" app:layout_constraintDimensionRatio="1:1"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="@id/search" app:layout_constraintBottom_toBottomOf="@id/search"
app:layout_constraintEnd_toStartOf="@id/btn_cart" app:layout_constraintEnd_toStartOf="@id/btn_cart"
app:layout_constraintTop_toTopOf="@id/search"/> app:layout_constraintTop_toTopOf="@id/search"/>

View File

@ -48,7 +48,7 @@
<style name="BottomNavigationTextStyle" parent="TextAppearance.MaterialComponents.Caption"> <style name="BottomNavigationTextStyle" parent="TextAppearance.MaterialComponents.Caption">
<item name="android:textSize">14sp</item> <item name="android:textSize">14sp</item>
<item name="fontFamily">@font/dmsans_semibold</item> <item name="fontFamily">@font/dmsans_medium</item>
<item name="android:paddingTop">8dp</item> <item name="android:paddingTop">8dp</item>
<item name="android:layout_marginTop">4dp</item> <item name="android:layout_marginTop">4dp</item>
</style> </style>