From bf810ddc3e646f4322b66aa02a701f422b8017fd Mon Sep 17 00:00:00 2001 From: Gracia Hotmauli <95269134+hotmauligracia@users.noreply.github.com> Date: Tue, 13 May 2025 19:39:54 +0700 Subject: [PATCH] edit store profile & topup --- .../data/api/dto/PaymentInfo.kt | 60 +++ .../data/api/dto/ShippingService.kt | 13 + .../data/api/response/store/StoreResponse.kt | 18 + .../response/store/profile/GenericResponse.kt | 11 + .../store/profile/StoreDataResponse.kt | 11 +- .../api/response/store/topup/TopUpResponse.kt | 87 ++++ .../data/api/retrofit/ApiService.kt | 58 ++- .../data/repository/PaymentInfoRepository.kt | 168 ++++++++ .../repository/ShippingServiceRepository.kt | 78 ++++ .../ui/order/CheckoutActivity.kt | 4 +- .../ui/profile/mystore/MyStoreActivity.kt | 54 ++- .../mystore/balance/BalanceActivity.kt | 380 +++++++++++++++++- .../mystore/balance/BalanceTopUpActivity.kt | 2 +- .../balance/BalanceTransactionAdapter.kt | 89 +++- .../profile/DetailStoreProfileActivity.kt | 34 +- .../address/DetailStoreAddressActivity.kt | 11 +- .../payment_info/PaymentInfoActivity.kt | 283 ++++++++++++- .../payment_info/PaymentInfoAdapter.kt | 82 ++++ .../ShippingServiceActivity.kt | 111 ++++- .../utils/UriToFileConverter.kt | 145 +++++++ .../utils/viewmodel/PaymentInfoViewModel.kt | 121 ++++++ .../viewmodel/ShippingServiceViewModel.kt | 80 ++++ app/src/main/res/layout/activity_balance.xml | 92 +++-- .../res/layout/activity_balance_top_up.xml | 42 -- app/src/main/res/layout/activity_checkout.xml | 2 +- .../layout/activity_detail_store_address.xml | 95 ++++- .../main/res/layout/activity_payment_info.xml | 75 +++- .../res/layout/activity_shipping_service.xml | 124 +++++- .../res/layout/dialog_add_payment_info.xml | 102 +++++ app/src/main/res/layout/item_payment_info.xml | 92 +++++ 30 files changed, 2374 insertions(+), 150 deletions(-) create mode 100644 app/src/main/java/com/alya/ecommerce_serang/data/api/dto/PaymentInfo.kt create mode 100644 app/src/main/java/com/alya/ecommerce_serang/data/api/dto/ShippingService.kt create mode 100644 app/src/main/java/com/alya/ecommerce_serang/data/api/response/store/StoreResponse.kt create mode 100644 app/src/main/java/com/alya/ecommerce_serang/data/api/response/store/profile/GenericResponse.kt create mode 100644 app/src/main/java/com/alya/ecommerce_serang/data/api/response/store/topup/TopUpResponse.kt create mode 100644 app/src/main/java/com/alya/ecommerce_serang/data/repository/PaymentInfoRepository.kt create mode 100644 app/src/main/java/com/alya/ecommerce_serang/data/repository/ShippingServiceRepository.kt create mode 100644 app/src/main/java/com/alya/ecommerce_serang/ui/profile/mystore/profile/payment_info/PaymentInfoAdapter.kt create mode 100644 app/src/main/java/com/alya/ecommerce_serang/utils/UriToFileConverter.kt create mode 100644 app/src/main/java/com/alya/ecommerce_serang/utils/viewmodel/PaymentInfoViewModel.kt create mode 100644 app/src/main/java/com/alya/ecommerce_serang/utils/viewmodel/ShippingServiceViewModel.kt create mode 100644 app/src/main/res/layout/dialog_add_payment_info.xml create mode 100644 app/src/main/res/layout/item_payment_info.xml diff --git a/app/src/main/java/com/alya/ecommerce_serang/data/api/dto/PaymentInfo.kt b/app/src/main/java/com/alya/ecommerce_serang/data/api/dto/PaymentInfo.kt new file mode 100644 index 0000000..2e5cdab --- /dev/null +++ b/app/src/main/java/com/alya/ecommerce_serang/data/api/dto/PaymentInfo.kt @@ -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 +) + +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? +) \ No newline at end of file diff --git a/app/src/main/java/com/alya/ecommerce_serang/data/api/dto/ShippingService.kt b/app/src/main/java/com/alya/ecommerce_serang/data/api/dto/ShippingService.kt new file mode 100644 index 0000000..f373219 --- /dev/null +++ b/app/src/main/java/com/alya/ecommerce_serang/data/api/dto/ShippingService.kt @@ -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 +) \ No newline at end of file diff --git a/app/src/main/java/com/alya/ecommerce_serang/data/api/response/store/StoreResponse.kt b/app/src/main/java/com/alya/ecommerce_serang/data/api/response/store/StoreResponse.kt new file mode 100644 index 0000000..7637538 --- /dev/null +++ b/app/src/main/java/com/alya/ecommerce_serang/data/api/response/store/StoreResponse.kt @@ -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 +) \ No newline at end of file diff --git a/app/src/main/java/com/alya/ecommerce_serang/data/api/response/store/profile/GenericResponse.kt b/app/src/main/java/com/alya/ecommerce_serang/data/api/response/store/profile/GenericResponse.kt new file mode 100644 index 0000000..6a665b5 --- /dev/null +++ b/app/src/main/java/com/alya/ecommerce_serang/data/api/response/store/profile/GenericResponse.kt @@ -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 +) diff --git a/app/src/main/java/com/alya/ecommerce_serang/data/api/response/store/profile/StoreDataResponse.kt b/app/src/main/java/com/alya/ecommerce_serang/data/api/response/store/profile/StoreDataResponse.kt index 711e8be..fa7ad97 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/data/api/response/store/profile/StoreDataResponse.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/data/api/response/store/profile/StoreDataResponse.kt @@ -4,9 +4,9 @@ import com.google.gson.annotations.SerializedName data class StoreDataResponse( val message: String, - val store: Store, - val shipping: List, - val payment: List + val store: Store? = null, + val shipping: List? = emptyList(), + val payment: List = 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? +) \ No newline at end of file diff --git a/app/src/main/java/com/alya/ecommerce_serang/data/api/response/store/topup/TopUpResponse.kt b/app/src/main/java/com/alya/ecommerce_serang/data/api/response/store/topup/TopUpResponse.kt new file mode 100644 index 0000000..c7cc1bb --- /dev/null +++ b/app/src/main/java/com/alya/ecommerce_serang/data/api/response/store/topup/TopUpResponse.kt @@ -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 +) + +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" + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/alya/ecommerce_serang/data/api/retrofit/ApiService.kt b/app/src/main/java/com/alya/ecommerce_serang/data/api/retrofit/ApiService.kt index b86b350..fdd6198 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/data/api/retrofit/ApiService.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/data/api/retrofit/ApiService.kt @@ -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 @GET("mystore") - suspend fun getStore (): Response + suspend fun getStore(): Response + + @GET("mystore") suspend fun getStoreData(): Response + + @GET("mystore") + suspend fun getMyStoreData(): Response + + @GET("mystore") suspend fun getStoreAddress(): Response @GET("mystore/product") // Replace with actual endpoint @@ -242,6 +253,12 @@ interface ApiService { @Part complaintimg: MultipartBody.Part ): Response + @GET("store/topup") + suspend fun getTopUpHistory(): Response + + @GET("store/topup") + suspend fun getFilteredTopUpHistory(@Query("date") date: String): Response + @Multipart @POST("store/createtopup") suspend fun addBalanceTopUp( @@ -253,11 +270,6 @@ interface ApiService { @Part("bank_num") bankNum: RequestBody ): Response - @PUT("mystore/edit") - suspend fun updateStoreProfile( - @Body requestBody: okhttp3.RequestBody - ): Response - @Multipart @PUT("mystore/edit") suspend fun updateStoreProfileMultipart( @@ -277,6 +289,40 @@ interface ApiService { @Part storeimg: MultipartBody.Part? ): Response + @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 + + @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 + + @DELETE("mystore/payment/delete/{id}") + suspend fun deletePaymentInfo( + @Path("id") paymentMethodId: Int + ): Response + + // Shipping Service API endpoints + @POST("mystore/shipping/add") + suspend fun addShippingService( + @Body request: ShippingServiceRequest + ): Response + + @POST("mystore/shipping/delete") + suspend fun deleteShippingService( + @Body request: ShippingServiceRequest + ): Response + @GET("provinces") suspend fun getProvinces(): Response diff --git a/app/src/main/java/com/alya/ecommerce_serang/data/repository/PaymentInfoRepository.kt b/app/src/main/java/com/alya/ecommerce_serang/data/repository/PaymentInfoRepository.kt new file mode 100644 index 0000000..b8803d4 --- /dev/null +++ b/app/src/main/java/com/alya/ecommerce_serang/data/repository/PaymentInfoRepository.kt @@ -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 = 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() + } + + 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 + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/alya/ecommerce_serang/data/repository/ShippingServiceRepository.kt b/app/src/main/java/com/alya/ecommerce_serang/data/repository/ShippingServiceRepository.kt new file mode 100644 index 0000000..770613b --- /dev/null +++ b/app/src/main/java/com/alya/ecommerce_serang/data/repository/ShippingServiceRepository.kt @@ -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 = 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): 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): 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 + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/order/CheckoutActivity.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/order/CheckoutActivity.kt index f3f9628..7e25aec 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/ui/order/CheckoutActivity.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/ui/order/CheckoutActivity.kt @@ -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 } diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/profile/mystore/MyStoreActivity.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/profile/mystore/MyStoreActivity.kt index 662e5a7..0b040d3 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/ui/profile/mystore/MyStoreActivity.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/ui/profile/mystore/MyStoreActivity.kt @@ -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() +// } } \ No newline at end of file diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/profile/mystore/balance/BalanceActivity.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/profile/mystore/balance/BalanceActivity.kt index 24e9618..b0c36ea 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/ui/profile/mystore/balance/BalanceActivity.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/ui/profile/mystore/balance/BalanceActivity.kt @@ -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 = 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) { + 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 + } } \ No newline at end of file diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/profile/mystore/balance/BalanceTopUpActivity.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/profile/mystore/balance/BalanceTopUpActivity.kt index 5ef8b31..fd85cbb 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/ui/profile/mystore/balance/BalanceTopUpActivity.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/ui/profile/mystore/balance/BalanceTopUpActivity.kt @@ -80,7 +80,7 @@ class BalanceTopUpActivity : AppCompatActivity() { // Setup back button val backButton = findViewById(R.id.header_left_icon) backButton.setOnClickListener { - finish() + onBackPressedDispatcher.onBackPressed() } // Setup photo selection diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/profile/mystore/balance/BalanceTransactionAdapter.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/profile/mystore/balance/BalanceTransactionAdapter.kt index e106b1d..0b96498 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/ui/profile/mystore/balance/BalanceTransactionAdapter.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/ui/profile/mystore/balance/BalanceTransactionAdapter.kt @@ -1,7 +1,90 @@ package com.alya.ecommerce_serang.ui.profile.mystore.balance -/* class BalanceTransactionAdapter(private val balanceTransactionList: List) : - RecyclerView.Adapter() { +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(DIFF_CALLBACK) { -}*/ \ No newline at end of file + 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() { + override fun areItemsTheSame(oldItem: TopUp, newItem: TopUp): Boolean { + return oldItem.id == newItem.id + } + + override fun areContentsTheSame(oldItem: TopUp, newItem: TopUp): Boolean { + return oldItem == newItem + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/profile/mystore/profile/DetailStoreProfileActivity.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/profile/mystore/profile/DetailStoreProfileActivity.kt index 3cfb95f..2200295 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/ui/profile/mystore/profile/DetailStoreProfileActivity.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/ui/profile/mystore/profile/DetailStoreProfileActivity.kt @@ -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) diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/profile/mystore/profile/address/DetailStoreAddressActivity.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/profile/mystore/profile/address/DetailStoreAddressActivity.kt index 897e6c8..9ad1f8c 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/ui/profile/mystore/profile/address/DetailStoreAddressActivity.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/ui/profile/mystore/profile/address/DetailStoreAddressActivity.kt @@ -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 || diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/profile/mystore/profile/payment_info/PaymentInfoActivity.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/profile/mystore/profile/payment_info/PaymentInfoActivity.kt index ad7c97d..bcb692a 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/ui/profile/mystore/profile/payment_info/PaymentInfoActivity.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/ui/profile/mystore/profile/payment_info/PaymentInfoActivity.kt @@ -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