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

# Conflicts:
#	app/src/main/AndroidManifest.xml
#	gradle/libs.versions.toml
This commit is contained in:
shaulascr
2025-05-21 12:19:24 +07:00
123 changed files with 10495 additions and 1452 deletions

View File

@ -8,10 +8,10 @@
<InsightsFilterSettings>
<option name="connection">
<ConnectionSetting>
<option name="appId" value="PLACEHOLDER" />
<option name="mobileSdkAppId" value="" />
<option name="projectId" value="" />
<option name="projectNumber" value="" />
<option name="appId" value="com.alya.ecommerce_serang" />
<option name="mobileSdkAppId" value="1:284675201257:android:2755670e3dbb1b48683878" />
<option name="projectId" value="ecommerce-serang" />
<option name="projectNumber" value="284675201257" />
</ConnectionSetting>
</option>
<option name="signal" value="SIGNAL_UNSPECIFIED" />

2
.idea/kotlinc.xml generated
View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="KotlinJpsPluginSettings">
<option name="version" value="1.9.0" />
<option name="version" value="2.1.0" />
</component>
</project>

View File

@ -6,6 +6,7 @@ plugins {
id("androidx.navigation.safeargs")
id("kotlin-parcelize")
alias(libs.plugins.dagger.hilt) // Use alias from catalog
id("com.google.gms.google-services")
}
val localProperties = Properties().apply {
@ -22,8 +23,8 @@ android {
defaultConfig {
applicationId = "com.alya.ecommerce_serang"
minSdk = 21
targetSdk = 34
minSdk = 26
targetSdk = 35
versionCode = 1
versionName = "1.0"
@ -119,6 +120,11 @@ dependencies {
implementation("io.socket:socket.io-client:2.1.0") // or latest version
//fcm token
implementation(platform("com.google.firebase:firebase-bom:33.13.0"))
implementation("com.google.firebase:firebase-analytics")
implementation("com.google.firebase:firebase-messaging-ktx")
}

29
app/google-services.json Normal file
View File

@ -0,0 +1,29 @@
{
"project_info": {
"project_number": "284675201257",
"project_id": "ecommerce-serang",
"storage_bucket": "ecommerce-serang.firebasestorage.app"
},
"client": [
{
"client_info": {
"mobilesdk_app_id": "1:284675201257:android:2755670e3dbb1b48683878",
"android_client_info": {
"package_name": "com.alya.ecommerce_serang"
}
},
"oauth_client": [],
"api_key": [
{
"current_key": "AIzaSyB-nWHsVbdV4PPIH06JZSStIVXjv9Qc4iU"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": []
}
}
}
],
"configuration_version": "1"
}

View File

@ -29,6 +29,18 @@
android:theme="@style/Theme.Ecommerce_serang"
android:usesCleartextTraffic="true"
tools:targetApi="31">
<activity
android:name=".ui.auth.RegisterStoreActivity"
android:exported="false" />
<activity
android:name=".ui.profile.editprofile.EditProfileCustActivity"
android:exported="false" />
<activity
android:name=".ui.order.history.detailorder.DetailOrderStatusActivity"
android:exported="false" />
<activity
android:name=".ui.order.review.CreateReviewActivity"
android:exported="false" />
<activity
android:name=".ui.profile.mystore.sells.shipment.ShipmentConfirmationActivity"
android:exported="false" />
@ -41,6 +53,9 @@
<activity
android:name=".ui.profile.mystore.sells.payment.DetailPaymentActivity"
android:exported="false" />
<activity
android:name=".ui.profile.mystore.sells.order.DetailOrderActivity"
android:exported="false" />
<activity
android:name=".ui.chat.ChatActivity"
android:exported="false" /> <!-- <provider -->
@ -52,7 +67,6 @@
android:enabled="true"
android:exported="false"
android:foregroundServiceType="dataSync" />
<activity
android:name=".ui.profile.mystore.profile.shipping_service.ShippingServiceActivity"
android:exported="false" />
@ -69,7 +83,7 @@
android:name=".ui.order.detail.PaymentActivity"
android:exported="false" />
<activity
android:name=".data.api.response.customer.cart.CartActivity"
android:name=".ui.cart.CartActivity"
android:exported="false" />
<activity
android:name=".ui.order.address.EditAddressActivity"
@ -140,6 +154,24 @@
<activity
android:name=".ui.MainActivity"
android:exported="false" />
<service
android:name=".ui.notif.fcm.FCMService"
android:exported="false">
<intent-filter>
<action android:name="com.google.firebase.MESSAGING_EVENT" />
</intent-filter>
</service>
<meta-data
android:name="com.google.firebase.messaging.default_notification_icon"
android:resource="@drawable/outline_notifications_24" />
<meta-data
android:name="com.google.firebase.messaging.default_notification_color"
android:resource="@color/blue_500" />
<meta-data
android:name="com.google.firebase.messaging.default_notification_channel_id"
android:value="fcm_default_channel" />
</application>
</manifest>

View File

@ -5,14 +5,13 @@ import dagger.hilt.android.HiltAndroidApp
@HiltAndroidApp
class App : Application(){
// override fun onCreate() {
// super.onCreate()
// private val TAG = "AppSerang"
//
//// var tokenTes: String? = null
//
// override fun onCreate() {
//
// val sessionManager = SessionManager(this)
// if (sessionManager.getUserId() != null) {
// val serviceIntent = Intent(this, SimpleWebSocketService::class.java)
// startService(serviceIntent)
// }
// }
}

View File

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

View File

@ -8,4 +8,5 @@ data class CartItem (
@SerializedName("quantity")
val quantity: Int
)
)

View File

@ -12,5 +12,6 @@ data class CheckoutData(
val sellerId: Int = 0,
val quantity: Int = 1,
val isBuyNow: Boolean = false,
val cartItems: List<CartItemsItem> = emptyList()
val cartItems: List<CartItemsItem> = emptyList(),
val cartItemWholesaleMap: Map<Int, Boolean> = emptyMap()
)

View File

@ -4,10 +4,10 @@ import com.google.gson.annotations.SerializedName
data class CreateAddressRequest (
@SerializedName("latitude")
val lat: Double,
val lat: Double? = null,
@SerializedName("longitude")
val long: Double,
val long: Double? = null,
@SerializedName("street")
val street: String,

View File

@ -0,0 +1,8 @@
package com.alya.ecommerce_serang.data.api.dto
import com.google.gson.annotations.SerializedName
data class FcmReq (
@SerializedName("fcm_token")
val fcmToken: String?= null
)

View File

@ -6,7 +6,7 @@ data class OrderRequest (
@SerializedName("address_id")
val addressId : Int,
@SerializedName("payment_method_id")
@SerializedName("payment_info_id")
val paymentMethodId : Int,
@SerializedName("ship_price")
@ -21,9 +21,12 @@ data class OrderRequest (
@SerializedName("is_negotiable")
val isNego: Boolean,
@SerializedName("cart_items_id")
@SerializedName("cart_item_ids")
val cartItemId: List<Int>,
@SerializedName("ship_etd")
val shipEtd: String
)
val shipEtd: String,
@SerializedName("is_reseller")
val isReseller: Boolean
)

View File

@ -6,7 +6,7 @@ data class OrderRequestBuy (
@SerializedName("address_id")
val addressId : Int,
@SerializedName("payment_method_id")
@SerializedName("payment_info_id")
val paymentMethodId : Int,
@SerializedName("ship_price")
@ -27,7 +27,10 @@ data class OrderRequestBuy (
@SerializedName("quantity")
val quantity : Int,
@SerializedName("ship_etd")
val shipEtd: String
val shipEtd: String,
@SerializedName("is_reseller")
val isReseller: Boolean
)

View File

@ -11,6 +11,12 @@ data class ProductsItem(
@field:SerializedName("image")
val image: String,
@field:SerializedName("is_wholesale")
val isWholesale: Boolean,
@field:SerializedName("sppirt")
val sppirt: String? = null,
@field:SerializedName("rating")
val rating: String,
@ -23,6 +29,9 @@ data class ProductsItem(
@field:SerializedName("is_pre_order")
val isPreOrder: Boolean,
@field:SerializedName("condition")
val condition: String? = null,
@field:SerializedName("category_id")
val categoryId: Int,
@ -32,6 +41,9 @@ data class ProductsItem(
@field:SerializedName("name")
val name: String,
@field:SerializedName("halal")
val halal: String?= null,
@field:SerializedName("id")
val id: Int,
@ -46,4 +58,4 @@ data class ProductsItem(
@field:SerializedName("status")
val status: String
)
)

View File

@ -1,7 +1,10 @@
package com.alya.ecommerce_serang.data.api.dto
import android.os.Parcelable
import com.google.gson.annotations.SerializedName
import kotlinx.android.parcel.Parcelize
@Parcelize
data class RegisterRequest (
val name: String?,
val email: String?,
@ -15,4 +18,4 @@ data class RegisterRequest (
val image: String? = null,
val otp: String? = null
)
): Parcelable

View File

@ -0,0 +1,22 @@
package com.alya.ecommerce_serang.data.api.dto
import com.google.gson.annotations.SerializedName
data class ReviewProductItem (
@SerializedName("order_item_id")
val orderItemId : Int,
@SerializedName("rating")
val rating : Int,
@SerializedName("review_text")
val reviewTxt : String = ""
)
data class ReviewUIItem(
val orderItemId: Int,
val productName: String,
val productImage: String,
var rating: Int = 5, // Default rating is 5 stars
var reviewText: String = "" // Empty by default, to be filled by user
)

View File

@ -0,0 +1,11 @@
package com.alya.ecommerce_serang.data.api.dto
import com.google.gson.annotations.SerializedName
data class VerifRegisReq (
@SerializedName("field")
val fieldRegis: String,
@SerializedName("value")
val valueRegis: String
)

View File

@ -0,0 +1,9 @@
package com.alya.ecommerce_serang.data.api.response.auth
import com.google.gson.annotations.SerializedName
data class CheckStoreResponse(
@field:SerializedName("hasStore")
val hasStore: Boolean
)

View File

@ -0,0 +1,9 @@
package com.alya.ecommerce_serang.data.api.response.auth
import com.google.gson.annotations.SerializedName
data class FcmTokenResponse(
@field:SerializedName("message")
val message: String? = null
)

View File

@ -0,0 +1,9 @@
package com.alya.ecommerce_serang.data.api.response.auth
import com.google.gson.annotations.SerializedName
data class HasStoreResponse(
@field:SerializedName("hasStore")
val hasStore: Boolean
)

View File

@ -0,0 +1,33 @@
package com.alya.ecommerce_serang.data.api.response.auth
import com.google.gson.annotations.SerializedName
data class ListNotifResponse(
@field:SerializedName("notif")
val notif: List<NotifItem>,
@field:SerializedName("message")
val message: String
)
data class NotifItem(
@field:SerializedName("user_id")
val userId: Int,
@field:SerializedName("created_at")
val createdAt: String,
@field:SerializedName("id")
val id: Int,
@field:SerializedName("title")
val title: String,
@field:SerializedName("message")
val message: String,
@field:SerializedName("type")
val type: String
)

View File

@ -0,0 +1,33 @@
package com.alya.ecommerce_serang.data.api.response.auth
import com.google.gson.annotations.SerializedName
data class ListStoreNotifResponse(
@field:SerializedName("notifstore")
val notifstore: List<NotifstoreItem>,
@field:SerializedName("message")
val message: String
)
data class NotifstoreItem(
@field:SerializedName("user_id")
val userId: Int,
@field:SerializedName("created_at")
val createdAt: String,
@field:SerializedName("id")
val id: Int,
@field:SerializedName("title")
val title: String,
@field:SerializedName("message")
val message: String,
@field:SerializedName("type")
val type: String
)

View File

@ -0,0 +1,21 @@
package com.alya.ecommerce_serang.data.api.response.auth
import com.google.gson.annotations.SerializedName
data class ListStoreTypeResponse(
@field:SerializedName("storeTypes")
val storeTypes: List<StoreTypesItem>,
@field:SerializedName("message")
val message: String
)
data class StoreTypesItem(
@field:SerializedName("name")
val name: String,
@field:SerializedName("id")
val id: Int
)

View File

@ -0,0 +1,57 @@
package com.alya.ecommerce_serang.data.api.response.auth
import com.google.gson.annotations.SerializedName
data class RegisterStoreResponse(
@field:SerializedName("store")
val store: Store,
@field:SerializedName("message")
val message: String
)
data class Store(
@field:SerializedName("image")
val image: String,
@field:SerializedName("ktp")
val ktp: String,
@field:SerializedName("nib")
val nib: String,
@field:SerializedName("npwp")
val npwp: String,
@field:SerializedName("address_id")
val addressId: Int,
@field:SerializedName("description")
val description: String,
@field:SerializedName("store_type_id")
val storeTypeId: Int,
@field:SerializedName("is_on_leave")
val isOnLeave: Boolean,
@field:SerializedName("balance")
val balance: String,
@field:SerializedName("user_id")
val userId: Int,
@field:SerializedName("name")
val name: String,
@field:SerializedName("persetujuan")
val persetujuan: String,
@field:SerializedName("id")
val id: Int,
@field:SerializedName("status")
val status: String
)

View File

@ -0,0 +1,9 @@
package com.alya.ecommerce_serang.data.api.response.auth
import com.google.gson.annotations.SerializedName
data class VerifRegisterResponse(
@field:SerializedName("available")
val available: Boolean
)

View File

@ -1,21 +0,0 @@
package com.alya.ecommerce_serang.data.api.response.customer.cart
import android.os.Bundle
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import com.alya.ecommerce_serang.R
class CartActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContentView(R.layout.activity_cart)
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
insets
}
}
}

View File

@ -0,0 +1,9 @@
package com.alya.ecommerce_serang.data.api.response.customer.cart
import com.google.gson.annotations.SerializedName
data class DeleteCartResponse(
@field:SerializedName("message")
val message: String
)

View File

@ -4,14 +4,14 @@ import com.google.gson.annotations.SerializedName
data class ListCartResponse(
@field:SerializedName("data")
val data: List<DataItem>,
@field:SerializedName("data")
val data: List<DataItemCart>,
@field:SerializedName("message")
@field:SerializedName("message")
val message: String
)
data class DataItem(
data class DataItemCart(
@field:SerializedName("store_id")
val storeId: Int,

View File

@ -0,0 +1,14 @@
package com.alya.ecommerce_serang.data.api.response.customer.order
data class CancelOrderResponse(
val data: DataCancel,
val message: String
)
data class DataCancel(
val reason: String,
val createdAt: String,
val id: Int,
val orderId: Int
)

View File

@ -4,16 +4,16 @@ import com.google.gson.annotations.SerializedName
data class CreateOrderResponse(
@field:SerializedName("shipping")
@field:SerializedName("shipping")
val shipping: Shipping,
@field:SerializedName("order_item")
@field:SerializedName("order_item")
val orderItem: List<OrderItemItem>,
@field:SerializedName("message")
@field:SerializedName("message")
val message: String,
@field:SerializedName("order")
@field:SerializedName("order")
val order: Order
)
@ -47,34 +47,10 @@ data class Shipping(
val status: String
)
data class OrderItemItem(
@field:SerializedName("quantity")
val quantity: Int,
@field:SerializedName("price")
val price: String,
@field:SerializedName("subtotal")
val subtotal: String,
@field:SerializedName("product_id")
val productId: Int,
@field:SerializedName("id")
val id: Int,
@field:SerializedName("order_id")
val orderId: Int
)
data class Order(
@field:SerializedName("payment_method_id")
val paymentMethodId: Int,
@field:SerializedName("auto_completed_at")
val autoCompletedAt: String? = null,
val autoCompletedAt: String,
@field:SerializedName("updated_at")
val updatedAt: String,
@ -97,9 +73,33 @@ data class Order(
@field:SerializedName("voucher_id")
val voucherId: String? = null,
@field:SerializedName("payment_info_id")
val paymentInfoId: Int,
@field:SerializedName("id")
val id: Int,
@field:SerializedName("status")
val status: String
)
data class OrderItemItem(
@field:SerializedName("quantity")
val quantity: Int,
@field:SerializedName("price")
val price: String,
@field:SerializedName("subtotal")
val subtotal: String,
@field:SerializedName("product_id")
val productId: Int,
@field:SerializedName("id")
val id: Int,
@field:SerializedName("order_id")
val orderId: Int
)

View File

@ -0,0 +1,15 @@
package com.alya.ecommerce_serang.data.api.response.customer.order
import com.google.gson.annotations.SerializedName
data class CreateReviewResponse(
@field:SerializedName("order_item_id")
val orderItemId: Int,
@field:SerializedName("rating")
val rating: Int,
@field:SerializedName("review_text")
val reviewText: String
)

View File

@ -12,4 +12,3 @@ data class AllProductResponse(
val products: List<ProductsItem>
)

View File

@ -4,6 +4,12 @@ import com.google.gson.annotations.SerializedName
data class DetailStoreProductResponse(
@field:SerializedName("shipping")
val shipping: List<ShippingItemDetail>,
@field:SerializedName("payment")
val payment: List<PaymentItemDetail>,
@field:SerializedName("store")
val store: StoreProduct,
@ -11,28 +17,11 @@ data class DetailStoreProductResponse(
val message: String
)
data class PaymentInfoItem(
val id: Int = 1,
@field:SerializedName("qris_image")
val qrisImage: String,
@field:SerializedName("bank_num")
val bankNum: String,
@field:SerializedName("name")
val name: String
)
data class StoreProduct(
@field:SerializedName("store_id")
val storeId: Int,
@field:SerializedName("shipping_service")
val shippingService: List<ShippingServiceItem>,
@field:SerializedName("store_rating")
val storeRating: String,
@ -45,21 +34,36 @@ data class StoreProduct(
@field:SerializedName("store_type")
val storeType: String,
@field:SerializedName("payment_info")
val paymentInfo: List<PaymentInfoItem>,
@field:SerializedName("store_location")
val storeLocation: String,
@field:SerializedName("store_image")
val storeImage: String,
val storeImage: String? = null,
@field:SerializedName("status")
val status: String
)
data class ShippingServiceItem(
data class ShippingItemDetail(
@field:SerializedName("courier")
val courier: String
)
data class PaymentItemDetail(
@field:SerializedName("qris_image")
val qrisImage: String,
@field:SerializedName("bank_num")
val bankNum: String,
@field:SerializedName("account_name")
val accountName: Any,
@field:SerializedName("bank_name")
val bankName: String,
@field:SerializedName("id")
val id: Int
)

View File

@ -1,13 +1,14 @@
package com.alya.ecommerce_serang.data.api.response.customer.product
import com.alya.ecommerce_serang.data.api.response.customer.cart.CartItemsItem
import com.google.gson.annotations.SerializedName
data class ProductResponse(
@field:SerializedName("product")
@field:SerializedName("product")
val product: Product,
@field:SerializedName("message")
@field:SerializedName("message")
val message: String
)
@ -17,7 +18,13 @@ data class Product(
val storeId: Int,
@field:SerializedName("image")
val image: String? = null,
val image: String,
@field:SerializedName("is_wholesale")
val isWholesale: Boolean? = false,
@field:SerializedName("sppirt")
val sppirt: String? = null,
@field:SerializedName("rating")
val rating: String,
@ -34,8 +41,8 @@ data class Product(
@field:SerializedName("is_pre_order")
val isPreOrder: Boolean,
@field:SerializedName("duration")
val duration: Any?,
@field:SerializedName("condition")
val condition: String? = null,
@field:SerializedName("category_id")
val categoryId: Int,
@ -46,6 +53,15 @@ data class Product(
@field:SerializedName("product_id")
val productId: Int,
@field:SerializedName("wholesale_price")
val wholesalePrice: String? = null,
@field:SerializedName("halal")
val halal: String? = null,
@field:SerializedName("wholesale_min_item")
val wholesaleMinItem: Int? = null,
@field:SerializedName("min_order")
val minOrder: Int,
@ -56,5 +72,19 @@ data class Product(
val stock: Int,
@field:SerializedName("product_category")
val productCategory: String
val productCategory: String,
@field:SerializedName("preorder_duration")
val preorderDuration: String? = null
)
data class CartItemWholesaleInfo(
val cartItemId: Int,
val isWholesale: Boolean,
val wholesalePrice: Double? = null
)
data class CartItemCheckoutInfo(
val cartItem: CartItemsItem,
val isWholesale: Boolean
)

View File

@ -0,0 +1,9 @@
package com.alya.ecommerce_serang.data.api.response.customer.profile
import com.google.gson.annotations.SerializedName
data class EditProfileResponse(
@field:SerializedName("message")
val message: String
)

View File

@ -22,6 +22,9 @@ class ApiConfig {
val client = OkHttpClient.Builder()
.addInterceptor(loggingInterceptor)
.addInterceptor(authInterceptor)
.connectTimeout(60, TimeUnit.SECONDS) // Increase to 60 seconds
.readTimeout(60, TimeUnit.SECONDS) // Increase to 60 seconds
.writeTimeout(60, TimeUnit.SECONDS)
.build()
val retrofit = Retrofit.Builder()

View File

@ -3,34 +3,49 @@ package com.alya.ecommerce_serang.data.api.retrofit
import com.alya.ecommerce_serang.data.api.dto.AddEvidenceRequest
import com.alya.ecommerce_serang.data.api.dto.AddPaymentInfoResponse
import com.alya.ecommerce_serang.data.api.dto.CancelOrderReq
import com.alya.ecommerce_serang.data.api.dto.CartItem
import com.alya.ecommerce_serang.data.api.dto.CityResponse
import com.alya.ecommerce_serang.data.api.dto.CompletedOrderRequest
import com.alya.ecommerce_serang.data.api.dto.CourierCostRequest
import com.alya.ecommerce_serang.data.api.dto.CreateAddressRequest
import com.alya.ecommerce_serang.data.api.dto.FcmReq
import com.alya.ecommerce_serang.data.api.dto.LoginRequest
import com.alya.ecommerce_serang.data.api.dto.OrderRequest
import com.alya.ecommerce_serang.data.api.dto.OrderRequestBuy
import com.alya.ecommerce_serang.data.api.dto.OtpRequest
import com.alya.ecommerce_serang.data.api.dto.ProvinceResponse
import com.alya.ecommerce_serang.data.api.dto.RegisterRequest
import com.alya.ecommerce_serang.data.api.dto.ReviewProductItem
import com.alya.ecommerce_serang.data.api.dto.SearchRequest
import com.alya.ecommerce_serang.data.api.dto.ShippingServiceRequest
import com.alya.ecommerce_serang.data.api.dto.StoreAddressResponse
import com.alya.ecommerce_serang.data.api.dto.UpdateCart
import com.alya.ecommerce_serang.data.api.dto.UpdateChatRequest
import com.alya.ecommerce_serang.data.api.dto.VerifRegisReq
import com.alya.ecommerce_serang.data.api.response.auth.CheckStoreResponse
import com.alya.ecommerce_serang.data.api.response.auth.FcmTokenResponse
import com.alya.ecommerce_serang.data.api.response.auth.HasStoreResponse
import com.alya.ecommerce_serang.data.api.response.auth.ListNotifResponse
import com.alya.ecommerce_serang.data.api.response.auth.ListStoreNotifResponse
import com.alya.ecommerce_serang.data.api.response.auth.ListStoreTypeResponse
import com.alya.ecommerce_serang.data.api.response.auth.LoginResponse
import com.alya.ecommerce_serang.data.api.response.auth.OtpResponse
import com.alya.ecommerce_serang.data.api.response.auth.RegisterResponse
import com.alya.ecommerce_serang.data.api.response.auth.RegisterStoreResponse
import com.alya.ecommerce_serang.data.api.response.auth.VerifRegisterResponse
import com.alya.ecommerce_serang.data.api.response.chat.ChatHistoryResponse
import com.alya.ecommerce_serang.data.api.response.chat.ChatListResponse
import com.alya.ecommerce_serang.data.api.response.chat.SendChatResponse
import com.alya.ecommerce_serang.data.api.response.chat.UpdateChatResponse
import com.alya.ecommerce_serang.data.api.response.customer.cart.AddCartResponse
import com.alya.ecommerce_serang.data.api.response.customer.cart.DeleteCartResponse
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.customer.order.CancelOrderResponse
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.CreateReviewResponse
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.order.OrderDetailResponse
@ -43,6 +58,7 @@ import com.alya.ecommerce_serang.data.api.response.customer.product.ReviewProduc
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.response.customer.profile.EditProfileResponse
import com.alya.ecommerce_serang.data.api.response.customer.profile.ProfileResponse
import com.alya.ecommerce_serang.data.api.response.order.AddEvidenceResponse
import com.alya.ecommerce_serang.data.api.response.order.ComplaintResponse
@ -68,6 +84,7 @@ import retrofit2.http.Multipart
import retrofit2.http.POST
import retrofit2.http.PUT
import retrofit2.http.Part
import retrofit2.http.PartMap
import retrofit2.http.Path
import retrofit2.http.Query
@ -77,11 +94,54 @@ interface ApiService {
@Body registerRequest: RegisterRequest
): Response<RegisterResponse>
@POST("verif")
suspend fun verifValue (
@Body verifRegisReq: VerifRegisReq
):VerifRegisterResponse
@GET("checkstore")
suspend fun checkStore (): Response<CheckStoreResponse>
@Multipart
@POST("registerstore")
suspend fun registerStore(
@Part("description") description: RequestBody,
@Part("store_type_id") storeTypeId: RequestBody,
@Part("latitude") latitude: RequestBody,
@Part("longitude") longitude: RequestBody,
@Part("street") street: RequestBody,
@Part("subdistrict") subdistrict: RequestBody,
@Part("city_id") cityId: RequestBody,
@Part("province_id") provinceId: RequestBody,
@Part("postal_code") postalCode: RequestBody,
@Part("detail") detail: RequestBody,
@Part("bank_name") bankName: RequestBody,
@Part("bank_num") bankNum: RequestBody,
@Part("store_name") storeName: RequestBody,
@Part storeimg: MultipartBody.Part?,
@Part ktp: MultipartBody.Part?,
@Part npwp: MultipartBody.Part?,
@Part nib: MultipartBody.Part?,
@Part persetujuan: MultipartBody.Part?,
@PartMap couriers: Map<String, @JvmSuppressWildcards RequestBody>,
@Part qris: MultipartBody.Part?,
@Part("account_name") accountName: RequestBody,
): Response<RegisterStoreResponse>
@POST("otp")
suspend fun getOTP(
@Body otpRequest: OtpRequest
):OtpResponse
@PUT("updatefcm")
suspend fun updateFcm(
@Body fcmReq: FcmReq
): FcmTokenResponse
@GET("checkstore")
suspend fun checkStoreUser(
): HasStoreResponse
@POST("login")
suspend fun login(
@Body loginRequest: LoginRequest
@ -91,6 +151,10 @@ interface ApiService {
suspend fun allCategory(
): Response<CategoryResponse>
@GET("storetype")
suspend fun listTypeStore(
): Response<ListStoreTypeResponse>
@GET("product")
suspend fun getAllProduct(): Response<AllProductResponse>
@ -117,6 +181,11 @@ interface ApiService {
@Body request: OrderRequest
): Response<CreateOrderResponse>
@POST("order/cancel")
suspend fun cancelOrder(
@Body cancelReq: CancelOrderReq
): Response<CancelOrderResponse>
@GET("order/detail/{id}")
suspend fun getDetailOrder(
@Path("id") orderId: Int
@ -135,6 +204,17 @@ interface ApiService {
@Part evidence: MultipartBody.Part
): Response<AddEvidenceResponse>
@Multipart
@PUT("profile/edit")
suspend fun editProfileCustomer(
@Part("username") username: RequestBody,
@Part("name") name: RequestBody,
@Part("phone") phone: RequestBody,
@Part("birth_date") birthDate: RequestBody,
@Part userimg: MultipartBody.Part,
@Part("email") email: RequestBody
): Response<EditProfileResponse>
@GET("order/{status}")
suspend fun getOrderList(
@Path("status") status: String
@ -215,6 +295,11 @@ interface ApiService {
@Body updateCart: UpdateCart
): Response<UpdateCartResponse>
@DELETE("cart/delete/{id}")
suspend fun deleteCart(
@Path("id") cartItemId : Int
):Response<DeleteCartResponse>
@POST("couriercost")
suspend fun countCourierCost(
@Body courierCost : CourierCostRequest
@ -253,6 +338,11 @@ interface ApiService {
@Part complaintimg: MultipartBody.Part
): Response<ComplaintResponse>
@POST("review")
suspend fun createReview(
@Body contentReview : ReviewProductItem
): Response<CreateReviewResponse>
@GET("store/topup")
suspend fun getTopUpHistory(): Response<TopUpResponse>
@ -366,4 +456,12 @@ interface ApiService {
@GET("chat")
suspend fun getChatList(
): Response<ChatListResponse>
@GET("notification")
suspend fun getNotif(
): Response<ListNotifResponse>
@GET("mystore/notification")
suspend fun getNotifStore(
): Response<ListStoreNotifResponse>
}

View File

@ -74,6 +74,8 @@ class ChatRepository @Inject constructor(
chatimg = imagePart
)
Log.d("ChatRepository", "check data productId=$productIdPart, storeId=$storeIdPart, messageTxt=$messagePart, chatImg=$imagePart")
if (response.isSuccessful) {
val body = response.body()
if (body != null) {

View File

@ -2,27 +2,33 @@ 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.CancelOrderReq
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.ReviewProductItem
import com.alya.ecommerce_serang.data.api.dto.UpdateCart
import com.alya.ecommerce_serang.data.api.dto.UserProfile
import com.alya.ecommerce_serang.data.api.response.customer.cart.DataItem
import com.alya.ecommerce_serang.data.api.response.customer.order.CreateOrderResponse
import com.alya.ecommerce_serang.data.api.response.customer.order.OrderDetailResponse
import com.alya.ecommerce_serang.data.api.response.customer.order.OrderListResponse
import com.alya.ecommerce_serang.data.api.response.customer.product.ProductResponse
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.cart.DataItemCart
import com.alya.ecommerce_serang.data.api.response.customer.order.CancelOrderResponse
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.CreateReviewResponse
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.order.OrderDetailResponse
import com.alya.ecommerce_serang.data.api.response.customer.order.OrderListResponse
import com.alya.ecommerce_serang.data.api.response.customer.product.PaymentItemDetail
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.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.retrofit.ApiService
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
@ -154,7 +160,7 @@ class OrderRepository(private val apiService: ApiService) {
}
}
suspend fun getCart(): Result<List<DataItem>> {
suspend fun getCart(): Result<List<DataItemCart>> {
return try {
val response = apiService.getCart()
@ -177,6 +183,40 @@ class OrderRepository(private val apiService: ApiService) {
}
}
suspend fun updateCart(updateCart: UpdateCart): Result<String> {
return try {
val response = apiService.updateCart(updateCart)
if (response.isSuccessful) {
Result.Success(response.body()?.message ?: "Cart updated successfully")
} else {
val errorMsg = response.errorBody()?.string() ?: "Failed to update cart"
Log.e("Order Repository", "Error updating cart: $errorMsg")
Result.Error(Exception(errorMsg))
}
} catch (e: Exception) {
Log.e("Order Repository", "Exception updating cart", e)
Result.Error(e)
}
}
suspend fun deleteCartItem(cartItemId: Int): Result<String> {
return try {
val response = apiService.deleteCart(cartItemId)
if (response.isSuccessful) {
Result.Success(response.body()?.message ?: "Item removed from cart")
} else {
val errorMsg = response.errorBody()?.string() ?: "Failed to remove item from cart"
Log.e("Order Repository", "Error deleting cart item: $errorMsg")
Result.Error(Exception(errorMsg))
}
} catch (e: Exception) {
Log.e("Order Repository", "Exception deleting cart item", e)
Result.Error(e)
}
}
suspend fun fetchStoreDetail(storeId: Int): Result<StoreProduct?> {
return try {
val response = apiService.getDetailStore(storeId)
@ -198,6 +238,27 @@ class OrderRepository(private val apiService: ApiService) {
}
}
suspend fun fetchPaymentStore(storeId: Int): Result<List<PaymentItemDetail?>> {
return try {
val response = apiService.getDetailStore(storeId)
if (response.isSuccessful) {
val store = response.body()?.payment
if (store != null) {
Result.Success(store)
} else {
Result.Error(Exception("Payment details not found"))
}
} else {
val errorMsg = response.errorBody()?.string() ?: "Unknown error"
Log.e("Order Repository", "Error fetching store: $errorMsg")
Result.Error(Exception(errorMsg))
}
} catch (e: Exception) {
Log.e("Order Repository", "Exception fetching payment details", e)
Result.Error(e)
}
}
suspend fun addAddress(request: CreateAddressRequest): Result<CreateAddressResponse> {
return try {
Log.d("OrderRepository", "Adding address: $request")
@ -258,59 +319,35 @@ class OrderRepository(private val apiService: ApiService) {
}
}
// 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...")
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
)
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)
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 {
Log.e("OrderRepository", "Response body was null")
Result.Error(Exception("Empty response from server"))
val errorBody = response.errorBody()?.string() ?: "Unknown error"
Log.e("OrderRepository", "Error uploading payment proof: $errorBody")
Result.Error(Exception(errorBody))
}
} 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)
}
} catch (e: Exception) {
Log.e("OrderRepository", "Exception uploading payment proof", e)
Result.Error(e)
}
}
suspend fun getOrderList(status: String): Result<OrderListResponse> {
return try {
@ -339,7 +376,7 @@ suspend fun uploadPaymentProof(request: AddEvidenceMultipartRequest): Result<Add
suspend fun confirmOrderCompleted(request: CompletedOrderRequest): Result<CompletedOrderResponse> {
return try {
Log.d("OrderRepository", "Cinfroming order request completed: $request")
Log.d("OrderRepository", "Conforming order request completed: $request")
val response = apiService.confirmOrder(request)
if(response.isSuccessful) {
@ -431,4 +468,47 @@ suspend fun uploadPaymentProof(request: AddEvidenceMultipartRequest): Result<Add
emit(Result.Error(e))
}
}.flowOn(Dispatchers.IO)
suspend fun createReviewProduct(review: ReviewProductItem): Result<CreateReviewResponse>{
return try{
Log.d("Order Repository", "Sending review item product: $review")
val response = apiService.createReview(review)
if (response.isSuccessful){
response.body()?.let { reviewProductResponse ->
Log.d("Order Repository", " Successful create review. Review item rating: ${reviewProductResponse.rating}, orderItemId: ${reviewProductResponse.orderItemId}")
Result.Success(reviewProductResponse)
} ?: run {
Result.Error(Exception("Failed to create review"))
}
} else {
val errorMsg = response.errorBody()?.string() ?: "Unknown Error"
Log.e("Order Repository", "Error create review. Code ${response.code()}, Error: $errorMsg")
Result.Error(Exception(errorMsg))
}
} catch (e:Exception) {
Result.Error(e)
}
}
suspend fun cancelOrder(cancelReq: CancelOrderReq): Result<CancelOrderResponse>{
return try{
val response= apiService.cancelOrder(cancelReq)
if (response.isSuccessful){
response.body()?.let { cancelOrderResponse ->
Result.Success(cancelOrderResponse)
} ?: run {
Result.Error(Exception("Failed to cancel order"))
}
} else {
val errorMsg = response.errorBody()?.string() ?: "Unknown Error"
Result.Error(Exception(errorMsg))
}
}catch (e: Exception){
Result.Error(e)
}
}
}

View File

@ -1,31 +1,241 @@
package com.alya.ecommerce_serang.data.repository
import android.content.Context
import android.net.Uri
import android.util.Log
import com.alya.ecommerce_serang.data.api.dto.FcmReq
import com.alya.ecommerce_serang.data.api.dto.LoginRequest
import com.alya.ecommerce_serang.data.api.dto.OtpRequest
import com.alya.ecommerce_serang.data.api.dto.RegisterRequest
import com.alya.ecommerce_serang.data.api.dto.UserProfile
import com.alya.ecommerce_serang.data.api.dto.VerifRegisReq
import com.alya.ecommerce_serang.data.api.response.auth.FcmTokenResponse
import com.alya.ecommerce_serang.data.api.response.auth.HasStoreResponse
import com.alya.ecommerce_serang.data.api.response.auth.ListStoreTypeResponse
import com.alya.ecommerce_serang.data.api.response.auth.LoginResponse
import com.alya.ecommerce_serang.data.api.response.auth.NotifItem
import com.alya.ecommerce_serang.data.api.response.auth.NotifstoreItem
import com.alya.ecommerce_serang.data.api.response.auth.OtpResponse
import com.alya.ecommerce_serang.data.api.response.auth.RegisterResponse
import com.alya.ecommerce_serang.data.api.response.auth.RegisterStoreResponse
import com.alya.ecommerce_serang.data.api.response.auth.VerifRegisterResponse
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.profile.EditProfileResponse
import com.alya.ecommerce_serang.data.api.retrofit.ApiService
import com.alya.ecommerce_serang.utils.FileUtils
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.MultipartBody
import okhttp3.RequestBody
import okhttp3.RequestBody.Companion.asRequestBody
import okhttp3.RequestBody.Companion.toRequestBody
import java.io.File
class UserRepository(private val apiService: ApiService) {
//post data without message/response
suspend fun requestOtpRep(email: String): OtpResponse {
return apiService.getOTP(OtpRequest(email))
}
suspend fun registerUser(request: RegisterRequest): String {
suspend fun listStoreType(): Result<ListStoreTypeResponse>{
return try{
val response = apiService.listTypeStore()
if (response.isSuccessful) {
response.body()?.let {
Result.Success(it)
} ?: Result.Error(Exception("No store type"))
} else {
throw Exception("No response ${response.errorBody()?.string()}")
}
} catch (e:Exception){
Result.Error(e)
}
}
suspend fun getListProvinces(): ListProvinceResponse? {
val response = apiService.getListProv()
return if (response.isSuccessful) response.body() else null
}
suspend fun getListCities(provId : Int): ListCityResponse? {
val response = apiService.getCityProvId(provId)
return if (response.isSuccessful) response.body() else null
}
suspend fun registerUser(request: RegisterRequest): RegisterResponse {
val response = apiService.register(request) // API call
if (response.isSuccessful) {
val responseBody = response.body() ?: throw Exception("Empty response body")
return responseBody.message // Get the message from RegisterResponse
return responseBody // Get the message from RegisterResponse
} else {
throw Exception("Registration failed: ${response.errorBody()?.string()}")
}
}
suspend fun registerStoreUser(
context: Context,
description: String,
storeTypeId: Int,
latitude: String,
longitude: String,
street: String,
subdistrict: String,
cityId: Int,
provinceId: Int,
postalCode: Int,
detail: String?,
bankName: String,
bankNum: Int,
storeName: String,
storeImg: Uri?,
ktp: Uri?,
npwp: Uri?,
nib: Uri?,
persetujuan: Uri?,
couriers: List<String>,
qris: Uri?,
accountName: String
): Result<RegisterStoreResponse> {
return try {
val descriptionPart = description.toRequestBody("text/plain".toMediaTypeOrNull())
val storeTypeIdPart = storeTypeId.toString().toRequestBody("text/plain".toMediaTypeOrNull())
val latitudePart = latitude.toRequestBody("text/plain".toMediaTypeOrNull())
val longitudePart = longitude.toRequestBody("text/plain".toMediaTypeOrNull())
val streetPart = street.toRequestBody("text/plain".toMediaTypeOrNull())
val subdistrictPart = subdistrict.toRequestBody("text/plain".toMediaTypeOrNull())
val cityIdPart = cityId.toString().toRequestBody("text/plain".toMediaTypeOrNull())
val provinceIdPart = provinceId.toString().toRequestBody("text/plain".toMediaTypeOrNull())
val postalCodePart = postalCode.toString().toRequestBody("text/plain".toMediaTypeOrNull())
val detailPart = detail?.toRequestBody("text/plain".toMediaTypeOrNull())
val bankNamePart = bankName.toRequestBody("text/plain".toMediaTypeOrNull())
val bankNumPart = bankNum.toString().toRequestBody("text/plain".toMediaTypeOrNull())
val storeNamePart = storeName.toRequestBody("text/plain".toMediaTypeOrNull())
val accountNamePart = accountName.toRequestBody("text/plain".toMediaTypeOrNull())
// Create a Map for courier values
val courierMap = HashMap<String, RequestBody>()
couriers.forEach { courier ->
courierMap["couriers[]"] = courier.toRequestBody("text/plain".toMediaTypeOrNull())
}
// Convert URIs to MultipartBody.Part
val storeImgPart = storeImg?.let {
val inputStream = context.contentResolver.openInputStream(it)
val file = File(context.cacheDir, "store_img_${System.currentTimeMillis()}")
inputStream?.use { input ->
file.outputStream().use { output ->
input.copyTo(output)
}
}
val mimeType = context.contentResolver.getType(it) ?: "application/octet-stream"
val requestFile = file.asRequestBody(mimeType.toMediaTypeOrNull())
MultipartBody.Part.createFormData("storeimg", file.name, requestFile)
}
val ktpPart = ktp?.let {
val inputStream = context.contentResolver.openInputStream(it)
val file = File(context.cacheDir, "ktp_${System.currentTimeMillis()}")
inputStream?.use { input ->
file.outputStream().use { output ->
input.copyTo(output)
}
}
val mimeType = context.contentResolver.getType(it) ?: "application/octet-stream"
val requestFile = file.asRequestBody(mimeType.toMediaTypeOrNull())
MultipartBody.Part.createFormData("ktp", file.name, requestFile)
}
val npwpPart = npwp?.let {
val inputStream = context.contentResolver.openInputStream(it)
val file = File(context.cacheDir, "npwp_${System.currentTimeMillis()}")
inputStream?.use { input ->
file.outputStream().use { output ->
input.copyTo(output)
}
}
val mimeType = context.contentResolver.getType(it) ?: "application/octet-stream"
val requestFile = file.asRequestBody(mimeType.toMediaTypeOrNull())
MultipartBody.Part.createFormData("npwp", file.name, requestFile)
}
val nibPart = nib?.let {
val inputStream = context.contentResolver.openInputStream(it)
val file = File(context.cacheDir, "nib_${System.currentTimeMillis()}")
inputStream?.use { input ->
file.outputStream().use { output ->
input.copyTo(output)
}
}
val mimeType = context.contentResolver.getType(it) ?: "application/octet-stream"
val requestFile = file.asRequestBody(mimeType.toMediaTypeOrNull())
MultipartBody.Part.createFormData("nib", file.name, requestFile)
}
val persetujuanPart = persetujuan?.let {
val inputStream = context.contentResolver.openInputStream(it)
val file = File(context.cacheDir, "persetujuan_${System.currentTimeMillis()}")
inputStream?.use { input ->
file.outputStream().use { output ->
input.copyTo(output)
}
}
val mimeType = context.contentResolver.getType(it) ?: "application/octet-stream"
val requestFile = file.asRequestBody(mimeType.toMediaTypeOrNull())
MultipartBody.Part.createFormData("persetujuan", file.name, requestFile)
}
val qrisPart = qris?.let {
val inputStream = context.contentResolver.openInputStream(it)
val file = File(context.cacheDir, "qris_${System.currentTimeMillis()}")
inputStream?.use { input ->
file.outputStream().use { output ->
input.copyTo(output)
}
}
val mimeType = context.contentResolver.getType(it) ?: "application/octet-stream"
val requestFile = file.asRequestBody(mimeType.toMediaTypeOrNull())
MultipartBody.Part.createFormData("qris", file.name, requestFile)
}
// Make the API call
val response = apiService.registerStore(
descriptionPart,
storeTypeIdPart,
latitudePart,
longitudePart,
streetPart,
subdistrictPart,
cityIdPart,
provinceIdPart,
postalCodePart,
detailPart ?: "".toRequestBody("text/plain".toMediaTypeOrNull()),
bankNamePart,
bankNumPart,
storeNamePart,
storeImgPart,
ktpPart,
npwpPart,
nibPart,
persetujuanPart,
courierMap,
qrisPart,
accountNamePart
)
// Check if response is successful
if (response.isSuccessful) {
Result.Success(response.body() ?: throw Exception("Response body is null"))
} else {
Result.Error(Exception("Registration failed with code: ${response.code()}"))
}
} catch (e: Exception) {
Result.Error(e)
}
}
suspend fun login(email: String, password: String): Result<LoginResponse> {
return try {
val response = apiService.login(LoginRequest(email, password))
@ -56,101 +266,120 @@ class UserRepository(private val apiService: ApiService) {
}
}
// suspend fun sendChatMessage(
// storeId: Int,
// message: String,
// productId: Int,
// imageFile: File? = null
// ): Result<SendChatResponse> {
// return try {
// // Create multipart request builder
// val requestBodyBuilder = MultipartBody.Builder().setType(MultipartBody.FORM)
//
// // Add text fields
// requestBodyBuilder.addFormDataPart("store_id", storeId.toString())
// requestBodyBuilder.addFormDataPart("message", message)
// requestBodyBuilder.addFormDataPart("product_id", productId.toString())
//
// // Add image if it exists
// if (imageFile != null && imageFile.exists()) {
// val requestFile = imageFile.asRequestBody("image/*".toMediaTypeOrNull())
// requestBodyBuilder.addFormDataPart("chatimg", imageFile.name, requestFile)
// }
//
// // Build the final request body
// val requestBody = requestBodyBuilder.build()
//
// // Make the API call using a custom endpoint that takes a plain MultipartBody
// val response = apiService.sendChatLineWithBody(requestBody)
//
// if (response.isSuccessful) {
// response.body()?.let {
// Result.Success(it)
// } ?: Result.Error(Exception("Send chat response is empty"))
// } else {
// val errorBody = response.errorBody()?.string() ?: "Unknown error"
// Log.e("ChatRepository", "HTTP Error: ${response.code()}, Body: $errorBody")
// Result.Error(Exception("API Error: ${response.code()} - $errorBody"))
// }
// } catch (e: Exception) {
// Log.e("ChatRepository", "Exception sending message", e)
// e.printStackTrace()
// Result.Error(e)
// }
// }
//
// /**
// * Updates the status of a message (sent, delivered, read)
// *
// * @param messageId The ID of the message to update
// * @param status The new status to set
// * @return Result containing the updated message details or error
// */
// suspend fun updateMessageStatus(
// messageId: Int,
// status: String
// ): Result<UpdateChatResponse> {
// return try {
// val requestBody = UpdateChatRequest(
// id = messageId,
// status = status
// )
//
// val response = apiService.updateChatStatus(requestBody)
//
// if (response.isSuccessful) {
// response.body()?.let {
// Result.Success(it)
// } ?: Result.Error(Exception("Update status response is empty"))
// } else {
// Result.Error(Exception(response.errorBody()?.string() ?: "Unknown error"))
// }
// } catch (e: Exception) {
// Result.Error(e)
// }
// }
//
// /**
// * Gets the chat history for a specific chat room
// *
// * @param chatRoomId The ID of the chat room
// * @return Result containing the list of chat messages or error
// */
// suspend fun getChatHistory(chatRoomId: Int): Result<ChatHistoryResponse> {
// return try {
// val response = apiService.getChatDetail(chatRoomId)
//
// if (response.isSuccessful) {
// response.body()?.let {
// Result.Success(it)
// } ?: Result.Error(Exception("Chat history response is empty"))
// } else {
// Result.Error(Exception(response.errorBody()?.string() ?: "Unknown error"))
// }
// } catch (e: Exception) {
// Result.Error(e)
// }
// }
suspend fun editProfileCust(
context: Context,
username: String,
name: String,
phone: String,
birthDate: String,
email: String,
imageUri: Uri?
): Result<EditProfileResponse> {
return try {
// Log the data being sent
Log.d(TAG, "Edit Profile - Username: $username, Name: $name, Phone: $phone, Birth Date: $birthDate, Email: $email")
Log.d(TAG, "Image URI: $imageUri")
// Create RequestBody objects for text fields
val usernameRequestBody = username.toRequestBody("text/plain".toMediaTypeOrNull())
val nameRequestBody = name.toRequestBody("text/plain".toMediaTypeOrNull())
val phoneRequestBody = phone.toRequestBody("text/plain".toMediaTypeOrNull())
val birthDateRequestBody = birthDate.toRequestBody("text/plain".toMediaTypeOrNull())
val emailRequestBody = email.toRequestBody("text/plain".toMediaTypeOrNull())
// Create MultipartBody.Part for the image
val imagePart = if (imageUri != null) {
// Create a temporary file from the URI using the utility class
val imageFile = FileUtils.createTempFileFromUri(context, imageUri, "profile")
if (imageFile != null) {
// Create MultipartBody.Part from the file
FileUtils.createMultipartFromFile("userimg", imageFile)
} else {
// Fallback to empty part
FileUtils.createEmptyMultipart("userimg")
}
} else {
// No image selected, use empty part
FileUtils.createEmptyMultipart("userimg")
}
// Make the API call
val response = apiService.editProfileCustomer(
username = usernameRequestBody,
name = nameRequestBody,
phone = phoneRequestBody,
birthDate = birthDateRequestBody,
userimg = imagePart,
email = emailRequestBody
)
// Process the response
if (response.isSuccessful) {
val editResponse = response.body()
if (editResponse != null) {
Log.d(TAG, "Edit profile success: ${editResponse.message}")
Result.Success(editResponse)
} else {
Log.e(TAG, "Response body is null")
Result.Error(Exception("Empty response from server"))
}
} else {
val errorBody = response.errorBody()?.string() ?: "Unknown Error"
Log.e(TAG, "Error editing profile: $errorBody")
Result.Error(Exception(errorBody))
}
} catch (e: Exception) {
Log.e(TAG, "Exception in editProfileCust: ${e.message}")
e.printStackTrace()
Result.Error(e)
}
}
suspend fun checkStore(): HasStoreResponse{
return apiService.checkStoreUser()
}
suspend fun checkValue(request: VerifRegisReq): VerifRegisterResponse{
return apiService.verifValue(request)
}
suspend fun sendFcm(request: FcmReq): FcmTokenResponse{
return apiService.updateFcm(request)
}
suspend fun getListNotif(): Result<List<NotifItem>> {
return try {
val response = apiService.getNotif()
if (response.isSuccessful){
val chat = response.body()?.notif ?: emptyList()
Result.Success(chat)
} else {
Result.Error(Exception("Failed to fetch list notif. Code: ${response.code()}"))
}
} catch (e: Exception){
Result.Error(e)
}
}
suspend fun getListNotifStore(): Result<List<NotifstoreItem>> {
return try {
val response = apiService.getNotifStore()
if (response.isSuccessful){
val chat = response.body()?.notifstore ?: emptyList()
Result.Success(chat)
} else {
Result.Error(Exception("Failed to fetch list notif. Code: ${response.code()}"))
}
} catch (e: Exception){
Result.Error(e)
}
}
companion object{
private const val TAG = "UserRepository"
}
}

View File

@ -1,8 +1,10 @@
package com.alya.ecommerce_serang.ui
import android.content.Context
import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle
import android.util.Log
import android.widget.Toast
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
@ -20,14 +22,19 @@ import com.alya.ecommerce_serang.data.api.retrofit.ApiService
import com.alya.ecommerce_serang.databinding.ActivityMainBinding
import com.alya.ecommerce_serang.ui.notif.WebSocketManager
import com.alya.ecommerce_serang.utils.SessionManager
import com.google.firebase.FirebaseApp
import com.google.firebase.messaging.FirebaseMessaging
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
private val TAG = "MainActivity"
private lateinit var binding: ActivityMainBinding
private lateinit var apiService: ApiService
private lateinit var sessionManager: SessionManager
// private val viewModel: NotifViewModel by viewModels()
private val navController by lazy {
(supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment).navController
@ -64,6 +71,11 @@ class MainActivity : AppCompatActivity() {
)
windowInsets
}
// Initialize Firebase
FirebaseApp.initializeApp(this)
// Request FCM token at app startup
retrieveFCMToken()
requestNotificationPermissionIfNeeded()
@ -150,4 +162,31 @@ class MainActivity : AppCompatActivity() {
}
}
}
private fun retrieveFCMToken() {
FirebaseMessaging.getInstance().token
.addOnCompleteListener { task ->
if (!task.isSuccessful) {
Log.e(TAG, "Failed to get FCM token", task.exception)
return@addOnCompleteListener
}
val token = task.result
// tokenTes = token
Log.d(TAG, "FCM token retrieved: $token")
// Save token locally
val sharedPreferences = getSharedPreferences("FCM_PREFS", Context.MODE_PRIVATE)
sharedPreferences.edit().putString("FCM_TOKEN", token).apply()
// Send to your server
sendTokenToServer(token)
}
}
private fun sendTokenToServer(token: String) {
Log.d(TAG, "Would send token to server: $token")
}
}

View File

@ -1,11 +1,14 @@
package com.alya.ecommerce_serang.ui.auth
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.util.Log
import android.widget.Toast
import androidx.activity.enableEdgeToEdge
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import com.alya.ecommerce_serang.data.api.dto.FcmReq
import com.alya.ecommerce_serang.data.api.retrofit.ApiConfig
import com.alya.ecommerce_serang.data.repository.Result
import com.alya.ecommerce_serang.data.repository.UserRepository
@ -14,15 +17,19 @@ import com.alya.ecommerce_serang.ui.MainActivity
import com.alya.ecommerce_serang.utils.BaseViewModelFactory
import com.alya.ecommerce_serang.utils.SessionManager
import com.alya.ecommerce_serang.utils.viewmodel.LoginViewModel
import com.google.firebase.FirebaseApp
import com.google.firebase.messaging.FirebaseMessaging
class LoginActivity : AppCompatActivity() {
private val TAG = "LoginActivity"
private lateinit var binding: ActivityLoginBinding
private val loginViewModel: LoginViewModel by viewModels{
BaseViewModelFactory {
val apiService = ApiConfig.getUnauthenticatedApiService()
val userRepository = UserRepository(apiService)
LoginViewModel(userRepository)
LoginViewModel(userRepository, this)
}
}
@ -35,6 +42,10 @@ class LoginActivity : AppCompatActivity() {
setupListeners()
observeLoginState()
FirebaseApp.initializeApp(this)
// Request FCM token at app startup
}
private fun setupListeners() {
@ -58,6 +69,7 @@ class LoginActivity : AppCompatActivity() {
val sessionManager = SessionManager(this)
sessionManager.saveToken(accessToken)
retrieveFCMToken()
// sessionManager.saveUserId(response.userId)
Toast.makeText(this, "Login Successful", Toast.LENGTH_SHORT).show()
@ -74,4 +86,35 @@ class LoginActivity : AppCompatActivity() {
}
}
}
private fun retrieveFCMToken() {
FirebaseMessaging.getInstance().token
.addOnCompleteListener { task ->
if (!task.isSuccessful) {
Log.e(TAG, "Failed to get FCM token", task.exception)
return@addOnCompleteListener
}
val token = task.result
// tokenTes = token
Log.d(TAG, "FCM token retrieved: $token")
// Save token locally
val sharedPreferences = getSharedPreferences("FCM_PREFS", Context.MODE_PRIVATE)
sharedPreferences.edit().putString("FCM_TOKEN", token).apply()
// Send to your server
sendTokenToServer(token)
}
}
private fun sendTokenToServer(token: String) {
Log.d(TAG, "Would send token to server: $token")
val tokenFcm=FcmReq(
fcmToken = token
)
loginViewModel.sendFcm(tokenFcm)
Log.d(TAG, "Sent token fcm: $token")
}
}

View File

@ -1,37 +1,39 @@
package com.alya.ecommerce_serang.ui.auth
import android.app.DatePickerDialog
import android.content.Intent
import android.os.Bundle
import android.util.Log
import android.widget.Toast
import androidx.activity.enableEdgeToEdge
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat
import com.alya.ecommerce_serang.R
import com.alya.ecommerce_serang.data.api.dto.RegisterRequest
import com.alya.ecommerce_serang.data.api.retrofit.ApiConfig
import com.alya.ecommerce_serang.data.repository.Result
import com.alya.ecommerce_serang.data.repository.OrderRepository
import com.alya.ecommerce_serang.data.repository.UserRepository
import com.alya.ecommerce_serang.databinding.ActivityRegisterBinding
import com.alya.ecommerce_serang.ui.MainActivity
import com.alya.ecommerce_serang.ui.auth.fragments.RegisterStep1Fragment
import com.alya.ecommerce_serang.ui.auth.fragments.RegisterStep2Fragment
import com.alya.ecommerce_serang.ui.auth.fragments.RegisterStep3Fragment
import com.alya.ecommerce_serang.utils.BaseViewModelFactory
import com.alya.ecommerce_serang.utils.SessionManager
import com.alya.ecommerce_serang.utils.viewmodel.RegisterViewModel
import java.text.SimpleDateFormat
import java.util.Calendar
import java.util.Locale
class RegisterActivity : AppCompatActivity() {
private lateinit var binding: ActivityRegisterBinding
private lateinit var sessionManager: SessionManager
private val registerViewModel: RegisterViewModel by viewModels{
BaseViewModelFactory {
val apiService = ApiConfig.getUnauthenticatedApiService()
val orderRepository = OrderRepository(apiService)
val userRepository = UserRepository(apiService)
RegisterViewModel(userRepository)
RegisterViewModel(userRepository, orderRepository, this)
}
}
@ -76,123 +78,27 @@ class RegisterActivity : AppCompatActivity() {
windowInsets
}
// Observe OTP state
observeOtpState()
binding.btnSignup.setOnClickListener {
// Retrieve values inside the click listener (so we get latest input)
val birthDate = binding.etBirthDate.text.toString()
val email = binding.etEmail.text.toString()
val password = binding.etPassword.text.toString()
val phone = binding.etNumberPhone.text.toString()
val username = binding.etUsername.text.toString()
val name = binding.etFullname.text.toString()
val image = null
val userData = RegisterRequest(name, email, password, username, phone, birthDate, image)
Log.d("RegisterActivity", "Requesting OTP for email: $email")
// Request OTP and wait for success before showing dialog
registerViewModel.requestOtp(userData.email.toString())
// Observe OTP state and show OTP dialog only when successful
registerViewModel.otpState.observe(this) { result ->
when (result) {
is Result.Success -> {
Log.d("RegisterActivity", "OTP sent successfully. Showing OTP dialog.")
// Show OTP dialog after OTP is successfully sent
val otpBottomSheet = OtpBottomSheetDialog(userData) { fullUserData ->
Log.d("RegisterActivity", "OTP entered successfully. Proceeding with registration.")
registerViewModel.registerUser(fullUserData) // Send complete data
}
otpBottomSheet.show(supportFragmentManager, "OtpBottomSheet")
}
is Result.Error -> {
// Show error message if OTP request fails
Log.e("RegisterActivity", "Failed to request OTP: ${result.exception.message}")
Toast.makeText(this, "Failed to request OTP: ${result.exception.message}", Toast.LENGTH_LONG).show()
}
is Result.Loading -> {
// Optional: Show loading indicator
}
}
}
// Observe Register state
observeRegisterState()
}
binding.tvLoginAlt.setOnClickListener{
val intent = Intent(this, LoginActivity::class.java)
startActivity(intent)
}
binding.etBirthDate.setOnClickListener{
showDatePicker()
if (savedInstanceState == null) {
supportFragmentManager.beginTransaction()
.replace(R.id.fragment_container, RegisterStep1Fragment.newInstance())
.commit()
}
}
private fun observeOtpState() {
registerViewModel.otpState.observe(this) { result ->
when (result) {
is Result.Loading -> {
// Show loading indicator
binding.progressBarOtp.visibility = android.view.View.VISIBLE
}
is Result.Success -> {
// Hide loading indicator and show success message
binding.progressBarOtp.visibility = android.view.View.GONE
// Toast.makeText(this@RegisterActivity, result.data, Toast.LENGTH_SHORT).show()
}
is Result.Error -> {
// Hide loading indicator and show error message
binding.progressBarOtp.visibility = android.view.View.GONE
Toast.makeText(this, "OTP Request Failed: ${result.exception.message}", Toast.LENGTH_SHORT).show()
}
}
}
}
// Function to navigate to the next fragment
fun navigateToStep(step: Int, userData: RegisterRequest?) {
val fragment = when (step) {
1 -> RegisterStep1Fragment.newInstance()
2 -> RegisterStep2Fragment.newInstance(userData)
3 -> RegisterStep3Fragment.newInstance()
else -> null
}
private fun observeRegisterState() {
registerViewModel.registerState.observe(this) { result ->
when (result) {
is Result.Loading -> {
// Show loading indicator for registration
binding.progressBarRegister.visibility = android.view.View.VISIBLE
}
is Result.Success -> {
// Hide loading indicator and show success message
binding.progressBarRegister.visibility = android.view.View.GONE
Toast.makeText(this, result.data, Toast.LENGTH_SHORT).show()
val intent = Intent(this, LoginActivity::class.java)
startActivity(intent)
// Navigate to another screen if needed
}
is com.alya.ecommerce_serang.data.repository.Result.Error -> {
// Hide loading indicator and show error message
binding.progressBarRegister.visibility = android.view.View.GONE
Toast.makeText(this, "Registration Failed: ${result.exception.message}", Toast.LENGTH_SHORT).show()
}
}
}
}
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.etBirthDate.setText(sdf.format(calendar.time))
},
year, month, day
).show()
fragment?.let {
supportFragmentManager.beginTransaction()
.replace(R.id.fragment_container, it)
.addToBackStack(null)
.commit()
}
}
}

View File

@ -0,0 +1,609 @@
package com.alya.ecommerce_serang.ui.auth
import android.Manifest
import android.app.Activity
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Bundle
import android.provider.MediaStore
import android.text.Editable
import android.text.TextWatcher
import android.util.Log
import android.view.View
import android.view.ViewGroup
import android.widget.AdapterView
import android.widget.ArrayAdapter
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView
import android.widget.Toast
import androidx.activity.enableEdgeToEdge
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.core.view.ViewCompat
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat
import com.alya.ecommerce_serang.data.api.response.auth.StoreTypesItem
import com.alya.ecommerce_serang.data.api.retrofit.ApiConfig
import com.alya.ecommerce_serang.data.repository.Result
import com.alya.ecommerce_serang.data.repository.UserRepository
import com.alya.ecommerce_serang.databinding.ActivityRegisterStoreBinding
import com.alya.ecommerce_serang.ui.order.address.CityAdapter
import com.alya.ecommerce_serang.ui.order.address.ProvinceAdapter
import com.alya.ecommerce_serang.utils.BaseViewModelFactory
import com.alya.ecommerce_serang.utils.SessionManager
class RegisterStoreActivity : AppCompatActivity() {
private lateinit var binding: ActivityRegisterStoreBinding
private lateinit var sessionManager: SessionManager
private lateinit var provinceAdapter: ProvinceAdapter
private lateinit var cityAdapter: CityAdapter
// Request codes for file picking
private val PICK_STORE_IMAGE_REQUEST = 1001
private val PICK_KTP_REQUEST = 1002
private val PICK_NPWP_REQUEST = 1003
private val PICK_NIB_REQUEST = 1004
private val PICK_PERSETUJUAN_REQUEST = 1005
private val PICK_QRIS_REQUEST = 1006
// Location request code
private val LOCATION_PERMISSION_REQUEST = 2001
private val viewModel: RegisterStoreViewModel by viewModels {
BaseViewModelFactory {
val apiService = ApiConfig.getApiService(sessionManager)
val orderRepository = UserRepository(apiService)
RegisterStoreViewModel(orderRepository)
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityRegisterStoreBinding.inflate(layoutInflater)
setContentView(binding.root)
sessionManager = SessionManager(this)
WindowCompat.setDecorFitsSystemWindows(window, false)
enableEdgeToEdge()
// Apply insets to your root layout
ViewCompat.setOnApplyWindowInsetsListener(binding.root) { view, windowInsets ->
val systemBars = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
view.setPadding(
systemBars.left,
systemBars.top,
systemBars.right,
systemBars.bottom
)
windowInsets
}
provinceAdapter = ProvinceAdapter(this)
cityAdapter = CityAdapter(this)
setupDataBinding()
setupSpinners() // Location spinners
// Setup observers
setupStoreTypesObserver() // Store type observer
setupObservers()
setupMap()
setupDocumentUploads()
setupCourierSelection()
viewModel.fetchStoreTypes()
viewModel.getProvinces()
// Setup register button
binding.btnRegister.setOnClickListener {
if (viewModel.validateForm()) {
viewModel.registerStore(this)
} else {
Toast.makeText(this, "Harap lengkapi semua field yang wajib diisi", Toast.LENGTH_SHORT).show()
}
}
}
private fun setupObservers() {
// Observe province state
viewModel.provincesState.observe(this) { state ->
when (state) {
is Result.Loading -> {
Log.d(TAG, "Loading provinces...")
binding.provinceProgressBar?.visibility = View.VISIBLE
binding.spinnerProvince.isEnabled = false
}
is Result.Success -> {
Log.d(TAG, "Provinces loaded: ${state.data.size}")
binding.provinceProgressBar?.visibility = View.GONE
binding.spinnerProvince.isEnabled = true
// Update adapter with data
provinceAdapter.updateData(state.data)
}
is Result.Error -> {
// Log.e(TAG, "Error loading provinces: ${state.}")
binding.provinceProgressBar?.visibility = View.GONE
binding.spinnerProvince.isEnabled = true
// Toast.makeText(this, "Gagal memuat provinsi: ${state.message}", Toast.LENGTH_SHORT).show()
}
}
}
// Observe city state
viewModel.citiesState.observe(this) { state ->
when (state) {
is Result.Loading -> {
Log.d(TAG, "Loading cities...")
binding.cityProgressBar?.visibility = View.VISIBLE
binding.spinnerCity.isEnabled = false
}
is Result.Success -> {
Log.d(TAG, "Cities loaded: ${state.data.size}")
binding.cityProgressBar?.visibility = View.GONE
binding.spinnerCity.isEnabled = true
// Update adapter with data
cityAdapter.updateData(state.data)
}
is Result.Error -> {
// Log.e(TAG, "Error loading cities: ${state.message}")
binding.cityProgressBar?.visibility = View.GONE
binding.spinnerCity.isEnabled = true
// Toast.makeText(this, "Gagal memuat kota: ${state.message}", Toast.LENGTH_SHORT).show()
}
}
}
// Observe registration state
viewModel.registerState.observe(this) { result ->
when (result) {
is Result.Loading -> {
showLoading(true)
}
is Result.Success -> {
showLoading(false)
Toast.makeText(this, "Toko berhasil didaftarkan", Toast.LENGTH_SHORT).show()
finish() // Return to previous screen
}
is Result.Error -> {
showLoading(false)
Toast.makeText(this, "Gagal mendaftarkan toko: ${result.exception.message}", Toast.LENGTH_SHORT).show()
}
}
}
}
private fun setupStoreTypesObserver() {
// Observe loading state
viewModel.isLoadingType.observe(this) { isLoading ->
if (isLoading) {
// Show loading indicator for store types spinner
binding.spinnerStoreType.isEnabled = false
binding.storeTypeProgressBar?.visibility = View.VISIBLE
} else {
binding.spinnerStoreType.isEnabled = true
binding.storeTypeProgressBar?.visibility = View.GONE
}
}
// Observe error messages
viewModel.errorMessage.observe(this) { errorMsg ->
if (errorMsg.isNotEmpty()) {
Toast.makeText(this, "Error loading store types: $errorMsg", Toast.LENGTH_SHORT).show()
}
}
// Observe store types data
viewModel.storeTypes.observe(this) { storeTypes ->
Log.d(TAG, "Store types loaded: ${storeTypes.size}")
if (storeTypes.isNotEmpty()) {
// Add "Pilih Jenis UMKM" as the first item if it's not already there
val displayList = if (storeTypes.any { it.name == "Pilih Jenis UMKM" || it.id == 0 }) {
storeTypes
} else {
val defaultItem = StoreTypesItem(name = "Pilih Jenis UMKM", id = 0)
listOf(defaultItem) + storeTypes
}
// Setup spinner with API data
setupStoreTypeSpinner(displayList)
}
}
}
private fun setupStoreTypeSpinner(storeTypes: List<StoreTypesItem>) {
Log.d(TAG, "Setting up store type spinner with ${storeTypes.size} items")
// Create a custom adapter to display just the name but hold the whole object
val adapter = object : ArrayAdapter<StoreTypesItem>(
this,
android.R.layout.simple_spinner_item,
storeTypes
) {
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
val view = super.getView(position, convertView, parent)
(view as TextView).text = getItem(position)?.name ?: ""
return view
}
override fun getDropDownView(position: Int, convertView: View?, parent: ViewGroup): View {
val view = super.getDropDownView(position, convertView, parent)
(view as TextView).text = getItem(position)?.name ?: ""
return view
}
// Override toString to ensure proper display
override fun getItem(position: Int): StoreTypesItem? {
return super.getItem(position)
}
}
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
// Set adapter to spinner
binding.spinnerStoreType.adapter = adapter
// Set item selection listener
binding.spinnerStoreType.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
val selectedItem = adapter.getItem(position)
Log.d(TAG, "Store type selected: position=$position, item=${selectedItem?.name}, id=${selectedItem?.id}")
if (selectedItem != null && selectedItem.id > 0) {
// Store the actual ID from the API, not just position
viewModel.storeTypeId.value = selectedItem.id
Log.d(TAG, "Set storeTypeId to ${selectedItem.id}")
}
}
override fun onNothingSelected(parent: AdapterView<*>?) {
Log.d(TAG, "No store type selected")
}
}
// Hide progress bar after setup
binding.storeTypeProgressBar?.visibility = View.GONE
}
private fun setupSpinners() {
// Setup province spinner
binding.spinnerProvince.adapter = provinceAdapter
binding.spinnerProvince.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
Log.d(TAG, "Province selected at position: $position")
val provinceId = provinceAdapter.getProvinceId(position)
if (provinceId != null) {
Log.d(TAG, "Setting province ID: $provinceId")
viewModel.provinceId.value = provinceId
viewModel.getCities(provinceId)
// Reset city selection when province changes
cityAdapter.clear()
binding.spinnerCity.setSelection(0)
} else {
Log.e(TAG, "Invalid province ID for position: $position")
}
}
override fun onNothingSelected(parent: AdapterView<*>?) {
// Do nothing
}
}
// Setup city spinner
binding.spinnerCity.adapter = cityAdapter
binding.spinnerCity.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
Log.d(TAG, "City selected at position: $position")
val cityId = cityAdapter.getCityId(position)
if (cityId != null) {
Log.d(TAG, "Setting city ID: $cityId")
viewModel.cityId.value = cityId
viewModel.selectedCityId = cityId
} else {
Log.e(TAG, "Invalid city ID for position: $position")
}
}
override fun onNothingSelected(parent: AdapterView<*>?) {
// Do nothing
}
}
// Add initial hints to the spinners
if (provinceAdapter.isEmpty) {
provinceAdapter.add("Pilih Provinsi")
}
if (cityAdapter.isEmpty) {
cityAdapter.add("Pilih Kabupaten/Kota")
}
}
// private fun setupSubdistrictSpinner(cityId: Int) {
// // This would typically be populated from API based on cityId
// val subdistricts = listOf("Pilih Kecamatan", "Kecamatan 1", "Kecamatan 2", "Kecamatan 3")
// val subdistrictAdapter = ArrayAdapter(this, R.layout.simple_spinner_dropdown_item, subdistricts)
// binding.spinnerSubdistrict.adapter = subdistrictAdapter
// binding.spinnerSubdistrict.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
// override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
// if (position > 0) {
// viewModel.subdistrict.value = subdistricts[position]
// }
// }
// override fun onNothingSelected(parent: AdapterView<*>?) {}
// }
// }
private fun setupDocumentUploads() {
// Store Image
binding.containerStoreImg.setOnClickListener {
pickImage(PICK_STORE_IMAGE_REQUEST)
}
// KTP
binding.containerKtp.setOnClickListener {
pickImage(PICK_KTP_REQUEST)
}
// NIB
binding.containerNib.setOnClickListener {
pickDocument(PICK_NIB_REQUEST)
}
// NPWP
binding.containerNpwp?.setOnClickListener {
pickImage(PICK_NPWP_REQUEST)
}
// SPPIRT
binding.containerSppirt.setOnClickListener {
pickDocument(PICK_PERSETUJUAN_REQUEST)
}
// Halal
binding.containerHalal.setOnClickListener {
pickDocument(PICK_QRIS_REQUEST)
}
}
private fun pickImage(requestCode: Int) {
val intent = Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI)
startActivityForResult(intent, requestCode)
}
private fun pickDocument(requestCode: Int) {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT)
intent.addCategory(Intent.CATEGORY_OPENABLE)
intent.type = "*/*"
val mimeTypes = arrayOf("application/pdf", "image/jpeg", "image/png")
intent.putExtra(Intent.EXTRA_MIME_TYPES, mimeTypes)
startActivityForResult(intent, requestCode)
}
private fun setupCourierSelection() {
binding.checkboxJne.setOnCheckedChangeListener { _, isChecked ->
handleCourierSelection("jne", isChecked)
}
binding.checkboxJnt.setOnCheckedChangeListener { _, isChecked ->
handleCourierSelection("tiki", isChecked)
}
binding.checkboxPos.setOnCheckedChangeListener { _, isChecked ->
handleCourierSelection("pos", isChecked)
}
}
private fun handleCourierSelection(courier: String, isSelected: Boolean) {
if (isSelected) {
if (!viewModel.selectedCouriers.contains(courier)) {
viewModel.selectedCouriers.add(courier)
}
} else {
viewModel.selectedCouriers.remove(courier)
}
}
private fun setupMap() {
// This would typically integrate with Google Maps SDK
// For simplicity, we're just using a placeholder
binding.mapContainer.setOnClickListener {
// Request location permission if not granted
if (ContextCompat.checkSelfPermission(
this,
Manifest.permission.ACCESS_FINE_LOCATION
) != PackageManager.PERMISSION_GRANTED
) {
ActivityCompat.requestPermissions(
this,
arrayOf(Manifest.permission.ACCESS_FINE_LOCATION),
LOCATION_PERMISSION_REQUEST
)
viewModel.latitude.value = "-6.2088"
viewModel.longitude.value = "106.8456"
Toast.makeText(this, "Lokasi dipilih", Toast.LENGTH_SHORT).show()
} else {
// Show map selection UI
// This would typically launch Maps UI for location selection
// For now, we'll just set some dummy coordinates
viewModel.latitude.value = "-6.2088"
viewModel.longitude.value = "106.8456"
Toast.makeText(this, "Lokasi dipilih", Toast.LENGTH_SHORT).show()
}
}
}
private fun setupDataBinding() {
// Two-way data binding for text fields
binding.etStoreName.addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
override fun afterTextChanged(s: Editable?) {
viewModel.storeName.value = s.toString()
}
})
binding.etStoreDescription.addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
override fun afterTextChanged(s: Editable?) {
viewModel.storeDescription.value = s.toString()
}
})
binding.etStreet.addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
override fun afterTextChanged(s: Editable?) {
viewModel.street.value = s.toString()
}
})
binding.etPostalCode.addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
override fun afterTextChanged(s: Editable?) {
try {
viewModel.postalCode.value = s.toString().toInt()
} catch (e: NumberFormatException) {
// Handle invalid input
//show toast
}
}
})
binding.etAddressDetail.addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
override fun afterTextChanged(s: Editable?) {
viewModel.addressDetail.value = s.toString()
}
})
binding.etBankNumber.addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
override fun afterTextChanged(s: Editable?) {
viewModel.bankNumber.value = s.toString().toInt()
}
})
binding.etSubdistrict.addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
override fun afterTextChanged(s: Editable?) {
viewModel.subdistrict.value = s.toString()
}
})
binding.etBankName.addTextChangedListener(object: TextWatcher {
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
override fun afterTextChanged(s: Editable?) {
viewModel.subdistrict.value = s.toString()
}
})
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (resultCode == Activity.RESULT_OK && data != null) {
val uri = data.data
when (requestCode) {
PICK_STORE_IMAGE_REQUEST -> {
viewModel.storeImageUri = uri
updateImagePreview(uri, binding.imgStore, binding.layoutUploadStoreImg)
}
PICK_KTP_REQUEST -> {
viewModel.ktpUri = uri
updateImagePreview(uri, binding.imgKtp, binding.layoutUploadKtp)
}
PICK_NPWP_REQUEST -> {
viewModel.npwpUri = uri
updateDocumentPreview(binding.layoutUploadNpwp)
}
PICK_NIB_REQUEST -> {
viewModel.nibUri = uri
updateDocumentPreview(binding.layoutUploadNib)
}
PICK_PERSETUJUAN_REQUEST -> {
viewModel.persetujuanUri = uri
updateDocumentPreview(binding.layoutUploadSppirt)
}
PICK_QRIS_REQUEST -> {
viewModel.qrisUri = uri
updateDocumentPreview(binding.layoutUploadHalal)
}
}
}
}
private fun updateImagePreview(uri: Uri?, imageView: ImageView, uploadLayout: LinearLayout) {
uri?.let {
imageView.setImageURI(it)
imageView.visibility = View.VISIBLE
uploadLayout.visibility = View.GONE
}
}
private fun updateDocumentPreview(uploadLayout: LinearLayout) {
// For documents, we just show a success indicator
val checkIcon = ImageView(this)
checkIcon.setImageResource(android.R.drawable.ic_menu_gallery)
val successText = TextView(this)
successText.text = "Dokumen berhasil diunggah"
uploadLayout.removeAllViews()
uploadLayout.addView(checkIcon)
uploadLayout.addView(successText)
}
//later implement get location form gps
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if (requestCode == LOCATION_PERMISSION_REQUEST) {
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
// Permission granted, proceed with location selection
viewModel.latitude.value = "-6.2088"
viewModel.longitude.value = "106.8456"
Toast.makeText(this, "Lokasi dipilih", Toast.LENGTH_SHORT).show()
} else {
viewModel.latitude.value = "-6.2088"
viewModel.longitude.value = "106.8456"
}
}
}
private fun showLoading(isLoading: Boolean) {
if (isLoading) {
// Show loading indicator
binding.btnRegister.isEnabled = false
binding.btnRegister.text = "Mendaftar..."
} else {
// Hide loading indicator
binding.btnRegister.isEnabled = true
binding.btnRegister.text = "Daftar"
}
}
companion object {
private const val TAG = "RegisterStoreActivity"
}
}

