diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml index b268ef3..199a303 100644 --- a/.idea/deploymentTargetSelector.xml +++ b/.idea/deploymentTargetSelector.xml @@ -4,6 +4,14 @@ diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 24db662..f2c8352 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -41,6 +41,18 @@ + + + + @@ -52,7 +64,9 @@ android:enabled="true" android:exported="false" android:foregroundServiceType="dataSync" /> - + @@ -104,12 +118,6 @@ - - diff --git a/app/src/main/java/com/alya/ecommerce_serang/data/api/dto/City.kt b/app/src/main/java/com/alya/ecommerce_serang/data/api/dto/City.kt new file mode 100644 index 0000000..f8160a2 --- /dev/null +++ b/app/src/main/java/com/alya/ecommerce_serang/data/api/dto/City.kt @@ -0,0 +1,31 @@ +package com.alya.ecommerce_serang.data.api.dto + +import com.google.gson.annotations.SerializedName + +data class City( + @SerializedName("city_id") + val cityId: String, + + @SerializedName("city_name") + val cityName: String, + + @SerializedName("province_id") + val provinceId: String, + + @SerializedName("province") + val provinceName: String, + + @SerializedName("type") + val type: String, + + @SerializedName("postal_code") + val postalCode: String +) + +data class CityResponse( + @SerializedName("message") + val message: String, + + @SerializedName("cities") + val data: List +) \ No newline at end of file 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/Province.kt b/app/src/main/java/com/alya/ecommerce_serang/data/api/dto/Province.kt new file mode 100644 index 0000000..056d517 --- /dev/null +++ b/app/src/main/java/com/alya/ecommerce_serang/data/api/dto/Province.kt @@ -0,0 +1,19 @@ +package com.alya.ecommerce_serang.data.api.dto + +import com.google.gson.annotations.SerializedName + +data class Province( + @SerializedName("province_id") + val provinceId: String, + + @SerializedName("province") + val provinceName: String +) + +data class ProvinceResponse( + @SerializedName("message") + val message: String, + + @SerializedName("provinces") + val data: List +) \ 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/dto/StoreAddress.kt b/app/src/main/java/com/alya/ecommerce_serang/data/api/dto/StoreAddress.kt new file mode 100644 index 0000000..f403fc2 --- /dev/null +++ b/app/src/main/java/com/alya/ecommerce_serang/data/api/dto/StoreAddress.kt @@ -0,0 +1,55 @@ +package com.alya.ecommerce_serang.data.api.dto + +import com.google.gson.annotations.SerializedName + +data class StoreAddress( + @SerializedName("id") + val id: String? = null, + + @SerializedName("store_id") + val storeId: String? = null, + + @SerializedName("province_id") + val provinceId: String = "", + + @SerializedName("province_name") + val provinceName: String = "", + + @SerializedName("city_id") + val cityId: String = "", + + @SerializedName("city_name") + val cityName: String = "", + + @SerializedName("street") + val street: String = "", + + @SerializedName("subdistrict") + val subdistrict: String = "", + + @SerializedName("detail") + val detail: String? = null, + + @SerializedName("postal_code") + val postalCode: String = "", + + @SerializedName("latitude") + val latitude: Double? = 0.0, + + @SerializedName("longitude") + val longitude: Double? = 0.0, + + @SerializedName("created_at") + val createdAt: String? = null, + + @SerializedName("updated_at") + val updatedAt: String? = null +) + +data class StoreAddressResponse( + @SerializedName("message") + val message: String, + + @SerializedName("store") + val data: StoreAddress? = null +) \ 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/orders/KonfirmasiTagihanResponse.kt b/app/src/main/java/com/alya/ecommerce_serang/data/api/response/store/orders/KonfirmasiTagihanResponse.kt index af331d4..2e3df6a 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/data/api/response/store/orders/KonfirmasiTagihanResponse.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/data/api/response/store/orders/KonfirmasiTagihanResponse.kt @@ -1,5 +1,6 @@ package com.alya.ecommerce_serang.data.api.response.store.orders +import com.alya.ecommerce_serang.data.api.dto.UpdatedOrder import com.google.gson.annotations.SerializedName data class KonfirmasiTagihanResponse( diff --git a/app/src/main/java/com/alya/ecommerce_serang/data/api/response/store/orders/OrderListResponse.kt b/app/src/main/java/com/alya/ecommerce_serang/data/api/response/store/orders/OrderListResponse.kt index 64725ae..e9b00c3 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/data/api/response/store/orders/OrderListResponse.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/data/api/response/store/orders/OrderListResponse.kt @@ -1,6 +1,5 @@ package com.alya.ecommerce_serang.data.api.response.store.orders -import com.alya.ecommerce_serang.data.api.dto.OrdersItem import com.google.gson.annotations.SerializedName data class OrderListResponse( @@ -12,43 +11,133 @@ data class OrderListResponse( val message: String? = null ) -data class Voucher( - - @field:SerializedName("name") - val name: Any? = null, - - @field:SerializedName("voucher_id") - val voucherId: Any? = null, - - @field:SerializedName("voucher_code") - val voucherCode: Any? = null -) - -data class Shipment( +data class OrdersItem( @field:SerializedName("receipt_num") val receiptNum: Any? = null, - @field:SerializedName("courier") - val courier: Any? = null, + @field:SerializedName("payment_upload_at") + val paymentUploadAt: Any? = null, - @field:SerializedName("price") - val price: Any? = null, + @field:SerializedName("latitude") + val latitude: String? = null, + + @field:SerializedName("pay_info_name") + val payInfoName: String? = null, + + @field:SerializedName("created_at") + val createdAt: String? = null, + + @field:SerializedName("voucher_code") + val voucherCode: Any? = null, + + @field:SerializedName("updated_at") + val updatedAt: String? = null, + + @field:SerializedName("etd") + val etd: String? = null, + + @field:SerializedName("street") + val street: String? = null, + + @field:SerializedName("cancel_date") + val cancelDate: String? = null, + + @field:SerializedName("payment_evidence") + val paymentEvidence: Any? = null, + + @field:SerializedName("longitude") + val longitude: String? = null, + + @field:SerializedName("shipment_status") + val shipmentStatus: String? = null, + + @field:SerializedName("order_items") + val orderItems: List? = null, + + @field:SerializedName("auto_completed_at") + val autoCompletedAt: Any? = null, + + @field:SerializedName("is_store_location") + val isStoreLocation: Boolean? = null, + + @field:SerializedName("qris_image") + val qrisImage: String? = null, + + @field:SerializedName("voucher_name") + val voucherName: Any? = null, + + @field:SerializedName("payment_status") + val paymentStatus: Any? = null, + + @field:SerializedName("address_id") + val addressId: Int? = null, + + @field:SerializedName("is_negotiable") + val isNegotiable: Boolean? = null, + + @field:SerializedName("payment_amount") + val paymentAmount: Any? = null, + + @field:SerializedName("cancel_reason") + val cancelReason: String? = null, + + @field:SerializedName("user_id") + val userId: Int? = null, + + @field:SerializedName("total_amount") + val totalAmount: String? = null, + + @field:SerializedName("province_id") + val provinceId: Int? = null, + + @field:SerializedName("courier") + val courier: String? = null, + + @field:SerializedName("subdistrict") + val subdistrict: String? = null, @field:SerializedName("service") - val service: Any? = null, + val service: String? = null, - @field:SerializedName("shipment_id") - val shipmentId: Any? = null, + @field:SerializedName("pay_info_num") + val payInfoNum: String? = null, + + @field:SerializedName("shipment_price") + val shipmentPrice: String? = null, + + @field:SerializedName("voucher_id") + val voucherId: Any? = null, + + @field:SerializedName("payment_info_id") + val paymentInfoId: Int? = null, + + @field:SerializedName("detail") + val detail: String? = null, + + @field:SerializedName("postal_code") + val postalCode: String? = null, + + @field:SerializedName("order_id") + val orderId: Int? = null, + + @field:SerializedName("username") + val username: String? = null, @field:SerializedName("status") - val status: Any? = null + val status: String? = null, + + @field:SerializedName("city_id") + val cityId: Int? = null ) data class OrderItemsItem( + @field:SerializedName("order_item_id") + val orderItemId: Int? = null, + @field:SerializedName("review_id") - val reviewId: Int? = null, + val reviewId: Any? = null, @field:SerializedName("quantity") val quantity: Int? = null, @@ -62,6 +151,9 @@ data class OrderItemsItem( @field:SerializedName("product_image") val productImage: String? = null, + @field:SerializedName("product_id") + val productId: Int? = null, + @field:SerializedName("store_name") val storeName: String? = null, @@ -71,48 +163,3 @@ data class OrderItemsItem( @field:SerializedName("product_name") val productName: String? = null ) - -data class Address( - - @field:SerializedName("is_store_location") - val isStoreLocation: Boolean? = null, - - @field:SerializedName("province_id") - val provinceId: Int? = null, - - @field:SerializedName("street") - val street: String? = null, - - @field:SerializedName("subdistrict") - val subdistrict: String? = null, - - @field:SerializedName("latitude") - val latitude: Any? = null, - - @field:SerializedName("address_id") - val addressId: Int? = null, - - @field:SerializedName("detail") - val detail: String? = null, - - @field:SerializedName("postal_code") - val postalCode: String? = null, - - @field:SerializedName("longitude") - val longitude: Any? = null, - - @field:SerializedName("city_id") - val cityId: Int? = null -) - -data class Payment( - - @field:SerializedName("evidence") - val evidence: Any? = null, - - @field:SerializedName("uploaded_at") - val uploadedAt: Any? = null, - - @field:SerializedName("payment_id") - val paymentId: Any? = null -) diff --git a/app/src/main/java/com/alya/ecommerce_serang/data/api/response/store/orders/UpdateOrderItemResponse.kt b/app/src/main/java/com/alya/ecommerce_serang/data/api/response/store/orders/UpdateOrderItemResponse.kt index 9e6b08c..f56b81b 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/data/api/response/store/orders/UpdateOrderItemResponse.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/data/api/response/store/orders/UpdateOrderItemResponse.kt @@ -4,72 +4,27 @@ import com.google.gson.annotations.SerializedName data class UpdateOrderItemResponse( - @field:SerializedName("message") - val message: String? = null, - - @field:SerializedName("updatedOrder") - val updatedOrder: UpdatedOrder? = null, - - @field:SerializedName("updatedItems") - val updatedItems: List? = null -) - -data class UpdatedItemsItem( - - @field:SerializedName("quantity") - val quantity: Int? = null, - - @field:SerializedName("price") - val price: String? = null, - - @field:SerializedName("subtotal") - val subtotal: String? = null, - - @field:SerializedName("product_id") - val productId: Int? = null, - - @field:SerializedName("id") - val id: Int? = null, + @field:SerializedName("total_amount") + val totalAmount: Int? = null, @field:SerializedName("order_id") - val orderId: Int? = null -) + val orderId: Int? = null, -data class UpdatedOrder( - - @field:SerializedName("auto_canceled_at") - val autoCanceledAt: String? = null, - - @field:SerializedName("payment_method_id") - val paymentMethodId: Int? = null, - - @field:SerializedName("auto_completed_at") - val autoCompletedAt: String? = null, - - @field:SerializedName("updated_at") - val updatedAt: String? = null, - - @field:SerializedName("total_amount") - val totalAmount: String? = null, - - @field:SerializedName("user_id") - val userId: Int? = null, - - @field:SerializedName("address_id") - val addressId: Int? = null, - - @field:SerializedName("is_negotiable") - val isNegotiable: Boolean? = null, - - @field:SerializedName("created_at") - val createdAt: String? = null, - - @field:SerializedName("voucher_id") - val voucherId: Any? = null, - - @field:SerializedName("id") - val id: Int? = null, + @field:SerializedName("items") + val items: List? = null, @field:SerializedName("status") val status: String? = null ) + +data class ItemsItem( + + @field:SerializedName("order_item_id") + val orderItemId: Int? = null, + + @field:SerializedName("price") + val price: Int? = null, + + @field:SerializedName("subtotal") + val subtotal: Int? = null +) 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 new file mode 100644 index 0000000..fa7ad97 --- /dev/null +++ b/app/src/main/java/com/alya/ecommerce_serang/data/api/response/store/profile/StoreDataResponse.kt @@ -0,0 +1,56 @@ +package com.alya.ecommerce_serang.data.api.response.store.profile + +import com.google.gson.annotations.SerializedName + +data class StoreDataResponse( + val message: String, + val store: Store? = null, + val shipping: List? = emptyList(), + val payment: List = emptyList() +) + +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, + val ktp: String, + val npwp: String, + val nib: String, + val persetujuan: String?, + @SerializedName("store_image") val storeImage: String, + @SerializedName("store_description") val storeDescription: String, + @SerializedName("is_on_leave") val isOnLeave: Boolean, + @SerializedName("store_type_id") val storeTypeId: Int, + @SerializedName("store_type") val storeType: String, + val id: Int, + val latitude: String, + val longitude: String, + val street: String, + val subdistrict: String, + @SerializedName("postal_code") val postalCode: String, + val detail: String, + @SerializedName("is_store_location") val isStoreLocation: Boolean, + @SerializedName("user_id") val userId: Int, + @SerializedName("city_id") val cityId: Int, + @SerializedName("province_id") val provinceId: Int, + val phone: String?, + val recipient: String?, + @SerializedName("approval_status") val approvalStatus: String, + @SerializedName("approval_reason") val approvalReason: String? +) + +data class Shipping( + val courier: String +) + +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("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/BalanceTopUpResponse.kt b/app/src/main/java/com/alya/ecommerce_serang/data/api/response/store/topup/BalanceTopUpResponse.kt new file mode 100644 index 0000000..a6965ce --- /dev/null +++ b/app/src/main/java/com/alya/ecommerce_serang/data/api/response/store/topup/BalanceTopUpResponse.kt @@ -0,0 +1,6 @@ +package com.alya.ecommerce_serang.data.api.response.store.topup + +data class BalanceTopUpResponse( + val success: Boolean, + val message: String +) 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 e1c09b7..6e8497e 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,7 +2,9 @@ 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 import com.alya.ecommerce_serang.data.api.dto.CourierCostRequest import com.alya.ecommerce_serang.data.api.dto.CreateAddressRequest @@ -10,9 +12,12 @@ import com.alya.ecommerce_serang.data.api.dto.LoginRequest import com.alya.ecommerce_serang.data.api.dto.OrderRequest import com.alya.ecommerce_serang.data.api.dto.OrderRequestBuy 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.ReviewProductItem 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 import com.alya.ecommerce_serang.data.api.response.auth.LoginResponse @@ -52,6 +57,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 @@ -161,7 +170,16 @@ 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 suspend fun getStoreProduct(): Response @@ -231,19 +249,22 @@ interface ApiService { suspend fun getListProv( ): Response - @GET("mystore/orders") - suspend fun getAllOrders(): Response - - @GET("mystore/orders/{status}") - suspend fun getOrdersByStatus( - @Query("status") status: String - ): Response + @GET("order/{status}") + suspend fun getSellList( + @Path("status") status: String + ): Response @PUT("store/order/update") suspend fun confirmOrder( @Body confirmOrder : CompletedOrderRequest ): Response + @PUT("store/order/update") + suspend fun updateOrder( + @Query("order_id") orderId: Int?, + @Query("status") status: String + ): Response + @Multipart @POST("addcomplaint") suspend fun addComplaint( @@ -257,6 +278,89 @@ interface ApiService { @Body contentReview : ReviewProductItem ): 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( + @Part topupimg: MultipartBody.Part, + @Part("amount") amount: RequestBody, + @Part("payment_info_id") paymentInfoId: RequestBody, + @Part("transaction_date") transactionDate: RequestBody, + @Part("bank_name") bankName: RequestBody, + @Part("bank_num") bankNum: RequestBody + ): Response + + @Multipart + @PUT("mystore/edit") + suspend fun updateStoreProfileMultipart( + @Part("store_name") storeName: RequestBody, + @Part("store_status") storeStatus: RequestBody, + @Part("store_description") storeDescription: RequestBody, + @Part("is_on_leave") isOnLeave: RequestBody, + @Part("city_id") cityId: RequestBody, + @Part("province_id") provinceId: RequestBody, + @Part("street") street: RequestBody, + @Part("subdistrict") subdistrict: RequestBody, + @Part("detail") detail: RequestBody, + @Part("postal_code") postalCode: RequestBody, + @Part("latitude") latitude: RequestBody, + @Part("longitude") longitude: RequestBody, + @Part("user_phone") userPhone: RequestBody, + @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 + + @GET("cities/{provinceId}") + suspend fun getCities( + @Path("provinceId") provinceId: String + ): Response + + @PUT("mystore/edit") + suspend fun updateStoreAddress( + @Body addressData: HashMap + ): Response + @POST("search") suspend fun saveSearchQuery( @Body searchRequest: SearchRequest diff --git a/app/src/main/java/com/alya/ecommerce_serang/data/repository/AddressRepository.kt b/app/src/main/java/com/alya/ecommerce_serang/data/repository/AddressRepository.kt new file mode 100644 index 0000000..8659ef7 --- /dev/null +++ b/app/src/main/java/com/alya/ecommerce_serang/data/repository/AddressRepository.kt @@ -0,0 +1,174 @@ +package com.alya.ecommerce_serang.data.repository + +import android.util.Log +import com.alya.ecommerce_serang.data.api.dto.City +import com.alya.ecommerce_serang.data.api.dto.Province +import com.alya.ecommerce_serang.data.api.dto.StoreAddress +import com.alya.ecommerce_serang.data.api.retrofit.ApiService +import com.google.gson.Gson +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import org.json.JSONObject + +class AddressRepository(private val apiService: ApiService) { + + private val TAG = "AddressRepository" + + suspend fun getProvinces(): List = withContext(Dispatchers.IO) { + Log.d(TAG, "getProvinces() called") + try { + val response = apiService.getProvinces() + Log.d(TAG, "getProvinces() response: isSuccessful=${response.isSuccessful}, code=${response.code()}") + + // Log the raw response body for debugging + val rawBody = response.raw().toString() + Log.d(TAG, "Raw response: $rawBody") + + if (response.isSuccessful) { + val responseBody = response.body() + Log.d(TAG, "Response body: ${Gson().toJson(responseBody)}") + + val provinces = responseBody?.data ?: emptyList() + Log.d(TAG, "getProvinces() success, got ${provinces.size} provinces") + return@withContext provinces + } else { + val errorBody = response.errorBody()?.string() ?: "Unknown error" + Log.e(TAG, "getProvinces() error: $errorBody") + throw Exception("API Error (${response.code()}): $errorBody") + } + } catch (e: Exception) { + Log.e(TAG, "Exception in getProvinces()", e) + throw Exception("Network error: ${e.message}") + } + } + + suspend fun getCities(provinceId: String): List = withContext(Dispatchers.IO) { + Log.d(TAG, "getCities() called with provinceId: $provinceId") + try { + val response = apiService.getCities(provinceId) + Log.d(TAG, "getCities() response: isSuccessful=${response.isSuccessful}, code=${response.code()}") + + if (response.isSuccessful) { + val responseBody = response.body() + Log.d(TAG, "Response body: ${Gson().toJson(responseBody)}") + + val cities = responseBody?.data ?: emptyList() + Log.d(TAG, "getCities() success, got ${cities.size} cities") + return@withContext cities + } else { + val errorBody = response.errorBody()?.string() ?: "Unknown error" + Log.e(TAG, "getCities() error: $errorBody") + throw Exception("API Error (${response.code()}): $errorBody") + } + } catch (e: Exception) { + Log.e(TAG, "Exception in getCities()", e) + throw Exception("Network error: ${e.message}") + } + } + + suspend fun getStoreAddress(): StoreAddress? = withContext(Dispatchers.IO) { + Log.d(TAG, "getStoreAddress() called") + try { + val response = apiService.getStoreAddress() + Log.d(TAG, "getStoreAddress() response: isSuccessful=${response.isSuccessful}, code=${response.code()}") + + if (response.isSuccessful) { + val responseBody = response.body() + val rawJson = Gson().toJson(responseBody) + Log.d(TAG, "Response body: $rawJson") + + val address = responseBody?.data + Log.d(TAG, "getStoreAddress() success, address: $address") + + // Convert numeric strings to proper types if needed + address?.let { + // Handle city_id if it's a number + if (it.cityId.isBlank() && rawJson.contains("city_id")) { + try { + val cityId = JSONObject(rawJson).getJSONObject("store").optInt("city_id", 0) + if (cityId > 0) { + it.javaClass.getDeclaredField("cityId").apply { + isAccessible = true + set(it, cityId.toString()) + } + Log.d(TAG, "Updated cityId to: ${it.cityId}") + } + } catch (e: Exception) { + Log.e(TAG, "Error parsing city_id", e) + } + } + + // Handle province_id if it's a number + if (it.provinceId.isBlank() && rawJson.contains("province_id")) { + try { + val provinceId = JSONObject(rawJson).getJSONObject("store").optInt("province_id", 0) + if (provinceId > 0) { + it.javaClass.getDeclaredField("provinceId").apply { + isAccessible = true + set(it, provinceId.toString()) + } + Log.d(TAG, "Updated provinceId to: ${it.provinceId}") + } + } catch (e: Exception) { + Log.e(TAG, "Error parsing province_id", e) + } + } + } + + return@withContext address + } else { + val errorBody = response.errorBody()?.string() ?: "Unknown error" + Log.e(TAG, "getStoreAddress() error: $errorBody") + throw Exception("API Error (${response.code()}): $errorBody") + } + } catch (e: Exception) { + Log.e(TAG, "Exception in getStoreAddress()", e) + throw Exception("Network error: ${e.message}") + } + } + + suspend fun saveStoreAddress( + provinceId: String, + provinceName: String, + cityId: String, + cityName: String, + street: String, + subdistrict: String, + detail: String, + postalCode: String, + latitude: Double, + longitude: Double + ): Boolean = withContext(Dispatchers.IO) { + Log.d(TAG, "saveStoreAddress() called with provinceId: $provinceId, cityId: $cityId") + + try { + val addressMap = HashMap() + addressMap["provinceId"] = provinceId + addressMap["provinceName"] = provinceName + addressMap["cityId"] = cityId + addressMap["cityName"] = cityName + addressMap["street"] = street + addressMap["subdistrict"] = subdistrict + addressMap["detail"] = detail + addressMap["postalCode"] = postalCode + addressMap["latitude"] = latitude + addressMap["longitude"] = longitude + + Log.d(TAG, "saveStoreAddress() request data: $addressMap") + val response = apiService.updateStoreAddress(addressMap) + Log.d(TAG, "saveStoreAddress() response: isSuccessful=${response.isSuccessful}, code=${response.code()}") + + if (response.isSuccessful) { + Log.d(TAG, "saveStoreAddress() success") + return@withContext true + } else { + val errorBody = response.errorBody()?.string() ?: "Unknown error" + Log.e(TAG, "saveStoreAddress() error: $errorBody") + throw Exception("API Error (${response.code()}): $errorBody") + } + } catch (e: Exception) { + Log.e(TAG, "Exception in saveStoreAddress()", e) + throw Exception("Network error: ${e.message}") + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/alya/ecommerce_serang/data/repository/OrderRepository.kt b/app/src/main/java/com/alya/ecommerce_serang/data/repository/OrderRepository.kt index 9ab9afa..080c3cb 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/data/repository/OrderRepository.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/data/repository/OrderRepository.kt @@ -293,35 +293,6 @@ class OrderRepository(private val apiService: ApiService) { return if (response.isSuccessful) response.body() else null } - suspend fun fetchSells(): List { - return try { - val response = apiService.getAllOrders() // Replace with the actual method from your ApiService - if (response.isSuccessful) { - response.body()?.orders ?: emptyList() // Assuming the response body has 'orders' - } else { - Log.e("OrderRepository", "Error fetching all sells. Code: ${response.code()}") - emptyList() - } - } catch (e: Exception) { - Log.e("OrderRepository", "Exception fetching sells", e) - emptyList() - } - } - - suspend fun fetchOrdersByStatus(status: String): List { - return try { - val response = apiService.getOrdersByStatus(status) // Replace with actual method for status-based fetch - if (response.isSuccessful) { - response.body()?.orders?.filterNotNull() ?: emptyList() // Assuming the response body has 'orders' - } else { - Log.e("OrderRepository", "Error fetching orders by status ($status). Code: ${response.code()}") - emptyList() - } - } catch (e: Exception) { - Log.e("OrderRepository", "Exception fetching orders by status", e) - emptyList() - } - } suspend fun fetchUserProfile(): Result { return try { val response = apiService.getUserProfile() 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/SellsRepository.kt b/app/src/main/java/com/alya/ecommerce_serang/data/repository/SellsRepository.kt new file mode 100644 index 0000000..f384565 --- /dev/null +++ b/app/src/main/java/com/alya/ecommerce_serang/data/repository/SellsRepository.kt @@ -0,0 +1,45 @@ +package com.alya.ecommerce_serang.data.repository + +import android.util.Log +import com.alya.ecommerce_serang.data.api.response.store.orders.OrderListResponse +import com.alya.ecommerce_serang.data.api.retrofit.ApiService + +class SellsRepository(private val apiService: ApiService) { + suspend fun getSellList(status: String): Result { + return try { + Log.d("SellsRepository", "Add Evidence : $status") + val response = apiService.getSellList(status) + + if (response.isSuccessful) { + val allListSell = response.body() + if (allListSell != null) { + Log.d("SellsRepository", "Add Evidence successfully: ${allListSell.message}") + Result.Success(allListSell) + } else { + Log.e("SellsRepository", "Response body was null") + Result.Error(Exception("Empty response from server")) + } + } else { + val errorBody = response.errorBody()?.string() ?: "Unknown error" + Log.e("SellsRepository", "Error Add Evidence : $errorBody") + Result.Error(Exception(errorBody)) + } + } catch (e: Exception) { + Log.e("SellsRepository", "Exception Add Evidence ", e) + Result.Error(e) + } + } + + suspend fun updateOrderStatus(orderId: Int?, status: String) { + try { + val response = apiService.updateOrder(orderId, status) + if (response.isSuccessful) { + Log.d("SellsRepository", "Order status updated successfully: orderId=$orderId, status=$status") + } else { + Log.e("SellsRepository", "Error updating order status: orderId=$orderId, status=$status") + } + } catch (e: Exception) { + Log.e("SellsRepository", "Exception updating order status", 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/profile/mystore/MyStoreActivity.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/profile/mystore/MyStoreActivity.kt index af8a8c4..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,10 +18,8 @@ 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.all_sells.AllSellsFragment -import com.alya.ecommerce_serang.ui.profile.mystore.sells.order.OrderFragment -import com.alya.ecommerce_serang.ui.profile.mystore.sells.payment.PaymentFragment -import com.alya.ecommerce_serang.ui.profile.mystore.sells.shipment.ShipmentFragment +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 import com.alya.ecommerce_serang.utils.viewmodel.MyStoreViewModel @@ -67,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") } } @@ -84,31 +91,26 @@ class MyStoreActivity : AppCompatActivity() { } binding.tvHistory.setOnClickListener { - supportFragmentManager.beginTransaction() - .replace(android.R.id.content, AllSellsFragment()) - .addToBackStack(null) - .commit() + val intent = Intent(this, SellsActivity::class.java) + startActivity(intent) } binding.layoutPerluTagihan.setOnClickListener { - supportFragmentManager.beginTransaction() - .replace(android.R.id.content, OrderFragment()) - .addToBackStack(null) - .commit() + val intent = Intent(this, SellsActivity::class.java) + startActivity(intent) + //navigateToSellsFragment("pending") } binding.layoutPembayaran.setOnClickListener { - supportFragmentManager.beginTransaction() - .replace(android.R.id.content, PaymentFragment()) - .addToBackStack(null) - .commit() + val intent = Intent(this, SellsActivity::class.java) + startActivity(intent) + //navigateToSellsFragment("paid") } binding.layoutPerluDikirim.setOnClickListener { - supportFragmentManager.beginTransaction() - .replace(android.R.id.content, ShipmentFragment()) - .addToBackStack(null) - .commit() + val intent = Intent(this, SellsActivity::class.java) + startActivity(intent) + //navigateToSellsFragment("processed") } binding.layoutProductMenu.setOnClickListener { @@ -129,4 +131,25 @@ class MyStoreActivity : AppCompatActivity() { .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 4192943..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,21 +1,414 @@ 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) enableEdgeToEdge() - setContentView(R.layout.activity_balance) + binding = ActivityBalanceBinding.inflate(layoutInflater) + setContentView(binding.root) + 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 } + + // 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) + 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 12c437a..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 @@ -1,21 +1,395 @@ package com.alya.ecommerce_serang.ui.profile.mystore.balance +import android.app.DatePickerDialog +import android.content.Intent +import android.net.Uri import android.os.Bundle -import androidx.activity.enableEdgeToEdge +import android.provider.MediaStore +import android.view.View +import android.widget.AdapterView +import android.widget.ArrayAdapter +import android.widget.Button +import android.widget.EditText +import android.widget.ImageView +import android.widget.Spinner +import android.widget.TextView +import android.widget.Toast +import androidx.activity.result.contract.ActivityResultContracts import androidx.appcompat.app.AppCompatActivity -import androidx.core.view.ViewCompat -import androidx.core.view.WindowInsetsCompat +import androidx.lifecycle.lifecycleScope import com.alya.ecommerce_serang.R +import com.alya.ecommerce_serang.data.api.response.store.profile.Payment +import com.alya.ecommerce_serang.data.api.retrofit.ApiConfig +import com.alya.ecommerce_serang.utils.SessionManager +import kotlinx.coroutines.launch +import okhttp3.MediaType.Companion.toMediaTypeOrNull +import okhttp3.MultipartBody +import okhttp3.RequestBody.Companion.asRequestBody +import okhttp3.RequestBody.Companion.toRequestBody +import java.io.File +import java.text.SimpleDateFormat +import java.util.Calendar +import java.util.Locale class BalanceTopUpActivity : AppCompatActivity() { - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - enableEdgeToEdge() - setContentView(R.layout.activity_balance_top_up) - 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 imgPreview: ImageView + private lateinit var addPhotoTextView: TextView + private lateinit var edtNominal: EditText + private lateinit var spinnerPaymentMethod: Spinner + private lateinit var edtTransactionDate: EditText + private lateinit var datePickerIcon: ImageView + private lateinit var btnSend: Button + private lateinit var sessionManager: SessionManager + + private var selectedImageUri: Uri? = null + private var paymentMethods: List = emptyList() + private var selectedPaymentId: Int = -1 + + private val calendar = Calendar.getInstance() + + private val getImageContent = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> + if (result.resultCode == RESULT_OK) { + val imageUri = result.data?.data + imageUri?.let { + selectedImageUri = it + imgPreview.setImageURI(it) + } } } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_balance_top_up) + + // Initialize session manager + sessionManager = SessionManager(this) + + // Initialize views + imgPreview = findViewById(R.id.img_preview) + addPhotoTextView = findViewById(R.id.tv_tambah_foto) + edtNominal = findViewById(R.id.edt_nominal_topup) + spinnerPaymentMethod = findViewById(R.id.spinner_metode_bayar) + edtTransactionDate = findViewById(R.id.edt_tgl_transaksi) + datePickerIcon = findViewById(R.id.img_date_picker) + btnSend = findViewById(R.id.btn_send) + + // Setup header title + val headerTitle = findViewById(R.id.header_title) + headerTitle.text = "Isi Ulang Saldo" + + // Setup back button + val backButton = findViewById(R.id.header_left_icon) + backButton.setOnClickListener { + onBackPressedDispatcher.onBackPressed() + } + + // Setup photo selection + addPhotoTextView.setOnClickListener { + openGallery() + } + + imgPreview.setOnClickListener { + openGallery() + } + + // Setup date picker + setupDatePicker() + + // Fetch payment methods + fetchPaymentMethods() + + // Setup submit button + btnSend.setOnClickListener { + submitForm() + } + } + + private fun openGallery() { + val intent = Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI) + getImageContent.launch(intent) + } + + 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() + } + + edtTransactionDate.setOnClickListener { + showDatePicker(dateSetListener) + } + + datePickerIcon.setOnClickListener { + showDatePicker(dateSetListener) + } + } + + 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 = "yyyy-MM-dd" + val sdf = SimpleDateFormat(format, Locale.US) + edtTransactionDate.setText(sdf.format(calendar.time)) + } + + private fun fetchPaymentMethods() { + lifecycleScope.launch { + try { + val response = ApiConfig.getApiService(sessionManager).getStoreData() + if (response.isSuccessful && response.body() != null) { + val storeData = response.body()!! + paymentMethods = storeData.payment + + setupPaymentMethodSpinner() + } else { + Toast.makeText( + this@BalanceTopUpActivity, + "Gagal memuat metode pembayaran", + Toast.LENGTH_SHORT + ).show() + } + } catch (e: Exception) { + Toast.makeText( + this@BalanceTopUpActivity, + "Error: ${e.message}", + Toast.LENGTH_SHORT + ).show() + } + } + } + + private fun setupPaymentMethodSpinner() { + if (paymentMethods.isEmpty()) { + Toast.makeText( + this, + "Tidak ada metode pembayaran tersedia", + Toast.LENGTH_SHORT + ).show() + return + } + + // Debug payment methods + for (payment in paymentMethods) { + android.util.Log.d("BalanceTopUp", "Payment Option - ID: ${payment.id}, Bank: ${payment.bankName}, Number: ${payment.bankNum}") + } + + val paymentOptions = paymentMethods.map { "${it.bankName} - ${it.bankNum}" }.toTypedArray() + val adapter = ArrayAdapter(this, android.R.layout.simple_spinner_item, paymentOptions) + adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) + + spinnerPaymentMethod.adapter = adapter + spinnerPaymentMethod.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { + override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) { + selectedPaymentId = paymentMethods[position].id + android.util.Log.d("BalanceTopUp", "Selected payment ID: $selectedPaymentId") + } + + override fun onNothingSelected(parent: AdapterView<*>?) { + selectedPaymentId = -1 + } + } + } + + private fun submitForm() { + // Prevent multiple clicks + if (!btnSend.isEnabled) { + return + } + + // Validate inputs + if (selectedImageUri == null) { + Toast.makeText(this, "Mohon pilih foto bukti pembayaran", Toast.LENGTH_SHORT).show() + return + } + + val nominal = edtNominal.text.toString().trim() + if (nominal.isEmpty()) { + Toast.makeText(this, "Mohon isi nominal top up", Toast.LENGTH_SHORT).show() + return + } + + try { + // Validate the amount is a valid number + val amountValue = nominal.replace("[^0-9]".toRegex(), "").toLong() + if (amountValue <= 0) { + Toast.makeText(this, "Nominal harus lebih dari 0", Toast.LENGTH_SHORT).show() + return + } + } catch (e: NumberFormatException) { + Toast.makeText(this, "Format nominal tidak valid", Toast.LENGTH_SHORT).show() + return + } + + if (selectedPaymentId == -1) { + Toast.makeText(this, "Mohon pilih metode pembayaran", Toast.LENGTH_SHORT).show() + return + } + + val transactionDate = edtTransactionDate.text.toString().trim() + if (transactionDate.isEmpty()) { + Toast.makeText(this, "Mohon pilih tanggal transaksi", Toast.LENGTH_SHORT).show() + return + } + + // Show progress indicator + btnSend.text = "Mengirim..." + btnSend.isEnabled = false + + // Proceed with the API call + uploadTopUpData(nominal, selectedPaymentId.toString(), transactionDate) + } + + private fun uploadTopUpData(amount: String, paymentInfoId: String, transactionDate: String) { + lifecycleScope.launch { + try { + // Log the values being sent + android.util.Log.d("BalanceTopUp", "Amount: $amount") + android.util.Log.d("BalanceTopUp", "Payment ID: $paymentInfoId") + android.util.Log.d("BalanceTopUp", "Transaction Date: $transactionDate") + + // Find the selected payment method to get bank name + val selectedPayment = paymentMethods.find { it.id.toString() == paymentInfoId } + if (selectedPayment == null) { + Toast.makeText( + this@BalanceTopUpActivity, + "Metode pembayaran tidak valid", + Toast.LENGTH_SHORT + ).show() + return@launch + } + + val bankName = selectedPayment.bankName + val bankNum = selectedPayment.bankNum + android.util.Log.d("BalanceTopUp", "Bank Name: $bankName") + android.util.Log.d("BalanceTopUp", "Bank Number: $bankNum") + + // Get the actual file from URI + val file = uriToFile(selectedImageUri!!) + android.util.Log.d("BalanceTopUp", "File size: ${file.length()} bytes") + android.util.Log.d("BalanceTopUp", "File name: ${file.name}") + + // Create multipart file with specific JPEG content type + val requestFile = file.asRequestBody("image/jpeg".toMediaTypeOrNull()) + val imagePart = MultipartBody.Part.createFormData("topupimg", file.name, requestFile) + + // Create other request bodies - ensure proper formatting + // Make sure amount has no commas, spaces or currency symbols + val cleanedAmount = amount.replace("[^0-9]".toRegex(), "") + val amountBody = cleanedAmount.toRequestBody("text/plain".toMediaTypeOrNull()) + val paymentInfoIdBody = paymentInfoId.toRequestBody("text/plain".toMediaTypeOrNull()) + val transactionDateBody = transactionDate.toRequestBody("text/plain".toMediaTypeOrNull()) + val bankNameBody = bankName.toRequestBody("text/plain".toMediaTypeOrNull()) + val bankNumBody = bankNum.toRequestBody("text/plain".toMediaTypeOrNull()) + + // Make the API call + val response = ApiConfig.getApiService(sessionManager).addBalanceTopUp( + imagePart, + amountBody, + paymentInfoIdBody, + transactionDateBody, + bankNameBody, + bankNumBody + ) + + if (response.isSuccessful) { + // Log the complete response + val responseBody = response.body() + android.util.Log.d("BalanceTopUp", "Success response: ${responseBody?.message}") + + // Show the actual message from backend + val successMessage = "Top Up Berhasil" + Toast.makeText( + this@BalanceTopUpActivity, + successMessage, + Toast.LENGTH_LONG + ).show() + + // Show a dialog with the success message + runOnUiThread { + androidx.appcompat.app.AlertDialog.Builder(this@BalanceTopUpActivity) + .setTitle("Berhasil") + .setMessage(successMessage) + .setPositiveButton("OK") { dialog, _ -> + dialog.dismiss() + finish() + } + .show() + } + } else { + // Get more detailed error information + val errorBody = response.errorBody()?.string() + android.util.Log.e("BalanceTopUp", "Error body: $errorBody") + android.util.Log.e("BalanceTopUp", "Error code: ${response.code()}") + + // Try to parse the error body to extract the message + var errorMessage = "Gagal mengirim permintaan: ${response.message() ?: "Error ${response.code()}"}" + try { + val jsonObject = org.json.JSONObject(errorBody ?: "{}") + if (jsonObject.has("message")) { + errorMessage = jsonObject.getString("message") + } + } catch (e: Exception) { + android.util.Log.e("BalanceTopUp", "Error parsing error body", e) + } + + Toast.makeText( + this@BalanceTopUpActivity, + errorMessage, + Toast.LENGTH_LONG + ).show() + + // Show a dialog with the error message + runOnUiThread { + androidx.appcompat.app.AlertDialog.Builder(this@BalanceTopUpActivity) + .setTitle("Error Response") + .setMessage(errorMessage) + .setPositiveButton("OK") { dialog, _ -> + dialog.dismiss() + } + .show() + } + } + } catch (e: Exception) { + android.util.Log.e("BalanceTopUp", "Exception: ${e.message}", e) + Toast.makeText( + this@BalanceTopUpActivity, + "Error: ${e.message}", + Toast.LENGTH_SHORT + ).show() + } finally { + // Reset button state + btnSend.text = "Kirim" + btnSend.isEnabled = true + } + } + } + + private fun uriToFile(uri: Uri): File { + val inputStream = contentResolver.openInputStream(uri) + val tempFile = File.createTempFile("upload", ".jpg", cacheDir) + tempFile.deleteOnExit() + + inputStream?.use { input -> + tempFile.outputStream().use { output -> + input.copyTo(output) + } + } + + // Validate file isn't empty + if (tempFile.length() == 0L) { + throw IllegalStateException("File is empty") + } + + return tempFile + } } \ No newline at end of file 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 8e098d3..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 @@ -1,15 +1,22 @@ package com.alya.ecommerce_serang.ui.profile.mystore.profile +import android.app.Activity +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 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 @@ -39,6 +46,36 @@ class DetailStoreProfileActivity : AppCompatActivity() { enableEdgeToEdge() + // Set up header title + binding.header.headerTitle.text = "Profil Toko" + + // Set up back button + binding.header.headerLeftIcon.setOnClickListener { + onBackPressedDispatcher.onBackPressed() + } + + binding.btnEditStoreProfile.setOnClickListener { + val intent = Intent(this, EditStoreProfileActivity::class.java) + startActivityForResult(intent, EDIT_PROFILE_REQUEST_CODE) + } + + binding.layoutAddress.setOnClickListener { + val intent = Intent(this, DetailStoreAddressActivity::class.java) + startActivityForResult(intent, ADDRESS_REQUEST_CODE) + } + + // Set up payment method layout click listener + binding.layoutPaymentMethod.setOnClickListener { + val intent = Intent(this, PaymentInfoActivity::class.java) + startActivityForResult(intent, PAYMENT_INFO_REQUEST_CODE) + } + + // Set up shipping services layout click listener + binding.layoutShipServices.setOnClickListener { + val intent = Intent(this, ShippingServiceActivity::class.java) + startActivityForResult(intent, SHIPPING_SERVICES_REQUEST_CODE) + } + viewModel.loadMyStore() viewModel.myStoreProfile.observe(this){ user -> @@ -50,11 +87,64 @@ class DetailStoreProfileActivity : AppCompatActivity() { } } - private fun updateStoreProfile(store: Store){ + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + if (requestCode == EDIT_PROFILE_REQUEST_CODE && resultCode == Activity.RESULT_OK) { + // Refresh the profile data + Toast.makeText(this, "Profil toko berhasil diperbarui", Toast.LENGTH_SHORT).show() + viewModel.loadMyStore() + // Pass the result back to parent activity + setResult(Activity.RESULT_OK) + } else if (requestCode == ADDRESS_REQUEST_CODE && resultCode == Activity.RESULT_OK) { + // Refresh the profile data after address update + 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) + } + } + + 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){ + // Update text fields binding.edtNamaToko.setText(store.storeName.toString()) binding.edtJenisToko.setText(store.storeType.toString()) binding.edtDeskripsiToko.setText(store.storeDescription.toString()) + // 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}" + Log.d("DetailStoreProfile", "Loading image from: $imageUrl") + + Glide.with(this) + .load(imageUrl) + .placeholder(R.drawable.placeholder_image) + .error(R.drawable.placeholder_image) + .into(binding.ivProfile) + } else { + Log.d("DetailStoreProfile", "No store image available") + } } } \ No newline at end of file diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/profile/mystore/profile/EditStoreProfileActivity.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/profile/mystore/profile/EditStoreProfileActivity.kt new file mode 100644 index 0000000..2d31f3b --- /dev/null +++ b/app/src/main/java/com/alya/ecommerce_serang/ui/profile/mystore/profile/EditStoreProfileActivity.kt @@ -0,0 +1,304 @@ +package com.alya.ecommerce_serang.ui.profile.mystore.profile + +import android.app.Activity +import android.content.Intent +import android.net.Uri +import android.os.Bundle +import android.provider.MediaStore +import android.util.Log +import android.view.View +import android.widget.Toast +import androidx.activity.result.contract.ActivityResultContracts +import androidx.appcompat.app.AppCompatActivity +import androidx.lifecycle.lifecycleScope +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.databinding.ActivityEditStoreProfileBinding +import com.alya.ecommerce_serang.utils.SessionManager +import com.bumptech.glide.Glide +import kotlinx.coroutines.launch +import okhttp3.MediaType.Companion.toMediaTypeOrNull +import okhttp3.MultipartBody +import okhttp3.RequestBody.Companion.asRequestBody +import okhttp3.RequestBody.Companion.toRequestBody +import java.io.File +import java.io.FileOutputStream + +class EditStoreProfileActivity : AppCompatActivity() { + private lateinit var binding: ActivityEditStoreProfileBinding + private lateinit var sessionManager: SessionManager + private var storeImageUri: Uri? = null + private lateinit var currentStore: Store + + private val pickImage = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> + if (result.resultCode == Activity.RESULT_OK) { + result.data?.data?.let { uri -> + storeImageUri = uri + Log.d("EditStoreProfile", "Image selected: $uri") + + // Set the image to the ImageView for immediate preview + try { + binding.ivStoreImage.setImageURI(null) // Clear any previous image + binding.ivStoreImage.setImageURI(uri) + + // Alternative way using Glide for more reliable preview + Glide.with(this) + .load(uri) + .placeholder(R.drawable.placeholder_image) + .error(R.drawable.placeholder_image) + .into(binding.ivStoreImage) + + Toast.makeText(this, "Gambar berhasil dipilih", Toast.LENGTH_SHORT).show() + } catch (e: Exception) { + Log.e("EditStoreProfile", "Error displaying image preview", e) + Toast.makeText(this, "Gagal menampilkan preview gambar", Toast.LENGTH_SHORT).show() + } + } + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = ActivityEditStoreProfileBinding.inflate(layoutInflater) + setContentView(binding.root) + + sessionManager = SessionManager(this) + + // Set up header + binding.header.headerTitle.text = "Edit Profil Toko" + binding.header.headerLeftIcon.setOnClickListener { finish() } + + loadStoreData() + setupClickListeners() + } + + private fun loadStoreData() { + binding.progressBar.visibility = View.VISIBLE + lifecycleScope.launch { + try { + val response = ApiConfig.getApiService(sessionManager).getStore() + binding.progressBar.visibility = View.GONE + + if (response.isSuccessful && response.body() != null) { + currentStore = response.body()!!.store + populateFields(currentStore) + } else { + showError("Gagal memuat profil toko") + } + } catch (e: Exception) { + binding.progressBar.visibility = View.GONE + showError("Terjadi kesalahan: ${e.message}") + } + } + } + + private fun populateFields(store: Store) { + // Load store image + if (store.storeImage != null && store.storeImage.toString().isNotEmpty() && store.storeImage.toString() != "null") { + Glide.with(this) + .load(store.storeImage.toString()) + .placeholder(R.drawable.placeholder_image) + .error(R.drawable.placeholder_image) + .into(binding.ivStoreImage) + } + + // Set other fields + binding.edtStoreName.setText(store.storeName) + binding.edtDescription.setText(store.storeDescription) + binding.edtUserPhone.setText(store.userPhone) + + // Set is on leave + binding.switchIsOnLeave.isChecked = store.isOnLeave + } + + private fun setupClickListeners() { + binding.btnSelectStoreImage.setOnClickListener { + val intent = Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI) + pickImage.launch(intent) + } + + binding.btnSave.setOnClickListener { + saveStoreProfile() + } + } + + private fun saveStoreProfile() { + val storeName = binding.edtStoreName.text.toString() + val storeDescription = binding.edtDescription.text.toString() + val userPhone = binding.edtUserPhone.text.toString() + val storeStatus = currentStore.storeStatus // Keep the current status + val isOnLeave = binding.switchIsOnLeave.isChecked + + if (storeName.isEmpty() || userPhone.isEmpty()) { + showError("Nama toko dan nomor telepon harus diisi") + return + } + + binding.progressBar.visibility = View.VISIBLE + binding.btnSave.isEnabled = false + + // Show progress indicator on the image if we're uploading one + if (storeImageUri != null) { + binding.progressImage.visibility = View.VISIBLE + } + + lifecycleScope.launch { + try { + Log.d("EditStoreProfile", "Starting profile update process") + + // Create multipart request for image if selected + var storeImagePart: MultipartBody.Part? = null + if (storeImageUri != null) { + try { + val storeImageFile = uriToFile(storeImageUri!!) + Log.d("EditStoreProfile", "Image file created: ${storeImageFile.name}, size: ${storeImageFile.length()}") + + // Get the MIME type + val mimeType = contentResolver.getType(storeImageUri!!) ?: "image/jpeg" + Log.d("EditStoreProfile", "MIME type: $mimeType") + + val storeImageRequestBody = storeImageFile.asRequestBody(mimeType.toMediaTypeOrNull()) + storeImagePart = MultipartBody.Part.createFormData("storeimg", storeImageFile.name, storeImageRequestBody) + Log.d("EditStoreProfile", "Image part created with name: storeimg, filename: ${storeImageFile.name}") + } catch (e: Exception) { + Log.e("EditStoreProfile", "Error creating image part", e) + runOnUiThread { + Toast.makeText(this@EditStoreProfileActivity, "Error preparing image: ${e.message}", Toast.LENGTH_SHORT).show() + } + } + } + + // Create text parts + val nameRequestBody = storeName.toRequestBody("text/plain".toMediaTypeOrNull()) + val descriptionRequestBody = storeDescription.toRequestBody("text/plain".toMediaTypeOrNull()) + val userPhoneRequestBody = userPhone.toRequestBody("text/plain".toMediaTypeOrNull()) + val statusRequestBody = storeStatus.toRequestBody("text/plain".toMediaTypeOrNull()) + val onLeaveRequestBody = isOnLeave.toString().toRequestBody("text/plain".toMediaTypeOrNull()) + + // Log request parameters + Log.d("EditStoreProfile", "Request parameters: " + + "\nstore_name: $storeName" + + "\nstore_status: $storeStatus" + + "\nstore_description: $storeDescription" + + "\nis_on_leave: $isOnLeave" + + "\nuser_phone: $userPhone" + + "\nimage: ${storeImageUri != null}") + + // Log all parts for debugging + Log.d("EditStoreProfile", "Request parts:" + + "\nstoreName: $nameRequestBody" + + "\nstoreStatus: $statusRequestBody" + + "\nstoreDescription: $descriptionRequestBody" + + "\nisOnLeave: $onLeaveRequestBody" + + "\nuserPhone: $userPhoneRequestBody" + + "\nstoreimg: ${storeImagePart != null}") + + val response = ApiConfig.getApiService(sessionManager).updateStoreProfileMultipart( + storeName = nameRequestBody, + storeStatus = statusRequestBody, + storeDescription = descriptionRequestBody, + isOnLeave = onLeaveRequestBody, + cityId = currentStore.cityId.toString().toRequestBody("text/plain".toMediaTypeOrNull()), + provinceId = currentStore.provinceId.toString().toRequestBody("text/plain".toMediaTypeOrNull()), + street = currentStore.street.toRequestBody("text/plain".toMediaTypeOrNull()), + subdistrict = currentStore.subdistrict.toRequestBody("text/plain".toMediaTypeOrNull()), + detail = currentStore.detail.toRequestBody("text/plain".toMediaTypeOrNull()), + postalCode = currentStore.postalCode.toRequestBody("text/plain".toMediaTypeOrNull()), + latitude = currentStore.latitude.toRequestBody("text/plain".toMediaTypeOrNull()), + longitude = currentStore.longitude.toRequestBody("text/plain".toMediaTypeOrNull()), + userPhone = userPhoneRequestBody, + storeimg = storeImagePart + ) + + Log.d("EditStoreProfile", "Response received: isSuccessful=${response.isSuccessful}, code=${response.code()}") + + runOnUiThread { + binding.progressBar.visibility = View.GONE + binding.progressImage.visibility = View.GONE + binding.btnSave.isEnabled = true + + if (response.isSuccessful) { + Log.d("EditStoreProfile", "Response body: ${response.body()?.toString()}") + // Try to log the updated store image URL + response.body()?.let { responseBody -> + val updatedStoreImage = responseBody.store?.storeImage + Log.d("EditStoreProfile", "Updated store image URL: $updatedStoreImage") + } + showSuccess("Profil toko berhasil diperbarui") + setResult(Activity.RESULT_OK) + finish() + } else { + val errorBodyString = response.errorBody()?.string() ?: "Error body is null" + Log.e("EditStoreProfile", "Full error response: $errorBodyString") + Log.e("EditStoreProfile", "Response headers: ${response.headers()}") + showError("Gagal memperbarui profil toko (${response.code()})") + } + } + } catch (e: Exception) { + Log.e("EditStoreProfile", "Exception during API call", e) + + runOnUiThread { + binding.progressBar.visibility = View.GONE + binding.progressImage.visibility = View.GONE + binding.btnSave.isEnabled = true + showError("Error: ${e.message}") + } + } + } + } + + private fun uriToFile(uri: Uri): File { + val contentResolver = applicationContext.contentResolver + val fileExtension = getFileExtension(contentResolver, uri) + val timeStamp = System.currentTimeMillis() + val fileName = "IMG_${timeStamp}.$fileExtension" + val tempFile = File(cacheDir, fileName) + + Log.d("EditStoreProfile", "Creating temp file: ${tempFile.absolutePath}") + + try { + contentResolver.openInputStream(uri)?.use { inputStream -> + FileOutputStream(tempFile).use { outputStream -> + val buffer = ByteArray(4 * 1024) // 4k buffer + var bytesRead: Int + while (inputStream.read(buffer).also { bytesRead = it } != -1) { + outputStream.write(buffer, 0, bytesRead) + } + outputStream.flush() + } + } + + Log.d("EditStoreProfile", "File created successfully: ${tempFile.name}, size: ${tempFile.length()} bytes") + return tempFile + } catch (e: Exception) { + Log.e("EditStoreProfile", "Error creating file from URI", e) + throw e + } + } + + private fun getFileExtension(contentResolver: android.content.ContentResolver, uri: Uri): String { + val mimeType = contentResolver.getType(uri) + return if (mimeType != null) { + val mime = android.webkit.MimeTypeMap.getSingleton() + mime.getExtensionFromMimeType(mimeType) ?: "jpg" + } else { + // If mime type is null, try to get from URI path + val path = uri.path + if (path != null) { + val extension = android.webkit.MimeTypeMap.getFileExtensionFromUrl(path) + if (!extension.isNullOrEmpty()) { + extension + } else "jpg" + } else "jpg" + } + } + + private fun showSuccess(message: String) { + Toast.makeText(this, message, Toast.LENGTH_LONG).show() + } + + private fun showError(message: String) { + Toast.makeText(this, message, Toast.LENGTH_LONG).show() + } +} \ No newline at end of file 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 94f258c..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,21 +1,315 @@ package com.alya.ecommerce_serang.ui.profile.mystore.profile.address +import android.app.Activity import android.os.Bundle -import androidx.activity.enableEdgeToEdge +import android.util.Log +import android.view.View +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 androidx.core.view.ViewCompat -import androidx.core.view.WindowInsetsCompat -import com.alya.ecommerce_serang.R +import com.alya.ecommerce_serang.BuildConfig +import com.alya.ecommerce_serang.data.api.dto.City +import com.alya.ecommerce_serang.data.api.dto.Province +import com.alya.ecommerce_serang.data.api.retrofit.ApiConfig +import com.alya.ecommerce_serang.data.api.retrofit.ApiService +import com.alya.ecommerce_serang.data.repository.AddressRepository +import com.alya.ecommerce_serang.databinding.ActivityDetailStoreAddressBinding +import com.alya.ecommerce_serang.utils.viewmodel.AddressViewModel +import com.alya.ecommerce_serang.utils.BaseViewModelFactory +import com.alya.ecommerce_serang.utils.SessionManager +import com.google.android.material.snackbar.Snackbar class DetailStoreAddressActivity : AppCompatActivity() { + private lateinit var binding: ActivityDetailStoreAddressBinding + private lateinit var apiService: ApiService + private lateinit var sessionManager: SessionManager + + private var selectedProvinceId: String? = null + private var provinces: List = emptyList() + private var cities: List = emptyList() + + private val TAG = "StoreAddressActivity" + + private val viewModel: AddressViewModel by viewModels { + BaseViewModelFactory { + val apiService = ApiConfig.getApiService(sessionManager) + val addressRepository = AddressRepository(apiService) + AddressViewModel(addressRepository) + } + } + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - enableEdgeToEdge() - setContentView(R.layout.activity_detail_store_address) - 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 + binding = ActivityDetailStoreAddressBinding.inflate(layoutInflater) + setContentView(binding.root) + + sessionManager = SessionManager(this) + apiService = ApiConfig.getApiService(sessionManager) + + // Log the base URL + Log.d(TAG, "BASE_URL: ${BuildConfig.BASE_URL}") + + // Add error text view + binding.tvError.visibility = View.GONE + + // Set up header title + binding.header.headerTitle.text = "Atur Alamat Toko" + + // Set up back button + binding.header.headerLeftIcon.setOnClickListener { + onBackPressedDispatcher.onBackPressed() + } + + setupSpinners() + setupObservers() + setupSaveButton() + + // Add retry button + binding.btnRetry.setOnClickListener { + binding.tvError.visibility = View.GONE + binding.progressBar.visibility = View.VISIBLE + Log.d(TAG, "Retrying to fetch provinces...") + viewModel.fetchProvinces() + } + + // Show loading spinners initially + showProvinceLoading(true) + + // Load existing address data first + Log.d(TAG, "Fetching store address...") + viewModel.fetchStoreAddress() + + // Load provinces data + Log.d(TAG, "Fetching provinces...") + viewModel.fetchProvinces() + } + + private fun setupSpinners() { + // Province spinner listener + binding.spinnerProvince.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { + override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) { + Log.d(TAG, "Province selected at position: $position") + if (position > 0 && provinces.isNotEmpty()) { + selectedProvinceId = provinces[position - 1].provinceId + Log.d(TAG, "Selected province ID: $selectedProvinceId") + selectedProvinceId?.let { + Log.d(TAG, "Fetching cities for province ID: $it") + showCityLoading(true) + viewModel.fetchCities(it) + } + } + } + + override fun onNothingSelected(p0: AdapterView<*>?) { + // Do nothing + } + } + } + + private fun setupObservers() { + // Observe provinces data + viewModel.provinces.observe(this) { provinceList -> + Log.d(TAG, "Received provinces: ${provinceList.size}") + showProvinceLoading(false) + + if (provinceList.isEmpty()) { + showError("No provinces available") + return@observe + } + + provinces = provinceList + val provinceNames = mutableListOf("Pilih Provinsi") + provinceNames.addAll(provinceList.map { it.provinceName }) + + Log.d(TAG, "Province names: $provinceNames") + + val adapter = ArrayAdapter( + this, + android.R.layout.simple_spinner_item, + provinceNames + ) + adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) + binding.spinnerProvince.adapter = adapter + } + + // Observe cities data + viewModel.cities.observe(this) { cityList -> + Log.d(TAG, "Received cities: ${cityList.size}") + showCityLoading(false) + + cities = cityList + val cityNames = mutableListOf("Pilih Kota/Kabupaten") + cityNames.addAll(cityList.map { it.cityName }) + + Log.d(TAG, "City names: $cityNames") + + val adapter = ArrayAdapter( + this, + android.R.layout.simple_spinner_item, + cityNames + ) + adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) + binding.spinnerCity.adapter = adapter + + // If we have a stored city_id, select it + viewModel.storeAddress.value?.let { address -> + if (address.cityId.isNotEmpty()) { + val cityIndex = cities.indexOfFirst { city -> + city.cityId == address.cityId + } + Log.d(TAG, "City index for ID ${address.cityId}: $cityIndex") + if (cityIndex != -1) { + binding.spinnerCity.setSelection(cityIndex + 1) // +1 because of "Pilih Kota/Kabupaten" + } + } + } + } + + // Observe store address data + viewModel.storeAddress.observe(this) { address -> + Log.d(TAG, "Received store address: $address") + address?.let { + // Set the fields + binding.edtStreet.setText(address.street) + binding.edtSubdistrict.setText(address.subdistrict) + binding.edtDetailAddress.setText(address.detail ?: "") + binding.edtPostalCode.setText(address.postalCode) + + // Handle latitude and longitude + 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 + + // Find province index and select it after provinces are loaded + if (provinces.isNotEmpty()) { + val provinceIndex = provinces.indexOfFirst { province -> + province.provinceId == address.provinceId + } + Log.d(TAG, "Province index for ID ${address.provinceId}: $provinceIndex") + if (provinceIndex != -1) { + binding.spinnerProvince.setSelection(provinceIndex + 1) // +1 because of "Pilih Provinsi" + + // Now fetch cities for this province + showCityLoading(true) + viewModel.fetchCities(address.provinceId) + } + } + } + } + } + + // Observe loading state + viewModel.isLoading.observe(this) { isLoading -> + binding.progressBar.visibility = if (isLoading) View.VISIBLE else View.GONE + } + + // Observe error messages + viewModel.errorMessage.observe(this) { errorMsg -> + Log.e(TAG, "Error: $errorMsg") + showError(errorMsg) + } + + // Observe save success + viewModel.saveSuccess.observe(this) { success -> + if (success) { + Toast.makeText(this, "Alamat berhasil disimpan", Toast.LENGTH_SHORT).show() + setResult(Activity.RESULT_OK) + finish() + } + } + } + + private fun showProvinceLoading(isLoading: Boolean) { + binding.provinceProgressBar.visibility = if (isLoading) View.VISIBLE else View.GONE + binding.spinnerProvince.visibility = if (isLoading) View.GONE else View.VISIBLE + } + + private fun showCityLoading(isLoading: Boolean) { + binding.cityProgressBar.visibility = if (isLoading) View.VISIBLE else View.GONE + binding.spinnerCity.visibility = if (isLoading) View.GONE else View.VISIBLE + } + + private fun showError(message: String) { + binding.progressBar.visibility = View.GONE + binding.tvError.visibility = View.VISIBLE + binding.tvError.text = "Error: $message\nURL: ${BuildConfig.BASE_URL}/provinces" + binding.btnRetry.visibility = View.VISIBLE + + // Also show in a dialog for immediate attention + AlertDialog.Builder(this) + .setTitle("Error") + .setMessage("$message\n\nAPI URL: ${BuildConfig.BASE_URL}/provinces") + .setPositiveButton("Retry") { _, _ -> + binding.tvError.visibility = View.GONE + binding.progressBar.visibility = View.VISIBLE + viewModel.fetchProvinces() + } + .setNegativeButton("Cancel") { dialog, _ -> + dialog.dismiss() + } + .show() + + // Also show a snackbar + Snackbar.make(binding.root, "Error: $message", Snackbar.LENGTH_LONG) + .setAction("Retry") { + binding.tvError.visibility = View.GONE + binding.progressBar.visibility = View.VISIBLE + viewModel.fetchProvinces() + } + .show() + } + + private fun setupSaveButton() { + binding.btnSaveAddress.setOnClickListener { + val street = binding.edtStreet.text.toString() + val subdistrict = binding.edtSubdistrict.text.toString() + val detail = binding.edtDetailAddress.text.toString() + val postalCode = binding.edtPostalCode.text.toString() + val latitudeStr = binding.edtLatitude.text.toString() + val longitudeStr = binding.edtLongitude.text.toString() + + // Validate required fields + if (selectedProvinceId == null || binding.spinnerCity.selectedItemPosition <= 0 || + street.isEmpty() || subdistrict.isEmpty() || postalCode.isEmpty()) { + Toast.makeText(this, "Mohon lengkapi data yang wajib diisi", Toast.LENGTH_SHORT).show() + return@setOnClickListener + } + + // Get selected city + val cityPosition = binding.spinnerCity.selectedItemPosition + if (cityPosition <= 0 || cities.isEmpty() || cityPosition > cities.size) { + Toast.makeText(this, "Mohon pilih kota/kabupaten", Toast.LENGTH_SHORT).show() + return@setOnClickListener + } + + val selectedCity = cities[cityPosition - 1] + + // Parse coordinates + val latitude = latitudeStr.toDoubleOrNull() ?: 0.0 + val longitude = longitudeStr.toDoubleOrNull() ?: 0.0 + + // Save address + viewModel.saveStoreAddress( + provinceId = selectedProvinceId!!, + provinceName = provinces.find { it.provinceId == selectedProvinceId }?.provinceName ?: "", + cityId = selectedCity.cityId, + cityName = selectedCity.cityName, + street = street, + subdistrict = subdistrict, + detail = detail, + postalCode = postalCode, + latitude = latitude, + longitude = longitude + ) } } } \ No newline at end of file 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