Merge remote-tracking branch 'origin/master' into gracia

This commit is contained in:
Gracia Hotmauli
2025-04-25 12:17:44 +07:00
56 changed files with 4159 additions and 451 deletions

1
.idea/misc.xml generated
View File

@ -1,4 +1,3 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CodeInsightWorkspaceSettings">
<option name="optimizeImportsOnTheFly" value="true" />

View File

@ -6,10 +6,9 @@
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32"/>
<application
android:allowBackup="true"
@ -25,7 +24,25 @@
android:usesCleartextTraffic="true"
tools:targetApi="31">
<activity
android:name=".data.api.response.customer.cart.CartActivity"
android:name=".ui.order.detail.AddEvidencePaymentActivity"
android:exported="false" />
<activity
android:name=".ui.order.history.HistoryActivity"
android:exported="false" />
<activity
android:name=".ui.order.detail.PaymentActivity"
android:exported="false" />
<activity
android:name=".ui.order.detail.AddEvidencePaymentActivity"
android:exported="false" />
<activity
android:name=".ui.order.history.HistoryActivity"
android:exported="false" />
<activity
android:name=".ui.order.detail.PaymentActivity"
android:exported="false" />
<activity
android:name=".data.api.response.cart.CartActivity"
android:exported="false" />
<activity
android:name=".ui.order.address.EditAddressActivity"

View File

@ -0,0 +1,10 @@
package com.alya.ecommerce_serang.data.api.dto
import okhttp3.MultipartBody
import okhttp3.RequestBody
data class AddEvidenceMultipartRequest(
val orderId: RequestBody,
val amount: RequestBody,
val evidence: MultipartBody.Part
)

View File

@ -0,0 +1,15 @@
package com.alya.ecommerce_serang.data.api.dto
import com.google.gson.annotations.SerializedName
import okhttp3.MultipartBody
data class AddEvidenceRequest (
@SerializedName("orer_id")
val orderId : Int,
@SerializedName("amount")
val amount : String,
@SerializedName("evidence")
val evidence: MultipartBody.Part
)

View File

@ -0,0 +1,16 @@
package com.alya.ecommerce_serang.data.api.dto
import com.google.gson.annotations.SerializedName
import okhttp3.MultipartBody
data class ComplaintRequest (
@SerializedName("order_id")
val orderId: Int,
@SerializedName("description")
val description: String,
@SerializedName("complaintimg")
val complaintImg: MultipartBody.Part
)

View File

@ -0,0 +1,12 @@
package com.alya.ecommerce_serang.data.api.dto
import com.google.gson.annotations.SerializedName
data class CompletedOrderRequest (
@SerializedName("order_id")
val orderId : Int,
@SerializedName("status")
val statusComplete: String
)

View File