View File

@ -0,0 +1,202 @@
package com.alya.ecommerce_serang.ui.auth
import android.content.Context
import android.net.Uri
import android.util.Log
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.alya.ecommerce_serang.data.api.response.auth.RegisterStoreResponse
import com.alya.ecommerce_serang.data.api.response.auth.StoreTypesItem
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.Result
import com.alya.ecommerce_serang.data.repository.UserRepository
import kotlinx.coroutines.launch
class RegisterStoreViewModel(
private val repository: UserRepository
) : ViewModel() {
// LiveData for UI state
private val _registerState = MutableLiveData<com.alya.ecommerce_serang.data.repository.Result<RegisterStoreResponse>>()
val registerState: LiveData<com.alya.ecommerce_serang.data.repository.Result<RegisterStoreResponse>> = _registerState
private val _storeTypes = MutableLiveData<List<StoreTypesItem>>()
val storeTypes: LiveData<List<StoreTypesItem>> = _storeTypes
// LiveData for error messages
private val _errorMessage = MutableLiveData<String>()
val errorMessage: LiveData<String> = _errorMessage
// LiveData for loading state
private val _isLoadingType = MutableLiveData<Boolean>()
val isLoadingType: LiveData<Boolean> = _isLoadingType
private val _provincesState = MutableLiveData<Result<List<ProvincesItem>>>()
val provincesState: LiveData<Result<List<ProvincesItem>>> = _provincesState
private val _citiesState = MutableLiveData<Result<List<CitiesItem>>>()
val citiesState: LiveData<Result<List<CitiesItem>>> = _citiesState
var selectedProvinceId: Int? = null
var selectedCityId: Int? = null
// Form fields
val storeName = MutableLiveData<String>()
val storeDescription = MutableLiveData<String>()
val storeTypeId = MutableLiveData<Int>()
val latitude = MutableLiveData<String>()
val longitude = MutableLiveData<String>()
val street = MutableLiveData<String>()
val subdistrict = MutableLiveData<String>()
val cityId = MutableLiveData<Int>()
val provinceId = MutableLiveData<Int>()
val postalCode = MutableLiveData<Int>()
val addressDetail = MutableLiveData<String>()
val bankName = MutableLiveData<String>()
val bankNumber = MutableLiveData<Int>()
val accountName = MutableLiveData<String>()
// Files
var storeImageUri: Uri? = null
var ktpUri: Uri? = null
var npwpUri: Uri? = null
var nibUri: Uri? = null
var persetujuanUri: Uri? = null
var qrisUri: Uri? = null
// Selected couriers
val selectedCouriers = mutableListOf<String>()
fun registerStore(context: Context) {
viewModelScope.launch {
try {
_registerState.value = Result.Loading
val result = repository.registerStoreUser(
context = context,
description = storeDescription.value ?: "",
storeTypeId = storeTypeId.value ?: 0,
latitude = latitude.value ?: "",
longitude = longitude.value ?: "",
street = street.value ?: "",
subdistrict = subdistrict.value ?: "",
cityId = cityId.value ?: 0,
provinceId = provinceId.value ?: 0,
postalCode = postalCode.value ?: 0,
detail = addressDetail.value,
bankName = bankName.value ?: "",
bankNum = bankNumber.value ?: 0,
storeName = storeName.value ?: "",
storeImg = storeImageUri,
ktp = ktpUri,
npwp = npwpUri,
nib = nibUri,
persetujuan = persetujuanUri,
couriers = selectedCouriers,
qris = qrisUri,
accountName = accountName.value ?: ""
)
_registerState.value = result
} catch (e: Exception) {
_registerState.value = com.alya.ecommerce_serang.data.repository.Result.Error(e)
}
}
}
// // Helper function to convert Uri to File
// private fun getFileFromUri(context: Context, uri: Uri): File {
// val inputStream = context.contentResolver.openInputStream(uri)
// val tempFile = File(context.cacheDir, "temp_file_${System.currentTimeMillis()}")
// inputStream?.use { input ->
// tempFile.outputStream().use { output ->
// input.copyTo(output)
// }
// }
// return tempFile
// }
fun validateForm(): Boolean {
// Implement form validation logic
return !(storeName.value.isNullOrEmpty() ||
storeTypeId.value == null ||
street.value.isNullOrEmpty() ||
subdistrict.value.isNullOrEmpty() ||
cityId.value == null ||
provinceId.value == null ||
postalCode.value == null ||
bankName.value.isNullOrEmpty() ||
bankNumber.value == null ||
selectedCouriers.isEmpty() ||
ktpUri == null ||
nibUri == null)
}
// Function to fetch store types
fun fetchStoreTypes() {
_isLoadingType.value = true
viewModelScope.launch {
when (val result = repository.listStoreType()) {
is Result.Success -> {
_storeTypes.value = result.data.storeTypes
_isLoadingType.value = false
}
is Result.Error -> {
_errorMessage.value = result.exception.message ?: "Unknown error occurred"
_isLoadingType.value = false
}
is Result.Loading -> {
_isLoadingType.value = true
}
}
}
}
fun getProvinces() {
_provincesState.value = Result.Loading
viewModelScope.launch {
try {
val result = repository.getListProvinces()
if (result?.provinces != null) {
_provincesState.postValue(Result.Success(result.provinces))
Log.d(TAG, "Provinces loaded: ${result.provinces.size}")
} else {
_provincesState.postValue(Result.Error(Exception("Failed to load provinces")))
Log.e(TAG, "Province result was null or empty")
}
} catch (e: Exception) {
_provincesState.postValue(Result.Error(Exception(e.message ?: "Error loading provinces")))
Log.e(TAG, "Error fetching provinces", e)
}
}
}
fun getCities(provinceId: Int){
_citiesState.value = Result.Loading
viewModelScope.launch {
try {
selectedProvinceId = provinceId
val result = repository.getListCities(provinceId)
result?.let {
_citiesState.postValue(Result.Success(it.cities))
Log.d(TAG, "Cities loaded for province $provinceId: ${it.cities.size}")
} ?: run {
_citiesState.postValue(Result.Error(Exception("Failed to load cities")))
Log.e(TAG, "City result was null for province $provinceId")
}
} catch (e: Exception) {
_citiesState.postValue(Result.Error(Exception(e.message ?: "Error loading cities")))
Log.e(TAG, "Error fetching cities for province $provinceId", e)
}
}
}
companion object {
private const val TAG = "RegisterStoreUserViewModel"
}
}

