edit store profile & topup

This commit is contained in:
Gracia Hotmauli
2025-05-13 19:39:54 +07:00
parent 1cbeb168dc
commit bf810ddc3e
30 changed files with 2374 additions and 150 deletions

View File

@ -0,0 +1,60 @@
package com.alya.ecommerce_serang.data.api.dto
import com.google.gson.annotations.SerializedName
data class PaymentInfo(
@SerializedName("id")
val id: Int,
@SerializedName("bank_num")
val bankNum: String,
@SerializedName("bank_name")
val bankName: String,
@SerializedName("qris_image")
val qrisImage: String?,
@SerializedName("account_name")
val accountName: String?
)
data class PaymentInfoResponse(
@SerializedName("message")
val message: String,
@SerializedName("payment")
val payment: List<PaymentInfo>
)
data class AddPaymentInfoRequest(
@SerializedName("bank_name")
val bankName: String,
@SerializedName("bank_num")
val bankNum: String,
@SerializedName("account_name")
val accountName: String
// qris will be sent as multipart form data
)
data class DeletePaymentInfoResponse(
@SerializedName("message")
val message: String,
@SerializedName("success")
val success: Boolean
)
data class AddPaymentInfoResponse(
@SerializedName("message")
val message: String,
@SerializedName("success")
val success: Boolean,
@SerializedName("payment_method")
val paymentInfo: PaymentInfo?
)

View File

@ -0,0 +1,13 @@
package com.alya.ecommerce_serang.data.api.dto
import com.google.gson.annotations.SerializedName
data class ShippingService(
@SerializedName("courier")
val courier: String
)
data class ShippingServiceRequest(
@SerializedName("couriers")
val couriers: List<String>
)

View File

@ -0,0 +1,18 @@
package com.alya.ecommerce_serang.data.api.response.store
import com.google.gson.annotations.SerializedName
data class StoreResponse(
val message: String,
val store: Store
)
data class Store(
@SerializedName("store_id") val storeId: Int,
@SerializedName("store_status") val storeStatus: String,
@SerializedName("store_name") val storeName: String,
@SerializedName("user_name") val userName: String,
val email: String,
@SerializedName("user_phone") val userPhone: String,
val balance: String
)

View File

@ -0,0 +1,11 @@
package com.alya.ecommerce_serang.data.api.response.store.profile
import com.google.gson.annotations.SerializedName
data class GenericResponse(
@SerializedName("message")
val message: String,
@SerializedName("success")
val success: Boolean = true
)

View File

