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(
val message: String,
val store: Store,
val shipping: List<Shipping>,
val payment: List<Payment>
val store: Store? = null,
val shipping: List<Shipping>? = emptyList(),
val payment: List<Payment> = emptyList()
)
data class Store(
@ -51,5 +51,6 @@ data class Payment(
val id: Int,
@SerializedName("bank_num") val bankNum: 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.AddPaymentInfoResponse
import com.alya.ecommerce_serang.data.api.dto.CartItem
import com.alya.ecommerce_serang.data.api.dto.CityResponse
import com.alya.ecommerce_serang.data.api.dto.CompletedOrderRequest
@ -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.RegisterRequest
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.UpdateCart
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.UpdateProductResponse
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.topup.BalanceTopUpResponse
import com.alya.ecommerce_serang.data.api.response.store.topup.TopUpResponse
import okhttp3.MultipartBody
import okhttp3.RequestBody
import retrofit2.Call
@ -151,8 +155,15 @@ interface ApiService {
): Response<CreateAddressResponse>
@GET("mystore")
suspend fun getStore (): Response<StoreResponse>
suspend fun getStore(): Response<StoreResponse>
@GET("mystore")
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>
@GET("mystore/product") // Replace with actual endpoint
@ -242,6 +253,12 @@ interface ApiService {
@Part complaintimg: MultipartBody.Part
): Response<ComplaintResponse>
@GET("store/topup")
suspend fun getTopUpHistory(): Response<TopUpResponse>
@GET("store/topup")
suspend fun getFilteredTopUpHistory(@Query("date") date: String): Response<TopUpResponse>
@Multipart
@POST("store/createtopup")
suspend fun addBalanceTopUp(
@ -253,11 +270,6 @@ interface ApiService {
@Part("bank_num") bankNum: RequestBody
): Response<BalanceTopUpResponse>
@PUT("mystore/edit")
suspend fun updateStoreProfile(
@Body requestBody: okhttp3.RequestBody
): Response<StoreDataResponse>
@Multipart
@PUT("mystore/edit")
suspend fun updateStoreProfileMultipart(
@ -277,6 +289,40 @@ interface ApiService {
@Part storeimg: MultipartBody.Part?
): 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")
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)
}
binding.rvPaymentMethods.apply {
binding.rvPaymentInfo.apply {
layoutManager = LinearLayoutManager(this@CheckoutActivity)
adapter = paymentAdapter
}
@ -187,7 +187,7 @@ class CheckoutActivity : AppCompatActivity() {
}
}
binding.rvPaymentMethods.apply {
binding.rvPaymentInfo.apply {
layoutManager = LinearLayoutManager(this@CheckoutActivity)
adapter = testAdapter
}

View File

@ -2,10 +2,12 @@ package com.alya.ecommerce_serang.ui.profile.mystore
import android.content.Intent
import android.os.Bundle
import android.util.Log
import android.widget.Toast
import androidx.activity.enableEdgeToEdge
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import com.alya.ecommerce_serang.R
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.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.profile.DetailStoreProfileActivity
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.utils.BaseViewModelFactory
import com.alya.ecommerce_serang.utils.SessionManager
@ -64,10 +67,17 @@ class MyStoreActivity : AppCompatActivity() {
binding.tvStoreName.text = store.storeName
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)
.load(it)
.load(imageUrl)
.placeholder(R.drawable.placeholder_image)
.error(R.drawable.placeholder_image)
.into(binding.ivProfile)
} else {
Log.d("MyStoreActivity", "No store image available")
}
}
@ -81,19 +91,26 @@ class MyStoreActivity : AppCompatActivity() {
}
binding.tvHistory.setOnClickListener {
navigateToSellsFragment("all")
val intent = Intent(this, SellsActivity::class.java)
startActivity(intent)
}
binding.layoutPerluTagihan.setOnClickListener {
navigateToSellsFragment("pending")
val intent = Intent(this, SellsActivity::class.java)
startActivity(intent)
//navigateToSellsFragment("pending")
}
binding.layoutPembayaran.setOnClickListener {
navigateToSellsFragment("paid")
val intent = Intent(this, SellsActivity::class.java)
startActivity(intent)
//navigateToSellsFragment("paid")
}
binding.layoutPerluDikirim.setOnClickListener {
navigateToSellsFragment("processed")
val intent = Intent(this, SellsActivity::class.java)
startActivity(intent)
//navigateToSellsFragment("processed")
}
binding.layoutProductMenu.setOnClickListener {
@ -115,11 +132,24 @@ class MyStoreActivity : AppCompatActivity() {
}
}
private fun navigateToSellsFragment(status: String) {
val sellsFragment = SellsListFragment.newInstance(status)
supportFragmentManager.beginTransaction()
.replace(android.R.id.content, sellsFragment)
.addToBackStack(null)
.commit()
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == PROFILE_REQUEST_CODE && resultCode == RESULT_OK) {
// Refresh store data
viewModel.loadMyStore()
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
import android.app.DatePickerDialog
import android.content.Intent
import android.os.Bundle
import android.util.Log
import android.view.View
import android.widget.Toast
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat
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.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.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() {
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?) {
super.onCreate(savedInstanceState)
@ -24,13 +46,369 @@ class BalanceActivity : AppCompatActivity() {
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()
}
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() {
binding.btnTopUp.setOnClickListener {
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
val backButton = findViewById<ImageView>(R.id.header_left_icon)
backButton.setOnClickListener {
finish()
onBackPressedDispatcher.onBackPressed()
}
// Setup photo selection

View File

@ -1,7 +1,90 @@
package com.alya.ecommerce_serang.ui.profile.mystore.balance
/* class BalanceTransactionAdapter(private val balanceTransactionList: List<BalanceTransaction>) :
RecyclerView.Adapter<BalanceTransactionAdapter.TransactionViewHolder>() {
import android.view.LayoutInflater
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.databinding.ActivityDetailStoreProfileBinding
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.BaseViewModelFactory
import com.alya.ecommerce_serang.utils.SessionManager
@ -59,7 +61,19 @@ class DetailStoreProfileActivity : AppCompatActivity() {
binding.layoutAddress.setOnClickListener {
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()
@ -87,6 +101,20 @@ class DetailStoreProfileActivity : AppCompatActivity() {
Toast.makeText(this, "Alamat toko berhasil diperbarui", Toast.LENGTH_SHORT).show()
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
setResult(Activity.RESULT_OK)
}
@ -95,6 +123,8 @@ class DetailStoreProfileActivity : AppCompatActivity() {
companion object {
private const val EDIT_PROFILE_REQUEST_CODE = 100
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){
@ -105,7 +135,7 @@ class DetailStoreProfileActivity : AppCompatActivity() {
// Update store image if available
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")
Glide.with(this)

View File

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

View File

@ -1,21 +1,282 @@
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 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.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.recyclerview.widget.LinearLayoutManager
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() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContentView(R.layout.activity_payment_info)
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
insets
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?) {
super.onCreate(savedInstanceState)
binding = ActivityPaymentInfoBinding.inflate(layoutInflater)
setContentView(binding.root)
sessionManager = SessionManager(this)
// 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
import android.app.Activity
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.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import com.alya.ecommerce_serang.R
import com.alya.ecommerce_serang.data.api.retrofit.ApiConfig
import com.alya.ecommerce_serang.data.repository.ShippingServiceRepository
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() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContentView(R.layout.activity_shipping_service)
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
insets
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?) {
super.onCreate(savedInstanceState)
binding = ActivityShippingServiceBinding.inflate(layoutInflater)
setContentView(binding.root)
sessionManager = SessionManager(this)
// 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,44 +82,61 @@
android:text="Riwayat Saldo"
android:layout_marginTop="10dp"/>
<!-- Date Picker dengan Icon -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/bg_text_field"
android:layout_marginTop="10dp"
android:orientation="horizontal"
android:gravity="center_vertical">
android:layout_marginTop="10dp">
<!-- Icon Kalender -->
<ImageView
android:id="@+id/iconDatePicker"
android:layout_width="16dp"
android:layout_height="16dp"
android:layout_marginStart="8dp"
android:src="@drawable/ic_calendar"
android:contentDescription="Pilih Tanggal" />
<EditText
android:id="@+id/edt_tgl_transaksi"
<!-- Date Picker dengan Icon -->
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:hint="Pilih tanggal di sini"
android:padding="8dp"
style="@style/body_small"
android:background="@null"
android:focusable="false"
android:clickable="true" />
android:background="@drawable/bg_text_field"
android:orientation="horizontal"
android:gravity="center_vertical">
<ImageView
android:id="@+id/img_date_picker"
android:layout_width="16dp"
android:layout_height="16dp"
android:layout_marginEnd="8dp"
android:src="@drawable/ic_navigate_next"
android:contentDescription="Pilih Tanggal"
app:tint="@color/black_300" />
<!-- Icon Kalender -->
<ImageView
android:id="@+id/iconDatePicker"
android:layout_width="16dp"
android:layout_height="16dp"
android:layout_marginStart="8dp"
android:src="@drawable/ic_calendar"
android:contentDescription="Pilih Tanggal" />
<EditText
android:id="@+id/edt_tgl_transaksi"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:hint="Pilih tanggal di sini"
android:padding="8dp"
style="@style/body_small"
android:background="@null"
android:focusable="false"
android:clickable="true" />
<ImageView
android:id="@+id/img_date_picker"
android:layout_width="16dp"
android:layout_height="16dp"
android:layout_marginEnd="8dp"
android:src="@drawable/ic_navigate_next"
android:contentDescription="Pilih Tanggal"
app:tint="@color/black_300" />
</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>
@ -138,8 +155,25 @@
android:scrollbars="vertical"
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>
</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>

View File

@ -203,48 +203,6 @@
</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 -->
<LinearLayout
android:layout_width="match_parent"

View File

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

View File

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

View File

@ -2,9 +2,82 @@
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/white"
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>

View File

@ -2,10 +2,130 @@
<androidx.constraintlayout.widget.ConstraintLayout 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:fitsSystemWindows="true"
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/white"
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>

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>