@ -7,7 +7,7 @@ data class CourierCostRequest(
val addressId: Int,
@SerializedName("items")
val itemCost: CostProduct
val itemCost: List<CostProduct>
)
data class CostProduct (

View File

@ -0,0 +1,33 @@
package com.alya.ecommerce_serang.data.api.response.order
import com.google.gson.annotations.SerializedName
data class AddEvidenceResponse(
@field:SerializedName("evidence")
val evidence: Evidence,
@field:SerializedName("message")
val message: String
)
data class Evidence(
@field:SerializedName("amount")
val amount: String,
@field:SerializedName("evidence")
val evidence: String,
@field:SerializedName("uploaded_at")
val uploadedAt: String,
@field:SerializedName("id")
val id: Int,
@field:SerializedName("order_id")
val orderId: Int,
@field:SerializedName("status")
val status: String
)

View File

@ -0,0 +1,36 @@
package com.alya.ecommerce_serang.data.api.response.order
import com.google.gson.annotations.SerializedName
data class ComplaintResponse(
@field:SerializedName("voucher")
val voucher: Voucher,
@field:SerializedName("message")
val message: String
)
data class Voucher(
@field:SerializedName("solution")
val solution: Any,
@field:SerializedName("evidence")
val evidence: String,
@field:SerializedName("description")
val description: String,
@field:SerializedName("created_at")
val createdAt: String,
@field:SerializedName("id")
val id: Int,
@field:SerializedName("order_id")
val orderId: Int,
@field:SerializedName("status")
val status: String
)

View File

@ -0,0 +1,51 @@
package com.alya.ecommerce_serang.data.api.response.order
import com.google.gson.annotations.SerializedName
data class CompletedOrderResponse(
@field:SerializedName("message")
val message: String,
@field:SerializedName("updatedOrder")
val updatedOrder: UpdatedOrder,
@field:SerializedName("updatedItems")
val updatedItems: List<Any>
)
data class UpdatedOrder(
@field:SerializedName("auto_completed_at")
val autoCompletedAt: Any,
@field:SerializedName("updated_at")
val updatedAt: String,
@field:SerializedName("total_amount")
val totalAmount: String,
@field:SerializedName("user_id")
val userId: Int,
@field:SerializedName("address_id")
val addressId: Int,
@field:SerializedName("is_negotiable")
val isNegotiable: Boolean,
@field:SerializedName("created_at")
val createdAt: String,
@field:SerializedName("voucher_id")
val voucherId: Any,
@field:SerializedName("payment_info_id")
val paymentInfoId: Any,
@field:SerializedName("id")
val id: Int,
@field:SerializedName("status")
val status: String
)

View File

@ -4,14 +4,128 @@ import com.google.gson.annotations.SerializedName
data class OrderDetailResponse(
@field:SerializedName("orders")
@field:SerializedName("orders")
val orders: Orders,
@field:SerializedName("message")
@field:SerializedName("message")
val message: String
)
data class OrderItemsItem(
data class Orders(
@field:SerializedName("receipt_num")
val receiptNum: String? = null,
@field:SerializedName("payment_upload_at")
val paymentUploadAt: String? = null,
@field:SerializedName("latitude")
val latitude: String,
@field:SerializedName("pay_info_name")
val payInfoName: String? = null,
@field:SerializedName("created_at")
val createdAt: String,
@field:SerializedName("voucher_code")
val voucherCode: String? = null,
@field:SerializedName("updated_at")
val updatedAt: String,
@field:SerializedName("etd")
val etd: String,
@field:SerializedName("street")
val street: String,
@field:SerializedName("cancel_date")
val cancelDate: String? = null,
@field:SerializedName("payment_evidence")
val paymentEvidence: String? = null,
@field:SerializedName("longitude")
val longitude: String,
@field:SerializedName("shipment_status")
val shipmentStatus: String,
@field:SerializedName("order_items")
val orderItems: List<OrderListItemsItem>,
@field:SerializedName("auto_completed_at")
val autoCompletedAt: String? = null,
@field:SerializedName("is_store_location")
val isStoreLocation: Boolean? = null,
@field:SerializedName("qris_image")
val qrisImage: String? = null,
@field:SerializedName("voucher_name")
val voucherName: String? = null,
@field:SerializedName("payment_status")
val paymentStatus: String? = null,
@field:SerializedName("address_id")
val addressId: Int,
@field:SerializedName("payment_amount")
val paymentAmount: String? = null,
@field:SerializedName("cancel_reason")
val cancelReason: String? = null,
@field:SerializedName("total_amount")
val totalAmount: String? = null,
@field:SerializedName("user_id")
val userId: Int,
@field:SerializedName("province_id")
val provinceId: Int,
@field:SerializedName("courier")
val courier: String,
@field:SerializedName("subdistrict")
val subdistrict: String,
@field:SerializedName("service")
val service: String,
@field:SerializedName("pay_info_num")
val payInfoNum: String? = null,
@field:SerializedName("shipment_price")
val shipmentPrice: String,
@field:SerializedName("voucher_id")
val voucherId: Int? = null,
@field:SerializedName("payment_info_id")
val paymentInfoId: Int? = null,
@field:SerializedName("detail")
val detail: String,
@field:SerializedName("postal_code")
val postalCode: String,
@field:SerializedName("order_id")
val orderId: Int,
@field:SerializedName("city_id")
val cityId: Int
)
data class OrderListItemsItem(
@field:SerializedName("order_item_id")
val orderItemId: Int,
@field:SerializedName("review_id")
val reviewId: Int? = null,
@ -26,7 +140,10 @@ data class OrderItemsItem(
val subtotal: Int,
@field:SerializedName("product_image")
val productImage: String? = null,
val productImage: String,
@field:SerializedName("product_id")
val productId: Int,
@field:SerializedName("store_name")
val storeName: String,
@ -37,93 +154,3 @@ data class OrderItemsItem(
@field:SerializedName("product_name")
val productName: String
)
data class Orders(
@field:SerializedName("receipt_num")
val receiptNum: String,
@field:SerializedName("latitude")
val latitude: String,
@field:SerializedName("created_at")
val createdAt: String,
@field:SerializedName("voucher_code")
val voucherCode: String? = null,
@field:SerializedName("updated_at")
val updatedAt: String,
@field:SerializedName("etd")
val etd: String,
@field:SerializedName("street")
val street: String,
@field:SerializedName("cancel_date")
val cancelDate: String,
@field:SerializedName("longitude")
val longitude: String,
@field:SerializedName("shipment_status")
val shipmentStatus: String,
@field:SerializedName("order_items")
val orderItems: List<OrderItemsItem>,
@field:SerializedName("auto_completed_at")
val autoCompletedAt: String,
@field:SerializedName("is_store_location")
val isStoreLocation: Boolean,
@field:SerializedName("voucher_name")
val voucherName: String? = null,
@field:SerializedName("address_id")
val addressId: Int,
@field:SerializedName("payment_method_id")
val paymentMethodId: Int,
@field:SerializedName("cancel_reason")
val cancelReason: String,
@field:SerializedName("total_amount")
val totalAmount: String,
@field:SerializedName("user_id")
val userId: Int,
@field:SerializedName("province_id")
val provinceId: Int,
@field:SerializedName("courier")
val courier: String,
@field:SerializedName("subdistrict")
val subdistrict: String,
@field:SerializedName("service")
val service: String,
@field:SerializedName("shipment_price")
val shipmentPrice: String,
@field:SerializedName("voucher_id")
val voucherId: Int? = null,
@field:SerializedName("detail")
val detail: String,
@field:SerializedName("postal_code")
val postalCode: String,
@field:SerializedName("order_id")
val orderId: Int,
@field:SerializedName("city_id")
val cityId: Int
)

View File

@ -11,81 +11,119 @@ data class OrderListResponse(
val message: String
)
data class OrdersItem(
data class OrderItemsItem(
@field:SerializedName("receipt_num")
val receiptNum: String,
@field:SerializedName("review_id")
val reviewId: Int? = null,
@field:SerializedName("latitude")
val latitude: String,
@field:SerializedName("quantity")
val quantity: Int,
@field:SerializedName("created_at")
val createdAt: String,
@field:SerializedName("price")
val price: Int,
@field:SerializedName("voucher_code")
val voucherCode: String? = null,
@field:SerializedName("subtotal")
val subtotal: Int,
@field:SerializedName("updated_at")
val updatedAt: String,
@field:SerializedName("product_image")
val productImage: String,
@field:SerializedName("street")
val street: String,
@field:SerializedName("store_name")
val storeName: String,
@field:SerializedName("longitude")
val longitude: String,
@field:SerializedName("product_price")
val productPrice: Int,
@field:SerializedName("shipment_status")
val shipmentStatus: String,
@field:SerializedName("order_items")
val orderItems: List<OrderItemsItem>,
@field:SerializedName("is_store_location")
val isStoreLocation: Boolean,
@field:SerializedName("voucher_name")
val voucherName: String? = null,
@field:SerializedName("address_id")
val addressId: Int,
@field:SerializedName("payment_method_id")
val paymentMethodId: Int,
@field:SerializedName("total_amount")
val totalAmount: String,
@field:SerializedName("user_id")
val userId: Int,
@field:SerializedName("province_id")
val provinceId: Int,
@field:SerializedName("courier")
val courier: String,
@field:SerializedName("subdistrict")
val subdistrict: String,
@field:SerializedName("service")
val service: String,
@field:SerializedName("shipment_price")
val shipmentPrice: String,
@field:SerializedName("voucher_id")
val voucherId: Int? = null,
@field:SerializedName("detail")
val detail: String,
@field:SerializedName("postal_code")
val postalCode: String,
@field:SerializedName("order_id")
val orderId: Int,
@field:SerializedName("city_id")
val cityId: Int
@field:SerializedName("product_name")
val productName: String
)
data class OrdersItem(
@field:SerializedName("receipt_num")
val receiptNum: Int? = null,
@field:SerializedName("latitude")
val latitude: String,
@field:SerializedName("created_at")
val createdAt: String,
@field:SerializedName("voucher_code")
val voucherCode: String? = null,
@field:SerializedName("updated_at")
val updatedAt: String,
@field:SerializedName("etd")
val etd: String,
@field:SerializedName("street")
val street: String,
@field:SerializedName("cancel_date")
val cancelDate: String? = null,
@field:SerializedName("longitude")
val longitude: String,
@field:SerializedName("shipment_status")
val shipmentStatus: String,
@field:SerializedName("order_items")
val orderItems: List<OrderItemsItem>,
@field:SerializedName("auto_completed_at")
val autoCompletedAt: String? = null,
@field:SerializedName("is_store_location")
val isStoreLocation: Boolean? = null,
@field:SerializedName("voucher_name")
val voucherName: String? = null,
@field:SerializedName("address_id")
val addressId: Int,
@field:SerializedName("cancel_reason")
val cancelReason: String? = null,
@field:SerializedName("total_amount")
val totalAmount: String,
@field:SerializedName("user_id")
val userId: Int,
@field:SerializedName("province_id")
val provinceId: Int,
@field:SerializedName("courier")
val courier: String,
@field:SerializedName("subdistrict")
val subdistrict: String,
@field:SerializedName("service")
val service: String,
@field:SerializedName("shipment_price")
val shipmentPrice: String,
@field:SerializedName("voucher_id")
val voucherId: Int? = null,
@field:SerializedName("payment_info_id")
val paymentInfoId: Int? = null,
@field:SerializedName("detail")
val detail: String,
@field:SerializedName("postal_code")
val postalCode: String,
@field:SerializedName("order_id")
val orderId: Int,
@field:SerializedName("city_id")
val cityId: Int
)

View File

@ -13,6 +13,8 @@ data class DetailStoreProductResponse(
data class PaymentInfoItem(
val id: Int = 1,
@field:SerializedName("qris_image")
val qrisImage: String,

View File

@ -1,6 +1,8 @@
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.CartItem
import com.alya.ecommerce_serang.data.api.dto.CompletedOrderRequest
import com.alya.ecommerce_serang.data.api.dto.CourierCostRequest
import com.alya.ecommerce_serang.data.api.dto.CreateAddressRequest
import com.alya.ecommerce_serang.data.api.dto.LoginRequest
@ -19,10 +21,15 @@ import com.alya.ecommerce_serang.data.api.response.auth.RegisterResponse
import com.alya.ecommerce_serang.data.api.response.customer.cart.AddCartResponse
import com.alya.ecommerce_serang.data.api.response.customer.cart.ListCartResponse
import com.alya.ecommerce_serang.data.api.response.customer.cart.UpdateCartResponse
import com.alya.ecommerce_serang.data.api.response.order.AddEvidenceResponse
import com.alya.ecommerce_serang.data.api.response.order.ComplaintResponse
import com.alya.ecommerce_serang.data.api.response.order.CompletedOrderResponse
import com.alya.ecommerce_serang.data.api.response.customer.order.CourierCostResponse
import com.alya.ecommerce_serang.data.api.response.customer.order.CreateOrderResponse
import com.alya.ecommerce_serang.data.api.response.customer.order.ListCityResponse
import com.alya.ecommerce_serang.data.api.response.customer.order.ListProvinceResponse
import com.alya.ecommerce_serang.data.api.response.order.OrderDetailResponse
import com.alya.ecommerce_serang.data.api.response.order.OrderListResponse
import com.alya.ecommerce_serang.data.api.response.customer.product.AllProductResponse
import com.alya.ecommerce_serang.data.api.response.customer.product.CategoryResponse
import com.alya.ecommerce_serang.data.api.response.customer.product.DetailStoreProductResponse
@ -38,12 +45,13 @@ import com.alya.ecommerce_serang.data.api.response.store.product.UpdateProductRe
import retrofit2.Call
import retrofit2.Response
import retrofit2.http.Body
import retrofit2.http.DELETE
import retrofit2.http.Field
import retrofit2.http.FormUrlEncoded
import retrofit2.http.GET
import retrofit2.http.Multipart
import retrofit2.http.POST
import retrofit2.http.Part
import retrofit2.http.PUT
import retrofit2.http.Part
import retrofit2.http.Path
import retrofit2.http.Query
@ -93,6 +101,29 @@ interface ApiService {
@Body request: OrderRequest
): Response<CreateOrderResponse>
@GET("order/detail/{id}")
suspend fun getDetailOrder(
@Path("id") orderId: Int
): Response<OrderDetailResponse>
@POST("order/addevidence")
suspend fun addEvidence(
@Body request : AddEvidenceRequest,
): Response<AddEvidenceResponse>
@Multipart
@POST("order/addevidence")
suspend fun addEvidenceMultipart(
@Part("order_id") orderId: RequestBody,
@Part("amount") amount: RequestBody,
@Part evidence: MultipartBody.Part
): Response<AddEvidenceResponse>
@GET("order/{status}")
suspend fun getOrderList(
@Path("status") status: String
):Response<OrderListResponse>
@POST("order")
suspend fun postOrderBuyNow(
@Body request: OrderRequestBuy
@ -180,4 +211,29 @@ interface ApiService {
suspend fun getOrdersByStatus(
@Query("status") status: String
): Response<OrderListResponse>
@PUT("store/order/update")
suspend fun confirmOrder(
@Body confirmOrder : CompletedOrderRequest
): Response<CompletedOrderResponse>
@Multipart
@POST("addcomplaint")
suspend fun addComplaint(
@Part("order_id") orderId: RequestBody,
@Part("description") description: RequestBody,
@Part complaintimg: MultipartBody.Part
): Response<ComplaintResponse>
@PUT("store/order/update")
suspend fun confirmOrder(
@Body confirmOrder : CompletedOrderRequest
): Response<CompletedOrderResponse>
@Multipart
@POST("addcomplaint")
suspend fun addComplaint(
@Part("order_id") orderId: RequestBody,
@Part("description") description: RequestBody,
@Part complaintimg: MultipartBody.Part
): Response<ComplaintResponse>
}

View File

@ -1,24 +1,39 @@
package com.alya.ecommerce_serang.data.repository
import android.util.Log
import com.alya.ecommerce_serang.data.api.dto.AddEvidenceMultipartRequest
import com.alya.ecommerce_serang.data.api.dto.CompletedOrderRequest
import com.alya.ecommerce_serang.data.api.dto.CourierCostRequest
import com.alya.ecommerce_serang.data.api.dto.CreateAddressRequest
import com.alya.ecommerce_serang.data.api.dto.OrderRequest
import com.alya.ecommerce_serang.data.api.dto.OrderRequestBuy
import com.alya.ecommerce_serang.data.api.dto.OrdersItem
import com.alya.ecommerce_serang.data.api.dto.ProductsItem
import com.alya.ecommerce_serang.data.api.response.customer.cart.DataItem
import com.alya.ecommerce_serang.data.api.response.customer.order.CourierCostResponse
import com.alya.ecommerce_serang.data.api.response.customer.order.CreateOrderResponse
import com.alya.ecommerce_serang.data.api.response.customer.order.ListCityResponse
import com.alya.ecommerce_serang.data.api.response.customer.order.ListProvinceResponse
import com.alya.ecommerce_serang.data.api.response.customer.product.ProductResponse
import com.alya.ecommerce_serang.data.api.response.customer.product.StoreProduct
import com.alya.ecommerce_serang.data.api.response.customer.product.StoreResponse
import com.alya.ecommerce_serang.data.api.response.customer.profile.AddressResponse
import com.alya.ecommerce_serang.data.api.response.customer.profile.CreateAddressResponse
import com.alya.ecommerce_serang.data.api.dto.UserProfile
import com.alya.ecommerce_serang.data.api.response.cart.DataItem
import com.alya.ecommerce_serang.data.api.response.order.AddEvidenceResponse
import com.alya.ecommerce_serang.data.api.response.order.ComplaintResponse
import com.alya.ecommerce_serang.data.api.response.order.CompletedOrderResponse
import com.alya.ecommerce_serang.data.api.response.order.CourierCostResponse
import com.alya.ecommerce_serang.data.api.response.order.CreateOrderResponse
import com.alya.ecommerce_serang.data.api.response.order.ListCityResponse
import com.alya.ecommerce_serang.data.api.response.order.ListProvinceResponse
import com.alya.ecommerce_serang.data.api.response.order.OrderDetailResponse
import com.alya.ecommerce_serang.data.api.response.order.OrderListResponse
import com.alya.ecommerce_serang.data.api.response.product.ProductResponse
import com.alya.ecommerce_serang.data.api.response.product.StoreProduct
import com.alya.ecommerce_serang.data.api.response.product.StoreResponse
import com.alya.ecommerce_serang.data.api.response.profile.AddressResponse
import com.alya.ecommerce_serang.data.api.response.profile.CreateAddressResponse
import com.alya.ecommerce_serang.data.api.retrofit.ApiService
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOn
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.MultipartBody
import okhttp3.RequestBody.Companion.asRequestBody
import okhttp3.RequestBody.Companion.toRequestBody
import retrofit2.Response
import java.io.File
class OrderRepository(private val apiService: ApiService) {
@ -183,18 +198,27 @@ class OrderRepository(private val apiService: ApiService) {
}
}
suspend fun addAddress(createAddressRequest: CreateAddressRequest): Result<CreateAddressResponse> {
suspend fun addAddress(request: CreateAddressRequest): Result<CreateAddressResponse> {
return try {
val response = apiService.createAddress(createAddressRequest)
if (response.isSuccessful){
response.body()?.let {
Result.Success(it)
} ?: Result.Error(Exception("Add Address failed"))
Log.d("OrderRepository", "Adding address: $request")
val response = apiService.createAddress(request)
if (response.isSuccessful) {
val createAddressResponse = response.body()
if (createAddressResponse != null) {
Log.d("OrderRepository", "Address added successfully: ${createAddressResponse.message}")
Result.Success(createAddressResponse)
} else {
Log.e("OrderRepository", "Response body was null")
Result.Error(Exception("Empty response from server"))
}
} else {
Log.e("OrderRepository", "Error: ${response.errorBody()?.string()}")
Result.Error(Exception(response.errorBody()?.string() ?: "Unknown error"))
val errorBody = response.errorBody()?.string() ?: "Unknown error"
Log.e("OrderRepository", "Error adding address: $errorBody")
Result.Error(Exception(errorBody))
}
} catch (e: Exception) {
Log.e("OrderRepository", "Exception adding address", e)
Result.Error(e)
}
}
@ -238,4 +262,203 @@ class OrderRepository(private val apiService: ApiService) {
emptyList()
}
}
suspend fun fetchUserProfile(): Result<UserProfile?> {
return try {
val response = apiService.getUserProfile()
if (response.isSuccessful) {
response.body()?.user?.let {
Result.Success(it) // ✅ Returning only UserProfile
} ?: Result.Error(Exception("User data not found"))
} else {
Result.Error(Exception("Error fetching profile: ${response.code()}"))
}
} catch (e: Exception) {
Result.Error(e)
}
}
suspend fun getOrderDetails(orderId: Int): OrderDetailResponse? {
return try {
val response = apiService.getDetailOrder(orderId)
if (response.isSuccessful) response.body() else null
} catch (e: Exception) {
Log.e("OrderRepository", "Error getting order details", e)
null
}
}
// suspend fun uploadPaymentProof(request : AddEvidenceRequest): Result<AddEvidenceResponse> {
// return try {
// Log.d("OrderRepository", "Add Evidence : $request")
// val response = apiService.addEvidence(request)
//
// if (response.isSuccessful) {
// val addEvidenceResponse = response.body()
// if (addEvidenceResponse != null) {
// Log.d("OrderRepository", "Add Evidence successfully: ${addEvidenceResponse.message}")
// Result.Success(addEvidenceResponse)
// } else {
// Log.e("OrderRepository", "Response body was null")
// Result.Error(Exception("Empty response from server"))
// }
// } else {
// val errorBody = response.errorBody()?.string() ?: "Unknown error"
// Log.e("OrderRepository", "Error Add Evidence : $errorBody")
// Result.Error(Exception(errorBody))
// }
// } catch (e: Exception) {
// Log.e("OrderRepository", "Exception Add Evidence ", e)
// Result.Error(e)
// }
// }
suspend fun uploadPaymentProof(request: AddEvidenceMultipartRequest): Result<AddEvidenceResponse> {
return try {
Log.d("OrderRepository", "Uploading payment proof...")
val response = apiService.addEvidenceMultipart(
orderId = request.orderId,
amount = request.amount,
evidence = request.evidence
)
if (response.isSuccessful) {
val addEvidenceResponse = response.body()
if (addEvidenceResponse != null) {
Log.d("OrderRepository", "Payment proof uploaded successfully: ${addEvidenceResponse.message}")
Result.Success(addEvidenceResponse)
} else {
Log.e("OrderRepository", "Response body was null")
Result.Error(Exception("Empty response from server"))
}
} else {
val errorBody = response.errorBody()?.string() ?: "Unknown error"
Log.e("OrderRepository", "Error uploading payment proof: $errorBody")
Result.Error(Exception(errorBody))
}
} catch (e: Exception) {
Log.e("OrderRepository", "Exception uploading payment proof", e)
Result.Error(e)
}
}
suspend fun getOrderList(status: String): Result<OrderListResponse> {
return try {
Log.d("OrderRepository", "Add Evidence : $status")
val response = apiService.getOrderList(status)
if (response.isSuccessful) {
val allListOrder = response.body()
if (allListOrder != null) {
Log.d("OrderRepository", "Add Evidence successfully: ${allListOrder.message}")
Result.Success(allListOrder)
} else {
Log.e("OrderRepository", "Response body was null")
Result.Error(Exception("Empty response from server"))
}
} else {
val errorBody = response.errorBody()?.string() ?: "Unknown error"
Log.e("OrderRepository", "Error Add Evidence : $errorBody")
Result.Error(Exception(errorBody))
}
} catch (e: Exception) {
Log.e("OrderRepository", "Exception Add Evidence ", e)
Result.Error(e)
}
}
suspend fun confirmOrderCompleted(request: CompletedOrderRequest): Result<CompletedOrderResponse> {
return try {
Log.d("OrderRepository", "Cinfroming order request completed: $request")
val response = apiService.confirmOrder(request)
if(response.isSuccessful) {
val completedOrderResponse = response.body()
if (completedOrderResponse != null) {
Log.d("OrderRepository", "Order confirmed successfully: ${completedOrderResponse.message}")
Result.Success(completedOrderResponse)
} else {
Log.e("OrderRepository", "Response body was null")
Result.Error(Exception("Empty response from server"))
}
} else {
val errorBody = response.errorBody()?.string() ?: "Unknown Error"
Log.e("OrderRepository", "Error confirming order: $errorBody")
Result.Error(Exception(errorBody))
}
} catch (e: Exception){
Result.Error(e)
}
}
fun submitComplaint(
orderId: String,
reason: String,
imageFile: File?
): Flow<Result<ComplaintResponse>> = flow {
emit(Result.Loading)
try {
// Debug logging
Log.d("OrderRepository", "Submitting complaint for order: $orderId")
Log.d("OrderRepository", "Reason: $reason")
Log.d("OrderRepository", "Image file: ${imageFile?.absolutePath ?: "null"}")
// Create form data for the multipart request
// Explicitly convert orderId to string to ensure correct formatting
val orderIdRequestBody = orderId.toString().toRequestBody("text/plain".toMediaTypeOrNull())
val reasonRequestBody = reason.toRequestBody("text/plain".toMediaTypeOrNull())
// Create the image part for the API
val imagePart = if (imageFile != null && imageFile.exists()) {
// Use the actual image file
// Use asRequestBody() for files which is more efficient
val imageRequestBody = imageFile.asRequestBody("image/*".toMediaTypeOrNull())
MultipartBody.Part.createFormData(
"complaintimg",
imageFile.name,
imageRequestBody
)
} else {
// Create a simple empty part if no image
val dummyRequestBody = "".toRequestBody("text/plain".toMediaTypeOrNull())
MultipartBody.Part.createFormData(
"complaintimg",
"",
dummyRequestBody
)
}
// Log request details before making the API call
Log.d("OrderRepository", "Making API call to add complaint")
Log.d("OrderRepository", "orderId: $orderId (as string)")
val response = apiService.addComplaint(
orderIdRequestBody,
reasonRequestBody,
imagePart
)
Log.d("OrderRepository", "Response code: ${response.code()}")
Log.d("OrderRepository", "Response message: ${response.message()}")
if (response.isSuccessful && response.body() != null) {
val complaintResponse = response.body() as ComplaintResponse
emit(Result.Success(complaintResponse))
} else {
// Get the error message from the response if possible
val errorBody = response.errorBody()?.string()
val errorMessage = if (!errorBody.isNullOrEmpty()) {
"Server error: $errorBody"
} else {
"Failed to submit complaint: ${response.code()} ${response.message()}"
}
Log.e("OrderRepository", errorMessage)
emit(Result.Error(Exception(errorMessage)))
}
} catch (e: Exception) {
Log.e("OrderRepository", "Error submitting complaint: ${e.message}")
emit(Result.Error(e))
}
}.flowOn(Dispatchers.IO)
}

View File

@ -3,11 +3,15 @@ package com.alya.ecommerce_serang.ui.order
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.util.Log
import android.view.ViewGroup
import android.widget.TextView
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.alya.ecommerce_serang.data.api.dto.CheckoutData
import com.alya.ecommerce_serang.data.api.dto.OrderRequest
import com.alya.ecommerce_serang.data.api.dto.OrderRequestBuy
@ -42,6 +46,7 @@ class CheckoutActivity : AppCompatActivity() {
sessionManager = SessionManager(this)
// Setup UI components
setupToolbar()
setupObservers()
@ -74,6 +79,11 @@ class CheckoutActivity : AppCompatActivity() {
finish()
}
}
viewModel.getPaymentMethods { paymentMethods ->
// Logging is just for debugging
Log.d("CheckoutActivity", "Loaded ${paymentMethods.size} payment methods")
}
}
private fun setupToolbar() {
@ -87,13 +97,6 @@ class CheckoutActivity : AppCompatActivity() {
viewModel.checkoutData.observe(this) { data ->
setupProductRecyclerView(data)
updateOrderSummary()
// Load payment methods
viewModel.getPaymentMethods { paymentMethods ->
if (paymentMethods.isNotEmpty()) {
setupPaymentMethodsRecyclerView(paymentMethods)
}
}
}
// Observe address details
@ -102,14 +105,24 @@ class CheckoutActivity : AppCompatActivity() {
binding.tvAddress.text = "${address?.street}, ${address?.subdistrict}"
}
// Observe payment details
viewModel.paymentDetails.observe(this) { payment ->
if (payment != null) {
// Update selected payment in adapter by name instead of ID
paymentAdapter?.setSelectedPaymentName(payment.name)
viewModel.availablePaymentMethods.observe(this) { paymentMethods ->
if (paymentMethods.isNotEmpty()) {
setupPaymentMethodsRecyclerView(paymentMethods)
}
}
// Observe selected payment
viewModel.selectedPayment.observe(this) { selectedPayment ->
if (selectedPayment != null) {
// Update the adapter to show the selected payment
paymentAdapter?.setSelectedPaymentName(selectedPayment.name)
// Optional: Update other UI elements to show the selected payment
// For example: binding.tvSelectedPaymentMethod.text = selectedPayment.name
}
}
// Observe loading state
viewModel.isLoading.observe(this) { isLoading ->
binding.btnPay.isEnabled = !isLoading
@ -133,6 +146,53 @@ class CheckoutActivity : AppCompatActivity() {
}
}
private fun setupPaymentMethodsRecyclerView(paymentMethods: List<PaymentInfoItem>) {
if (paymentMethods.isEmpty()) {
Log.e("CheckoutActivity", "Payment methods list is empty")
Toast.makeText(this, "No payment methods available", Toast.LENGTH_SHORT).show()
return
}
// Debug logging
Log.d("CheckoutActivity", "Setting up payment methods: ${paymentMethods.size} methods available")
paymentAdapter = PaymentMethodAdapter(paymentMethods) { payment ->
// We're using a hardcoded ID for now
viewModel.setPaymentMethod(1)
}
binding.rvPaymentMethods.apply {
layoutManager = LinearLayoutManager(this@CheckoutActivity)
adapter = paymentAdapter
}
}
private fun updatePaymentMethodsAdapter(paymentMethods: List<PaymentInfoItem>, selectedId: Int?) {
Log.d("CheckoutActivity", "Updating payment adapter with ${paymentMethods.size} methods")
// Simple test adapter
val testAdapter = object : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val textView = TextView(parent.context)
textView.setPadding(16, 16, 16, 16)
textView.textSize = 16f
return object : RecyclerView.ViewHolder(textView) {}
}
override fun getItemCount() = paymentMethods.size
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
val payment = paymentMethods[position]
(holder.itemView as TextView).text = "Payment: ${payment.name}"
}
}
binding.rvPaymentMethods.apply {
layoutManager = LinearLayoutManager(this@CheckoutActivity)
adapter = testAdapter
}
}
private fun setupProductRecyclerView(checkoutData: CheckoutData) {
val adapter = if (checkoutData.isBuyNow || checkoutData.cartItems.size <= 1) {
CheckoutSellerAdapter(checkoutData)
@ -147,21 +207,6 @@ class CheckoutActivity : AppCompatActivity() {
}
}
private fun setupPaymentMethodsRecyclerView(paymentMethods: List<PaymentInfoItem>) {
paymentAdapter = PaymentMethodAdapter(paymentMethods) { payment ->
// When a payment method is selected
// Since PaymentInfoItem doesn't have an id field, we'll use the name as identifier
// You might need to convert the name to an ID if your backend expects an integer
val paymentId = payment.name.toIntOrNull() ?: 0
viewModel.setPaymentMethod(paymentId)
}
binding.rvPaymentMethods.apply {
layoutManager = LinearLayoutManager(this@CheckoutActivity)
adapter = paymentAdapter
}
}
private fun updateOrderSummary() {
viewModel.checkoutData.value?.let { data ->
// Update price information
@ -251,6 +296,9 @@ class CheckoutActivity : AppCompatActivity() {
val addressId = result.data?.getIntExtra(AddressActivity.EXTRA_ADDRESS_ID, 0) ?: 0
if (addressId > 0) {
viewModel.setSelectedAddress(addressId)
// You might want to show a toast or some UI feedback
Toast.makeText(this, "Address selected successfully", Toast.LENGTH_SHORT).show()
}
}
}
@ -299,7 +347,7 @@ class CheckoutActivity : AppCompatActivity() {
}
// Check if payment method is selected
if (viewModel.paymentDetails.value == null) {
if (viewModel.selectedPayment.value == null) {
Toast.makeText(this, "Silakan pilih metode pembayaran", Toast.LENGTH_SHORT).show()
return false
}

View File

@ -24,8 +24,12 @@ class CheckoutViewModel(private val repository: OrderRepository) : ViewModel() {
private val _addressDetails = MutableLiveData<AddressesItem?>()
val addressDetails: LiveData<AddressesItem?> = _addressDetails
private val _paymentDetails = MutableLiveData<PaymentInfoItem?>()
val paymentDetails: LiveData<PaymentInfoItem?> = _paymentDetails
private val _availablePaymentMethods = MutableLiveData<List<PaymentInfoItem>>()
val availablePaymentMethods: LiveData<List<PaymentInfoItem>> = _availablePaymentMethods
// Selected payment method
private val _selectedPayment = MutableLiveData<PaymentInfoItem?>()
val selectedPayment: LiveData<PaymentInfoItem?> = _selectedPayment
private val _isLoading = MutableLiveData<Boolean>()
val isLoading: LiveData<Boolean> = _isLoading
@ -144,7 +148,6 @@ class CheckoutViewModel(private val repository: OrderRepository) : ViewModel() {
}
}
// Get payment methods from API
fun getPaymentMethods(callback: (List<PaymentInfoItem>) -> Unit) {
viewModelScope.launch {
try {
@ -154,17 +157,78 @@ class CheckoutViewModel(private val repository: OrderRepository) : ViewModel() {
val storeResult = repository.fetchStoreDetail(storeId)
if (storeResult is Result.Success && storeResult.data != null) {
callback(storeResult.data.paymentInfo)
// For now, we'll use hardcoded payment ID (1) for all payment methods
// This will be updated once the backend provides proper IDs
val paymentMethodsList = storeResult.data.paymentInfo.map { paymentInfo ->
PaymentInfoItem(
id = 1, // Hardcoded payment ID
name = paymentInfo.name,
bankNum = paymentInfo.bankNum,
qrisImage = paymentInfo.qrisImage
)
}
Log.d(TAG, "Fetched ${paymentMethodsList.size} payment methods")
_availablePaymentMethods.value = paymentMethodsList
callback(paymentMethodsList)
} else {
_availablePaymentMethods.value = emptyList()
callback(emptyList())
}
} catch (e: Exception) {
Log.e(TAG, "Error fetching payment methods", e)
_availablePaymentMethods.value = emptyList()
callback(emptyList())
}
}
}
// Updated setPaymentMethod function
fun setPaymentMethod(paymentId: Int) {
// We'll use the hardcoded ID (1) for now
val currentPaymentId = 1
viewModelScope.launch {
try {
// Get the available payment methods
val paymentMethods = _availablePaymentMethods.value
if (paymentMethods.isNullOrEmpty()) {
// If no payment methods available, try to fetch them
getPaymentMethods { /* do nothing here */ }
return@launch
}
// Use the first payment method (or specific one if you prefer)
val selectedPayment = paymentMethods.first()
// Set the selected payment
_selectedPayment.value = selectedPayment
Log.d(TAG, "Payment selected: Name=${selectedPayment.name}")
// Update the order request with the payment method ID (hardcoded for now)
val currentData = _checkoutData.value ?: return@launch
// Different handling for Buy Now vs Cart checkout
if (currentData.isBuyNow) {
// For Buy Now checkout
val buyRequest = currentData.orderRequest as OrderRequestBuy
val updatedRequest = buyRequest.copy(paymentMethodId = currentPaymentId)
_checkoutData.value = currentData.copy(orderRequest = updatedRequest)
} else {
// For Cart checkout
val cartRequest = currentData.orderRequest as OrderRequest
val updatedRequest = cartRequest.copy(paymentMethodId = currentPaymentId)
_checkoutData.value = currentData.copy(orderRequest = updatedRequest)
}
} catch (e: Exception) {
_errorMessage.value = "Error setting payment method: ${e.message}"
Log.e(TAG, "Error setting payment method", e)
}
}
}
// Set selected address
fun setSelectedAddress(addressId: Int) {
viewModelScope.launch {
@ -227,39 +291,6 @@ class CheckoutViewModel(private val repository: OrderRepository) : ViewModel() {
}
}
// Set payment method
fun setPaymentMethod(paymentId: Int) {
viewModelScope.launch {
try {
val storeId = _checkoutData.value?.sellerId ?: return@launch
// Use fetchStoreDetail instead of getStore
val storeResult = repository.fetchStoreDetail(storeId)
if (storeResult is Result.Success && storeResult.data != null) {
// Find the selected payment in the payment info list
val payment = storeResult.data.paymentInfo.find { it.name == paymentId.toString() }
_paymentDetails.value = payment
// Update order request if payment isn't null
if (payment != null) {
val currentData = _checkoutData.value ?: return@launch
if (currentData.isBuyNow) {
val buyRequest = currentData.orderRequest as OrderRequestBuy
val updatedRequest = buyRequest.copy(paymentMethodId = paymentId)
_checkoutData.value = currentData.copy(orderRequest = updatedRequest)
} else {
val cartRequest = currentData.orderRequest as OrderRequest
val updatedRequest = cartRequest.copy(paymentMethodId = paymentId)
_checkoutData.value = currentData.copy(orderRequest = updatedRequest)
}
}
}
} catch (e: Exception) {
_errorMessage.value = "Error setting payment method: ${e.message}"
}
}
}
// Create order
fun createOrder() {
viewModelScope.launch {

View File

@ -14,8 +14,8 @@ class PaymentMethodAdapter(
private val onPaymentSelected: (PaymentInfoItem) -> Unit
) : RecyclerView.Adapter<PaymentMethodAdapter.PaymentMethodViewHolder>() {
// Track the selected position
private var selectedPosition = -1
// Selected payment name
private var selectedPaymentName: String? = null
class PaymentMethodViewHolder(val binding: ItemPaymentMethodBinding) :
RecyclerView.ViewHolder(binding.root)
@ -38,14 +38,23 @@ class PaymentMethodAdapter(
// Set payment method name
tvPaymentMethodName.text = payment.name
// Set radio button state
rbPaymentMethod.isChecked = selectedPosition == position
// // Set bank account number if available
// if (!payment.bankNum.isNullOrEmpty()) {
// tvPaymentAccountNumber.visibility = View.VISIBLE
// tvPaymentAccountNumber.text = payment.bankNum
// } else {
// tvPaymentAccountNumber.visibility = View.GONE
// }
// Set radio button state based on selected payment name
rbPaymentMethod.isChecked = payment.name == selectedPaymentName
// Load payment icon if available
if (payment.qrisImage.isNotEmpty()) {
if (!payment.qrisImage.isNullOrEmpty()) {
Glide.with(ivPaymentMethod.context)
.load(payment.qrisImage)
.apply(RequestOptions()
.apply(
RequestOptions()
.placeholder(R.drawable.outline_store_24)
.error(R.drawable.outline_store_24))
.into(ivPaymentMethod)
@ -56,35 +65,21 @@ class PaymentMethodAdapter(
// Handle click on the entire item
root.setOnClickListener {
selectPayment(position)
onPaymentSelected(payment)
setSelectedPaymentName(payment.name)
}
// Handle click on the radio button
rbPaymentMethod.setOnClickListener {
selectPayment(position)
onPaymentSelected(payment)
setSelectedPaymentName(payment.name)
}
}
}
// Helper method to handle payment selection
private fun selectPayment(position: Int) {
if (selectedPosition != position) {
val previousPosition = selectedPosition
selectedPosition = position
// Update UI for previous and new selection
notifyItemChanged(previousPosition)
notifyItemChanged(position)
}
}
//selected by name
// Set selected payment by name and refresh the UI
fun setSelectedPaymentName(paymentName: String) {
val position = paymentMethods.indexOfFirst { it.name == paymentName }
if (position != -1 && position != selectedPosition) {
selectPayment(position)
}
selectedPaymentName = paymentName
notifyDataSetChanged() // Update all items to reflect selection change
}
}

View File

@ -2,10 +2,11 @@ package com.alya.ecommerce_serang.ui.order
import android.content.Intent
import android.os.Bundle
import android.util.Log
import android.view.View
import android.widget.Toast
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.isVisible
import androidx.recyclerview.widget.LinearLayoutManager
import com.alya.ecommerce_serang.data.api.retrofit.ApiConfig
import com.alya.ecommerce_serang.data.repository.OrderRepository
@ -18,6 +19,7 @@ class ShippingActivity : AppCompatActivity() {
private lateinit var binding: ActivityShippingBinding
private lateinit var sessionManager: SessionManager
private lateinit var shippingAdapter: ShippingAdapter
private val TAG = "ShippingActivity"
private val viewModel: ShippingViewModel by viewModels {
BaseViewModelFactory {
@ -40,8 +42,11 @@ class ShippingActivity : AppCompatActivity() {
val productId = intent.getIntExtra(EXTRA_PRODUCT_ID, 0)
val quantity = intent.getIntExtra(EXTRA_QUANTITY, 1)
Log.d(TAG, "Received data: addressId=$addressId, productId=$productId, quantity=$quantity")
// Validate required information
if (addressId <= 0 || productId <= 0) {
Log.e(TAG, "Missing required shipping information: addressId=$addressId, productId=$productId")
Toast.makeText(this, "Missing required shipping information", Toast.LENGTH_SHORT).show()
finish()
return
@ -51,9 +56,10 @@ class ShippingActivity : AppCompatActivity() {
setupToolbar()
setupRecyclerView()
setupObservers()
setupRetryButton() // Add a retry button for error cases
// Load shipping options
viewModel.loadShippingOptions(addressId, productId, quantity)
loadShippingOptions(addressId, productId, quantity)
}
private fun setupToolbar() {
@ -65,6 +71,7 @@ class ShippingActivity : AppCompatActivity() {
private fun setupRecyclerView() {
shippingAdapter = ShippingAdapter { courierCostsItem, service ->
// Handle shipping method selection
Log.d(TAG, "Selected shipping: ${courierCostsItem.courier} - ${service.service} - ${service.cost} - ${service.etd}")
returnSelectedShipping(
courierCostsItem.courier,
service.service,
@ -79,29 +86,65 @@ class ShippingActivity : AppCompatActivity() {
}
}
private fun setupRetryButton() {
// If you have a retry button in your layout
// binding.btnRetry?.setOnClickListener {
// val addressId = intent.getIntExtra(EXTRA_ADDRESS_ID, 0)
// val productId = intent.getIntExtra(EXTRA_PRODUCT_ID, 0)
// val quantity = intent.getIntExtra(EXTRA_QUANTITY, 1)
// loadShippingOptions(addressId, productId, quantity)
// }
}
private fun loadShippingOptions(addressId: Int, productId: Int, quantity: Int) {
// Show loading state
binding.progressBar?.visibility = View.VISIBLE
binding.rvShipmentOrder.visibility = View.GONE
// binding.layoutEmptyShipping?.visibility = View.GONE
// Load shipping options
Log.d(TAG, "Loading shipping options: addressId=$addressId, productId=$productId, quantity=$quantity")
viewModel.loadShippingOptions(addressId, productId, quantity)
}
private fun setupObservers() {
// Observe shipping options
viewModel.shippingOptions.observe(this) { courierOptions ->
Log.d(TAG, "Received ${courierOptions.size} shipping options")
shippingAdapter.submitList(courierOptions)
updateEmptyState(courierOptions.isEmpty() || courierOptions.all { it.services.isEmpty() })
}
// Observe loading state
viewModel.isLoading.observe(this) { isLoading ->
// binding.progressBar.isVisible = isLoading
binding.progressBar?.visibility = if (isLoading) View.VISIBLE else View.GONE
Log.d(TAG, "Loading state: $isLoading")
}
// Observe error messages
viewModel.errorMessage.observe(this) { message ->
if (message.isNotEmpty()) {
Log.e(TAG, "Error: $message")
Toast.makeText(this, message, Toast.LENGTH_SHORT).show()
// Show error view if you have one
// binding.layoutError?.visibility = View.VISIBLE
// binding.tvErrorMessage?.text = message
} else {
// binding.layoutError?.visibility = View.GONE
}
}
}
private fun updateEmptyState(isEmpty: Boolean) {
// binding.layoutEmptyShipping.isVisible = isEmpty
binding.rvShipmentOrder.isVisible = !isEmpty
Log.d(TAG, "Updating empty state: isEmpty=$isEmpty")
// binding.layoutEmptyShipping?.visibility = if (isEmpty) View.VISIBLE else View.GONE
binding.rvShipmentOrder.visibility = if (isEmpty) View.GONE else View.VISIBLE
// If empty, show appropriate message
if (isEmpty) {
// binding.tvEmptyMessage?.text = "No shipping options available for this address and product"
}
}
private fun returnSelectedShipping(
@ -116,11 +159,13 @@ class ShippingActivity : AppCompatActivity() {
putExtra(EXTRA_SHIP_PRICE, shipPrice)
putExtra(EXTRA_SHIP_ETD, shipEtd)
}
Log.d(TAG, "Returning selected shipping: name=$shipName, service=$shipService, price=$shipPrice, etd=$shipEtd")
setResult(RESULT_OK, intent)
finish()
}
companion object {
// Constants for intent extras
const val EXTRA_ADDRESS_ID = "extra_address_id"
const val EXTRA_PRODUCT_ID = "extra_product_id"

View File

@ -36,12 +36,14 @@ class ShippingViewModel(
_errorMessage.value = ""
// Prepare the request
val costProduct = CostProduct(
productId = productId,
quantity = quantity
)
val request = CourierCostRequest(
addressId = addressId,
itemCost = CostProduct(
productId = productId,
quantity = quantity
)
itemCost = listOf(costProduct) // Wrap in a list
)
viewModelScope.launch {

View File

@ -1,18 +1,22 @@
package com.alya.ecommerce_serang.ui.order.address
import android.annotation.SuppressLint
import android.content.pm.PackageManager
import android.content.Intent
import android.location.Criteria
import android.location.Location
import android.location.LocationListener
import android.location.LocationManager
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.provider.Settings
import android.util.Log
import android.view.View
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import com.alya.ecommerce_serang.data.api.dto.CreateAddressRequest
import com.alya.ecommerce_serang.data.api.dto.UserProfile
import com.alya.ecommerce_serang.data.api.response.customer.order.CitiesItem
@ -20,18 +24,20 @@ import com.alya.ecommerce_serang.data.api.response.customer.order.ProvincesItem
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.OrderRepository
import com.alya.ecommerce_serang.data.repository.UserRepository
import com.alya.ecommerce_serang.databinding.ActivityAddAddressBinding
import com.alya.ecommerce_serang.utils.SavedStateViewModelFactory
import com.alya.ecommerce_serang.utils.SessionManager
import kotlinx.coroutines.launch
class AddAddressActivity : AppCompatActivity() {
private lateinit var binding: ActivityAddAddressBinding
private lateinit var apiService: ApiService
private lateinit var sessionManager: SessionManager
private lateinit var profileUser: UserProfile
private var profileUser: Int = 1
private lateinit var locationManager: LocationManager
private var isRequestingLocation = false
private var latitude: Double? = null
private var longitude: Double? = null
private val provinceAdapter by lazy { ProvinceAdapter(this) }
@ -41,7 +47,8 @@ class AddAddressActivity : AppCompatActivity() {
SavedStateViewModelFactory(this) { savedStateHandle ->
val apiService = ApiConfig.getApiService(sessionManager)
val orderRepository = OrderRepository(apiService)
AddAddressViewModel(orderRepository, savedStateHandle)
val userRepository = UserRepository(apiService)
AddAddressViewModel(orderRepository, userRepository, savedStateHandle)
}
}
@ -54,45 +61,79 @@ class AddAddressActivity : AppCompatActivity() {
apiService = ApiConfig.getApiService(sessionManager)
locationManager = getSystemService(LOCATION_SERVICE) as LocationManager
// Get user profile from session manager
// profileUser =UserProfile.
viewModel.userProfile.observe(this){ user ->
user?.let { updateProfile(it) }
}
setupToolbar()
requestLocationPermission()
setupReloadButtons()
setupAutoComplete()
setupButtonListeners()
collectFlows()
requestLocationPermission()
setupObservers()
// Force trigger province loading to ensure it happens
viewModel.getProvinces()
}
private fun updateProfile(userProfile: UserProfile){
profileUser = userProfile.userId
}
// private fun viewModelAddAddress(request: CreateAddressRequest) {
// // Call the private fun in your ViewModel using reflection or expose it in ViewModel
// val method = AddAddressViewModel::class.java.getDeclaredMethod("addAddress", CreateAddressRequest::class.java)
// method.isAccessible = true
// method.invoke(viewModel, request)
// }
// UI setup methods
private fun setupToolbar() {
binding.toolbar.setNavigationOnClickListener {
onBackPressedDispatcher.onBackPressed()
private fun setupToolbar() {
binding.toolbar.setNavigationOnClickListener {
onBackPressedDispatcher.onBackPressed()
}
}
}
private fun setupAutoComplete() {
Log.d(TAG, "Setting up AutoComplete dropdowns")
// Set adapters
binding.autoCompleteProvinsi.setAdapter(provinceAdapter)
binding.autoCompleteKabupaten.setAdapter(cityAdapter)
// Set listeners
binding.autoCompleteProvinsi.setOnItemClickListener { _, _, position, _ ->
provinceAdapter.getProvinceId(position)?.let { provinceId ->
viewModel.getCities(provinceId)
binding.autoCompleteKabupaten.text.clear()
// Make dropdown appear on click (not just when typing)
binding.autoCompleteProvinsi.setOnClickListener {
Log.d(TAG, "Province dropdown clicked, showing dropdown")
binding.autoCompleteProvinsi.showDropDown()
}
binding.autoCompleteKabupaten.setOnClickListener {
// Only show dropdown if we have cities loaded
if (cityAdapter.count > 0) {
Log.d(TAG, "City dropdown clicked, showing dropdown with ${cityAdapter.count} items")
binding.autoCompleteKabupaten.showDropDown()
} else {
Log.d(TAG, "City dropdown clicked but no cities available")
Toast.makeText(this, "Pilih provinsi terlebih dahulu", Toast.LENGTH_SHORT).show()
}
}
// Set listeners for selection
binding.autoCompleteProvinsi.setOnItemClickListener { _, _, position, _ ->
val provinceId = provinceAdapter.getProvinceId(position)
Log.d(TAG, "Province selected at position $position, provinceId=$provinceId")
provinceId?.let { id ->
Log.d(TAG, "Getting cities for provinceId=$id")
viewModel.getCities(id)
binding.autoCompleteKabupaten.text.clear()
} ?: Log.e(TAG, "Could not get provinceId for position $position")
}
binding.autoCompleteKabupaten.setOnItemClickListener { _, _, position, _ ->
cityAdapter.getCityId(position)?.let { cityId ->
viewModel.selectedCityId = cityId
}
val cityId = cityAdapter.getCityId(position)
Log.d(TAG, "City selected at position $position, cityId=$cityId")
cityId?.let { id ->
Log.d(TAG, "Setting selectedCityId=$id")
viewModel.selectedCityId = id
} ?: Log.e(TAG, "Could not get cityId for position $position")
}
}
@ -102,73 +143,93 @@ private fun setupToolbar() {
}
}
private fun collectFlows() {
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
launch {
viewModel.provincesState.collect { state ->
handleProvinceState(state)
}
}
private fun setupObservers() {
Log.d(TAG, "Setting up LiveData observers")
launch {
viewModel.citiesState.collect { state ->
handleCityState(state)
}
}
// Observe provinces
viewModel.provincesState.observe(this) { state ->
Log.d(TAG, "Received provincesState update: $state")
handleProvinceState(state)
}
launch {
viewModel.addressSubmissionState.collect { state ->
handleAddressSubmissionState(state)
}
}
}
// Observe cities
viewModel.citiesState.observe(this) { state ->
Log.d(TAG, "Received citiesState update: $state")
handleCityState(state)
}
// Observe address submission
viewModel.addressSubmissionState.observe(this) { state ->
Log.d(TAG, "Received addressSubmissionState update: $state")
handleAddressSubmissionState(state)
}
}
private fun handleProvinceState(state: ViewState<List<ProvincesItem>>) {
when (state) {
is ViewState.Loading -> null //showProvinceLoading(true)
is ViewState.Loading -> {
Log.d("AddAddressActivity", "Loading provinces...")
// Show loading indicator
}
is ViewState.Success -> {
provinceAdapter.updateData(state.data)
Log.d("AddAddressActivity", "Provinces loaded: ${state.data.size}")
// Hide loading indicator
if (state.data.isNotEmpty()) {
provinceAdapter.updateData(state.data)
} else {
showError("No provinces available")
}
}
is ViewState.Error -> {
showError(state.message)
// Hide loading indicator
showError("Failed to load provinces: ${state.message}")
Log.e("AddAddressActivity", "Province error: ${state.message}")
}
}
}
private fun handleCityState(state: ViewState<List<CitiesItem>>) {
when (state) {
is ViewState.Loading -> null //showCityLoading(true)
is ViewState.Loading -> {
Log.d("AddAddressActivity", "Loading cities...")
binding.cityProgressBar.visibility = View.VISIBLE
}
is ViewState.Success -> {
// showCityLoading(false)
Log.d("AddAddressActivity", "Cities loaded: ${state.data.size}")
binding.cityProgressBar.visibility = View.GONE
cityAdapter.updateData(state.data)
}
is ViewState.Error -> {
// showCityLoading(false)
showError(state.message)
binding.cityProgressBar.visibility = View.GONE
showError("Failed to load cities: ${state.message}")
Log.e("AddAddressActivity", "City error: ${state.message}")
}
}
}
private fun handleAddressSubmissionState(state: ViewState<String>) {
when (state) {
is ViewState.Loading -> showSubmitLoading(true)
is ViewState.Loading -> {
Log.d(TAG, "Address submission: Loading")
showSubmitLoading(true)
}
is ViewState.Success -> {
Log.d(TAG, "Address submission: Success - ${state.data}")
showSubmitLoading(false)
showSuccessAndFinish(state.data)
}
is ViewState.Error -> {
Log.e(TAG, "Address submission: Error - ${state.message}")
showSubmitLoading(false)
showError(state.message)
}
}
}
private fun showSubmitLoading(isLoading: Boolean) {
binding.buttonSimpan.isEnabled = !isLoading
binding.buttonSimpan.text = if (isLoading) "Menyimpan..." else "Simpan"
// You might want to show a progress bar as well
// binding.submitProgressBar.visibility = if (isLoading) View.VISIBLE else View.GONE
}
private fun showError(message: String) {
@ -177,47 +238,83 @@ private fun setupToolbar() {
private fun showSuccessAndFinish(message: String) {
Toast.makeText(this, "Sukses: $message", Toast.LENGTH_SHORT).show()
onBackPressed()
setResult(RESULT_OK)
finish()
}
private fun validateAndSubmitForm() {
val lat = latitude
val long = longitude
Log.d(TAG, "Validating form...")
Log.d(TAG, "Current location: lat=$latitude, long=$longitude")
if (lat == null || long == null) {
showError("Lokasi belum terdeteksi")
return
// Check if we have location - always use default if not available
if (latitude == null || longitude == null) {
Log.w(TAG, "No location detected, using default location")
// Default location for Jakarta
latitude = -6.200000
longitude = 106.816666
binding.tvLocationStatus.text = "Menggunakan lokasi default: Jakarta"
}
val street = binding.etDetailAlamat.text.toString()
val subDistrict = binding.etKecamatan.text.toString()
val postalCode = binding.etKodePos.text.toString()
val recipient = binding.etNamaPenerima.text.toString()
val phone = binding.etNomorHp.text.toString()
val userId = profileUser.userId
val street = binding.etDetailAlamat.text.toString().trim()
val subDistrict = binding.etKecamatan.text.toString().trim()
val postalCode = binding.etKodePos.text.toString().trim()
val recipient = binding.etNamaPenerima.text.toString().trim()
val phone = binding.etNomorHp.text.toString().trim()
val userId = try {
profileUser
} catch (e: Exception) {
Log.w(TAG, "Error getting userId, using default", e)
1 // Default userId for testing
}
val isStoreLocation = false
val provinceId = viewModel.selectedProvinceId
val cityId = viewModel.selectedCityId
if (street.isBlank() || recipient.isBlank() || phone.isBlank()) {
showError("Lengkapi semua field wajib")
Log.d(TAG, "Form data: street=$street, subDistrict=$subDistrict, postalCode=$postalCode, " +
"recipient=$recipient, phone=$phone, userId=$userId, provinceId=$provinceId, cityId=$cityId, " +
"lat=$latitude, long=$longitude")
// Validate required fields
if (street.isBlank()) {
Log.w(TAG, "Validation failed: street is blank")
binding.etDetailAlamat.error = "Alamat tidak boleh kosong"
binding.etDetailAlamat.requestFocus()
return
}
if (recipient.isBlank()) {
Log.w(TAG, "Validation failed: recipient is blank")
binding.etNamaPenerima.error = "Nama penerima tidak boleh kosong"
binding.etNamaPenerima.requestFocus()
return
}
if (phone.isBlank()) {
Log.w(TAG, "Validation failed: phone is blank")
binding.etNomorHp.error = "Nomor HP tidak boleh kosong"
binding.etNomorHp.requestFocus()
return
}
if (provinceId == null) {
Log.w(TAG, "Validation failed: provinceId is null")
showError("Pilih provinsi terlebih dahulu")
binding.autoCompleteProvinsi.requestFocus()
return
}
if (cityId == null) {
Log.w(TAG, "Validation failed: cityId is null")
showError("Pilih kota/kabupaten terlebih dahulu")
binding.autoCompleteKabupaten.requestFocus()
return
}
// Create request with all fields
val request = CreateAddressRequest(
lat = lat,
long = long,
lat = latitude!!, // Safe to use !! as we've checked above
long = longitude!!,
street = street,
subDistrict = subDistrict,
cityId = cityId,
@ -230,12 +327,17 @@ private fun setupToolbar() {
isStoreLocation = isStoreLocation
)
Log.d(TAG, "Form validation successful, submitting address: $request")
viewModel.addAddress(request)
}
private val locationPermissionLauncher =
registerForActivityResult(ActivityResultContracts.RequestPermission()) { granted ->
if (granted) requestLocation() else Toast.makeText(this, "Izin lokasi ditolak",Toast.LENGTH_SHORT).show()
if (granted) {
requestLocation()
} else {
Toast.makeText(this, "Izin lokasi ditolak", Toast.LENGTH_SHORT).show()
}
}
private fun requestLocationPermission() {
@ -244,36 +346,164 @@ private fun setupToolbar() {
@SuppressLint("MissingPermission")
private fun requestLocation() {
val isGpsEnabled = locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER)
val isNetworkEnabled = locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER)
Log.d(TAG, "Requesting device location")
if (!isGpsEnabled && !isNetworkEnabled) {
Toast.makeText(this, "Provider lokasi tidak tersedia", Toast.LENGTH_SHORT).show()
// Check if we're already requesting location to avoid multiple requests
if (isRequestingLocation) {
Log.w(TAG, "Location request already in progress")
return
}
val provider = if (isGpsEnabled) LocationManager.GPS_PROVIDER else LocationManager.NETWORK_PROVIDER
isRequestingLocation = true
binding.locationProgressBar.visibility = View.VISIBLE
binding.tvLocationStatus.text = "Mencari lokasi..."
locationManager.requestSingleUpdate(provider, object : LocationListener {
val isGpsEnabled = locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER)
val isNetworkEnabled = locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER)
Log.d(TAG, "Location providers: GPS=$isGpsEnabled, Network=$isNetworkEnabled")
if (!isGpsEnabled && !isNetworkEnabled) {
Log.w(TAG, "No location providers enabled")
binding.locationProgressBar.visibility = View.GONE
binding.tvLocationStatus.text = "Provider lokasi tidak tersedia"
isRequestingLocation = false
Toast.makeText(this, "Provider lokasi tidak tersedia", Toast.LENGTH_SHORT).show()
showEnableLocationDialog()
return
}
// Create location criteria
val criteria = Criteria()
criteria.accuracy = Criteria.ACCURACY_FINE
criteria.isBearingRequired = false
criteria.isAltitudeRequired = false
criteria.isSpeedRequired = false
criteria.powerRequirement = Criteria.POWER_LOW
// Get the best provider based on criteria
val provider = locationManager.getBestProvider(criteria, true) ?:
if (isGpsEnabled) LocationManager.GPS_PROVIDER else LocationManager.NETWORK_PROVIDER
Log.d(TAG, "Using location provider: $provider")
// Set timeout for location
Handler(Looper.getMainLooper()).postDelayed({
if (isRequestingLocation) {
Log.w(TAG, "Location timeout, using default")
binding.locationProgressBar.visibility = View.GONE
binding.tvLocationStatus.text = "Lokasi default: Jakarta"
latitude = -6.200000
longitude = 106.816666
isRequestingLocation = false
Toast.makeText(this, "Timeout lokasi, menggunakan lokasi default", Toast.LENGTH_SHORT).show()
}
}, 15000) // 15 seconds timeout
// Try getting last known location first
try {
val lastLocation = locationManager.getLastKnownLocation(provider)
if (lastLocation != null) {
Log.d(TAG, "Using last known location")
latitude = lastLocation.latitude
longitude = lastLocation.longitude
binding.locationProgressBar.visibility = View.GONE
binding.tvLocationStatus.text = "Lokasi terdeteksi: ${lastLocation.latitude}, ${lastLocation.longitude}"
isRequestingLocation = false
Toast.makeText(this, "Lokasi terdeteksi", Toast.LENGTH_SHORT).show()
return
} else {
Log.d(TAG, "No last known location, requesting updates")
}
} catch (e: Exception) {
Log.e(TAG, "Error getting last known location", e)
}
// Create a location listener
val locationListener = object : LocationListener {
override fun onLocationChanged(location: Location) {
Log.d(TAG, "onLocationChanged called: lat=${location.latitude}, long=${location.longitude}")
latitude = location.latitude
longitude = location.longitude
binding.locationProgressBar.visibility = View.GONE
binding.tvLocationStatus.text = "Lokasi terdeteksi: ${location.latitude}, ${location.longitude}"
isRequestingLocation = false
Toast.makeText(this@AddAddressActivity, "Lokasi terdeteksi", Toast.LENGTH_SHORT).show()
// Remove location updates after receiving a location
try {
locationManager.removeUpdates(this)
} catch (e: Exception) {
Log.e(TAG, "Error removing location updates", e)
}
}
override fun onStatusChanged(provider: String?, status: Int, extras: Bundle?) {
Log.d(TAG, "Location provider status changed: provider=$provider, status=$status")
}
override fun onProviderEnabled(provider: String) {
Log.d(TAG, "Location provider enabled: $provider")
}
override fun onStatusChanged(provider: String?, status: Int, extras: Bundle?) {}
override fun onProviderEnabled(provider: String) {}
override fun onProviderDisabled(provider: String) {
Toast.makeText(this@AddAddressActivity, "Provider dimatikan", Toast.LENGTH_SHORT).show()
Log.w(TAG, "Location provider disabled: $provider")
binding.locationProgressBar.visibility = View.GONE
binding.tvLocationStatus.text = "Provider lokasi dimatikan"
isRequestingLocation = false
Toast.makeText(this@AddAddressActivity, "Provider $provider dimatikan", Toast.LENGTH_SHORT).show()
}
}, null)
}
}
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if (requestCode == 100 && grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
requestLocation()
} else {
Toast.makeText(this, "Location permission denied", Toast.LENGTH_SHORT).show()
try {
// Request location updates
Log.d(TAG, "Requesting location updates from $provider")
locationManager.requestLocationUpdates(
provider,
0, // minimum time interval between updates (in milliseconds)
0f, // minimum distance between updates (in meters)
locationListener,
Looper.getMainLooper()
)
} catch (e: Exception) {
Log.e(TAG, "Exception requesting location update", e)
binding.locationProgressBar.visibility = View.GONE
binding.tvLocationStatus.text = "Error: ${e.message}"
isRequestingLocation = false
Toast.makeText(this, "Error mendapatkan lokasi: ${e.message}", Toast.LENGTH_SHORT).show()
// Set default location
latitude = -6.200000
longitude = 106.816666
}
}
private fun showEnableLocationDialog() {
AlertDialog.Builder(this)
.setTitle("Aktifkan Lokasi")
.setMessage("Aplikasi memerlukan akses lokasi. Silakan aktifkan lokasi di pengaturan.")
.setPositiveButton("Pengaturan") { _, _ ->
startActivity(Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS))
}
.setNegativeButton("Batal") { dialog, _ ->
dialog.dismiss()
}
.create()
.show()
}
private fun setupReloadButtons() {
// Add button to reload provinces (add this button to your layout)
// Add button to reload location (add this button to your layout)
binding.btnReloadLocation.setOnClickListener {
Log.d(TAG, "Reload location button clicked")
Toast.makeText(this, "Memuat ulang lokasi...", Toast.LENGTH_SHORT).show()
requestLocation()
}
}
companion object {
private const val TAG = "AddAddressViewModel"
}
}

View File

@ -1,28 +1,35 @@
package com.alya.ecommerce_serang.ui.order.address
import android.util.Log
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.alya.ecommerce_serang.data.api.dto.CreateAddressRequest
import com.alya.ecommerce_serang.data.api.dto.UserProfile
import com.alya.ecommerce_serang.data.api.response.customer.order.CitiesItem
import com.alya.ecommerce_serang.data.api.response.customer.order.ProvincesItem
import com.alya.ecommerce_serang.data.repository.OrderRepository
import com.alya.ecommerce_serang.data.repository.Result
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import com.alya.ecommerce_serang.data.repository.UserRepository
import kotlinx.coroutines.launch
class AddAddressViewModel(private val repository: OrderRepository, private val savedStateHandle: SavedStateHandle): ViewModel() {
// Flow states for data
private val _addressSubmissionState = MutableStateFlow<ViewState<String>>(ViewState.Loading)
val addressSubmissionState = _addressSubmissionState.asStateFlow()
class AddAddressViewModel(private val repository: OrderRepository, private val userRepo: UserRepository, private val savedStateHandle: SavedStateHandle): ViewModel() {
private val _addressSubmissionState = MutableLiveData<ViewState<String>>()
val addressSubmissionState: LiveData<ViewState<String>> = _addressSubmissionState
private val _provincesState = MutableStateFlow<ViewState<List<ProvincesItem>>>(ViewState.Loading)
val provincesState = _provincesState.asStateFlow()
private val _userProfile = MutableLiveData<UserProfile?>()
val userProfile: LiveData<UserProfile?> = _userProfile
private val _citiesState = MutableStateFlow<ViewState<List<CitiesItem>>>(ViewState.Loading)
val citiesState = _citiesState.asStateFlow()
private val _errorMessageUser = MutableLiveData<String>()
val errorMessageUser : LiveData<String> = _errorMessageUser
private val _provincesState = MutableLiveData<ViewState<List<ProvincesItem>>>()
val provincesState: LiveData<ViewState<List<ProvincesItem>>> = _provincesState
private val _citiesState = MutableLiveData<ViewState<List<CitiesItem>>>()
val citiesState: LiveData<ViewState<List<CitiesItem>>> = _citiesState
// Stored in SavedStateHandle for configuration changes
var selectedProvinceId: Int?
@ -38,47 +45,82 @@ class AddAddressViewModel(private val repository: OrderRepository, private val s
getProvinces()
}
fun addAddress(request: CreateAddressRequest){
fun addAddress(request: CreateAddressRequest) {
Log.d(TAG, "Starting address submission process")
_addressSubmissionState.value = ViewState.Loading
viewModelScope.launch {
when (val result = repository.addAddress(request)) {
is Result.Success -> {
val message = result.data.message // Ambil `message` dari CreateAddressResponse
_addressSubmissionState.value = ViewState.Success(message)
}
is Result.Error -> {
_addressSubmissionState.value =
ViewState.Error(result.exception.message ?: "Unknown error")
}
is Result.Loading -> {
// Optional, karena sudah set Loading di awal
try {
Log.d(TAG, "Calling repository.addAddress with request: $request")
val result = repository.addAddress(request)
when (result) {
is Result.Success -> {
val message = result.data.message
Log.d(TAG, "Address added successfully: $message")
_addressSubmissionState.postValue(ViewState.Success(message))
}
is Result.Error -> {
val errorMsg = result.exception.message ?: "Unknown error"
Log.e(TAG, "Error from repository: $errorMsg", result.exception)
_addressSubmissionState.postValue(ViewState.Error(errorMsg))
}
is Result.Loading -> {
Log.d(TAG, "Repository returned Loading state")
// We already set Loading at the beginning
}
else -> {
Log.e(TAG, "Repository returned unexpected result type: $result")
_addressSubmissionState.postValue(ViewState.Error("Unexpected error occurred"))
}
}
} catch (e: Exception) {
Log.e(TAG, "Exception occurred during address submission", e)
val errorMessage = e.message ?: "Unknown error occurred"
Log.e(TAG, "Error message: $errorMessage")
// Log the exception stack trace
e.printStackTrace()
_addressSubmissionState.postValue(ViewState.Error(errorMessage))
}
}
}
fun getProvinces(){
fun getProvinces() {
_provincesState.value = ViewState.Loading
viewModelScope.launch {
try {
val result = repository.getListProvinces()
result?.let {
_provincesState.value = ViewState.Success(it.provinces)
if (result?.provinces != null) {
_provincesState.postValue(ViewState.Success(result.provinces))
Log.d(TAG, "Provinces loaded: ${result.provinces.size}")
} else {
_provincesState.postValue(ViewState.Error("Failed to load provinces"))
Log.e(TAG, "Province result was null or empty")
}
} catch (e: Exception) {
Log.e("AddAddressViewModel", "Error fetching provinces: ${e.message}")
_provincesState.postValue(ViewState.Error(e.message ?: "Error loading provinces"))
Log.e(TAG, "Error fetching provinces", e)
}
}
}
fun getCities(provinceId: Int){
_citiesState.value = ViewState.Loading
viewModelScope.launch {
try {
selectedProvinceId = provinceId
val result = repository.getListCities(provinceId)
result?.let {
_citiesState.value = ViewState.Success(it.cities)
_citiesState.postValue(ViewState.Success(it.cities))
Log.d(TAG, "Cities loaded for province $provinceId: ${it.cities.size}")
} ?: run {
_citiesState.postValue(ViewState.Error("Failed to load cities"))
Log.e(TAG, "City result was null for province $provinceId")
}
} catch (e: Exception) {
Log.e("AddAddressViewModel", "Error fetching cities: ${e.message}")
_citiesState.postValue(ViewState.Error(e.message ?: "Error loading cities"))
Log.e(TAG, "Error fetching cities for province $provinceId", e)
}
}
}
@ -91,6 +133,16 @@ class AddAddressViewModel(private val repository: OrderRepository, private val s
selectedCityId = id
}
fun loadUserProfile(){
viewModelScope.launch {
when (val result = userRepo.fetchUserProfile()){
is Result.Success -> _userProfile.postValue(result.data)
is Result.Error -> _errorMessageUser.postValue(result.exception.message ?: "Unknown Error")
is Result.Loading -> null
}
}
}
companion object {
private const val TAG = "AddAddressViewModel"
}

View File

@ -4,9 +4,9 @@ import android.content.Intent
import android.os.Bundle
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.isVisible
import androidx.recyclerview.widget.LinearLayoutManager
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.OrderRepository
import com.alya.ecommerce_serang.databinding.ActivityAddressBinding
import com.alya.ecommerce_serang.utils.BaseViewModelFactory
@ -14,7 +14,6 @@ import com.alya.ecommerce_serang.utils.SessionManager
class AddressActivity : AppCompatActivity() {
private lateinit var binding: ActivityAddressBinding
private lateinit var apiService: ApiService
private lateinit var sessionManager: SessionManager
private lateinit var adapter: AddressAdapter
@ -26,55 +25,82 @@ class AddressActivity : AppCompatActivity() {
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityAddressBinding.inflate(layoutInflater)
setContentView(binding.root)
sessionManager = SessionManager(this)
apiService = ApiConfig.getApiService(sessionManager)
setupToolbar()
setupRecyclerView()
setupObservers()
adapter = AddressAdapter { selectedId ->
viewModel.selectAddress(selectedId)
viewModel.fetchAddresses()
}
private fun addAddressClicked(){
binding.addAddressClick.setOnClickListener{
val intent = Intent(this, AddAddressActivity::class.java)
startActivity(intent)
}
}
private fun setupToolbar() {
// Remove duplicate toolbar setup
addAddressClicked()
binding.toolbar.setNavigationOnClickListener {
onBackPressedWithResult()
}
}
binding.rvSellerOrder.layoutManager = LinearLayoutManager(this)
binding.rvSellerOrder.adapter = adapter
private fun setupRecyclerView() {
adapter = AddressAdapter { address ->
// Select the address in the ViewModel
viewModel.selectAddress(address.id)
viewModel.fetchAddresses()
// Return immediately with the selected address
returnResultAndFinish(address.id)
}
binding.rvSellerOrder.apply {
layoutManager = LinearLayoutManager(this@AddressActivity)
adapter = this@AddressActivity.adapter
}
}
private fun setupObservers() {
viewModel.addresses.observe(this) { addressList ->
adapter.submitList(addressList)
// Show empty state if needed
// binding.emptyView?.isVisible = addressList.isEmpty()
binding.rvSellerOrder.isVisible = addressList.isNotEmpty()
}
viewModel.selectedAddressId.observe(this) { selectedId ->
adapter.setSelectedAddressId(selectedId)
}
}
private fun setupToolbar() {
binding.toolbar.setNavigationOnClickListener {
private fun onBackPressedWithResult() {
// If an address is selected, return it as result
val selectedId = viewModel.selectedAddressId.value
if (selectedId != null) {
returnResultAndFinish(selectedId)
finish()
} else {
// No selection, just finish
setResult(RESULT_CANCELED)
finish()
}
}
// private fun updateEmptyState(isEmpty: Boolean) {
// binding.layoutEmptyAddresses.isVisible = isEmpty
// binding.rvAddresses.isVisible = !isEmpty
// }
private fun onBackPressedWithResult() {
viewModel.selectedAddressId.value?.let {
val intent = Intent()
intent.putExtra(EXTRA_ADDRESS_ID, it)
setResult(RESULT_OK, intent)
}
finish()
private fun returnResultAndFinish(addressId: Int) {
val intent = Intent()
intent.putExtra(EXTRA_ADDRESS_ID, addressId)
setResult(RESULT_OK, intent)
}
companion object {

View File

@ -13,14 +13,25 @@ import com.alya.ecommerce_serang.data.api.response.customer.profile.AddressesIte
import com.google.android.material.card.MaterialCardView
class AddressAdapter(
private val onAddressClick: (Int) -> Unit
private val onAddressClick: (AddressesItem) -> Unit
) : ListAdapter<AddressesItem, AddressAdapter.AddressViewHolder>(DIFF_CALLBACK) {
private var selectedAddressId: Int? = null
fun setSelectedAddressId(id: Int?) {
val oldSelectedId = selectedAddressId
selectedAddressId = id
notifyDataSetChanged()
// Only refresh the changed items
if (oldSelectedId != null) {
val oldPosition = currentList.indexOfFirst { it.id == oldSelectedId }
if (oldPosition >= 0) notifyItemChanged(oldPosition)
}
if (id != null) {
val newPosition = currentList.indexOfFirst { it.id == id }
if (newPosition >= 0) notifyItemChanged(newPosition)
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AddressViewHolder {
@ -33,7 +44,8 @@ class AddressAdapter(
val address = getItem(position)
holder.bind(address, selectedAddressId == address.id)
holder.itemView.setOnClickListener {
onAddressClick(address.id)
// Pass the whole address object to provide more context
onAddressClick(address)
}
}
@ -46,6 +58,12 @@ class AddressAdapter(
tvName.text = address.recipient
tvDetail.text = "${address.street}, ${address.subdistrict}, ${address.phone}"
// Make selection more visible
card.strokeWidth = if (isSelected) 3 else 0
card.strokeColor = if (isSelected)
ContextCompat.getColor(itemView.context, R.color.blue_400)
else 0
card.setCardBackgroundColor(
ContextCompat.getColor(
itemView.context,

View File

@ -1,6 +1,7 @@
package com.alya.ecommerce_serang.ui.order.address
import android.content.Context
import android.util.Log
import android.widget.ArrayAdapter
import com.alya.ecommerce_serang.data.api.response.customer.order.CitiesItem
import com.alya.ecommerce_serang.data.api.response.customer.order.ProvincesItem
@ -20,6 +21,8 @@ class ProvinceAdapter(
clear()
addAll(provinces.map { it.province })
notifyDataSetChanged()
Log.d("ProvinceAdapter", "Updated with ${provinces.size} provinces")
}
fun getProvinceId(position: Int): Int? {

View File

@ -0,0 +1,340 @@
package com.alya.ecommerce_serang.ui.order.detail
import android.Manifest
import android.R
import android.app.DatePickerDialog
import android.content.pm.PackageManager
import android.graphics.BitmapFactory
import android.net.Uri
import android.os.Bundle
import android.util.Log
import android.view.View
import android.webkit.MimeTypeMap
import android.widget.AdapterView
import android.widget.ArrayAdapter
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import com.alya.ecommerce_serang.data.api.dto.AddEvidenceMultipartRequest
import com.alya.ecommerce_serang.data.api.retrofit.ApiConfig
import com.alya.ecommerce_serang.data.repository.OrderRepository
import com.alya.ecommerce_serang.data.repository.Result
import com.alya.ecommerce_serang.databinding.ActivityAddEvidencePaymentBinding
import com.alya.ecommerce_serang.utils.BaseViewModelFactory
import com.alya.ecommerce_serang.utils.SessionManager
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 AddEvidencePaymentActivity : AppCompatActivity() {
private lateinit var binding: ActivityAddEvidencePaymentBinding
private lateinit var sessionManager: SessionManager
private var orderId: Int = 0
private var paymentInfoId: Int = 0
private lateinit var productPrice: String
private var selectedImageUri: Uri? = null
private val viewModel: PaymentViewModel by viewModels {
BaseViewModelFactory {
val apiService = ApiConfig.getApiService(sessionManager)
val orderRepository = OrderRepository(apiService)
PaymentViewModel(orderRepository)
}
}
private val paymentMethods = arrayOf(
"Pilih metode pembayaran",
"Transfer Bank",
"E-Wallet",
"Virtual Account",
"Cash on Delivery"
)
private val getContent = registerForActivityResult(ActivityResultContracts.GetContent()) { uri: Uri? ->
uri?.let {
selectedImageUri = it
binding.ivUploadedImage.setImageURI(selectedImageUri)
binding.ivUploadedImage.visibility = View.VISIBLE
binding.layoutUploadPlaceholder.visibility = View.GONE
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityAddEvidencePaymentBinding.inflate(layoutInflater)
setContentView(binding.root)
sessionManager = SessionManager(this)
intent.extras?.let { bundle ->
orderId = bundle.getInt("ORDER_ID", 0)
paymentInfoId = bundle.getInt("PAYMENT_INFO_ID", 0)
productPrice = intent.getStringExtra("TOTAL_AMOUNT") ?: "Rp0"
}
setupUI()
viewModel.getOrderDetails(orderId)
setupListeners()
setupObservers()
}
private fun setupUI() {
// Set product details\
// Setup payment methods spinner
val adapter = ArrayAdapter(this, R.layout.simple_spinner_item, paymentMethods)
adapter.setDropDownViewResource(R.layout.simple_spinner_dropdown_item)
binding.spinnerPaymentMethod.adapter = adapter
}
private fun setupListeners() {
// Upload image button
binding.tvAddPhoto.setOnClickListener {
checkPermissionAndPickImage()
}
binding.frameUploadImage.setOnClickListener {
checkPermissionAndPickImage()
}
// Date picker
binding.tvPaymentDate.setOnClickListener {
showDatePicker()
}
// Payment method spinner
binding.spinnerPaymentMethod.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
// Skip the hint (first item)
if (position > 0) {
val selectedMethod = paymentMethods[position]
// You can also use it for further processing
Log.d(TAG, "Selected payment method: $selectedMethod")
}
}
override fun onNothingSelected(parent: AdapterView<*>?) {
// Do nothing
}
}
// Submit button
binding.btnSubmit.setOnClickListener {
validateAndUpload()
}
}
private fun setupObservers() {
viewModel.uploadResult.observe(this) { result ->
when (result) {
is com.alya.ecommerce_serang.data.repository.Result.Success -> {
Toast.makeText(this, "Bukti pembayaran berhasil dikirim", Toast.LENGTH_SHORT).show()
Log.d(TAG, "Upload successful: ${result.data}")
// Navigate back or to confirmation screen
finish()
}
is com.alya.ecommerce_serang.data.repository.Result.Error -> {
Log.e(TAG, "Upload failed: ${result.exception.message}")
Toast.makeText(this, "Gagal mengirim bukti pembayaran: ${result.exception.message}", Toast.LENGTH_SHORT).show()
}
is Result.Loading -> {
// Show loading indicator if needed
Log.d(TAG, "Uploading payment proof...")
}
}
}
}
private fun validateAndUpload() {
// Validate all fields
if (selectedImageUri == null) {
Toast.makeText(this, "Silahkan pilih bukti pembayaran", Toast.LENGTH_SHORT).show()
return
}
if (binding.spinnerPaymentMethod.selectedItemPosition == 0) {
Toast.makeText(this, "Silahkan pilih metode pembayaran", Toast.LENGTH_SHORT).show()
return
}
if (binding.etAccountNumber.text.toString().trim().isEmpty()) {
Toast.makeText(this, "Silahkan isi nomor rekening/HP", Toast.LENGTH_SHORT).show()
return
}
if (binding.tvPaymentDate.text.toString() == "Pilih tanggal") {
Toast.makeText(this, "Silahkan pilih tanggal pembayaran", Toast.LENGTH_SHORT).show()
return
}
// All validations passed, proceed with upload
uploadPaymentProof()
}
private fun uploadPaymentProof() {
selectedImageUri?.let { uri ->
// Convert URI to File
val file = getFileFromUri(uri)
file?.let {
try {
// Create MultipartBody.Part from File
val requestFile = file.asRequestBody("image/jpeg".toMediaTypeOrNull())
val evidencePart = MultipartBody.Part.createFormData("evidence", file.name, requestFile)
// Create RequestBody for order ID and amount
val orderIdPart = orderId.toString().toRequestBody("text/plain".toMediaTypeOrNull())
// Clean up the price string to get only the numeric value
val amountPart = productPrice.replace("Rp", "").replace(".", "").trim()
.toRequestBody("text/plain".toMediaTypeOrNull())
// Create the request object with the parts we need
val request = AddEvidenceMultipartRequest(
orderId = orderIdPart,
amount = amountPart,
evidence = evidencePart
)
// Log request details for debugging
Log.d(TAG, "Uploading payment proof - OrderID: $orderId, Amount: ${productPrice.replace("Rp", "").replace(".", "").trim()}")
Log.d(TAG, "File details - Name: ${file.name}, Size: ${file.length()} bytes, MIME: image/jpeg")
// Call the viewModel method
viewModel.uploadPaymentProof(request)
} catch (e: Exception) {
Log.e(TAG, "Error creating upload request: ${e.message}", e)
Toast.makeText(this, "Error preparing upload: ${e.message}", Toast.LENGTH_SHORT).show()
}
}
}
}
private fun getFileFromUri(uri: Uri): File? {
val contentResolver = applicationContext.contentResolver
val mimeType = contentResolver.getType(uri) ?: "image/jpeg"
val extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType) ?: "jpg"
Log.d("UploadEvidence", "URI: $uri")
Log.d("UploadEvidence", "Detected MIME type: $mimeType, extension: $extension")
// Ensure it's an image (either PNG, JPG, or JPEG)
if (mimeType != "image/png" && mimeType != "image/jpeg" && mimeType != "image/jpg") {
Log.e("UploadEvidence", "Invalid image MIME type: $mimeType. Only images are allowed.")
Toast.makeText(applicationContext, "Only image files are allowed", Toast.LENGTH_SHORT).show()
return null
}
try {
val inputStream = contentResolver.openInputStream(uri)
if (inputStream == null) {
Log.e("UploadEvidence", "Failed to open input stream from URI: $uri")
return null
}
// Create a temporary file with the correct extension
val tempFile = File.createTempFile("evidence_", ".$extension", cacheDir)
Log.d("UploadEvidence", "Temp file created at: ${tempFile.absolutePath}")
// Copy the content from inputStream to the temporary file
tempFile.outputStream().use { outputStream ->
inputStream.copyTo(outputStream)
inputStream.close()
}
// Verify if the file is a valid image
val bitmap = BitmapFactory.decodeFile(tempFile.absolutePath)
if (bitmap == null) {
Log.e("UploadEvidence", "File is not a valid image!")
tempFile.delete()
return null
} else {
bitmap.recycle() // Free memory
Log.d("UploadEvidence", "Valid image detected.")
}
Log.d("UploadEvidence", "File copied successfully. Size: ${tempFile.length()} bytes")
return tempFile
} catch (e: Exception) {
Log.e("UploadEvidence", "Error processing file: ${e.message}", e)
Toast.makeText(applicationContext, "Error processing image: ${e.message}", Toast.LENGTH_SHORT).show()
return null
}
}
private fun checkPermissionAndPickImage() {
val permission = if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.TIRAMISU) {
Manifest.permission.READ_MEDIA_IMAGES
} else {
Manifest.permission.READ_EXTERNAL_STORAGE
}
if (ContextCompat.checkSelfPermission(this, permission) != PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(this, arrayOf(permission), REQUEST_CODE_STORAGE_PERMISSION)
} else {
pickImage()
}
}
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if (requestCode == REQUEST_CODE_STORAGE_PERMISSION && grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
pickImage()
} else {
Toast.makeText(this, "Izin dibutuhkan untuk memilih gambar", Toast.LENGTH_SHORT).show()
}
}
private fun pickImage() {
getContent.launch("image/*")
}
private fun showDatePicker() {
val calendar = Calendar.getInstance()
val year = calendar.get(Calendar.YEAR)
val month = calendar.get(Calendar.MONTH)
val day = calendar.get(Calendar.DAY_OF_MONTH)
DatePickerDialog(
this,
{ _, selectedYear, selectedMonth, selectedDay ->
calendar.set(selectedYear, selectedMonth, selectedDay)
val sdf = SimpleDateFormat("dd-MM-yyyy", Locale.getDefault())
binding.tvPaymentDate.text = sdf.format(calendar.time)
},
year, month, day
).show()
}
companion object {
private const val PERMISSION_REQUEST_CODE = 100
private const val TAG = "AddEvidenceActivity"
private const val REQUEST_CODE_STORAGE_PERMISSION = 100
}
}

View File

@ -0,0 +1,198 @@
package com.alya.ecommerce_serang.ui.order.detail
import android.content.Intent
import android.os.Bundle
import android.util.Log
import android.widget.Toast
import androidx.activity.viewModels
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import com.alya.ecommerce_serang.data.api.retrofit.ApiConfig
import com.alya.ecommerce_serang.data.repository.OrderRepository
import com.alya.ecommerce_serang.databinding.ActivityPaymentBinding
import com.alya.ecommerce_serang.utils.BaseViewModelFactory
import com.alya.ecommerce_serang.utils.SessionManager
import java.text.SimpleDateFormat
import java.util.Calendar
import java.util.Locale
import java.util.TimeZone
class PaymentActivity : AppCompatActivity() {
private lateinit var binding: ActivityPaymentBinding
private lateinit var sessionManager: SessionManager
companion object {
private const val TAG = "PaymentActivity"
}
private val viewModel: PaymentViewModel by viewModels {
BaseViewModelFactory {
val apiService = ApiConfig.getApiService(sessionManager)
val orderRepository = OrderRepository(apiService)
PaymentViewModel(orderRepository)
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityPaymentBinding.inflate(layoutInflater)
setContentView(binding.root)
sessionManager = SessionManager(this)
// Mengambil data dari intent
val orderId = intent.getIntExtra("ORDER_ID", 0)
val paymentInfoId = intent.getIntExtra("ORDER_PAYMENT_ID", 0)
if (orderId == 0) {
Toast.makeText(this, "ID pesanan tidak valid", Toast.LENGTH_SHORT).show()
finish()
}
// Setup toolbar
binding.toolbar.setNavigationOnClickListener {
finish()
}
// Setup petunjuk transfer
binding.layoutMBankingInstructions.setOnClickListener {
// Tampilkan instruksi mBanking
showInstructions("mBanking")
}
binding.layoutATMInstructions.setOnClickListener {
// Tampilkan instruksi ATM
showInstructions("ATM")
}
// Setup button upload bukti bayar
binding.btnUploadPaymentProof.setOnClickListener {
// Intent ke activity upload bukti bayar
val intent = Intent(this, AddEvidencePaymentActivity::class.java)
intent.putExtra("ORDER_ID", orderId)
intent.putExtra("PAYMENT_INFO_ID", paymentInfoId)
intent.putExtra("TOTAL_AMOUNT", binding.tvTotalAmount.text.toString())
Log.d(TAG, "Received Order ID: $orderId, Payment Info ID: $paymentInfoId, Total Amount: ${binding.tvTotalAmount.text}")
startActivity(intent)
}
// Setup button negosiasi harga
binding.btnNegotiatePrice.setOnClickListener {
// Intent ke activity negosiasi harga
// val intent = Intent(this, NegotiatePriceActivity::class.java)
// intent.putExtra("ORDER_ID", orderId)
// startActivity(intent)
}
// Observe data
observeData()
// Load data
Log.d(TAG, "Fetching order details for Order ID: $orderId")
viewModel.getOrderDetails(orderId)
}
private fun observeData() {
// Observe Order Details
viewModel.orderDetails.observe(this) { order ->
Log.d(TAG, "Order details received: $order")
// Set total amount
binding.tvTotalAmount.text = order.totalAmount ?: "Rp0"
Log.d(TAG, "Total Amount: ${order.totalAmount}")
// Set bank information
binding.tvBankName.text = order.payInfoName ?: "Bank BCA"
binding.tvAccountNumber.text = order.payInfoNum ?: "0123456789"
Log.d(TAG, "Bank Name: ${order.payInfoName}, Account Number: ${order.payInfoNum}")
// Calculate remaining time and due date
setupPaymentDueDate(order.updatedAt)
}
// Observe loading state
viewModel.isLoading.observe(this) { isLoading ->
// Show loading indicator if needed
// binding.progressBar.visibility = if (isLoading) View.VISIBLE else View.GONE
}
// Observe error
viewModel.error.observe(this) { error ->
if (error.isNotEmpty()) {
Toast.makeText(this, error, Toast.LENGTH_SHORT).show()
}
}
}
private fun setupPaymentDueDate(createdAt: String) {
Log.d(TAG, "Setting up payment due date from updated at: $createdAt")
try {
// Parse the ISO 8601 date
val isoDateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.getDefault())
isoDateFormat.timeZone = TimeZone.getTimeZone("UTC")
val createdDate = isoDateFormat.parse(createdAt) ?: return
// Add 24 hours to get due date
val calendar = Calendar.getInstance()
calendar.time = createdDate
calendar.add(Calendar.HOUR, 24)
val dueDate = calendar.time
// Format due date for display
val dueDateFormat = SimpleDateFormat("dd MMM yyyy", Locale.getDefault())
binding.tvDueDate.text = "Jatuh tempo: ${dueDateFormat.format(dueDate)}"
Log.d(TAG, "Due Date: ${dueDateFormat.format(dueDate)}")
// Calculate remaining time
val now = Calendar.getInstance().time
val diff = dueDate.time - now.time
if (diff > 0) {
val hours = diff / (60 * 60 * 1000)
val minutes = (diff % (60 * 60 * 1000)) / (60 * 1000)
binding.tvRemainingTime.text = "$hours jam $minutes menit"
Log.d(TAG, "Remaining Time: $hours hours $minutes minutes")
} else {
binding.tvRemainingTime.text = "Waktu habis"
}
} catch (e: Exception) {
Log.e(TAG, "Error parsing date", e)
binding.tvDueDate.text = "Jatuh tempo: -"
binding.tvRemainingTime.text = "-"
}
}
private fun showInstructions(type: String) {
// Implementasi tampilkan instruksi
val instructions = when (type) {
"mBanking" -> listOf(
"1. Login ke aplikasi mobile banking",
"2. Pilih menu Transfer",
"3. Pilih menu Antar Rekening",
"4. Masukkan nomor rekening tujuan",
"5. Masukkan nominal transfer sesuai tagihan",
"6. Konfirmasi dan selesaikan transfer"
)
"ATM" -> listOf(
"1. Masukkan kartu ATM dan PIN",
"2. Pilih menu Transfer",
"3. Pilih menu Antar Rekening",
"4. Masukkan kode bank dan nomor rekening tujuan",
"5. Masukkan nominal transfer sesuai tagihan",
"6. Konfirmasi dan selesaikan transfer"
)
else -> emptyList()
}
// Tampilkan instruksi dalam dialog
val dialog = AlertDialog.Builder(this)
.setTitle("Petunjuk Transfer $type")
.setItems(instructions.toTypedArray(), null)
.setPositiveButton("Tutup", null)
.create()
dialog.show()
}
}

View File

@ -0,0 +1,78 @@
package com.alya.ecommerce_serang.ui.order.detail
import android.util.Log
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.alya.ecommerce_serang.data.api.dto.AddEvidenceMultipartRequest
import com.alya.ecommerce_serang.data.api.response.order.AddEvidenceResponse
import com.alya.ecommerce_serang.data.api.response.order.CompletedOrderResponse
import com.alya.ecommerce_serang.data.api.response.order.OrderListItemsItem
import com.alya.ecommerce_serang.data.api.response.order.Orders
import com.alya.ecommerce_serang.data.repository.OrderRepository
import com.alya.ecommerce_serang.data.repository.Result
import kotlinx.coroutines.launch
class PaymentViewModel(private val repository: OrderRepository) : ViewModel() {
companion object {
private const val TAG = "PaymentViewModel"
}
// LiveData untuk Order
private val _orderDetails = MutableLiveData<Orders>()
val orderDetails: LiveData<Orders> get() = _orderDetails
// LiveData untuk OrderItems
private val _orderItems = MutableLiveData<List<OrderListItemsItem>>()
val orderItems: LiveData<List<OrderListItemsItem>> get() = _orderItems
private val _orderCompletionStatus = MutableLiveData<Result<CompletedOrderResponse>>()
val orderCompletionStatus: LiveData<Result<CompletedOrderResponse>> = _orderCompletionStatus
// LiveData untuk status loading
private val _isLoading = MutableLiveData<Boolean>()
val isLoading: LiveData<Boolean> get() = _isLoading
// LiveData untuk error
private val _error = MutableLiveData<String>()
val error: LiveData<String> get() = _error
private val _uploadResult = MutableLiveData<Result<AddEvidenceResponse>>()
val uploadResult: LiveData<com.alya.ecommerce_serang.data.repository.Result<AddEvidenceResponse>> = _uploadResult
fun getOrderDetails(orderId: Int) {
_isLoading.value = true
viewModelScope.launch {
try {
val response = repository.getOrderDetails(orderId)
if (response != null) {
_orderDetails.value = response.orders
_orderItems.value = response.orders.orderItems
} else {
_error.value = "Gagal memuat detail pesanan"
}
} catch (e: Exception) {
_error.value = "Terjadi kesalahan: ${e.message}"
Log.e(TAG, "Error fetching order details", e)
} finally {
_isLoading.value = false
}
}
}
fun uploadPaymentProof(request: AddEvidenceMultipartRequest) {
viewModelScope.launch {
_uploadResult.value = com.alya.ecommerce_serang.data.repository.Result.Loading
try {
val result = repository.uploadPaymentProof(request)
_uploadResult.value = result
} catch (e: Exception) {
Log.e("PaymentProofViewModel", "Error uploading payment proof", e)
_uploadResult.value = com.alya.ecommerce_serang.data.repository.Result.Error(e)
}
}
}
}

View File

@ -0,0 +1,56 @@
package com.alya.ecommerce_serang.ui.order.history
import android.os.Bundle
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.commit
import com.alya.ecommerce_serang.R
import com.alya.ecommerce_serang.data.api.retrofit.ApiConfig
import com.alya.ecommerce_serang.data.repository.OrderRepository
import com.alya.ecommerce_serang.databinding.ActivityHistoryBinding
import com.alya.ecommerce_serang.utils.BaseViewModelFactory
import com.alya.ecommerce_serang.utils.SessionManager
class HistoryActivity : AppCompatActivity() {
private lateinit var binding: ActivityHistoryBinding
private lateinit var sessionManager: SessionManager
private val viewModel: HistoryViewModel by viewModels {
BaseViewModelFactory {
val apiService = ApiConfig.getApiService(sessionManager)
val orderRepository = OrderRepository(apiService)
HistoryViewModel(orderRepository)
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityHistoryBinding.inflate(layoutInflater)
setContentView(binding.root)
sessionManager = SessionManager(this)
setupToolbar()
if (savedInstanceState == null) {
showOrderFragment()
}
}
private fun setupToolbar() {
setSupportActionBar(binding.toolbar)
supportActionBar?.setDisplayShowTitleEnabled(false)
binding.btnBack.setOnClickListener {
onBackPressed()
}
}
private fun showOrderFragment() {
supportFragmentManager.commit {
replace(R.id.fragment_container_history, OrderHistoryFragment())
}
}
}

View File

@ -0,0 +1,102 @@
package com.alya.ecommerce_serang.ui.order.history
import android.util.Log
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.alya.ecommerce_serang.data.api.dto.CompletedOrderRequest
import com.alya.ecommerce_serang.data.api.response.order.CompletedOrderResponse
import com.alya.ecommerce_serang.data.api.response.order.OrdersItem
import com.alya.ecommerce_serang.data.repository.OrderRepository
import com.alya.ecommerce_serang.data.repository.Result
import com.alya.ecommerce_serang.ui.order.address.ViewState
import kotlinx.coroutines.launch
import java.io.File
class HistoryViewModel(private val repository: OrderRepository) : ViewModel() {
companion object {
private const val TAG = "HistoryViewModel"
}
private val _orders = MutableLiveData<ViewState<List<OrdersItem>>>()
val orders: LiveData<ViewState<List<OrdersItem>>> = _orders
private val _orderCompletionStatus = MutableLiveData<Result<CompletedOrderResponse>>()
val orderCompletionStatus: LiveData<Result<CompletedOrderResponse>> = _orderCompletionStatus
private val _isLoading = MutableLiveData<Boolean>()
val isLoading: LiveData<Boolean> = _isLoading
private val _message = MutableLiveData<String>()
val message: LiveData<String> = _message
private val _isSuccess = MutableLiveData<Boolean>()
val isSuccess: LiveData<Boolean> = _isSuccess
fun getOrderList(status: String) {
_orders.value = ViewState.Loading
viewModelScope.launch {
_orders.value = ViewState.Loading
try {
when (val result = repository.getOrderList(status)) {
is Result.Success -> {
_orders.value = ViewState.Success(result.data.orders)
Log.d("HistoryViewModel", "Orders loaded successfully: ${result.data.orders.size} items")
}
is Result.Error -> {
_orders.value = ViewState.Error(result.exception.message ?: "Unknown error occurred")
Log.e("HistoryViewModel", "Error loading orders", result.exception)
}
is Result.Loading -> {
null
}
}
} catch (e: Exception) {
_orders.value = ViewState.Error("An unexpected error occurred: ${e.message}")
Log.e("HistoryViewModel", "Exception in getOrderList", e)
}
}
}
fun confirmOrderCompleted(orderId: Int, status: String) {
Log.d(TAG, "Confirming order completed: orderId=$orderId, status=$status")
viewModelScope.launch {
_orderCompletionStatus.value = Result.Loading
val request = CompletedOrderRequest(orderId, status)
Log.d(TAG, "Sending order completion request: $request")
val result = repository.confirmOrderCompleted(request)
Log.d(TAG, "Order completion result: $result")
_orderCompletionStatus.value = result
}
}
fun cancelOrderWithImage(orderId: String, reason: String, imageFile: File?) {
Log.d(TAG, "Cancelling order with image: orderId=$orderId, reason=$reason, hasImage=${imageFile != null}")
viewModelScope.launch {
repository.submitComplaint(orderId, reason, imageFile).collect { result ->
when (result) {
is Result.Loading -> {
Log.d(TAG, "Submitting complaint: Loading")
_isLoading.value = true
}
is Result.Success -> {
Log.d(TAG, "Complaint submitted successfully: ${result.data.message}")
_message.value = result.data.message
_isSuccess.value = true
_isLoading.value = false
}
is Result.Error -> {
val errorMessage = result.exception.message ?: "Error submitting complaint"
Log.e(TAG, "Error submitting complaint: $errorMessage", result.exception)
_message.value = errorMessage
_isSuccess.value = false
_isLoading.value = false
}
}
}
}
}
}

View File

@ -0,0 +1,504 @@
package com.alya.ecommerce_serang.ui.order.history
import android.app.Activity
import android.app.Dialog
import android.content.Intent
import android.graphics.Color
import android.net.Uri
import android.provider.MediaStore
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.Window
import android.widget.ArrayAdapter
import android.widget.AutoCompleteTextView
import android.widget.ImageView
import android.widget.ProgressBar
import android.widget.TextView
import android.widget.Toast
import androidx.lifecycle.findViewTreeLifecycleOwner
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.alya.ecommerce_serang.R
import com.alya.ecommerce_serang.data.api.response.order.OrdersItem
import com.alya.ecommerce_serang.ui.order.detail.PaymentActivity
import com.google.android.material.button.MaterialButton
import com.google.android.material.textfield.TextInputLayout
import java.io.File
import java.text.SimpleDateFormat
import java.util.Calendar
import java.util.Locale
import java.util.TimeZone
class OrderHistoryAdapter(
private val onOrderClickListener: (OrdersItem) -> Unit,
private val viewModel: HistoryViewModel // Add this parameter
) : RecyclerView.Adapter<OrderHistoryAdapter.OrderViewHolder>() {
private val orders = mutableListOf<OrdersItem>()
private var fragmentStatus: String = "all"
fun setFragmentStatus(status: String) {
fragmentStatus = status
}
fun submitList(newOrders: List<OrdersItem>) {
orders.clear()
orders.addAll(newOrders)
notifyDataSetChanged()
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): OrderViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.item_order_history, parent, false)
return OrderViewHolder(view)
}
override fun onBindViewHolder(holder: OrderViewHolder, position: Int) {
holder.bind(orders[position])
}
override fun getItemCount(): Int = orders.size
inner class OrderViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
private val tvStoreName: TextView = itemView.findViewById(R.id.tvStoreName)
private val rvOrderItems: RecyclerView = itemView.findViewById(R.id.rvOrderItems)
private val tvShowMore: TextView = itemView.findViewById(R.id.tvShowMore)
private val tvTotalAmount: TextView = itemView.findViewById(R.id.tvTotalAmount)
private val tvItemCountLabel: TextView = itemView.findViewById(R.id.tv_count_total_item)
// private val tvDeadlineDate: TextView = itemView.findViewById(R.id.tvDeadlineDate)
fun bind(order: OrdersItem) {
// Get store name from the first order item
val storeName = if (order.orderItems.isNotEmpty()) order.orderItems[0].storeName else ""
tvStoreName.text = storeName
// Set total amount
tvTotalAmount.text = order.totalAmount
// Set item count
val itemCount = order.orderItems.size
tvItemCountLabel.text = itemView.context.getString(R.string.item_count_prod, itemCount)
// Set deadline date, adjust to each status
// tvDeadlineDate.text = formatDate(order.updatedAt)
// Set up the order items RecyclerView
val productAdapter = OrderProductAdapter()
rvOrderItems.apply {
layoutManager = LinearLayoutManager(itemView.context)
adapter = productAdapter
}
// Display only the first product and show "View more" for the rest
if (order.orderItems.isNotEmpty()) {
productAdapter.submitList(order.orderItems.take(1))
// Show or hide the "View more" text based on number of items
if (order.orderItems.size > 1) {
val itemString = order.orderItems.size - 1
tvShowMore.visibility = View.VISIBLE
tvShowMore.text = itemView.context.getString(R.string.show_more_product, itemString)
} else {
tvShowMore.visibility = View.GONE
}
} else {
tvShowMore.visibility = View.GONE
}
// Set click listener for the entire order item
itemView.setOnClickListener {
onOrderClickListener(order)
}
//adjust each fragment
adjustButtonsAndText(fragmentStatus, order)
}
private fun adjustButtonsAndText(status: String, order: OrdersItem) {
Log.d("OrderHistoryAdapter", "Adjusting buttons for status: $status")
// Mendapatkan referensi ke tombol-tombol
val btnLeft = itemView.findViewById<MaterialButton>(R.id.btn_left)
val btnRight = itemView.findViewById<MaterialButton>(R.id.btn_right)
val statusOrder = itemView.findViewById<TextView>(R.id.tvOrderStatus)
val deadlineLabel = itemView.findViewById<TextView>(R.id.tvDeadlineLabel)
val deadlineDate = itemView.findViewById<TextView>(R.id.tvDeadlineDate)
// Reset visibility
btnLeft.visibility = View.GONE
btnRight.visibility = View.GONE
statusOrder.visibility = View.GONE
deadlineLabel.visibility = View.GONE
when (status) {
"pending" -> {
statusOrder.apply {
visibility = View.VISIBLE
text = itemView.context.getString(R.string.pending_orders)
}
deadlineLabel.apply {
visibility = View.VISIBLE
text = itemView.context.getString(R.string.dl_pending)
}
btnLeft.apply {
visibility = View.VISIBLE
text = itemView.context.getString(R.string.canceled_order_btn)
setOnClickListener {
showCancelOrderDialog(order.orderId.toString())
}
}
deadlineDate.apply {
visibility = View.VISIBLE
text = formatDate(order.createdAt)
}
}
"unpaid" -> {
statusOrder.apply {
visibility = View.VISIBLE
text = itemView.context.getString(R.string.unpaid_orders)
}
deadlineLabel.apply {
visibility = View.VISIBLE
text = itemView.context.getString(R.string.dl_unpaid)
}
btnLeft.apply {
visibility = View.VISIBLE
text = itemView.context.getString(R.string.canceled_order_btn)
setOnClickListener {
showCancelOrderDialog(order.orderId.toString())
}
}
btnRight.apply {
visibility = View.VISIBLE
text = itemView.context.getString(R.string.sent_evidence)
setOnClickListener {
val intent = Intent(itemView.context, PaymentActivity::class.java)
// Menambahkan data yang diperlukan
intent.putExtra("ORDER_ID", order.orderId)
intent.putExtra("ORDER_PAYMENT_ID", order.paymentInfoId)
// Memulai aktivitas
itemView.context.startActivity(intent)
}
}
deadlineDate.apply {
visibility = View.VISIBLE
text = formatDatePay(order.updatedAt)
}
}
"processed" -> {
// Untuk status processed, tampilkan "Hubungi Penjual"
statusOrder.apply {
visibility = View.VISIBLE
text = itemView.context.getString(R.string.processed_orders)
}
deadlineLabel.apply {
visibility = View.VISIBLE
text = itemView.context.getString(R.string.dl_processed)
}
btnLeft.apply {
visibility = View.VISIBLE
text = itemView.context.getString(R.string.canceled_order_btn)
setOnClickListener {
showCancelOrderDialog(order.orderId.toString())
}
}
}
"shipped" -> {
// Untuk status shipped, tampilkan "Lacak Pengiriman" dan "Terima Barang"
statusOrder.apply {
visibility = View.VISIBLE
text = itemView.context.getString(R.string.shipped_orders)
}
deadlineLabel.apply {
visibility = View.VISIBLE
text = itemView.context.getString(R.string.dl_shipped)
}
btnLeft.apply {
visibility = View.VISIBLE
text = itemView.context.getString(R.string.claim_complaint)
setOnClickListener {
showCancelOrderDialog(order.orderId.toString())
// Handle click event
}
}
btnRight.apply {
visibility = View.VISIBLE
text = itemView.context.getString(R.string.claim_order)
setOnClickListener {
// Handle click event
viewModel.confirmOrderCompleted(order.orderId, "completed")
}
}
deadlineDate.apply {
visibility = View.VISIBLE
text = formatShipmentDate(order.updatedAt, order.etd.toInt())
}
}
"delivered" -> {
// Untuk status delivered, tampilkan "Beri Ulasan"
btnRight.apply {
visibility = View.VISIBLE
text = itemView.context.getString(R.string.add_review)
setOnClickListener {
// Handle click event
}
}
}
"completed" -> {
statusOrder.apply {
visibility = View.VISIBLE
text = itemView.context.getString(R.string.shipped_orders)
}
deadlineLabel.apply {
visibility = View.VISIBLE
text = itemView.context.getString(R.string.dl_shipped)
}
btnRight.apply {
visibility = View.VISIBLE
text = itemView.context.getString(R.string.add_review)
setOnClickListener {
// Handle click event
}
}
}
"canceled" -> {
statusOrder.apply {
visibility = View.VISIBLE
text = itemView.context.getString(R.string.canceled_orders)
}
}
}
}
private fun formatDate(dateString: String): String {
return try {
val inputFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.getDefault())
inputFormat.timeZone = TimeZone.getTimeZone("UTC")
val outputFormat = SimpleDateFormat("HH:mm dd MMMM yyyy", Locale("id", "ID"))
val date = inputFormat.parse(dateString)
date?.let {
val calendar = Calendar.getInstance()
calendar.time = it
calendar.set(Calendar.HOUR_OF_DAY, 23)
calendar.set(Calendar.MINUTE, 59)
outputFormat.format(calendar.time)
} ?: dateString
} catch (e: Exception) {
Log.e("DateFormatting", "Error formatting date: ${e.message}")
dateString
}
}
private fun formatDatePay(dateString: String): String {
return try {
// Parse the ISO 8601 date
val isoDateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.getDefault())
isoDateFormat.timeZone = TimeZone.getTimeZone("UTC")
val createdDate = isoDateFormat.parse(dateString)
// Add 24 hours to get due date
val calendar = Calendar.getInstance()
calendar.time = createdDate
calendar.add(Calendar.HOUR, 24)
val dueDate = calendar.time
// Format due date for display
val dueDateFormat = SimpleDateFormat("dd MMM yyyy", Locale.getDefault())
dueDateFormat.format(calendar.time)
} catch (e: Exception) {
Log.e("DateFormatting", "Error formatting date: ${e.message}")
dateString
}
}
private fun formatShipmentDate(dateString: String, estimate: Int): String {
return try {
// Parse the input date
val inputFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.getDefault())
inputFormat.timeZone = TimeZone.getTimeZone("UTC")
// Output format
val outputFormat = SimpleDateFormat("dd MMMM yyyy", Locale("id", "ID"))
// Parse the input date
val date = inputFormat.parse(dateString)
date?.let {
val calendar = Calendar.getInstance()
calendar.time = it
// Add estimated days
calendar.add(Calendar.DAY_OF_MONTH, estimate)
outputFormat.format(calendar.time)
} ?: dateString
} catch (e: Exception) {
Log.e("ShipmentDateFormatting", "Error formatting shipment date: ${e.message}")
dateString
}
}
private fun showCancelOrderDialog(orderId: String) {
val context = itemView.context
val dialog = Dialog(context)
dialog.requestWindowFeature(Window.FEATURE_NO_TITLE)
dialog.setContentView(R.layout.dialog_cancel_order)
dialog.setCancelable(true)
// Set the dialog width to match parent
val window = dialog.window
window?.setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
// Get references to the views in the dialog
val spinnerCancelReason = dialog.findViewById<AutoCompleteTextView>(R.id.spinnerCancelReason)
val tilCancelReason = dialog.findViewById<TextInputLayout>(R.id.tilCancelReason)
val btnCancelDialog = dialog.findViewById<MaterialButton>(R.id.btnCancelDialog)
val btnConfirmCancel = dialog.findViewById<MaterialButton>(R.id.btnConfirmCancel)
val ivComplaintImage = dialog.findViewById<ImageView>(R.id.ivComplaintImage)
val tvSelectImage = dialog.findViewById<TextView>(R.id.tvSelectImage)
// Set up the reasons dropdown
val reasons = context.resources.getStringArray(R.array.cancellation_reasons)
val adapter = ArrayAdapter(context, android.R.layout.simple_dropdown_item_1line, reasons)
spinnerCancelReason.setAdapter(adapter)
// For storing the selected image URI
var selectedImageUri: Uri? = null
// Set click listener for image selection
ivComplaintImage.setOnClickListener {
// Create an intent to open the image picker
val galleryIntent = Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI)
(context as? Activity)?.startActivityForResult(galleryIntent, REQUEST_IMAGE_PICK)
// Set up result handler in the activity
val activity = context as? Activity
activity?.let {
// Remove any existing callbacks to avoid memory leaks
if (imagePickCallback != null) {
imagePickCallback = null
}
// Create a new callback for this specific dialog
imagePickCallback = { uri ->
selectedImageUri = uri
// Load and display the selected image
ivComplaintImage.setImageURI(uri)
tvSelectImage.visibility = View.GONE
}
}
}
// Set click listeners for buttons
btnCancelDialog.setOnClickListener {
dialog.dismiss()
}
btnConfirmCancel.setOnClickListener {
val reason = spinnerCancelReason.text.toString().trim()
if (reason.isEmpty()) {
tilCancelReason.error = context.getString(R.string.please_select_cancellation_reason)
return@setOnClickListener
}
// Clear error if any
tilCancelReason.error = null
// Convert selected image to file if available
val imageFile = selectedImageUri?.let { uri ->
try {
// Get the file path from URI
val filePathColumn = arrayOf(MediaStore.Images.Media.DATA)
val cursor = context.contentResolver.query(uri, filePathColumn, null, null, null)
cursor?.use {
if (it.moveToFirst()) {
val columnIndex = it.getColumnIndex(filePathColumn[0])
val filePath = it.getString(columnIndex)
return@let File(filePath)
}
}
null
} catch (e: Exception) {
Log.e("OrderHistoryAdapter", "Error getting file from URI: ${e.message}")
null
}
}
// Show loading indicator
val loadingView = View(context).apply {
layoutParams = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
)
setBackgroundColor(Color.parseColor("#80000000"))
val progressBar = ProgressBar(context).apply {
layoutParams = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT
)
}
// addView(progressBar)
// (progressBar.layoutParams as? ViewGroup.MarginLayoutParams)?.apply {
// gravity = Gravity.CENTER
// }
}
dialog.addContentView(loadingView, loadingView.layoutParams)
// Call the ViewModel to cancel the order with image
viewModel.cancelOrderWithImage(orderId, reason, imageFile)
// Observe for success/failure
viewModel.isSuccess.observe(itemView.findViewTreeLifecycleOwner()!!) { isSuccess ->
// Remove loading indicator
(loadingView.parent as? ViewGroup)?.removeView(loadingView)
if (isSuccess) {
Toast.makeText(context, context.getString(R.string.order_canceled_successfully), Toast.LENGTH_SHORT).show()
dialog.dismiss()
// Find the order in the list and remove it or update its status
val position = orders.indexOfFirst { it.orderId.toString() == orderId }
if (position != -1) {
orders.removeAt(position)
notifyItemRemoved(position)
notifyItemRangeChanged(position, orders.size)
}
} else {
Toast.makeText(context, viewModel.message.value ?: context.getString(R.string.failed_to_cancel_order), Toast.LENGTH_SHORT).show()
}
}
}
dialog.show()
}
}
companion object {
private const val REQUEST_IMAGE_PICK = 100
private var imagePickCallback: ((Uri) -> Unit)? = null
// This method should be called from the activity's onActivityResult
fun handleImageResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (requestCode == REQUEST_IMAGE_PICK && resultCode == Activity.RESULT_OK && data != null) {
val selectedImageUri = data.data
selectedImageUri?.let { uri ->
imagePickCallback?.invoke(uri)
}
}
}
}
}

View File

@ -0,0 +1,64 @@
package com.alya.ecommerce_serang.ui.order.history
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import com.alya.ecommerce_serang.R
import com.alya.ecommerce_serang.databinding.FragmentOrderHistoryBinding
import com.alya.ecommerce_serang.utils.SessionManager
import com.google.android.material.tabs.TabLayoutMediator
class OrderHistoryFragment : Fragment() {
private var _binding: FragmentOrderHistoryBinding? = null
private val binding get() = _binding!!
private lateinit var sessionManager: SessionManager
private lateinit var viewPagerAdapter: OrderViewPagerAdapter
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentOrderHistoryBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
sessionManager = SessionManager(requireContext())
setupViewPager()
}
private fun setupViewPager() {
// Initialize the ViewPager adapter
viewPagerAdapter = OrderViewPagerAdapter(requireActivity())
binding.viewPager.adapter = viewPagerAdapter
// Connect TabLayout with ViewPager2
TabLayoutMediator(binding.tabLayout, binding.viewPager) { tab, position ->
tab.text = when (position) {
0 -> getString(R.string.all_orders)
1 -> getString(R.string.pending_orders)
2 -> getString(R.string.unpaid_orders)
3 -> getString(R.string.processed_orders)
4 -> getString(R.string.paid_orders)
5 -> getString(R.string.shipped_orders)
6 -> getString(R.string.delivered_orders)
7 -> getString(R.string.completed_orders)
8 -> getString(R.string.canceled_orders)
else -> "Tab $position"
}
}.attach()
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}

View File

@ -0,0 +1,151 @@
package com.alya.ecommerce_serang.ui.order.history
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.recyclerview.widget.LinearLayoutManager
import com.alya.ecommerce_serang.data.api.response.order.OrdersItem
import com.alya.ecommerce_serang.data.api.retrofit.ApiConfig
import com.alya.ecommerce_serang.data.repository.OrderRepository
import com.alya.ecommerce_serang.data.repository.Result
import com.alya.ecommerce_serang.databinding.FragmentOrderListBinding
import com.alya.ecommerce_serang.ui.order.address.ViewState
import com.alya.ecommerce_serang.utils.BaseViewModelFactory
import com.alya.ecommerce_serang.utils.SessionManager
class OrderListFragment : Fragment() {
private var _binding: FragmentOrderListBinding? = null
private val binding get() = _binding!!
private lateinit var sessionManager: SessionManager
private val viewModel: HistoryViewModel by viewModels {
BaseViewModelFactory {
val apiService = ApiConfig.getApiService(sessionManager)
val orderRepository = OrderRepository(apiService)
HistoryViewModel(orderRepository)
}
}
private lateinit var orderAdapter: OrderHistoryAdapter
private var status: String = "all"
companion object {
private const val ARG_STATUS = "status"
fun newInstance(status: String): OrderListFragment {
return OrderListFragment().apply {
arguments = Bundle().apply {
putString(ARG_STATUS, status)
}
}
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
sessionManager = SessionManager(requireContext())
arguments?.let {
status = it.getString(ARG_STATUS) ?: "all"
}
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentOrderListBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupRecyclerView()
observeOrderList()
observeOrderCompletionStatus()
loadOrders()
}
private fun setupRecyclerView() {
orderAdapter = OrderHistoryAdapter(
onOrderClickListener = { order ->
// Handle order click
navigateToOrderDetail(order)
},
viewModel = viewModel // Pass the ViewModel to the adapter
)
orderAdapter.setFragmentStatus(status)
binding.rvOrders.apply {
layoutManager = LinearLayoutManager(requireContext())
adapter = orderAdapter
}
}
private fun observeOrderList() {
viewModel.orders.observe(viewLifecycleOwner) { result ->
when (result) {
is ViewState.Success -> {
binding.progressBar.visibility = View.GONE
if (result.data.isNullOrEmpty()) {
binding.tvEmptyState.visibility = View.VISIBLE
binding.rvOrders.visibility = View.GONE
} else {
binding.tvEmptyState.visibility = View.GONE
binding.rvOrders.visibility = View.VISIBLE
orderAdapter.submitList(result.data)
}
}
is ViewState.Error -> {
binding.progressBar.visibility = View.GONE
binding.tvEmptyState.visibility = View.VISIBLE
Toast.makeText(requireContext(), result.message, Toast.LENGTH_SHORT).show()
}
is ViewState.Loading -> {
null
}
}
}
}
private fun loadOrders() {
viewModel.getOrderList(status)
}
private fun navigateToOrderDetail(order: OrdersItem) {
// In a real app, you would navigate to order detail screen
// For example: findNavController().navigate(OrderListFragmentDirections.actionToOrderDetail(order.orderId))
Toast.makeText(requireContext(), "Order ID: ${order.orderId}", Toast.LENGTH_SHORT).show()
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
private fun observeOrderCompletionStatus(){
viewModel.orderCompletionStatus.observe(viewLifecycleOwner){ result ->
when(result){
is Result.Loading -> {
}
is Result.Success -> {
Toast.makeText(requireContext(), "Order completed successfully!", Toast.LENGTH_SHORT).show()
loadOrders()
}
is Result.Error -> {
Toast.makeText(requireContext(), "Failed to complete order: ${result.exception.message}", Toast.LENGTH_SHORT).show()
}
}
}
}
}

View File

@ -0,0 +1,68 @@
package com.alya.ecommerce_serang.ui.order.history
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import com.alya.ecommerce_serang.R
import com.alya.ecommerce_serang.data.api.response.order.OrderItemsItem
import com.bumptech.glide.Glide
import com.google.android.material.button.MaterialButton
class OrderProductAdapter : RecyclerView.Adapter<OrderProductAdapter.ProductViewHolder>() {
private val products = mutableListOf<OrderItemsItem>()
fun submitList(newProducts: List<OrderItemsItem>) {
products.clear()
products.addAll(newProducts)
notifyDataSetChanged()
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ProductViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.item_order_product, parent, false)
return ProductViewHolder(view)
}
override fun onBindViewHolder(holder: ProductViewHolder, position: Int) {
holder.bind(products[position])
}
override fun getItemCount(): Int = products.size
inner class ProductViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
private val ivProductImage: ImageView = itemView.findViewById(R.id.iv_product)
private val tvProductName: TextView = itemView.findViewById(R.id.tv_product_name)
private val tvQuantity: TextView = itemView.findViewById(R.id.tv_product_quantity)
private val tvProductPrice: TextView = itemView.findViewById(R.id.tv_product_price)
fun bind(product: OrderItemsItem) {
// Set product name
tvProductName.text = product.productName
// Set quantity with suffix
tvQuantity.text = "${product.quantity} buah"
// Set price with currency format
tvProductPrice.text = formatCurrency(product.price)
// Load product image using Glide
Glide.with(itemView.context)
.load(product.productImage)
.placeholder(R.drawable.placeholder_image)
// .error(R.drawable.error_image)
.into(ivProductImage)
}
private fun formatCurrency(amount: Int): String {
// In a real app, you would use NumberFormat for proper currency formatting
// For simplicity, just return a basic formatted string
return "Rp${amount}"
}
}
}

View File

@ -0,0 +1,30 @@
package com.alya.ecommerce_serang.ui.order.history
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import androidx.viewpager2.adapter.FragmentStateAdapter
class OrderViewPagerAdapter(
fragmentActivity: FragmentActivity
) : FragmentStateAdapter(fragmentActivity) {
// Define all possible order statuses
private val orderStatuses = listOf(
"all", // All orders
"pending", // Menunggu Tagihan
"unpaid", // Belum Dibayar
"processed", // Diproses
"paid", // Dibayar
"shipped", // Dikirim
"delivered", // Diterima
"completed", // Selesai
"canceled" // Dibatalkan
)
override fun getItemCount(): Int = orderStatuses.size
override fun createFragment(position: Int): Fragment {
// Create a new instance of OrderListFragment with the appropriate status
return OrderListFragment.newInstance(orderStatuses[position])
}
}

View File

@ -15,6 +15,7 @@ import com.alya.ecommerce_serang.data.api.dto.UserProfile
import com.alya.ecommerce_serang.data.api.retrofit.ApiConfig
import com.alya.ecommerce_serang.data.repository.UserRepository
import com.alya.ecommerce_serang.databinding.FragmentProfileBinding
import com.alya.ecommerce_serang.ui.order.history.HistoryActivity
import com.alya.ecommerce_serang.ui.profile.mystore.MyStoreActivity
import com.alya.ecommerce_serang.utils.BaseViewModelFactory
import com.alya.ecommerce_serang.utils.SessionManager
@ -63,6 +64,16 @@ class ProfileFragment : Fragment() {
val intentDetail = Intent(requireContext(), DetailProfileActivity::class.java)
startActivity(intentDetail)
}
binding.tvLihatRiwayat.setOnClickListener{
val intent = Intent(requireContext(), HistoryActivity::class.java)
startActivity(intent)
}
binding.cardPesanan.setOnClickListener{
val intent = Intent(requireContext(), HistoryActivity::class.java)
startActivity(intent)
}
}
private fun observeUserProfile() {

View File

@ -52,23 +52,6 @@ class HomeViewModel (
loadProducts()
loadCategories()
}
// private fun fetchUserData() {
// viewModelScope.launch {
// try {
// val response = apiService.getProtectedData() // Example API request
// if (response.isSuccessful) {
// val data = response.body()
// Log.d("HomeFragment", "User Data: $data")
// // Update UI with data
// } else {
// Log.e("HomeFragment", "Error: ${response.message()}")
// }
// } catch (e: Exception) {
// Log.e("HomeFragment", "Exception: ${e.message}")
// }
// }
// }
}
sealed class HomeUiState {

View File

@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#211E1E" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M14,2L6,2c-1.1,0 -1.99,0.9 -1.99,2L4,20c0,1.1 0.89,2 1.99,2L18,22c1.1,0 2,-0.9 2,-2L20,8l-6,-6zM18,20L6,20L6,4h7v5h5v11zM8,15.01l1.41,1.41L11,14.84L11,19h2v-4.16l1.59,1.59L16,15.01 12.01,11z"/>
</vector>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@color/blue_500" />
<corners android:radius="8dp" />
</shape>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<corners android:radius="8dp" />
<stroke
android:width="2dp"
android:color="@color/white" />
</shape>

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<stroke
android:width="2dp"
android:color="#BCBCBC"
android:dashWidth="10dp"
android:dashGap="6dp" />
<corners android:radius="8dp" />
<solid android:color="#F5F5F5" />
</shape>

View File

@ -115,10 +115,21 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="none"
android:focusable="false"
android:clickable="true"
android:padding="12dp"
android:textSize="14sp" />
</com.google.android.material.textfield.TextInputLayout>
<ProgressBar
android:id="@+id/progressBar"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_gravity="center_horizontal"
android:layout_marginBottom="8dp"
android:visibility="gone" />
<!-- Kabupaten / Kota -->
<TextView
android:layout_width="wrap_content"
@ -138,11 +149,20 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="none"
android:hint="Masukkan Kabupaten"
android:focusable="false"
android:clickable="true"
android:padding="12dp"
android:textSize="14sp" />
</com.google.android.material.textfield.TextInputLayout>
<ProgressBar
android:id="@+id/cityProgressBar"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_gravity="center_horizontal"
android:layout_marginBottom="8dp"
android:visibility="gone" />
<!-- Kecamatan / Desa -->
<TextView
android:layout_width="wrap_content"
@ -188,6 +208,8 @@
</LinearLayout>
</ScrollView>
<Button
android:id="@+id/buttonSimpan"
android:layout_width="match_parent"
@ -200,5 +222,60 @@
android:textSize="16sp"
app:layout_constraintBottom_toBottomOf="parent" />
<ProgressBar
android:id="@+id/submitProgressBar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="16dp"
android:visibility="gone"
app:layout_constraintTop_toBottomOf="@id/buttonSimpan"
app:layout_constraintEnd_toEndOf="parent"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginTop="16dp"
android:gravity="center_vertical"
app:layout_constraintTop_toBottomOf="@id/buttonSimpan"
app:layout_constraintEnd_toEndOf="parent">
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Status Lokasi"
android:textColor="@android:color/black"
android:textSize="14sp" />
<TextView
android:id="@+id/tvLocationStatus"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="Menunggu lokasi..."
android:textSize="12sp" />
</LinearLayout>
<ProgressBar
android:id="@+id/locationProgressBar"
android:layout_width="24dp"
android:layout_height="24dp"
android:visibility="gone" />
<Button
android:id="@+id/btnReloadLocation"
android:layout_width="wrap_content"
android:layout_height="36dp"
android:text="Reload"
android:textSize="12sp"
android:layout_marginStart="8dp"
android:textAllCaps="false" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,185 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.order.detail.AddEvidencePaymentActivity">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="#FFFFFF"
app:navigationIcon="@drawable/ic_back_24"
app:layout_constraintTop_toTopOf="parent">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:text="Kirim Bukti Bayar"
android:textColor="#000000"
android:textSize="18sp"
android:layout_marginVertical="8dp"
android:fontFamily="@font/dmsans_bold" />
</androidx.appcompat.widget.Toolbar>
<View
android:id="@+id/divider"
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="#EEEEEE"
app:layout_constraintTop_toBottomOf="@id/toolbar" />
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@id/divider">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="Unggah Foto *"
android:fontFamily="@font/dmsans_semibold"
android:textSize="16sp" />
<androidx.cardview.widget.CardView
android:id="@+id/cardAddPhoto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
app:cardCornerRadius="8dp"
app:cardElevation="0dp"
app:cardBackgroundColor="#FFFFFF">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="8dp">
<TextView
android:id="@+id/tvAddPhoto"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Tambah Foto"
android:textColor="#1E88E5"
android:textSize="14sp" />
<FrameLayout
android:id="@+id/frameUploadImage"
android:layout_width="120dp"
android:layout_height="120dp"
android:layout_marginTop="8dp"
android:background="@drawable/edit_text_background"
android:padding="8dp">
<ImageView
android:id="@+id/ivUploadedImage"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="centerCrop"
android:visibility="gone" />
<LinearLayout
android:id="@+id/layoutUploadPlaceholder"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/edit_text_background"
android:gravity="center"
android:orientation="vertical">
<ImageView
android:layout_width="48dp"
android:layout_height="48dp"
android:src="@drawable/baseline_upload_file_24" />
</LinearLayout>
</FrameLayout>
</LinearLayout>
</androidx.cardview.widget.CardView>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="Metode Pembayaran *"
android:fontFamily="@font/dmsans_semibold"
android:textSize="16sp" />
<Spinner
android:id="@+id/spinnerPaymentMethod"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:background="@drawable/edit_text_background"
android:minHeight="50dp"
android:padding="12dp" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="Nomor Rekening / Nomor HP *"
android:fontFamily="@font/dmsans_semibold"
android:textSize="16sp" />
<EditText
android:id="@+id/etAccountNumber"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:background="@drawable/edit_text_background"
android:hint="Isi nomor rekening atau nomor hp pembayaran"
android:inputType="text"
android:minHeight="50dp"
android:textSize="14sp"
android:padding="12dp" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="Tanggal Pembayaran *"
android:fontFamily="@font/dmsans_semibold"
android:textSize="16sp" />
<TextView
android:id="@+id/tvPaymentDate"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:background="@drawable/edit_text_background"
android:drawableEnd="@drawable/ic_calendar"
android:drawablePadding="8dp"
android:hint="Pilih tanggal"
android:minHeight="50dp"
android:padding="12dp" />
<Button
android:id="@+id/btnSubmit"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:layout_marginBottom="16dp"
android:background="@drawable/bg_button_filled"
android:text="Kirim"
android:textAllCaps="false"
android:textColor="#FFFFFF"
android:textSize="16sp"
android:padding="12dp" />
</LinearLayout>
</androidx.core.widget.NestedScrollView>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -24,6 +24,7 @@
app:title="Alamat Pengiriman " />
<TextView
android:id="@+id/add_address_click"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAlignment="textEnd"

View File

@ -287,8 +287,7 @@
android:id="@+id/rv_payment_methods"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:listitem="@layout/item_payment_method"
tools:itemCount="2" />
tools:listitem="@layout/item_payment_method" />
</LinearLayout>
<View

View File

@ -0,0 +1,58 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.order.history.HistoryActivity">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@android:color/white"
android:elevation="4dp"
app:layout_constraintTop_toTopOf="parent">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageButton
android:id="@+id/btnBack"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="Kembali"
android:src="@drawable/ic_back_24"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/tvTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:text="Riwayat Pesanan"
android:textColor="@android:color/black"
android:textSize="18sp"
android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@+id/btnBack"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.appcompat.widget.Toolbar>
<androidx.fragment.app.FragmentContainerView
android:id="@+id/fragment_container_history"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@id/toolbar" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,244 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/white"
tools:context=".ui.order.detail.PaymentActivity">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/appBarLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="@android:color/white"
app:navigationIcon="@drawable/ic_back_24">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Pembayaran"
android:fontFamily="@font/dmsans_bold"
android:textSize="18sp" />
</androidx.appcompat.widget.Toolbar>
</com.google.android.material.appbar.AppBarLayout>
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintBottom_toTopOf="@id/buttonLayout"
app:layout_constraintTop_toBottomOf="@id/appBarLayout">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Total Bayar"
android:textColor="@android:color/black"
android:textSize="16sp" />
<TextView
android:id="@+id/tvTotalAmount"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="end"
android:text="Rp75.000"
android:textColor="@color/blue_500"
android:textSize="18sp"
android:textStyle="bold" />
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginTop="16dp"
android:background="#E0E0E0" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="Bayar Dalam"
android:textColor="@android:color/black"
android:textSize="16sp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:id="@+id/tvRemainingTime"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="end"
android:text="23 jam 15 menit"
android:textColor="@color/blue_500"
android:textSize="16sp" />
</LinearLayout>
<TextView
android:id="@+id/tvDueDate"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="end"
android:text="Jatuh tempo 15 Nov 2024"
android:textColor="#808080"
android:textSize="14sp" />
<androidx.cardview.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
app:cardCornerRadius="8dp"
app:cardElevation="4dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="16dp">
<ImageView
android:id="@+id/ivBankLogo"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_gravity="center_vertical"
android:src="@drawable/outline_store_24" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Nomor Rekening:"
android:textColor="#808080"
android:textSize="14sp" />
<TextView
android:id="@+id/tvAccountNumber"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="01233435436363757537856"
android:textColor="@android:color/black"
android:textSize="16sp" />
<TextView
android:id="@+id/tvBankName"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Bank BCA"
android:textColor="@android:color/black"
android:textSize="16sp"
android:textStyle="bold" />
</LinearLayout>
</LinearLayout>
</androidx.cardview.widget.CardView>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginTop="16dp"
android:background="#E0E0E0" />
<LinearLayout
android:id="@+id/layoutMBankingInstructions"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:orientation="horizontal"
android:padding="8dp">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Petunjuk Transfer mBanking"
android:textColor="@android:color/black"
android:textSize="16sp" />
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:src="@drawable/ic_arrow_right" />
</LinearLayout>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="#E0E0E0" />
<LinearLayout
android:id="@+id/layoutATMInstructions"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="8dp">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Petunjuk Transfer ATM"
android:textColor="@android:color/black"
android:textSize="16sp" />
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:src="@drawable/ic_arrow_right" />
</LinearLayout>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="#E0E0E0" />
</LinearLayout>
</androidx.core.widget.NestedScrollView>
<LinearLayout
android:id="@+id/buttonLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp"
app:layout_constraintBottom_toBottomOf="parent">
<com.google.android.material.button.MaterialButton
android:id="@+id/btnNegotiatePrice"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:background="@drawable/bg_button_outline"
android:text="Negosiasi Harga"
android:textAllCaps="false"
android:textColor="@color/blue_500" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btnUploadPaymentProof"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/bg_button_filled"
android:text="Kirim Bukti Bayar"
android:textAllCaps="false"
android:textColor="@android:color/white" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -18,6 +18,7 @@
app:title="Pengiriman" />
<LinearLayout
android:id="@+id/linear_shipment"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
@ -29,4 +30,13 @@
android:layout_height="wrap_content"
tools:listitem="@layout/item_shipping_order"/>
</LinearLayout>
<ProgressBar
android:id="@+id/progressBar"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_gravity="center_horizontal"
android:layout_marginBottom="8dp"
android:visibility="gone"
app:layout_constraintTop_toBottomOf="@id/linear_shipment"
app:layout_constraintEnd_toEndOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,103 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:cardCornerRadius="16dp"
app:cardElevation="8dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="24dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:text="@string/cancel_order_confirmation"
android:textAlignment="center"
android:fontFamily="@font/dmsans_semibold"
android:textAppearance="@style/TextAppearance.MaterialComponents.Headline6" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/tilCancelReason"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.ExposedDropdownMenu"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:hint="@string/reason_for_cancellation">
<AutoCompleteTextView
android:id="@+id/spinnerCancelReason"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="none"
android:focusable="false" />
</com.google.android.material.textfield.TextInputLayout>
<!-- Image Upload Section -->
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/upload_evidence"
android:layout_marginBottom="8dp"
android:fontFamily="@font/dmsans_medium"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1" />
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp">
<ImageView
android:id="@+id/ivComplaintImage"
android:layout_width="match_parent"
android:layout_height="150dp"
android:scaleType="centerCrop"
android:background="@drawable/bg_dashboard_border"
android:contentDescription="@string/complaint_image" />
<TextView
android:id="@+id/tvSelectImage"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="@string/tap_to_select_image"
android:drawableTop="@drawable/baseline_upload_file_24"
android:drawablePadding="8dp"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body2" />
</FrameLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<com.google.android.material.button.MaterialButton
android:id="@+id/btnCancelDialog"
style="@style/RoundedBorderStyle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:layout_weight="1"
android:fontFamily="@font/dmsans_semibold"
android:textColor="@color/blue_500"
android:text="@string/cancel" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btnConfirmCancel"
style="@style/RoundedBorderStyleFilled"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_weight="1"
android:fontFamily="@font/dmsans_semibold"
android:text="@string/confirm" />
</LinearLayout>
</LinearLayout>
</androidx.cardview.widget.CardView>

View File

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.material.tabs.TabLayout
android:id="@+id/tabLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@android:color/white"
app:layout_constraintTop_toTopOf="parent"
app:tabGravity="fill"
app:tabIndicatorColor="@color/blue_200"
app:tabMode="scrollable"
app:tabSelectedTextColor="@color/blue_200"
app:tabTextColor="@android:color/darker_gray" />
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/viewPager"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tabLayout" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,40 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#F5F5F5">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rvOrders"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:padding="8dp"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:listitem="@layout/item_order_history" />
<TextView
android:id="@+id/tvEmptyState"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/no_available_orders"
android:textSize="16sp"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ProgressBar
android:id="@+id/progressBar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,158 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
app:cardCornerRadius="8dp"
app:cardElevation="2dp">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp">
<TextView
android:id="@+id/tvStoreName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@android:color/black"
android:textSize="16sp"
android:textStyle="bold"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="SnackEnak" />
<TextView
android:id="@+id/tvOrderStatus"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/blue_500"
android:textSize="14sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="Menunggu Tagihan" />
<View
android:id="@+id/divider"
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginTop="8dp"
android:background="#E0E0E0"
app:layout_constraintTop_toBottomOf="@+id/tvStoreName" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rvOrderItems"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_constraintTop_toBottomOf="@+id/divider"
tools:itemCount="1"
tools:listitem="@layout/item_order_product" />
<TextView
android:id="@+id/tvShowMore"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:gravity="center_vertical"
android:textColor="@android:color/darker_gray"
android:textSize="14sp"
android:visibility="visible"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/rvOrderItems"
app:layout_constraintEnd_toEndOf="parent"
tools:text="@string/show_more_product" />
<View
android:id="@+id/divider2"
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginTop="8dp"
android:background="#E0E0E0"
app:layout_constraintTop_toBottomOf="@+id/tvShowMore" />
<TextView
android:id="@+id/tv_count_total_item"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@string/item_count_prod"
android:textSize="14sp"
android:fontFamily="@font/dmsans_bold"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/divider2" />
<TextView
android:id="@+id/tvTotalLabel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@string/total_order"
android:textColor="@android:color/black"
android:textSize="14sp"
android:fontFamily="@font/dmsans_bold"
app:layout_constraintEnd_toStartOf="@+id/tvTotalAmount"
app:layout_constraintTop_toBottomOf="@+id/divider2"/>
<TextView
android:id="@+id/tvTotalAmount"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:textColor="@android:color/black"
android:textSize="14sp"
android:fontFamily="@font/dmsans_semibold"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/divider2"
tools:text="Rp500.000" />
<TextView
android:id="@+id/tvDeadlineLabel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@string/batas_tagihan"
android:textSize="14sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tv_count_total_item" />
<TextView
android:id="@+id/tvDeadlineDate"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:layout_marginTop="8dp"
android:textColor="@android:color/black"
android:textSize="14sp"
app:layout_constraintStart_toEndOf="@+id/tvDeadlineLabel"
app:layout_constraintTop_toBottomOf="@+id/tv_count_total_item"
tools:text="28 Oktober 2024" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_left"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/bg_button_outline"
android:backgroundTint="@color/white"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@id/tvDeadlineDate"
app:layout_constraintStart_toStartOf="parent"
android:layout_marginTop="8dp"/>
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_right"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/bg_button_filled"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
android:text="Kirim Bukti Bayar"
app:layout_constraintTop_toBottomOf="@id/tvDeadlineDate"
app:layout_constraintEnd_toEndOf="parent"
android:layout_marginTop="8dp"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.cardview.widget.CardView>

View File

@ -0,0 +1,57 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="8dp"
android:paddingBottom="8dp">
<ImageView
android:id="@+id/ivProductImage"
android:layout_width="60dp"
android:layout_height="60dp"
android:background="#F0F0F0"
android:scaleType="centerCrop"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:src="@tools:sample/avatars" />
<TextView
android:id="@+id/tvProductName"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:layout_marginEnd="12dp"
android:ellipsize="end"
android:maxLines="2"
android:textColor="@android:color/black"
android:textSize="14sp"
app:layout_constraintEnd_toStartOf="@+id/tvProductPrice"
app:layout_constraintStart_toEndOf="@+id/ivProductImage"
app:layout_constraintTop_toTopOf="parent"
tools:text="Jaket Pink Fuschia" />
<TextView
android:id="@+id/tvQuantity"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:layout_marginTop="4dp"
android:textColor="@android:color/darker_gray"
android:textSize="14sp"
app:layout_constraintStart_toEndOf="@+id/ivProductImage"
app:layout_constraintTop_toBottomOf="@+id/tvProductName"
tools:text="2 buah" />
<TextView
android:id="@+id/tvProductPrice"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@android:color/black"
android:textSize="14sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="Rp150.000" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,75 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
app:cardCornerRadius="8dp"
app:cardElevation="2dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="12dp">
<ImageView
android:id="@+id/ivProductImage"
android:layout_width="80dp"
android:layout_height="80dp"
android:scaleType="centerCrop"
tools:src="@drawable/placeholder_image" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:orientation="vertical">
<TextView
android:id="@+id/tvStoreName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="#808080"
android:textSize="14sp"
tools:text="Store Name" />
<TextView
android:id="@+id/tvProductName"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:ellipsize="end"
android:maxLines="2"
android:textColor="@android:color/black"
android:textSize="16sp"
tools:text="Product Name with potentially long description that might get truncated" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:orientation="horizontal">
<TextView
android:id="@+id/tvProductPrice"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:textColor="@color/blue_500"
android:textSize="16sp"
android:textStyle="bold"
tools:text="Rp50.000" />
<TextView
android:id="@+id/tvQuantity"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="#808080"
android:textSize="14sp"
tools:text="x2" />
</LinearLayout>
</LinearLayout>
</LinearLayout>
</androidx.cardview.widget.CardView>

View File

@ -61,4 +61,61 @@
<string name="add_to_cart">Keranjang</string>
<string name="beli_sekarang">Beli Sekarang</string>
<string name="hello_blank_fragment">Hello blank fragment</string>
<string name="batas_tagihan">Batas Tagihan :</string>
<string name="total_order">Total: </string>
<string name="dl_date_wait">23:59 15 April 2025</string>
<string name="no_available_orders">Tidak ada order</string>
<string name="item_count_prod">%d produk</string>
<string name="show_more_product">%d produk lainnya</string>
<!-- status order-->
<string name="all_orders">Semua Pesanan </string>
<string name="pending_orders">Menunggu Tagihan</string>
<string name="unpaid_orders">Konfrimasi Bayar</string>
<string name="processed_orders">Diproses</string>
<string name="paid_orders">Sudah Dibayar</string>
<string name="delivered_orders">Pesanan Sampai</string>
<string name="completed_orders">Selesai</string>
<string name="shipped_orders">Dikirim</string>
<string name="canceled_orders">Dibatalkan</string>
<!-- deadline label -->
<string name="dl_pending">Batas Tagihan</string>
<string name="dl_unpaid">Batas Pembayaran</string>
<string name="dl_processed">Batas Pengiriman</string>
<string name="dal_paid">Semua Pesanan </string>
<string name="dl_delivered">Semua Pesanan </string>
<string name="dl_completed">Semua Pesanan </string>
<string name="dl_shipped">Tanggal Pesanan Sampai</string>
<string name="dl_canceled">Semua Pesanan </string>
<string name="sent_evidence">Kirim Bukti Pembayaran </string>
<string name="canceled_order_btn">Batalkan Pesanan </string>
<string name="claim_complaint">Ajukan Komplain </string>
<string name="claim_order">Pesanan Diterima </string>
<string name="add_review">Beri Ulasan </string>
<string name="warning_icon">Warning Icon</string>
<string name="cancel_order_confirmation">Apakah anda yakin ingin membatalkan pesanan?</string>
<string name="reason_for_cancellation">Alasan Batalkan Pesanan</string>
<string name="cancel">Kembali</string>
<string name="confirm">Batalkan Pesanan</string>
<string name="order_canceled_successfully">Order canceled successfully</string>
<string name="failed_to_cancel_order">Failed to cancel order</string>
<string name="please_select_cancellation_reason">Please select a reason for cancellation</string>
<string name="upload_evidence">Unggah Bukti Komplain</string>
<string name="complaint_image">Complaint evidence image</string>
<string name="tap_to_select_image">Tekan untuk unggah foto</string>
<string name="please_select_image">Please select an image as evidence</string>
<string name="image_too_large">Image is too large. Please select a smaller image.</string>
<!-- Cancellation Reasons -->
<string-array name="cancellation_reasons">
<item>Found a better price elsewhere</item>
<item>Changed my mind about the product</item>
<item>Ordered the wrong item</item>
<item>Shipping time is too long</item>
<item>Financial reasons</item>
<item>Other reason</item>
</string-array>
</resources>

View File

@ -7,4 +7,22 @@
<item name="android:padding">12dp</item>
<!-- Add more style attributes as needed -->
</style>
<style name="RoundedBorderStyle">
<!-- This style can be applied to views -->
<!-- <item name="android:background">@drawable/bg_button_outline</item>-->
<item name="strokeColor">@color/blue_500</item>
<item name="strokeWidth">2dp</item>
<item name="cornerRadius">8dp</item>
<item name="backgroundTint">@android:color/transparent</item>
</style>
<style name="RoundedBorderStyleFilled">
<!-- This style can be applied to views -->
<!-- <item name="android:background">@drawable/bg_button_outline</item>-->
<item name="strokeColor">@color/blue_500</item>
<item name="strokeWidth">2dp</item>
<item name="cornerRadius">8dp</item>
<item name="backgroundTint">@color/blue_500</item>
</style>
</resources>