@ -4,9 +4,9 @@ import com.google.gson.annotations.SerializedName
data class StoreDataResponse( data class StoreDataResponse(
val message: String, val message: String,
val store: Store, val store: Store? = null,
val shipping: List<Shipping>, val shipping: List<Shipping>? = emptyList(),
val payment: List<Payment> val payment: List<Payment> = emptyList()
) )
data class Store( data class Store(
@ -51,5 +51,6 @@ data class Payment(
val id: Int, val id: Int,
@SerializedName("bank_num") val bankNum: String, @SerializedName("bank_num") val bankNum: String,
@SerializedName("bank_name") val bankName: String, @SerializedName("bank_name") val bankName: String,
@SerializedName("qris_image") val qrisImage: String @SerializedName("qris_image") val qrisImage: String?,
@SerializedName("account_name") val accountName: String?
) )

View File

@ -0,0 +1,87 @@
package com.alya.ecommerce_serang.data.api.response.store.topup
import android.util.Log
import com.google.gson.annotations.SerializedName
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
import java.util.TimeZone
data class TopUpResponse(
val message: String,
val topup: List<TopUp>
)
data class TopUp(
val id: Int,
val amount: String,
@SerializedName("store_id") val storeId: Int,
val status: String,
@SerializedName("created_at") val createdAt: String,
val image: String,
@SerializedName("payment_info_id") val paymentInfoId: Int,
@SerializedName("transaction_date") val transactionDate: String,
@SerializedName("payment_method") val paymentMethod: String,
@SerializedName("account_name") val accountName: String?
) {
fun getFormattedDate(): String {
try {
// Try to use transaction_date first, then fall back to created_at
val dateStr = if (transactionDate.isNotEmpty()) transactionDate else createdAt
// Try different formats to parse the date
val parsedDate = parseApiDate(dateStr) ?: return dateStr
// Format with Indonesian locale for month names
val outputFormat = SimpleDateFormat("dd MMM yyyy", Locale("id"))
return outputFormat.format(parsedDate)
} catch (e: Exception) {
Log.e("TopUp", "Error formatting date: ${e.message}")
return createdAt
}
}
private fun parseApiDate(dateStr: String): Date? {
if (dateStr.isEmpty()) return null
// List of possible date formats to try
val formats = listOf(
"yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", // Standard ISO with milliseconds
"yyyy-MM-dd'T'HH:mm:ss'Z'", // ISO without milliseconds
"yyyy-MM-dd'T'HH:mm:ss.SSSZ", // ISO with timezone offset
"yyyy-MM-dd'T'HH:mm:ssZ", // ISO with timezone offset, no milliseconds
"yyyy-MM-dd", // Just the date part
"dd-MM-yyyy" // Alternative date format
)
for (format in formats) {
try {
val sdf = SimpleDateFormat(format, Locale.US)
sdf.timeZone = TimeZone.getTimeZone("UTC") // Assuming API dates are in UTC
return sdf.parse(dateStr)
} catch (e: Exception) {
// Try next format
continue
}
}
// If all formats fail, just try to extract the date part and parse it
try {
val datePart = dateStr.split("T").firstOrNull() ?: return null
val simpleDateFormat = SimpleDateFormat("yyyy-MM-dd", Locale.US)
return simpleDateFormat.parse(datePart)
} catch (e: Exception) {
Log.e("TopUp", "Failed to parse date: $dateStr", e)
return null
}
}
fun getFormattedAmount(): String {
return try {
val amountValue = amount.toDouble()
String.format("+ Rp%,.0f", amountValue)
} catch (e: Exception) {
"Rp$amount"
}
}
}

View File

@ -2,6 +2,7 @@ package com.alya.ecommerce_serang.data.api.retrofit
import com.alya.ecommerce_serang.data.api.dto.AddEvidenceRequest import com.alya.ecommerce_serang.data.api.dto.AddEvidenceRequest
import com.alya.ecommerce_serang.data.api.dto.AddPaymentInfoResponse
import com.alya.ecommerce_serang.data.api.dto.CartItem import com.alya.ecommerce_serang.data.api.dto.CartItem
import com.alya.ecommerce_serang.data.api.dto.CityResponse import com.alya.ecommerce_serang.data.api.dto.CityResponse
import com.alya.ecommerce_serang.data.api.dto.CompletedOrderRequest import com.alya.ecommerce_serang.data.api.dto.CompletedOrderRequest
@ -14,6 +15,7 @@ import com.alya.ecommerce_serang.data.api.dto.OtpRequest
import com.alya.ecommerce_serang.data.api.dto.ProvinceResponse import com.alya.ecommerce_serang.data.api.dto.ProvinceResponse
import com.alya.ecommerce_serang.data.api.dto.RegisterRequest import com.alya.ecommerce_serang.data.api.dto.RegisterRequest
import com.alya.ecommerce_serang.data.api.dto.SearchRequest import com.alya.ecommerce_serang.data.api.dto.SearchRequest
import com.alya.ecommerce_serang.data.api.dto.ShippingServiceRequest
import com.alya.ecommerce_serang.data.api.dto.StoreAddressResponse import com.alya.ecommerce_serang.data.api.dto.StoreAddressResponse
import com.alya.ecommerce_serang.data.api.dto.UpdateCart import com.alya.ecommerce_serang.data.api.dto.UpdateCart
import com.alya.ecommerce_serang.data.api.dto.UpdateChatRequest import com.alya.ecommerce_serang.data.api.dto.UpdateChatRequest
@ -51,8 +53,10 @@ import com.alya.ecommerce_serang.data.api.response.store.product.CreateProductRe
import com.alya.ecommerce_serang.data.api.response.store.product.DeleteProductResponse import com.alya.ecommerce_serang.data.api.response.store.product.DeleteProductResponse
import com.alya.ecommerce_serang.data.api.response.store.product.UpdateProductResponse import com.alya.ecommerce_serang.data.api.response.store.product.UpdateProductResponse
import com.alya.ecommerce_serang.data.api.response.store.product.ViewStoreProductsResponse import com.alya.ecommerce_serang.data.api.response.store.product.ViewStoreProductsResponse
import com.alya.ecommerce_serang.data.api.response.store.profile.GenericResponse
import com.alya.ecommerce_serang.data.api.response.store.profile.StoreDataResponse import com.alya.ecommerce_serang.data.api.response.store.profile.StoreDataResponse
import com.alya.ecommerce_serang.data.api.response.store.topup.BalanceTopUpResponse import com.alya.ecommerce_serang.data.api.response.store.topup.BalanceTopUpResponse
import com.alya.ecommerce_serang.data.api.response.store.topup.TopUpResponse
import okhttp3.MultipartBody import okhttp3.MultipartBody
import okhttp3.RequestBody import okhttp3.RequestBody
import retrofit2.Call import retrofit2.Call
@ -152,7 +156,14 @@ interface ApiService {
@GET("mystore") @GET("mystore")
suspend fun getStore(): Response<StoreResponse> suspend fun getStore(): Response<StoreResponse>
@GET("mystore")
suspend fun getStoreData(): Response<StoreDataResponse> suspend fun getStoreData(): Response<StoreDataResponse>
@GET("mystore")
suspend fun getMyStoreData(): Response<com.alya.ecommerce_serang.data.api.response.store.StoreResponse>
@GET("mystore")
suspend fun getStoreAddress(): Response<StoreAddressResponse> suspend fun getStoreAddress(): Response<StoreAddressResponse>
@GET("mystore/product") // Replace with actual endpoint @GET("mystore/product") // Replace with actual endpoint
@ -242,6 +253,12 @@ interface ApiService {
@Part complaintimg: MultipartBody.Part @Part complaintimg: MultipartBody.Part
): Response<ComplaintResponse> ): Response<ComplaintResponse>
@GET("store/topup")
suspend fun getTopUpHistory(): Response<TopUpResponse>
@GET("store/topup")
suspend fun getFilteredTopUpHistory(@Query("date") date: String): Response<TopUpResponse>
@Multipart @Multipart
@POST("store/createtopup") @POST("store/createtopup")
suspend fun addBalanceTopUp( suspend fun addBalanceTopUp(
@ -253,11 +270,6 @@ interface ApiService {
@Part("bank_num") bankNum: RequestBody @Part("bank_num") bankNum: RequestBody
): Response<BalanceTopUpResponse> ): Response<BalanceTopUpResponse>
@PUT("mystore/edit")
suspend fun updateStoreProfile(
@Body requestBody: okhttp3.RequestBody
): Response<StoreDataResponse>
@Multipart @Multipart
@PUT("mystore/edit") @PUT("mystore/edit")
suspend fun updateStoreProfileMultipart( suspend fun updateStoreProfileMultipart(
@ -277,6 +289,40 @@ interface ApiService {
@Part storeimg: MultipartBody.Part? @Part storeimg: MultipartBody.Part?
): Response<StoreDataResponse> ): Response<StoreDataResponse>
@Multipart
@POST("mystore/payment/add")
suspend fun addPaymentInfo(
@Part("bank_name") bankName: RequestBody,
@Part("bank_num") bankNum: RequestBody,
@Part("account_name") accountName: RequestBody,
@Part qris: MultipartBody.Part?
): Response<GenericResponse>
@Multipart
@POST("mystore/payment/add")
suspend fun addPaymentInfoDirect(
@Part("bank_name") bankName: RequestBody,
@Part("bank_num") bankNum: RequestBody,
@Part("account_name") accountName: RequestBody,
@Part qris: MultipartBody.Part?
): Response<AddPaymentInfoResponse>
@DELETE("mystore/payment/delete/{id}")
suspend fun deletePaymentInfo(
@Path("id") paymentMethodId: Int
): Response<GenericResponse>
// Shipping Service API endpoints
@POST("mystore/shipping/add")
suspend fun addShippingService(
@Body request: ShippingServiceRequest
): Response<GenericResponse>
@POST("mystore/shipping/delete")
suspend fun deleteShippingService(
@Body request: ShippingServiceRequest
): Response<GenericResponse>
@GET("provinces") @GET("provinces")
suspend fun getProvinces(): Response<ProvinceResponse> suspend fun getProvinces(): Response<ProvinceResponse>

View File

@ -0,0 +1,168 @@
package com.alya.ecommerce_serang.data.repository
import android.util.Log
import com.alya.ecommerce_serang.data.api.dto.PaymentInfo
import com.alya.ecommerce_serang.data.api.retrofit.ApiService
import com.google.gson.Gson
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.MultipartBody
import okhttp3.RequestBody.Companion.asRequestBody
import okhttp3.RequestBody.Companion.toRequestBody
import java.io.File
class PaymentInfoRepository(private val apiService: ApiService) {
private val TAG = "PaymentInfoRepository"
private val gson = Gson()
suspend fun getPaymentInfo(): List<PaymentInfo> = withContext(Dispatchers.IO) {
try {
Log.d(TAG, "Getting payment info")
val response = apiService.getStoreData()
if (response.isSuccessful) {
val result = response.body()
// Log the raw response
Log.d(TAG, "API Response body: ${gson.toJson(result)}")
// Check if payment list is null or empty
val paymentList = result?.payment
if (paymentList.isNullOrEmpty()) {
Log.d(TAG, "Payment list is null or empty in response")
return@withContext emptyList<PaymentInfo>()
}
Log.d(TAG, "Raw payment list: ${gson.toJson(paymentList)}")
Log.d(TAG, "Get payment methods success: ${paymentList.size} methods")
// Convert Payment objects to PaymentMethod objects
val convertedPayments = paymentList.map { payment ->
PaymentInfo(
id = payment.id,
bankNum = payment.bankNum,
bankName = payment.bankName,
qrisImage = payment.qrisImage,
accountName = payment.accountName
)
}
return@withContext convertedPayments
} else {
val errorBody = response.errorBody()?.string() ?: "Unknown error"
Log.e(TAG, "Get payment methods error: $errorBody, HTTP code: ${response.code()}")
throw Exception("Failed to get payment methods: ${response.message()}, code: ${response.code()}, error: $errorBody")
}
} catch (e: Exception) {
Log.e(TAG, "Exception getting payment methods", e)
throw e
}
}
suspend fun addPaymentMethod(
bankName: String,
bankNumber: String,
accountName: String,
qrisImageFile: File?
): Boolean = withContext(Dispatchers.IO) {
try {
Log.d(TAG, "===== START PAYMENT METHOD ADD =====")
Log.d(TAG, "Adding payment method with parameters:")
Log.d(TAG, "Bank Name: $bankName")
Log.d(TAG, "Bank Number: $bankNumber")
Log.d(TAG, "Account Name: $accountName")
Log.d(TAG, "QRIS Image File: ${qrisImageFile?.absolutePath}")
Log.d(TAG, "QRIS File exists: ${qrisImageFile?.exists()}")
Log.d(TAG, "QRIS File size: ${qrisImageFile?.length() ?: 0} bytes")
// Create text RequestBody objects with explicit content type
val contentType = "text/plain".toMediaTypeOrNull()
val bankNamePart = bankName.toRequestBody(contentType)
val bankNumPart = bankNumber.toRequestBody(contentType)
val accountNamePart = accountName.toRequestBody(contentType)
// Log request parameters details
Log.d(TAG, "Request parameters details:")
Log.d(TAG, "bank_name RequestBody created with value: $bankName")
Log.d(TAG, "bank_num RequestBody created with value: $bankNumber")
Log.d(TAG, "account_name RequestBody created with value: $accountName")
// Create image part if file exists
var qrisPart: MultipartBody.Part? = null
if (qrisImageFile != null && qrisImageFile.exists() && qrisImageFile.length() > 0) {
// Use image/* content type to ensure proper MIME type for images
val imageContentType = "image/jpeg".toMediaTypeOrNull()
val requestFile = qrisImageFile.asRequestBody(imageContentType)
qrisPart = MultipartBody.Part.createFormData("qris", qrisImageFile.name, requestFile)
Log.d(TAG, "qris MultipartBody.Part created with filename: ${qrisImageFile.name}")
Log.d(TAG, "qris file size: ${qrisImageFile.length()} bytes")
} else {
Log.d(TAG, "No qris image part will be included in the request")
}
// Example input data being sent to API
Log.d(TAG, "Example input data sent to API endpoint http://192.168.100.31:3000/mystore/payment/add:")
Log.d(TAG, "Method: POST")
Log.d(TAG, "Content-Type: multipart/form-data")
Log.d(TAG, "Form fields:")
Log.d(TAG, "- bank_name: $bankName")
Log.d(TAG, "- bank_num: $bankNumber")
Log.d(TAG, "- account_name: $accountName")
if (qrisPart != null) {
Log.d(TAG, "- qris: [binary image file: ${qrisImageFile?.name}, size: ${qrisImageFile?.length()} bytes]")
}
try {
// Use the direct API method call
val response = apiService.addPaymentInfoDirect(
bankName = bankNamePart,
bankNum = bankNumPart,
accountName = accountNamePart,
qris = qrisPart
)
if (response.isSuccessful) {
val result = response.body()
Log.d(TAG, "API response: ${gson.toJson(result)}")
Log.d(TAG, "Add payment method success")
Log.d(TAG, "===== END PAYMENT METHOD ADD - SUCCESS =====")
return@withContext true
} else {
val errorBody = response.errorBody()?.string() ?: "Unknown error"
Log.e(TAG, "Add payment method error: $errorBody, HTTP code: ${response.code()}")
Log.e(TAG, "===== END PAYMENT METHOD ADD - FAILURE =====")
throw Exception("Failed to add payment method: ${response.message()}, code: ${response.code()}, error: $errorBody")
}
} catch (e: Exception) {
Log.e(TAG, "API call exception", e)
throw e
}
} catch (e: Exception) {
Log.e(TAG, "Exception adding payment method", e)
Log.e(TAG, "===== END PAYMENT METHOD ADD - EXCEPTION =====")
throw e
}
}
suspend fun deletePaymentMethod(paymentMethodId: Int): Boolean = withContext(Dispatchers.IO) {
try {
Log.d(TAG, "Deleting payment method with ID: $paymentMethodId")
val response = apiService.deletePaymentInfo(paymentMethodId)
if (response.isSuccessful) {
Log.d(TAG, "Delete payment method success: ${response.body()?.message}")
return@withContext true
} else {
val errorBody = response.errorBody()?.string() ?: "Unknown error"
Log.e(TAG, "Delete payment method error: $errorBody, HTTP code: ${response.code()}")
throw Exception("Failed to delete payment method: ${response.message()}, code: ${response.code()}, error: $errorBody")
}
} catch (e: Exception) {
Log.e(TAG, "Exception deleting payment method", e)
throw e
}
}
}

View File

@ -0,0 +1,78 @@
package com.alya.ecommerce_serang.data.repository
import android.util.Log
import com.alya.ecommerce_serang.data.api.dto.ShippingServiceRequest
import com.alya.ecommerce_serang.data.api.retrofit.ApiService
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
class ShippingServiceRepository(private val apiService: ApiService) {
private val TAG = "ShippingServiceRepo"
suspend fun getAvailableCouriers(): List<String> = withContext(Dispatchers.IO) {
try {
Log.d(TAG, "Getting available shipping services")
val response = apiService.getStoreData()
if (response.isSuccessful) {
val result = response.body()
val shippingList = result?.shipping
val couriers = shippingList?.map { it.courier } ?: emptyList()
Log.d(TAG, "Get shipping services success: ${couriers.size} couriers")
return@withContext couriers
} else {
val errorBody = response.errorBody()?.string() ?: "Unknown error"
Log.e(TAG, "Get shipping services error: $errorBody")
throw Exception("Failed to get shipping services: ${response.message()}")
}
} catch (e: Exception) {
Log.e(TAG, "Exception getting shipping services", e)
throw e
}
}
suspend fun addShippingServices(couriers: List<String>): Boolean = withContext(Dispatchers.IO) {
try {
Log.d(TAG, "Adding shipping services: $couriers")
val request = ShippingServiceRequest(couriers = couriers)
val response = apiService.addShippingService(request)
if (response.isSuccessful) {
Log.d(TAG, "Add shipping services success: ${response.body()?.message}")
return@withContext true
} else {
val errorBody = response.errorBody()?.string() ?: "Unknown error"
Log.e(TAG, "Add shipping services error: $errorBody")
throw Exception("Failed to add shipping services: ${response.message()}")
}
} catch (e: Exception) {
Log.e(TAG, "Exception adding shipping services", e)
throw e
}
}
suspend fun deleteShippingServices(couriers: List<String>): Boolean = withContext(Dispatchers.IO) {
try {
Log.d(TAG, "Deleting shipping services: $couriers")
val request = ShippingServiceRequest(couriers = couriers)
val response = apiService.deleteShippingService(request)
if (response.isSuccessful) {
Log.d(TAG, "Delete shipping services success: ${response.body()?.message}")
return@withContext true
} else {
val errorBody = response.errorBody()?.string() ?: "Unknown error"
Log.e(TAG, "Delete shipping services error: $errorBody")
throw Exception("Failed to delete shipping services: ${response.message()}")
}
} catch (e: Exception) {
Log.e(TAG, "Exception deleting shipping services", e)
throw e
}
}
}

View File

@ -161,7 +161,7 @@ class CheckoutActivity : AppCompatActivity() {
viewModel.setPaymentMethod(1) viewModel.setPaymentMethod(1)
} }
binding.rvPaymentMethods.apply { binding.rvPaymentInfo.apply {
layoutManager = LinearLayoutManager(this@CheckoutActivity) layoutManager = LinearLayoutManager(this@CheckoutActivity)
adapter = paymentAdapter adapter = paymentAdapter
} }
@ -187,7 +187,7 @@ class CheckoutActivity : AppCompatActivity() {
} }
} }
binding.rvPaymentMethods.apply { binding.rvPaymentInfo.apply {
layoutManager = LinearLayoutManager(this@CheckoutActivity) layoutManager = LinearLayoutManager(this@CheckoutActivity)
adapter = testAdapter adapter = testAdapter
} }

View File