View File

@ -0,0 +1,268 @@
package com.alya.ecommerce_serang.ui.auth.fragments
import android.app.DatePickerDialog
import android.content.Intent
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import android.widget.Toast
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import com.alya.ecommerce_serang.R
import com.alya.ecommerce_serang.data.api.dto.RegisterRequest
import com.alya.ecommerce_serang.data.api.dto.VerifRegisReq
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.data.repository.UserRepository
import com.alya.ecommerce_serang.databinding.FragmentRegisterStep1Binding
import com.alya.ecommerce_serang.ui.auth.LoginActivity
import com.alya.ecommerce_serang.ui.auth.RegisterActivity
import com.alya.ecommerce_serang.utils.BaseViewModelFactory
import com.alya.ecommerce_serang.utils.viewmodel.RegisterViewModel
import com.google.android.material.progressindicator.LinearProgressIndicator
import java.text.SimpleDateFormat
import java.util.Calendar
import java.util.Locale
class RegisterStep1Fragment : Fragment() {
private var _binding: FragmentRegisterStep1Binding? = null
private val binding get() = _binding!!
private val registerViewModel: RegisterViewModel by activityViewModels {
BaseViewModelFactory {
val apiService = ApiConfig.getUnauthenticatedApiService()
val orderRepository = OrderRepository(apiService)
val userRepository = UserRepository(apiService)
RegisterViewModel(userRepository, orderRepository, requireContext())
}
}
private var isEmailValid = false
private var isPhoneValid = false
companion object {
private const val TAG = "RegisterStep1Fragment"
fun newInstance() = RegisterStep1Fragment()
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentRegisterStep1Binding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// Set step progress and description
(activity as? RegisterActivity)?.let {
it.findViewById<LinearProgressIndicator>(R.id.registration_progress)?.progress = 33
it.findViewById<TextView>(R.id.tv_step_title)?.text = "Step 1: Account & Personal Info"
it.findViewById<TextView>(R.id.tv_step_description)?.text =
"Fill in your account and personal details to create your profile."
}
setupFieldValidations()
setupObservers()
setupDatePicker()
binding.btnNext.setOnClickListener {
validateAndProceed()
}
binding.tvLoginAlt.setOnClickListener {
startActivity(Intent(requireContext(), LoginActivity::class.java))
}
}
private fun setupDatePicker() {
binding.etBirthDate.setOnClickListener {
showDatePicker()
}
}
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(
requireContext(),
{ _, selectedYear, selectedMonth, selectedDay ->
calendar.set(selectedYear, selectedMonth, selectedDay)
val sdf = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault())
binding.etBirthDate.setText(sdf.format(calendar.time))
},
year, month, day
).show()
}
private fun setupFieldValidations() {
// Validate email when focus changes
binding.etEmail.setOnFocusChangeListener { _, hasFocus ->
if (!hasFocus) {
val email = binding.etEmail.text.toString()
if (email.isNotEmpty()) {
validateEmail(email)
}
}
}
// Validate phone when focus changes
binding.etNumberPhone.setOnFocusChangeListener { _, hasFocus ->
if (!hasFocus) {
val phone = binding.etNumberPhone.text.toString()
if (phone.isNotEmpty()) {
validatePhone(phone)
}
}
}
}
private fun validateEmail(email: String) {
val checkValueEmail = VerifRegisReq(
fieldRegis = "email",
valueRegis = email
)
registerViewModel.checkValueReg(checkValueEmail)
}
private fun validatePhone(phone: String) {
val checkValuePhone = VerifRegisReq(
fieldRegis = "phone",
valueRegis = phone
)
registerViewModel.checkValueReg(checkValuePhone)
}
private fun setupObservers() {
registerViewModel.checkValue.observe(viewLifecycleOwner) { result ->
when (result) {
is com.alya.ecommerce_serang.data.repository.Result.Loading -> {
// Show loading if needed
}
is com.alya.ecommerce_serang.data.repository.Result.Success -> {
val isValid = (result.data as? Boolean) ?: false
when (val fieldType = registerViewModel.lastCheckedField) {
"email" -> {
isEmailValid = isValid
if (!isValid) {
Toast.makeText(requireContext(), "Email is already registered", Toast.LENGTH_SHORT).show()
}
}
"phone" -> {
isPhoneValid = isValid
if (!isValid) {
Toast.makeText(requireContext(), "Phone number is already registered", Toast.LENGTH_SHORT).show()
}
}
}
}
is com.alya.ecommerce_serang.data.repository.Result.Error -> {
Toast.makeText(requireContext(), "Validation failed: ${result.exception.message}", Toast.LENGTH_SHORT).show()
}
}
}
registerViewModel.otpState.observe(viewLifecycleOwner) { result ->
when (result) {
is com.alya.ecommerce_serang.data.repository.Result.Loading -> {
binding.progressBar.visibility = View.VISIBLE
binding.btnNext.isEnabled = false
}
is com.alya.ecommerce_serang.data.repository.Result.Success -> {
binding.progressBar.visibility = View.GONE
binding.btnNext.isEnabled = true
// Create user data with both account and personal info
val userData = RegisterRequest(
name = binding.etFullname.text.toString(),
email = binding.etEmail.text.toString(),
password = binding.etPassword.text.toString(),
username = binding.etUsername.text.toString(),
phone = binding.etNumberPhone.text.toString(),
birthDate = binding.etBirthDate.text.toString(),
otp = "" // Will be filled in step 2
)
registerViewModel.updateUserData(userData)
registerViewModel.setStep(2)
(activity as? RegisterActivity)?.navigateToStep(2, userData)
}
is Result.Error -> {
binding.progressBar.visibility = View.GONE
binding.btnNext.isEnabled = true
Toast.makeText(requireContext(), "OTP Request Failed: ${result.exception.message}", Toast.LENGTH_SHORT).show()
}
}
}
}
private fun validateAndProceed() {
// Validate account information
val email = binding.etEmail.text.toString()
val password = binding.etPassword.text.toString()
val confirmPassword = binding.etConfirmPassword.text.toString()
val phone = binding.etNumberPhone.text.toString()
val username = binding.etUsername.text.toString()
// Validate personal information
val fullName = binding.etFullname.text.toString()
val birthDate = binding.etBirthDate.text.toString()
// val gender = binding.etGender.text.toString()
// Check if all fields are filled
if (email.isEmpty() || password.isEmpty() || confirmPassword.isEmpty() || phone.isEmpty() ||
username.isEmpty() || fullName.isEmpty() || birthDate.isEmpty()) {
Toast.makeText(requireContext(), "Please fill all required fields", Toast.LENGTH_SHORT).show()
return
}
// Check if passwords match
if (password != confirmPassword) {
Toast.makeText(requireContext(), "Passwords do not match", Toast.LENGTH_SHORT).show()
return
}
// If both validations are already done and successful, request OTP
if (isEmailValid && isPhoneValid) {
requestOtp(email)
return
}
// Validate email and phone
validateEmail(email)
validatePhone(phone)
// Only proceed if both are valid
if (isEmailValid && isPhoneValid) {
requestOtp(email)
} else {
Toast.makeText(requireContext(), "Please fix validation errors before proceeding", Toast.LENGTH_SHORT).show()
}
}
private fun requestOtp(email: String) {
registerViewModel.requestOtp(email)
registerViewModel.message.observe(viewLifecycleOwner) { message ->
Log.d(TAG, "Message from server: $message")
// You can use the message here if needed, e.g., for showing in a specific UI element
// or for storing for later use
}
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}

View File

@ -0,0 +1,291 @@
package com.alya.ecommerce_serang.ui.auth.fragments
import android.os.Build
import android.os.Bundle
import android.os.CountDownTimer
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import android.widget.Toast
import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import com.alya.ecommerce_serang.R
import com.alya.ecommerce_serang.data.api.dto.RegisterRequest
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.data.repository.UserRepository
import com.alya.ecommerce_serang.databinding.FragmentRegisterStep2Binding
import com.alya.ecommerce_serang.ui.auth.RegisterActivity
import com.alya.ecommerce_serang.utils.BaseViewModelFactory
import com.alya.ecommerce_serang.utils.SessionManager
import com.alya.ecommerce_serang.utils.viewmodel.RegisterViewModel
import com.google.android.material.progressindicator.LinearProgressIndicator
class RegisterStep2Fragment : Fragment() {
private var _binding: FragmentRegisterStep2Binding? = null
private val binding get() = _binding!!
private lateinit var sessionManager: SessionManager
// In RegisterStep2Fragment AND RegisterStep3Fragment:
private val registerViewModel: RegisterViewModel by activityViewModels {
BaseViewModelFactory {
val apiService = ApiConfig.getUnauthenticatedApiService()
val orderRepository = OrderRepository(apiService)
val userRepository = UserRepository(apiService)
RegisterViewModel(userRepository, orderRepository, requireContext())
}
}
private var countDownTimer: CountDownTimer? = null
private var timeRemaining = 30 // 30 seconds cooldown for resend
companion object {
private const val TAG = "RegisterStep2Fragment"
fun newInstance(userData: RegisterRequest?) = RegisterStep2Fragment().apply {
arguments = Bundle().apply {
putParcelable("userData", userData)
}
}
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentRegisterStep2Binding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
sessionManager = SessionManager(requireContext())
Log.d(TAG, "SessionManager initialized, token: ${sessionManager.getToken()}")
// Set step progress and description
(activity as? RegisterActivity)?.let {
it.findViewById<LinearProgressIndicator>(R.id.registration_progress)?.progress = 66
it.findViewById<TextView>(R.id.tv_step_title)?.text = "Step 2: Verify Your Email"
it.findViewById<TextView>(R.id.tv_step_description)?.text =
"Enter the verification code sent to your email to continue."
Log.d(TAG, "Step indicators updated to Step 2")
}
// Get the user data from arguments
val userData = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
arguments?.getParcelable("userData", RegisterRequest::class.java)
} else {
@Suppress("DEPRECATION")
arguments?.getParcelable("userData") as? RegisterRequest
}
Log.d(TAG, "User data retrieved from arguments: ${userData?.email}, ${userData?.name}")
// Update the email sent message
userData?.let {
binding.tvEmailSent.text = "We've sent a verification code to ${it.email}"
}
// Start the resend cooldown timer
startResendCooldown()
Log.d(TAG, "Resend cooldown timer started")
// Set up button listeners
binding.btnVerify.setOnClickListener {
verifyOtp(userData)
}
binding.tvResendOtp.setOnClickListener {
if (timeRemaining <= 0) {
Log.d(TAG, "Resend OTP clicked, remaining time: $timeRemaining")
resendOtp(userData?.email)
} else {
Log.d(TAG, "Resend OTP clicked but cooldown active, remaining time: $timeRemaining")
}
}
observeRegistrationState()
observeLoginState()
Log.d(TAG, "Registration and login state observers set up")
}
private fun verifyOtp(userData: RegisterRequest?) {
val otp = binding.etOtp.text.toString()
Log.d(TAG, "verifyOtp called with OTP: $otp")
if (otp.isEmpty()) {
Toast.makeText(requireContext(), "Please enter the verification code", Toast.LENGTH_SHORT).show()
return
}
// Update the user data with the OTP
userData?.let {
val updatedUserData = it.copy(otp = otp)
Log.d(TAG, "Updating user data with OTP: $otp")
registerViewModel.updateUserData(updatedUserData)
// For demo purposes, we're just proceeding to Step 3
// In a real app, you would verify the OTP with the server first
// registerViewModel.setStep(3)
// (activity as? RegisterActivity)?.navigateToStep(3, updatedUserData)
registerViewModel.registerUser(updatedUserData)
} ?: Log.e(TAG, "userData is null, cannot proceed with verification")
}
private fun resendOtp(email: String?) {
Log.d(TAG, "resendOtp called for email: $email")
email?.let {
binding.progressBar.visibility = View.VISIBLE
Log.d(TAG, "Requesting OTP for: $it")
registerViewModel.requestOtp(it)
// Observe the OTP state
registerViewModel.otpState.observe(viewLifecycleOwner) { result ->
when (result) {
is com.alya.ecommerce_serang.data.repository.Result.Loading -> {
binding.progressBar.visibility = View.VISIBLE
}
is com.alya.ecommerce_serang.data.repository.Result.Success -> {
binding.progressBar.visibility = View.GONE
Toast.makeText(requireContext(), "Verification code resent", Toast.LENGTH_SHORT).show()
startResendCooldown()
}
is Result.Error -> {
Log.e(TAG, "OTP request: Error - ${result.exception.message}")
binding.progressBar.visibility = View.GONE
Toast.makeText(requireContext(), "Failed to resend code: ${result.exception.message}", Toast.LENGTH_SHORT).show()
}
else -> {
Log.d(TAG, "OTP request: Unknown state")
binding.progressBar.visibility = View.GONE
}
}
}
} ?: Log.e(TAG, "Cannot resend OTP: email is null")
}
private fun startResendCooldown() {
Log.d(TAG, "startResendCooldown called")
timeRemaining = 30
binding.tvResendOtp.isEnabled = false
binding.tvResendOtp.setTextColor(ContextCompat.getColor(requireContext(), R.color.soft_gray))
countDownTimer?.cancel()
countDownTimer = object : CountDownTimer(30000, 1000) {
override fun onTick(millisUntilFinished: Long) {
timeRemaining = (millisUntilFinished / 1000).toInt()
binding.tvTimer.text = "Resend available in 00:${String.format("%02d", timeRemaining)}"
if (timeRemaining % 5 == 0) {
Log.d(TAG, "Cooldown remaining: $timeRemaining seconds")
}
}
override fun onFinish() {
Log.d(TAG, "Cooldown finished, enabling resend button")
binding.tvTimer.text = "You can now resend the code"
binding.tvResendOtp.isEnabled = true
binding.tvResendOtp.setTextColor(ContextCompat.getColor(requireContext(), R.color.blue1))
timeRemaining = 0
}
}.start()
}
private fun observeRegistrationState() {
registerViewModel.message.observe(viewLifecycleOwner) { message ->
Log.d(TAG, "Message from server: $message")
// You can use the message here if needed, e.g., for showing in a specific UI element
// or for storing for later use
}
registerViewModel.registerState.observe(viewLifecycleOwner) { result ->
when (result) {
is Result.Loading -> {
binding.progressBar.visibility = View.VISIBLE
binding.btnVerify.isEnabled = false
}
is Result.Success -> {
Log.d(TAG, "Registration: Success - ${result.data}")
// Don't hide progress bar or re-enable button yet
// We'll wait for login to complete
// Don't show success toast yet - wait until address is added
Log.d("RegisterStep2Fragment", "Registration successful, waiting for login")
}
is Result.Error -> {
Log.e(TAG, "Registration: Error - ${result.exception.message}", result.exception)
binding.progressBar.visibility = View.GONE
binding.btnVerify.isEnabled = true
// Show error message
Toast.makeText(requireContext(), "Registration Failed: ${result.exception.message}", Toast.LENGTH_SHORT).show()
}
else -> {
Log.d(TAG, "Registration: Unknown state")
binding.progressBar.visibility = View.GONE
binding.btnVerify.isEnabled = true
}
}
}
}
private fun observeLoginState() {
registerViewModel.loginState.observe(viewLifecycleOwner) { result ->
when (result) {
is Result.Loading -> {
// Keep showing progress
binding.progressBar.visibility = View.VISIBLE
binding.btnVerify.isEnabled = false
}
is Result.Success -> {
Log.d(TAG, "Login: Success - token received")
binding.progressBar.visibility = View.GONE
binding.btnVerify.isEnabled = true
// Save the token in fragment
val accessToken = result.data.accessToken
sessionManager.saveToken(accessToken)
Log.d(TAG, "Token saved to SessionManager: $accessToken")
// Also save user ID if available in the login response
// result.data.?.let { userId ->
// sessionManager.saveUserId(userId)
// }
Log.d(TAG, "Login successful, token saved: $accessToken")
// Proceed to Step 3
Log.d(TAG, "Proceeding to Step 3 after successful login")
(activity as? RegisterActivity)?.navigateToStep(3, null )
}
is Result.Error -> {
Log.e(TAG, "Login: Error - ${result.exception.message}", result.exception)
binding.progressBar.visibility = View.GONE
binding.btnVerify.isEnabled = true
// Show error message but continue to Step 3 anyway
Log.e(TAG, "Login failed but proceeding to Step 3", result.exception)
Toast.makeText(requireContext(), "Note: Auto-login failed, but registration was successful", Toast.LENGTH_SHORT).show()
// Proceed to Step 3
(activity as? RegisterActivity)?.navigateToStep(3, null)
}
else -> {
Log.d(TAG, "Login: Unknown state")
binding.progressBar.visibility = View.GONE
binding.btnVerify.isEnabled = true
}
}
}
}
override fun onDestroyView() {
super.onDestroyView()
countDownTimer?.cancel()
_binding = null
}
}

View File

