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:usesCleartextTraffic="true"
tools:targetApi="31">
<activity
android:name=".ui.auth.RegisterStoreActivity"
android:exported="false" />
<activity
android:name=".ui.profile.editprofile.EditProfileCustActivity"
android:exported="false" />
@ -64,7 +67,7 @@
<activity
android:name=".ui.profile.mystore.profile.shipping_service.ShippingServiceActivity"
android:exported="false"/>
android:exported="false" />
<activity
android:name=".ui.notif.NotificationActivity"
android:exported="false" />
@ -157,6 +160,7 @@
<action android:name="com.google.firebase.MESSAGING_EVENT" />
</intent-filter>
</service>
<meta-data
android:name="com.google.firebase.messaging.default_notification_icon"
android:resource="@drawable/outline_notifications_24" />
@ -168,6 +172,4 @@
android:value="fcm_default_channel" />
</application>
</manifest>

View File

@ -1,50 +1,17 @@
package com.alya.ecommerce_serang.app
import android.app.Application
import android.content.Context
import android.util.Log
import com.google.firebase.FirebaseApp
import com.google.firebase.messaging.FirebaseMessaging
import dagger.hilt.android.HiltAndroidApp
@HiltAndroidApp
class App : Application(){
private val TAG = "AppSerang"
// private val TAG = "AppSerang"
//
//// var tokenTes: String? = null
//
// override fun onCreate() {
//
// }
// var tokenTes: String? = null
override fun onCreate() {
super.onCreate()
// Initialize Firebase
FirebaseApp.initializeApp(this)
// Request FCM token at app startup
retrieveFCMToken()
}
private fun retrieveFCMToken() {
FirebaseMessaging.getInstance().token
.addOnCompleteListener { task ->
if (!task.isSuccessful) {
Log.e(TAG, "Failed to get FCM token", task.exception)
return@addOnCompleteListener
}
val token = task.result
// tokenTes = token
Log.d(TAG, "FCM token retrieved: $token")
// Save token locally
val sharedPreferences = getSharedPreferences("FCM_PREFS", Context.MODE_PRIVATE)
sharedPreferences.edit().putString("FCM_TOKEN", token).apply()
// Send to your server
sendTokenToServer(token)
}
}
private fun sendTokenToServer(token: String) {
Log.d(TAG, "Would send token to server: $token")
}
}

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()
.addInterceptor(loggingInterceptor)
.addInterceptor(authInterceptor)
.connectTimeout(60, TimeUnit.SECONDS) // Increase to 60 seconds
.readTimeout(60, TimeUnit.SECONDS) // Increase to 60 seconds
.writeTimeout(60, TimeUnit.SECONDS)
.build()
val retrofit = Retrofit.Builder()

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.AddPaymentInfoResponse
import com.alya.ecommerce_serang.data.api.dto.CancelOrderReq
import com.alya.ecommerce_serang.data.api.dto.CartItem
import com.alya.ecommerce_serang.data.api.dto.CityResponse
import com.alya.ecommerce_serang.data.api.dto.CompletedOrderRequest
import com.alya.ecommerce_serang.data.api.dto.CourierCostRequest
import com.alya.ecommerce_serang.data.api.dto.CreateAddressRequest
import com.alya.ecommerce_serang.data.api.dto.FcmReq
import com.alya.ecommerce_serang.data.api.dto.LoginRequest
import com.alya.ecommerce_serang.data.api.dto.OrderRequest
import com.alya.ecommerce_serang.data.api.dto.OrderRequestBuy
@ -20,10 +22,16 @@ import com.alya.ecommerce_serang.data.api.dto.ShippingServiceRequest
import com.alya.ecommerce_serang.data.api.dto.StoreAddressResponse
import com.alya.ecommerce_serang.data.api.dto.UpdateCart
import com.alya.ecommerce_serang.data.api.dto.UpdateChatRequest
import com.alya.ecommerce_serang.data.api.dto.VerifRegisReq
import com.alya.ecommerce_serang.data.api.response.auth.CheckStoreResponse
import com.alya.ecommerce_serang.data.api.response.auth.FcmTokenResponse
import com.alya.ecommerce_serang.data.api.response.auth.HasStoreResponse
import com.alya.ecommerce_serang.data.api.response.auth.ListStoreTypeResponse
import com.alya.ecommerce_serang.data.api.response.auth.LoginResponse
import com.alya.ecommerce_serang.data.api.response.auth.OtpResponse
import com.alya.ecommerce_serang.data.api.response.auth.RegisterResponse
import com.alya.ecommerce_serang.data.api.response.auth.RegisterStoreResponse
import com.alya.ecommerce_serang.data.api.response.auth.VerifRegisterResponse
import com.alya.ecommerce_serang.data.api.response.chat.ChatHistoryResponse
import com.alya.ecommerce_serang.data.api.response.chat.ChatListResponse
import com.alya.ecommerce_serang.data.api.response.chat.SendChatResponse
@ -32,6 +40,7 @@ import com.alya.ecommerce_serang.data.api.response.customer.cart.AddCartResponse
import com.alya.ecommerce_serang.data.api.response.customer.cart.DeleteCartResponse
import com.alya.ecommerce_serang.data.api.response.customer.cart.ListCartResponse
import com.alya.ecommerce_serang.data.api.response.customer.cart.UpdateCartResponse
import com.alya.ecommerce_serang.data.api.response.customer.order.CancelOrderResponse
import com.alya.ecommerce_serang.data.api.response.customer.order.CourierCostResponse
import com.alya.ecommerce_serang.data.api.response.customer.order.CreateOrderResponse
import com.alya.ecommerce_serang.data.api.response.customer.order.CreateReviewResponse
@ -73,6 +82,7 @@ import retrofit2.http.Multipart
import retrofit2.http.POST
import retrofit2.http.PUT
import retrofit2.http.Part
import retrofit2.http.PartMap
import retrofit2.http.Path
import retrofit2.http.Query
@ -82,20 +92,54 @@ interface ApiService {
@Body registerRequest: RegisterRequest
): Response<RegisterResponse>
@POST("verif")
suspend fun verifValue (
@Body verifRegisReq: VerifRegisReq
):VerifRegisterResponse
@GET("checkstore")
suspend fun checkStore (): Response<CheckStoreResponse>
// @Multipart
// @POST("registerstore")
// suspend fun registerStore(
//
// ): Response<>
@Multipart
@POST("registerstore")
suspend fun registerStore(
@Part("description") description: RequestBody,
@Part("store_type_id") storeTypeId: RequestBody,
@Part("latitude") latitude: RequestBody,
@Part("longitude") longitude: RequestBody,
@Part("street") street: RequestBody,
@Part("subdistrict") subdistrict: RequestBody,
@Part("city_id") cityId: RequestBody,
@Part("province_id") provinceId: RequestBody,
@Part("postal_code") postalCode: RequestBody,
@Part("detail") detail: RequestBody,
@Part("bank_name") bankName: RequestBody,
@Part("bank_num") bankNum: RequestBody,
@Part("store_name") storeName: RequestBody,
@Part storeimg: MultipartBody.Part?,
@Part ktp: MultipartBody.Part?,
@Part npwp: MultipartBody.Part?,
@Part nib: MultipartBody.Part?,
@Part persetujuan: MultipartBody.Part?,
@PartMap couriers: Map<String, @JvmSuppressWildcards RequestBody>,
@Part qris: MultipartBody.Part?,
@Part("account_name") accountName: RequestBody,
): Response<RegisterStoreResponse>
@POST("otp")
suspend fun getOTP(
@Body otpRequest: OtpRequest
):OtpResponse
@PUT("updatefcm")
suspend fun updateFcm(
@Body fcmReq: FcmReq
): FcmTokenResponse
@GET("checkstore")
suspend fun checkStoreUser(
): HasStoreResponse
@POST("login")
suspend fun login(
@Body loginRequest: LoginRequest
@ -105,6 +149,10 @@ interface ApiService {
suspend fun allCategory(
): Response<CategoryResponse>
@GET("storetype")
suspend fun listTypeStore(
): Response<ListStoreTypeResponse>
@GET("product")
suspend fun getAllProduct(): Response<AllProductResponse>
@ -131,6 +179,11 @@ interface ApiService {
@Body request: OrderRequest
): Response<CreateOrderResponse>
@POST("order/cancel")
suspend fun cancelOrder(
@Body cancelReq: CancelOrderReq
): Response<CancelOrderResponse>
@GET("order/detail/{id}")
suspend fun getDetailOrder(
@Path("id") orderId: Int

View File

@ -2,16 +2,17 @@ package com.alya.ecommerce_serang.data.repository
import android.util.Log
import com.alya.ecommerce_serang.data.api.dto.AddEvidenceMultipartRequest
import com.alya.ecommerce_serang.data.api.dto.CancelOrderReq
import com.alya.ecommerce_serang.data.api.dto.CompletedOrderRequest
import com.alya.ecommerce_serang.data.api.dto.CourierCostRequest
import com.alya.ecommerce_serang.data.api.dto.CreateAddressRequest
import com.alya.ecommerce_serang.data.api.dto.OrderRequest
import com.alya.ecommerce_serang.data.api.dto.OrderRequestBuy
import com.alya.ecommerce_serang.data.api.dto.OrdersItem
import com.alya.ecommerce_serang.data.api.dto.ReviewProductItem
import com.alya.ecommerce_serang.data.api.dto.UpdateCart
import com.alya.ecommerce_serang.data.api.dto.UserProfile
import com.alya.ecommerce_serang.data.api.response.customer.cart.DataItemCart
import com.alya.ecommerce_serang.data.api.response.customer.order.CancelOrderResponse
import com.alya.ecommerce_serang.data.api.response.customer.order.CourierCostResponse
import com.alya.ecommerce_serang.data.api.response.customer.order.CreateOrderResponse
import com.alya.ecommerce_serang.data.api.response.customer.order.CreateReviewResponse
@ -491,4 +492,23 @@ class OrderRepository(private val apiService: ApiService) {
}
suspend fun cancelOrder(cancelReq: CancelOrderReq): Result<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.net.Uri
import android.util.Log
import com.alya.ecommerce_serang.data.api.dto.FcmReq
import com.alya.ecommerce_serang.data.api.dto.LoginRequest
import com.alya.ecommerce_serang.data.api.dto.OtpRequest
import com.alya.ecommerce_serang.data.api.dto.RegisterRequest
import com.alya.ecommerce_serang.data.api.dto.UserProfile
import com.alya.ecommerce_serang.data.api.dto.VerifRegisReq
import com.alya.ecommerce_serang.data.api.response.auth.FcmTokenResponse
import com.alya.ecommerce_serang.data.api.response.auth.HasStoreResponse
import com.alya.ecommerce_serang.data.api.response.auth.ListStoreTypeResponse
import com.alya.ecommerce_serang.data.api.response.auth.LoginResponse
import com.alya.ecommerce_serang.data.api.response.auth.OtpResponse
import com.alya.ecommerce_serang.data.api.response.auth.RegisterStoreResponse
import com.alya.ecommerce_serang.data.api.response.auth.VerifRegisterResponse
import com.alya.ecommerce_serang.data.api.response.customer.order.ListCityResponse
import com.alya.ecommerce_serang.data.api.response.customer.order.ListProvinceResponse
import com.alya.ecommerce_serang.data.api.response.customer.profile.EditProfileResponse
import com.alya.ecommerce_serang.data.api.retrofit.ApiService
import com.alya.ecommerce_serang.utils.FileUtils
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.MultipartBody
import okhttp3.RequestBody
import okhttp3.RequestBody.Companion.asRequestBody
import okhttp3.RequestBody.Companion.toRequestBody
import java.io.File
class UserRepository(private val apiService: ApiService) {
//post data without message/response
@ -21,6 +34,31 @@ class UserRepository(private val apiService: ApiService) {
return apiService.getOTP(OtpRequest(email))
}
suspend fun listStoreType(): Result<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 {
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> {
return try {
val response = apiService.login(LoginRequest(email, password))
@ -130,6 +331,18 @@ class UserRepository(private val apiService: ApiService) {
Result.Error(e)
}
}
suspend fun checkStore(): HasStoreResponse{
return apiService.checkStoreUser()
}
suspend fun checkValue(request: VerifRegisReq): VerifRegisterResponse{
return apiService.verifValue(request)
}
suspend fun sendFcm(request: FcmReq): FcmTokenResponse{
return apiService.updateFcm(request)
}
companion object{
private const val TAG = "UserRepository"

View File

@ -1,8 +1,10 @@
package com.alya.ecommerce_serang.ui
import android.content.Context
import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle
import android.util.Log
import android.widget.Toast
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
@ -20,11 +22,15 @@ import com.alya.ecommerce_serang.data.api.retrofit.ApiService
import com.alya.ecommerce_serang.databinding.ActivityMainBinding
import com.alya.ecommerce_serang.ui.notif.WebSocketManager
import com.alya.ecommerce_serang.utils.SessionManager
import com.google.firebase.FirebaseApp
import com.google.firebase.messaging.FirebaseMessaging
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
private val TAG = "MainActivity"
private lateinit var binding: ActivityMainBinding
private lateinit var apiService: ApiService
private lateinit var sessionManager: SessionManager
@ -65,6 +71,11 @@ class MainActivity : AppCompatActivity() {
)
windowInsets
}
// Initialize Firebase
FirebaseApp.initializeApp(this)
// Request FCM token at app startup
retrieveFCMToken()
requestNotificationPermissionIfNeeded()
@ -151,4 +162,31 @@ class MainActivity : AppCompatActivity() {
}
}
}
private fun retrieveFCMToken() {
FirebaseMessaging.getInstance().token
.addOnCompleteListener { task ->
if (!task.isSuccessful) {
Log.e(TAG, "Failed to get FCM token", task.exception)
return@addOnCompleteListener
}
val token = task.result
// tokenTes = token
Log.d(TAG, "FCM token retrieved: $token")
// Save token locally
val sharedPreferences = getSharedPreferences("FCM_PREFS", Context.MODE_PRIVATE)
sharedPreferences.edit().putString("FCM_TOKEN", token).apply()
// Send to your server
sendTokenToServer(token)
}
}
private fun sendTokenToServer(token: String) {
Log.d(TAG, "Would send token to server: $token")
}
}

View File

@ -1,11 +1,14 @@
package com.alya.ecommerce_serang.ui.auth
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.util.Log
import android.widget.Toast
import androidx.activity.enableEdgeToEdge
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import com.alya.ecommerce_serang.data.api.dto.FcmReq
import com.alya.ecommerce_serang.data.api.retrofit.ApiConfig
import com.alya.ecommerce_serang.data.repository.Result
import com.alya.ecommerce_serang.data.repository.UserRepository
@ -14,9 +17,13 @@ import com.alya.ecommerce_serang.ui.MainActivity
import com.alya.ecommerce_serang.utils.BaseViewModelFactory
import com.alya.ecommerce_serang.utils.SessionManager
import com.alya.ecommerce_serang.utils.viewmodel.LoginViewModel
import com.google.firebase.FirebaseApp
import com.google.firebase.messaging.FirebaseMessaging
class LoginActivity : AppCompatActivity() {
private val TAG = "LoginActivity"
private lateinit var binding: ActivityLoginBinding
private val loginViewModel: LoginViewModel by viewModels{
BaseViewModelFactory {
@ -35,6 +42,11 @@ class LoginActivity : AppCompatActivity() {
setupListeners()
observeLoginState()
FirebaseApp.initializeApp(this)
// Request FCM token at app startup
retrieveFCMToken()
}
private fun setupListeners() {
@ -74,4 +86,35 @@ class LoginActivity : AppCompatActivity() {
}
}
}
private fun retrieveFCMToken() {
FirebaseMessaging.getInstance().token
.addOnCompleteListener { task ->
if (!task.isSuccessful) {
Log.e(TAG, "Failed to get FCM token", task.exception)
return@addOnCompleteListener
}
val token = task.result
// tokenTes = token
Log.d(TAG, "FCM token retrieved: $token")
// Save token locally
val sharedPreferences = getSharedPreferences("FCM_PREFS", Context.MODE_PRIVATE)
sharedPreferences.edit().putString("FCM_TOKEN", token).apply()
// Send to your server
sendTokenToServer(token)
}
}
private fun sendTokenToServer(token: String) {
Log.d(TAG, "Would send token to server: $token")
val tokenFcm=FcmReq(
fcmToken = token
)
loginViewModel.sendFcm(tokenFcm)
Log.d(TAG, "Sent token fcm: $token")
}
}

View File

@ -12,6 +12,7 @@ import androidx.core.view.ViewCompat
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat
import com.alya.ecommerce_serang.data.api.dto.RegisterRequest
import com.alya.ecommerce_serang.data.api.dto.VerifRegisReq
import com.alya.ecommerce_serang.data.api.retrofit.ApiConfig
import com.alya.ecommerce_serang.data.repository.Result
import com.alya.ecommerce_serang.data.repository.UserRepository
@ -27,6 +28,17 @@ import java.util.Locale
class RegisterActivity : AppCompatActivity() {
private lateinit var binding: ActivityRegisterBinding
private lateinit var sessionManager: SessionManager
private var isEmailValid = false
private var isPhoneValid = false
// Track which validation was last performed
private var lastCheckField = ""
// Counter for signup validation
private var signupValidationsComplete = 0
private var signupInProgress = false
private val registerViewModel: RegisterViewModel by viewModels{
BaseViewModelFactory {
val apiService = ApiConfig.getUnauthenticatedApiService()
@ -76,107 +88,248 @@ class RegisterActivity : AppCompatActivity() {
windowInsets
}
setupObservers()
// Observe OTP state
observeOtpState()
// Set up field validations
setupFieldValidations()
binding.btnSignup.setOnClickListener {
// Retrieve values inside the click listener (so we get latest input)
val birthDate = binding.etBirthDate.text.toString()
val email = binding.etEmail.text.toString()
val password = binding.etPassword.text.toString()
val phone = binding.etNumberPhone.text.toString()
val username = binding.etUsername.text.toString()
val name = binding.etFullname.text.toString()
val image = null
val userData = RegisterRequest(name, email, password, username, phone, birthDate, image)
Log.d("RegisterActivity", "Requesting OTP for email: $email")
// Request OTP and wait for success before showing dialog
registerViewModel.requestOtp(userData.email.toString())
// Observe OTP state and show OTP dialog only when successful
registerViewModel.otpState.observe(this) { result ->
when (result) {
is Result.Success -> {
Log.d("RegisterActivity", "OTP sent successfully. Showing OTP dialog.")
// Show OTP dialog after OTP is successfully sent
val otpBottomSheet = OtpBottomSheetDialog(userData) { fullUserData ->
Log.d("RegisterActivity", "OTP entered successfully. Proceeding with registration.")
registerViewModel.registerUser(fullUserData) // Send complete data
}
otpBottomSheet.show(supportFragmentManager, "OtpBottomSheet")
}
is Result.Error -> {
// Show error message if OTP request fails
Log.e("RegisterActivity", "Failed to request OTP: ${result.exception.message}")
Toast.makeText(this, "Failed to request OTP: ${result.exception.message}", Toast.LENGTH_LONG).show()
}
is Result.Loading -> {
// Optional: Show loading indicator
}
}
}
// Observe Register state
observeRegisterState()
handleSignUp()
}
binding.tvLoginAlt.setOnClickListener{
binding.tvLoginAlt.setOnClickListener {
val intent = Intent(this, LoginActivity::class.java)
startActivity(intent)
}
binding.etBirthDate.setOnClickListener{
binding.etBirthDate.setOnClickListener {
showDatePicker()
}
}
private fun observeOtpState() {
registerViewModel.otpState.observe(this) { result ->
when (result) {
is Result.Loading -> {
// Show loading indicator
binding.progressBarOtp.visibility = android.view.View.VISIBLE
}
is Result.Success -> {
// Hide loading indicator and show success message
binding.progressBarOtp.visibility = android.view.View.GONE
// Toast.makeText(this@RegisterActivity, result.data, Toast.LENGTH_SHORT).show()
}
is Result.Error -> {
// Hide loading indicator and show error message
binding.progressBarOtp.visibility = android.view.View.GONE
Toast.makeText(this, "OTP Request Failed: ${result.exception.message}", Toast.LENGTH_SHORT).show()
}
private fun setupFieldValidations() {
// Validate email when focus changes
binding.etEmail.setOnFocusChangeListener { _, hasFocus ->
if (!hasFocus) {
val email = binding.etEmail.text.toString()
if (email.isNotEmpty()) {
validateEmail(email, false)
}
}
}
// Validate phone when focus changes
binding.etNumberPhone.setOnFocusChangeListener { _, hasFocus ->
if (!hasFocus) {
val phone = binding.etNumberPhone.text.toString()
if (phone.isNotEmpty()) {
validatePhone(phone, false)
}
}
}
}
private fun observeRegisterState() {
registerViewModel.registerState.observe(this) { result ->
when (result) {
is Result.Loading -> {
// Show loading indicator for registration
binding.progressBarRegister.visibility = android.view.View.VISIBLE
private fun validateEmail(email: String, isSignup: Boolean) {
lastCheckField = "email"
Log.d("RegisterActivity", "Validating email: $email (signup: $isSignup)")
val checkValueEmail = VerifRegisReq(
fieldRegis = "email",
valueRegis = email
)
registerViewModel.checkValueReg(checkValueEmail)
}
private fun validatePhone(phone: String, isSignup: Boolean) {
lastCheckField = "phone"
Log.d("RegisterActivity", "Validating phone: $phone (signup: $isSignup)")
val checkValuePhone = VerifRegisReq(
fieldRegis = "phone",
valueRegis = phone
)
registerViewModel.checkValueReg(checkValuePhone)
}
private fun setupObservers() {
registerViewModel.checkValue.observe(this) { result ->
when (result) {
is Result.Loading -> {
// Show loading if needed
}
is Result.Success -> {
val isValid = (result.data as? Boolean) ?: false
when (lastCheckField) {
"email" -> {
isEmailValid = isValid
if (!isValid) {
Toast.makeText(this, "Email is already registered", Toast.LENGTH_SHORT).show()
} else {
Log.d("RegisterActivity", "Email is valid")
}
}
"phone" -> {
isPhoneValid = isValid
if (!isValid) {
Toast.makeText(this, "Phone number is already registered", Toast.LENGTH_SHORT).show()
} else {
Log.d("RegisterActivity", "Phone is valid")
}
}
}
is Result.Success -> {
// Hide loading indicator and show success message
binding.progressBarRegister.visibility = android.view.View.GONE
Toast.makeText(this, result.data, Toast.LENGTH_SHORT).show()
val intent = Intent(this, LoginActivity::class.java)
startActivity(intent)
// Navigate to another screen if needed
}
is com.alya.ecommerce_serang.data.repository.Result.Error -> {
// Hide loading indicator and show error message
binding.progressBarRegister.visibility = android.view.View.GONE
Toast.makeText(this, "Registration Failed: ${result.exception.message}", Toast.LENGTH_SHORT).show()
// Check if we're in signup process
if (signupInProgress) {
signupValidationsComplete++
// Check if both validations completed
if (signupValidationsComplete >= 2) {
signupInProgress = false
signupValidationsComplete = 0
// If both validations passed, request OTP
if (isEmailValid && isPhoneValid) {
requestOtp()
}
}
}
}
is Result.Error -> {
val fieldType = if (lastCheckField == "email") "Email" else "Phone"
Toast.makeText(this, "$fieldType validation failed: ${result.exception.message}", Toast.LENGTH_SHORT).show()
// Mark validation as invalid
if (lastCheckField == "email") {
isEmailValid = false
} else if (lastCheckField == "phone") {
isPhoneValid = false
}
// Update signup validation counter if in signup process
if (signupInProgress) {
signupValidationsComplete++
// Check if both validations completed
if (signupValidationsComplete >= 2) {
signupInProgress = false
signupValidationsComplete = 0
}
}
}
else -> {
Log.e("RegisterActivity", "Unexpected result type: $result")
}
}
}
registerViewModel.otpState.observe(this) { result ->
when (result) {
is Result.Loading -> {
binding.progressBarOtp.visibility = android.view.View.VISIBLE
}
is Result.Success -> {
binding.progressBarOtp.visibility = android.view.View.GONE
Log.d("RegisterActivity", "OTP sent successfully. Showing OTP dialog.")
// Create user data before showing OTP dialog
val userData = createUserData()
// Show OTP dialog
val otpBottomSheet = OtpBottomSheetDialog(userData) { fullUserData ->
Log.d("RegisterActivity", "OTP entered successfully. Proceeding with registration.")
registerViewModel.registerUser(fullUserData)
}
otpBottomSheet.show(supportFragmentManager, "OtpBottomSheet")
}
is Result.Error -> {
binding.progressBarOtp.visibility = android.view.View.GONE
Toast.makeText(this, "OTP Request Failed: ${result.exception.message}", Toast.LENGTH_SHORT).show()
}
else -> {
Log.e("RegisterActivity", "Unexpected result type: $result")
}
}
}
registerViewModel.registerState.observe(this) { result ->
when (result) {
is Result.Loading -> {
// Show loading indicator for registration
binding.progressBarRegister.visibility = android.view.View.VISIBLE
}
is Result.Success -> {
// Hide loading indicator and show success message
binding.progressBarRegister.visibility = android.view.View.GONE
Toast.makeText(this, result.data, Toast.LENGTH_SHORT).show()
val intent = Intent(this, LoginActivity::class.java)
startActivity(intent)
// Navigate to another screen if needed
}
is com.alya.ecommerce_serang.data.repository.Result.Error -> {
// Hide loading indicator and show error message
binding.progressBarRegister.visibility = android.view.View.GONE
Toast.makeText(this, "Registration Failed: ${result.exception.message}", Toast.LENGTH_SHORT).show()
}
}
}
}
private fun handleSignUp() {
// Basic validation first
val email = binding.etEmail.text.toString()
val password = binding.etPassword.text.toString()
val confirmPassword = binding.etConfirmPassword.text.toString()
val phone = binding.etNumberPhone.text.toString()
val username = binding.etUsername.text.toString()
val name = binding.etFullname.text.toString()
val birthDate = binding.etBirthDate.text.toString()
// Check if fields are filled
if (email.isEmpty() || password.isEmpty() || confirmPassword.isEmpty() ||
phone.isEmpty() || username.isEmpty() || name.isEmpty() || birthDate.isEmpty()) {
Toast.makeText(this, "Please fill all required fields", Toast.LENGTH_SHORT).show()
return
}
// Check if passwords match
if (password != confirmPassword) {
Toast.makeText(this, "Passwords do not match", Toast.LENGTH_SHORT).show()
return
}
// If both validations are already done and successful, just request OTP
if (isEmailValid && isPhoneValid) {
requestOtp()
return
}
// Reset validation counters
signupInProgress = true
signupValidationsComplete = 0
// Start validations in parallel
validateEmail(email, true)
validatePhone(phone, true)
}
private fun requestOtp() {
val email = binding.etEmail.text.toString()
Log.d("RegisterActivity", "Requesting OTP for email: $email")
registerViewModel.requestOtp(email)
}
private fun createUserData(): RegisterRequest {
// Get all form values
val birthDate = binding.etBirthDate.text.toString()
val email = binding.etEmail.text.toString()
val password = binding.etPassword.text.toString()
val phone = binding.etNumberPhone.text.toString()
val username = binding.etUsername.text.toString()
val name = binding.etFullname.text.toString()
val image = null
// Create and return user data object
return RegisterRequest(name, email, password, username, phone, birthDate, image)
}
private fun showDatePicker() {
@ -189,7 +342,7 @@ class RegisterActivity : AppCompatActivity() {
this,
{ _, selectedYear, selectedMonth, selectedDay ->
calendar.set(selectedYear, selectedMonth, selectedDay)
val sdf = SimpleDateFormat("dd-MM-yyyy", Locale.getDefault())
val sdf = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault())
binding.etBirthDate.setText(sdf.format(calendar.time))
},
year, month, day

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

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

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

View File

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

View File

@ -398,7 +398,7 @@ class AddAddressActivity : AppCompatActivity() {
isRequestingLocation = false
Toast.makeText(this, "Timeout lokasi, menggunakan lokasi default", Toast.LENGTH_SHORT).show()
}
}, 15000) // 15 seconds timeout
}, 60000) // 15 seconds timeout
// Try getting last known location first
try {

View File

@ -5,8 +5,10 @@ import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.alya.ecommerce_serang.data.api.dto.CancelOrderReq
import com.alya.ecommerce_serang.data.api.dto.CompletedOrderRequest
import com.alya.ecommerce_serang.data.api.dto.OrdersItem
import com.alya.ecommerce_serang.data.api.response.customer.order.CancelOrderResponse
import com.alya.ecommerce_serang.data.api.response.customer.order.OrderListItemsItem
import com.alya.ecommerce_serang.data.api.response.customer.order.Orders
import com.alya.ecommerce_serang.data.api.response.order.CompletedOrderResponse
@ -31,6 +33,11 @@ class HistoryViewModel(private val repository: OrderRepository) : ViewModel() {
private val _orderDetails = MutableLiveData<Orders>()
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
private val _orderItems = MutableLiveData<List<OrderListItemsItem>>()
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.Dialog
import android.content.ContextWrapper
import android.content.Intent
import android.graphics.Color
import android.net.Uri
@ -17,6 +18,7 @@ import android.widget.ImageView
import android.widget.ProgressBar
import android.widget.TextView
import android.widget.Toast
import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.findViewTreeLifecycleOwner
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
@ -24,6 +26,7 @@ import com.alya.ecommerce_serang.R
import com.alya.ecommerce_serang.data.api.dto.OrdersItem
import com.alya.ecommerce_serang.data.api.dto.ReviewUIItem
import com.alya.ecommerce_serang.ui.order.detail.PaymentActivity
import com.alya.ecommerce_serang.ui.order.history.cancelorder.CancelOrderBottomSheet
import com.alya.ecommerce_serang.ui.order.review.CreateReviewActivity
import com.alya.ecommerce_serang.ui.product.ReviewProductActivity
import com.google.android.material.button.MaterialButton
@ -150,7 +153,8 @@ class OrderHistoryAdapter(
visibility = View.VISIBLE
text = itemView.context.getString(R.string.canceled_order_btn)
setOnClickListener {
showCancelOrderDialog(order.orderId.toString())
showCancelOrderBottomSheet(order.orderId)
viewModel.refreshOrders()
}
}
deadlineDate.apply {
@ -171,7 +175,8 @@ class OrderHistoryAdapter(
visibility = View.VISIBLE
text = itemView.context.getString(R.string.canceled_order_btn)
setOnClickListener {
showCancelOrderDialog(order.orderId.toString())
showCancelOrderBottomSheet(order.orderId)
viewModel.refreshOrders()
}
}
@ -208,6 +213,7 @@ class OrderHistoryAdapter(
text = itemView.context.getString(R.string.canceled_order_btn)
setOnClickListener {
showCancelOrderDialog(order.orderId.toString())
viewModel.refreshOrders()
}
}
}
@ -226,7 +232,7 @@ class OrderHistoryAdapter(
text = itemView.context.getString(R.string.claim_complaint)
setOnClickListener {
showCancelOrderDialog(order.orderId.toString())
// Handle click event
viewModel.refreshOrders()
}
}
btnRight.apply {
@ -235,6 +241,7 @@ class OrderHistoryAdapter(
setOnClickListener {
// Handle click event
viewModel.confirmOrderCompleted(order.orderId, "completed")
viewModel.refreshOrders()
}
}
@ -243,16 +250,6 @@ class OrderHistoryAdapter(
text = formatShipmentDate(order.updatedAt, order.etd ?: "0")
}
}
"delivered" -> {
// Untuk status delivered, tampilkan "Beri Ulasan"
btnRight.apply {
visibility = View.VISIBLE
text = itemView.context.getString(R.string.add_review)
setOnClickListener {
// Handle click event
}
}
}
"completed" -> {
statusOrder.apply {
visibility = View.VISIBLE
@ -267,6 +264,7 @@ class OrderHistoryAdapter(
text = itemView.context.getString(R.string.add_review)
setOnClickListener {
addReviewProduct(order)
viewModel.refreshOrders()
// Handle click event
}
}
@ -492,6 +490,48 @@ class OrderHistoryAdapter(
dialog.show()
}
private fun showCancelOrderBottomSheet(orderId : Int) {
val context = itemView.context
// We need a FragmentManager to show the bottom sheet
// Try to get it from the context
val fragmentActivity = when (context) {
is FragmentActivity -> context
is ContextWrapper -> {
val baseContext = context.baseContext
if (baseContext is FragmentActivity) {
baseContext
} else {
// Log error and show a Toast instead if we can't get a FragmentManager
Log.e("OrderHistoryAdapter", "Cannot show bottom sheet: Context is not a FragmentActivity")
Toast.makeText(context, "Cannot show cancel order dialog", Toast.LENGTH_SHORT).show()
return
}
}
else -> {
// Log error and show a Toast instead if we can't get a FragmentManager
Log.e("OrderHistoryAdapter", "Cannot show bottom sheet: Context is not a FragmentActivity")
Toast.makeText(context, "Cannot show cancel order dialog", Toast.LENGTH_SHORT).show()
return
}
}
// Create and show the bottom sheet using the obtained FragmentManager
val bottomSheet = CancelOrderBottomSheet(
orderId = orderId,
onOrderCancelled = {
// Handle the successful cancellation
// Refresh the data
viewModel.refreshOrders() // Assuming there's a method to refresh orders
// Show a success message
Toast.makeText(context, "Order cancelled successfully", Toast.LENGTH_SHORT).show()
}
)
bottomSheet.show(fragmentActivity.supportFragmentManager, CancelOrderBottomSheet.TAG)
}
private fun addReviewProduct(order: OrdersItem) {
// Use ViewModel to fetch order details
viewModel.getOrderDetails(order.orderId)

View File

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

View File

@ -14,9 +14,7 @@ class OrderViewPagerAdapter(
"pending", // Menunggu Tagihan
"unpaid", // Belum Dibayar
"processed", // Diproses
"paid", // Dibayar
"shipped", // Dikirim
"delivered", // Diterima
"completed", // Selesai
"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.databinding.ActivityDetailOrderStatusBinding
import com.alya.ecommerce_serang.ui.order.detail.PaymentActivity
import com.alya.ecommerce_serang.ui.order.history.cancelorder.CancelOrderBottomSheet
import com.alya.ecommerce_serang.ui.order.review.CreateReviewActivity
import com.alya.ecommerce_serang.ui.product.ReviewProductActivity
import com.alya.ecommerce_serang.utils.BaseViewModelFactory
@ -50,6 +51,7 @@ class DetailOrderStatusActivity : AppCompatActivity() {
private lateinit var binding: ActivityDetailOrderStatusBinding
private lateinit var sessionManager: SessionManager
private var orderId: Int = -1
private var orderStatus: String = ""
private val orders = mutableListOf<OrdersItem>()
@ -181,9 +183,9 @@ class DetailOrderStatusActivity : AppCompatActivity() {
setupProductsRecyclerView(orders.orderItems)
// 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
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'")
when (status) {
"pending", "unpaid" -> {
Log.d(TAG, "adjustButtonsBasedOnStatus: Setting up UI for pending/unpaid order")
// Show status note
"pending"->{
binding.tvStatusHeader.text = "Menunggu Tagihan"
binding.tvStatusNote.visibility = View.VISIBLE
binding.tvStatusNote.text = "Pesanan ini harus dibayar sebelum ${formatDatePay(orders.updatedAt)}"
@ -250,7 +250,27 @@ class DetailOrderStatusActivity : AppCompatActivity() {
text = "Batalkan Pesanan"
setOnClickListener {
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" -> {
Log.d(TAG, "adjustButtonsBasedOnStatus: Setting up UI for processed order")
binding.tvStatusHeader.text = "Sedang Diproses"
binding.tvStatusNote.visibility = View.VISIBLE
binding.tvStatusNote.text = "Penjual sedang memproses pesanan Anda"
@ -279,13 +300,19 @@ class DetailOrderStatusActivity : AppCompatActivity() {
setOnClickListener {
Log.d(TAG, "Cancel Order button clicked for processed order")
showCancelOrderDialog(orders.orderId.toString())
viewModel.getOrderDetails(orders.orderId)
}
}
binding.btnPrimary.apply {
visibility = View.GONE
}
}
"shipped" -> {
Log.d(TAG, "adjustButtonsBasedOnStatus: Setting up UI for shipped order")
binding.tvStatusHeader.text = "Sudah Dikirim"
binding.tvStatusNote.visibility = View.VISIBLE
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"
setOnClickListener {
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")
binding.tvStatusNote.visibility = View.VISIBLE
binding.tvStatusNote.text = "Pesanan telah selesai"
binding.tvStatusHeader.text = "Pesanan Selesai"
binding.tvStatusNote.visibility = View.GONE
binding.btnPrimary.apply {
visibility = View.VISIBLE
@ -326,15 +354,27 @@ class DetailOrderStatusActivity : AppCompatActivity() {
setOnClickListener {
Log.d(TAG, "Review button clicked")
addReviewForOrder(orders)
viewModel.getOrderDetails(orders.orderId)
}
}
binding.btnSecondary.apply {
visibility = View.GONE
}
}
"canceled" -> {
Log.d(TAG, "adjustButtonsBasedOnStatus: Setting up UI for canceled order")
binding.tvStatusHeader.text = "Pesanan Selesai"
binding.tvStatusNote.visibility = View.VISIBLE
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()
}
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 {
Log.d(TAG, "formatDate: Formatting date: $dateString")

View File

@ -414,7 +414,8 @@ class DetailProductActivity : AppCompatActivity() {
productImage = productDetail.image,
productRating = productDetail.rating,
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
import android.app.AlertDialog
import android.app.ProgressDialog
import android.content.Intent
import android.os.Bundle
import android.util.Log
@ -9,18 +11,24 @@ import android.view.ViewGroup
import android.widget.Toast
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope
import com.alya.ecommerce_serang.BuildConfig.BASE_URL
import com.alya.ecommerce_serang.R
import com.alya.ecommerce_serang.data.api.dto.UserProfile
import com.alya.ecommerce_serang.data.api.retrofit.ApiConfig
import com.alya.ecommerce_serang.data.repository.UserRepository
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.profile.mystore.MyStoreActivity
import com.alya.ecommerce_serang.utils.BaseViewModelFactory
import com.alya.ecommerce_serang.utils.SessionManager
import com.alya.ecommerce_serang.utils.viewmodel.ProfileViewModel
import com.bumptech.glide.Glide
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
class ProfileFragment : Fragment() {
@ -53,10 +61,21 @@ class ProfileFragment : Fragment() {
super.onViewCreated(view, savedInstanceState)
observeUserProfile()
viewModel.loadUserProfile()
viewModel.checkStoreUser()
binding.cardBukaToko.setOnClickListener{
val intentBuka = Intent(requireContext(), MyStoreActivity::class.java)
startActivity(intentBuka)
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)
startActivity(intentBuka)
} else {
val intentBuka = Intent(requireContext(), RegisterStoreActivity::class.java)
startActivity(intentBuka)
}
}
binding.btnDetailProfile.setOnClickListener{
@ -73,6 +92,16 @@ class ProfileFragment : Fragment() {
val intent = Intent(requireContext(), HistoryActivity::class.java)
startActivity(intent)
}
binding.cardLogout.setOnClickListener({
logout()
})
binding.cardAddress.setOnClickListener({
val intent = Intent(requireContext(), AddressActivity::class.java)
startActivity(intent)
})
}
private fun observeUserProfile() {
@ -103,4 +132,41 @@ class ProfileFragment : Fragment() {
.placeholder(R.drawable.placeholder_image)
.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_IMAGE = "product_image"
const val EXTRA_PRODUCT_RATING = "product_rating"
const val EXTRA_STORE_IMAGE = "store_image"
// Request codes
const val REQUEST_IMAGE_PICK = 1001

View File

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

View File

@ -1,9 +1,12 @@
package com.alya.ecommerce_serang.utils.viewmodel
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.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.repository.Result
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>>()
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) {
viewModelScope.launch {
_loginState.value = Result.Loading
@ -20,4 +30,31 @@ class LoginViewModel(private val repository: UserRepository) : ViewModel() {
_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.viewModelScope
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.repository.Result
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>>()
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(){
viewModelScope.launch {
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(
context: Context,
username: String,
@ -71,6 +100,17 @@ class ProfileViewModel(private val userRepository: UserRepository) : ViewModel()
}
}
fun logout(){
viewModelScope.launch {
try{
} catch (e: Exception){
}
}
}
companion object {
private const val TAG = "ProfileViewModel"
}

View File

@ -6,7 +6,9 @@ import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
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.VerifRegisterResponse
import com.alya.ecommerce_serang.data.repository.Result
import com.alya.ecommerce_serang.data.repository.UserRepository
import kotlinx.coroutines.launch
@ -21,6 +23,9 @@ class RegisterViewModel(private val repository: UserRepository) : ViewModel() {
private val _otpState = MutableLiveData<Result<Unit>>()
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
private val _message = MutableLiveData<String>()
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:textColor="@color/blue_500"
android:fontFamily="@font/dmsans_bold"
android:textSize="22sp"
android:textSize="18sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/banners" />
@ -89,7 +89,7 @@
android:text="@string/sold_product_text"
android:textColor="@color/blue_500"
android:fontFamily="@font/dmsans_bold"
android:textSize="22sp"
android:textSize="18sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/categories" />
<com.google.android.material.button.MaterialButton
@ -101,7 +101,7 @@
android:text="@string/show_all"
android:textAllCaps="false"
android:textColor="@color/blue_600"
android:textSize="16sp"
android:textSize="14sp"
app:layout_constraintBaseline_toBaselineOf="@id/new_products_text"
app:layout_constraintEnd_toEndOf="parent" />

View File

@ -16,6 +16,7 @@
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:src="@drawable/outline_account_circle_24"
android:scaleType="centerCrop"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
@ -118,6 +119,7 @@
android:layout_marginStart="16dp"
android:layout_marginTop="8dp"
android:paddingVertical="8dp"
android:textSize="16sp"
android:fontFamily="@font/dmsans_medium"
android:text="Pesanan Saya"
app:layout_constraintStart_toStartOf="parent"
@ -129,7 +131,7 @@
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackgroundBorderless"
android:layout_marginEnd="16dp"
android:textSize="12sp"
android:textSize="14sp"
android:padding="0dp"
android:fontFamily="@font/dmsans_light"
android:text="Lihat Riwayat Pesanan"
@ -164,6 +166,8 @@
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="12sp"
android:fontFamily="@font/dmsans_regular"
android:layout_marginTop="8dp"
android:text="@string/waiting_payment" />
</LinearLayout>
@ -185,6 +189,8 @@
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="@font/dmsans_regular"
android:textSize="12sp"
android:layout_marginTop="8dp"
android:text="@string/packages" />
</LinearLayout>
@ -206,6 +212,8 @@
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="@font/dmsans_regular"
android:textSize="12sp"
android:layout_marginTop="8dp"
android:text="@string/delivery" />
</LinearLayout>
@ -223,103 +231,176 @@
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:padding="16dp"
android:textSize="16sp"
android:text="Pengaturan Akun"
android:fontFamily="@font/dmsans_medium"
app:layout_constraintTop_toBottomOf="@id/cardPesanan" />
<!-- Address -->
<ImageView
android:id="@+id/ivAddress"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginStart="16dp"
android:src="@drawable/ic_address"
app:layout_constraintBottom_toBottomOf="@id/tvAddress"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@id/tvAddress" />
<TextView
android:id="@+id/tvAddress"
android:layout_width="0dp"
<LinearLayout
android:id="@+id/container_settings"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:orientation="vertical"
android:padding="16dp"
android:text="Alamat"
app:layout_constraintEnd_toStartOf="@id/ivAddressArrow"
app:layout_constraintStart_toEndOf="@id/ivAddress"
app:layout_constraintTop_toBottomOf="@id/tvPengaturanAkun" />
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
android:id="@+id/ivAddress"
android:layout_width="24dp"
android:layout_height="24dp"
android:src="@drawable/ic_address"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent" />
<TextView
android:id="@+id/tvAddress"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:text="Alamat"
android:textSize="14sp"
android:fontFamily="@font/dmsans_regular"
app:layout_constraintStart_toEndOf="@id/ivAddress"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/ivAddressArrow" />
<ImageView
android:id="@+id/ivAddressArrow"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ic_arrow_right"
app:layout_constraintEnd_toEndOf="parent"
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">
<ImageView
android:id="@+id/ivAbout"
android:layout_width="24dp"
android:layout_height="24dp"
android:src="@drawable/baseline_info_24"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent" />
<TextView
android:id="@+id/tvAbout"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:text="Tentang"
android:textSize="14sp"
app:layout_constraintStart_toEndOf="@id/ivAbout"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/ivAboutArrow" />
<ImageView
android:id="@+id/ivAboutArrow"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ic_arrow_right"
app:layout_constraintEnd_toEndOf="parent"
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">
<ImageView
android:id="@+id/ivLogout"
android:layout_width="24dp"
android:layout_height="24dp"
android:src="@drawable/baseline_logout_24"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent" />
<TextView
android:id="@+id/tvLogout"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:text="Keluar"
android:textSize="14sp"
app:layout_constraintStart_toEndOf="@id/ivLogout"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/ivLogoutArrow" />
<ImageView
android:id="@+id/ivLogoutArrow"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ic_arrow_right"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.cardview.widget.CardView>
</LinearLayout>
<ImageView
android:id="@+id/ivAddressArrow"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:src="@drawable/ic_arrow_right"
app:layout_constraintBottom_toBottomOf="@id/tvAddress"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/tvAddress" />
<!-- About -->
<ImageView
android:id="@+id/ivAbout"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginStart="16dp"
app:layout_constraintBottom_toBottomOf="@id/tvAbout"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@id/tvAbout" />
<TextView
android:id="@+id/tvAbout"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:padding="16dp"
android:text="Tentang"
app:layout_constraintEnd_toStartOf="@id/ivAboutArrow"
app:layout_constraintStart_toEndOf="@id/ivAbout"
app:layout_constraintTop_toBottomOf="@id/tvAddress" />
<ImageView
android:id="@+id/ivAboutArrow"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:src="@drawable/ic_arrow_right"
app:layout_constraintBottom_toBottomOf="@id/tvAbout"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/tvAbout" />
<!-- Logout -->
<ImageView
android:id="@+id/ivLogout"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginStart="16dp"
app:layout_constraintBottom_toBottomOf="@id/tvLogout"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@id/tvLogout" />
<TextView
android:id="@+id/tvLogout"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:padding="16dp"
android:text="Keluar"
app:layout_constraintEnd_toStartOf="@id/ivLogoutArrow"
app:layout_constraintStart_toEndOf="@id/ivLogout"
app:layout_constraintTop_toBottomOf="@id/tvAbout" />
<ImageView
android:id="@+id/ivLogoutArrow"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:src="@drawable/ic_arrow_right"
app:layout_constraintBottom_toBottomOf="@id/tvLogout"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/tvLogout" />
</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
android:id="@+id/imageLayout"
android:layout_width="match_parent"
android:layout_width="150dp"
android:layout_height="0dp"
app:cardCornerRadius="14dp"
app:layout_constraintDimensionRatio="272:218"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:cardElevation="0dp"
app:strokeColor="@color/gray_1"
app:strokeWidth="1dp">
@ -32,7 +33,7 @@
android:text="Banana"
android:textColor="@color/black"
android:fontFamily="@font/dmsans_medium"
android:textSize="18sp"
android:textSize="16sp"
app:layout_constraintTop_toBottomOf="@id/imageLayout" />
<TextView
@ -42,7 +43,7 @@
android:text="@string/item_price_txt"
android:textColor="@color/black"
android:textStyle="bold"
android:textSize="18sp"
android:textSize="16sp"
app:layout_constraintTop_toBottomOf="@id/item_name" />
<TextView
@ -58,7 +59,7 @@
android:text="@string/rating"
android:textColor="@color/black"
android:fontFamily="@font/dmsans_regular"
android:textSize="14sp"
android:textSize="12sp"
android:textAlignment="center"
android:gravity="center"
app:drawableStartCompat="@drawable/baseline_star_24"

View File

@ -1,16 +1,18 @@
<?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_height="wrap_content"
android:layout_gravity="center_horizontal"
xmlns:app="http://schemas.android.com/apk/res-auto">
<LinearLayout
android:id="@+id/content"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="start"
android:layout_marginStart="16dp"
android:layout_marginVertical="8dp"
android:layout_marginBottom="4dp"
android:layout_gravity="center_horizontal"
android:orientation="horizontal">
<RadioButton
@ -27,7 +29,7 @@
android:orientation="vertical">
<TextView
android:id="@+id/courier_name_cost"
android:fontFamily="@font/dmsans_bold"
android:fontFamily="@font/dmsans_semibold"
android:textSize="20sp"
android:padding="4dp"
android:layout_width="wrap_content"
@ -40,8 +42,8 @@
android:textSize="16sp"
android:paddingHorizontal="8dp"
android:text="Estimasi 3-4 hari"/>
</LinearLayout>
</LinearLayout>
<TextView
android:id="@+id/cost_price"
@ -53,12 +55,16 @@
android:layout_height="match_parent"
android:text="Rp15.0000"/>
</LinearLayout>
<View
android:id="@+id/divider"
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginTop="4dp"
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:src="@drawable/outline_notifications_24"
app:layout_constraintDimensionRatio="1:1"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="@id/search"
app:layout_constraintEnd_toStartOf="@id/btn_cart"
app:layout_constraintTop_toTopOf="@id/search"/>

View File

@ -48,7 +48,7 @@
<style name="BottomNavigationTextStyle" parent="TextAppearance.MaterialComponents.Caption">
<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:layout_marginTop">4dp</item>
</style>