@ -2,10 +2,12 @@ package com.alya.ecommerce_serang.ui.profile.mystore
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.util.Log
import android.widget.Toast import android.widget.Toast
import androidx.activity.enableEdgeToEdge import androidx.activity.enableEdgeToEdge
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import com.alya.ecommerce_serang.R
import com.alya.ecommerce_serang.data.api.dto.Store import com.alya.ecommerce_serang.data.api.dto.Store
import com.alya.ecommerce_serang.data.api.retrofit.ApiConfig import com.alya.ecommerce_serang.data.api.retrofit.ApiConfig
import com.alya.ecommerce_serang.data.api.retrofit.ApiService import com.alya.ecommerce_serang.data.api.retrofit.ApiService
@ -16,6 +18,7 @@ import com.alya.ecommerce_serang.ui.profile.mystore.balance.BalanceActivity
import com.alya.ecommerce_serang.ui.profile.mystore.product.ProductActivity import com.alya.ecommerce_serang.ui.profile.mystore.product.ProductActivity
import com.alya.ecommerce_serang.ui.profile.mystore.profile.DetailStoreProfileActivity import com.alya.ecommerce_serang.ui.profile.mystore.profile.DetailStoreProfileActivity
import com.alya.ecommerce_serang.ui.profile.mystore.review.ReviewFragment import com.alya.ecommerce_serang.ui.profile.mystore.review.ReviewFragment
import com.alya.ecommerce_serang.ui.profile.mystore.sells.SellsActivity
import com.alya.ecommerce_serang.ui.profile.mystore.sells.SellsListFragment import com.alya.ecommerce_serang.ui.profile.mystore.sells.SellsListFragment
import com.alya.ecommerce_serang.utils.BaseViewModelFactory import com.alya.ecommerce_serang.utils.BaseViewModelFactory
import com.alya.ecommerce_serang.utils.SessionManager import com.alya.ecommerce_serang.utils.SessionManager
@ -64,10 +67,17 @@ class MyStoreActivity : AppCompatActivity() {
binding.tvStoreName.text = store.storeName binding.tvStoreName.text = store.storeName
binding.tvStoreType.text = store.storeType binding.tvStoreType.text = store.storeType
store.storeImage.let { if (store.storeImage != null && store.storeImage.toString().isNotEmpty() && store.storeImage.toString() != "null") {
val imageUrl = "http://192.168.100.156:3000${store.storeImage}"
Log.d("MyStoreActivity", "Loading store image from: $imageUrl")
Glide.with(this) Glide.with(this)
.load(it) .load(imageUrl)
.placeholder(R.drawable.placeholder_image)
.error(R.drawable.placeholder_image)
.into(binding.ivProfile) .into(binding.ivProfile)
} else {
Log.d("MyStoreActivity", "No store image available")
} }
} }
@ -81,19 +91,26 @@ class MyStoreActivity : AppCompatActivity() {
} }
binding.tvHistory.setOnClickListener { binding.tvHistory.setOnClickListener {
navigateToSellsFragment("all") val intent = Intent(this, SellsActivity::class.java)
startActivity(intent)
} }
binding.layoutPerluTagihan.setOnClickListener { binding.layoutPerluTagihan.setOnClickListener {
navigateToSellsFragment("pending") val intent = Intent(this, SellsActivity::class.java)
startActivity(intent)
//navigateToSellsFragment("pending")
} }
binding.layoutPembayaran.setOnClickListener { binding.layoutPembayaran.setOnClickListener {
navigateToSellsFragment("paid") val intent = Intent(this, SellsActivity::class.java)
startActivity(intent)
//navigateToSellsFragment("paid")
} }
binding.layoutPerluDikirim.setOnClickListener { binding.layoutPerluDikirim.setOnClickListener {
navigateToSellsFragment("processed") val intent = Intent(this, SellsActivity::class.java)
startActivity(intent)
//navigateToSellsFragment("processed")
} }
binding.layoutProductMenu.setOnClickListener { binding.layoutProductMenu.setOnClickListener {
@ -115,11 +132,24 @@ class MyStoreActivity : AppCompatActivity() {
} }
} }
private fun navigateToSellsFragment(status: String) { override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
val sellsFragment = SellsListFragment.newInstance(status) super.onActivityResult(requestCode, resultCode, data)
supportFragmentManager.beginTransaction() if (requestCode == PROFILE_REQUEST_CODE && resultCode == RESULT_OK) {
.replace(android.R.id.content, sellsFragment) // Refresh store data
.addToBackStack(null) viewModel.loadMyStore()
.commit() Toast.makeText(this, "Profil toko berhasil diperbarui", Toast.LENGTH_SHORT).show()
} }
} }
companion object {
private const val PROFILE_REQUEST_CODE = 100
}
// private fun navigateToSellsFragment(status: String) {
// val sellsFragment = SellsListFragment.newInstance(status)
// supportFragmentManager.beginTransaction()
// .replace(android.R.id.content, sellsFragment)
// .addToBackStack(null)
// .commit()
// }
}

View File

@ -1,16 +1,38 @@
package com.alya.ecommerce_serang.ui.profile.mystore.balance package com.alya.ecommerce_serang.ui.profile.mystore.balance
import android.app.DatePickerDialog
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.util.Log
import android.view.View
import android.widget.Toast
import androidx.activity.enableEdgeToEdge import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsCompat
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import com.alya.ecommerce_serang.R import com.alya.ecommerce_serang.R
import com.alya.ecommerce_serang.data.api.response.store.topup.TopUp
import com.alya.ecommerce_serang.data.api.retrofit.ApiConfig
import com.alya.ecommerce_serang.databinding.ActivityBalanceBinding import com.alya.ecommerce_serang.databinding.ActivityBalanceBinding
import com.alya.ecommerce_serang.utils.SessionManager
import kotlinx.coroutines.launch
import java.text.SimpleDateFormat
import java.util.Calendar
import java.util.Date
import java.util.Locale
import java.util.TimeZone
class BalanceActivity : AppCompatActivity() { class BalanceActivity : AppCompatActivity() {
private lateinit var binding: ActivityBalanceBinding private lateinit var binding: ActivityBalanceBinding
private lateinit var topUpAdapter: BalanceTransactionAdapter
private lateinit var sessionManager: SessionManager
private val calendar = Calendar.getInstance()
private var selectedDate: String? = null
private var allTopUps: List<TopUp> = emptyList()
private val TAG = "BalanceActivity"
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -24,13 +46,369 @@ class BalanceActivity : AppCompatActivity() {
insets insets
} }
// Initialize session manager
sessionManager = SessionManager(this)
// Setup header
val headerTitle = binding.header.headerTitle
headerTitle.text = "Saldo"
val backButton = binding.header.headerLeftIcon
backButton.setOnClickListener {
finish()
}
// Setup RecyclerView
setupRecyclerView()
// Setup DatePicker
setupDatePicker()
// Add clear filter button
setupClearFilter()
// Fetch data
fetchBalance()
fetchTopUpHistory()
// Setup listeners
setupListeners() setupListeners()
} }
private fun setupRecyclerView() {
topUpAdapter = BalanceTransactionAdapter()
binding.rvBalanceTransaction.apply {
layoutManager = LinearLayoutManager(this@BalanceActivity)
adapter = topUpAdapter
}
}
private fun setupDatePicker() {
val dateSetListener = DatePickerDialog.OnDateSetListener { _, year, month, dayOfMonth ->
calendar.set(Calendar.YEAR, year)
calendar.set(Calendar.MONTH, month)
calendar.set(Calendar.DAY_OF_MONTH, dayOfMonth)
updateDateInView()
// Store selected date for filtering
selectedDate = SimpleDateFormat("yyyy-MM-dd", Locale.US).format(calendar.time)
// Show debugging information
Log.d(TAG, "Selected date: $selectedDate")
// Display all top-up dates for debugging
allTopUps.forEach { topUp ->
Log.d(TAG, "Top-up ID: ${topUp.id}, transaction_date: ${topUp.transactionDate}, created_at: ${topUp.createdAt}")
}
// Apply filter
filterTopUpsByDate(selectedDate)
// Show clear filter button
binding.btnClearFilter.visibility = View.VISIBLE
}
binding.edtTglTransaksi.setOnClickListener {
showDatePicker(dateSetListener)
}
binding.imgDatePicker.setOnClickListener {
showDatePicker(dateSetListener)
}
binding.iconDatePicker.setOnClickListener {
showDatePicker(dateSetListener)
}
}
private fun setupClearFilter() {
binding.btnClearFilter.setOnClickListener {
// Clear date selection
binding.edtTglTransaksi.text = null
selectedDate = null
// Reset to show all topups
if (allTopUps.isNotEmpty()) {
updateTopUpList(allTopUps)
} else {
fetchTopUpHistory()
}
// Hide clear button
binding.btnClearFilter.visibility = View.GONE
}
}
private fun showDatePicker(dateSetListener: DatePickerDialog.OnDateSetListener) {
DatePickerDialog(
this,
dateSetListener,
calendar.get(Calendar.YEAR),
calendar.get(Calendar.MONTH),
calendar.get(Calendar.DAY_OF_MONTH)
).show()
}
private fun updateDateInView() {
val format = "dd MMMM yyyy"
val sdf = SimpleDateFormat(format, Locale("id"))
binding.edtTglTransaksi.setText(sdf.format(calendar.time))
}
private fun setupListeners() { private fun setupListeners() {
binding.btnTopUp.setOnClickListener { binding.btnTopUp.setOnClickListener {
val intent = Intent(this, BalanceTopUpActivity::class.java) val intent = Intent(this, BalanceTopUpActivity::class.java)
startActivity(intent) startActivityForResult(intent, TOP_UP_REQUEST_CODE)
}
}
private fun fetchBalance() {
showLoading(true)
lifecycleScope.launch {
try {
val response = ApiConfig.getApiService(sessionManager).getMyStoreData()
if (response.isSuccessful && response.body() != null) {
val storeData = response.body()!!
val balance = storeData.store.balance
// Format the balance
try {
val balanceValue = balance.toDouble()
binding.tvBalance.text = String.format("Rp%,.0f", balanceValue)
} catch (e: Exception) {
binding.tvBalance.text = "Rp$balance"
}
} else {
Toast.makeText(
this@BalanceActivity,
"Gagal memuat data saldo: ${response.message()}",
Toast.LENGTH_SHORT
).show()
}
} catch (e: Exception) {
Log.e(TAG, "Error fetching balance", e)
Toast.makeText(
this@BalanceActivity,
"Error: ${e.message}",
Toast.LENGTH_SHORT
).show()
} finally {
showLoading(false)
} }
} }
} }
private fun fetchTopUpHistory() {
showLoading(true)
lifecycleScope.launch {
try {
val response = ApiConfig.getApiService(sessionManager).getTopUpHistory()
if (response.isSuccessful && response.body() != null) {
val topUpData = response.body()!!
allTopUps = topUpData.topup
// Apply date filter if selected
if (selectedDate != null) {
filterTopUpsByDate(selectedDate)
} else {
updateTopUpList(allTopUps)
}
} else {
Toast.makeText(
this@BalanceActivity,
"Gagal memuat riwayat isi ulang: ${response.message()}",
Toast.LENGTH_SHORT
).show()
}
} catch (e: Exception) {
Log.e(TAG, "Error fetching top-up history", e)
Toast.makeText(
this@BalanceActivity,
"Error: ${e.message}",
Toast.LENGTH_SHORT
).show()
} finally {
showLoading(false)
}
}
}
private fun filterTopUpsByDate(dateStr: String?) {
if (dateStr == null || allTopUps.isEmpty()) {
return
}
try {
Log.d(TAG, "Filtering by date: $dateStr")
// Parse the selected date - set to start of day
val cal1 = Calendar.getInstance()
cal1.time = parseSelectedDate(dateStr)
cal1.set(Calendar.HOUR_OF_DAY, 0)
cal1.set(Calendar.MINUTE, 0)
cal1.set(Calendar.SECOND, 0)
cal1.set(Calendar.MILLISECOND, 0)
// Extract the date components we care about (year, month, day)
val selectedYear = cal1.get(Calendar.YEAR)
val selectedMonth = cal1.get(Calendar.MONTH)
val selectedDay = cal1.get(Calendar.DAY_OF_MONTH)
Log.d(TAG, "Selected date components: Year=$selectedYear, Month=$selectedMonth, Day=$selectedDay")
// Format for comparing with API dates
val filtered = allTopUps.filter { topUp ->
try {
// Debug logging
Log.d(TAG, "Examining top-up: ID=${topUp.id}")
Log.d(TAG, " - created_at=${topUp.createdAt}")
Log.d(TAG, " - transaction_date=${topUp.transactionDate}")
// Try both dates for more flexibility
val cal2 = Calendar.getInstance()
var matched = false
// Try transaction_date first
if (topUp.transactionDate.isNotEmpty()) {
val transactionDate = parseApiDate(topUp.transactionDate)
if (transactionDate != null) {
cal2.time = transactionDate
val transYear = cal2.get(Calendar.YEAR)
val transMonth = cal2.get(Calendar.MONTH)
val transDay = cal2.get(Calendar.DAY_OF_MONTH)
Log.d(TAG, " - Transaction date components: Year=$transYear, Month=$transMonth, Day=$transDay")
if (transYear == selectedYear &&
transMonth == selectedMonth &&
transDay == selectedDay) {
Log.d(TAG, " - MATCH on transaction_date")
matched = true
}
}
}
// If no match yet, try created_at
if (!matched && topUp.createdAt.isNotEmpty()) {
val createdAtDate = parseApiDate(topUp.createdAt)
if (createdAtDate != null) {
cal2.time = createdAtDate
val createdYear = cal2.get(Calendar.YEAR)
val createdMonth = cal2.get(Calendar.MONTH)
val createdDay = cal2.get(Calendar.DAY_OF_MONTH)
Log.d(TAG, " - Created date components: Year=$createdYear, Month=$createdMonth, Day=$createdDay")
if (createdYear == selectedYear &&
createdMonth == selectedMonth &&
createdDay == selectedDay) {
Log.d(TAG, " - MATCH on created_at")
matched = true
}
}
}
// Final result
Log.d(TAG, " - Match result: $matched")
matched
} catch (e: Exception) {
Log.e(TAG, "Date parsing error for top-up ${topUp.id}: ${e.message}", e)
false
}
}
Log.d(TAG, "Found ${filtered.size} matching records out of ${allTopUps.size}")
updateTopUpList(filtered)
} catch (e: Exception) {
Log.e(TAG, "Error filtering by date", e)
Toast.makeText(
this@BalanceActivity,
"Error filtering data: ${e.message}",
Toast.LENGTH_SHORT
).show()
}
}
private fun parseSelectedDate(dateStr: String): Date {
// Parse the user-selected date
try {
val dateFormat = SimpleDateFormat("yyyy-MM-dd", Locale.US)
return dateFormat.parse(dateStr) ?: Date()
} catch (e: Exception) {
Log.e(TAG, "Error parsing selected date: $dateStr", e)
return Date()
}
}
/**
* Parse ISO 8601 date format from API (handles multiple formats)
*/
private fun parseApiDate(dateStr: String): Date? {
if (dateStr.isEmpty()) return null
// List of possible date formats to try
val formats = listOf(
"yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", // Standard ISO with milliseconds
"yyyy-MM-dd'T'HH:mm:ss'Z'", // ISO without milliseconds
"yyyy-MM-dd'T'HH:mm:ss.SSSZ", // ISO with timezone offset
"yyyy-MM-dd'T'HH:mm:ssZ", // ISO with timezone offset, no milliseconds
"yyyy-MM-dd", // Just the date part
"dd-MM-yyyy" // Alternative date format
)
for (format in formats) {
try {
val sdf = SimpleDateFormat(format, Locale.US)
sdf.timeZone = TimeZone.getTimeZone("UTC") // Assuming API dates are in UTC
return sdf.parse(dateStr)
} catch (e: Exception) {
// Try next format
continue
}
}
// If all formats fail, just try to extract the date part and parse it
try {
val datePart = dateStr.split("T").firstOrNull() ?: return null
val simpleDateFormat = SimpleDateFormat("yyyy-MM-dd", Locale.US)
return simpleDateFormat.parse(datePart)
} catch (e: Exception) {
Log.e(TAG, "Failed to parse date: $dateStr", e)
return null
}
}
private fun updateTopUpList(topUps: List<TopUp>) {
if (topUps.isEmpty()) {
binding.rvBalanceTransaction.visibility = View.GONE
binding.tvEmptyState.visibility = View.VISIBLE
} else {
binding.rvBalanceTransaction.visibility = View.VISIBLE
binding.tvEmptyState.visibility = View.GONE
topUpAdapter.submitList(topUps)
}
}
private fun showLoading(isLoading: Boolean) {
if (isLoading) {
binding.progressBar.visibility = View.VISIBLE
} else {
binding.progressBar.visibility = View.GONE
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == TOP_UP_REQUEST_CODE && resultCode == RESULT_OK) {
// Refresh balance and top-up history after successful top-up
fetchBalance()
fetchTopUpHistory()
Toast.makeText(this, "Top up berhasil", Toast.LENGTH_SHORT).show()
}
}
companion object {
private const val TOP_UP_REQUEST_CODE = 101
}
}