@ -0,0 +1,360 @@
package com.alya.ecommerce_serang.ui.auth.fragments
import android.content.Intent
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import android.widget.Toast
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import com.alya.ecommerce_serang.R
import com.alya.ecommerce_serang.data.api.dto.CreateAddressRequest
import com.alya.ecommerce_serang.data.api.retrofit.ApiConfig
import com.alya.ecommerce_serang.data.repository.OrderRepository
import com.alya.ecommerce_serang.data.repository.UserRepository
import com.alya.ecommerce_serang.databinding.FragmentRegisterStep3Binding
import com.alya.ecommerce_serang.ui.auth.LoginActivity
import com.alya.ecommerce_serang.ui.auth.RegisterActivity
import com.alya.ecommerce_serang.ui.order.address.CityAdapter
import com.alya.ecommerce_serang.ui.order.address.ProvinceAdapter
import com.alya.ecommerce_serang.ui.order.address.ViewState
import com.alya.ecommerce_serang.utils.BaseViewModelFactory
import com.alya.ecommerce_serang.utils.SessionManager
import com.alya.ecommerce_serang.utils.viewmodel.RegisterViewModel
import com.google.android.material.progressindicator.LinearProgressIndicator
class RegisterStep3Fragment : Fragment() {
private var _binding: FragmentRegisterStep3Binding? = null
private val binding get() = _binding!!
private lateinit var sessionManager: SessionManager
private val defaultLatitude = -6.200000
private val defaultLongitude = 106.816666
// In RegisterStep2Fragment AND RegisterStep3Fragment:
private val registerViewModel: RegisterViewModel by activityViewModels {
BaseViewModelFactory {
val apiService = ApiConfig.getUnauthenticatedApiService()
val orderRepository = OrderRepository(apiService)
val userRepository = UserRepository(apiService)
RegisterViewModel(userRepository, orderRepository, requireContext())
}
}
// For province and city selection
private val provinceAdapter by lazy { ProvinceAdapter(requireContext()) }
private val cityAdapter by lazy { CityAdapter(requireContext()) }
companion object {
private const val TAG = "RegisterStep3Fragment"
fun newInstance() = RegisterStep3Fragment()
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentRegisterStep3Binding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
sessionManager = SessionManager(requireContext())
Log.d(TAG, "SessionManager initialized, token: ${sessionManager.getToken()}")
// Set step progress and description
(activity as? RegisterActivity)?.let {
it.findViewById<LinearProgressIndicator>(R.id.registration_progress)?.progress = 33
it.findViewById<TextView>(R.id.tv_step_title)?.text = "Step 1: Account & Personal Info"
it.findViewById<TextView>(R.id.tv_step_description)?.text =
"Fill in your account and personal details to create your profile."
Log.d(TAG, "Step indicators updated to Step 1")
}
// Get registered user data
val user = registerViewModel.registeredUser.value
Log.d(TAG, "Retrieved user data: ${user?.name}, ID: ${user?.id}")
// Auto-fill recipient name and phone if available
user?.let {
binding.etNamaPenerima.setText(it.name)
binding.etNomorHp.setText(it.phone)
Log.d(TAG, "Auto-filled name: ${it.name}, phone: ${it.phone}")
}
// Set up province and city dropdowns
setupAutoComplete()
// Set up button listeners
binding.btnPrevious.setOnClickListener {
// Go back to the previous step
parentFragmentManager.popBackStack()
}
binding.btnRegister.setOnClickListener {
submitAddress()
}
// If user skips address entry
// binding.btnSkip.setOnClickListener {
// showRegistrationSuccess()
// }
// Observe address submission state
observeAddressSubmissionState()
// Load provinces
Log.d(TAG, "Requesting provinces data")
registerViewModel.getProvinces()
setupProvinceObserver()
setupCityObserver()
}
private fun setupAutoComplete() {
// Same implementation as before
binding.autoCompleteProvinsi.setAdapter(provinceAdapter)
binding.autoCompleteKabupaten.setAdapter(cityAdapter)
binding.autoCompleteProvinsi.setOnClickListener {
binding.autoCompleteProvinsi.showDropDown()
}
binding.autoCompleteKabupaten.setOnClickListener {
if (cityAdapter.count > 0) {
Log.d(TAG, "City dropdown clicked, showing ${cityAdapter.count} cities")
binding.autoCompleteKabupaten.showDropDown()
} else {
Toast.makeText(requireContext(), "Pilih provinsi terlebih dahulu", Toast.LENGTH_SHORT).show()
}
}
binding.autoCompleteProvinsi.setOnItemClickListener { _, _, position, _ ->
val provinceId = provinceAdapter.getProvinceId(position)
Log.d(TAG, "Province selected at position $position, ID: $provinceId")
provinceId?.let { id ->
registerViewModel.selectedProvinceId = id
Log.d(TAG, "Requesting cities for province ID: $id")
registerViewModel.getCities(id)
binding.autoCompleteKabupaten.text.clear()
}
}
binding.autoCompleteKabupaten.setOnItemClickListener { _, _, position, _ ->
val cityId = cityAdapter.getCityId(position)
Log.d(TAG, "City selected at position $position, ID: $cityId")
cityId?.let { id ->
Log.d(TAG, "Selected city ID set to: $id")
registerViewModel.selectedCityId = id
}
}
}
private fun setupProvinceObserver() {
// Same implementation as before
registerViewModel.provincesState.observe(viewLifecycleOwner) { state ->
when (state) {
is ViewState.Loading -> {
binding.progressBarProvinsi.visibility = View.VISIBLE
}
is ViewState.Success -> {
Log.d(TAG, "Provinces: Success - received ${state.data.size} provinces")
binding.progressBarProvinsi.visibility = View.GONE
if (state.data.isNotEmpty()) {
provinceAdapter.updateData(state.data)
} else {
showError("No provinces available")
}
}
is ViewState.Error -> {
Log.e(TAG, "Provinces: Error - ${state.message}")
binding.progressBarProvinsi.visibility = View.GONE
showError("Failed to load provinces: ${state.message}")
}
}
}
}
private fun setupCityObserver() {
// Same implementation as before
registerViewModel.citiesState.observe(viewLifecycleOwner) { state ->
when (state) {
is ViewState.Loading -> {
binding.progressBarKabupaten.visibility = View.VISIBLE
}
is ViewState.Success -> {
Log.d(TAG, "Cities: Success - received ${state.data.size} cities")
binding.progressBarKabupaten.visibility = View.GONE
cityAdapter.updateData(state.data)
Log.d(TAG, "Updated city adapter with ${state.data.size} items")
}
is ViewState.Error -> {
Log.e(TAG, "Cities: Error - ${state.message}")
binding.progressBarKabupaten.visibility = View.GONE
showError("Failed to load cities: ${state.message}")
}
}
}
}
private fun submitAddress() {
Log.d(TAG, "submitAddress called")
if (!validateAddressForm()) {
Log.w(TAG, "Address form validation failed")
return
}
val user = registerViewModel.registeredUser.value
if (user == null) {
Log.e(TAG, "User data not available")
showError("User data not available. Please try again.")
return
}
val userId = user.id
Log.d(TAG, "Using user ID: $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 provinceId = registerViewModel.selectedProvinceId?.toInt() ?: 0
val cityId = registerViewModel.selectedCityId?.toInt() ?: 0
Log.d(TAG, "Address data - Street: $street, SubDistrict: $subDistrict, PostalCode: $postalCode")
Log.d(TAG, "Address data - Recipient: $recipient, Phone: $phone")
Log.d(TAG, "Address data - ProvinceId: $provinceId, CityId: $cityId")
Log.d(TAG, "Address data - Lat: $defaultLatitude, Long: $defaultLongitude")
// Create address request
val addressRequest = CreateAddressRequest(
lat = defaultLatitude,
long = defaultLongitude,
street = street,
subDistrict = subDistrict,
cityId = cityId,
provId = provinceId,
postCode = postalCode,
detailAddress = street,
userId = userId,
recipient = recipient,
phone = phone,
isStoreLocation = false
)
Log.d(TAG, "Address request created: $addressRequest")
// Show loading
binding.progressBar.visibility = View.VISIBLE
binding.btnRegister.isEnabled = false
// binding.btnSkip.isEnabled = false
// Submit address
registerViewModel.addAddress(addressRequest)
}
private fun validateAddressForm(): Boolean {
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 provinceId = registerViewModel.selectedProvinceId
val cityId = registerViewModel.selectedCityId
Log.d(TAG, "Validating - Street: $street, SubDistrict: $subDistrict, PostalCode: $postalCode")
Log.d(TAG, "Validating - Recipient: $recipient, Phone: $phone")
Log.d(TAG, "Validating - ProvinceId: $provinceId, CityId: $cityId")
// Validate required fields
if (street.isBlank()) {
binding.etDetailAlamat.error = "Alamat tidak boleh kosong"
binding.etDetailAlamat.requestFocus()
return false
}
if (recipient.isBlank()) {
binding.etNamaPenerima.error = "Nama penerima tidak boleh kosong"
binding.etNamaPenerima.requestFocus()
return false
}
if (phone.isBlank()) {
binding.etNomorHp.error = "Nomor HP tidak boleh kosong"
binding.etNomorHp.requestFocus()
return false
}
if (provinceId == null) {
showError("Pilih provinsi terlebih dahulu")
binding.autoCompleteProvinsi.requestFocus()
return false
}
if (cityId == null) {
showError("Pilih kota/kabupaten terlebih dahulu")
binding.autoCompleteKabupaten.requestFocus()
return false
}
return true
}
private fun observeAddressSubmissionState() {
registerViewModel.addressSubmissionState.observe(viewLifecycleOwner) { state ->
when (state) {
is ViewState.Loading -> {
binding.progressBar.visibility = View.VISIBLE
binding.btnRegister.isEnabled = false
// binding.btnSkip.isEnabled = false
}
is ViewState.Success -> {
Log.d(TAG, "Address submission: Success - ${state.data}")
binding.progressBar.visibility = View.GONE
showRegistrationSuccess()
}
is ViewState.Error -> {
Log.e(TAG, "Address submission: Error - ${state.message}")
binding.progressBar.visibility = View.GONE
binding.btnRegister.isEnabled = true
// binding.btnSkip.isEnabled = true
showError("Failed to add address: ${state.message}")
}
}
}
}
private fun showRegistrationSuccess() {
// Now we can show the success message for the overall registration process
Toast.makeText(requireContext(), "Registration completed successfully!", Toast.LENGTH_LONG).show()
// Navigate to login screen
startActivity(Intent(requireContext(), LoginActivity::class.java))
Log.d(TAG, "Navigating to LoginActivity")
activity?.finish()
}
private fun showError(message: String) {
Toast.makeText(requireContext(), message, Toast.LENGTH_SHORT).show()
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
//
// // Data classes for province and city
// data class Province(val id: String, val name: String)
// data class City(val id: String, val name: String)
}

View File

@ -0,0 +1,200 @@
package com.alya.ecommerce_serang.ui.cart
import android.os.Bundle
import android.view.View
import android.widget.Toast
import androidx.activity.enableEdgeToEdge
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.view.ViewCompat
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat
import androidx.recyclerview.widget.LinearLayoutManager
import com.alya.ecommerce_serang.R
import com.alya.ecommerce_serang.data.api.response.customer.product.CartItemCheckoutInfo
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.ActivityCartBinding
import com.alya.ecommerce_serang.ui.order.CheckoutActivity
import com.alya.ecommerce_serang.utils.BaseViewModelFactory
import com.alya.ecommerce_serang.utils.SessionManager
import java.text.NumberFormat
import java.util.Locale
class CartActivity : AppCompatActivity() {
private lateinit var binding: ActivityCartBinding
private lateinit var apiService: ApiService
private lateinit var sessionManager: SessionManager
private lateinit var storeAdapter: StoreAdapter
private val viewModel: CartViewModel by viewModels {
BaseViewModelFactory {
val apiService = ApiConfig.getApiService(sessionManager)
val orderRepository = OrderRepository(apiService)
CartViewModel(orderRepository)
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityCartBinding.inflate(layoutInflater)
setContentView(binding.root)
sessionManager = SessionManager(this)
apiService = ApiConfig.getApiService(sessionManager)
WindowCompat.setDecorFitsSystemWindows(window, false)
enableEdgeToEdge()
// Apply insets to your root layout
ViewCompat.setOnApplyWindowInsetsListener(binding.root) { view, windowInsets ->
val systemBars = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
view.setPadding(
systemBars.left,
systemBars.top,
systemBars.right,
systemBars.bottom
)
windowInsets
}
setupRecyclerView()
setupListeners()
observeViewModel()
viewModel.getCart()
}
private fun setupRecyclerView() {
storeAdapter = StoreAdapter(
onStoreCheckChanged = { storeId, isChecked ->
if (isChecked) {
viewModel.toggleStoreSelection(storeId)
} else {
viewModel.toggleStoreSelection(storeId)
}
},
onItemCheckChanged = { cartItemId, storeId, isChecked ->
viewModel.toggleItemSelection(cartItemId, storeId)
},
onItemQuantityChanged = { cartItemId, quantity ->
viewModel.updateCartItem(cartItemId, quantity)
},
onItemDeleted = { cartItemId ->
viewModel.deleteCartItem(cartItemId)
}
)
binding.rvCart.apply {
layoutManager = LinearLayoutManager(this@CartActivity)
adapter = storeAdapter
}
}
private fun setupListeners() {
binding.cbSelectAll.setOnCheckedChangeListener { _, _ ->
viewModel.toggleSelectAll()
}
binding.btnCheckout.setOnClickListener {
if (viewModel.totalSelectedCount.value ?: 0 > 0) {
val selectedItems = viewModel.prepareCheckout()
if (selectedItems.isNotEmpty()) {
// Check if all items are from the same store
val storeId = viewModel.activeStoreId.value
if (storeId != null) {
// Start checkout with the prepared items
startCheckoutWithWholesaleInfo(selectedItems)
} else {
Toast.makeText(this, "Please select items from a single store only", Toast.LENGTH_SHORT).show()
}
}
} else {
Toast.makeText(this, "Pilih produk terlebih dahulu", Toast.LENGTH_SHORT).show()
}
}
binding.btnShopNow.setOnClickListener {
// Navigate to product listing/home
//implement home or search activity
finish()
}
}
private fun startCheckoutWithWholesaleInfo(checkoutItems: List<CartItemCheckoutInfo>) {
// Extract cart item IDs and wholesale status
val cartItemIds = checkoutItems.map { it.cartItem.cartItemId }
val wholesaleArray = checkoutItems.map { it.isWholesale }.toBooleanArray()
// Start checkout activity with the cart items and wholesale info
CheckoutActivity.startForCart(this, cartItemIds, wholesaleArray)
}
private fun observeViewModel() {
viewModel.cartItems.observe(this) { cartItems ->
if (cartItems.isNullOrEmpty()) {
showEmptyState(true)
} else {
showEmptyState(false)
storeAdapter.submitList(cartItems)
}
}
viewModel.isLoading.observe(this) { isLoading ->
// Show/hide loading indicator if needed
}
viewModel.errorMessage.observe(this) { errorMessage ->
errorMessage?.let {
Toast.makeText(this, it, Toast.LENGTH_SHORT).show()
}
}
viewModel.totalPrice.observe(this) { totalPrice ->
binding.tvTotalPrice.text = formatCurrency(totalPrice)
}
viewModel.totalSelectedCount.observe(this) { count ->
binding.btnCheckout.text = "Beli ($count)"
}
viewModel.selectedItems.observe(this) { selectedItems ->
viewModel.selectedStores.value?.let { selectedStores ->
viewModel.activeStoreId.value?.let { activeStoreId ->
storeAdapter.updateSelectedItems(selectedItems, selectedStores, activeStoreId)
}
}
}
viewModel.allSelected.observe(this) { allSelected ->
// Update the "select all" checkbox without triggering the listener
val selectCbAll = binding.cbSelectAll
selectCbAll.setOnCheckedChangeListener(null)
selectCbAll.isChecked = allSelected
selectCbAll.setOnCheckedChangeListener { _, _ ->
viewModel.toggleSelectAll()
}
}
}
private fun showEmptyState(isEmpty: Boolean) {
if (isEmpty) {
binding.rvCart.visibility = View.GONE
binding.emptyStateLayout.visibility = View.VISIBLE
findViewById<ConstraintLayout>(R.id.bottomCheckoutLayout).visibility = View.GONE
} else {
binding.rvCart.visibility = View.VISIBLE
binding.emptyStateLayout.visibility = View.GONE
findViewById<ConstraintLayout>(R.id.bottomCheckoutLayout).visibility = View.VISIBLE
}
}
private fun formatCurrency(amount: Int): String {
val format = NumberFormat.getCurrencyInstance(Locale("id", "ID"))
return format.format(amount).replace("Rp", "Rp ")
}
}

View File

@ -0,0 +1,382 @@
package com.alya.ecommerce_serang.ui.cart
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.UpdateCart
import com.alya.ecommerce_serang.data.api.response.customer.cart.DataItemCart
import com.alya.ecommerce_serang.data.api.response.customer.product.CartItemCheckoutInfo
import com.alya.ecommerce_serang.data.repository.OrderRepository
import com.alya.ecommerce_serang.data.repository.Result
import kotlinx.coroutines.launch
class CartViewModel(private val repository: OrderRepository) : ViewModel() {
private val _cartItems = MutableLiveData<List<DataItemCart>>()
val cartItems: LiveData<List<DataItemCart>> = _cartItems
private val _isLoading = MutableLiveData<Boolean>()
val isLoading: LiveData<Boolean> = _isLoading
private val _errorMessage = MutableLiveData<String?>()
val errorMessage: LiveData<String?> = _errorMessage
private val _totalPrice = MutableLiveData<Int>(0)
val totalPrice: LiveData<Int> = _totalPrice
private val _selectedItems = MutableLiveData<HashSet<Int>>(HashSet())
val selectedItems: LiveData<HashSet<Int>> = _selectedItems
private val _selectedStores = MutableLiveData<HashSet<Int>>(HashSet())
val selectedStores: LiveData<HashSet<Int>> = _selectedStores
private val _totalSelectedCount = MutableLiveData<Int>(0)
val totalSelectedCount: LiveData<Int> = _totalSelectedCount
// Track the currently active store ID for checkout
private val _activeStoreId = MutableLiveData<Int?>(null)
val activeStoreId: LiveData<Int?> = _activeStoreId
// Track if all items are selected
private val _allSelected = MutableLiveData<Boolean>(false)
val allSelected: LiveData<Boolean> = _allSelected
private val _cartItemWholesaleStatus = MutableLiveData<Map<Int, Boolean>>(mapOf())
val cartItemWholesaleStatus: LiveData<Map<Int, Boolean>> = _cartItemWholesaleStatus
private val _cartItemWholesalePrice = MutableLiveData<Map<Int, Double>>(mapOf())
val cartItemWholesalePrice: LiveData<Map<Int, Double>> = _cartItemWholesalePrice
fun getCart() {
_isLoading.value = true
_errorMessage.value = null
viewModelScope.launch {
when (val result = repository.getCart()) {
is Result.Success -> {
_cartItems.value = result.data
_isLoading.value = false
// After loading cart items, check wholesale status
checkWholesaleStatus()
}
is Result.Error -> {
_errorMessage.value = result.exception.message
_isLoading.value = false
}
is Result.Loading -> {
null
}
}
}
}
fun updateCartItem(cartItemId: Int, quantity: Int) {
_isLoading.value = true
viewModelScope.launch {
try {
val updateCart = UpdateCart(cartItemId, quantity)
val result = repository.updateCart(updateCart)
if (result is com.alya.ecommerce_serang.data.repository.Result.Success) {
// Refresh cart data after successful update
getCart()
calculateTotalPrice()
} else {
_errorMessage.value = (result as com.alya.ecommerce_serang.data.repository.Result.Error).exception.message
}
} catch (e: Exception) {
_errorMessage.value = e.message
} finally {
_isLoading.value = false
}
}
}
fun deleteCartItem(cartItemId: Int) {
_isLoading.value = true
viewModelScope.launch {
try {
val result = repository.deleteCartItem(cartItemId)
if (result is com.alya.ecommerce_serang.data.repository.Result.Success) {
// Remove the item from selected items if it was selected
val currentSelectedItems = _selectedItems.value ?: HashSet()
if (currentSelectedItems.contains(cartItemId)) {
currentSelectedItems.remove(cartItemId)
_selectedItems.value = currentSelectedItems
}
// Refresh cart data after successful deletion
getCart()
calculateTotalPrice()
} else {
_errorMessage.value = (result as Result.Error).exception.message
}
} catch (e: Exception) {
_errorMessage.value = e.message
} finally {
_isLoading.value = false
}
}
}
fun toggleItemSelection(cartItemId: Int, storeId: Int) {
val currentSelectedItems = _selectedItems.value ?: HashSet()
val currentSelectedStores = _selectedStores.value ?: HashSet()
if (currentSelectedItems.contains(cartItemId)) {
currentSelectedItems.remove(cartItemId)
// Check if there are no more selected items for this store
val storeHasSelectedItems = _cartItems.value?.find { it.storeId == storeId }
?.cartItems?.any { currentSelectedItems.contains(it.cartItemId) } ?: false
if (!storeHasSelectedItems) {
currentSelectedStores.remove(storeId)
// If this was the active store, set active store to null
if (_activeStoreId.value == storeId) {
_activeStoreId.value = null
}
}
} else {
// If there's an active store different from this item's store, deselect all items first
if (_activeStoreId.value != null && _activeStoreId.value != storeId) {
currentSelectedItems.clear()
currentSelectedStores.clear()
}
currentSelectedItems.add(cartItemId)
currentSelectedStores.add(storeId)
// Set the active store
_activeStoreId.value = storeId
}
_selectedItems.value = currentSelectedItems
_selectedStores.value = currentSelectedStores
calculateTotalPrice()
updateTotalSelectedCount()
checkAllSelected()
}
fun toggleStoreSelection(storeId: Int) {
val currentSelectedItems = _selectedItems.value ?: HashSet()
val currentSelectedStores = _selectedStores.value ?: HashSet()
val storeItems = _cartItems.value?.find { it.storeId == storeId }?.cartItems ?: emptyList()
if (currentSelectedStores.contains(storeId)) {
// Deselect all items of this store
currentSelectedStores.remove(storeId)
storeItems.forEach { currentSelectedItems.remove(it.cartItemId) }
// If this was the active store, set active store to null
if (_activeStoreId.value == storeId) {
_activeStoreId.value = null
}
} else {
// If there's another active store, deselect all items first
if (_activeStoreId.value != null && _activeStoreId.value != storeId) {
currentSelectedItems.clear()
currentSelectedStores.clear()
}
// Select all items of this store
currentSelectedStores.add(storeId)
storeItems.forEach { currentSelectedItems.add(it.cartItemId) }
// Set this as the active store
_activeStoreId.value = storeId
}
_selectedItems.value = currentSelectedItems
_selectedStores.value = currentSelectedStores
calculateTotalPrice()
updateTotalSelectedCount()
checkAllSelected()
}
fun toggleSelectAll() {
val allItems = _cartItems.value ?: emptyList()
val currentSelected = _allSelected.value ?: false
if (currentSelected) {
// Deselect all
_selectedItems.value = HashSet()
_selectedStores.value = HashSet()
_activeStoreId.value = null
_allSelected.value = false
} else {
// If we have multiple stores, we need a special handling
if (allItems.size > 1) {
// Select all items from the first store only
val firstStore = allItems.firstOrNull()
if (firstStore != null) {
val selectedItems = HashSet<Int>()
firstStore.cartItems.forEach { selectedItems.add(it.cartItemId) }
_selectedItems.value = selectedItems
_selectedStores.value = HashSet<Int>().apply { add(firstStore.storeId) }
_activeStoreId.value = firstStore.storeId
}
} else {
// Single store, select all items
val selectedItems = HashSet<Int>()
val selectedStores = HashSet<Int>()
allItems.forEach { dataItem ->
selectedStores.add(dataItem.storeId)
dataItem.cartItems.forEach { cartItem ->
selectedItems.add(cartItem.cartItemId)
}
}
_selectedItems.value = selectedItems
_selectedStores.value = selectedStores
if (allItems.isNotEmpty()) {
_activeStoreId.value = allItems[0].storeId
}
}
_allSelected.value = true
}
calculateTotalPrice()
updateTotalSelectedCount()
}
private fun calculateTotalPrice() {
val selectedItems = _selectedItems.value ?: HashSet()
val wholesaleStatus = _cartItemWholesaleStatus.value ?: mapOf()
val wholesalePrices = _cartItemWholesalePrice.value ?: mapOf()
var total = 0
_cartItems.value?.forEach { dataItem ->
dataItem.cartItems.forEach { cartItem ->
if (selectedItems.contains(cartItem.cartItemId)) {
// Check if this item qualifies for wholesale pricing
if (wholesaleStatus[cartItem.cartItemId] == true &&
wholesalePrices.containsKey(cartItem.cartItemId)) {
// Use wholesale price
total += (wholesalePrices[cartItem.cartItemId]!!.toInt() * cartItem.quantity)
} else {
// Use regular price
total += cartItem.price * cartItem.quantity
}
}
}
}
_totalPrice.value = total
}
private fun updateTotalSelectedCount() {
_totalSelectedCount.value = _selectedItems.value?.size ?: 0
}
private fun checkAllSelected() {
val allItems = _cartItems.value ?: emptyList()
val selectedItems = _selectedItems.value ?: HashSet()
// If there are multiple stores, "all selected" is true only if all items of the active store are selected
val activeStoreId = _activeStoreId.value
val isAllSelected = if (activeStoreId != null) {
val activeStoreItems = allItems.find { it.storeId == activeStoreId }?.cartItems ?: emptyList()
activeStoreItems.all { selectedItems.contains(it.cartItemId) }
} else {
// No active store, so check if all items of any store are selected
allItems.any { dataItem ->
dataItem.cartItems.all { selectedItems.contains(it.cartItemId) }
}
}
_allSelected.value = isAllSelected
}
fun prepareCheckout(): List<CartItemCheckoutInfo> {
val selectedItemsIds = _selectedItems.value ?: HashSet()
val wholesaleStatus = _cartItemWholesaleStatus.value ?: mapOf()
val result = mutableListOf<CartItemCheckoutInfo>()
if (_activeStoreId.value != null) {
_cartItems.value?.forEach { dataItem ->
dataItem.cartItems.forEach { cartItem ->
if (selectedItemsIds.contains(cartItem.cartItemId)) {
// Check wholesale status for this cart item
val isWholesale = wholesaleStatus[cartItem.cartItemId] ?: false
result.add(
CartItemCheckoutInfo(
cartItem = cartItem,
isWholesale = isWholesale
)
)
}
}
}
}
return result
}
private fun checkWholesaleStatus() {
viewModelScope.launch {
val cartItems = _cartItems.value ?: return@launch
val wholesaleStatusMap = mutableMapOf<Int, Boolean>()
val wholesalePriceMap = mutableMapOf<Int, Double>()
// Process each cart item
for (store in cartItems) {
for (item in store.cartItems) {
try {
// Fetch product details to get wholesale information
val productResponse = repository.fetchProductDetail(item.productId)
if (productResponse != null) {
val product = productResponse.product
// Check if wholesale is available and if quantity meets minimum
val isWholesale = product.isWholesale == true &&
product.wholesaleMinItem != null &&
item.quantity >= product.wholesaleMinItem
wholesaleStatusMap[item.cartItemId] = isWholesale
// If wholesale applies, store the wholesale price
if (isWholesale && product.wholesalePrice != null) {
wholesalePriceMap[item.cartItemId] = product.wholesalePrice.toDouble()
}
Log.d("CartViewModel", "Cart item ${item.cartItemId}: isWholesale=$isWholesale, min=${product.wholesaleMinItem}, qty=${item.quantity}")
} else {
// If product details couldn't be fetched, default to non-wholesale
Log.e("CartViewModel", "Failed to fetch product details for ID: ${item.productId}")
wholesaleStatusMap[item.cartItemId] = false
}
} catch (e: Exception) {
// If we can't determine wholesale status, default to false
Log.e("CartViewModel", "Exception checking wholesale status: ${e.message}")
wholesaleStatusMap[item.cartItemId] = false
}
}
}
Log.d("CartViewModel", "Wholesale status map: $wholesaleStatusMap")
Log.d("CartViewModel", "Wholesale price map: $wholesalePriceMap")
_cartItemWholesaleStatus.value = wholesaleStatusMap
_cartItemWholesalePrice.value = wholesalePriceMap
// Recalculate total price to account for wholesale prices
calculateTotalPrice()
}
}
}

View File

@ -0,0 +1,231 @@
package com.alya.ecommerce_serang.ui.cart
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.CheckBox
import android.widget.ImageButton
import android.widget.ImageView
import android.widget.TextView
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import com.alya.ecommerce_serang.R
import com.alya.ecommerce_serang.data.api.response.customer.cart.CartItemsItem
import com.alya.ecommerce_serang.data.api.response.customer.cart.DataItemCart
import com.bumptech.glide.Glide
import java.text.NumberFormat
import java.util.Locale
class StoreAdapter(
private val onStoreCheckChanged: (Int, Boolean) -> Unit,
private val onItemCheckChanged: (Int, Int, Boolean) -> Unit,
private val onItemQuantityChanged: (Int, Int) -> Unit,
private val onItemDeleted: (Int) -> Unit
) : ListAdapter<DataItemCart, RecyclerView.ViewHolder>(StoreDiffCallback()) {
private var selectedItems = HashSet<Int>()
private var selectedStores = HashSet<Int>()
private var activeStoreId: Int? = null
companion object {
private const val VIEW_TYPE_STORE = 0
private const val VIEW_TYPE_ITEM = 1
}
fun updateSelectedItems(selectedItems: HashSet<Int>, selectedStores: HashSet<Int>, activeStoreId: Int?) {
this.selectedItems = selectedItems
this.selectedStores = selectedStores
this.activeStoreId = activeStoreId
notifyDataSetChanged()
}
override fun getItemViewType(position: Int): Int {
var itemCount = 0
for (store in currentList) {
// Store header
if (position == itemCount) {
return VIEW_TYPE_STORE
}
itemCount++
// Check if position is in the range of this store's items
if (position < itemCount + store.cartItems.size) {
return VIEW_TYPE_ITEM
}
itemCount += store.cartItems.size
}
return -1
}
override fun getItemCount(): Int {
var count = 0
for (store in currentList) {
// One for store header
count++
// Plus the items in this store
count += store.cartItems.size
}
return count
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return when (viewType) {
VIEW_TYPE_STORE -> {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.item_store_cart, parent, false)
StoreViewHolder(view)
}
VIEW_TYPE_ITEM -> {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.item_cart_product, parent, false)
CartItemViewHolder(view)
}
else -> throw IllegalArgumentException("Invalid view type")
}
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
val (storeIndex, itemIndex) = getStoreAndItemIndex(position)
val store = currentList[storeIndex]
when (holder) {
is StoreViewHolder -> {
holder.bind(store, selectedStores.contains(store.storeId), activeStoreId == store.storeId) { isChecked ->
onStoreCheckChanged(store.storeId, isChecked)
}
}
is CartItemViewHolder -> {
val cartItem = store.cartItems[itemIndex]
val isSelected = selectedItems.contains(cartItem.cartItemId)
val isEnabled = activeStoreId == null || activeStoreId == store.storeId
holder.bind(
cartItem,
isSelected,
isEnabled,
{ isChecked -> onItemCheckChanged(cartItem.cartItemId, store.storeId, isChecked) },
{ quantity -> onItemQuantityChanged(cartItem.cartItemId, quantity) },
{ onItemDeleted(cartItem.cartItemId) }
)
}
}
}
private fun getStoreAndItemIndex(position: Int): Pair<Int, Int> {
var itemCount = 0
for (storeIndex in currentList.indices) {
// Store header position
if (position == itemCount) {
return Pair(storeIndex, -1)
}
itemCount++
// Check if position is in the range of this store's items
val store = currentList[storeIndex]
if (position < itemCount + store.cartItems.size) {
return Pair(storeIndex, position - itemCount)
}
itemCount += store.cartItems.size
}
throw IllegalArgumentException("Invalid position")
}
class StoreViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
private val cbStore: CheckBox = itemView.findViewById(R.id.cbStore)
private val tvStoreName: TextView = itemView.findViewById(R.id.tvStoreName)
fun bind(store: DataItemCart, isChecked: Boolean, isActiveStore: Boolean, onCheckedChange: (Boolean) -> Unit) {
tvStoreName.text = store.storeName
// Set checkbox state without triggering listener
cbStore.setOnCheckedChangeListener(null)
cbStore.isChecked = isChecked
// Only enable checkbox if this store is active or no store is active
cbStore.setOnCheckedChangeListener { _, isChecked ->
onCheckedChange(isChecked)
}
}
}
class CartItemViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
private val cbItem: CheckBox = itemView.findViewById(R.id.cbItem)
private val ivProduct: ImageView = itemView.findViewById(R.id.ivProduct)
private val tvProductName: TextView = itemView.findViewById(R.id.tvProductName)
private val tvPrice: TextView = itemView.findViewById(R.id.tvPrice)
private val btnMinus: ImageButton = itemView.findViewById(R.id.btnMinus)
private val tvQuantity: TextView = itemView.findViewById(R.id.tvQuantity)
private val btnPlus: ImageButton = itemView.findViewById(R.id.btnPlus)
private val quantityController: ConstraintLayout = itemView.findViewById(R.id.quantityController)
fun bind(
cartItem: CartItemsItem,
isChecked: Boolean,
isEnabled: Boolean,
onCheckedChange: (Boolean) -> Unit,
onQuantityChanged: (Int) -> Unit,
onDelete: () -> Unit
) {
tvProductName.text = cartItem.productName
tvPrice.text = formatCurrency(cartItem.price)
tvQuantity.text = cartItem.quantity.toString()
// Load product image
Glide.with(itemView.context)
.load("https://example.com/images/${cartItem.productId}.jpg") // Assume image URL based on product ID
.placeholder(R.drawable.placeholder_image)
.error(R.drawable.placeholder_image)
.into(ivProduct)
// Set checkbox state without triggering listener
cbItem.setOnCheckedChangeListener(null)
cbItem.isChecked = isChecked
cbItem.isEnabled = isEnabled
cbItem.setOnCheckedChangeListener { _, isChecked ->
onCheckedChange(isChecked)
}
// Quantity control
btnMinus.setOnClickListener {
val currentQty = tvQuantity.text.toString().toInt()
if (currentQty > 1) {
val newQty = currentQty - 1
tvQuantity.text = newQty.toString()
onQuantityChanged(newQty)
} else {
// If quantity would be 0, delete the item
onDelete()
}
}
btnPlus.setOnClickListener {
val currentQty = tvQuantity.text.toString().toInt()
val newQty = currentQty + 1
tvQuantity.text = newQty.toString()
onQuantityChanged(newQty)
}
// Disable quantity controls if item is not from active store
btnMinus.isEnabled = isEnabled
btnPlus.isEnabled = isEnabled
}
private fun formatCurrency(amount: Int): String {
val format = NumberFormat.getCurrencyInstance(Locale("id", "ID"))
return format.format(amount).replace("Rp", "Rp ")
}
}
}
class StoreDiffCallback : DiffUtil.ItemCallback<DataItemCart>() {
override fun areItemsTheSame(oldItem: DataItemCart, newItem: DataItemCart): Boolean {
return oldItem.storeId == newItem.storeId
}
override fun areContentsTheSame(oldItem: DataItemCart, newItem: DataItemCart): Boolean {
return oldItem == newItem
}
}

View File

