mirror of
https://github.com/shaulascr/ecommerce_serang.git
synced 2025-08-10 09:22:21 +00:00
Merge remote-tracking branch 'origin/master' into gracia
# Conflicts: # app/src/main/AndroidManifest.xml # gradle/libs.versions.toml
This commit is contained in:
8
.idea/appInsightsSettings.xml
generated
8
.idea/appInsightsSettings.xml
generated
@ -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
2
.idea/kotlinc.xml
generated
@ -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>
|
@ -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
29
app/google-services.json
Normal 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"
|
||||
}
|
@ -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>
|
@ -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)
|
||||
// }
|
||||
// }
|
||||
|
||||
|
||||
}
|
@ -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
|
||||
)
|
@ -8,4 +8,5 @@ data class CartItem (
|
||||
|
||||
@SerializedName("quantity")
|
||||
val quantity: Int
|
||||
)
|
||||
)
|
||||
|
||||
|
@ -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()
|
||||
)
|
@ -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,
|
||||
|
@ -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
|
||||
)
|
@ -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
|
||||
)
|
@ -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
|
||||
|
||||
)
|
@ -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
|
||||
)
|
||||
)
|
||||
|
@ -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
|
@ -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
|
||||
)
|
@ -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
|
||||
)
|
@ -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
|
||||
)
|
@ -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
|
||||
)
|
@ -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
|
||||
)
|
@ -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
|
||||
)
|
@ -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
|
||||
)
|
@ -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
|
||||
)
|
@ -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
|
||||
)
|
@ -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
|
||||
)
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
)
|
@ -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,
|
||||
|
@ -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
|
||||
)
|
||||
|
@ -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
|
||||
)
|
||||
|
@ -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
|
||||
)
|
@ -12,4 +12,3 @@ data class AllProductResponse(
|
||||
val products: List<ProductsItem>
|
||||
)
|
||||
|
||||
|
||||
|
@ -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
|
||||
)
|
||||
|
@ -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
|
||||
)
|
||||
|
@ -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
|
||||
)
|
@ -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()
|
||||
|
@ -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>
|
||||
}
|
@ -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) {
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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"
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -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")
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -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")
|
||||
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
@ -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"
|
||||
}
|
||||
}
|
@ -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"
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
@ -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 ")
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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) {
|
||||
|
@ -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
|
||||
// }
|
||||
//}
|
@ -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{
|
||||
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -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")
|
||||
}
|
||||
}
|
@ -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"
|
||||
}
|
||||
}
|
@ -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"
|
||||
|
||||
}
|
||||
}
|
@ -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())
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
@ -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 {
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -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()
|
||||
|
@ -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() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -14,9 +14,7 @@ class OrderViewPagerAdapter(
|
||||
"pending", // Menunggu Tagihan
|
||||
"unpaid", // Belum Dibayar
|
||||
"processed", // Diproses
|
||||
"paid", // Dibayar
|
||||
"shipped", // Dikirim
|
||||
"delivered", // Diterima
|
||||
"completed", // Selesai
|
||||
"canceled" // Dibatalkan
|
||||
)
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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", "")
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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
|
||||
)
|
||||
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
||||
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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"
|
||||
}
|
||||
}
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
@ -57,8 +57,6 @@ class SessionManager(context: Context) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
//clear data when log out
|
||||
fun clearAll() {
|
||||
sharedPreferences.edit() {
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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"
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
@ -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"/>
|
||||
|
||||
|
5
app/src/main/res/drawable/baseline_info_24.xml
Normal file
5
app/src/main/res/drawable/baseline_info_24.xml
Normal 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>
|
5
app/src/main/res/drawable/baseline_logout_24.xml
Normal file
5
app/src/main/res/drawable/baseline_logout_24.xml
Normal 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>
|
8
app/src/main/res/drawable/bg_bottom_sheet.xml
Normal file
8
app/src/main/res/drawable/bg_bottom_sheet.xml
Normal 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>
|
8
app/src/main/res/drawable/bg_spinner_reason.xml
Normal file
8
app/src/main/res/drawable/bg_spinner_reason.xml
Normal 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>
|
@ -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>
|
72
app/src/main/res/layout/activity_create_review.xml
Normal file
72
app/src/main/res/layout/activity_create_review.xml
Normal 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>
|
429
app/src/main/res/layout/activity_detail_order_status.xml
Normal file
429
app/src/main/res/layout/activity_detail_order_status.xml
Normal 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>
|
@ -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
Reference in New Issue
Block a user