View File

@ -80,7 +80,7 @@ class BalanceTopUpActivity : AppCompatActivity() {
// Setup back button // Setup back button
val backButton = findViewById<ImageView>(R.id.header_left_icon) val backButton = findViewById<ImageView>(R.id.header_left_icon)
backButton.setOnClickListener { backButton.setOnClickListener {
finish() onBackPressedDispatcher.onBackPressed()
} }
// Setup photo selection // Setup photo selection

View File

@ -1,7 +1,90 @@
package com.alya.ecommerce_serang.ui.profile.mystore.balance package com.alya.ecommerce_serang.ui.profile.mystore.balance
/* class BalanceTransactionAdapter(private val balanceTransactionList: List<BalanceTransaction>) : import android.view.LayoutInflater
RecyclerView.Adapter<BalanceTransactionAdapter.TransactionViewHolder>() { import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import com.alya.ecommerce_serang.R
import com.alya.ecommerce_serang.data.api.response.store.topup.TopUp
import com.alya.ecommerce_serang.ui.profile.mystore.balance.BalanceTransactionAdapter.BalanceTransactionViewHolder
class BalanceTransactionAdapter : ListAdapter<TopUp, BalanceTransactionViewHolder>(DIFF_CALLBACK) {
}*/ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BalanceTransactionViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.item_balance_transaction, parent, false)
return BalanceTransactionViewHolder(view)
}
override fun onBindViewHolder(holder: BalanceTransactionViewHolder, position: Int) {
holder.bind(getItem(position))
}
inner class BalanceTransactionViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
private val tvDate: TextView = itemView.findViewById(R.id.tv_balance_trans_date)
private val tvTitle: TextView = itemView.findViewById(R.id.tv_balance_trans_title)
private val tvDesc: TextView = itemView.findViewById(R.id.tv_balance_trans_desc)
private val tvAmount: TextView = itemView.findViewById(R.id.tv_balance_trans_amount)
private val ivIcon: ImageView = itemView.findViewById(R.id.iv_balance_trans_icon)
private val divider: View = itemView.findViewById(R.id.divider_balance_trans)
fun bind(topUp: TopUp) {
// Set date
tvDate.text = topUp.getFormattedDate()
// Set title
tvTitle.text = "Isi Ulang Saldo"
// Set description - payment details
val paymentMethod = topUp.paymentMethod
val accountName = topUp.accountName ?: ""
val desc = if (accountName.isNotEmpty()) {
"Isi ulang dari $paymentMethod $accountName"
} else {
"Isi ulang dari $paymentMethod"
}
tvDesc.text = desc
// Set amount
tvAmount.text = topUp.getFormattedAmount()
// Set color based on status
val context = itemView.context
val activeColor = ContextCompat.getColor(context, R.color.blue_500)
val pendingColor = ContextCompat.getColor(context, R.color.black_500)
when (topUp.status.lowercase()) {
"approved" -> {
tvAmount.setTextColor(activeColor)
ivIcon.setImageResource(R.drawable.ic_graph_arrow_increase)
}
"pending" -> {
tvAmount.setTextColor(pendingColor)
}
else -> {
tvAmount.setTextColor(activeColor)
}
}
// Show divider for all items except the last one
divider.visibility = if (bindingAdapterPosition == itemCount - 1) View.GONE else View.VISIBLE
}
}
companion object {
private val DIFF_CALLBACK = object : DiffUtil.ItemCallback<TopUp>() {
override fun areItemsTheSame(oldItem: TopUp, newItem: TopUp): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: TopUp, newItem: TopUp): Boolean {
return oldItem == newItem
}
}
}
}

View File