@ -125,6 +125,7 @@ class ChatActivity : AppCompatActivity() {
val productRating = intent.getFloatExtra(Constants.EXTRA_PRODUCT_RATING, 0f)
val storeName = intent.getStringExtra(Constants.EXTRA_STORE_NAME) ?: ""
val chatRoomId = intent.getIntExtra(Constants.EXTRA_CHAT_ROOM_ID, 0)
val storeImg = intent.getStringExtra(Constants.EXTRA_STORE_IMAGE) ?: ""
// Check if user is logged in
val token = sessionManager.getToken()
@ -137,7 +138,20 @@ class ChatActivity : AppCompatActivity() {
return
}
// Set chat parameters to ViewModel
binding.tvStoreName.text = storeName
val fullImageUrl = when (val img = storeImg) {
is String -> {
if (img.startsWith("/")) BASE_URL + img.substring(1) else img
}
else -> R.drawable.placeholder_image
}
Glide.with(this)
.load(fullImageUrl)
.placeholder(R.drawable.placeholder_image)
.into(binding.imgProfile)
// Set chat parameters to ViewModel
viewModel.setChatParameters(
storeId = storeId,
productId = productId,
@ -227,6 +241,7 @@ class ChatActivity : AppCompatActivity() {
}
})
// Observe state changes using LiveData
viewModel.state.observe(this, Observer { state ->
// Update messages
@ -244,6 +259,7 @@ class ChatActivity : AppCompatActivity() {
binding.ratingBar.rating = state.productRating
binding.tvRating.text = state.productRating.toString()
binding.tvSellerName.text = state.storeName
binding.tvStoreName.text=state.storeName
// Load product image
if (!state.productImageUrl.isNullOrEmpty()) {
@ -270,6 +286,7 @@ class ChatActivity : AppCompatActivity() {
binding.editTextMessage.hint = getString(R.string.write_message)
}
// Show typing indicator
binding.tvTypingIndicator.visibility =
if (state.isOtherUserTyping) View.VISIBLE else View.GONE
@ -467,7 +484,8 @@ class ChatActivity : AppCompatActivity() {
productImage: String? = null,
productRating: String? = null,
storeName: String? = null,
chatRoomId: Int = 0
chatRoomId: Int = 0,
storeImage: String? = null
) {
val intent = Intent(context, ChatActivity::class.java).apply {
putExtra(Constants.EXTRA_STORE_ID, storeId)
@ -475,6 +493,7 @@ class ChatActivity : AppCompatActivity() {
putExtra(Constants.EXTRA_PRODUCT_NAME, productName)
putExtra(Constants.EXTRA_PRODUCT_PRICE, productPrice)
putExtra(Constants.EXTRA_PRODUCT_IMAGE, productImage)
putExtra(Constants.EXTRA_STORE_IMAGE, storeImage)
// Convert productRating string to float if provided
if (productRating != null) {

View File

@ -1,337 +0,0 @@
//package com.alya.ecommerce_serang.ui.chat
//
//import android.Manifest
//import android.app.Activity
//import android.content.Intent
//import android.content.pm.PackageManager
//import android.net.Uri
//import android.os.Bundle
//import android.provider.MediaStore
//import android.text.Editable
//import android.text.TextWatcher
//import androidx.fragment.app.Fragment
//import android.view.LayoutInflater
//import android.view.View
//import android.view.ViewGroup
//import android.widget.Toast
//import androidx.activity.result.contract.ActivityResultContracts
//import androidx.core.app.ActivityCompat
//import androidx.core.content.ContextCompat
//import androidx.core.content.FileProvider
//import androidx.fragment.app.viewModels
//import androidx.lifecycle.lifecycleScope
//import androidx.navigation.fragment.navArgs
//import androidx.recyclerview.widget.LinearLayoutManager
//import com.alya.ecommerce_serang.BuildConfig.BASE_URL
//import com.alya.ecommerce_serang.R
//import com.alya.ecommerce_serang.databinding.FragmentChatBinding
//import com.alya.ecommerce_serang.utils.Constants
//import com.bumptech.glide.Glide
//import dagger.hilt.android.AndroidEntryPoint
//import kotlinx.coroutines.launch
//import java.io.File
//import java.text.SimpleDateFormat
//import java.util.Locale
//
//@AndroidEntryPoint
//class ChatFragment : Fragment() {
//
// private var _binding: FragmentChatBinding? = null
// private val binding get() = _binding!!
//
// private val viewModel: ChatViewModel by viewModels()
//// private val args: ChatFragmentArgs by navArgs()
//
// private lateinit var chatAdapter: ChatAdapter
//
// // For image attachment
// private var tempImageUri: Uri? = null
//
// // Typing indicator handler
// private val typingHandler = android.os.Handler(android.os.Looper.getMainLooper())
// private val stopTypingRunnable = Runnable {
// viewModel.sendTypingStatus(false)
// }
//
// // Activity Result Launchers
// private val pickImageLauncher = registerForActivityResult(
// ActivityResultContracts.StartActivityForResult()
// ) { result ->
// if (result.resultCode == Activity.RESULT_OK) {
// result.data?.data?.let { uri ->
// handleSelectedImage(uri)
// }
// }
// }
//
// private val takePictureLauncher = registerForActivityResult(
// ActivityResultContracts.StartActivityForResult()
// ) { result ->
// if (result.resultCode == Activity.RESULT_OK) {
// tempImageUri?.let { uri ->
// handleSelectedImage(uri)
// }
// }
// }
//
// override fun onCreateView(
// inflater: LayoutInflater,
// container: ViewGroup?,
// savedInstanceState: Bundle?
// ): View {
// _binding = FragmentChatBinding.inflate(inflater, container, false)
// return binding.root
// }
//
// override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
// super.onViewCreated(view, savedInstanceState)
//
// setupRecyclerView()
// setupListeners()
// setupTypingIndicator()
// observeViewModel()
// }
//
// private fun setupRecyclerView() {
// chatAdapter = ChatAdapter()
// binding.recyclerChat.apply {
// adapter = chatAdapter
// layoutManager = LinearLayoutManager(requireContext()).apply {
// stackFromEnd = true
// }
// }
// }
//
// private fun setupListeners() {
// // Back button
// binding.btnBack.setOnClickListener {
// requireActivity().onBackPressed()
// }
//
// // Options button
// binding.btnOptions.setOnClickListener {
// showOptionsMenu()
// }
//
// // Send button
// binding.btnSend.setOnClickListener {
// val message = binding.editTextMessage.text.toString().trim()
// if (message.isNotEmpty() || viewModel.state.value.hasAttachment) {
// viewModel.sendMessage(message)
// binding.editTextMessage.text.clear()
// }
// }
//
// // Attachment button
// binding.btnAttachment.setOnClickListener {
// checkPermissionsAndShowImagePicker()
// }
// }
//
// private fun setupTypingIndicator() {
// binding.editTextMessage.addTextChangedListener(object : TextWatcher {
// override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
//
// override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
// viewModel.sendTypingStatus(true)
//
// // Reset the timer
// typingHandler.removeCallbacks(stopTypingRunnable)
// typingHandler.postDelayed(stopTypingRunnable, 1000)
// }
//
// override fun afterTextChanged(s: Editable?) {}
// })
// }
//
// private fun observeViewModel() {
// viewLifecycleOwner.lifecycleScope.launch {
// viewModel.state.collectLatest { state ->
// // Update messages
// chatAdapter.submitList(state.messages)
//
// // Scroll to bottom if new message
// if (state.messages.isNotEmpty()) {
// binding.recyclerChat.scrollToPosition(state.messages.size - 1)
// }
//
// // Update product info
// binding.tvProductName.text = state.productName
// binding.tvProductPrice.text = state.productPrice
// binding.ratingBar.rating = state.productRating
// binding.tvRating.text = state.productRating.toString()
// binding.tvSellerName.text = state.storeName
//
// // Load product image
// if (state.productImageUrl.isNotEmpty()) {
// Glide.with(requireContext())
// .load(BASE_URL + state.productImageUrl)
// .centerCrop()
// .placeholder(R.drawable.placeholder_image)
// .error(R.drawable.placeholder_image)
// .into(binding.imgProduct)
// }
//
// // Show/hide loading indicators
// binding.progressBar.visibility = if (state.isLoading) View.VISIBLE else View.GONE
// binding.btnSend.isEnabled = !state.isSending
//
// // Update attachment hint
// if (state.hasAttachment) {
// binding.editTextMessage.hint = getString(R.string.image_attached)
// } else {
// binding.editTextMessage.hint = getString(R.string.write_message)
// }
//
// // Show typing indicator
// binding.tvTypingIndicator.visibility =
// if (state.isOtherUserTyping) View.VISIBLE else View.GONE
//
// // Handle connection state
// handleConnectionState(state.connectionState)
//
// // Show error if any
// state.error?.let { error ->
// Toast.makeText(requireContext(), error, Toast.LENGTH_SHORT).show()
// viewModel.clearError()
// }
// }
// }
// }
//
// private fun handleConnectionState(state: ConnectionState) {
// when (state) {
// is ConnectionState.Connected -> {
// binding.tvConnectionStatus.visibility = View.GONE
// }
// is ConnectionState.Connecting -> {
// binding.tvConnectionStatus.visibility = View.VISIBLE
// binding.tvConnectionStatus.text = getString(R.string.connecting)
// }
// is ConnectionState.Disconnected -> {
// binding.tvConnectionStatus.visibility = View.VISIBLE
// binding.tvConnectionStatus.text = getString(R.string.disconnected_reconnecting)
// }
// is ConnectionState.Error -> {
// binding.tvConnectionStatus.visibility = View.VISIBLE
// binding.tvConnectionStatus.text = getString(R.string.connection_error, state.message)
// }
// }
// }
//
// private fun showOptionsMenu() {
// val options = arrayOf(
// getString(R.string.block_user),
// getString(R.string.report),
// getString(R.string.clear_chat),
// getString(R.string.cancel)
// )
//
// androidx.appcompat.app.AlertDialog.Builder(requireContext())
// .setTitle(getString(R.string.options))
// .setItems(options) { dialog, which ->
// when (which) {
// 0 -> Toast.makeText(requireContext(), R.string.block_user_selected, Toast.LENGTH_SHORT).show()
// 1 -> Toast.makeText(requireContext(), R.string.report_selected, Toast.LENGTH_SHORT).show()
// 2 -> Toast.makeText(requireContext(), R.string.clear_chat_selected, Toast.LENGTH_SHORT).show()
// }
// dialog.dismiss()
// }
// .show()
// }
//
// private fun checkPermissionsAndShowImagePicker() {
// if (ContextCompat.checkSelfPermission(
// requireContext(),
// Manifest.permission.READ_EXTERNAL_STORAGE
// ) != PackageManager.PERMISSION_GRANTED
// ) {
// ActivityCompat.requestPermissions(
// requireActivity(),
// arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.CAMERA),
// Constants.REQUEST_STORAGE_PERMISSION
// )
// } else {
// showImagePickerOptions()
// }
// }
//
// private fun showImagePickerOptions() {
// val options = arrayOf(
// getString(R.string.take_photo),
// getString(R.string.choose_from_gallery),
// getString(R.string.cancel)
// )
//
// androidx.appcompat.app.AlertDialog.Builder(requireContext())
// .setTitle(getString(R.string.select_attachment))
// .setItems(options) { dialog, which ->
// when (which) {
// 0 -> openCamera()
// 1 -> openGallery()
// }
// dialog.dismiss()
// }
// .show()
// }
//
// private fun openCamera() {
// val timeStamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(Date())
// val imageFileName = "IMG_${timeStamp}.jpg"
// val storageDir = requireContext().getExternalFilesDir(null)
// val imageFile = File(storageDir, imageFileName)
//
// tempImageUri = FileProvider.getUriForFile(
// requireContext(),
// "${requireContext().packageName}.fileprovider",
// imageFile
// )
//
// val intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE).apply {
// putExtra(MediaStore.EXTRA_OUTPUT, tempImageUri)
// }
//
// takePictureLauncher.launch(intent)
// }
//
// private fun openGallery() {
// val intent = Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI)
// pickImageLauncher.launch(intent)
// }
//
// private fun handleSelectedImage(uri: Uri) {
// // Get the file from Uri
// val filePathColumn = arrayOf(MediaStore.Images.Media.DATA)
// val cursor = requireContext().contentResolver.query(uri, filePathColumn, null, null, null)
// cursor?.moveToFirst()
// val columnIndex = cursor?.getColumnIndex(filePathColumn[0])
// val filePath = cursor?.getString(columnIndex ?: 0)
// cursor?.close()
//
// if (filePath != null) {
// viewModel.setSelectedImageFile(File(filePath))
// Toast.makeText(requireContext(), R.string.image_selected, Toast.LENGTH_SHORT).show()
// }
// }
//
// override fun onRequestPermissionsResult(
// requestCode: Int,
// permissions: Array<out String>,
// grantResults: IntArray
// ) {
// super.onRequestPermissionsResult(requestCode, permissions, grantResults)
// if (requestCode == Constants.REQUEST_STORAGE_PERMISSION) {
// if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
// showImagePickerOptions()
// } else {
// Toast.makeText(requireContext(), R.string.permission_denied, Toast.LENGTH_SHORT).show()
// }
// }
// }
//
// override fun onDestroyView() {
// super.onDestroyView()
// typingHandler.removeCallbacks(stopTypingRunnable)
// _binding = null
// }
//}

View File

@ -21,6 +21,7 @@ class ChatListFragment : Fragment() {
private val binding get() = _binding!!
private lateinit var socketService: SocketIOService
private lateinit var sessionManager: SessionManager
private val viewModel: com.alya.ecommerce_serang.ui.chat.ChatViewModel by viewModels {
BaseViewModelFactory {
val apiService = ApiConfig.getApiService(sessionManager)
@ -65,7 +66,8 @@ class ChatListFragment : Fragment() {
productImage = null,
productRating = null,
storeName = chatItem.storeName,
chatRoomId = chatItem.chatRoomId
chatRoomId = chatItem.chatRoomId,
storeImage = chatItem.storeImage
)
}
binding.chatListRecyclerView.adapter = adapter
@ -85,4 +87,8 @@ class ChatListFragment : Fragment() {
super.onDestroyView()
_binding = null
}
companion object{
}
}

View File

@ -8,6 +8,7 @@ import androidx.lifecycle.viewModelScope
import com.alya.ecommerce_serang.data.api.response.chat.ChatItem
import com.alya.ecommerce_serang.data.api.response.chat.ChatItemList
import com.alya.ecommerce_serang.data.api.response.chat.ChatLine
import com.alya.ecommerce_serang.data.api.response.customer.product.StoreProduct
import com.alya.ecommerce_serang.data.repository.ChatRepository
import com.alya.ecommerce_serang.data.repository.Result
import com.alya.ecommerce_serang.utils.Constants
@ -23,6 +24,8 @@ import javax.inject.Inject
class ChatViewModel @Inject constructor(
private val chatRepository: ChatRepository,
private val socketService: SocketIOService,
private val sessionManager: SessionManager
) : ViewModel() {
@ -38,6 +41,9 @@ class ChatViewModel @Inject constructor(
private val _chatList = MutableLiveData<Result<List<ChatItemList>>>()
val chatList: LiveData<Result<List<ChatItemList>>> = _chatList
private val _storeDetail = MutableLiveData<Result<StoreProduct?>>()
val storeDetail : LiveData<Result<StoreProduct?>> get() = _storeDetail
// Store and product parameters
private var storeId: Int = 0
private var productId: Int? = 0

View File

@ -21,6 +21,7 @@ import com.alya.ecommerce_serang.data.api.dto.ProductsItem
import com.alya.ecommerce_serang.data.api.retrofit.ApiConfig
import com.alya.ecommerce_serang.data.repository.ProductRepository
import com.alya.ecommerce_serang.databinding.FragmentHomeBinding
import com.alya.ecommerce_serang.ui.cart.CartActivity
import com.alya.ecommerce_serang.ui.notif.NotificationActivity
import com.alya.ecommerce_serang.ui.product.DetailProductActivity
import com.alya.ecommerce_serang.utils.BaseViewModelFactory
@ -129,6 +130,8 @@ class HomeFragment : Fragment() {
// Setup cart and notification buttons
binding.searchContainer.btnCart.setOnClickListener {
// Navigate to cart
val intent = Intent(requireContext(), CartActivity::class.java)
startActivity(intent)
}
binding.searchContainer.btnNotification.setOnClickListener {

View File

@ -1,67 +1,136 @@
package com.alya.ecommerce_serang.ui.notif
import android.content.Context
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
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.UserProfile
import com.alya.ecommerce_serang.data.api.response.auth.HasStoreResponse
import com.alya.ecommerce_serang.data.api.response.auth.NotifItem
import com.alya.ecommerce_serang.data.api.response.auth.NotifstoreItem
import com.alya.ecommerce_serang.data.repository.Result
import com.alya.ecommerce_serang.data.repository.UserRepository
import com.alya.ecommerce_serang.utils.SessionManager
import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import javax.inject.Inject
@HiltViewModel
class NotifViewModel @Inject constructor(
private val notificationBuilder: NotificationCompat.Builder,
private val notificationManager: NotificationManagerCompat,
@ApplicationContext private val context: Context,
private val userRepository: UserRepository,
private val webSocketManager: WebSocketManager,
private val sessionManager: SessionManager
) : ViewModel() {
private val _userProfile = MutableStateFlow<Result<UserProfile?>>(Result.Loading)
val userProfile: StateFlow<Result<UserProfile?>> = _userProfile.asStateFlow()
private val _notifList = MutableLiveData<Result<List<NotifItem>>>()
val notifList: LiveData<Result<List<NotifItem>>> = _notifList
init {
fetchUserProfile()
}
private val _checkStore = MutableLiveData<Boolean>()
val checkStore: LiveData<Boolean> = _checkStore
// Fetch user profile to get necessary data
fun fetchUserProfile() {
private val _notifStoreList = MutableLiveData<Result<List<NotifstoreItem>>>()
val notifStoreList: LiveData<Result<List<NotifstoreItem>>> = _notifStoreList
fun getNotifList() {
Log.d(TAG, "getNotifList: Fetching personal notifications")
viewModelScope.launch {
_userProfile.value = Result.Loading
val result = userRepository.fetchUserProfile()
_userProfile.value = result
try {
Log.d(TAG, "getNotifList: Setting state to Loading")
_notifList.value = Result.Loading
// If successful, save the user ID for WebSocket use
if (result is Result.Success && result.data != null) {
sessionManager.saveUserId(result.data.userId.toString())
Log.d(TAG, "getNotifList: Calling repository to get notifications")
val result = userRepository.getListNotif()
when (result) {
is Result.Success -> {
Log.d(TAG, "getNotifList: Success, received ${result.data?.size ?: 0} notifications")
if (result.data != null && result.data.isNotEmpty()) {
Log.d(TAG, "getNotifList: First notification - id: ${result.data[0].id}, title: ${result.data[0].title}")
if (result.data.size > 1) {
Log.d(TAG, "getNotifList: Last notification - id: ${result.data[result.data.size-1].id}, title: ${result.data[result.data.size-1].title}")
}
}
}
is Result.Error -> {
Log.e(TAG, "getNotifList: Error fetching notifications", result.exception)
}
is Result.Loading -> {
Log.d(TAG, "getNotifList: State is Loading")
}
}
_notifList.value = result
} catch (e: Exception) {
Log.e(TAG, "getNotifList: Unexpected error", e)
_notifList.value = Result.Error(e)
}
}
}
// Start WebSocket connection
fun startWebSocketConnection() {
webSocketManager.startWebSocketConnection()
fun getNotifStoreList() {
Log.d(TAG, "getNotifStoreList: Fetching store notifications")
viewModelScope.launch {
try {
Log.d(TAG, "getNotifStoreList: Setting state to Loading")
_notifStoreList.value = Result.Loading
Log.d(TAG, "getNotifStoreList: Calling repository to get store notifications")
val result = userRepository.getListNotifStore()
when (result) {
is Result.Success -> {
Log.d(TAG, "getNotifStoreList: Success, received ${result.data?.size ?: 0} store notifications")
if (result.data != null && result.data.isNotEmpty()) {
Log.d(TAG, "getNotifStoreList: First store notification - id: ${result.data[0].id}, title: ${result.data[0].title}")
if (result.data.size > 1) {
Log.d(TAG, "getNotifStoreList: Last store notification - id: ${result.data[result.data.size-1].id}, title: ${result.data[result.data.size-1].title}")
}
}
}
is Result.Error -> {
Log.e(TAG, "getNotifStoreList: Error fetching store notifications", result.exception)
}
is Result.Loading -> {
Log.d(TAG, "getNotifStoreList: State is Loading")
}
}
_notifStoreList.value = result
} catch (e: Exception) {
Log.e(TAG, "getNotifStoreList: Unexpected error", e)
_notifStoreList.value = Result.Error(e)
}
}
}
// Stop WebSocket connection
fun stopWebSocketConnection() {
webSocketManager.stopWebSocketConnection()
fun checkStoreUser() {
Log.d(TAG, "checkStoreUser: Checking if user has a store")
viewModelScope.launch {
try {
// Call the repository function to check store
Log.d(TAG, "checkStoreUser: Calling repository to check store")
val response: HasStoreResponse = userRepository.checkStore()
// Log and store success message
Log.d(TAG, "checkStoreUser: Response received, hasStore=${response.hasStore}")
_checkStore.value = response.hasStore // Store the value for UI feedback
Log.d(TAG, "checkStoreUser: Updated _checkStore value to ${response.hasStore}")
} catch (exception: Exception) {
// Handle any errors and update state
Log.e(TAG, "checkStoreUser: Error checking store", exception)
_checkStore.value = false
Log.d(TAG, "checkStoreUser: Set _checkStore to false due to error")
}
}
}
// Call when ViewModel is cleared (e.g., app closing)
override fun onCleared() {
super.onCleared()
// No need to stop here - the service will manage its own lifecycle
companion object {
private const val TAG = "NotifViewModel" // Constant for logging tag
}
}

View File

@ -1,118 +1,324 @@
package com.alya.ecommerce_serang.ui.notif
import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle
import android.widget.Toast
import android.util.Log
import android.view.View
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.recyclerview.widget.LinearLayoutManager
import com.alya.ecommerce_serang.data.repository.Result
import com.alya.ecommerce_serang.databinding.ActivityNotificationBinding
import com.google.android.material.tabs.TabLayout
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch
@AndroidEntryPoint // Required for Hilt
private const val TAG = "NotificationActivity"
@AndroidEntryPoint
class NotificationActivity : AppCompatActivity() {
private lateinit var binding: ActivityNotificationBinding
private val viewModel: NotifViewModel by viewModels()
// Permission request code
private val NOTIFICATION_PERMISSION_CODE = 100
private lateinit var personalAdapter: PersonalNotificationAdapter
private lateinit var storeAdapter: StoreNotificationAdapter
private var hasStore = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Log.d(TAG, "onCreate: Starting NotificationActivity")
binding = ActivityNotificationBinding.inflate(layoutInflater)
setContentView(binding.root)
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.userProfile.collect { result ->
when (result) {
is com.alya.ecommerce_serang.data.repository.Result.Success -> {
// User profile loaded successfully
// Potentially do something with user profile
setupToolbar()
setupAdapters()
setupTabLayout()
setupSwipeRefresh()
setupObservers()
// Load initial data
Log.d(TAG, "onCreate: Checking if user has a store")
viewModel.checkStoreUser()
Log.d(TAG, "onCreate: Loading personal notifications")
viewModel.getNotifList()
// Show personal notifications by default
binding.tabLayout.selectTab(binding.tabLayout.getTabAt(0))
}
private fun setupToolbar() {
binding.btnBack.setOnClickListener {
onBackPressed()
}
}
private fun setupAdapters() {
Log.d(TAG, "setupAdapters: Creating adapters")
// Create LayoutManager explicitly
val layoutManager = LinearLayoutManager(this)
Log.d(TAG, "setupAdapters: Created LinearLayoutManager")
// Personal notifications adapter
personalAdapter = PersonalNotificationAdapter { notifItem ->
// Handle personal notification click
Log.d(TAG, "Personal notification clicked: id=${notifItem.id}, type=${notifItem.type}")
}
Log.d(TAG, "setupAdapters: Created personalAdapter")
// Store notifications adapter
storeAdapter = StoreNotificationAdapter { storeNotifItem ->
// Handle store notification click
Log.d(TAG, "Store notification clicked: id=${storeNotifItem.id}, type=${storeNotifItem.type}")
}
Log.d(TAG, "setupAdapters: Created storeAdapter")
// Configure RecyclerView with explicit steps
binding.recyclerViewNotif.setHasFixedSize(true)
binding.recyclerViewNotif.layoutManager = layoutManager
binding.recyclerViewNotif.adapter = personalAdapter
Log.d(TAG, "setupAdapters: RecyclerView configured with personalAdapter")
Log.d(TAG, "setupAdapters: RecyclerView visibility: ${binding.recyclerViewNotif.visibility == View.VISIBLE}")
}
private fun setupTabLayout() {
binding.tabLayout.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener {
override fun onTabSelected(tab: TabLayout.Tab) {
Log.d(TAG, "Tab selected: position ${tab.position}")
when (tab.position) {
0 -> {
Log.d(TAG, "Showing personal notifications tab")
binding.recyclerViewNotif.adapter = personalAdapter
showPersonalNotifications()
}
1 -> {
Log.d(TAG, "Showing store notifications tab, hasStore=$hasStore")
binding.recyclerViewNotif.adapter = storeAdapter
if (hasStore) {
viewModel.getNotifStoreList()
}
is com.alya.ecommerce_serang.data.repository.Result.Error -> {
// Handle error - show message, etc.
Toast.makeText(this@NotificationActivity,
"Failed to load profile",
Toast.LENGTH_SHORT
).show()
}
Result.Loading -> {
// Show loading indicator if needed
showStoreNotifications()
}
}
}
override fun onTabUnselected(tab: TabLayout.Tab) {}
override fun onTabReselected(tab: TabLayout.Tab) {}
})
}
private fun setupSwipeRefresh() {
binding.swipeRefreshLayout.setOnRefreshListener {
Log.d(TAG, "Swipe refresh triggered, current tab: ${binding.tabLayout.selectedTabPosition}")
when (binding.tabLayout.selectedTabPosition) {
0 -> viewModel.getNotifList()
1 -> {
if (hasStore) {
viewModel.getNotifStoreList()
}
}
}
}
}
private fun setupObservers() {
// Observe checkStore to determine if user has a store
viewModel.checkStore.observe(this) { hasStoreValue ->
Log.d(TAG, "checkStore observed: $hasStoreValue")
// Update the local hasStore variable
hasStore = hasStoreValue
// If we're on the store tab, update UI based on hasStore value
if (binding.tabLayout.selectedTabPosition == 1) {
if (hasStore) {
Log.d(TAG, "User has store, loading store notifications")
viewModel.getNotifStoreList()
} else {
Log.d(TAG, "User doesn't have store, showing empty state")
showEmptyState("Anda belum memiliki toko", true)
}
}
}
// Observe personal notifications
viewModel.notifList.observe(this) { result ->
Log.d(TAG, "notifList observed: ${result.javaClass.simpleName}")
binding.swipeRefreshLayout.isRefreshing = false
if (binding.tabLayout.selectedTabPosition == 0) {
when (result) {
is Result.Success -> {
val notifications = result.data
Log.d(TAG, "Personal notifications received: ${notifications?.size ?: 0}")
if (notifications.isNullOrEmpty()) {
showEmptyState("Belum Ada Notifikasi", false)
} else {
hideEmptyState()
// Ensure adapter is attached
if (binding.recyclerViewNotif.adapter != personalAdapter) {
Log.d(TAG, "Re-attaching personalAdapter to RecyclerView")
binding.recyclerViewNotif.adapter = personalAdapter
}
personalAdapter.submitList(notifications)
// Force a layout pass
binding.recyclerViewNotif.post {
Log.d(TAG, "Forcing layout pass on RecyclerView")
binding.recyclerViewNotif.requestLayout()
}
}
}
is Result.Error -> {
Log.e(TAG, "Error loading personal notifications", result.exception)
showEmptyState("Gagal memuat notifikasi", false)
}
is Result.Loading -> {
Log.d(TAG, "Loading personal notifications")
}
}
}
}
// Start WebSocket connection
// viewModel.startWebSocketConnection()
// Observe store notifications
viewModel.notifStoreList.observe(this) { result ->
Log.d(TAG, "notifStoreList observed: ${result.javaClass.simpleName}")
binding.swipeRefreshLayout.isRefreshing = false
binding = ActivityNotificationBinding.inflate(layoutInflater)
setContentView(binding.root)
// Check and request notification permission for Android 13+
requestNotificationPermissionIfNeeded()
// Set up button click listeners
// setupButtonListeners()
}
// private fun setupButtonListeners() {
// binding.simpleNotification.setOnClickListener {
// viewModel.showSimpleNotification()
// }
//
// binding.updateNotification.setOnClickListener {
// viewModel.updateSimpleNotification()
// }
//
// binding.cancelNotification.setOnClickListener {
// viewModel.cancelSimpleNotification()
// }
// }
private fun requestNotificationPermissionIfNeeded() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
if (ContextCompat.checkSelfPermission(
this,
android.Manifest.permission.POST_NOTIFICATIONS
) != PackageManager.PERMISSION_GRANTED
) {
ActivityCompat.requestPermissions(
this,
arrayOf(android.Manifest.permission.POST_NOTIFICATIONS),
NOTIFICATION_PERMISSION_CODE
)
if (binding.tabLayout.selectedTabPosition == 1) {
when (result) {
is Result.Success -> {
val notifications = result.data
Log.d(TAG, "Store notifications received: ${notifications?.size ?: 0}, hasStore=$hasStore")
if (!hasStore) {
showEmptyState("Anda belum memiliki toko", true)
} else if (notifications.isNullOrEmpty()) {
showEmptyState("Belum Ada Notifikasi Toko", false)
} else {
hideEmptyState()
// Ensure adapter is attached
if (binding.recyclerViewNotif.adapter != storeAdapter) {
Log.d(TAG, "Re-attaching storeAdapter to RecyclerView")
binding.recyclerViewNotif.adapter = storeAdapter
}
storeAdapter.submitList(notifications)
// Force a layout pass
binding.recyclerViewNotif.post {
Log.d(TAG, "Forcing layout pass on RecyclerView")
binding.recyclerViewNotif.requestLayout()
}
}
}
is Result.Error -> {
Log.e(TAG, "Error loading store notifications", result.exception)
showEmptyState("Gagal memuat notifikasi toko", false)
}
is Result.Loading -> {
Log.d(TAG, "Loading store notifications")
}
}
}
}
}
// Handle permission request result
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
private fun showPersonalNotifications() {
Log.d(TAG, "showPersonalNotifications called")
val result = viewModel.notifList.value
if (requestCode == NOTIFICATION_PERMISSION_CODE) {
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
// Permission granted
Toast.makeText(this, "Notification permission granted", Toast.LENGTH_SHORT).show()
if (result is Result.Success) {
val notifications = result.data
Log.d(TAG, "showPersonalNotifications: Success with ${notifications?.size ?: 0} notifications")
if (notifications.isNullOrEmpty()) {
showEmptyState("Belum Ada Notifikasi", false)
} else {
// Permission denied
Toast.makeText(this, "Notification permission denied", Toast.LENGTH_SHORT).show()
// You might want to show a dialog explaining why notifications are important
hideEmptyState()
if (binding.recyclerViewNotif.adapter != personalAdapter) {
Log.d(TAG, "Re-attaching personalAdapter to RecyclerView")
binding.recyclerViewNotif.adapter = personalAdapter
}
personalAdapter.submitList(notifications)
// DEBUG: Debug the RecyclerView state
Log.d(TAG, "RecyclerView visibility: ${binding.recyclerViewNotif.visibility == View.VISIBLE}")
Log.d(TAG, "RecyclerView adapter item count: ${personalAdapter.itemCount}")
}
} else if (result is Result.Error) {
Log.e(TAG, "showPersonalNotifications: Error", result.exception)
showEmptyState("Gagal memuat notifikasi", false)
} else {
Log.d(TAG, "showPersonalNotifications: No data yet, triggering fetch")
// If we don't have data yet, trigger a fetch
viewModel.getNotifList()
}
}
private fun showStoreNotifications() {
Log.d(TAG, "showStoreNotifications called, hasStore=$hasStore")
if (!hasStore) {
showEmptyState("Anda belum memiliki toko", true)
return
}
val result = viewModel.notifStoreList.value
if (result is Result.Success) {
val notifications = result.data
Log.d(TAG, "showStoreNotifications: Success with ${notifications?.size ?: 0} notifications")
if (notifications.isNullOrEmpty()) {
showEmptyState("Belum Ada Notifikasi Toko", false)
} else {
hideEmptyState()
// Ensure adapter is attached
if (binding.recyclerViewNotif.adapter != storeAdapter) {
Log.d(TAG, "Re-attaching storeAdapter to RecyclerView")
binding.recyclerViewNotif.adapter = storeAdapter
}
storeAdapter.submitList(notifications)
// DEBUG: Debug the RecyclerView state
Log.d(TAG, "RecyclerView visibility: ${binding.recyclerViewNotif.visibility == View.VISIBLE}")
Log.d(TAG, "RecyclerView adapter item count: ${storeAdapter.itemCount}")
}
} else if (result is Result.Error) {
Log.e(TAG, "showStoreNotifications: Error", result.exception)
showEmptyState("Gagal memuat notifikasi toko", false)
} else {
Log.d(TAG, "showStoreNotifications: No data yet, triggering fetch")
// If we don't have data yet, trigger a fetch
viewModel.getNotifStoreList()
}
}
private fun showEmptyState(message: String, showCreateStoreButton: Boolean) {
Log.d(TAG, "showEmptyState: message='$message', showCreateStoreButton=$showCreateStoreButton")
binding.swipeRefreshLayout.visibility = View.GONE
binding.emptyStateLayout.visibility = View.VISIBLE
// Set empty state message
binding.tvEmptyTitle.text = message
// Show "Create Store" button and description if user doesn't have a store
if (showCreateStoreButton) {
binding.tvEmptyDesc.visibility = View.VISIBLE
binding.btnCreateStore.visibility = View.VISIBLE
// Set up create store button click listener
binding.btnCreateStore.setOnClickListener {
Log.d(TAG, "Create store button clicked")
// Navigate to create store screen
// Intent to CreateStoreActivity
}
} else {
binding.tvEmptyDesc.visibility = View.GONE
binding.btnCreateStore.visibility = View.GONE
}
}
private fun hideEmptyState() {
Log.d(TAG, "hideEmptyState called")
binding.swipeRefreshLayout.visibility = View.VISIBLE
binding.emptyStateLayout.visibility = View.GONE
// Ensure recycler view is visible
binding.recyclerViewNotif.visibility = View.VISIBLE
Log.d(TAG, "hideEmptyState: Set RecyclerView visibility to VISIBLE")
}
}

View File

@ -0,0 +1,93 @@
package com.alya.ecommerce_serang.ui.notif
import android.util.Log
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import com.alya.ecommerce_serang.data.api.response.auth.NotifItem
import com.alya.ecommerce_serang.databinding.ItemNotificationBinding
import java.text.SimpleDateFormat
import java.util.Locale
class PersonalNotificationAdapter(
private val onNotificationClick: (NotifItem) -> Unit
) : ListAdapter<NotifItem, PersonalNotificationAdapter.ViewHolder>(NotificationDiffCallback()) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
Log.d(TAG, "onCreateViewHolder: Creating ViewHolder")
val binding = ItemNotificationBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
return ViewHolder(binding, onNotificationClick)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = getItem(position)
Log.d(TAG, "onBindViewHolder: Binding notification at position $position, id=${item.id}")
holder.bind(item)
}
override fun submitList(list: List<NotifItem>?) {
Log.d(TAG, "submitList: Received list with ${list?.size ?: 0} items")
super.submitList(list)
}
class ViewHolder(
private val binding: ItemNotificationBinding,
private val onNotificationClick: (NotifItem) -> Unit
) : RecyclerView.ViewHolder(binding.root) {
fun bind(notification: NotifItem) {
binding.apply {
tvNotificationType.text = notification.type
tvTitle.text = notification.title
tvDescription.text = notification.message
// Format the date to show just the time
formatTimeDisplay(notification.createdAt)
// Handle notification click
root.setOnClickListener {
Log.d(TAG, "ViewHolder: Notification clicked, id=${notification.id}")
onNotificationClick(notification)
}
}
}
private fun formatTimeDisplay(createdAt: String) {
try {
// Parse the date with the expected format from API
val inputFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.getDefault())
val outputFormat = SimpleDateFormat("HH:mm", Locale.getDefault())
val date = inputFormat.parse(createdAt)
date?.let {
binding.tvTime.text = outputFormat.format(it)
}
} catch (e: Exception) {
// If date parsing fails, just display the raw value
Log.e(TAG, "formatTimeDisplay: Error parsing date", e)
binding.tvTime.text = createdAt
}
}
}
private class NotificationDiffCallback : DiffUtil.ItemCallback<NotifItem>() {
override fun areItemsTheSame(oldItem: NotifItem, newItem: NotifItem): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: NotifItem, newItem: NotifItem): Boolean {
return oldItem == newItem
}
}
companion object {
private const val TAG = "PersonalNotifAdapter"
}
}

View File

@ -0,0 +1,94 @@
package com.alya.ecommerce_serang.ui.notif
import android.util.Log
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import com.alya.ecommerce_serang.data.api.response.auth.NotifstoreItem
import com.alya.ecommerce_serang.databinding.ItemNotificationBinding
import java.text.SimpleDateFormat
import java.util.Locale
class StoreNotificationAdapter(
private val onNotificationClick: (NotifstoreItem) -> Unit
) : ListAdapter<NotifstoreItem, StoreNotificationAdapter.ViewHolder>(NotificationDiffCallback()) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
Log.d(TAG, "onCreateViewHolder: Creating ViewHolder")
val binding = ItemNotificationBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
return ViewHolder(binding, onNotificationClick)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = getItem(position)
Log.d(TAG, "onBindViewHolder: Binding store notification at position $position, id=${item.id}")
holder.bind(item)
}
override fun submitList(list: List<NotifstoreItem>?) {
Log.d(TAG, "submitList: Received list with ${list?.size ?: 0} items")
super.submitList(list)
}
class ViewHolder(
private val binding: ItemNotificationBinding,
private val onNotificationClick: (NotifstoreItem) -> Unit
) : RecyclerView.ViewHolder(binding.root) {
fun bind(notification: NotifstoreItem) {
binding.apply {
tvNotificationType.text = notification.type
tvTitle.text = notification.title
tvDescription.text = notification.message
// Format the date to show just the time
formatTimeDisplay(notification.createdAt)
// Handle notification click
root.setOnClickListener {
Log.d(TAG, "ViewHolder: Store notification clicked, id=${notification.id}")
onNotificationClick(notification)
}
}
}
private fun formatTimeDisplay(createdAt: String) {
try {
// Parse the date with the expected format from API
val inputFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.getDefault())
val outputFormat = SimpleDateFormat("HH:mm", Locale.getDefault())
val date = inputFormat.parse(createdAt)
date?.let {
binding.tvTime.text = outputFormat.format(it)
}
} catch (e: Exception) {
// If date parsing fails, just display the raw value
Log.e(TAG, "formatTimeDisplay: Error parsing date", e)
binding.tvTime.text = createdAt
}
}
}
private class NotificationDiffCallback : DiffUtil.ItemCallback<NotifstoreItem>() {
override fun areItemsTheSame(oldItem: NotifstoreItem, newItem: NotifstoreItem): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: NotifstoreItem, newItem: NotifstoreItem): Boolean {
return oldItem == newItem
}
}
companion object{
private const val TAG = "StoreNotifAdapter"
}
}

View File

@ -0,0 +1,81 @@
package com.alya.ecommerce_serang.ui.notif.fcm
import android.app.NotificationChannel
import android.app.NotificationManager
import android.content.Context
import android.os.Build
import android.util.Log
import androidx.core.app.NotificationCompat
import com.alya.ecommerce_serang.R
import com.google.firebase.messaging.FirebaseMessagingService
import com.google.firebase.messaging.RemoteMessage
class FCMService : FirebaseMessagingService() {
private val TAG = "FCMService"
override fun onNewToken(token: String) {
super.onNewToken(token)
Log.d(TAG, "Refreshed FCM token: $token")
// Store the token locally
storeTokenLocally(token)
// Send token to your server
sendTokenToServer(token)
}
override fun onMessageReceived(remoteMessage: RemoteMessage) {
super.onMessageReceived(remoteMessage)
Log.d(TAG, "From: ${remoteMessage.from}")
// Handle data payload
if (remoteMessage.data.isNotEmpty()) {
Log.d(TAG, "Message data payload: ${remoteMessage.data}")
// Process data payload if needed
}
// Handle notification payload
remoteMessage.notification?.let {
Log.d(TAG, "Message notification: ${it.title} / ${it.body}")
showNotification(it.title, it.body)
}
}
private fun storeTokenLocally(token: String) {
val sharedPreferences = getSharedPreferences("FCM_PREFS", Context.MODE_PRIVATE)
sharedPreferences.edit().putString("FCM_TOKEN", token).apply()
}
private fun sendTokenToServer(token: String) {
// TODO: Implement API call to your server to send the token
// This is a placeholder - you'll need to replace with actual API call to your server
Log.d(TAG, "Token would be sent to server: $token")
}
private fun showNotification(title: String?, body: String?) {
val channelId = "fcm_default_channel"
// Create notification channel for Android O and above
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel = NotificationChannel(
channelId,
"FCM Notifications",
NotificationManager.IMPORTANCE_DEFAULT
)
val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
notificationManager.createNotificationChannel(channel)
}
// Build notification
val notificationBuilder = NotificationCompat.Builder(this, channelId)
.setSmallIcon(R.drawable.outline_notifications_24) // Make sure this resource exists
.setContentTitle(title ?: "New Message")
.setContentText(body ?: "You have a new notification")
.setAutoCancel(true)
// Show notification
val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
val notificationId = System.currentTimeMillis().toInt()
notificationManager.notify(notificationId, notificationBuilder.build())
}
}

View File

@ -0,0 +1,29 @@
package com.alya.ecommerce_serang.ui.notif.fcm
import android.content.Context
import android.util.Log
import com.google.firebase.messaging.FirebaseMessaging
object FCMTokenManager {
private const val TAG = "FCMTokenManager"
fun getToken(callback: (String?) -> Unit) {
FirebaseMessaging.getInstance().token
.addOnCompleteListener { task ->
if (!task.isSuccessful) {
Log.e(TAG, "Failed to get FCM token", task.exception)
callback(null)
return@addOnCompleteListener
}
val token = task.result
Log.d(TAG, "FCM token retrieved: $token")
callback(token)
}
}
fun getStoredToken(context: Context): String? {
val sharedPreferences = context.getSharedPreferences("FCM_PREFS", Context.MODE_PRIVATE)
return sharedPreferences.getString("FCM_TOKEN", null)
}
}

View File

@ -7,15 +7,19 @@ import android.util.Log
import android.view.ViewGroup
import android.widget.TextView
import android.widget.Toast
import androidx.activity.enableEdgeToEdge
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat
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
import com.alya.ecommerce_serang.data.api.response.customer.product.PaymentInfoItem
import com.alya.ecommerce_serang.data.api.response.customer.product.PaymentItemDetail
import com.alya.ecommerce_serang.data.api.retrofit.ApiConfig
import com.alya.ecommerce_serang.data.repository.OrderRepository
import com.alya.ecommerce_serang.databinding.ActivityCheckoutBinding
@ -30,6 +34,7 @@ class CheckoutActivity : AppCompatActivity() {
private lateinit var binding: ActivityCheckoutBinding
private lateinit var sessionManager: SessionManager
private var paymentAdapter: PaymentMethodAdapter? = null
private var paymentMethodsLoaded = false
private val viewModel: CheckoutViewModel by viewModels {
BaseViewModelFactory {
@ -46,6 +51,21 @@ class CheckoutActivity : AppCompatActivity() {
sessionManager = SessionManager(this)
WindowCompat.setDecorFitsSystemWindows(window, false)
enableEdgeToEdge()
// Apply insets to your root layout
ViewCompat.setOnApplyWindowInsetsListener(binding.root) { view, windowInsets ->
val systemBars = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
view.setPadding(
systemBars.left,
systemBars.top,
systemBars.right,
systemBars.bottom
)
windowInsets
}
// Setup UI components
setupToolbar()
@ -57,6 +77,7 @@ class CheckoutActivity : AppCompatActivity() {
private fun processIntentData() {
// Determine if this is Buy Now or Cart checkout
val isBuyNow = intent.hasExtra(EXTRA_PRODUCT_ID) && !intent.hasExtra(EXTRA_CART_ITEM_IDS)
val isWholesaleNow = intent.getBooleanExtra(EXTRA_ISWHOLESALE, false)
if (isBuyNow) {
// Process Buy Now flow
@ -67,23 +88,33 @@ class CheckoutActivity : AppCompatActivity() {
productName = intent.getStringExtra(EXTRA_PRODUCT_NAME),
productImage = intent.getStringExtra(EXTRA_PRODUCT_IMAGE),
quantity = intent.getIntExtra(EXTRA_QUANTITY, 1),
price = intent.getDoubleExtra(EXTRA_PRICE, 0.0)
price = intent.getDoubleExtra(EXTRA_PRICE, 0.0),
isWholesale = isWholesaleNow
)
} else {
// Process Cart checkout flow
val cartItemIds = intent.getIntArrayExtra(EXTRA_CART_ITEM_IDS)?.toList() ?: emptyList()
val isWholesaleArray = intent.getBooleanArrayExtra(EXTRA_CART_ITEM_WHOLESALE)
if (cartItemIds.isNotEmpty()) {
viewModel.initializeFromCart(cartItemIds)
// Create a map of cart item IDs to wholesale status if available
val wholesaleMap = if (isWholesaleArray != null && isWholesaleArray.size == cartItemIds.size) {
cartItemIds.mapIndexed { index, id -> id to isWholesaleArray[index] }.toMap()
} else {
emptyMap()
}
viewModel.initializeFromCart(cartItemIds, wholesaleMap)
} else {
Toast.makeText(this, "Error: No cart items specified", Toast.LENGTH_SHORT).show()
finish()
}
}
viewModel.getPaymentMethods { paymentMethods ->
// Logging is just for debugging
Log.d("CheckoutActivity", "Loaded ${paymentMethods.size} payment methods")
}
// viewModel.getPaymentMethods { paymentMethods ->
// // Logging is just for debugging
// Log.d("CheckoutActivity", "Loaded ${paymentMethods.size} payment methods")
// }
}
private fun setupToolbar() {
@ -97,6 +128,10 @@ class CheckoutActivity : AppCompatActivity() {
viewModel.checkoutData.observe(this) { data ->
setupProductRecyclerView(data)
updateOrderSummary()
if (data != null) {
viewModel.getPaymentMethods()
}
}
// Observe address details
@ -106,23 +141,27 @@ class CheckoutActivity : AppCompatActivity() {
}
viewModel.availablePaymentMethods.observe(this) { paymentMethods ->
if (paymentMethods.isNotEmpty()) {
if (paymentMethods.isNotEmpty() && !paymentMethodsLoaded) {
Log.d("CheckoutActivity", "Setting up payment methods: ${paymentMethods.size} methods available")
setupPaymentMethodsRecyclerView(paymentMethods)
paymentMethodsLoaded = true
}
}
// Observe selected payment
viewModel.selectedPayment.observe(this) { selectedPayment ->
if (selectedPayment != null) {
// Update the adapter to show the selected payment
paymentAdapter?.setSelectedPaymentName(selectedPayment.name)
Log.d("CheckoutActivity", "Observer notified of selected payment: ${selectedPayment.bankName}")
// Optional: Update other UI elements to show the selected payment
// For example: binding.tvSelectedPaymentMethod.text = selectedPayment.name
// Update the adapter ONLY if it exists
paymentAdapter?.let { adapter ->
// This line was causing issues - using setSelectedPayment instead of setSelectedPaymentName
adapter.setSelectedPaymentId(selectedPayment.id)
Log.d("CheckoutActivity", "Updated adapter with selected payment: ${selectedPayment.id}")
}
}
}
// Observe loading state
viewModel.isLoading.observe(this) { isLoading ->
binding.btnPay.isEnabled = !isLoading
@ -146,7 +185,7 @@ class CheckoutActivity : AppCompatActivity() {
}
}
private fun setupPaymentMethodsRecyclerView(paymentMethods: List<PaymentInfoItem>) {
private fun setupPaymentMethodsRecyclerView(paymentMethods: List<PaymentItemDetail>) {
if (paymentMethods.isEmpty()) {
Log.e("CheckoutActivity", "Payment methods list is empty")
Toast.makeText(this, "No payment methods available", Toast.LENGTH_SHORT).show()
@ -156,18 +195,22 @@ class CheckoutActivity : AppCompatActivity() {
// 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)
}
if (paymentAdapter == null) {
paymentAdapter = PaymentMethodAdapter(paymentMethods) { payment ->
Log.d("CheckoutActivity", "Payment selected in adapter: ${payment.bankName}")
binding.rvPaymentInfo.apply {
layoutManager = LinearLayoutManager(this@CheckoutActivity)
adapter = paymentAdapter
// Set this payment as selected in the ViewModel
viewModel.setPaymentMethod(payment.id)
}
binding.rvPaymentInfo.apply {
layoutManager = LinearLayoutManager(this@CheckoutActivity)
adapter = paymentAdapter
}
}
}
private fun updatePaymentMethodsAdapter(paymentMethods: List<PaymentInfoItem>, selectedId: Int?) {
private fun updatePaymentMethodsAdapter(paymentMethods: List<PaymentItemDetail>, selectedId: Int?) {
Log.d("CheckoutActivity", "Updating payment adapter with ${paymentMethods.size} methods")
// Simple test adapter
@ -183,7 +226,7 @@ class CheckoutActivity : AppCompatActivity() {
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
val payment = paymentMethods[position]
(holder.itemView as TextView).text = "Payment: ${payment.name}"
(holder.itemView as TextView).text = "Payment: ${payment.bankName}"
}
}
@ -347,7 +390,13 @@ class CheckoutActivity : AppCompatActivity() {
}
// Check if payment method is selected
if (viewModel.selectedPayment.value == null) {
val paymentMethodId = if (checkoutData.isBuyNow) {
(checkoutData.orderRequest as OrderRequestBuy).paymentMethodId
} else {
(checkoutData.orderRequest as OrderRequest).paymentMethodId
}
if (paymentMethodId <= 0) {
Toast.makeText(this, "Silakan pilih metode pembayaran", Toast.LENGTH_SHORT).show()
return false
}
@ -365,9 +414,12 @@ class CheckoutActivity : AppCompatActivity() {
const val EXTRA_PRODUCT_IMAGE = "PRODUCT_IMAGE"
const val EXTRA_QUANTITY = "QUANTITY"
const val EXTRA_PRICE = "PRICE"
const val EXTRA_ISWHOLESALE = "ISWHOLESALE"
const val EXTRA_CART_ITEM_WHOLESALE = "EXTRA_CART_ITEM_WHOLESALE"
// Helper methods for starting activity
// TO DO: delete iswholesale klo ngga dibuthin
// For Buy Now
fun startForBuyNow(
context: Context,
@ -377,7 +429,8 @@ class CheckoutActivity : AppCompatActivity() {
productName: String?,
productImage: String?,
quantity: Int,
price: Double
price: Double,
isWholesale: Boolean
) {
val intent = Intent(context, CheckoutActivity::class.java).apply {
putExtra(EXTRA_STORE_ID, storeId)
@ -387,6 +440,7 @@ class CheckoutActivity : AppCompatActivity() {
putExtra(EXTRA_PRODUCT_IMAGE, productImage)
putExtra(EXTRA_QUANTITY, quantity)
putExtra(EXTRA_PRICE, price)
putExtra(EXTRA_ISWHOLESALE, isWholesale)
}
context.startActivity(intent)
}
@ -394,10 +448,14 @@ class CheckoutActivity : AppCompatActivity() {
// For Cart checkout
fun startForCart(
context: Context,
cartItemIds: List<Int>
cartItemIds: List<Int>,
isWholesaleArray: BooleanArray? = null
) {
val intent = Intent(context, CheckoutActivity::class.java).apply {
putExtra(EXTRA_CART_ITEM_IDS, cartItemIds.toIntArray())
if (isWholesaleArray != null) {
putExtra(EXTRA_CART_ITEM_WHOLESALE, isWholesaleArray)
}
}
context.startActivity(intent)
}

View File

@ -9,8 +9,8 @@ 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
import com.alya.ecommerce_serang.data.api.response.customer.cart.CartItemsItem
import com.alya.ecommerce_serang.data.api.response.customer.cart.DataItem
import com.alya.ecommerce_serang.data.api.response.customer.product.PaymentInfoItem
import com.alya.ecommerce_serang.data.api.response.customer.cart.DataItemCart
import com.alya.ecommerce_serang.data.api.response.customer.product.PaymentItemDetail
import com.alya.ecommerce_serang.data.api.response.customer.profile.AddressesItem
import com.alya.ecommerce_serang.data.repository.OrderRepository
import com.alya.ecommerce_serang.data.repository.Result
@ -24,12 +24,12 @@ class CheckoutViewModel(private val repository: OrderRepository) : ViewModel() {
private val _addressDetails = MutableLiveData<AddressesItem?>()
val addressDetails: LiveData<AddressesItem?> = _addressDetails
private val _availablePaymentMethods = MutableLiveData<List<PaymentInfoItem>>()
val availablePaymentMethods: LiveData<List<PaymentInfoItem>> = _availablePaymentMethods
private val _availablePaymentMethods = MutableLiveData<List<PaymentItemDetail>>()
val availablePaymentMethods: LiveData<List<PaymentItemDetail>> = _availablePaymentMethods
// Selected payment method
private val _selectedPayment = MutableLiveData<PaymentInfoItem?>()
val selectedPayment: LiveData<PaymentInfoItem?> = _selectedPayment
private val _selectedPayment = MutableLiveData<PaymentItemDetail?>()
val selectedPayment: LiveData<PaymentItemDetail?> = _selectedPayment
private val _isLoading = MutableLiveData<Boolean>()
val isLoading: LiveData<Boolean> = _isLoading
@ -40,6 +40,8 @@ class CheckoutViewModel(private val repository: OrderRepository) : ViewModel() {
private val _orderCreated = MutableLiveData<Boolean>()
val orderCreated: LiveData<Boolean> = _orderCreated
// Initialize "Buy Now" checkout
fun initializeBuyNow(
storeId: Int,
@ -48,7 +50,8 @@ class CheckoutViewModel(private val repository: OrderRepository) : ViewModel() {
productName: String?,
productImage: String?,
quantity: Int,
price: Double
price: Double,
isWholesale: Boolean
) {
viewModelScope.launch {
_isLoading.value = true
@ -64,7 +67,8 @@ class CheckoutViewModel(private val repository: OrderRepository) : ViewModel() {
isNego = false, // Default value
productId = productId,
quantity = quantity,
shipEtd = ""
shipEtd = "",
isReseller = isWholesale
)
// Create checkout data
@ -89,7 +93,7 @@ class CheckoutViewModel(private val repository: OrderRepository) : ViewModel() {
}
// Initialize checkout from cart
fun initializeFromCart(cartItemIds: List<Int>) {
fun initializeFromCart(cartItemIds: List<Int>, isWholesaleMap: Map<Int, Boolean> = emptyMap()) {
viewModelScope.launch {
_isLoading.value = true
@ -100,7 +104,7 @@ class CheckoutViewModel(private val repository: OrderRepository) : ViewModel() {
if (cartResult is Result.Success) {
// Find matching cart items
val matchingItems = mutableListOf<CartItemsItem>()
var storeData: DataItem? = null
var storeData: DataItemCart? = null
for (store in cartResult.data) {
val storeItems = store.cartItems.filter { it.cartItemId in cartItemIds }
@ -114,14 +118,16 @@ class CheckoutViewModel(private val repository: OrderRepository) : ViewModel() {
if (matchingItems.isNotEmpty() && storeData != null) {
// Create initial OrderRequest object
val orderRequest = OrderRequest(
addressId = 0, // Will be set when user selects address
paymentMethodId = 0, // Will be set when user selects payment
shipPrice = 0, // Will be set when user selects shipping
addressId = 0,
paymentMethodId = 0,
shipPrice = 0,
shipName = "",
shipService = "",
isNego = false,
cartItemId = cartItemIds,
shipEtd = ""
shipEtd = "",
// Add a list tracking which items are wholesale
isReseller = isWholesaleMap.any { it.value } // Set true if any item is wholesale
)
// Create checkout data
@ -131,8 +137,12 @@ class CheckoutViewModel(private val repository: OrderRepository) : ViewModel() {
sellerName = storeData.storeName,
sellerId = storeData.storeId,
isBuyNow = false,
cartItems = matchingItems
cartItems = matchingItems,
cartItemWholesaleMap = isWholesaleMap // Store the wholesale map
)
calculateSubtotal()
calculateTotal()
} else {
_errorMessage.value = "No matching cart items found"
}
@ -140,7 +150,6 @@ class CheckoutViewModel(private val repository: OrderRepository) : ViewModel() {
_errorMessage.value = "Failed to fetch cart items: ${cartResult.exception.message}"
}
} catch (e: Exception) {
Log.e(TAG, "Error initializing cart checkout", e)
_errorMessage.value = "Error: ${e.message}"
} finally {
_isLoading.value = false
@ -148,47 +157,51 @@ class CheckoutViewModel(private val repository: OrderRepository) : ViewModel() {
}
}
fun getPaymentMethods(callback: (List<PaymentInfoItem>) -> Unit) {
fun getPaymentMethods() {
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) {
// 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 {
val storeId = _checkoutData.value?.sellerId ?: run {
Log.e(TAG, "StoreId is null - cannot fetch payment methods")
_availablePaymentMethods.value = emptyList()
callback(emptyList())
return@launch
}
Log.d(TAG, "Attempting to fetch payment methods for storeId: $storeId")
if (storeId <= 0) {
Log.e(TAG, "Invalid storeId: $storeId - cannot fetch payment methods")
_availablePaymentMethods.value = emptyList()
return@launch
}
val result = repository.fetchPaymentStore(storeId)
when (result) {
is Result.Success -> {
val paymentMethods = result.data?.filterNotNull() ?: emptyList()
Log.d(TAG, "Fetched ${paymentMethods.size} payment methods")
// Update payment methods
_availablePaymentMethods.value = paymentMethods
}
is Result.Error -> {
Log.e(TAG, "Error fetching payment methods: ${result.exception.message}")
_availablePaymentMethods.value = emptyList()
}
is Result.Loading -> {
null
}
}
} catch (e: Exception) {
Log.e(TAG, "Error fetching payment methods", e)
Log.e(TAG, "Exception in getPaymentMethods", 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
@ -196,30 +209,34 @@ class CheckoutViewModel(private val repository: OrderRepository) : ViewModel() {
if (paymentMethods.isNullOrEmpty()) {
// If no payment methods available, try to fetch them
getPaymentMethods { /* do nothing here */ }
getPaymentMethods()
return@launch
}
// Use the first payment method (or specific one if you prefer)
val selectedPayment = paymentMethods.first()
val selectedPayment = paymentMethods.find { it.id == paymentId }
// Set the selected payment
if (selectedPayment == null) {
Log.e(TAG, "Payment with ID $paymentId not found")
return@launch
}
// Set the selected payment - IMPORTANT: do this first
_selectedPayment.value = selectedPayment
Log.d(TAG, "Payment selected: Name=${selectedPayment.name}")
Log.d(TAG, "Payment selected: ID=${selectedPayment.id}, Name=${selectedPayment.bankName}")
// Update the order request with the payment method ID (hardcoded for now)
// Update the order request with the payment method ID
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)
val updatedRequest = buyRequest.copy(paymentMethodId = paymentId)
_checkoutData.value = currentData.copy(orderRequest = updatedRequest)
} else {
// For Cart checkout
val cartRequest = currentData.orderRequest as OrderRequest
val updatedRequest = cartRequest.copy(paymentMethodId = currentPaymentId)
val updatedRequest = cartRequest.copy(paymentMethodId = paymentId)
_checkoutData.value = currentData.copy(orderRequest = updatedRequest)
}
} catch (e: Exception) {
@ -299,6 +316,39 @@ class CheckoutViewModel(private val repository: OrderRepository) : ViewModel() {
try {
val data = _checkoutData.value ?: throw Exception("No checkout data available")
if (data.orderRequest is OrderRequest) {
val request = data.orderRequest
// Check for required fields
if (request.addressId <= 0) {
_errorMessage.value = "Please select a delivery address"
_isLoading.value = false
return@launch
}
if (request.paymentMethodId <= 0) {
_errorMessage.value = "Please select a payment method"
_isLoading.value = false
return@launch
}
if (request.shipPrice <= 0 || request.shipName.isBlank() || request.shipService.isBlank()) {
_errorMessage.value = "Please select a shipping method"
_isLoading.value = false
return@launch
}
} else if (data.orderRequest is OrderRequestBuy) {
val request = data.orderRequest
// Similar validation for buy now
if (request.addressId <= 0 || request.paymentMethodId <= 0 ||
request.shipPrice <= 0 || request.shipName.isBlank() || request.shipService.isBlank()) {
_errorMessage.value = "Please complete all required checkout information"
_isLoading.value = false
return@launch
}
}
val response = if (data.isBuyNow) {
// For Buy Now, use the dedicated endpoint
val buyRequest = data.orderRequest as OrderRequestBuy
@ -355,6 +405,8 @@ class CheckoutViewModel(private val repository: OrderRepository) : ViewModel() {
}
}
companion object {
private const val TAG = "CheckoutViewModel"
}

View File

@ -1,21 +1,23 @@
package com.alya.ecommerce_serang.ui.order
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.alya.ecommerce_serang.R
import com.alya.ecommerce_serang.data.api.response.customer.product.PaymentInfoItem
import com.alya.ecommerce_serang.data.api.response.customer.product.PaymentItemDetail
import com.alya.ecommerce_serang.databinding.ItemPaymentMethodBinding
import com.bumptech.glide.Glide
import com.bumptech.glide.request.RequestOptions
class PaymentMethodAdapter(
private val paymentMethods: List<PaymentInfoItem>,
private val onPaymentSelected: (PaymentInfoItem) -> Unit
private val paymentMethods: List<PaymentItemDetail>,
private val onPaymentSelected: (PaymentItemDetail) -> Unit
) : RecyclerView.Adapter<PaymentMethodAdapter.PaymentMethodViewHolder>() {
// Selected payment name
private var selectedPaymentName: String? = null
// Track the selected payment by ID
private var selectedPaymentId: Int? = null
class PaymentMethodViewHolder(val binding: ItemPaymentMethodBinding) :
RecyclerView.ViewHolder(binding.root)
@ -36,18 +38,13 @@ class PaymentMethodAdapter(
with(holder.binding) {
// Set payment method name
tvPaymentMethodName.text = payment.name
tvPaymentMethodName.text = payment.bankName
// // 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 ID
rbPaymentMethod.isChecked = payment.id == selectedPaymentId
// Set radio button state based on selected payment name
rbPaymentMethod.isChecked = payment.name == selectedPaymentName
// Debug log
Log.d("PaymentAdapter", "Binding item ${payment.bankName}, checked=${rbPaymentMethod.isChecked}")
// Load payment icon if available
if (!payment.qrisImage.isNullOrEmpty()) {
@ -55,31 +52,73 @@ class PaymentMethodAdapter(
.load(payment.qrisImage)
.apply(
RequestOptions()
.placeholder(R.drawable.outline_store_24)
.error(R.drawable.outline_store_24))
.placeholder(R.drawable.outline_store_24)
.error(R.drawable.outline_store_24)
)
.into(ivPaymentMethod)
} else {
// Default icon for bank transfers
ivPaymentMethod.setImageResource(R.drawable.outline_store_24)
}
// Handle click on the entire item
root.setOnClickListener {
onPaymentSelected(payment)
setSelectedPaymentName(payment.name)
// IMPORTANT: We need to fix the click handling to prevent re-fetching
val clickListener = View.OnClickListener {
val previousSelectedId = selectedPaymentId
selectedPaymentId = payment.id
// Force the radio button to be checked
rbPaymentMethod.isChecked = true
// Only notify if there was a change in selection
if (previousSelectedId != payment.id) {
notifyItemChanged(position)
// Notify previous selection if it exists
if (previousSelectedId != null) {
val prevPosition = paymentMethods.indexOfFirst { it.id == previousSelectedId }
if (prevPosition >= 0) {
notifyItemChanged(prevPosition)
}
}
// Call the callback ONLY ONCE
onPaymentSelected(payment)
Log.d("PaymentAdapter", "Payment selected: ${payment.bankName}")
}
}
// Handle click on the radio button
rbPaymentMethod.setOnClickListener {
onPaymentSelected(payment)
setSelectedPaymentName(payment.name)
// Apply the same click listener to both the root and the radio button
root.setOnClickListener(clickListener)
rbPaymentMethod.setOnClickListener(clickListener)
}
}
// Set selected payment
fun setSelectedPaymentId(paymentId: Int) {
if (selectedPaymentId != paymentId) {
val previousSelectedId = selectedPaymentId
selectedPaymentId = paymentId
Log.d("PaymentAdapter", "Setting selected payment ID to: $paymentId")
// Update affected items only
if (previousSelectedId != null) {
val prevPosition = paymentMethods.indexOfFirst { it.id == previousSelectedId }
if (prevPosition >= 0) {
notifyItemChanged(prevPosition)
}
}
val newPosition = paymentMethods.indexOfFirst { it.id == paymentId }
if (newPosition >= 0) {
notifyItemChanged(newPosition)
}
}
}
// Set selected payment by name and refresh the UI
fun setSelectedPaymentName(paymentName: String) {
selectedPaymentName = paymentName
notifyDataSetChanged() // Update all items to reflect selection change
// Set selected payment object
fun setSelectedPayment(payment: PaymentItemDetail) {
setSelectedPaymentId(payment.id)
}
}

View File

@ -1,5 +1,6 @@
package com.alya.ecommerce_serang.ui.order
import android.util.Log
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
@ -9,6 +10,7 @@ import com.alya.ecommerce_serang.data.api.dto.CourierCostRequest
import com.alya.ecommerce_serang.data.api.response.customer.order.CourierCostsItem
import com.alya.ecommerce_serang.data.repository.OrderRepository
import com.alya.ecommerce_serang.data.repository.Result
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
class ShippingViewModel(
@ -30,12 +32,71 @@ class ShippingViewModel(
/**
* Load shipping options based on address, product, and quantity
*/
// fun loadShippingOptions(addressId: Int, productId: Int, quantity: Int) {
// _isLoading.value = true
// _errorMessage.value = ""
//
// val costProduct = CostProduct(
// productId = productId,
// quantity = quantity
// )
//
// viewModelScope.launch {
// // Define the courier services to try
// val courierServices = listOf("pos", "jne", "tiki")
//
// // Create a mutable list to collect successful courier options
// val availableCourierOptions = mutableListOf<CourierCostsItem>()
//
// // Try each courier service
// for (courier in courierServices) {
// try {
// // Create a request for this specific courier
// val courierRequest = CourierCostRequest(
// addressId = addressId,
// itemCost = listOf(costProduct),
// courier = courier // Add the courier to the request
// )
//
// // Make a separate API call for each courier
// val result = repository.getCountCourierCost(courierRequest)
//
// when (result) {
// is Result.Success -> {
// // Add this courier's options to our collection
// result.data.courierCosts?.let { costs ->
// availableCourierOptions.addAll(costs)
// }
// // Update UI with what we have so far
// _shippingOptions.value = availableCourierOptions
// }
// is Result.Error -> {
// // Log the error but continue with next courier
// Log.e("ShippingViewModel", "Error fetching cost for courier $courier: ${result.exception.message}")
// }
// is Result.Loading -> {
// // Handle loading state
// }
// }
// } catch (e: Exception) {
// // Log the exception but continue with next courier
// Log.e("ShippingViewModel", "Exception for courier $courier: ${e.message}")
// }
// }
//
// // Show error only if we couldn't get any shipping options
// if (availableCourierOptions.isEmpty()) {
// _errorMessage.value = "No shipping options available. Please try again later."
// }
//
// _isLoading.value = false
// }
// }
fun loadShippingOptions(addressId: Int, productId: Int, quantity: Int) {
// Reset previous state
_isLoading.value = true
_errorMessage.value = ""
// Prepare the request
val costProduct = CostProduct(
productId = productId,
quantity = quantity
@ -43,34 +104,47 @@ class ShippingViewModel(
val request = CourierCostRequest(
addressId = addressId,
itemCost = listOf(costProduct) // Wrap in a list
itemCost = listOf(costProduct)
)
viewModelScope.launch {
try {
// Fetch courier costs
val result = repository.getCountCourierCost(request)
var success = false
var attempt = 0
val maxAttempts = 3
when (result) {
is Result.Success -> {
// Update shipping options directly with courier costs
_shippingOptions.value = result.data.courierCosts
}
is Result.Error -> {
// Handle error case
_errorMessage.value = result.exception.message ?: "Unknown error occurred"
}
is Result.Loading -> {
// Typically handled by the loading state
while (!success && attempt < maxAttempts) {
attempt++
try {
val result = repository.getCountCourierCost(request)
when (result) {
is Result.Success -> {
_shippingOptions.value = result.data.courierCosts
success = true
}
is com.alya.ecommerce_serang.data.repository.Result.Error -> {
Log.e("ShippingViewModel", "Attempt $attempt failed: ${result.exception.message}")
// Wait before retrying
delay(120000)
}
is com.alya.ecommerce_serang.data.repository.Result.Loading -> {
// Handle loading state
}
}
} catch (e: Exception) {
Log.e("ShippingViewModel", "Attempt $attempt exception: ${e.message}")
// Wait before retrying
delay(1000)
}
} catch (e: Exception) {
// Catch any unexpected exceptions
_errorMessage.value = e.localizedMessage ?: "An unexpected error occurred"
} finally {
// Always set loading to false
_isLoading.value = false
}
// After all attempts, check if we have any shipping options
if (!success || _shippingOptions.value.isNullOrEmpty()) {
_errorMessage.value = "No shipping options available. Please try again later."
}
_isLoading.value = false
}
}
}

View File

@ -398,7 +398,7 @@ class AddAddressActivity : AppCompatActivity() {
isRequestingLocation = false
Toast.makeText(this, "Timeout lokasi, menggunakan lokasi default", Toast.LENGTH_SHORT).show()
}
}, 15000) // 15 seconds timeout
}, 60000) // 15 seconds timeout
// Try getting last known location first
try {

View File

@ -5,8 +5,12 @@ import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.alya.ecommerce_serang.data.api.dto.CancelOrderReq
import com.alya.ecommerce_serang.data.api.dto.CompletedOrderRequest
import com.alya.ecommerce_serang.data.api.dto.OrdersItem
import com.alya.ecommerce_serang.data.api.response.customer.order.CancelOrderResponse
import com.alya.ecommerce_serang.data.api.response.customer.order.OrderListItemsItem
import com.alya.ecommerce_serang.data.api.response.customer.order.Orders
import com.alya.ecommerce_serang.data.api.response.order.CompletedOrderResponse
import com.alya.ecommerce_serang.data.repository.OrderRepository
import com.alya.ecommerce_serang.data.repository.Result
@ -26,6 +30,18 @@ class HistoryViewModel(private val repository: OrderRepository) : ViewModel() {
private val _orderCompletionStatus = MutableLiveData<Result<CompletedOrderResponse>>()
val orderCompletionStatus: LiveData<Result<CompletedOrderResponse>> = _orderCompletionStatus
private val _orderDetails = MutableLiveData<Orders>()
val orderDetails: LiveData<Orders> get() = _orderDetails
private val _cancelOrderStatus = MutableLiveData<Result<CancelOrderResponse>>()
val cancelOrderStatus: LiveData<Result<CancelOrderResponse>> = _cancelOrderStatus
private val _isCancellingOrder = MutableLiveData<Boolean>()
val isCancellingOrder: LiveData<Boolean> = _isCancellingOrder
// LiveData untuk OrderItems
private val _orderItems = MutableLiveData<List<OrderListItemsItem>>()
val orderItems: LiveData<List<OrderListItemsItem>> get() = _orderItems
private val _isLoading = MutableLiveData<Boolean>()
val isLoading: LiveData<Boolean> = _isLoading
@ -35,6 +51,9 @@ class HistoryViewModel(private val repository: OrderRepository) : ViewModel() {
private val _isSuccess = MutableLiveData<Boolean>()
val isSuccess: LiveData<Boolean> = _isSuccess
private val _error = MutableLiveData<String>()
val error: LiveData<String> get() = _error
fun getOrderList(status: String) {
_orders.value = ViewState.Loading
viewModelScope.launch {
@ -99,4 +118,46 @@ class HistoryViewModel(private val repository: OrderRepository) : ViewModel() {
}
}
}
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 cancelOrder(cancelReq: CancelOrderReq) {
viewModelScope.launch {
try {
_cancelOrderStatus.value = Result.Loading
val result = repository.cancelOrder(cancelReq)
_cancelOrderStatus.value = result
} catch (e: Exception) {
Log.e("HistoryViewModel", "Error cancelling order: ${e.message}")
_cancelOrderStatus.value = Result.Error(e)
}
}
}
fun refreshOrders(status: String = "all") {
Log.d(TAG, "Refreshing orders with status: $status")
// Clear current orders before fetching new ones
_orders.value = ViewState.Loading
// Re-fetch the orders with the current status
getOrderList(status)
}
}

View File

@ -2,6 +2,7 @@ package com.alya.ecommerce_serang.ui.order.history
import android.app.Activity
import android.app.Dialog
import android.content.ContextWrapper
import android.content.Intent
import android.graphics.Color
import android.net.Uri
@ -17,14 +18,20 @@ import android.widget.ImageView
import android.widget.ProgressBar
import android.widget.TextView
import android.widget.Toast
import androidx.fragment.app.FragmentActivity
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.dto.OrdersItem
import com.alya.ecommerce_serang.data.api.dto.ReviewUIItem
import com.alya.ecommerce_serang.ui.order.detail.PaymentActivity
import com.alya.ecommerce_serang.ui.order.history.cancelorder.CancelOrderBottomSheet
import com.alya.ecommerce_serang.ui.order.review.CreateReviewActivity
import com.alya.ecommerce_serang.ui.product.ReviewProductActivity
import com.google.android.material.button.MaterialButton
import com.google.android.material.textfield.TextInputLayout
import com.google.gson.Gson
import java.io.File
import java.text.SimpleDateFormat
import java.util.Calendar
@ -146,7 +153,8 @@ class OrderHistoryAdapter(
visibility = View.VISIBLE
text = itemView.context.getString(R.string.canceled_order_btn)
setOnClickListener {
showCancelOrderDialog(order.orderId.toString())
showCancelOrderBottomSheet(order.orderId)
viewModel.refreshOrders()
}
}
deadlineDate.apply {
@ -167,7 +175,8 @@ class OrderHistoryAdapter(
visibility = View.VISIBLE
text = itemView.context.getString(R.string.canceled_order_btn)
setOnClickListener {
showCancelOrderDialog(order.orderId.toString())
showCancelOrderBottomSheet(order.orderId)
viewModel.refreshOrders()
}
}
@ -204,6 +213,7 @@ class OrderHistoryAdapter(
text = itemView.context.getString(R.string.canceled_order_btn)
setOnClickListener {
showCancelOrderDialog(order.orderId.toString())
viewModel.refreshOrders()
}
}
}
@ -222,7 +232,7 @@ class OrderHistoryAdapter(
text = itemView.context.getString(R.string.claim_complaint)
setOnClickListener {
showCancelOrderDialog(order.orderId.toString())
// Handle click event
viewModel.refreshOrders()
}
}
btnRight.apply {
@ -231,22 +241,13 @@ class OrderHistoryAdapter(
setOnClickListener {
// Handle click event
viewModel.confirmOrderCompleted(order.orderId, "completed")
viewModel.refreshOrders()
}
}
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
}
text = formatShipmentDate(order.updatedAt, order.etd ?: "0")
}
}
"completed" -> {
@ -262,6 +263,8 @@ class OrderHistoryAdapter(
visibility = View.VISIBLE
text = itemView.context.getString(R.string.add_review)
setOnClickListener {
addReviewProduct(order)
viewModel.refreshOrders()
// Handle click event
}
}
@ -322,9 +325,10 @@ class OrderHistoryAdapter(
}
}
private fun formatShipmentDate(dateString: String, estimate: Int): String {
private fun formatShipmentDate(dateString: String, estimate: String): String {
return try {
// Parse the input date
val estimateTD = if (estimate.isNullOrEmpty()) 0 else estimate.toInt()
val inputFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.getDefault())
inputFormat.timeZone = TimeZone.getTimeZone("UTC")
@ -339,7 +343,7 @@ class OrderHistoryAdapter(
calendar.time = it
// Add estimated days
calendar.add(Calendar.DAY_OF_MONTH, estimate)
calendar.add(Calendar.DAY_OF_MONTH, estimateTD)
outputFormat.format(calendar.time)
} ?: dateString
} catch (e: Exception) {
@ -485,10 +489,112 @@ class OrderHistoryAdapter(
}
dialog.show()
}
private fun showCancelOrderBottomSheet(orderId : Int) {
val context = itemView.context
// We need a FragmentManager to show the bottom sheet
// Try to get it from the context
val fragmentActivity = when (context) {
is FragmentActivity -> context
is ContextWrapper -> {
val baseContext = context.baseContext
if (baseContext is FragmentActivity) {
baseContext
} else {
// Log error and show a Toast instead if we can't get a FragmentManager
Log.e("OrderHistoryAdapter", "Cannot show bottom sheet: Context is not a FragmentActivity")
Toast.makeText(context, "Cannot show cancel order dialog", Toast.LENGTH_SHORT).show()
return
}
}
else -> {
// Log error and show a Toast instead if we can't get a FragmentManager
Log.e("OrderHistoryAdapter", "Cannot show bottom sheet: Context is not a FragmentActivity")
Toast.makeText(context, "Cannot show cancel order dialog", Toast.LENGTH_SHORT).show()
return
}
}
// Create and show the bottom sheet using the obtained FragmentManager
val bottomSheet = CancelOrderBottomSheet(
orderId = orderId,
onOrderCancelled = {
// Handle the successful cancellation
// Refresh the data
viewModel.refreshOrders() // Assuming there's a method to refresh orders
// Show a success message
Toast.makeText(context, "Order cancelled successfully", Toast.LENGTH_SHORT).show()
}
)
bottomSheet.show(fragmentActivity.supportFragmentManager, CancelOrderBottomSheet.TAG)
}
private fun addReviewProduct(order: OrdersItem) {
// Use ViewModel to fetch order details
viewModel.getOrderDetails(order.orderId)
// Create loading dialog
// val loadingDialog = Dialog(itemView.context).apply {
// requestWindowFeature(Window.FEATURE_NO_TITLE)
// setContentView(R.layout.dialog_loading)
// window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
// setCancelable(false)
// }
// loadingDialog.show()
viewModel.error.observe(itemView.findViewTreeLifecycleOwner()!!) { errorMsg ->
if (!errorMsg.isNullOrEmpty()) {
Toast.makeText(itemView.context, errorMsg, Toast.LENGTH_SHORT).show()
}
}
// Observe the order details result
viewModel.orderItems.observe(itemView.findViewTreeLifecycleOwner()!!) { orderItems ->
if (orderItems != null && orderItems.isNotEmpty()) {
// For single item review
if (orderItems.size == 1) {
val item = orderItems[0]
val intent = Intent(itemView.context, CreateReviewActivity::class.java).apply {
putExtra("order_item_id", item.orderItemId)
putExtra("product_name", item.productName)
putExtra("product_image", item.productImage)
}
(itemView.context as Activity).startActivityForResult(intent, REQUEST_CODE_REVIEW)
}
// For multiple items
else {
val reviewItems = orderItems.map { item ->
ReviewUIItem(
orderItemId = item.orderItemId,
productName = item.productName,
productImage = item.productImage
)
}
val itemsJson = Gson().toJson(reviewItems)
val intent = Intent(itemView.context, ReviewProductActivity::class.java).apply {
putExtra("order_items", itemsJson)
}
(itemView.context as Activity).startActivityForResult(intent, REQUEST_CODE_REVIEW)
}
} else {
Toast.makeText(
itemView.context,
"No items to review",
Toast.LENGTH_SHORT
).show()
}
}
}
}
companion object {
private const val REQUEST_IMAGE_PICK = 100
const val REQUEST_CODE_REVIEW = 101
private var imagePickCallback: ((Uri) -> Unit)? = null
// This method should be called from the activity's onActivityResult

View File

@ -47,11 +47,9 @@ class OrderHistoryFragment : Fragment() {
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)
4 -> getString(R.string.shipped_orders)
5 -> getString(R.string.completed_orders)
6 -> getString(R.string.canceled_orders)
else -> "Tab $position"
}
}.attach()

View File

@ -1,10 +1,13 @@
package com.alya.ecommerce_serang.ui.order.history
import android.app.Activity
import android.content.Intent
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.recyclerview.widget.LinearLayoutManager
@ -14,6 +17,7 @@ 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.ui.order.history.detailorder.DetailOrderStatusActivity
import com.alya.ecommerce_serang.utils.BaseViewModelFactory
import com.alya.ecommerce_serang.utils.SessionManager
@ -121,10 +125,21 @@ class OrderListFragment : Fragment() {
viewModel.getOrderList(status)
}
private val detailOrderLauncher = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) { result ->
if (result.resultCode == Activity.RESULT_OK) {
// Refresh order list when returning with OK result
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()
val intent = Intent(requireContext(), DetailOrderStatusActivity::class.java).apply {
putExtra("ORDER_ID", order.orderId)
putExtra("ORDER_STATUS", status) // Pass the current status
}
detailOrderLauncher.launch(intent)
}
override fun onDestroyView() {
@ -148,4 +163,5 @@ class OrderListFragment : Fragment() {
}
}
}
}

View File

@ -14,9 +14,7 @@ class OrderViewPagerAdapter(
"pending", // Menunggu Tagihan
"unpaid", // Belum Dibayar
"processed", // Diproses
"paid", // Dibayar
"shipped", // Dikirim
"delivered", // Diterima
"completed", // Selesai
"canceled" // Dibatalkan
)

View File

@ -0,0 +1,173 @@
package com.alya.ecommerce_serang.ui.order.history.cancelorder
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.AdapterView
import android.widget.Button
import android.widget.Spinner
import android.widget.TextView
import android.widget.Toast
import androidx.fragment.app.viewModels
import com.alya.ecommerce_serang.R
import com.alya.ecommerce_serang.data.api.dto.CancelOrderReq
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.ui.order.history.HistoryViewModel
import com.alya.ecommerce_serang.utils.BaseViewModelFactory
import com.alya.ecommerce_serang.utils.SessionManager
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
class CancelOrderBottomSheet(
private val orderId: Int,
private val onOrderCancelled: () -> Unit
) : BottomSheetDialogFragment() {
private lateinit var sessionManager: SessionManager
private val viewModel: HistoryViewModel by viewModels {
BaseViewModelFactory {
val apiService = ApiConfig.getApiService(sessionManager)
val orderRepository = OrderRepository(apiService)
HistoryViewModel(orderRepository)
}
}
private var selectedReason: CancelOrderReq? = null
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.layout_cancel_order_bottom, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
sessionManager = SessionManager(requireContext())
val tvTitle = view.findViewById<TextView>(R.id.tv_title)
val spinnerReason = view.findViewById<Spinner>(R.id.spinner_reason)
val btnCancel = view.findViewById<Button>(R.id.btn_cancel)
val btnConfirm = view.findViewById<Button>(R.id.btn_confirm)
// Set the title
tvTitle.text = "Cancel Order #$orderId"
// Set up the spinner with cancellation reasons
setupReasonSpinner(spinnerReason)
// Handle button clicks
btnCancel.setOnClickListener {
dismiss()
}
btnConfirm.setOnClickListener {
if (selectedReason == null) {
Toast.makeText(context, "Please select a reason", Toast.LENGTH_SHORT).show()
return@setOnClickListener
}
cancelOrder()
}
}
private fun setupReasonSpinner(spinner: Spinner) {
val reasons = getCancellationReasons()
val adapter = CancelReasonAdapter(requireContext(), reasons)
spinner.adapter = adapter
spinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
selectedReason = reasons[position]
}
override fun onNothingSelected(parent: AdapterView<*>?) {
selectedReason = null
}
}
}
private fun getCancellationReasons(): List<CancelOrderReq> {
// These should ideally come from the server or a configuration
return listOf(
CancelOrderReq(1, "Changed my mind"),
CancelOrderReq(2, "Found a better option"),
CancelOrderReq(3, "Ordered by mistake"),
CancelOrderReq(4, "Delivery time too long"),
CancelOrderReq(5, "Other reason")
)
}
private fun cancelOrder() {
// Validate reason selection
if (selectedReason == null) {
Toast.makeText(context, "Mohon pilih alasan pembatalan", Toast.LENGTH_SHORT).show()
return
}
// Create cancel request
val cancelRequest = CancelOrderReq(
orderId = orderId,
reason = selectedReason!!.reason
)
Log.d(TAG, "Sending cancel request to ViewModel: orderId=${cancelRequest.orderId}, reason='${cancelRequest.reason}'")
// Submit the cancellation
viewModel.cancelOrder(cancelRequest)
// Observe the status
viewModel.cancelOrderStatus.observe(viewLifecycleOwner) { result ->
when (result) {
is Result.Loading -> {
// Show loading indicator
// showLoading(true)
}
is Result.Success -> {
// Hide loading indicator
showLoading(false)
// Show success message
Toast.makeText(
context,
"Pesanan berhasil dibatalkan",
Toast.LENGTH_SHORT
).show()
Log.d(TAG, "Cancel order status: SUCCESS, message: ${result.data.message}")
// Notify callback and close dialog
onOrderCancelled()
dismiss()
}
is Result.Error -> {
// Hide loading indicator
showLoading(false)
Log.e(TAG, "Cancel order status: ERROR", result.exception)
// Show error message
val errorMsg = result.exception.message ?: "Gagal membatalkan pesanan"
Toast.makeText(context, errorMsg, Toast.LENGTH_SHORT).show()
}
}
}
}
// private fun showLoading(isLoading: Boolean) {
// binding.progressBar.isVisible = isLoading
// binding.btnCancel.isEnabled = !isLoading
// binding.btnConfirm.isEnabled = !isLoading
// }
private fun showLoading(isLoading: Boolean) {
// Implement loading indicator if needed
}
companion object {
const val TAG = "CancelOrderBottomSheet"
}
}

View File

@ -0,0 +1,36 @@
package com.alya.ecommerce_serang.ui.order.history.cancelorder
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ArrayAdapter
import android.widget.TextView
import com.alya.ecommerce_serang.R
import com.alya.ecommerce_serang.data.api.dto.CancelOrderReq
class CancelReasonAdapter(
context: Context,
private val reasons: List<CancelOrderReq>
) : ArrayAdapter<CancelOrderReq>(context, 0, reasons) {
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
return createItemView(position, convertView, parent)
}
override fun getDropDownView(position: Int, convertView: View?, parent: ViewGroup): View {
return createItemView(position, convertView, parent)
}
private fun createItemView(position: Int, recycledView: View?, parent: ViewGroup): View {
val reason = getItem(position) ?: return recycledView ?: View(context)
val view = recycledView ?: LayoutInflater.from(context)
.inflate(R.layout.item_cancel_order, parent, false)
val tvReason = view.findViewById<TextView>(R.id.tv_reason)
tvReason.text = reason.reason
return view
}
}

View File

@ -0,0 +1,63 @@
package com.alya.ecommerce_serang.ui.order.history.detailorder
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.customer.order.OrderListItemsItem
import com.bumptech.glide.Glide
import java.text.NumberFormat
import java.util.Locale
class DetailOrderItemsAdapter : RecyclerView.Adapter<DetailOrderItemsAdapter.DetailOrderItemViewHolder>() {
private val items = mutableListOf<OrderListItemsItem>()
fun submitList(newItems: List<OrderListItemsItem>) {
items.clear()
items.addAll(newItems)
notifyDataSetChanged()
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DetailOrderItemViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.item_order_detail_product, parent, false)
return DetailOrderItemViewHolder(view)
}
override fun onBindViewHolder(holder: DetailOrderItemViewHolder, position: Int) {
holder.bind(items[position])
}
override fun getItemCount(): Int = items.size
inner class DetailOrderItemViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
private val ivProduct: ImageView = itemView.findViewById(R.id.ivProduct)
private val tvProductName: TextView = itemView.findViewById(R.id.tvProductName)
private val tvQuantity: TextView = itemView.findViewById(R.id.tvQuantity)
private val tvPrice: TextView = itemView.findViewById(R.id.tvPrice)
fun bind(item: OrderListItemsItem) {
// Load product image
Glide.with(itemView.context)
.load(item.productImage)
.placeholder(R.drawable.placeholder_image)
.error(R.drawable.placeholder_image)
.into(ivProduct)
val newPrice = formatCurrency(item.price.toDouble())
tvProductName.text = item.productName
tvQuantity.text = "${item.quantity} buah"
tvPrice.text = "Rp${newPrice}"
}
}
private fun formatCurrency(amount: Double): String {
val formatter = NumberFormat.getCurrencyInstance(Locale("in", "ID"))
return formatter.format(amount).replace(",00", "")
}
}

View File

@ -0,0 +1,739 @@
package com.alya.ecommerce_serang.ui.order.history.detailorder
import android.app.Activity
import android.app.Dialog
import android.content.Intent
import android.graphics.Color
import android.net.Uri
import android.os.Bundle
import android.provider.MediaStore
import android.util.Log
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.TextView
import android.widget.Toast
import androidx.activity.enableEdgeToEdge
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat
import androidx.recyclerview.widget.LinearLayoutManager
import com.alya.ecommerce_serang.R
import com.alya.ecommerce_serang.data.api.dto.CompletedOrderRequest
import com.alya.ecommerce_serang.data.api.dto.OrdersItem
import com.alya.ecommerce_serang.data.api.dto.ReviewUIItem
import com.alya.ecommerce_serang.data.api.response.customer.order.OrderListItemsItem
import com.alya.ecommerce_serang.data.api.response.customer.order.Orders
import com.alya.ecommerce_serang.data.api.retrofit.ApiConfig
import com.alya.ecommerce_serang.data.repository.OrderRepository
import com.alya.ecommerce_serang.databinding.ActivityDetailOrderStatusBinding
import com.alya.ecommerce_serang.ui.order.detail.PaymentActivity
import com.alya.ecommerce_serang.ui.order.history.cancelorder.CancelOrderBottomSheet
import com.alya.ecommerce_serang.ui.order.review.CreateReviewActivity
import com.alya.ecommerce_serang.ui.product.ReviewProductActivity
import com.alya.ecommerce_serang.utils.BaseViewModelFactory
import com.alya.ecommerce_serang.utils.SessionManager
import com.google.android.material.button.MaterialButton
import com.google.android.material.textfield.TextInputLayout
import com.google.gson.Gson
import java.io.File
import java.text.SimpleDateFormat
import java.util.Calendar
import java.util.Locale
import java.util.TimeZone
class DetailOrderStatusActivity : AppCompatActivity() {
private lateinit var binding: ActivityDetailOrderStatusBinding
private lateinit var sessionManager: SessionManager
private var orderId: Int = -1
private var orderStatus: String = ""
private val orders = mutableListOf<OrdersItem>()
private var selectedImageUri: Uri? = null
private var cancelDialog: Dialog? = null
private var dialogImageView: ImageView? = null
private var dialogSelectTextView: TextView? = null
private val viewModel: DetailOrderViewModel by viewModels {
BaseViewModelFactory {
val apiService = ApiConfig.getApiService(sessionManager)
val orderRepository = OrderRepository(apiService)
DetailOrderViewModel(orderRepository)
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Log.d(TAG, "onCreate: Starting activity initialization")
binding = ActivityDetailOrderStatusBinding.inflate(layoutInflater)
setContentView(binding.root)
sessionManager = SessionManager(this)
WindowCompat.setDecorFitsSystemWindows(window, false)
enableEdgeToEdge()
// Apply insets to your root layout
ViewCompat.setOnApplyWindowInsetsListener(binding.root) { view, windowInsets ->
val systemBars = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
view.setPadding(
systemBars.left,
systemBars.top,
systemBars.right,
systemBars.bottom
)
windowInsets
}
orderId = intent.getIntExtra("ORDER_ID", -1)
orderStatus = intent.getStringExtra("ORDER_STATUS") ?: ""
Log.d(TAG, "onCreate: orderID=$orderId, orderStatus=$orderStatus")
if (orderId == -1) {
Log.e(TAG, "onCreate: Invalid order ID received")
Toast.makeText(this, "Invalid order ID", Toast.LENGTH_SHORT).show()
finish()
return
}
setupObservers()
loadOrderDetails()
Log.d(TAG, "onCreate: Activity initialization completed")
}
private fun setupObservers() {
Log.d(TAG, "setupObservers: Setting up LiveData observers")
// Observe order details
viewModel.orderDetails.observe(this) { orders ->
if (orders != null) {
Log.d(TAG, "Observer: orderDetails received, orderId=${orders.orderId}")
populateOrderDetails(orders)
} else {
Log.w(TAG, "Observer: orderDetails is null")
}
}
// Observe loading state
viewModel.isLoading.observe(this) { isLoading ->
Log.d(TAG, "Observer: isLoading=$isLoading")
binding.progressBar.visibility = if (isLoading) View.VISIBLE else View.GONE
}
// Observe error messages
viewModel.error.observe(this) { errorMsg ->
if (!errorMsg.isNullOrEmpty()) {
Log.e(TAG, "Observer: Error received: $errorMsg")
Toast.makeText(this, errorMsg, Toast.LENGTH_SHORT).show()
}
}
// Observe success status
viewModel.isSuccess.observe(this) { isSuccess ->
Log.d(TAG, "Observer: isSuccess=$isSuccess")
}
// Observe messages
viewModel.message.observe(this) { message ->
if (!message.isNullOrEmpty()) {
Log.d(TAG, "Observer: Message: $message")
}
}
}
private fun loadOrderDetails() {
Log.d(TAG, "loadOrderDetails: Requesting order details for orderId=$orderId")
viewModel.getOrderDetails(orderId)
}
private fun populateOrderDetails(orders: Orders) {
Log.d(TAG, "populateOrderDetails: Populating UI with order data")
try {
// Set order date and payment deadline
binding.tvOrderDate.text = formatDate(orders.createdAt)
binding.tvPaymentDeadline.text = formatDatePay(orders.updatedAt)
Log.d(TAG, "populateOrderDetails: Order created at ${orders.createdAt}, formatted as ${binding.tvOrderDate.text}")
// Set address information
binding.tvRecipientName.text = orders.detail
binding.tvAddress.text = "${orders.street}, ${orders.subdistrict}"
Log.d(TAG, "populateOrderDetails: Shipping to ${orders.detail} at ${orders.street}")
// Set courier info
binding.tvCourier.text = "${orders.courier} ${orders.service}"
Log.d(TAG, "populateOrderDetails: Courier=${orders.courier}, Service=${orders.service}")
// Set product details using RecyclerView
Log.d(TAG, "populateOrderDetails: Setting up products RecyclerView with ${orders.orderItems.size} items")
setupProductsRecyclerView(orders.orderItems)
// Set payment method
binding.tvPaymentMethod.text = "Bank Transfer - ${orders.payInfoName ?: "Tidak tersedia"}"
Log.d(TAG, "populateOrderDetails: Payment method=${orders.payInfoName ?: "Tidak tersedia"}")
// Set subtotal, shipping cost, and total
val subtotal = orders.totalAmount?.toIntOrNull()?.minus(orders.shipmentPrice.toIntOrNull() ?: 0) ?: 0
binding.tvSubtotal.text = "Rp$subtotal"
binding.tvShippingCost.text = "Rp${orders.shipmentPrice}"
binding.tvTotal.text = "Rp${orders.totalAmount}"
Log.d(TAG, "populateOrderDetails: Subtotal=$subtotal, Shipping=${orders.shipmentPrice}, Total=${orders.totalAmount}")
// Adjust buttons based on order status
Log.d(TAG, "populateOrderDetails: Adjusting buttons for status=$orderStatus")
adjustButtonsBasedOnStatus(orders, orderStatus)
} catch (e: Exception) {
Log.e(TAG, "populateOrderDetails: Error while populating UI", e)
Toast.makeText(this, "Error loading order details: ${e.message}", Toast.LENGTH_SHORT).show()
}
}
private fun setupProductsRecyclerView(orderItems: List<OrderListItemsItem>) {
Log.d(TAG, "setupProductsRecyclerView: Setting up RecyclerView with ${orderItems.size} items")
val adapter = DetailOrderItemsAdapter()
binding.rvOrderItems.apply {
layoutManager = LinearLayoutManager(this@DetailOrderStatusActivity)
this.adapter = adapter
}
adapter.submitList(orderItems)
}
private fun adjustButtonsBasedOnStatus(orders: Orders, status: String) {
Log.d(TAG, "adjustButtonsBasedOnStatus: Adjusting UI for status=$status")
// Reset button visibility first
binding.btnPrimary.visibility = View.GONE
binding.btnSecondary.visibility = View.GONE
// Set status header
val statusText = when(status) {
"pending" -> "Belum Bayar"
"unpaid" -> "Belum Bayar"
"processed" -> "Diproses"
"shipped" -> "Dikirim"
"delivered" -> "Diterima"
"completed" -> "Selesai"
"canceled" -> "Dibatalkan"
else -> "Detail Pesanan"
}
binding.tvStatusHeader.text = statusText
Log.d(TAG, "adjustButtonsBasedOnStatus: Status header set to '$statusText'")
when (status) {
"pending"->{
binding.tvStatusHeader.text = "Menunggu Tagihan"
binding.tvStatusNote.visibility = View.VISIBLE
binding.tvStatusNote.text = "Pesanan ini harus dibayar sebelum ${formatDatePay(orders.updatedAt)}"
// Set buttons
binding.btnSecondary.apply {
visibility = View.VISIBLE
text = "Batalkan Pesanan"
setOnClickListener {
Log.d(TAG, "Cancel Order button clicked")
showCancelOrderBottomSheet(orders.orderId)
viewModel.getOrderDetails(orders.orderId)
}
}
}
"unpaid" -> {
Log.d(TAG, "adjustButtonsBasedOnStatus: Setting up UI for pending/unpaid order")
// Show status note
binding.tvStatusHeader.text = "Belum Dibayar"
binding.tvStatusNote.visibility = View.VISIBLE
binding.tvStatusNote.text = "Pesanan ini harus dibayar sebelum ${formatDatePay(orders.updatedAt)}"
// Set buttons
binding.btnSecondary.apply {
visibility = View.VISIBLE
text = "Batalkan Pesanan"
setOnClickListener {
Log.d(TAG, "Cancel Order button clicked")
showCancelOrderBottomSheet(orders.orderId)
viewModel.getOrderDetails(orders.orderId)
}
}
binding.btnPrimary.apply {
visibility = View.VISIBLE
text = "Bayar Sekarang"
setOnClickListener {
Log.d(TAG, "Pay Now button clicked, navigating to PaymentActivity")
val intent = Intent(this@DetailOrderStatusActivity, PaymentActivity::class.java)
intent.putExtra("ORDER_ID", orders.orderId)
intent.putExtra("ORDER_PAYMENT_ID", orders.paymentInfoId)
startActivity(intent)
}
}
}
"processed" -> {
Log.d(TAG, "adjustButtonsBasedOnStatus: Setting up UI for processed order")
binding.tvStatusHeader.text = "Sedang Diproses"
binding.tvStatusNote.visibility = View.VISIBLE
binding.tvStatusNote.text = "Penjual sedang memproses pesanan Anda"
binding.btnSecondary.apply {
visibility = View.VISIBLE
text = "Batalkan Pesanan"
setOnClickListener {
Log.d(TAG, "Cancel Order button clicked for processed order")
showCancelOrderDialog(orders.orderId.toString())
viewModel.getOrderDetails(orders.orderId)
}
}
binding.btnPrimary.apply {
visibility = View.GONE
}
}
"shipped" -> {
Log.d(TAG, "adjustButtonsBasedOnStatus: Setting up UI for shipped order")
binding.tvStatusHeader.text = "Sudah Dikirim"
binding.tvStatusNote.visibility = View.VISIBLE
binding.tvStatusNote.text = "Pesanan Anda sedang dalam perjalanan. Akan sampai sekitar ${formatShipmentDate(orders.updatedAt, orders.etd ?: "0")}"
binding.btnSecondary.apply {
visibility = View.VISIBLE
text = "Ajukan Komplain"
setOnClickListener {
Log.d(TAG, "Complaint button clicked")
showCancelOrderDialog(orders.orderId.toString())
viewModel.getOrderDetails(orders.orderId)
}
}
binding.btnPrimary.apply {
visibility = View.VISIBLE
text = "Terima Pesanan"
val completedOrderRequest = CompletedOrderRequest(
orderId = orders.orderId,
statusComplete = "completed"
)
setOnClickListener {
Log.d(TAG, "Confirm receipt button clicked, marking order as completed")
viewModel.confirmOrderCompleted(completedOrderRequest)
}
}
}
"completed" -> {
Log.d(TAG, "adjustButtonsBasedOnStatus: Setting up UI for delivered/completed order")
binding.tvStatusHeader.text = "Pesanan Selesai"
binding.tvStatusNote.visibility = View.GONE
binding.btnPrimary.apply {
visibility = View.VISIBLE
text = "Beri Ulasan"
setOnClickListener {
Log.d(TAG, "Review button clicked")
addReviewForOrder(orders)
viewModel.getOrderDetails(orders.orderId)
}
}
binding.btnSecondary.apply {
visibility = View.GONE
}
}
"canceled" -> {
Log.d(TAG, "adjustButtonsBasedOnStatus: Setting up UI for canceled order")
binding.tvStatusHeader.text = "Pesanan Selesai"
binding.tvStatusNote.visibility = View.VISIBLE
binding.tvStatusNote.text = "Pesanan dibatalkan: ${orders.cancelReason ?: "Alasan tidak diberikan"}"
binding.btnSecondary.apply {
visibility = View.GONE
}
binding.btnPrimary.apply {
visibility = View.GONE
}
}
}
}
private fun addReviewForOrder(orders: Orders) {
Log.d(TAG, "addReviewForOrder: Preparing to add review for order ${orders.orderId}")
val orderItems = orders.orderItems
if (orderItems.isNotEmpty()) {
Log.d(TAG, "addReviewForOrder: Found ${orderItems.size} items to review")
// For single item review
if (orderItems.size == 1) {
val item = orderItems[0]
Log.d(TAG, "addReviewForOrder: Launching single item review for orderItemId=${item.orderItemId}")
val intent = Intent(this, CreateReviewActivity::class.java).apply {
putExtra("order_item_id", item.orderItemId)
putExtra("product_name", item.productName)
putExtra("product_image", item.productImage)
}
startActivityForResult(intent, REQUEST_CODE_REVIEW)
}
// For multiple items
else {
Log.d(TAG, "addReviewForOrder: Launching multi-item review with ${orderItems.size} items")
val reviewItems = orderItems.map { item ->
ReviewUIItem(
orderItemId = item.orderItemId,
productName = item.productName,
productImage = item.productImage
)
}
val itemsJson = Gson().toJson(reviewItems)
Log.d(TAG, "addReviewForOrder: JSON prepared for items: ${itemsJson.take(100)}...")
val intent = Intent(this, ReviewProductActivity::class.java).apply {
putExtra("order_items", itemsJson)
}
startActivityForResult(intent, REQUEST_CODE_REVIEW)
}
} else {
Log.w(TAG, "addReviewForOrder: No items found to review")
Toast.makeText(this, "No items to review", Toast.LENGTH_SHORT).show()
}
}
private fun showCancelOrderDialog(orderId: String) {
Log.d(TAG, "showCancelOrderDialog: Showing dialog for orderId=$orderId")
val dialog = Dialog(this)
dialog.requestWindowFeature(Window.FEATURE_NO_TITLE)
dialog.setContentView(R.layout.dialog_cancel_order)
dialog.setCancelable(true)
// Store dialog reference
cancelDialog = dialog
// 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)
dialogImageView = ivComplaintImage
dialogSelectTextView = tvSelectImage
// Set up the reasons dropdown
val reasons = this.resources.getStringArray(R.array.cancellation_reasons)
Log.d(TAG, "showCancelOrderDialog: Setting up dropdown with ${reasons.size} reasons")
val adapter = ArrayAdapter(this, 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 {
Log.d(TAG, "showCancelOrderDialog: Image selection clicked")
// Create an intent to open the image picker
val galleryIntent = Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI)
(this as? Activity)?.startActivityForResult(galleryIntent, REQUEST_IMAGE_PICK)
// Set up result handler in the activity
val activity = this 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 ->
Log.d(TAG, "imagePickCallback: Image selected, URI=$uri")
selectedImageUri = uri
// Load and display the selected image
ivComplaintImage.setImageURI(uri)
tvSelectImage.visibility = View.GONE
}
}
}
// Set click listeners for buttons
btnCancelDialog.setOnClickListener {
Log.d(TAG, "showCancelOrderDialog: Cancel button clicked, dismissing dialog")
dialog.dismiss()
}
btnConfirmCancel.setOnClickListener {
val reason = spinnerCancelReason.text.toString().trim()
Log.d(TAG, "showCancelOrderDialog: Confirm cancel clicked with reason: $reason")
if (reason.isEmpty()) {
Log.w(TAG, "showCancelOrderDialog: No reason selected")
tilCancelReason.error = this.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 {
Log.d(TAG, "showCancelOrderDialog: Converting URI to file: $uri")
// Get the file path from URI
val filePathColumn = arrayOf(MediaStore.Images.Media.DATA)
val cursor = this.contentResolver.query(uri, filePathColumn, null, null, null)
cursor?.use {
if (it.moveToFirst()) {
val columnIndex = it.getColumnIndex(filePathColumn[0])
val filePath = it.getString(columnIndex)
Log.d(TAG, "showCancelOrderDialog: File path: $filePath")
return@let File(filePath)
}
}
Log.w(TAG, "showCancelOrderDialog: Failed to get file path from URI")
null
} catch (e: Exception) {
Log.e(TAG, "showCancelOrderDialog: Error getting file from URI: ${e.message}", e)
null
}
}
// Show loading indicator
Log.d(TAG, "showCancelOrderDialog: Showing loading indicator")
val loadingView = View(this).apply {
layoutParams = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
)
setBackgroundColor(Color.parseColor("#80000000"))
}
dialog.addContentView(loadingView, loadingView.layoutParams)
// Call the ViewModel to cancel the order with image
Log.d(TAG, "showCancelOrderDialog: Calling cancelOrderWithImage for orderId=$orderId")
viewModel.cancelOrderWithImage(orderId.toInt(), reason, imageFile)
// Observe for success/failure
viewModel.isSuccess.observe(this) { isSuccess ->
Log.d(TAG, "showCancelOrderDialog observer: isSuccess=$isSuccess")
if (isSuccess) {
Log.d(TAG, "showCancelOrderDialog: Order canceled successfully")
Toast.makeText(this, getString(R.string.order_canceled_successfully), Toast.LENGTH_SHORT).show()
dialog.dismiss()
// Set result and finish
setResult(RESULT_OK)
finish()
} else {
Log.e(TAG, "showCancelOrderDialog: Failed to cancel order: ${viewModel.message.value}")
Toast.makeText(this, viewModel.message.value ?: getString(R.string.failed_to_cancel_order), Toast.LENGTH_SHORT).show()
}
}
}
Log.d(TAG, "showCancelOrderDialog: Dialog setup complete, showing dialog")
dialog.show()
}
private fun showCancelOrderBottomSheet(orderId: Int) {
// Create and show the bottom sheet directly since we're already in an Activity
val bottomSheet = CancelOrderBottomSheet(
orderId = orderId,
onOrderCancelled = {
// Handle the successful cancellation
// Refresh the data
// Show a success message
Toast.makeText(this, "Order cancelled successfully", Toast.LENGTH_SHORT).show()
}
)
bottomSheet.show(supportFragmentManager, CancelOrderBottomSheet.TAG)
}
private fun formatDate(dateString: String): String {
Log.d(TAG, "formatDate: Formatting date: $dateString")
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)
val formatted = outputFormat.format(calendar.time)
Log.d(TAG, "formatDate: Formatted date: $formatted")
formatted
} ?: dateString
} catch (e: Exception) {
Log.e(TAG, "formatDate: Error formatting date: ${e.message}", e)
dateString
}
}
private fun formatDatePay(dateString: String): String {
Log.d(TAG, "formatDatePay: Formatting payment date: $dateString")
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())
val formatted = dueDateFormat.format(calendar.time)
Log.d(TAG, "formatDatePay: Formatted payment date: $formatted")
formatted
} catch (e: Exception) {
Log.e(TAG, "formatDatePay: Error formatting date: ${e.message}", e)
dateString
}
}
private fun formatShipmentDate(dateString: String, estimateString: String): String {
Log.d(TAG, "formatShipmentDate: Formatting shipment date: $dateString with ETD: $estimateString")
return try {
// Safely parse the estimate to Int
val estimate = if (estimateString.isNullOrEmpty()) 0 else estimateString.toInt()
Log.d(TAG, "formatShipmentDate: Parsed ETD as $estimate days")
// 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)
val formatted = outputFormat.format(calendar.time)
Log.d(TAG, "formatShipmentDate: Estimated arrival date: $formatted")
formatted
} ?: dateString
} catch (e: Exception) {
Log.e(TAG, "formatShipmentDate: Error formatting shipment date: ${e.message}", e)
dateString
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
Log.d(TAG, "onActivityResult: requestCode=$requestCode, resultCode=$resultCode")
when (requestCode) {
REQUEST_IMAGE_PICK -> {
if (resultCode == RESULT_OK && data != null) {
// Get the selected image URI
selectedImageUri = data.data
Log.d(TAG, "onActivityResult: Image selected, URI=$selectedImageUri")
// Update the image view in the dialog if the dialog is still showing
if (cancelDialog?.isShowing == true) {
Log.d(TAG, "onActivityResult: Updating image in dialog")
dialogImageView?.setImageURI(selectedImageUri)
dialogSelectTextView?.visibility = View.GONE
} else {
Log.w(TAG, "onActivityResult: Dialog is not showing, cannot update image")
}
} else {
Log.w(TAG, "onActivityResult: Image selection canceled or failed")
}
}
REQUEST_CODE_REVIEW -> {
if (resultCode == RESULT_OK) {
// Review submitted successfully
Log.d(TAG, "onActivityResult: Review submitted successfully")
Toast.makeText(this, "Review submitted successfully", Toast.LENGTH_SHORT).show()
// Refresh order details
loadOrderDetails()
// Set result to notify parent activity
setResult(RESULT_OK)
} else {
Log.w(TAG, "onActivityResult: Review submission canceled or failed")
}
}
}
}
override fun onDestroy() {
Log.d(TAG, "onDestroy: Cleaning up references")
super.onDestroy()
// Clean up references
cancelDialog = null
dialogImageView = null
dialogSelectTextView = null
}
companion object {
private const val REQUEST_IMAGE_PICK = 100
private const val REQUEST_CODE_REVIEW = 101
private const val TAG = "DetailOrderActivity" // Add tag for logging
private var imagePickCallback: ((Uri) -> Unit)? = null
}
}

View File

@ -0,0 +1,88 @@
package com.alya.ecommerce_serang.ui.order.history.detailorder
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.customer.order.Orders
import com.alya.ecommerce_serang.data.repository.OrderRepository
import kotlinx.coroutines.launch
import java.io.File
class DetailOrderViewModel(private val orderRepository: OrderRepository): ViewModel() {
private val _orderDetails = MutableLiveData<Orders>()
val orderDetails: LiveData<Orders> = _orderDetails
private val _isLoading = MutableLiveData<Boolean>()
val isLoading: LiveData<Boolean> = _isLoading
private val _error = MutableLiveData<String>()
val error: LiveData<String> = _error
private val _isSuccess = MutableLiveData<Boolean>()
val isSuccess: LiveData<Boolean> = _isSuccess
private val _message = MutableLiveData<String>()
val message: LiveData<String> = _message
fun getOrderDetails(orderId: Int) {
_isLoading.value = true
viewModelScope.launch {
try {
val response = orderRepository.getOrderDetails(orderId)
if (response != null) {
_orderDetails.value = response.orders
} else {
_error.value = "Failed to load order details"
}
} catch (e: Exception) {
_error.value = "Error: ${e.message}"
Log.e("DetailOrderViewModel", "Error loading order details", e)
} finally {
_isLoading.value = false
}
}
}
fun confirmOrderCompleted(detailOrderRequest: CompletedOrderRequest) {
_isLoading.value = true
viewModelScope.launch {
try {
orderRepository.confirmOrderCompleted(detailOrderRequest)
_isSuccess.value = true
_message.value = "Order status updated successfully"
getOrderDetails(detailOrderRequest.orderId)
} catch (e: Exception) {
_isSuccess.value = false
_message.value = "Error: ${e.message}"
Log.e("DetailOrderViewModel", "Error updating order status", e)
} finally {
_isLoading.value = false
}
}
}
fun cancelOrderWithImage(orderId: Int, reason: String, imageFile: File?) {
_isLoading.value = true
viewModelScope.launch {
try {
orderRepository.submitComplaint(orderId.toString(), reason, imageFile)
_isSuccess.value = true
_message.value = "Order canceled successfully"
} catch (e: Exception) {
_isSuccess.value = false
_message.value = "Error: ${e.message}"
Log.e("DetailOrderViewModel", "Error canceling order", e)
} finally {
_isLoading.value = false
}
}
}
}

View File

@ -0,0 +1,64 @@
package com.alya.ecommerce_serang.ui.order.review
import android.text.Editable
import android.text.TextWatcher
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.alya.ecommerce_serang.R
import com.alya.ecommerce_serang.data.api.dto.ReviewUIItem
import com.alya.ecommerce_serang.databinding.ItemReviewProductBinding
import com.bumptech.glide.Glide
class AddReviewAdapter(
private val items: List<ReviewUIItem>,
private val onRatingChanged: (position: Int, rating: Int) -> Unit,
private val onReviewTextChanged: (position: Int, text: String) -> Unit
) : RecyclerView.Adapter<AddReviewAdapter.AddReviewViewHolder>() {
inner class AddReviewViewHolder(private val binding: ItemReviewProductBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(item: ReviewUIItem) {
binding.apply {
tvProductName.text = item.productName
Glide.with(itemView.context)
.load(item.productImage)
.placeholder(R.drawable.placeholder_image)
.error(R.drawable.placeholder_image)
.into(ivProduct)
ratingBar.rating = item.rating.toFloat()
etReviewText.setText(item.reviewText)
ratingBar.setOnRatingBarChangeListener { _, rating, _ ->
onRatingChanged(adapterPosition, rating.toInt())
}
etReviewText.addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) {}
override fun onTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) {}
override fun afterTextChanged(editable: Editable?) {
onReviewTextChanged(adapterPosition, editable.toString())
}
})
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AddReviewViewHolder {
val binding = ItemReviewProductBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
return AddReviewViewHolder(binding)
}
override fun onBindViewHolder(holder: AddReviewViewHolder, position: Int) {
holder.bind(items[position])
}
override fun getItemCount(): Int = items.size
}

View File

@ -0,0 +1,188 @@
package com.alya.ecommerce_serang.ui.order.review
import android.os.Bundle
import android.util.Log
import android.widget.Toast
import androidx.activity.enableEdgeToEdge
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat
import androidx.recyclerview.widget.LinearLayoutManager
import com.alya.ecommerce_serang.data.api.dto.ReviewProductItem
import com.alya.ecommerce_serang.data.api.dto.ReviewUIItem
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.ActivityCreateReviewBinding
import com.alya.ecommerce_serang.utils.BaseViewModelFactory
import com.alya.ecommerce_serang.utils.SessionManager
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
class CreateReviewActivity : AppCompatActivity() {
private lateinit var binding: ActivityCreateReviewBinding
private lateinit var sessionManager: SessionManager
private val reviewItems = mutableListOf<ReviewUIItem>()
private var addReviewAdapter: AddReviewAdapter? = null
private val viewModel : CreateReviewViewModel by viewModels {
BaseViewModelFactory {
val apiService = ApiConfig.getApiService(sessionManager)
val orderRepository = OrderRepository(apiService)
CreateReviewViewModel(orderRepository)
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityCreateReviewBinding.inflate(layoutInflater)
setContentView(binding.root)
sessionManager = SessionManager(this)
WindowCompat.setDecorFitsSystemWindows(window, false)
enableEdgeToEdge()
ViewCompat.setOnApplyWindowInsetsListener(binding.root) { view, windowInsets ->
val systemBars = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
view.setPadding(
systemBars.left,
systemBars.top,
systemBars.right,
systemBars.bottom
)
windowInsets
}
setupToolbar()
getIntentData()
setupRecyclerView()
observeViewModel()
setupSubmitButton()
}
private fun setupToolbar() {
setSupportActionBar(binding.toolbar)
supportActionBar?.setDisplayShowTitleEnabled(false)
binding.btnBack.setOnClickListener { onBackPressed() }
}
private fun getIntentData() {
// First check if multiple items were passed
val orderItemsJson = intent.getStringExtra("order_items")
if (orderItemsJson != null) {
try {
val type = object : TypeToken<List<ReviewUIItem>>() {}.type
val items: List<ReviewUIItem> = Gson().fromJson(orderItemsJson, type)
// Make sure we explicitly set rating and reviewText
reviewItems.addAll(items.map { item ->
ReviewUIItem(
orderItemId = item.orderItemId,
productName = item.productName,
productImage = item.productImage,
rating = 5, // Default to 5 stars
reviewText = "" // Empty by default
)
})
} catch (e: Exception) {
Toast.makeText(this, "Error loading review items", Toast.LENGTH_SHORT).show()
finish()
}
} else {
// Check if a single item was passed
val orderItemId = intent.getIntExtra("order_item_id", -1)
val productName = intent.getStringExtra("product_name") ?: ""
val productImage = intent.getStringExtra("product_image") ?: ""
if (orderItemId != -1) {
reviewItems.add(
ReviewUIItem(
orderItemId = orderItemId,
productName = productName,
productImage = productImage,
rating = 5, // Default to 5 stars
reviewText = "" // Empty by default
)
)
} else {
Toast.makeText(this, "No items to review", Toast.LENGTH_SHORT).show()
finish()
}
}
}
private fun setupRecyclerView() {
addReviewAdapter = AddReviewAdapter(
reviewItems,
onRatingChanged = { position, rating ->
reviewItems[position].rating = rating
},
onReviewTextChanged = { position, text ->
reviewItems[position].reviewText = text
}
)
binding.rvReviewItems.apply {
layoutManager = LinearLayoutManager(this@CreateReviewActivity)
adapter = addReviewAdapter
}
}
private fun observeViewModel() {
viewModel.reviewSubmitStatus.observe(this) { result ->
when (result) {
is Result.Loading -> {
// Show loading indicator
// You can add a ProgressBar in your layout and show/hide it here
}
is Result.Success -> {
// All reviews submitted successfully
Toast.makeText(this, "Ulasan berhasil dikirim", Toast.LENGTH_SHORT).show()
setResult(RESULT_OK)
finish()
}
is Result.Error -> {
// Show error message
Log.e("CreateReviewActivity", "Error: ${result.exception}")
// Toast.makeText(this, result.message, Toast.LENGTH_SHORT).show()
}
}
}
}
private fun setupSubmitButton() {
binding.btnSubmitReview.setOnClickListener {
// Validate all reviews
var isValid = true
for (item in reviewItems) {
if (item.reviewText.isBlank()) {
isValid = false
Toast.makeText(this, "Mohon isi semua ulasan", Toast.LENGTH_SHORT).show()
break
}
}
// In setupSubmitButton() method
if (isValid) {
viewModel.setTotalReviewsToSubmit(reviewItems.size)
// Submit all reviews
for (item in reviewItems) {
Log.d("ReviewActivity", "Submitting review for item ${item.orderItemId}: rating=${item.rating}, text=${item.reviewText}")
val reviewProductItem = ReviewProductItem(
orderItemId = item.orderItemId,
rating = item.rating,
reviewTxt = item.reviewText
)
viewModel.submitReview(reviewProductItem)
}
}
}
}
}

View File

@ -0,0 +1,41 @@
package com.alya.ecommerce_serang.ui.order.review
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.ReviewProductItem
import com.alya.ecommerce_serang.data.api.response.customer.order.CreateReviewResponse
import com.alya.ecommerce_serang.data.repository.OrderRepository
import com.alya.ecommerce_serang.data.repository.Result
import kotlinx.coroutines.launch
class CreateReviewViewModel(private val repository: OrderRepository): ViewModel() {
private val _reviewSubmitStatus = MutableLiveData<Result<CreateReviewResponse>>()
val reviewSubmitStatus: LiveData<Result<CreateReviewResponse>> = _reviewSubmitStatus
private val _reviewsSubmitted = MutableLiveData(0)
private var totalReviewsToSubmit = 0
private var anyFailures = false
fun submitReview(reviewItem: ReviewProductItem) {
viewModelScope.launch {
try {
_reviewSubmitStatus.value = Result.Loading
val result = repository.createReviewProduct(reviewItem)
_reviewSubmitStatus.value = result
} catch (e: Exception) {
anyFailures = true
Log.e("CreateReviewViewModel", "Error create review: ${e.message}")
_reviewSubmitStatus.value = Result.Error(e)
}
}
}
fun setTotalReviewsToSubmit(count: Int) {
totalReviewsToSubmit = count
_reviewsSubmitted.value = 0
anyFailures = false
}
}

View File

@ -12,6 +12,7 @@ import android.widget.Toast
import androidx.activity.enableEdgeToEdge
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.SwitchCompat
import androidx.core.view.ViewCompat
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat
@ -28,6 +29,7 @@ import com.alya.ecommerce_serang.data.api.retrofit.ApiService
import com.alya.ecommerce_serang.data.repository.ProductRepository
import com.alya.ecommerce_serang.data.repository.Result
import com.alya.ecommerce_serang.databinding.ActivityDetailProductBinding
import com.alya.ecommerce_serang.ui.cart.CartActivity
import com.alya.ecommerce_serang.ui.chat.ChatActivity
import com.alya.ecommerce_serang.ui.home.HorizontalProductAdapter
import com.alya.ecommerce_serang.ui.order.CheckoutActivity
@ -45,6 +47,9 @@ class DetailProductActivity : AppCompatActivity() {
private var productAdapter: HorizontalProductAdapter? = null
private var reviewsAdapter: ReviewsAdapter? = null
private var currentQuantity = 1
private var isWholesaleAvailable: Boolean = false
private var isWholesaleSelected: Boolean = false
private var minOrder: Int = 0
private val viewModel: ProductUserViewModel by viewModels {
BaseViewModelFactory {
@ -193,6 +198,46 @@ class DetailProductActivity : AppCompatActivity() {
}
}
val searchContainerView = binding.searchContainer
searchContainerView.btnCart.setOnClickListener{
navigateToCart()
}
setupRecyclerViewOtherProducts()
}
private fun navigateToCart() {
val intent = Intent(this, CartActivity::class.java)
startActivity(intent)
}
private fun updateUI(product: Product){
binding.tvProductName.text = product.productName
binding.tvPrice.text = "Rp${formatCurrency(product.price.toDouble())}"
binding.tvSold.text = "Terjual ${product.totalSold} buah"
binding.tvRating.text = product.rating
binding.tvWeight.text = "${product.weight} gram"
binding.tvStock.text = "${product.stock} buah"
binding.tvCategory.text = product.productCategory
binding.tvDescription.text = product.description
minOrder = product.wholesaleMinItem ?: 1
isWholesaleAvailable = product.isWholesale ?: false
isWholesaleSelected = false // Default to regular pricing
if (isWholesaleAvailable) {
binding.containerWholesale.visibility = View.VISIBLE
binding.tvPriceWholesale.text = "Rp${formatCurrency(product.wholesalePrice!!.toDouble())}"
binding.descMinOrder.text = "Minimal pembelian ${minOrder}"
} else {
binding.containerWholesale.visibility = View.GONE
}
binding.btnChat.setOnClickListener{
navigateToChat()
}
binding.btnBuyNow.setOnClickListener {
viewModel.productDetail.value?.productId?.let { id ->
showBuyNowPopup(id)
@ -205,24 +250,6 @@ class DetailProductActivity : AppCompatActivity() {
}
}
setupRecyclerViewOtherProducts()
}
private fun updateUI(product: Product){
binding.tvProductName.text = product.productName
binding.tvPrice.text = formatCurrency(product.price.toDouble())
binding.tvSold.text = product.totalSold.toString()
binding.tvRating.text = product.rating
binding.tvWeight.text = product.weight.toString()
binding.tvStock.text = product.stock.toString()
binding.tvCategory.text = product.productCategory
binding.tvDescription.text = product.description
binding.btnChat.setOnClickListener{
navigateToChat()
}
val fullImageUrl = when (val img = product.image) {
is String -> {
if (img.startsWith("/")) BASE_URL + img.substring(1) else img
@ -296,6 +323,7 @@ class DetailProductActivity : AppCompatActivity() {
private fun showAddToCartPopup(productId: Int) {
showQuantityDialog(productId, false)
}
private fun showQuantityDialog(productId: Int, isBuyNow: Boolean) {
@ -303,26 +331,53 @@ class DetailProductActivity : AppCompatActivity() {
val view = layoutInflater.inflate(R.layout.dialog_count_buy, null)
bottomSheetDialog.setContentView(view)
val btnDecrease = view.findViewById<Button>(R.id.btnDecrease)
val btnIncrease = view.findViewById<Button>(R.id.btnIncrease)
val btnDecrease = view.findViewById<ImageButton>(R.id.btnDecrease)
val btnIncrease = view.findViewById<ImageButton>(R.id.btnIncrease)
val tvQuantity = view.findViewById<TextView>(R.id.tvQuantity)
val btnBuyNow = view.findViewById<Button>(R.id.btnBuyNow)
val btnClose = view.findViewById<ImageButton>(R.id.btnCloseDialog)
// Set button text based on action
val switchWholesale = view.findViewById<SwitchCompat>(R.id.switch_price)
if (!isBuyNow) {
btnBuyNow.setText(R.string.add_to_cart)
}
currentQuantity = 1
switchWholesale.isEnabled = isWholesaleAvailable
switchWholesale.isChecked = isWholesaleSelected
// Set initial quantity based on current selection
currentQuantity = if (isWholesaleSelected) minOrder else 1
tvQuantity.text = currentQuantity.toString()
switchWholesale.setOnCheckedChangeListener { _, isChecked ->
isWholesaleSelected = isChecked
// Reset quantity when switching between retail and wholesale
if (isChecked) {
currentQuantity = minOrder
} else {
currentQuantity = 1
}
tvQuantity.text = currentQuantity.toString()
}
val maxStock = viewModel.productDetail.value?.stock ?: 1
btnDecrease.setOnClickListener {
if (currentQuantity > 1) {
currentQuantity--
tvQuantity.text = currentQuantity.toString()
if (isWholesaleSelected) {
if (currentQuantity > minOrder) {
currentQuantity--
tvQuantity.text = currentQuantity.toString()
} else {
Toast.makeText(this, "Sudah mencapai jumlah minimum", Toast.LENGTH_SHORT).show()
}
} else {
if (currentQuantity > 1) {
currentQuantity--
tvQuantity.text = currentQuantity.toString()
}
}
}
@ -331,7 +386,7 @@ class DetailProductActivity : AppCompatActivity() {
currentQuantity++
tvQuantity.text = currentQuantity.toString()
} else {
Toast.makeText(this, "Maximum stock reached", Toast.LENGTH_SHORT).show()
Toast.makeText(this, "Sudah mencapai jumlah maksimum", Toast.LENGTH_SHORT).show()
}
}
@ -339,10 +394,8 @@ class DetailProductActivity : AppCompatActivity() {
bottomSheetDialog.dismiss()
if (isBuyNow) {
// If it's Buy Now, navigate directly to checkout without adding to cart
navigateToCheckout()
} else {
// If it's Add to Cart, add the item to the cart
val cartItem = CartItem(
productId = productId,
quantity = currentQuantity
@ -372,17 +425,34 @@ class DetailProductActivity : AppCompatActivity() {
return
}
// Start checkout activity with buy now flow
CheckoutActivity.startForBuyNow(
context = this,
storeId = productDetail.storeId,
storeName = storeDetail.data.storeName,
productId = productDetail.productId,
productName = productDetail.productName,
productImage = productDetail.image,
quantity = currentQuantity,
price = productDetail.price.toDouble()
)
if (isWholesaleSelected) {
// Start checkout activity with buy now flow
//checkout klo grosiran
CheckoutActivity.startForBuyNow(
context = this,
storeId = productDetail.storeId,
storeName = storeDetail.data.storeName,
productId = productDetail.productId,
productName = productDetail.productName,
productImage = productDetail.image,
quantity = currentQuantity,
price = productDetail.wholesalePrice!!.toDouble(),
isWholesale = true
)
} else {
//checkout klo direct buy normal price
CheckoutActivity.startForBuyNow(
context = this,
storeId = productDetail.storeId,
storeName = storeDetail.data.storeName,
productId = productDetail.productId,
productName = productDetail.productName,
productImage = productDetail.image,
quantity = currentQuantity,
price = productDetail.price.toDouble(),
isWholesale = false
)
}
}
private fun navigateToChat(){
@ -402,7 +472,8 @@ class DetailProductActivity : AppCompatActivity() {
productImage = productDetail.image,
productRating = productDetail.rating,
storeName = storeDetail.data.storeName,
chatRoomId = 0
chatRoomId = 0,
storeImage = storeDetail.data.storeImage
)
}

View File

@ -1,21 +1,29 @@
package com.alya.ecommerce_serang.ui.profile
import android.app.Activity
import android.content.Intent
import android.os.Bundle
import android.util.Log
import android.widget.Toast
import androidx.activity.enableEdgeToEdge
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import com.alya.ecommerce_serang.BuildConfig.BASE_URL
import com.alya.ecommerce_serang.R
import com.alya.ecommerce_serang.data.api.dto.UserProfile
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.UserRepository
import com.alya.ecommerce_serang.databinding.ActivityDetailProfileBinding
import com.alya.ecommerce_serang.ui.profile.editprofile.EditProfileCustActivity
import com.alya.ecommerce_serang.utils.BaseViewModelFactory
import com.alya.ecommerce_serang.utils.SessionManager
import com.alya.ecommerce_serang.utils.viewmodel.ProfileViewModel
import com.bumptech.glide.Glide
import com.google.gson.Gson
import java.text.SimpleDateFormat
import java.util.Locale
import java.util.TimeZone
@ -24,6 +32,8 @@ class DetailProfileActivity : AppCompatActivity() {
private lateinit var binding: ActivityDetailProfileBinding
private lateinit var apiService: ApiService
private lateinit var sessionManager: SessionManager
private var currentUserProfile: UserProfile? = null
private val viewModel: ProfileViewModel by viewModels {
BaseViewModelFactory {
@ -33,6 +43,15 @@ class DetailProfileActivity : AppCompatActivity() {
}
}
private val editProfileLauncher = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) { result ->
if (result.resultCode == Activity.RESULT_OK) {
// Refresh profile after edit
viewModel.loadUserProfile()
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityDetailProfileBinding.inflate(layoutInflater)
@ -42,24 +61,61 @@ class DetailProfileActivity : AppCompatActivity() {
apiService = ApiConfig.getApiService(sessionManager)
enableEdgeToEdge()
// ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
// val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
// v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
// insets
// }
setupClickListeners()
ViewCompat.setOnApplyWindowInsetsListener(binding.root) { view, windowInsets ->
val systemBars = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
view.setPadding(
systemBars.left,
systemBars.top,
systemBars.right,
systemBars.bottom
)
windowInsets
}
viewModel.loadUserProfile()
viewModel.userProfile.observe(this){ user ->
user?.let { updateProfile(it) }
viewModel.userProfile.observe(this) { user ->
Log.d("DetailProfileActivity", "Observed userProfile: $user")
user?.let {
updateProfile(it)
} ?: run {
Log.e("DetailProfileActivity", "Received null user profile from ViewModel")
}
}
viewModel.errorMessage.observe(this) { error ->
Log.e("DetailProfileActivity", "Error from ViewModel: $error")
Toast.makeText(this, error, Toast.LENGTH_SHORT).show()
}
}
private fun updateProfile(user: UserProfile){
private fun setupClickListeners() {
binding.btnBack.setOnClickListener {
finish()
}
binding.btnUbahProfil.setOnClickListener {
currentUserProfile?.let { profile ->
val gson = Gson()
val userProfileJson = gson.toJson(currentUserProfile)
val intent = Intent(this, EditProfileCustActivity::class.java).apply {
putExtra("user_profile_json", userProfileJson)
}
editProfileLauncher.launch(intent)
} ?: run {
Toast.makeText(this, "Profile data is not available", Toast.LENGTH_SHORT).show()
}
}
}
private fun updateProfile(user: UserProfile) {
Log.d("DetailProfileActivity", "updateProfile called with user: $user")
// Store the user profile for later use
currentUserProfile = user
binding.tvNameUser.setText(user.name.toString())
binding.tvUsername.setText(user.username)
@ -69,9 +125,16 @@ class DetailProfileActivity : AppCompatActivity() {
Log.d("ProfileActivity", "Formatted Birth Date: ${formatDate(user.birthDate)}")
binding.tvNumberPhoneUser.setText(user.phone)
if (user.image != null && user.image is String) {
val fullImageUrl = when (val img = user.image) {
is String -> {
if (img.startsWith("/")) BASE_URL + img.substring(1) else img
}
else -> R.drawable.placeholder_image // Default image for null
}
if (fullImageUrl != null && fullImageUrl is String) {
Glide.with(this)
.load(user.image)
.load(fullImageUrl)
.placeholder(R.drawable.baseline_account_circle_24)
.into(binding.profileImage)
}
@ -93,4 +156,10 @@ class DetailProfileActivity : AppCompatActivity() {
}
}
override fun onResume() {
super.onResume()
// Refresh profile data when returning to this screen
viewModel.loadUserProfile()
}
}

View File

@ -1,5 +1,7 @@
package com.alya.ecommerce_serang.ui.profile
import android.app.AlertDialog
import android.app.ProgressDialog
import android.content.Intent
import android.os.Bundle
import android.util.Log
@ -9,18 +11,24 @@ import android.view.ViewGroup
import android.widget.Toast
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope
import com.alya.ecommerce_serang.BuildConfig.BASE_URL
import com.alya.ecommerce_serang.R
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.auth.LoginActivity
import com.alya.ecommerce_serang.ui.auth.RegisterStoreActivity
import com.alya.ecommerce_serang.ui.order.address.AddressActivity
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
import com.alya.ecommerce_serang.utils.viewmodel.ProfileViewModel
import com.bumptech.glide.Glide
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
class ProfileFragment : Fragment() {
@ -53,10 +61,21 @@ class ProfileFragment : Fragment() {
super.onViewCreated(view, savedInstanceState)
observeUserProfile()
viewModel.loadUserProfile()
viewModel.checkStoreUser()
binding.cardBukaToko.setOnClickListener{
val intentBuka = Intent(requireContext(), MyStoreActivity::class.java)
startActivity(intentBuka)
val hasStore = viewModel.checkStore.value
// val hasStore = false
Log.d("Profile Fragment", "Check store $hasStore")
if (hasStore == true){
val intentBuka = Intent(requireContext(), MyStoreActivity::class.java)
startActivity(intentBuka)
} else {
val intentBuka = Intent(requireContext(), RegisterStoreActivity::class.java)
startActivity(intentBuka)
}
}
binding.btnDetailProfile.setOnClickListener{
@ -73,6 +92,16 @@ class ProfileFragment : Fragment() {
val intent = Intent(requireContext(), HistoryActivity::class.java)
startActivity(intent)
}
binding.cardLogout.setOnClickListener({
logout()
})
binding.cardAddress.setOnClickListener({
val intent = Intent(requireContext(), AddressActivity::class.java)
startActivity(intent)
})
}
private fun observeUserProfile() {
@ -103,4 +132,41 @@ class ProfileFragment : Fragment() {
.placeholder(R.drawable.placeholder_image)
.into(profileImage)
}
private fun logout(){
AlertDialog.Builder(requireContext())
.setTitle("Konfirmasi")
.setMessage("Apakah anda yakin ingin keluar?")
.setPositiveButton("Ya") { _, _ ->
actionLogout()
}
.setNegativeButton("Tidak", null)
.show()
}
private fun actionLogout(){
val loadingDialog = ProgressDialog(requireContext()).apply {
setMessage("Mohon ditunggu")
setCancelable(false)
show()
}
lifecycleScope.launch {
try {
delay(500)
loadingDialog.dismiss()
sessionManager.clearAll()
val intent = Intent(requireContext(), LoginActivity::class.java)
startActivity(intent)
} catch (e: Exception) {
Toast.makeText(
requireContext(),
"Gagal keluar: ${e.message}",
Toast.LENGTH_SHORT
).show()
}
}
}
}

View File

@ -0,0 +1,390 @@
package com.alya.ecommerce_serang.ui.profile.editprofile
import android.app.Activity
import android.app.DatePickerDialog
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Bundle
import android.provider.MediaStore
import android.util.Log
import android.widget.Toast
import androidx.activity.enableEdgeToEdge
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 androidx.core.view.ViewCompat
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat
import com.alya.ecommerce_serang.BuildConfig.BASE_URL
import com.alya.ecommerce_serang.R
import com.alya.ecommerce_serang.data.api.dto.UserProfile
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.Result
import com.alya.ecommerce_serang.data.repository.UserRepository
import com.alya.ecommerce_serang.databinding.ActivityEditProfileCustBinding
import com.alya.ecommerce_serang.utils.BaseViewModelFactory
import com.alya.ecommerce_serang.utils.SessionManager
import com.alya.ecommerce_serang.utils.viewmodel.ProfileViewModel
import com.bumptech.glide.Glide
import com.google.gson.Gson
import java.io.File
import java.text.SimpleDateFormat
import java.util.Calendar
import java.util.Locale
import java.util.TimeZone
class EditProfileCustActivity : AppCompatActivity() {
private lateinit var binding: ActivityEditProfileCustBinding
private lateinit var apiService: ApiService
private lateinit var sessionManager: SessionManager
private var selectedImageUri: Uri? = null
private val viewModel: ProfileViewModel by viewModels {
BaseViewModelFactory {
val apiService = ApiConfig.getApiService(sessionManager)
val userRepository = UserRepository(apiService)
ProfileViewModel(userRepository)
}
}
private val getContent = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
if (result.resultCode == Activity.RESULT_OK) {
val data: Intent? = result.data
data?.data?.let {
selectedImageUri = it
val fullImageUrl = when (val img = selectedImageUri.toString()) {
is String -> {
if (img.startsWith("/")) BASE_URL + img.substring(1) else img
}
else -> R.drawable.placeholder_image // Default image for null
}
Glide.with(this)
.load(fullImageUrl)
.into(binding.profileImage)
}
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityEditProfileCustBinding.inflate(layoutInflater)
setContentView(binding.root)
sessionManager = SessionManager(this)
WindowCompat.setDecorFitsSystemWindows(window, false)
enableEdgeToEdge()
// Apply insets to your root layout
ViewCompat.setOnApplyWindowInsetsListener(binding.root) { view, windowInsets ->
val systemBars = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
view.setPadding(
systemBars.left,
systemBars.top,
systemBars.right,
systemBars.bottom
)
windowInsets
}
val userProfileJson = intent.getStringExtra("user_profile_json")
val userProfile = if (userProfileJson != null) {
val gson = Gson()
gson.fromJson(userProfileJson, UserProfile::class.java)
} else {
null
}
userProfile?.let {
populateFields(it)
setupClickListeners()
observeViewModel()
}
}
private fun populateFields(profile: UserProfile) {
binding.etNameUser.setText(profile.name)
binding.etUsername.setText(profile.username)
binding.etEmailUser.setText(profile.email)
binding.etNumberPhoneUser.setText(profile.phone)
// Format birth date for display
profile.birthDate?.let {
binding.etDateBirth.setText(formatDate(it))
}
val fullImageUrl = when (val img = profile.image) {
is String -> {
if (img.startsWith("/")) BASE_URL + img.substring(1) else img
}
else -> R.drawable.placeholder_image // Default image for null
}
// Load profile image
if (fullImageUrl != null && fullImageUrl is String) {
Glide.with(this)
.load(fullImageUrl)
.placeholder(R.drawable.baseline_account_circle_24)
.into(binding.profileImage)
}
}
private fun setupClickListeners() {
binding.btnBack.setOnClickListener {
finish()
}
binding.editIcon.setOnClickListener {
openImagePicker()
}
binding.tvSelectImage.setOnClickListener {
openImagePicker()
}
binding.etDateBirth.setOnClickListener {
showDatePicker()
}
binding.btnSave.setOnClickListener {
saveProfile()
}
}
private fun openImagePicker() {
// Check for permission first
if (ContextCompat.checkSelfPermission(
this,
android.Manifest.permission.READ_EXTERNAL_STORAGE
) != PackageManager.PERMISSION_GRANTED
) {
ActivityCompat.requestPermissions(
this,
arrayOf(android.Manifest.permission.READ_EXTERNAL_STORAGE),
REQUEST_STORAGE_PERMISSION
)
} else {
launchImagePicker()
}
}
private fun launchImagePicker() {
val intent = Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI)
getContent.launch(intent)
}
private fun showDatePicker() {
val calendar = Calendar.getInstance()
// If there's already a date in the field, parse it
val dateText = binding.etDateBirth.text.toString()
if (dateText.isNotEmpty() && dateText != "N/A" && dateText != "Invalid Date") {
try {
val displayFormat = SimpleDateFormat("dd-MM-yy", Locale.getDefault())
val date = displayFormat.parse(dateText)
date?.let {
calendar.time = it
}
} catch (e: Exception) {
Log.e(TAG, "Error parsing date: ${e.message}")
}
}
val year = calendar.get(Calendar.YEAR)
val month = calendar.get(Calendar.MONTH)
val day = calendar.get(Calendar.DAY_OF_MONTH)
val datePickerDialog = DatePickerDialog(
this,
{ _, selectedYear, selectedMonth, selectedDay ->
calendar.set(selectedYear, selectedMonth, selectedDay)
val displayFormat = SimpleDateFormat("dd-MM-yy", Locale.getDefault())
val formattedDate = displayFormat.format(calendar.time)
binding.etDateBirth.setText(formattedDate)
},
year, month, day
)
datePickerDialog.show()
}
private fun saveProfile() {
val name = binding.etNameUser.text.toString()
val username = binding.etUsername.text.toString()
val email = binding.etEmailUser.text.toString()
val phone = binding.etNumberPhoneUser.text.toString()
val displayDate = binding.etDateBirth.text.toString()
if (name.isEmpty() || username.isEmpty() || email.isEmpty() || phone.isEmpty() || displayDate.isEmpty()) {
Toast.makeText(this, "Semua field harus diisi", Toast.LENGTH_SHORT).show()
return
}
// Convert date to server format
val serverBirthDate = convertToServerDateFormat(displayDate)
Log.d(TAG, "Starting profile save with direct method")
Log.d(TAG, "Selected image URI: $selectedImageUri")
// Disable the button to prevent multiple clicks
binding.btnSave.isEnabled = false
// Call the repository method via ViewModel
viewModel.editProfileDirect(
context = this, // Pass context for file operations
username = username,
name = name,
phone = phone,
birthDate = serverBirthDate,
email = email,
imageUri = selectedImageUri
)
}
private fun getRealPathFromURI(uri: Uri): String? {
Log.d(TAG, "Getting real path from URI: $uri")
// Handle different URI schemes
when {
// File URI
uri.scheme == "file" -> {
val path = uri.path
Log.d(TAG, "URI is file scheme, path: $path")
return path
}
// Content URI
uri.scheme == "content" -> {
try {
val projection = arrayOf(MediaStore.Images.Media.DATA)
contentResolver.query(uri, projection, null, null, null)?.use { cursor ->
if (cursor.moveToFirst()) {
val columnIndex = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA)
val path = cursor.getString(columnIndex)
Log.d(TAG, "Found path from content URI: $path")
return path
} else {
Log.e(TAG, "Cursor is empty")
}
} ?: Log.e(TAG, "Cursor is null")
// If the above fails, try the documented API way
contentResolver.openInputStream(uri)?.use { inputStream ->
// Create a temp file
val fileName = getFileName(uri) ?: "temp_img_${System.currentTimeMillis()}.jpg"
val tempFile = File(cacheDir, fileName)
tempFile.outputStream().use { outputStream ->
inputStream.copyTo(outputStream)
}
Log.d(TAG, "Created temporary file: ${tempFile.absolutePath}")
return tempFile.absolutePath
}
} catch (e: Exception) {
Log.e(TAG, "Error getting real path: ${e.message}", e)
}
}
}
Log.e(TAG, "Could not get real path for URI: $uri")
return null
}
private fun getFileName(uri: Uri): String? {
var result: String? = null
if (uri.scheme == "content") {
contentResolver.query(uri, null, null, null, null)?.use { cursor ->
if (cursor.moveToFirst()) {
val columnIndex = cursor.getColumnIndex(MediaStore.Images.Media.DISPLAY_NAME)
if (columnIndex >= 0) {
result = cursor.getString(columnIndex)
Log.d(TAG, "Found filename from content URI: $result")
}
}
}
}
if (result == null) {
result = uri.path
val cut = result?.lastIndexOf('/') ?: -1
if (cut != -1) {
result = result?.substring(cut + 1)
}
Log.d(TAG, "Extracted filename from path: $result")
}
return result
}
private fun formatDate(dateString: String?): String {
if (dateString.isNullOrEmpty()) return "N/A"
return try {
val inputFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.getDefault())
inputFormat.timeZone = TimeZone.getTimeZone("UTC")
val outputFormat = SimpleDateFormat("dd-MM-yy", Locale.getDefault())
val date = inputFormat.parse(dateString)
outputFormat.format(date ?: return "Invalid Date")
} catch (e: Exception) {
Log.e("ERROR", "Date parsing error: ${e.message}")
"Invalid Date"
}
}
private fun convertToServerDateFormat(displayDate: String): String {
return try {
val displayFormat = SimpleDateFormat("dd-MM-yy", Locale.getDefault())
val date = displayFormat.parse(displayDate) ?: return ""
val serverFormat = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault())
serverFormat.format(date)
} catch (e: Exception) {
Log.e(TAG, "Error converting date format: ${e.message}")
""
}
}
private fun observeViewModel() {
viewModel.editProfileResult.observe(this) { result ->
when (result) {
is com.alya.ecommerce_serang.data.repository.Result.Loading -> {
// Show loading indicator
binding.btnSave.isEnabled = false
}
is com.alya.ecommerce_serang.data.repository.Result.Success -> {
// Show success message
Toast.makeText(this, result.data.message, Toast.LENGTH_SHORT).show()
setResult(Activity.RESULT_OK)
finish()
}
is Result.Error -> {
// Show error message
Toast.makeText(this, result.exception.message ?: "Error updating profile", Toast.LENGTH_SHORT).show()
binding.btnSave.isEnabled = true
}
}
}
}
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if (requestCode == REQUEST_STORAGE_PERMISSION && grantResults.isNotEmpty() && grantResults[0] == android.content.pm.PackageManager.PERMISSION_GRANTED) {
launchImagePicker()
} else {
Toast.makeText(this, "Permission needed to select image", Toast.LENGTH_SHORT).show()
}
}
companion object {
private const val REQUEST_STORAGE_PERMISSION = 100
private const val TAG = "EditProfileCustActivity"
}
}

View File

@ -7,6 +7,7 @@ import android.widget.Toast
import androidx.activity.enableEdgeToEdge
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import com.alya.ecommerce_serang.BuildConfig.BASE_URL
import com.alya.ecommerce_serang.R
import com.alya.ecommerce_serang.data.api.dto.Store
import com.alya.ecommerce_serang.data.api.retrofit.ApiConfig
@ -19,12 +20,10 @@ import com.alya.ecommerce_serang.ui.profile.mystore.product.ProductActivity
import com.alya.ecommerce_serang.ui.profile.mystore.profile.DetailStoreProfileActivity
import com.alya.ecommerce_serang.ui.profile.mystore.review.ReviewFragment
import com.alya.ecommerce_serang.ui.profile.mystore.sells.SellsActivity
import com.alya.ecommerce_serang.ui.profile.mystore.sells.SellsListFragment
import com.alya.ecommerce_serang.utils.BaseViewModelFactory
import com.alya.ecommerce_serang.utils.SessionManager
import com.alya.ecommerce_serang.utils.viewmodel.MyStoreViewModel
import com.bumptech.glide.Glide
import kotlin.getValue
class MyStoreActivity : AppCompatActivity() {
private lateinit var binding: ActivityMyStoreBinding
@ -68,7 +67,7 @@ class MyStoreActivity : AppCompatActivity() {
binding.tvStoreType.text = store.storeType
if (store.storeImage != null && store.storeImage.toString().isNotEmpty() && store.storeImage.toString() != "null") {
val imageUrl = "http://192.168.100.156:3000${store.storeImage}"
val imageUrl = "$BASE_URL${store.storeImage}"
Log.d("MyStoreActivity", "Loading store image from: $imageUrl")
Glide.with(this)

View File

@ -20,6 +20,7 @@ object Constants {
const val EXTRA_PRODUCT_PRICE = "product_price"
const val EXTRA_PRODUCT_IMAGE = "product_image"
const val EXTRA_PRODUCT_RATING = "product_rating"
const val EXTRA_STORE_IMAGE = "store_image"
// Request codes
const val REQUEST_IMAGE_PICK = 1001

View File

@ -0,0 +1,88 @@
package com.alya.ecommerce_serang.utils
import android.content.Context
import android.net.Uri
import android.util.Log
import android.webkit.MimeTypeMap
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.MultipartBody
import okhttp3.RequestBody.Companion.asRequestBody
import java.io.File
import java.io.FileOutputStream
object FileUtils {
private const val TAG = "FileUtils"
/**
* Creates a temporary file from a URI in the app's cache directory
*/
fun createTempFileFromUri(context: Context, uri: Uri, prefix: String = "temp"): File? {
try {
val fileExtension = getFileExtension(context, uri)
val fileName = "${prefix}_${System.currentTimeMillis()}.$fileExtension"
val tempFile = File(context.cacheDir, fileName)
context.contentResolver.openInputStream(uri)?.use { inputStream ->
FileOutputStream(tempFile).use { outputStream ->
inputStream.copyTo(outputStream)
}
}
return if (tempFile.exists() && tempFile.length() > 0) {
Log.d(TAG, "Created temp file: ${tempFile.absolutePath}, size: ${tempFile.length()} bytes")
tempFile
} else {
Log.e(TAG, "Created file is empty or doesn't exist")
null
}
} catch (e: Exception) {
Log.e(TAG, "Error creating temp file: ${e.message}", e)
return null
}
}
/**
* Gets the file extension from a URI using ContentResolver
*/
fun getFileExtension(context: Context, uri: Uri): String {
val mimeType = context.contentResolver.getType(uri)
return if (mimeType != null) {
MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType) ?: "jpg"
} else {
// Try to extract from the URI path
val path = uri.toString()
if (path.contains(".")) {
path.substring(path.lastIndexOf(".") + 1)
} else {
"jpg" // Default extension
}
}
}
/**
* Creates a MultipartBody.Part from a File for API requests
*/
fun createMultipartFromFile(paramName: String, file: File): MultipartBody.Part {
val requestFile = file.asRequestBody(getMimeType(file).toMediaTypeOrNull())
return MultipartBody.Part.createFormData(paramName, file.name, requestFile)
}
/**
* Creates an empty MultipartBody.Part
*/
fun createEmptyMultipart(paramName: String): MultipartBody.Part {
return MultipartBody.Part.createFormData(paramName, "")
}
/**
* Gets the MIME type for a file based on its extension
*/
fun getMimeType(file: File): String {
return when (file.extension.lowercase()) {
"jpg", "jpeg" -> "image/jpeg"
"png" -> "image/png"
"pdf" -> "application/pdf"
else -> "application/octet-stream"
}
}
}

View File

@ -57,8 +57,6 @@ class SessionManager(context: Context) {
}
}
//clear data when log out
fun clearAll() {
sharedPreferences.edit() {

View File

@ -1,18 +1,38 @@
package com.alya.ecommerce_serang.utils.viewmodel
import android.content.Context
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.FcmReq
import com.alya.ecommerce_serang.data.api.response.auth.FcmTokenResponse
import com.alya.ecommerce_serang.data.api.response.auth.LoginResponse
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.Result
import com.alya.ecommerce_serang.data.repository.UserRepository
import com.alya.ecommerce_serang.utils.SessionManager
import kotlinx.coroutines.launch
class LoginViewModel(private val repository: UserRepository) : ViewModel() {
class LoginViewModel(private val repository: UserRepository, private val context: Context) : ViewModel() {
private val _loginState = MutableLiveData<Result<LoginResponse>>()
val loginState: LiveData<Result<LoginResponse>> get() = _loginState
private val _otpState = MutableLiveData<Result<Unit>>()
val otpState: LiveData<Result<Unit>> = _otpState
// MutableLiveData to store messages from API responses
private val _message = MutableLiveData<String>()
val message: LiveData<String> = _message
private val sessionManager by lazy { SessionManager(context) }
private fun getAuthenticatedApiService(): ApiService {
return ApiConfig.getApiService(sessionManager)
}
fun login(email: String, password: String) {
viewModelScope.launch {
_loginState.value = Result.Loading
@ -20,4 +40,33 @@ class LoginViewModel(private val repository: UserRepository) : ViewModel() {
_loginState.value = result
}
}
fun sendFcm(token: FcmReq) {
viewModelScope.launch {
_otpState.value = Result.Loading // Indicating API call in progress
try {
// Call the repository function to request OTP
val authenticatedApiService = getAuthenticatedApiService()
val authenticatedOrderRepo = UserRepository(authenticatedApiService)
val response: FcmTokenResponse = authenticatedOrderRepo.sendFcm(token)
// Log and store success message
Log.d("LoginViewModel", "OTP Response: ${response.message}")
_message.value = response.message ?: "berhasil" // Store the message for UI feedback
// Update state to indicate success
_otpState.value = Result.Success(Unit)
} catch (exception: Exception) {
// Handle any errors and update state
_otpState.value = Result.Error(exception)
_message.value = exception.localizedMessage ?: "Failed to request OTP"
// Log the error for debugging
Log.e("LoginViewModel", "OTP request failed for: $token", exception)
}
}
}
}

View File

@ -1,10 +1,15 @@
package com.alya.ecommerce_serang.utils.viewmodel
import android.content.Context
import android.net.Uri
import android.util.Log
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.alya.ecommerce_serang.data.api.dto.UserProfile
import com.alya.ecommerce_serang.data.api.response.auth.HasStoreResponse
import com.alya.ecommerce_serang.data.api.response.customer.profile.EditProfileResponse
import com.alya.ecommerce_serang.data.repository.Result
import com.alya.ecommerce_serang.data.repository.UserRepository
import kotlinx.coroutines.launch
@ -16,6 +21,15 @@ class ProfileViewModel(private val userRepository: UserRepository) : ViewModel()
private val _errorMessage = MutableLiveData<String>()
val errorMessage : LiveData<String> = _errorMessage
private val _editProfileResult = MutableLiveData<Result<EditProfileResponse>>()
val editProfileResult: LiveData<Result<EditProfileResponse>> = _editProfileResult
private val _checkStore = MutableLiveData<Boolean>()
val checkStore: LiveData<Boolean> = _checkStore
private val _logout = MutableLiveData<Boolean>()
val logout : LiveData<Boolean> = _checkStore
fun loadUserProfile(){
viewModelScope.launch {
when (val result = userRepository.fetchUserProfile()){
@ -25,4 +39,79 @@ class ProfileViewModel(private val userRepository: UserRepository) : ViewModel()
}
}
}
fun checkStoreUser(){
viewModelScope.launch {
try {
// Call the repository function to request OTP
val response: HasStoreResponse = userRepository.checkStore()
// Log and store success message
Log.d("RegisterViewModel", "OTP Response: ${response.hasStore}")
_checkStore.value = response.hasStore // Store the message for UI feedback
} catch (exception: Exception) {
// Handle any errors and update state
_checkStore.value = false
// Log the error for debugging
Log.e("RegisterViewModel", "Error:", exception)
}
}
}
fun editProfileDirect(
context: Context,
username: String,
name: String,
phone: String,
birthDate: String,
email: String,
imageUri: Uri?
) {
_editProfileResult.value = Result.Loading
viewModelScope.launch {
try {
Log.d(TAG, "Calling editProfileCust with direct parameters")
val result = userRepository.editProfileCust(
context = context,
username = username,
name = name,
phone = phone,
birthDate = birthDate,
email = email,
imageUri = imageUri
)
_editProfileResult.value = result
// Reload user profile after successful update
if (result is Result.Success) {
Log.d(TAG, "Edit profile successful, reloading profile data")
loadUserProfile()
}
} catch (e: Exception) {
Log.e(TAG, "Error in editProfileDirect: ${e.message}")
e.printStackTrace()
_editProfileResult.value = Result.Error(e)
}
}
}
fun logout(){
viewModelScope.launch {
try{
} catch (e: Exception){
}
}
}
companion object {
private const val TAG = "ProfileViewModel"
}
}

View File

@ -1,18 +1,49 @@
package com.alya.ecommerce_serang.utils.viewmodel
import android.content.Context
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.CreateAddressRequest
import com.alya.ecommerce_serang.data.api.dto.RegisterRequest
import com.alya.ecommerce_serang.data.api.dto.VerifRegisReq
import com.alya.ecommerce_serang.data.api.response.auth.LoginResponse
import com.alya.ecommerce_serang.data.api.response.auth.OtpResponse
import com.alya.ecommerce_serang.data.api.response.auth.RegisterResponse
import com.alya.ecommerce_serang.data.api.response.auth.User
import com.alya.ecommerce_serang.data.api.response.auth.VerifRegisterResponse
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.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.Result
import com.alya.ecommerce_serang.data.repository.UserRepository
import com.alya.ecommerce_serang.ui.order.address.ViewState
import com.alya.ecommerce_serang.utils.SessionManager
import kotlinx.coroutines.launch
class RegisterViewModel(private val repository: UserRepository) : ViewModel() {
class RegisterViewModel(private val repository: UserRepository, private val orderRepo: OrderRepository, private val context: Context) : ViewModel() {
private val _loginState = MutableLiveData<Result<LoginResponse>>()
val loginState: LiveData<Result<LoginResponse>> get() = _loginState
// To track if user is authenticated
private val _isAuthenticated = MutableLiveData<Boolean>(false)
val isAuthenticated: LiveData<Boolean> = _isAuthenticated
private var _lastCheckedField = MutableLiveData<String>()
val lastCheckedField: String
get() = _lastCheckedField.value ?: ""
private val _userData = MutableLiveData<RegisterRequest>()
val userData: LiveData<RegisterRequest> = _userData
// Current step in the registration process
private val _currentStep = MutableLiveData<Int>(1)
val currentStep: LiveData<Int> = _currentStep
// MutableLiveData for handling register state (Loading, Success, or Error)
private val _registerState = MutableLiveData<Result<String>>()
val registerState: LiveData<Result<String>> = _registerState
@ -21,22 +52,61 @@ class RegisterViewModel(private val repository: UserRepository) : ViewModel() {
private val _otpState = MutableLiveData<Result<Unit>>()
val otpState: LiveData<Result<Unit>> = _otpState
private val _checkValue = MutableLiveData<Result<Boolean>>()
val checkValue: LiveData<Result<Boolean>> = _checkValue
// MutableLiveData to store messages from API responses
private val _message = MutableLiveData<String>()
val message: LiveData<String> = _message
private val _registeredUser = MutableLiveData<User>()
val registeredUser: LiveData<User> = _registeredUser
// For address data
var selectedProvinceId: Int? = null
var selectedCityId: Int? = null
// For provinces and cities
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
// For address submission
private val _addressSubmissionState = MutableLiveData<ViewState<String>>()
val addressSubmissionState: LiveData<ViewState<String>> = _addressSubmissionState
private val sessionManager by lazy { SessionManager(context) }
// For authenticated API calls
private fun getAuthenticatedApiService(): ApiService {
return ApiConfig.getApiService(sessionManager)
}
fun updateUserData(updatedData: RegisterRequest) {
_userData.value = updatedData
}
// Set current step
fun setStep(step: Int) {
_currentStep.value = step
}
/**
* Function to request OTP by sending an email to the API.
* - It sets the OTP state to `Loading` before calling the repository.
* - If successful, it updates `_message` with the response message and signals success.
* - If an error occurs, it updates `_otpState` with `Result.Error` and logs the failure.
*/
fun requestOtp(email: String) {
viewModelScope.launch {
_otpState.value = Result.Loading // Indicating API call in progress
try {
// Call the repository function to request OTP
// val authenticatedApiService = getAuthenticatedApiService()
// val authenticatedOrderRepo = UserRepository(authenticatedApiService)
val response: OtpResponse = repository.requestOtpRep(email)
// Log and store success message
@ -70,20 +140,179 @@ class RegisterViewModel(private val repository: UserRepository) : ViewModel() {
try {
// Call repository function to register the user
val message = repository.registerUser(request)
val response: RegisterResponse = repository.registerUser(request)
// Store and display success message
_message.value = message
_registerState.value = Result.Success(message) // Store success result
Log.d(TAG, "Registration API call successful")
Log.d(TAG, "Response message: ${response.message}")
Log.d(TAG, "User ID received: ${response.user.id}")
Log.d(TAG, "User details - Name: ${response.user.name}, Email: ${response.user.email}, Username: ${response.user.username}")
// Store the user data
_registeredUser.value = response.user
Log.d(TAG, "User data stored in ViewModel")
// Store success message
_message.value = response.message
Log.d(TAG, "Success message stored: ${response.message}")
_registerState.value = Result.Success(response.message)
// Automatically login after successful registration
request.email?.let { email ->
request.password?.let { password ->
Log.d(TAG, "Attempting auto-login with email: $email")
login(email, password)
}
}
} catch (exception: Exception) {
Log.e(TAG, "Registration failed with exception: ${exception.javaClass.simpleName}", exception)
Log.e(TAG, "Exception message: ${exception.message}")
Log.e(TAG, "Exception cause: ${exception.cause}")
// Handle any errors and update state
_registerState.value = Result.Error(exception)
_message.value = exception.localizedMessage ?: "Registration failed"
Log.d(TAG, "Error message stored: ${exception.localizedMessage ?: "Registration failed"}")
// Log the error for debugging
Log.e("RegisterViewModel", "User registration failed", exception)
}
}
}
fun login(email: String, password: String) {
viewModelScope.launch {
_loginState.value = Result.Loading
try {
val result = repository.login(email, password)
_loginState.value = result
// Update authentication status if login was successful
if (result is Result.Success) {
_isAuthenticated.value = true
}
} catch (exception: Exception) {
_loginState.value = Result.Error(exception)
Log.e("RegisterViewModel", "Login failed", exception)
}
}
}
fun checkValueReg(request: VerifRegisReq){
_lastCheckedField.value = request.fieldRegis
viewModelScope.launch {
try {
// Call the repository function to request OTP
val response: VerifRegisterResponse = repository.checkValue(request)
// Log and store success message
Log.d("RegisterViewModel", "OTP Response: ${response.available}")
_checkValue.value = Result.Success(response.available)// Store the message for UI feedback
} catch (exception: Exception) {
// Handle any errors and update state
_checkValue.value = Result.Error(exception)
// Log the error for debugging
Log.e("RegisterViewModel", "Error:", exception)
}
}
}
fun getProvinces() {
_provincesState.value = ViewState.Loading
viewModelScope.launch {
try {
val result = repository.getListProvinces()
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) {
_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.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) {
_citiesState.postValue(ViewState.Error(e.message ?: "Error loading cities"))
Log.e(TAG, "Error fetching cities for province $provinceId", e)
}
}
}
fun setSelectedProvinceId(id: Int) {
selectedProvinceId = id
}
fun setSelectedCityId(id: Int) {
selectedCityId = id
}
fun addAddress(request: CreateAddressRequest) {
Log.d(TAG, "Starting address submission process")
_addressSubmissionState.value = ViewState.Loading
viewModelScope.launch {
try {
val authenticatedApiService = getAuthenticatedApiService()
val authenticatedOrderRepo = OrderRepository(authenticatedApiService)
Log.d(TAG, "Calling repository.addAddress with request: $request")
val result = authenticatedOrderRepo.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
}
}
} 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))
}
}
}
companion object {
private const val TAG = "RegisterViewModel"
}
//require auth
}

View File

@ -1,4 +1,4 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#FFFFFF" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<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="M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/>

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="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM13,17h-2v-6h2v6zM13,9h-2L11,7h2v2z"/>
</vector>

View File

@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:autoMirrored="true" android:height="24dp" android:tint="#211E1E" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M17,7l-1.41,1.41L18.17,11H8v2h10.17l-2.58,2.58L17,17l5,-5zM4,5h8V3H4c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h8v-2H4V5z"/>
</vector>

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">
<solid android:color="@color/white" />
<corners
android:topLeftRadius="16dp"
android:topRightRadius="16dp" />
</shape>

View File

@ -0,0 +1,8 @@
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@color/white" />
<corners android:radius="8dp" />
<stroke
android:width="1dp"
android:color="@color/light_gray" />
</shape>

View File

@ -5,6 +5,128 @@
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".data.api.response.customer.cart.CartActivity">
tools:context=".ui.cart.CartActivity">
<include
android:id="@+id/header"
layout="@layout/header" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rvCart"
android:layout_width="match_parent"
android:layout_height="0dp"
android:background="#F5F5F5"
app:layout_constraintBottom_toTopOf="@+id/bottomCheckoutLayout"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/header"/>
<!-- Bottom Checkout Layout -->
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/bottomCheckoutLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@android:color/white"
android:elevation="8dp"
android:padding="16dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent">
<CheckBox
android:id="@+id/cbSelectAll"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Semua"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/tvTotalLabel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Total: "
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@+id/cbSelectAll"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/tvTotalPrice"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Rp0"
android:textColor="@android:color/black"
android:textSize="16sp"
android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@+id/tvTotalLabel"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="@+id/btnCheckout"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/bg_button_outline"
android:paddingStart="24dp"
android:paddingEnd="24dp"
android:text="Beli (0)"
android:textColor="@android:color/white"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<!-- Empty State View (Shown when cart is empty) -->
<LinearLayout
android:id="@+id/emptyStateLayout"
android:layout_width="match_parent"
android:layout_height="0dp"
android:gravity="center"
android:orientation="vertical"
android:visibility="gone"
app:layout_constraintBottom_toTopOf="@+id/bottomCheckoutLayout"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/header">
<ImageView
android:layout_width="120dp"
android:layout_height="120dp"
android:src="@drawable/outline_shopping_cart_24" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="Keranjang Anda kosong"
android:textColor="@android:color/black"
android:textSize="18sp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:gravity="center"
android:paddingStart="32dp"
android:paddingEnd="32dp"
android:text="Silakan tambahkan produk ke keranjang"
android:textColor="#757575"
android:textSize="14sp" />
<Button
android:id="@+id/btnShopNow"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:background="@drawable/bg_button_outline"
android:paddingStart="24dp"
android:paddingEnd="24dp"
android:text="Belanja Sekarang"
android:textColor="@android:color/white" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,72 @@
<?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.review.CreateReviewActivity">
<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="@color/white">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageButton
android:id="@+id/btnBack"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackgroundBorderless"
android:src="@drawable/ic_back_24"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Tambah Ulasan"
android:textColor="@color/black"
android:textSize="18sp"
android:layout_marginHorizontal="8dp"
android:fontFamily="@font/dmsans_medium"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@+id/btnBack"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/btnSubmitReview"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:text="Kirim Ulasan"
android:fontFamily="@font/dmsans_semibold"
android:textColor="@color/blue_500"
android:textSize="14sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.appcompat.widget.Toolbar>
</com.google.android.material.appbar.AppBarLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rvReviewItems"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@+id/appBarLayout" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,429 @@
<?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.detailorder.DetailOrderStatusActivity">
<include
android:id="@+id/header"
layout="@layout/header"/>
<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.core.widget.NestedScrollView
android:id="@+id/scrollViewDetail"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintBottom_toTopOf="@+id/buttonLayout"
app:layout_constraintTop_toBottomOf="@+id/header">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<!-- Status header -->
<!-- Order Status Card -->
<androidx.cardview.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
app:cardCornerRadius="8dp"
app:cardElevation="2dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="@+id/tvStatusHeader"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/blue_500"
android:paddingVertical="8dp"
android:paddingHorizontal="16dp"
android:text="Belum Bayar"
android:fontFamily="@font/dmsans_medium"
android:textColor="@color/white"
android:textSize="16sp"
android:textStyle="bold" />
<!-- Status Note (if any) -->
<TextView
android:id="@+id/tvStatusNote"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingHorizontal="16dp"
android:paddingVertical="8dp"
android:fontFamily="@font/dmsans_medium"
android:text="Pesanan ini harus dibayar sebelum 18 November 2024"
android:textColor="@color/blue_400"
android:textSize="14sp"
android:visibility="visible"/>
<!-- Order dates -->
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/white"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:paddingBottom="16dp">
<TextView
android:id="@+id/tvOrderDateLabel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginStart="8dp"
android:text="Tanggal Pesanan:"
android:textColor="@color/black"
android:textSize="14sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/tvOrderDate"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="15 November 2024"
android:textColor="@color/black"
android:textSize="14sp"
app:layout_constraintStart_toStartOf="@id/tvOrderDateLabel"
app:layout_constraintTop_toBottomOf="@id/tvOrderDateLabel" />
<TextView
android:id="@+id/tvPaymentDeadlineLabel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:text="Batas Pembayaran:"
android:textColor="@color/black"
android:textSize="14sp"
app:layout_constraintTop_toTopOf="@id/tvOrderDateLabel"
app:layout_constraintEnd_toEndOf="parent"/>
<TextView
android:id="@+id/tvPaymentDeadline"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="18 November 2024"
android:textColor="@color/black"
android:textSize="14sp"
app:layout_constraintStart_toStartOf="@id/tvPaymentDeadlineLabel"
app:layout_constraintTop_toTopOf="@id/tvOrderDate" />
</androidx.constraintlayout.widget.ConstraintLayout>
</LinearLayout>
</androidx.cardview.widget.CardView>
<View
android:layout_width="match_parent"
android:layout_height="8dp"
android:background="@color/light_gray" />
<!-- Shipping Information -->
<androidx.cardview.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
app:cardCornerRadius="8dp"
app:cardElevation="2dp">
<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="Informasi Pengiriman"
android:textColor="@color/black"
android:fontFamily="@font/dmsans_bold"
android:textSize="16sp"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:text="Alamat"
android:fontFamily="@font/dmsans_medium"
android:textColor="@color/black"
android:textSize="14sp" />
<TextView
android:id="@+id/tvRecipientName"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="Gracia 081234533453"
android:fontFamily="@font/dmsans_italic"
android:textColor="@color/black_400"
android:textSize="14sp" />
<TextView
android:id="@+id/tvAddress"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Jl. Pegangsaan Timur"
android:fontFamily="@font/dmsans_italic"
android:textColor="@color/black_400"
android:textSize="14sp" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="Kurir"
android:fontFamily="@font/dmsans_medium"
android:textColor="@color/black"
android:textSize="14sp" />
<TextView
android:id="@+id/tvCourier"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="JNE Reguler"
android:fontFamily="@font/dmsans_italic"
android:textColor="@color/black_400"
android:textSize="14sp" />
</LinearLayout>
</androidx.cardview.widget.CardView>
<View
android:layout_width="match_parent"
android:layout_height="8dp"
android:background="@color/light_gray" />
<!-- Order Items -->
<androidx.cardview.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
app:cardCornerRadius="8dp"
app:cardElevation="2dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:id="@+id/tvStoreName"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="SnackEnak"
android:fontFamily="@font/dmsans_semibold"
android:textColor="@color/black"
android:textSize="16sp"
android:textStyle="bold" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rvOrderItems"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:nestedScrollingEnabled="false"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:itemCount="2"
tools:listitem="@layout/item_order_detail_product" />
</LinearLayout>
</androidx.cardview.widget.CardView>
<!-- Payment Method Card -->
<androidx.cardview.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
app:cardCornerRadius="8dp"
app:cardElevation="2dp">
<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="Metode Pembayaran"
android:textColor="@color/black"
android:textSize="16sp"
android:textStyle="bold" />
<TextView
android:id="@+id/tvPaymentMethod"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="Bank Transfer - Bank BCA"
android:textColor="@color/black_400"
android:textSize="14sp" />
</LinearLayout>
</androidx.cardview.widget.CardView>
<!-- Order Summary Card -->
<androidx.cardview.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
app:cardCornerRadius="8dp"
app:cardElevation="2dp">
<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="Subtotal"
android:textColor="@color/black"
android:textSize="16sp"
android:textStyle="bold" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:orientation="horizontal">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="1 item"
android:textColor="@color/black_400"
android:textSize="14sp" />
<TextView
android:id="@+id/tvSubtotal"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Rp65.000"
android:textColor="@color/black"
android:textSize="14sp" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:orientation="horizontal">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Biaya Pengiriman"
android:textColor="@color/black_400"
android:textSize="14sp" />
<TextView
android:id="@+id/tvShippingCost"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Rp15.000"
android:textColor="@color/black"
android:textSize="14sp" />
</LinearLayout>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginTop="8dp"
android:background="@color/light_gray" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:orientation="horizontal">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Total"
android:textColor="@color/black"
android:textSize="16sp"
android:textStyle="bold" />
<TextView
android:id="@+id/tvTotal"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Rp75.000"
android:textColor="@color/blue_500"
android:textSize="16sp"
android:textStyle="bold" />
</LinearLayout>
</LinearLayout>
</androidx.cardview.widget.CardView>
<!-- Add space at the bottom for buttons -->
<View
android:layout_width="match_parent"
android:layout_height="80dp" />
</LinearLayout>
</androidx.core.widget.NestedScrollView>
<!-- Buttons at the bottom -->
<LinearLayout
android:id="@+id/buttonLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/white"
android:elevation="8dp"
android:orientation="horizontal"
android:padding="16dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@+id/scrollViewDetail">
<com.google.android.material.button.MaterialButton
android:id="@+id/btnSecondary"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:layout_weight="1"
android:text="Batalkan Pesanan"
android:textAllCaps="false"
android:visibility="gone"
tools:visibility="visible" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btnPrimary"
style="@style/Widget.MaterialComponents.Button"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_weight="1"
android:text="Bayar Sekarang"
android:textAllCaps="false"
android:visibility="gone"
tools:visibility="visible" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -37,6 +37,49 @@
android:contentDescription="@string/product_image"
tools:src="@drawable/placeholder_image" />
<androidx.cardview.widget.CardView
android:id="@+id/container_wholesale"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:backgroundTint="@color/blue_50"
android:visibility="gone"
app:cardElevation="0dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="16dp"
android:paddingVertical="4dp"
android:orientation="vertical">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:fontFamily="@font/dmsans_semibold"
android:text="Harga Grosir"/>
<TextView
android:id="@+id/tvPriceWholesale"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/blue_500"
android:text="Rp50.000"
android:fontFamily="@font/dmsans_semibold"/>
</LinearLayout>
<TextView
android:id="@+id/desc_min_order"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fontFamily="@font/dmsans_mediumitalic"
android:textColor="@color/black_300"
android:text="Minimal pembelian 10 buah"/>
</LinearLayout>
</androidx.cardview.widget.CardView>
<!-- Product Price and Name -->
<androidx.cardview.widget.CardView
android:layout_width="match_parent"
@ -58,6 +101,50 @@
android:textStyle="bold"
tools:text="Rp65.000" />
<!-- <androidx.cardview.widget.CardView-->
<!-- android:layout_width="match_parent"-->
<!-- android:layout_height="wrap_content"-->
<!-- android:backgroundTint="@color/blue_50"-->
<!-- app:cardElevation="0dp">-->
<!-- <LinearLayout-->
<!-- android:layout_width="match_parent"-->
<!-- android:layout_height="wrap_content"-->
<!-- android:paddingStart="16dp"-->
<!-- android:paddingVertical="4dp"-->
<!-- android:orientation="vertical">-->
<!-- <LinearLayout-->
<!-- android:layout_width="wrap_content"-->
<!-- android:layout_height="wrap_content"-->
<!-- android:orientation="horizontal">-->
<!-- <TextView-->
<!-- android:layout_width="wrap_content"-->
<!-- android:layout_height="wrap_content"-->
<!-- android:layout_marginEnd="8dp"-->
<!-- android:fontFamily="@font/dmsans_semibold"-->
<!-- android:text="Harga Grosir"/>-->
<!-- <TextView-->
<!-- android:id="@+id/tvPriceWholesale"-->
<!-- android:layout_width="wrap_content"-->
<!-- android:layout_height="wrap_content"-->
<!-- android:textColor="@color/blue_500"-->
<!-- android:text="Rp50.000"-->
<!-- android:fontFamily="@font/dmsans_semibold"/>-->
<!-- </LinearLayout>-->
<!-- <TextView-->
<!-- android:id="@+id/desc_min_order"-->
<!-- android:layout_width="match_parent"-->
<!-- android:layout_height="match_parent"-->
<!-- android:fontFamily="@font/dmsans_mediumitalic"-->
<!-- android:textColor="@color/black_300"-->
<!-- android:text="Minimal pembelian 10 buah"/>-->
<!-- </LinearLayout>-->
<!-- </androidx.cardview.widget.CardView>-->
<TextView
android:id="@+id/tvProductName"
android:layout_width="wrap_content"
@ -67,6 +154,7 @@
android:textSize="16sp"
tools:text="Keripik Ikan Tenggiri" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
@ -91,7 +179,7 @@
android:layout_width="16dp"
android:layout_height="16dp"
android:contentDescription="@string/rating"
android:src="@drawable/baseline_star_24"/>
android:src="@drawable/baseline_star_24" />
<TextView
android:id="@+id/tvRating"
@ -103,10 +191,14 @@
</LinearLayout>
</LinearLayout>
</androidx.cardview.widget.CardView>
<com.google.android.material.divider.MaterialDivider
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
<!-- Buyer Reviews Section -->
<androidx.cardview.widget.CardView
android:layout_width="match_parent"

Some files were not shown because too many files have changed in this diff Show More