@ -15,6 +15,8 @@ import com.alya.ecommerce_serang.data.api.retrofit.ApiService
import com.alya.ecommerce_serang.data.repository.MyStoreRepository import com.alya.ecommerce_serang.data.repository.MyStoreRepository
import com.alya.ecommerce_serang.databinding.ActivityDetailStoreProfileBinding import com.alya.ecommerce_serang.databinding.ActivityDetailStoreProfileBinding
import com.alya.ecommerce_serang.ui.profile.mystore.profile.address.DetailStoreAddressActivity import com.alya.ecommerce_serang.ui.profile.mystore.profile.address.DetailStoreAddressActivity
import com.alya.ecommerce_serang.ui.profile.mystore.profile.payment_info.PaymentInfoActivity
import com.alya.ecommerce_serang.ui.profile.mystore.profile.shipping_service.ShippingServiceActivity
import com.alya.ecommerce_serang.utils.viewmodel.MyStoreViewModel import com.alya.ecommerce_serang.utils.viewmodel.MyStoreViewModel
import com.alya.ecommerce_serang.utils.BaseViewModelFactory import com.alya.ecommerce_serang.utils.BaseViewModelFactory
import com.alya.ecommerce_serang.utils.SessionManager import com.alya.ecommerce_serang.utils.SessionManager
@ -59,7 +61,19 @@ class DetailStoreProfileActivity : AppCompatActivity() {
binding.layoutAddress.setOnClickListener { binding.layoutAddress.setOnClickListener {
val intent = Intent(this, DetailStoreAddressActivity::class.java) val intent = Intent(this, DetailStoreAddressActivity::class.java)
startActivity(intent) startActivityForResult(intent, ADDRESS_REQUEST_CODE)
}
// Set up payment method layout click listener
binding.layoutPaymentMethod.setOnClickListener {
val intent = Intent(this, PaymentInfoActivity::class.java)
startActivityForResult(intent, PAYMENT_INFO_REQUEST_CODE)
}
// Set up shipping services layout click listener
binding.layoutShipServices.setOnClickListener {
val intent = Intent(this, ShippingServiceActivity::class.java)
startActivityForResult(intent, SHIPPING_SERVICES_REQUEST_CODE)
} }
viewModel.loadMyStore() viewModel.loadMyStore()
@ -87,6 +101,20 @@ class DetailStoreProfileActivity : AppCompatActivity() {
Toast.makeText(this, "Alamat toko berhasil diperbarui", Toast.LENGTH_SHORT).show() Toast.makeText(this, "Alamat toko berhasil diperbarui", Toast.LENGTH_SHORT).show()
viewModel.loadMyStore() viewModel.loadMyStore()
// Pass the result back to parent activity
setResult(Activity.RESULT_OK)
} else if (requestCode == PAYMENT_INFO_REQUEST_CODE && resultCode == Activity.RESULT_OK) {
// Refresh the profile data after payment method update
Toast.makeText(this, "Metode pembayaran berhasil diperbarui", Toast.LENGTH_SHORT).show()
viewModel.loadMyStore()
// Pass the result back to parent activity
setResult(Activity.RESULT_OK)
} else if (requestCode == SHIPPING_SERVICES_REQUEST_CODE && resultCode == Activity.RESULT_OK) {
// Refresh the profile data after shipping services update
Toast.makeText(this, "Layanan pengiriman berhasil diperbarui", Toast.LENGTH_SHORT).show()
viewModel.loadMyStore()
// Pass the result back to parent activity // Pass the result back to parent activity
setResult(Activity.RESULT_OK) setResult(Activity.RESULT_OK)
} }
@ -95,6 +123,8 @@ class DetailStoreProfileActivity : AppCompatActivity() {
companion object { companion object {
private const val EDIT_PROFILE_REQUEST_CODE = 100 private const val EDIT_PROFILE_REQUEST_CODE = 100
private const val ADDRESS_REQUEST_CODE = 101 private const val ADDRESS_REQUEST_CODE = 101
private const val PAYMENT_INFO_REQUEST_CODE = 102
private const val SHIPPING_SERVICES_REQUEST_CODE = 103
} }
private fun updateStoreProfile(store: Store){ private fun updateStoreProfile(store: Store){
@ -105,7 +135,7 @@ class DetailStoreProfileActivity : AppCompatActivity() {
// Update store image if available // Update store image if available
if (store.storeImage != null && store.storeImage.toString().isNotEmpty() && store.storeImage.toString() != "null") { if (store.storeImage != null && store.storeImage.toString().isNotEmpty() && store.storeImage.toString() != "null") {
val imageUrl = "http:/192.168.100.156:3000${store.storeImage}" val imageUrl = "http://192.168.100.156:3000${store.storeImage}"
Log.d("DetailStoreProfile", "Loading image from: $imageUrl") Log.d("DetailStoreProfile", "Loading image from: $imageUrl")
Glide.with(this) Glide.with(this)

View File

@ -1,7 +1,6 @@
package com.alya.ecommerce_serang.ui.profile.mystore.profile.address package com.alya.ecommerce_serang.ui.profile.mystore.profile.address
import android.app.Activity import android.app.Activity
import android.app.AlertDialog
import android.os.Bundle import android.os.Bundle
import android.util.Log import android.util.Log
import android.view.View import android.view.View
@ -9,6 +8,7 @@ import android.widget.AdapterView
import android.widget.ArrayAdapter import android.widget.ArrayAdapter
import android.widget.Toast import android.widget.Toast
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import com.alya.ecommerce_serang.BuildConfig import com.alya.ecommerce_serang.BuildConfig
import com.alya.ecommerce_serang.data.api.dto.City import com.alya.ecommerce_serang.data.api.dto.City
@ -56,7 +56,7 @@ class DetailStoreAddressActivity : AppCompatActivity() {
binding.tvError.visibility = View.GONE binding.tvError.visibility = View.GONE
// Set up header title // Set up header title
binding.header.headerTitle.text = "Alamat Toko" binding.header.headerTitle.text = "Atur Alamat Toko"
// Set up back button // Set up back button
binding.header.headerLeftIcon.setOnClickListener { binding.header.headerLeftIcon.setOnClickListener {
@ -182,6 +182,9 @@ class DetailStoreAddressActivity : AppCompatActivity() {
val lat = if (address.latitude == null || address.latitude.toString() == "NaN") 0.0 else address.latitude val lat = if (address.latitude == null || address.latitude.toString() == "NaN") 0.0 else address.latitude
val lng = if (address.longitude == null || address.longitude.toString() == "NaN") 0.0 else address.longitude val lng = if (address.longitude == null || address.longitude.toString() == "NaN") 0.0 else address.longitude
binding.edtLatitude.setText(lat.toString())
binding.edtLongitude.setText(lng.toString())
// Set selected province ID to trigger city loading // Set selected province ID to trigger city loading
if (address.provinceId.isNotEmpty()) { if (address.provinceId.isNotEmpty()) {
selectedProvinceId = address.provinceId selectedProvinceId = address.provinceId
@ -271,8 +274,8 @@ class DetailStoreAddressActivity : AppCompatActivity() {
val subdistrict = binding.edtSubdistrict.text.toString() val subdistrict = binding.edtSubdistrict.text.toString()
val detail = binding.edtDetailAddress.text.toString() val detail = binding.edtDetailAddress.text.toString()
val postalCode = binding.edtPostalCode.text.toString() val postalCode = binding.edtPostalCode.text.toString()
val latitudeStr = TODO() val latitudeStr = binding.edtLatitude.text.toString()
val longitudeStr = TODO() val longitudeStr = binding.edtLongitude.text.toString()
// Validate required fields // Validate required fields
if (selectedProvinceId == null || binding.spinnerCity.selectedItemPosition <= 0 || if (selectedProvinceId == null || binding.spinnerCity.selectedItemPosition <= 0 ||

View File

@ -1,21 +1,282 @@
package com.alya.ecommerce_serang.ui.profile.mystore.profile.payment_info package com.alya.ecommerce_serang.ui.profile.mystore.profile.payment_info
import android.app.Activity
import android.app.AlertDialog
import android.net.Uri
import android.os.Bundle import android.os.Bundle
import androidx.activity.enableEdgeToEdge import android.util.Log
import android.view.View
import android.widget.Button
import android.widget.EditText
import android.widget.ImageView
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat import androidx.recyclerview.widget.LinearLayoutManager
import androidx.core.view.WindowInsetsCompat
import com.alya.ecommerce_serang.R import com.alya.ecommerce_serang.R
import com.alya.ecommerce_serang.data.api.dto.PaymentInfo
import com.alya.ecommerce_serang.data.api.retrofit.ApiConfig
import com.alya.ecommerce_serang.data.repository.PaymentInfoRepository
import com.alya.ecommerce_serang.databinding.ActivityPaymentInfoBinding
import com.alya.ecommerce_serang.utils.BaseViewModelFactory
import com.alya.ecommerce_serang.utils.SessionManager
import com.alya.ecommerce_serang.utils.UriToFileConverter
import com.alya.ecommerce_serang.utils.viewmodel.PaymentInfoViewModel
import com.google.android.material.snackbar.Snackbar
import java.io.File
class PaymentInfoActivity : AppCompatActivity() { class PaymentInfoActivity : AppCompatActivity() {
private val TAG = "PaymentInfoActivity"
private lateinit var binding: ActivityPaymentInfoBinding
private lateinit var adapter: PaymentInfoAdapter
private lateinit var sessionManager: SessionManager
private var selectedQrisImageUri: Uri? = null
private var selectedQrisImageFile: File? = null
// Store form data between dialog reopenings
private var savedBankName: String = ""
private var savedBankNumber: String = ""
private var savedAccountName: String = ""
private val viewModel: PaymentInfoViewModel by viewModels {
BaseViewModelFactory {
val apiService = ApiConfig.getApiService(sessionManager)
val repository = PaymentInfoRepository(apiService)
PaymentInfoViewModel(repository)
}
}
private val getContent = registerForActivityResult(ActivityResultContracts.GetContent()) { uri: Uri? ->
uri?.let {
try {
Log.d(TAG, "Selected image URI: $uri")
selectedQrisImageUri = it
// Convert URI to File
selectedQrisImageFile = UriToFileConverter.uriToFile(it, this)
if (selectedQrisImageFile == null) {
Log.e(TAG, "Failed to convert URI to file")
showSnackbar("Failed to process image. Please try another image.")
return@let
}
Log.d(TAG, "Converted to file: ${selectedQrisImageFile?.absolutePath}, size: ${selectedQrisImageFile?.length()} bytes")
// Check if file exists and has content
if (!selectedQrisImageFile!!.exists() || selectedQrisImageFile!!.length() == 0L) {
Log.e(TAG, "File doesn't exist or is empty: ${selectedQrisImageFile?.absolutePath}")
showSnackbar("Failed to process image. Please try another image.")
selectedQrisImageFile = null
return@let
}
showAddPaymentDialog(true) // Reopen dialog with selected image
} catch (e: Exception) {
Log.e(TAG, "Error processing selected image", e)
showSnackbar("Error processing image: ${e.message}")
selectedQrisImageUri = null
selectedQrisImageFile = null
}
}
}
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
enableEdgeToEdge() binding = ActivityPaymentInfoBinding.inflate(layoutInflater)
setContentView(R.layout.activity_payment_info) setContentView(binding.root)
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars()) sessionManager = SessionManager(this)
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
insets // Configure header
binding.header.headerTitle.text = "Atur Metode Pembayaran"
binding.header.headerLeftIcon.setOnClickListener {
onBackPressedDispatcher.onBackPressed()
}
setupRecyclerView()
setupObservers()
binding.btnAddPayment.setOnClickListener {
// Clear saved values when opening a new dialog
savedBankName = ""
savedBankNumber = ""
savedAccountName = ""
selectedQrisImageUri = null
selectedQrisImageFile = null
showAddPaymentDialog(false)
}
// Load payment info
viewModel.getPaymentInfo()
}
private fun setupRecyclerView() {
adapter = PaymentInfoAdapter(
onDeleteClick = { paymentMethod ->
showDeleteConfirmationDialog(paymentMethod)
}
)
binding.rvPaymentInfo.layoutManager = LinearLayoutManager(this)
binding.rvPaymentInfo.adapter = adapter
}
private fun setupObservers() {
viewModel.paymentInfos.observe(this) { paymentInfo ->
binding.progressBar.visibility = View.GONE
if (paymentInfo.isEmpty()) {
binding.tvEmptyState.visibility = View.VISIBLE
binding.rvPaymentInfo.visibility = View.GONE
} else {
binding.tvEmptyState.visibility = View.GONE
binding.rvPaymentInfo.visibility = View.VISIBLE
adapter.submitList(paymentInfo)
}
}
viewModel.isLoading.observe(this) { isLoading ->
binding.progressBar.visibility = if (isLoading) View.VISIBLE else View.GONE
binding.btnAddPayment.isEnabled = !isLoading
}
viewModel.errorMessage.observe(this) { errorMessage ->
if (errorMessage.isNotEmpty()) {
showSnackbar(errorMessage)
Log.e(TAG, "Error: $errorMessage")
}
}
viewModel.addPaymentSuccess.observe(this) { success ->
if (success) {
showSnackbar("Metode pembayaran berhasil ditambahkan")
setResult(Activity.RESULT_OK)
}
}
viewModel.deletePaymentSuccess.observe(this) { success ->
if (success) {
showSnackbar("Metode pembayaran berhasil dihapus")
setResult(Activity.RESULT_OK)
} }
} }
} }
private fun showSnackbar(message: String) {
Snackbar.make(binding.root, message, Snackbar.LENGTH_LONG).show()
}
private fun showAddPaymentDialog(isReopened: Boolean) {
val builder = AlertDialog.Builder(this)
val dialogView = layoutInflater.inflate(R.layout.dialog_add_payment_info, null)
builder.setView(dialogView)
val dialog = builder.create()
// Get references to views in the dialog
val btnAddQris = dialogView.findViewById<Button>(R.id.btn_add_qris)
val bankNameEditText = dialogView.findViewById<EditText>(R.id.edt_bank_name)
val bankNumberEditText = dialogView.findViewById<EditText>(R.id.edt_bank_number)
val accountNameEditText = dialogView.findViewById<EditText>(R.id.edt_account_name)
val qrisPreview = dialogView.findViewById<ImageView>(R.id.iv_qris_preview)
val btnCancel = dialogView.findViewById<Button>(R.id.btn_cancel)
val btnSave = dialogView.findViewById<Button>(R.id.btn_save)
// When reopening, restore the previously entered values
if (isReopened) {
bankNameEditText.setText(savedBankName)
bankNumberEditText.setText(savedBankNumber)
accountNameEditText.setText(savedAccountName)
if (selectedQrisImageUri != null) {
Log.d(TAG, "Showing selected QRIS image: $selectedQrisImageUri")
qrisPreview.setImageURI(selectedQrisImageUri)
qrisPreview.visibility = View.VISIBLE
showSnackbar("Gambar QRIS berhasil dipilih")
}
}
btnAddQris.setOnClickListener {
// Save the current values before dismissing
savedBankName = bankNameEditText.text.toString().trim()
savedBankNumber = bankNumberEditText.text.toString().trim()
savedAccountName = accountNameEditText.text.toString().trim()
getContent.launch("image/*")
dialog.dismiss() // Dismiss the current dialog as we'll reopen it
}
btnCancel.setOnClickListener {
dialog.dismiss()
}
btnSave.setOnClickListener {
val bankName = bankNameEditText.text.toString().trim()
val bankNumber = bankNumberEditText.text.toString().trim()
val accountName = accountNameEditText.text.toString().trim()
// Validation
if (bankName.isEmpty()) {
showSnackbar("Nama bank tidak boleh kosong")
return@setOnClickListener
}
if (bankNumber.isEmpty()) {
showSnackbar("Nomor rekening tidak boleh kosong")
return@setOnClickListener
}
if (accountName.isEmpty()) {
showSnackbar("Nama pemilik rekening tidak boleh kosong")
return@setOnClickListener
}
if (bankNumber.any { !it.isDigit() }) {
showSnackbar("Nomor rekening hanya boleh berisi angka")
return@setOnClickListener
}
// Log the data being sent
Log.d(TAG, "====== SENDING PAYMENT METHOD DATA ======")
Log.d(TAG, "Bank Name: $bankName")
Log.d(TAG, "Bank Number: $bankNumber")
Log.d(TAG, "Account Name: $accountName")
if (selectedQrisImageFile != null) {
Log.d(TAG, "QRIS file path: ${selectedQrisImageFile?.absolutePath}")
Log.d(TAG, "QRIS file exists: ${selectedQrisImageFile?.exists()}")
Log.d(TAG, "QRIS file size: ${selectedQrisImageFile?.length()} bytes")
} else {
Log.d(TAG, "No QRIS file selected")
}
// Temporarily disable the save button
btnSave.isEnabled = false
btnSave.text = "Menyimpan..."
// Add payment info
viewModel.addPaymentInfo(
bankName = bankName,
bankNumber = bankNumber,
accountName = accountName,
qrisImageUri = selectedQrisImageUri,
qrisImageFile = selectedQrisImageFile
)
dialog.dismiss()
}
dialog.show()
}
private fun showDeleteConfirmationDialog(paymentInfo: PaymentInfo) {
AlertDialog.Builder(this)
.setTitle("Hapus Metode Pembayaran")
.setMessage("Apakah Anda yakin ingin menghapus metode pembayaran ini?")
.setPositiveButton("Hapus") { _, _ ->
viewModel.deletePaymentInfo(paymentInfo.id)
}
.setNegativeButton("Batal", null)
.show()
}
}

View File

@ -0,0 +1,82 @@
package com.alya.ecommerce_serang.ui.profile.mystore.profile.payment_info
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import com.alya.ecommerce_serang.R
import com.alya.ecommerce_serang.data.api.dto.PaymentInfo
import com.bumptech.glide.Glide
class PaymentInfoAdapter(
private val onDeleteClick: (PaymentInfo) -> Unit
) : ListAdapter<PaymentInfo, PaymentInfoAdapter.PaymentInfoViewHolder>(DIFF_CALLBACK) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PaymentInfoViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.item_payment_info, parent, false)
return PaymentInfoViewHolder(view)
}
override fun onBindViewHolder(holder: PaymentInfoViewHolder, position: Int) {
holder.bind(getItem(position))
}
inner class PaymentInfoViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
private val tvBankName: TextView = itemView.findViewById(R.id.tv_bank_name)
private val tvAccountName: TextView = itemView.findViewById(R.id.tv_account_name)
private val tvBankNumber: TextView = itemView.findViewById(R.id.tv_bank_number)
private val ivDelete: ImageView = itemView.findViewById(R.id.iv_delete)
private val layoutQris: LinearLayout = itemView.findViewById(R.id.layout_qris)
private val ivQris: ImageView = itemView.findViewById(R.id.iv_qris)
fun bind(paymentInfo: PaymentInfo) {
tvBankName.text = paymentInfo.bankName
tvAccountName.text = paymentInfo.accountName ?: ""
tvBankNumber.text = paymentInfo.bankNum
// Handle QRIS image if available
if (paymentInfo.qrisImage != null && paymentInfo.qrisImage.isNotEmpty() && paymentInfo.qrisImage != "null") {
layoutQris.visibility = View.VISIBLE
// Make sure the URL is correct by handling both relative and absolute paths
val imageUrl = if (paymentInfo.qrisImage.startsWith("http")) {
paymentInfo.qrisImage
} else {
"http://192.168.100.156:3000${paymentInfo.qrisImage}"
}
Log.d("PaymentMethodAdapter", "Loading QRIS image from: $imageUrl")
Glide.with(itemView.context)
.load(imageUrl)
.placeholder(R.drawable.placeholder_image)
.error(R.drawable.placeholder_image)
.into(ivQris)
} else {
layoutQris.visibility = View.GONE
}
ivDelete.setOnClickListener {
onDeleteClick(paymentInfo)
}
}
}
companion object {
private val DIFF_CALLBACK = object : DiffUtil.ItemCallback<PaymentInfo>() {
override fun areItemsTheSame(oldItem: PaymentInfo, newItem: PaymentInfo): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: PaymentInfo, newItem: PaymentInfo): Boolean {
return oldItem == newItem
}
}
}
}

View File

@ -1,21 +1,108 @@
package com.alya.ecommerce_serang.ui.profile.mystore.profile.shipping_service package com.alya.ecommerce_serang.ui.profile.mystore.profile.shipping_service
import android.app.Activity
import android.os.Bundle import android.os.Bundle
import androidx.activity.enableEdgeToEdge import android.view.View
import android.widget.CheckBox
import android.widget.Toast
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat import com.alya.ecommerce_serang.data.api.retrofit.ApiConfig
import androidx.core.view.WindowInsetsCompat import com.alya.ecommerce_serang.data.repository.ShippingServiceRepository
import com.alya.ecommerce_serang.R import com.alya.ecommerce_serang.databinding.ActivityShippingServiceBinding
import com.alya.ecommerce_serang.utils.BaseViewModelFactory
import com.alya.ecommerce_serang.utils.SessionManager
import com.alya.ecommerce_serang.utils.viewmodel.ShippingServiceViewModel
class ShippingServiceActivity : AppCompatActivity() { class ShippingServiceActivity : AppCompatActivity() {
private lateinit var binding: ActivityShippingServiceBinding
private lateinit var sessionManager: SessionManager
private val courierCheckboxes = mutableListOf<Pair<CheckBox, String>>()
private val viewModel: ShippingServiceViewModel by viewModels {
BaseViewModelFactory {
val apiService = ApiConfig.getApiService(sessionManager)
val repository = ShippingServiceRepository(apiService)
ShippingServiceViewModel(repository)
}
}
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
enableEdgeToEdge() binding = ActivityShippingServiceBinding.inflate(layoutInflater)
setContentView(R.layout.activity_shipping_service) setContentView(binding.root)
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars()) sessionManager = SessionManager(this)
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
insets // Configure header
binding.header.headerTitle.text = "Atur Layanan Pengiriman"
binding.header.headerLeftIcon.setOnClickListener {
onBackPressedDispatcher.onBackPressed()
}
setupCourierCheckboxes()
setupObservers()
binding.btnSave.setOnClickListener {
saveShippingServices()
}
// Load shipping services
viewModel.getAvailableCouriers()
}
private fun setupCourierCheckboxes() {
// Add all courier checkboxes to the list for easy management
courierCheckboxes.add(Pair(binding.checkboxJne, "jne"))
courierCheckboxes.add(Pair(binding.checkboxPos, "pos"))
courierCheckboxes.add(Pair(binding.checkboxTiki, "tiki"))
}
private fun setupObservers() {
viewModel.availableCouriers.observe(this) { couriers ->
// Check the appropriate checkboxes based on available couriers
for (pair in courierCheckboxes) {
val checkbox = pair.first
val courierCode = pair.second
checkbox.isChecked = couriers.contains(courierCode)
}
}
viewModel.isLoading.observe(this) { isLoading ->
binding.progressBar.visibility = if (isLoading) View.VISIBLE else View.GONE
binding.contentLayout.visibility = if (isLoading) View.GONE else View.VISIBLE
}
viewModel.errorMessage.observe(this) { errorMessage ->
Toast.makeText(this, errorMessage, Toast.LENGTH_LONG).show()
}
viewModel.saveSuccess.observe(this) { success ->
if (success) {
Toast.makeText(this, "Layanan pengiriman berhasil disimpan", Toast.LENGTH_SHORT).show()
setResult(Activity.RESULT_OK)
finish()
} }
} }
} }
private fun saveShippingServices() {
val selectedCouriers = mutableListOf<String>()
for (pair in courierCheckboxes) {
val checkbox = pair.first
val courierCode = pair.second
if (checkbox.isChecked) {
selectedCouriers.add(courierCode)
}
}
if (selectedCouriers.isEmpty()) {
Toast.makeText(this, "Pilih minimal satu layanan pengiriman", Toast.LENGTH_SHORT).show()
return
}
viewModel.saveShippingServices(selectedCouriers)
}
}

View File

@ -0,0 +1,145 @@
package com.alya.ecommerce_serang.utils
import android.content.Context
import android.net.Uri
import android.provider.MediaStore
import android.provider.OpenableColumns
import android.util.Log
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import java.io.InputStream
import kotlin.random.Random
object UriToFileConverter {
private const val TAG = "UriToFileConverter"
fun uriToFile(uri: Uri, context: Context): File? {
return try {
Log.d(TAG, "Converting URI to file: $uri")
// Try to get original filename
val fileName = getFileNameFromUri(uri, context) ?: "upload_${System.currentTimeMillis()}"
val extension = getFileExtension(fileName) ?: ".jpg"
// Create a temporary file in the cache directory with proper name
val tempFile = File.createTempFile(
"upload_${Random.nextInt(10000)}",
extension,
context.cacheDir
)
Log.d(TAG, "Created temp file: ${tempFile.absolutePath}")
// Open the input stream and copy content
var inputStream: InputStream? = null
try {
inputStream = context.contentResolver.openInputStream(uri)
if (inputStream == null) {
Log.e(TAG, "Failed to open input stream for URI: $uri")
return null
}
// Copy content using a buffer
val outputStream = FileOutputStream(tempFile)
val buffer = ByteArray(4 * 1024) // 4 KB buffer
var bytesRead: Int
var totalBytesRead = 0
while (inputStream.read(buffer).also { bytesRead = it } != -1) {
outputStream.write(buffer, 0, bytesRead)
totalBytesRead += bytesRead
}
outputStream.flush()
outputStream.close()
Log.d(TAG, "Successfully copied $totalBytesRead bytes to file")
} catch (e: Exception) {
Log.e(TAG, "Error copying file data", e)
return null
} finally {
inputStream?.close()
}
// Verify the file
if (!tempFile.exists() || tempFile.length() == 0L) {
Log.e(TAG, "Created file doesn't exist or is empty: ${tempFile.absolutePath}")
return null
}
Log.d(TAG, "Successfully converted URI to file: ${tempFile.absolutePath}, size: ${tempFile.length()} bytes")
tempFile
} catch (e: Exception) {
Log.e(TAG, "Error converting URI to file", e)
null
}
}
private fun getFileNameFromUri(uri: Uri, context: Context): String? {
// Try the OpenableColumns query method first
val cursor = context.contentResolver.query(uri, null, null, null, null)
cursor?.use { c ->
if (c.moveToFirst()) {
val nameIndex = c.getColumnIndex(OpenableColumns.DISPLAY_NAME)
if (nameIndex != -1) {
val fileName = c.getString(nameIndex)
Log.d(TAG, "Retrieved filename from OpenableColumns: $fileName")
return fileName
}
}
}
// Try MediaStore method
val projection = arrayOf(MediaStore.Images.Media.DISPLAY_NAME)
try {
context.contentResolver.query(uri, projection, null, null, null)?.use { c ->
if (c.moveToFirst()) {
val nameIndex = c.getColumnIndexOrThrow(MediaStore.Images.Media.DISPLAY_NAME)
val fileName = c.getString(nameIndex)
Log.d(TAG, "Retrieved filename from MediaStore: $fileName")
return fileName
}
}
} catch (e: Exception) {
Log.e(TAG, "Error getting filename from MediaStore", e)
}
// Last resort: extract from URI path
uri.path?.let { path ->
val fileName = path.substring(path.lastIndexOf('/') + 1)
Log.d(TAG, "Retrieved filename from URI path: $fileName")
return fileName
}
return null
}
private fun getFileExtension(fileName: String): String? {
val lastDot = fileName.lastIndexOf('.')
return if (lastDot >= 0) {
fileName.substring(lastDot)
} else {
null
}
}
fun getFilePathFromUri(uri: Uri, context: Context): String? {
// For Media Gallery
val projection = arrayOf(MediaStore.Images.Media.DATA)
try {
val cursor = context.contentResolver.query(uri, projection, null, null, null)
cursor?.use {
if (it.moveToFirst()) {
val columnIndex = it.getColumnIndexOrThrow(MediaStore.Images.Media.DATA)
return it.getString(columnIndex)
}
}
} catch (e: Exception) {
Log.e(TAG, "Error getting file path from URI", e)
}
// If the above method fails, try direct conversion
return uri.path
}
}

View File

@ -0,0 +1,121 @@
package com.alya.ecommerce_serang.utils.viewmodel
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.dto.PaymentInfo
import com.alya.ecommerce_serang.data.repository.PaymentInfoRepository
import kotlinx.coroutines.launch
import java.io.File
class PaymentInfoViewModel(private val repository: PaymentInfoRepository) : ViewModel() {
private val TAG = "PaymentInfoViewModel"
private val _paymentInfos = MutableLiveData<List<PaymentInfo>>()
val paymentInfos: LiveData<List<PaymentInfo>> = _paymentInfos
private val _isLoading = MutableLiveData<Boolean>()
val isLoading: LiveData<Boolean> = _isLoading
private val _errorMessage = MutableLiveData<String>()
val errorMessage: LiveData<String> = _errorMessage
private val _addPaymentSuccess = MutableLiveData<Boolean>()
val addPaymentSuccess: LiveData<Boolean> = _addPaymentSuccess
private val _deletePaymentSuccess = MutableLiveData<Boolean>()
val deletePaymentSuccess: LiveData<Boolean> = _deletePaymentSuccess
fun getPaymentInfo() {
_isLoading.value = true
viewModelScope.launch {
try {
Log.d(TAG, "Loading payment info...")
val result = repository.getPaymentInfo()
if (result.isEmpty()) {
Log.d(TAG, "No payment info found")
} else {
Log.d(TAG, "Successfully loaded ${result.size} payment info")
for (method in result) {
Log.d(TAG, "Payment method: id=${method.id}, bank=${method.bankName}, account=${method.accountName}")
}
}
_paymentInfos.value = result
_isLoading.value = false
} catch (e: Exception) {
Log.e(TAG, "Error getting payment info", e)
_errorMessage.value = "Gagal memuat metode pembayaran: ${e.message?.take(100) ?: "Unknown error"}"
_isLoading.value = false
// Still set empty payment info to show empty state
_paymentInfos.value = emptyList()
}
}
}
fun addPaymentInfo(bankName: String, bankNumber: String, accountName: String, qrisImageUri: Uri?, qrisImageFile: File?) {
_isLoading.value = true
viewModelScope.launch {
try {
Log.d(TAG, "Adding payment info: bankName=$bankName, bankNumber=$bankNumber, accountName=$accountName")
Log.d(TAG, "Image file: ${qrisImageFile?.absolutePath}, exists: ${qrisImageFile?.exists()}, size: ${qrisImageFile?.length() ?: 0} bytes")
// Validate the file if it was provided
if (qrisImageUri != null && qrisImageFile == null) {
_errorMessage.value = "Gagal memproses gambar. Silakan pilih gambar lain."
_isLoading.value = false
_addPaymentSuccess.value = false
return@launch
}
// If we have a file, make sure it exists and has some content
if (qrisImageFile != null && (!qrisImageFile.exists() || qrisImageFile.length() == 0L)) {
Log.e(TAG, "Image file does not exist or is empty: ${qrisImageFile.absolutePath}")
_errorMessage.value = "File gambar tidak valid. Silakan pilih gambar lain."
_isLoading.value = false
_addPaymentSuccess.value = false
return@launch
}
val success = repository.addPaymentMethod(bankName, bankNumber, accountName, qrisImageFile)
_addPaymentSuccess.value = success
_isLoading.value = false
if (success) {
// Refresh the payment info list
getPaymentInfo()
}
} catch (e: Exception) {
Log.e(TAG, "Error adding payment info", e)
_errorMessage.value = "Gagal menambahkan metode pembayaran: ${e.message?.take(100) ?: "Unknown error"}"
_isLoading.value = false
_addPaymentSuccess.value = false
}
}
}
fun deletePaymentInfo(paymentInfoId: Int) {
_isLoading.value = true
viewModelScope.launch {
try {
val success = repository.deletePaymentMethod(paymentInfoId)
_deletePaymentSuccess.value = success
_isLoading.value = false
if (success) {
// Refresh the payment info list
getPaymentInfo()
}
} catch (e: Exception) {
Log.e(TAG, "Error deleting payment info", e)
_errorMessage.value = "Gagal menghapus metode pembayaran: ${e.message?.take(100) ?: "Unknown error"}"
_isLoading.value = false
_deletePaymentSuccess.value = false
}
}
}
}

View File

@ -0,0 +1,80 @@
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.repository.ShippingServiceRepository
import kotlinx.coroutines.launch
class ShippingServiceViewModel(private val repository: ShippingServiceRepository) : ViewModel() {
private val TAG = "ShippingServicesVM"
private val _availableCouriers = MutableLiveData<List<String>>()
val availableCouriers: LiveData<List<String>> = _availableCouriers
private val _isLoading = MutableLiveData<Boolean>()
val isLoading: LiveData<Boolean> = _isLoading
private val _errorMessage = MutableLiveData<String>()
val errorMessage: LiveData<String> = _errorMessage
private val _saveSuccess = MutableLiveData<Boolean>()
val saveSuccess: LiveData<Boolean> = _saveSuccess
fun getAvailableCouriers() {
_isLoading.value = true
viewModelScope.launch {
try {
val result = repository.getAvailableCouriers()
_availableCouriers.value = result
_isLoading.value = false
} catch (e: Exception) {
Log.e(TAG, "Error getting available couriers", e)
_errorMessage.value = "Failed to load shipping services: ${e.message}"
_isLoading.value = false
}
}
}
fun saveShippingServices(selectedCouriers: List<String>) {
if (selectedCouriers.isEmpty()) {
_errorMessage.value = "Please select at least one courier"
return
}
_isLoading.value = true
viewModelScope.launch {
try {
// First get current couriers to determine what to add/delete
val currentCouriers = repository.getAvailableCouriers()
// Calculate couriers to add (selected but not in current)
val couriersToAdd = selectedCouriers.filter { !currentCouriers.contains(it) }
// Calculate couriers to delete (in current but not selected)
val couriersToDelete = currentCouriers.filter { !selectedCouriers.contains(it) }
// Perform additions if needed
if (couriersToAdd.isNotEmpty()) {
repository.addShippingServices(couriersToAdd)
}
// Perform deletions if needed
if (couriersToDelete.isNotEmpty()) {
repository.deleteShippingServices(couriersToDelete)
}
_saveSuccess.value = true
_isLoading.value = false
} catch (e: Exception) {
Log.e(TAG, "Error saving shipping services", e)
_errorMessage.value = "Failed to save shipping services: ${e.message}"
_isLoading.value = false
_saveSuccess.value = false
}
}
}
}

View File

@ -82,12 +82,18 @@
android:text="Riwayat Saldo" android:text="Riwayat Saldo"
android:layout_marginTop="10dp"/> android:layout_marginTop="10dp"/>
<!-- Date Picker dengan Icon -->
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginTop="10dp">
<!-- Date Picker dengan Icon -->
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:background="@drawable/bg_text_field" android:background="@drawable/bg_text_field"
android:layout_marginTop="10dp"
android:orientation="horizontal" android:orientation="horizontal"
android:gravity="center_vertical"> android:gravity="center_vertical">
@ -123,6 +129,17 @@
</LinearLayout> </LinearLayout>
<!-- Clear Filter Button -->
<Button
android:id="@+id/btn_clear_filter"
android:layout_width="wrap_content"
android:text="Clear"
android:layout_marginStart="8dp"
style="@style/button.small.secondary.short"
android:visibility="gone"/>
</LinearLayout>
</LinearLayout> </LinearLayout>
<View <View
@ -138,8 +155,25 @@
android:scrollbars="vertical" android:scrollbars="vertical"
tools:listitem="@layout/item_balance_transaction" /> tools:listitem="@layout/item_balance_transaction" />
<TextView
android:id="@+id/tv_empty_state"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Tidak ada riwayat transaksi"
android:gravity="center"
android:padding="24dp"
style="@style/body_medium"
android:visibility="gone" />
</LinearLayout> </LinearLayout>
</ScrollView> </ScrollView>
<ProgressBar
android:id="@+id/progress_bar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:visibility="gone" />
</LinearLayout> </LinearLayout>

View File

@ -203,48 +203,6 @@
</LinearLayout> </LinearLayout>
<!-- Nomor Rekening -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_marginBottom="24dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Nomor Rekening"
style="@style/body_medium"
android:layout_marginEnd="4dp"/>
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="*"
style="@style/body_medium"
android:textColor="@color/red_required"
android:layout_gravity="end"/>
</LinearLayout>
<EditText
android:id="@+id/edt_no_rekening"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/bg_text_field"
android:hint="Isi nomor rekening Anda di sini"
android:padding="8dp"
style="@style/body_small"
android:layout_marginTop="10dp"/>
</LinearLayout>
<!-- Tanggal Transaksi --> <!-- Tanggal Transaksi -->
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"

View File

@ -284,7 +284,7 @@
android:layout_marginBottom="8dp" /> android:layout_marginBottom="8dp" />
<androidx.recyclerview.widget.RecyclerView <androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_payment_methods" android:id="@+id/rv_payment_info"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
tools:listitem="@layout/item_payment_method" /> tools:listitem="@layout/item_payment_method" />

View File

@ -175,6 +175,7 @@
android:background="@drawable/bg_text_field" android:background="@drawable/bg_text_field"
android:padding="8dp" android:padding="8dp"
style="@style/body_small" style="@style/body_small"
android:hint="Isi nama jalan di sini"
android:layout_marginTop="10dp"/> android:layout_marginTop="10dp"/>
</LinearLayout> </LinearLayout>
@ -200,6 +201,7 @@
android:background="@drawable/bg_text_field" android:background="@drawable/bg_text_field"
android:padding="8dp" android:padding="8dp"
style="@style/body_small" style="@style/body_small"
android:hint="Isi nama kecamatan di sini"
android:layout_marginTop="10dp"/> android:layout_marginTop="10dp"/>
</LinearLayout> </LinearLayout>
@ -225,6 +227,7 @@
android:background="@drawable/bg_text_field" android:background="@drawable/bg_text_field"
android:padding="8dp" android:padding="8dp"
style="@style/body_small" style="@style/body_small"
android:hint="Isi kode pos di sini"
android:layout_marginTop="10dp"/> android:layout_marginTop="10dp"/>
</LinearLayout> </LinearLayout>
@ -258,31 +261,91 @@
</LinearLayout> </LinearLayout>
<!-- Pinpoint Lokasi --> <!-- Pinpoint Lokasi -->
<!-- <LinearLayout-->
<!-- android:layout_width="match_parent"-->
<!-- android:layout_height="wrap_content"-->
<!-- android:orientation="vertical"-->
<!-- android:layout_marginBottom="24dp">-->
<!-- <TextView-->
<!-- android:layout_width="match_parent"-->
<!-- android:layout_height="wrap_content"-->
<!-- android:text="Pinpoint Lokasi"-->
<!-- style="@style/body_medium"-->
<!-- android:layout_marginEnd="4dp"/>-->
<!-- &lt;!&ndash; Map &ndash;&gt;-->
<!-- <org.osmdroid.views.MapView android:id="@+id/map"-->
<!-- android:layout_width="match_parent"-->
<!-- android:layout_height="220dp" />-->
<!-- <TextView-->
<!-- android:id="@+id/tv_location_display"-->
<!-- android:layout_width="match_parent"-->
<!-- android:layout_height="wrap_content"-->
<!-- android:text="Lokasi: Tidak dipilih"-->
<!-- style="@style/body_medium"/>-->
<!-- </LinearLayout>-->
<!-- Coordinates -->
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginBottom="16dp">
<!-- Latitude -->
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical" android:orientation="vertical"
android:layout_marginBottom="24dp"> android:layout_marginEnd="8dp">
<TextView <TextView
android:layout_width="match_parent" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="Pinpoint Lokasi" android:text="Latitude"
style="@style/body_medium" style="@style/body_medium"
android:layout_marginEnd="4dp"/> android:layout_marginBottom="4dp"/>
<!-- Map --> <EditText
<org.osmdroid.views.MapView android:id="@+id/map" android:id="@+id/edt_latitude"
android:layout_width="match_parent"
android:layout_height="220dp" />
<TextView
android:id="@+id/tv_location_display"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="Lokasi: Tidak dipilih" android:background="@drawable/bg_text_field"
style="@style/body_medium"/> android:padding="12dp"
android:hint="Latitude"
android:inputType="numberDecimal|numberSigned"
style="@style/body_small"/>
</LinearLayout>
<!-- Longitude -->
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical"
android:layout_marginStart="8dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Longitude"
style="@style/body_medium"
android:layout_marginBottom="4dp"/>
<EditText
android:id="@+id/edt_longitude"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/bg_text_field"
android:padding="12dp"
android:hint="Longitude"
android:inputType="numberDecimal|numberSigned"
style="@style/body_small"/>
</LinearLayout>
</LinearLayout> </LinearLayout>
<Button <Button

View File

@ -2,9 +2,82 @@
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:background="@android:color/white"
tools:context=".ui.profile.mystore.profile.payment_info.PaymentInfoActivity"> tools:context=".ui.profile.mystore.profile.payment_info.PaymentInfoActivity">
<include
android:id="@+id/header"
layout="@layout/header"
app:layout_constraintTop_toTopOf="parent" />
<LinearLayout
android:id="@+id/tv_empty_state"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:orientation="vertical"
android:padding="16dp"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@id/header">
<ImageView
android:layout_width="100dp"
android:layout_height="100dp"
android:layout_marginBottom="16dp"
android:src="@drawable/placeholder_image"
android:alpha="0.5" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Belum ada metode pembayaran"
android:textAlignment="center"
android:textSize="18sp"
android:textStyle="bold" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="Tambahkan metode pembayaran untuk memudahkan pembeli melakukan transaksi"
android:textAlignment="center"
android:textSize="14sp" />
</LinearLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_payment_info"
android:layout_width="match_parent"
android:layout_height="0dp"
android:paddingHorizontal="@dimen/horizontal_safe_area"
android:paddingVertical="8dp"
android:clipToPadding="false"
app:layout_constraintTop_toBottomOf="@id/header"
app:layout_constraintBottom_toTopOf="@id/btn_add_payment"
tools:listitem="@layout/item_payment_info"
tools:itemCount="2" />
<ProgressBar
android:id="@+id/progress_bar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/header" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_add_payment"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:text="Tambahkan Metode Pembayaran"
android:paddingVertical="12dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -2,10 +2,130 @@
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:fitsSystemWindows="true"
android:id="@+id/main"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:background="@android:color/white"
tools:context=".ui.profile.mystore.profile.shipping_service.ShippingServiceActivity"> tools:context=".ui.profile.mystore.profile.shipping_service.ShippingServiceActivity">
<include
android:id="@+id/header"
layout="@layout/header"
app:layout_constraintTop_toTopOf="parent" />
<ScrollView
android:id="@+id/content_layout"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintTop_toBottomOf="@id/header"
app:layout_constraintBottom_toTopOf="@id/btn_save">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Pilih Layanan Pengiriman"
android:textSize="18sp"
android:textStyle="bold"
android:layout_marginBottom="16dp" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Layanan pengiriman yang dipilih akan tersedia untuk pembeli saat checkout"
android:textSize="14sp"
android:layout_marginBottom="24dp" />
<CheckBox
android:id="@+id/checkbox_jne"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="JNE"
android:textSize="16sp"
android:paddingStart="8dp" />
<CheckBox
android:id="@+id/checkbox_pos"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="POS Indonesia"
android:textSize="16sp"
android:paddingStart="8dp" />
<CheckBox
android:id="@+id/checkbox_tiki"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="TIKI"
android:textSize="16sp"
android:paddingStart="8dp" />
<CheckBox
android:id="@+id/checkbox_sicepat"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="SiCepat"
android:textSize="16sp"
android:paddingStart="8dp" />
<CheckBox
android:id="@+id/checkbox_jnt"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="J&amp;T Express"
android:textSize="16sp"
android:paddingStart="8dp" />
<CheckBox
android:id="@+id/checkbox_ninja"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Ninja Express"
android:textSize="16sp"
android:paddingStart="8dp" />
<CheckBox
android:id="@+id/checkbox_antaraja"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="AnterAja"
android:textSize="16sp"
android:paddingStart="8dp" />
<CheckBox
android:id="@+id/checkbox_spx"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Shopee Express (SPX)"
android:textSize="16sp"
android:paddingStart="8dp" />
</LinearLayout>
</ScrollView>
<ProgressBar
android:id="@+id/progress_bar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/header" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_save"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:text="Simpan"
android:paddingVertical="12dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,102 @@
<?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"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Tambah Metode Pembayaran"
android:textSize="18sp"
android:textStyle="bold"
android:layout_marginBottom="16dp" />
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/edt_bank_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Nama Bank" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/edt_account_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Nama Pemilik Rekening" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/edt_bank_number"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Nomor Rekening"
android:inputType="number" />
</com.google.android.material.textfield.TextInputLayout>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="QRIS (Opsional)"
android:textStyle="bold"
android:layout_marginBottom="8dp" />
<Button
android:id="@+id/btn_add_qris"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Tambah Gambar QRIS"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_marginBottom="8dp" />
<ImageView
android:id="@+id/iv_qris_preview"
android:layout_width="150dp"
android:layout_height="150dp"
android:layout_gravity="center_horizontal"
android:layout_marginBottom="16dp"
android:scaleType="centerCrop"
android:visibility="gone"
android:contentDescription="QRIS Preview" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="end">
<Button
android:id="@+id/btn_cancel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Batal"
style="@style/Widget.MaterialComponents.Button.TextButton"
android:layout_marginEnd="8dp" />
<Button
android:id="@+id/btn_save"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Simpan" />
</LinearLayout>
</LinearLayout>

View File

@ -0,0 +1,92 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView 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:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
app:cardCornerRadius="8dp"
app:cardElevation="2dp">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp">
<LinearLayout
android:id="@+id/layout_info"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toStartOf="@id/iv_delete"
app:layout_constraintBottom_toTopOf="@id/layout_qris">
<TextView
android:id="@+id/tv_bank_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="16sp"
android:textStyle="bold"
tools:text="Mandiri" />
<TextView
android:id="@+id/tv_account_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="14sp"
android:layout_marginTop="4dp"
tools:text="Kemas" />
<TextView
android:id="@+id/tv_bank_number"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="14sp"
android:layout_marginTop="4dp"
tools:text="941281212313" />
</LinearLayout>
<ImageView
android:id="@+id/iv_delete"
android:layout_width="24dp"
android:layout_height="24dp"
android:src="@drawable/ic_delete"
android:contentDescription="Delete payment method"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<LinearLayout
android:id="@+id/layout_qris"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_marginTop="16dp"
android:visibility="gone"
app:layout_constraintTop_toBottomOf="@id/layout_info"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="QRIS"
android:textSize="14sp"
android:textStyle="bold" />
<ImageView
android:id="@+id/iv_qris"
android:layout_width="120dp"
android:layout_height="120dp"
android:layout_marginTop="8dp"
android:scaleType="centerCrop"
android:layout_gravity="center_horizontal"
android:contentDescription="QRIS" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>