mirror of
https://github.com/shaulascr/ecommerce_serang.git
synced 2025-12-15 23:51:01 +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>
|
<InsightsFilterSettings>
|
||||||
<option name="connection">
|
<option name="connection">
|
||||||
<ConnectionSetting>
|
<ConnectionSetting>
|
||||||
<option name="appId" value="PLACEHOLDER" />
|
<option name="appId" value="com.alya.ecommerce_serang" />
|
||||||
<option name="mobileSdkAppId" value="" />
|
<option name="mobileSdkAppId" value="1:284675201257:android:2755670e3dbb1b48683878" />
|
||||||
<option name="projectId" value="" />
|
<option name="projectId" value="ecommerce-serang" />
|
||||||
<option name="projectNumber" value="" />
|
<option name="projectNumber" value="284675201257" />
|
||||||
</ConnectionSetting>
|
</ConnectionSetting>
|
||||||
</option>
|
</option>
|
||||||
<option name="signal" value="SIGNAL_UNSPECIFIED" />
|
<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"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="KotlinJpsPluginSettings">
|
<component name="KotlinJpsPluginSettings">
|
||||||
<option name="version" value="1.9.0" />
|
<option name="version" value="2.1.0" />
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</project>
|
||||||
@ -6,6 +6,7 @@ plugins {
|
|||||||
id("androidx.navigation.safeargs")
|
id("androidx.navigation.safeargs")
|
||||||
id("kotlin-parcelize")
|
id("kotlin-parcelize")
|
||||||
alias(libs.plugins.dagger.hilt) // Use alias from catalog
|
alias(libs.plugins.dagger.hilt) // Use alias from catalog
|
||||||
|
id("com.google.gms.google-services")
|
||||||
}
|
}
|
||||||
|
|
||||||
val localProperties = Properties().apply {
|
val localProperties = Properties().apply {
|
||||||
@ -22,8 +23,8 @@ android {
|
|||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId = "com.alya.ecommerce_serang"
|
applicationId = "com.alya.ecommerce_serang"
|
||||||
minSdk = 21
|
minSdk = 26
|
||||||
targetSdk = 34
|
targetSdk = 35
|
||||||
versionCode = 1
|
versionCode = 1
|
||||||
versionName = "1.0"
|
versionName = "1.0"
|
||||||
|
|
||||||
@ -119,6 +120,11 @@ dependencies {
|
|||||||
|
|
||||||
implementation("io.socket:socket.io-client:2.1.0") // or latest version
|
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:theme="@style/Theme.Ecommerce_serang"
|
||||||
android:usesCleartextTraffic="true"
|
android:usesCleartextTraffic="true"
|
||||||
tools:targetApi="31">
|
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
|
<activity
|
||||||
android:name=".ui.profile.mystore.sells.shipment.ShipmentConfirmationActivity"
|
android:name=".ui.profile.mystore.sells.shipment.ShipmentConfirmationActivity"
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
@ -41,6 +53,9 @@
|
|||||||
<activity
|
<activity
|
||||||
android:name=".ui.profile.mystore.sells.payment.DetailPaymentActivity"
|
android:name=".ui.profile.mystore.sells.payment.DetailPaymentActivity"
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
|
<activity
|
||||||
|
android:name=".ui.profile.mystore.sells.order.DetailOrderActivity"
|
||||||
|
android:exported="false" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.chat.ChatActivity"
|
android:name=".ui.chat.ChatActivity"
|
||||||
android:exported="false" /> <!-- <provider -->
|
android:exported="false" /> <!-- <provider -->
|
||||||
@ -52,7 +67,6 @@
|
|||||||
android:enabled="true"
|
android:enabled="true"
|
||||||
android:exported="false"
|
android:exported="false"
|
||||||
android:foregroundServiceType="dataSync" />
|
android:foregroundServiceType="dataSync" />
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.profile.mystore.profile.shipping_service.ShippingServiceActivity"
|
android:name=".ui.profile.mystore.profile.shipping_service.ShippingServiceActivity"
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
@ -69,7 +83,7 @@
|
|||||||
android:name=".ui.order.detail.PaymentActivity"
|
android:name=".ui.order.detail.PaymentActivity"
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".data.api.response.customer.cart.CartActivity"
|
android:name=".ui.cart.CartActivity"
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.order.address.EditAddressActivity"
|
android:name=".ui.order.address.EditAddressActivity"
|
||||||
@ -140,6 +154,24 @@
|
|||||||
<activity
|
<activity
|
||||||
android:name=".ui.MainActivity"
|
android:name=".ui.MainActivity"
|
||||||
android:exported="false" />
|
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>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
@ -5,14 +5,13 @@ import dagger.hilt.android.HiltAndroidApp
|
|||||||
|
|
||||||
@HiltAndroidApp
|
@HiltAndroidApp
|
||||||
class App : Application(){
|
class App : Application(){
|
||||||
// override fun onCreate() {
|
// private val TAG = "AppSerang"
|
||||||
// super.onCreate()
|
//
|
||||||
|
//// 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")
|
@SerializedName("quantity")
|
||||||
val quantity: Int
|
val quantity: Int
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@ -12,5 +12,6 @@ data class CheckoutData(
|
|||||||
val sellerId: Int = 0,
|
val sellerId: Int = 0,
|
||||||
val quantity: Int = 1,
|
val quantity: Int = 1,
|
||||||
val isBuyNow: Boolean = false,
|
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 (
|
data class CreateAddressRequest (
|
||||||
@SerializedName("latitude")
|
@SerializedName("latitude")
|
||||||
val lat: Double,
|
val lat: Double? = null,
|
||||||
|
|
||||||
@SerializedName("longitude")
|
@SerializedName("longitude")
|
||||||
val long: Double,
|
val long: Double? = null,
|
||||||
|
|
||||||
@SerializedName("street")
|
@SerializedName("street")
|
||||||
val street: String,
|
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")
|
@SerializedName("address_id")
|
||||||
val addressId : Int,
|
val addressId : Int,
|
||||||
|
|
||||||
@SerializedName("payment_method_id")
|
@SerializedName("payment_info_id")
|
||||||
val paymentMethodId : Int,
|
val paymentMethodId : Int,
|
||||||
|
|
||||||
@SerializedName("ship_price")
|
@SerializedName("ship_price")
|
||||||
@ -21,9 +21,12 @@ data class OrderRequest (
|
|||||||
@SerializedName("is_negotiable")
|
@SerializedName("is_negotiable")
|
||||||
val isNego: Boolean,
|
val isNego: Boolean,
|
||||||
|
|
||||||
@SerializedName("cart_items_id")
|
@SerializedName("cart_item_ids")
|
||||||
val cartItemId: List<Int>,
|
val cartItemId: List<Int>,
|
||||||
|
|
||||||
@SerializedName("ship_etd")
|
@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")
|
@SerializedName("address_id")
|
||||||
val addressId : Int,
|
val addressId : Int,
|
||||||
|
|
||||||
@SerializedName("payment_method_id")
|
@SerializedName("payment_info_id")
|
||||||
val paymentMethodId : Int,
|
val paymentMethodId : Int,
|
||||||
|
|
||||||
@SerializedName("ship_price")
|
@SerializedName("ship_price")
|
||||||
@ -27,7 +27,10 @@ data class OrderRequestBuy (
|
|||||||
@SerializedName("quantity")
|
@SerializedName("quantity")
|
||||||
val quantity : Int,
|
val quantity : Int,
|
||||||
|
|
||||||
|
|
||||||
@SerializedName("ship_etd")
|
@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")
|
@field:SerializedName("image")
|
||||||
val image: String,
|
val image: String,
|
||||||
|
|
||||||
|
@field:SerializedName("is_wholesale")
|
||||||
|
val isWholesale: Boolean,
|
||||||
|
|
||||||
|
@field:SerializedName("sppirt")
|
||||||
|
val sppirt: String? = null,
|
||||||
|
|
||||||
@field:SerializedName("rating")
|
@field:SerializedName("rating")
|
||||||
val rating: String,
|
val rating: String,
|
||||||
|
|
||||||
@ -23,6 +29,9 @@ data class ProductsItem(
|
|||||||
@field:SerializedName("is_pre_order")
|
@field:SerializedName("is_pre_order")
|
||||||
val isPreOrder: Boolean,
|
val isPreOrder: Boolean,
|
||||||
|
|
||||||
|
@field:SerializedName("condition")
|
||||||
|
val condition: String? = null,
|
||||||
|
|
||||||
@field:SerializedName("category_id")
|
@field:SerializedName("category_id")
|
||||||
val categoryId: Int,
|
val categoryId: Int,
|
||||||
|
|
||||||
@ -32,6 +41,9 @@ data class ProductsItem(
|
|||||||
@field:SerializedName("name")
|
@field:SerializedName("name")
|
||||||
val name: String,
|
val name: String,
|
||||||
|
|
||||||
|
@field:SerializedName("halal")
|
||||||
|
val halal: String?= null,
|
||||||
|
|
||||||
@field:SerializedName("id")
|
@field:SerializedName("id")
|
||||||
val id: Int,
|
val id: Int,
|
||||||
|
|
||||||
@ -46,4 +58,4 @@ data class ProductsItem(
|
|||||||
|
|
||||||
@field:SerializedName("status")
|
@field:SerializedName("status")
|
||||||
val status: String
|
val status: String
|
||||||
)
|
)
|
||||||
|
|||||||
@ -1,7 +1,10 @@
|
|||||||
package com.alya.ecommerce_serang.data.api.dto
|
package com.alya.ecommerce_serang.data.api.dto
|
||||||
|
|
||||||
|
import android.os.Parcelable
|
||||||
import com.google.gson.annotations.SerializedName
|
import com.google.gson.annotations.SerializedName
|
||||||
|
import kotlinx.android.parcel.Parcelize
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
data class RegisterRequest (
|
data class RegisterRequest (
|
||||||
val name: String?,
|
val name: String?,
|
||||||
val email: String?,
|
val email: String?,
|
||||||
@ -15,4 +18,4 @@ data class RegisterRequest (
|
|||||||
val image: String? = null,
|
val image: String? = null,
|
||||||
|
|
||||||
val otp: 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(
|
data class ListCartResponse(
|
||||||
|
|
||||||
@field:SerializedName("data")
|
@field:SerializedName("data")
|
||||||
val data: List<DataItem>,
|
val data: List<DataItemCart>,
|
||||||
|
|
||||||
@field:SerializedName("message")
|
@field:SerializedName("message")
|
||||||
val message: String
|
val message: String
|
||||||
)
|
)
|
||||||
|
|
||||||
data class DataItem(
|
data class DataItemCart(
|
||||||
|
|
||||||
@field:SerializedName("store_id")
|
@field:SerializedName("store_id")
|
||||||
val storeId: Int,
|
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(
|
data class CreateOrderResponse(
|
||||||
|
|
||||||
@field:SerializedName("shipping")
|
@field:SerializedName("shipping")
|
||||||
val shipping: Shipping,
|
val shipping: Shipping,
|
||||||
|
|
||||||
@field:SerializedName("order_item")
|
@field:SerializedName("order_item")
|
||||||
val orderItem: List<OrderItemItem>,
|
val orderItem: List<OrderItemItem>,
|
||||||
|
|
||||||
@field:SerializedName("message")
|
@field:SerializedName("message")
|
||||||
val message: String,
|
val message: String,
|
||||||
|
|
||||||
@field:SerializedName("order")
|
@field:SerializedName("order")
|
||||||
val order: Order
|
val order: Order
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -47,34 +47,10 @@ data class Shipping(
|
|||||||
val status: String
|
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(
|
data class Order(
|
||||||
|
|
||||||
@field:SerializedName("payment_method_id")
|
|
||||||
val paymentMethodId: Int,
|
|
||||||
|
|
||||||
@field:SerializedName("auto_completed_at")
|
@field:SerializedName("auto_completed_at")
|
||||||
val autoCompletedAt: String? = null,
|
val autoCompletedAt: String,
|
||||||
|
|
||||||
@field:SerializedName("updated_at")
|
@field:SerializedName("updated_at")
|
||||||
val updatedAt: String,
|
val updatedAt: String,
|
||||||
@ -97,9 +73,33 @@ data class Order(
|
|||||||
@field:SerializedName("voucher_id")
|
@field:SerializedName("voucher_id")
|
||||||
val voucherId: String? = null,
|
val voucherId: String? = null,
|
||||||
|
|
||||||
|
@field:SerializedName("payment_info_id")
|
||||||
|
val paymentInfoId: Int,
|
||||||
|
|
||||||
@field:SerializedName("id")
|
@field:SerializedName("id")
|
||||||
val id: Int,
|
val id: Int,
|
||||||
|
|
||||||
@field:SerializedName("status")
|
@field:SerializedName("status")
|
||||||
val status: String
|
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>
|
val products: List<ProductsItem>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -4,6 +4,12 @@ import com.google.gson.annotations.SerializedName
|
|||||||
|
|
||||||
data class DetailStoreProductResponse(
|
data class DetailStoreProductResponse(
|
||||||
|
|
||||||
|
@field:SerializedName("shipping")
|
||||||
|
val shipping: List<ShippingItemDetail>,
|
||||||
|
|
||||||
|
@field:SerializedName("payment")
|
||||||
|
val payment: List<PaymentItemDetail>,
|
||||||
|
|
||||||
@field:SerializedName("store")
|
@field:SerializedName("store")
|
||||||
val store: StoreProduct,
|
val store: StoreProduct,
|
||||||
|
|
||||||
@ -11,28 +17,11 @@ data class DetailStoreProductResponse(
|
|||||||
val message: String
|
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(
|
data class StoreProduct(
|
||||||
|
|
||||||
@field:SerializedName("store_id")
|
@field:SerializedName("store_id")
|
||||||
val storeId: Int,
|
val storeId: Int,
|
||||||
|
|
||||||
@field:SerializedName("shipping_service")
|
|
||||||
val shippingService: List<ShippingServiceItem>,
|
|
||||||
|
|
||||||
@field:SerializedName("store_rating")
|
@field:SerializedName("store_rating")
|
||||||
val storeRating: String,
|
val storeRating: String,
|
||||||
|
|
||||||
@ -45,21 +34,36 @@ data class StoreProduct(
|
|||||||
@field:SerializedName("store_type")
|
@field:SerializedName("store_type")
|
||||||
val storeType: String,
|
val storeType: String,
|
||||||
|
|
||||||
@field:SerializedName("payment_info")
|
|
||||||
val paymentInfo: List<PaymentInfoItem>,
|
|
||||||
|
|
||||||
@field:SerializedName("store_location")
|
@field:SerializedName("store_location")
|
||||||
val storeLocation: String,
|
val storeLocation: String,
|
||||||
|
|
||||||
@field:SerializedName("store_image")
|
@field:SerializedName("store_image")
|
||||||
val storeImage: String,
|
val storeImage: String? = null,
|
||||||
|
|
||||||
@field:SerializedName("status")
|
@field:SerializedName("status")
|
||||||
val status: String
|
val status: String
|
||||||
)
|
)
|
||||||
|
|
||||||
data class ShippingServiceItem(
|
data class ShippingItemDetail(
|
||||||
|
|
||||||
@field:SerializedName("courier")
|
@field:SerializedName("courier")
|
||||||
val courier: String
|
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
|
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
|
import com.google.gson.annotations.SerializedName
|
||||||
|
|
||||||
data class ProductResponse(
|
data class ProductResponse(
|
||||||
|
|
||||||
@field:SerializedName("product")
|
@field:SerializedName("product")
|
||||||
val product: Product,
|
val product: Product,
|
||||||
|
|
||||||
@field:SerializedName("message")
|
@field:SerializedName("message")
|
||||||
val message: String
|
val message: String
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -17,7 +18,13 @@ data class Product(
|
|||||||
val storeId: Int,
|
val storeId: Int,
|
||||||
|
|
||||||
@field:SerializedName("image")
|
@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")
|
@field:SerializedName("rating")
|
||||||
val rating: String,
|
val rating: String,
|
||||||
@ -34,8 +41,8 @@ data class Product(
|
|||||||
@field:SerializedName("is_pre_order")
|
@field:SerializedName("is_pre_order")
|
||||||
val isPreOrder: Boolean,
|
val isPreOrder: Boolean,
|
||||||
|
|
||||||
@field:SerializedName("duration")
|
@field:SerializedName("condition")
|
||||||
val duration: Any?,
|
val condition: String? = null,
|
||||||
|
|
||||||
@field:SerializedName("category_id")
|
@field:SerializedName("category_id")
|
||||||
val categoryId: Int,
|
val categoryId: Int,
|
||||||
@ -46,6 +53,15 @@ data class Product(
|
|||||||
@field:SerializedName("product_id")
|
@field:SerializedName("product_id")
|
||||||
val productId: Int,
|
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")
|
@field:SerializedName("min_order")
|
||||||
val minOrder: Int,
|
val minOrder: Int,
|
||||||
|
|
||||||
@ -56,5 +72,19 @@ data class Product(
|
|||||||
val stock: Int,
|
val stock: Int,
|
||||||
|
|
||||||
@field:SerializedName("product_category")
|
@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()
|
val client = OkHttpClient.Builder()
|
||||||
.addInterceptor(loggingInterceptor)
|
.addInterceptor(loggingInterceptor)
|
||||||
.addInterceptor(authInterceptor)
|
.addInterceptor(authInterceptor)
|
||||||
|
.connectTimeout(60, TimeUnit.SECONDS) // Increase to 60 seconds
|
||||||
|
.readTimeout(60, TimeUnit.SECONDS) // Increase to 60 seconds
|
||||||
|
.writeTimeout(60, TimeUnit.SECONDS)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
val retrofit = Retrofit.Builder()
|
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.AddEvidenceRequest
|
||||||
import com.alya.ecommerce_serang.data.api.dto.AddPaymentInfoResponse
|
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.CartItem
|
||||||
import com.alya.ecommerce_serang.data.api.dto.CityResponse
|
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.CompletedOrderRequest
|
||||||
import com.alya.ecommerce_serang.data.api.dto.CourierCostRequest
|
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.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.LoginRequest
|
||||||
import com.alya.ecommerce_serang.data.api.dto.OrderRequest
|
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.OrderRequestBuy
|
||||||
import com.alya.ecommerce_serang.data.api.dto.OtpRequest
|
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.ProvinceResponse
|
||||||
import com.alya.ecommerce_serang.data.api.dto.RegisterRequest
|
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.SearchRequest
|
||||||
import com.alya.ecommerce_serang.data.api.dto.ShippingServiceRequest
|
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.StoreAddressResponse
|
||||||
import com.alya.ecommerce_serang.data.api.dto.UpdateCart
|
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.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.LoginResponse
|
||||||
import com.alya.ecommerce_serang.data.api.response.auth.OtpResponse
|
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.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.ChatHistoryResponse
|
||||||
import com.alya.ecommerce_serang.data.api.response.chat.ChatListResponse
|
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.SendChatResponse
|
||||||
import com.alya.ecommerce_serang.data.api.response.chat.UpdateChatResponse
|
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.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.ListCartResponse
|
||||||
import com.alya.ecommerce_serang.data.api.response.customer.cart.UpdateCartResponse
|
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.CourierCostResponse
|
||||||
import com.alya.ecommerce_serang.data.api.response.customer.order.CreateOrderResponse
|
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.ListCityResponse
|
||||||
import com.alya.ecommerce_serang.data.api.response.customer.order.ListProvinceResponse
|
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.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.product.StoreResponse
|
||||||
import com.alya.ecommerce_serang.data.api.response.customer.profile.AddressResponse
|
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.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.customer.profile.ProfileResponse
|
||||||
import com.alya.ecommerce_serang.data.api.response.order.AddEvidenceResponse
|
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.ComplaintResponse
|
||||||
@ -68,6 +84,7 @@ import retrofit2.http.Multipart
|
|||||||
import retrofit2.http.POST
|
import retrofit2.http.POST
|
||||||
import retrofit2.http.PUT
|
import retrofit2.http.PUT
|
||||||
import retrofit2.http.Part
|
import retrofit2.http.Part
|
||||||
|
import retrofit2.http.PartMap
|
||||||
import retrofit2.http.Path
|
import retrofit2.http.Path
|
||||||
import retrofit2.http.Query
|
import retrofit2.http.Query
|
||||||
|
|
||||||
@ -77,11 +94,54 @@ interface ApiService {
|
|||||||
@Body registerRequest: RegisterRequest
|
@Body registerRequest: RegisterRequest
|
||||||
): Response<RegisterResponse>
|
): 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")
|
@POST("otp")
|
||||||
suspend fun getOTP(
|
suspend fun getOTP(
|
||||||
@Body otpRequest: OtpRequest
|
@Body otpRequest: OtpRequest
|
||||||
):OtpResponse
|
):OtpResponse
|
||||||
|
|
||||||
|
@PUT("updatefcm")
|
||||||
|
suspend fun updateFcm(
|
||||||
|
@Body fcmReq: FcmReq
|
||||||
|
): FcmTokenResponse
|
||||||
|
|
||||||
|
@GET("checkstore")
|
||||||
|
suspend fun checkStoreUser(
|
||||||
|
): HasStoreResponse
|
||||||
|
|
||||||
@POST("login")
|
@POST("login")
|
||||||
suspend fun login(
|
suspend fun login(
|
||||||
@Body loginRequest: LoginRequest
|
@Body loginRequest: LoginRequest
|
||||||
@ -91,6 +151,10 @@ interface ApiService {
|
|||||||
suspend fun allCategory(
|
suspend fun allCategory(
|
||||||
): Response<CategoryResponse>
|
): Response<CategoryResponse>
|
||||||
|
|
||||||
|
@GET("storetype")
|
||||||
|
suspend fun listTypeStore(
|
||||||
|
): Response<ListStoreTypeResponse>
|
||||||
|
|
||||||
@GET("product")
|
@GET("product")
|
||||||
suspend fun getAllProduct(): Response<AllProductResponse>
|
suspend fun getAllProduct(): Response<AllProductResponse>
|
||||||
|
|
||||||
@ -117,6 +181,11 @@ interface ApiService {
|
|||||||
@Body request: OrderRequest
|
@Body request: OrderRequest
|
||||||
): Response<CreateOrderResponse>
|
): Response<CreateOrderResponse>
|
||||||
|
|
||||||
|
@POST("order/cancel")
|
||||||
|
suspend fun cancelOrder(
|
||||||
|
@Body cancelReq: CancelOrderReq
|
||||||
|
): Response<CancelOrderResponse>
|
||||||
|
|
||||||
@GET("order/detail/{id}")
|
@GET("order/detail/{id}")
|
||||||
suspend fun getDetailOrder(
|
suspend fun getDetailOrder(
|
||||||
@Path("id") orderId: Int
|
@Path("id") orderId: Int
|
||||||
@ -135,6 +204,17 @@ interface ApiService {
|
|||||||
@Part evidence: MultipartBody.Part
|
@Part evidence: MultipartBody.Part
|
||||||
): Response<AddEvidenceResponse>
|
): 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}")
|
@GET("order/{status}")
|
||||||
suspend fun getOrderList(
|
suspend fun getOrderList(
|
||||||
@Path("status") status: String
|
@Path("status") status: String
|
||||||
@ -215,6 +295,11 @@ interface ApiService {
|
|||||||
@Body updateCart: UpdateCart
|
@Body updateCart: UpdateCart
|
||||||
): Response<UpdateCartResponse>
|
): Response<UpdateCartResponse>
|
||||||
|
|
||||||
|
@DELETE("cart/delete/{id}")
|
||||||
|
suspend fun deleteCart(
|
||||||
|
@Path("id") cartItemId : Int
|
||||||
|
):Response<DeleteCartResponse>
|
||||||
|
|
||||||
@POST("couriercost")
|
@POST("couriercost")
|
||||||
suspend fun countCourierCost(
|
suspend fun countCourierCost(
|
||||||
@Body courierCost : CourierCostRequest
|
@Body courierCost : CourierCostRequest
|
||||||
@ -253,6 +338,11 @@ interface ApiService {
|
|||||||
@Part complaintimg: MultipartBody.Part
|
@Part complaintimg: MultipartBody.Part
|
||||||
): Response<ComplaintResponse>
|
): Response<ComplaintResponse>
|
||||||
|
|
||||||
|
@POST("review")
|
||||||
|
suspend fun createReview(
|
||||||
|
@Body contentReview : ReviewProductItem
|
||||||
|
): Response<CreateReviewResponse>
|
||||||
|
|
||||||
@GET("store/topup")
|
@GET("store/topup")
|
||||||
suspend fun getTopUpHistory(): Response<TopUpResponse>
|
suspend fun getTopUpHistory(): Response<TopUpResponse>
|
||||||
|
|
||||||
@ -366,4 +456,12 @@ interface ApiService {
|
|||||||
@GET("chat")
|
@GET("chat")
|
||||||
suspend fun getChatList(
|
suspend fun getChatList(
|
||||||
): Response<ChatListResponse>
|
): 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
|
chatimg = imagePart
|
||||||
)
|
)
|
||||||
|
|
||||||
|
Log.d("ChatRepository", "check data productId=$productIdPart, storeId=$storeIdPart, messageTxt=$messagePart, chatImg=$imagePart")
|
||||||
|
|
||||||
if (response.isSuccessful) {
|
if (response.isSuccessful) {
|
||||||
val body = response.body()
|
val body = response.body()
|
||||||
if (body != null) {
|
if (body != null) {
|
||||||
|
|||||||
@ -2,27 +2,33 @@ package com.alya.ecommerce_serang.data.repository
|
|||||||
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.alya.ecommerce_serang.data.api.dto.AddEvidenceMultipartRequest
|
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.CompletedOrderRequest
|
||||||
import com.alya.ecommerce_serang.data.api.dto.CourierCostRequest
|
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.CreateAddressRequest
|
||||||
import com.alya.ecommerce_serang.data.api.dto.OrderRequest
|
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.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.dto.UserProfile
|
||||||
import com.alya.ecommerce_serang.data.api.response.customer.cart.DataItem
|
import com.alya.ecommerce_serang.data.api.response.customer.cart.DataItemCart
|
||||||
import com.alya.ecommerce_serang.data.api.response.customer.order.CreateOrderResponse
|
import com.alya.ecommerce_serang.data.api.response.customer.order.CancelOrderResponse
|
||||||
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.order.CourierCostResponse
|
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.ListCityResponse
|
||||||
import com.alya.ecommerce_serang.data.api.response.customer.order.ListProvinceResponse
|
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.StoreProduct
|
||||||
import com.alya.ecommerce_serang.data.api.response.customer.product.StoreResponse
|
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.AddressResponse
|
||||||
import com.alya.ecommerce_serang.data.api.response.customer.profile.CreateAddressResponse
|
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 com.alya.ecommerce_serang.data.api.retrofit.ApiService
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.Flow
|
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 {
|
return try {
|
||||||
val response = apiService.getCart()
|
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?> {
|
suspend fun fetchStoreDetail(storeId: Int): Result<StoreProduct?> {
|
||||||
return try {
|
return try {
|
||||||
val response = apiService.getDetailStore(storeId)
|
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> {
|
suspend fun addAddress(request: CreateAddressRequest): Result<CreateAddressResponse> {
|
||||||
return try {
|
return try {
|
||||||
Log.d("OrderRepository", "Adding address: $request")
|
Log.d("OrderRepository", "Adding address: $request")
|
||||||
@ -258,59 +319,35 @@ class OrderRepository(private val apiService: ApiService) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// suspend fun uploadPaymentProof(request : AddEvidenceRequest): Result<AddEvidenceResponse> {
|
suspend fun uploadPaymentProof(request: AddEvidenceMultipartRequest): Result<AddEvidenceResponse> {
|
||||||
// return try {
|
return try {
|
||||||
// Log.d("OrderRepository", "Add Evidence : $request")
|
Log.d("OrderRepository", "Uploading payment proof...")
|
||||||
// val response = apiService.addEvidence(request)
|
|
||||||
//
|
|
||||||
// if (response.isSuccessful) {
|
|
||||||
// val addEvidenceResponse = response.body()
|
|
||||||
// if (addEvidenceResponse != null) {
|
|
||||||
// Log.d("OrderRepository", "Add Evidence successfully: ${addEvidenceResponse.message}")
|
|
||||||
// Result.Success(addEvidenceResponse)
|
|
||||||
// } else {
|
|
||||||
// Log.e("OrderRepository", "Response body was null")
|
|
||||||
// Result.Error(Exception("Empty response from server"))
|
|
||||||
// }
|
|
||||||
// } else {
|
|
||||||
// val errorBody = response.errorBody()?.string() ?: "Unknown error"
|
|
||||||
// Log.e("OrderRepository", "Error Add Evidence : $errorBody")
|
|
||||||
// Result.Error(Exception(errorBody))
|
|
||||||
// }
|
|
||||||
// } catch (e: Exception) {
|
|
||||||
// Log.e("OrderRepository", "Exception Add Evidence ", e)
|
|
||||||
// Result.Error(e)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
suspend fun uploadPaymentProof(request: AddEvidenceMultipartRequest): Result<AddEvidenceResponse> {
|
|
||||||
return try {
|
|
||||||
Log.d("OrderRepository", "Uploading payment proof...")
|
|
||||||
|
|
||||||
val response = apiService.addEvidenceMultipart(
|
val response = apiService.addEvidenceMultipart(
|
||||||
orderId = request.orderId,
|
orderId = request.orderId,
|
||||||
amount = request.amount,
|
amount = request.amount,
|
||||||
evidence = request.evidence
|
evidence = request.evidence
|
||||||
)
|
)
|
||||||
|
|
||||||
if (response.isSuccessful) {
|
if (response.isSuccessful) {
|
||||||
val addEvidenceResponse = response.body()
|
val addEvidenceResponse = response.body()
|
||||||
if (addEvidenceResponse != null) {
|
if (addEvidenceResponse != null) {
|
||||||
Log.d("OrderRepository", "Payment proof uploaded successfully: ${addEvidenceResponse.message}")
|
Log.d("OrderRepository", "Payment proof uploaded successfully: ${addEvidenceResponse.message}")
|
||||||
Result.Success(addEvidenceResponse)
|
Result.Success(addEvidenceResponse)
|
||||||
|
} else {
|
||||||
|
Log.e("OrderRepository", "Response body was null")
|
||||||
|
Result.Error(Exception("Empty response from server"))
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
Log.e("OrderRepository", "Response body was null")
|
val errorBody = response.errorBody()?.string() ?: "Unknown error"
|
||||||
Result.Error(Exception("Empty response from server"))
|
Log.e("OrderRepository", "Error uploading payment proof: $errorBody")
|
||||||
|
Result.Error(Exception(errorBody))
|
||||||
}
|
}
|
||||||
} else {
|
} catch (e: Exception) {
|
||||||
val errorBody = response.errorBody()?.string() ?: "Unknown error"
|
Log.e("OrderRepository", "Exception uploading payment proof", e)
|
||||||
Log.e("OrderRepository", "Error uploading payment proof: $errorBody")
|
Result.Error(e)
|
||||||
Result.Error(Exception(errorBody))
|
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e("OrderRepository", "Exception uploading payment proof", e)
|
|
||||||
Result.Error(e)
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun getOrderList(status: String): Result<OrderListResponse> {
|
suspend fun getOrderList(status: String): Result<OrderListResponse> {
|
||||||
return try {
|
return try {
|
||||||
@ -339,7 +376,7 @@ suspend fun uploadPaymentProof(request: AddEvidenceMultipartRequest): Result<Add
|
|||||||
|
|
||||||
suspend fun confirmOrderCompleted(request: CompletedOrderRequest): Result<CompletedOrderResponse> {
|
suspend fun confirmOrderCompleted(request: CompletedOrderRequest): Result<CompletedOrderResponse> {
|
||||||
return try {
|
return try {
|
||||||
Log.d("OrderRepository", "Cinfroming order request completed: $request")
|
Log.d("OrderRepository", "Conforming order request completed: $request")
|
||||||
val response = apiService.confirmOrder(request)
|
val response = apiService.confirmOrder(request)
|
||||||
|
|
||||||
if(response.isSuccessful) {
|
if(response.isSuccessful) {
|
||||||
@ -431,4 +468,47 @@ suspend fun uploadPaymentProof(request: AddEvidenceMultipartRequest): Result<Add
|
|||||||
emit(Result.Error(e))
|
emit(Result.Error(e))
|
||||||
}
|
}
|
||||||
}.flowOn(Dispatchers.IO)
|
}.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
|
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.LoginRequest
|
||||||
import com.alya.ecommerce_serang.data.api.dto.OtpRequest
|
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.RegisterRequest
|
||||||
import com.alya.ecommerce_serang.data.api.dto.UserProfile
|
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.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.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.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) {
|
class UserRepository(private val apiService: ApiService) {
|
||||||
|
|
||||||
//post data without message/response
|
//post data without message/response
|
||||||
suspend fun requestOtpRep(email: String): OtpResponse {
|
suspend fun requestOtpRep(email: String): OtpResponse {
|
||||||
return apiService.getOTP(OtpRequest(email))
|
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
|
val response = apiService.register(request) // API call
|
||||||
|
|
||||||
if (response.isSuccessful) {
|
if (response.isSuccessful) {
|
||||||
val responseBody = response.body() ?: throw Exception("Empty response body")
|
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 {
|
} else {
|
||||||
throw Exception("Registration failed: ${response.errorBody()?.string()}")
|
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> {
|
suspend fun login(email: String, password: String): Result<LoginResponse> {
|
||||||
return try {
|
return try {
|
||||||
val response = apiService.login(LoginRequest(email, password))
|
val response = apiService.login(LoginRequest(email, password))
|
||||||
@ -56,101 +266,120 @@ class UserRepository(private val apiService: ApiService) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// suspend fun sendChatMessage(
|
suspend fun editProfileCust(
|
||||||
// storeId: Int,
|
context: Context,
|
||||||
// message: String,
|
username: String,
|
||||||
// productId: Int,
|
name: String,
|
||||||
// imageFile: File? = null
|
phone: String,
|
||||||
// ): Result<SendChatResponse> {
|
birthDate: String,
|
||||||
// return try {
|
email: String,
|
||||||
// // Create multipart request builder
|
imageUri: Uri?
|
||||||
// val requestBodyBuilder = MultipartBody.Builder().setType(MultipartBody.FORM)
|
): Result<EditProfileResponse> {
|
||||||
//
|
return try {
|
||||||
// // Add text fields
|
// Log the data being sent
|
||||||
// requestBodyBuilder.addFormDataPart("store_id", storeId.toString())
|
Log.d(TAG, "Edit Profile - Username: $username, Name: $name, Phone: $phone, Birth Date: $birthDate, Email: $email")
|
||||||
// requestBodyBuilder.addFormDataPart("message", message)
|
Log.d(TAG, "Image URI: $imageUri")
|
||||||
// requestBodyBuilder.addFormDataPart("product_id", productId.toString())
|
|
||||||
//
|
// Create RequestBody objects for text fields
|
||||||
// // Add image if it exists
|
val usernameRequestBody = username.toRequestBody("text/plain".toMediaTypeOrNull())
|
||||||
// if (imageFile != null && imageFile.exists()) {
|
val nameRequestBody = name.toRequestBody("text/plain".toMediaTypeOrNull())
|
||||||
// val requestFile = imageFile.asRequestBody("image/*".toMediaTypeOrNull())
|
val phoneRequestBody = phone.toRequestBody("text/plain".toMediaTypeOrNull())
|
||||||
// requestBodyBuilder.addFormDataPart("chatimg", imageFile.name, requestFile)
|
val birthDateRequestBody = birthDate.toRequestBody("text/plain".toMediaTypeOrNull())
|
||||||
// }
|
val emailRequestBody = email.toRequestBody("text/plain".toMediaTypeOrNull())
|
||||||
//
|
|
||||||
// // Build the final request body
|
// Create MultipartBody.Part for the image
|
||||||
// val requestBody = requestBodyBuilder.build()
|
val imagePart = if (imageUri != null) {
|
||||||
//
|
// Create a temporary file from the URI using the utility class
|
||||||
// // Make the API call using a custom endpoint that takes a plain MultipartBody
|
val imageFile = FileUtils.createTempFileFromUri(context, imageUri, "profile")
|
||||||
// val response = apiService.sendChatLineWithBody(requestBody)
|
if (imageFile != null) {
|
||||||
//
|
// Create MultipartBody.Part from the file
|
||||||
// if (response.isSuccessful) {
|
FileUtils.createMultipartFromFile("userimg", imageFile)
|
||||||
// response.body()?.let {
|
} else {
|
||||||
// Result.Success(it)
|
// Fallback to empty part
|
||||||
// } ?: Result.Error(Exception("Send chat response is empty"))
|
FileUtils.createEmptyMultipart("userimg")
|
||||||
// } else {
|
}
|
||||||
// val errorBody = response.errorBody()?.string() ?: "Unknown error"
|
} else {
|
||||||
// Log.e("ChatRepository", "HTTP Error: ${response.code()}, Body: $errorBody")
|
// No image selected, use empty part
|
||||||
// Result.Error(Exception("API Error: ${response.code()} - $errorBody"))
|
FileUtils.createEmptyMultipart("userimg")
|
||||||
// }
|
}
|
||||||
// } catch (e: Exception) {
|
|
||||||
// Log.e("ChatRepository", "Exception sending message", e)
|
// Make the API call
|
||||||
// e.printStackTrace()
|
val response = apiService.editProfileCustomer(
|
||||||
// Result.Error(e)
|
username = usernameRequestBody,
|
||||||
// }
|
name = nameRequestBody,
|
||||||
// }
|
phone = phoneRequestBody,
|
||||||
//
|
birthDate = birthDateRequestBody,
|
||||||
// /**
|
userimg = imagePart,
|
||||||
// * Updates the status of a message (sent, delivered, read)
|
email = emailRequestBody
|
||||||
// *
|
)
|
||||||
// * @param messageId The ID of the message to update
|
|
||||||
// * @param status The new status to set
|
// Process the response
|
||||||
// * @return Result containing the updated message details or error
|
if (response.isSuccessful) {
|
||||||
// */
|
val editResponse = response.body()
|
||||||
// suspend fun updateMessageStatus(
|
if (editResponse != null) {
|
||||||
// messageId: Int,
|
Log.d(TAG, "Edit profile success: ${editResponse.message}")
|
||||||
// status: String
|
Result.Success(editResponse)
|
||||||
// ): Result<UpdateChatResponse> {
|
} else {
|
||||||
// return try {
|
Log.e(TAG, "Response body is null")
|
||||||
// val requestBody = UpdateChatRequest(
|
Result.Error(Exception("Empty response from server"))
|
||||||
// id = messageId,
|
}
|
||||||
// status = status
|
} else {
|
||||||
// )
|
val errorBody = response.errorBody()?.string() ?: "Unknown Error"
|
||||||
//
|
Log.e(TAG, "Error editing profile: $errorBody")
|
||||||
// val response = apiService.updateChatStatus(requestBody)
|
Result.Error(Exception(errorBody))
|
||||||
//
|
}
|
||||||
// if (response.isSuccessful) {
|
} catch (e: Exception) {
|
||||||
// response.body()?.let {
|
Log.e(TAG, "Exception in editProfileCust: ${e.message}")
|
||||||
// Result.Success(it)
|
e.printStackTrace()
|
||||||
// } ?: Result.Error(Exception("Update status response is empty"))
|
Result.Error(e)
|
||||||
// } else {
|
}
|
||||||
// Result.Error(Exception(response.errorBody()?.string() ?: "Unknown error"))
|
}
|
||||||
// }
|
|
||||||
// } catch (e: Exception) {
|
suspend fun checkStore(): HasStoreResponse{
|
||||||
// Result.Error(e)
|
return apiService.checkStoreUser()
|
||||||
// }
|
}
|
||||||
// }
|
|
||||||
//
|
suspend fun checkValue(request: VerifRegisReq): VerifRegisterResponse{
|
||||||
// /**
|
return apiService.verifValue(request)
|
||||||
// * Gets the chat history for a specific chat room
|
}
|
||||||
// *
|
|
||||||
// * @param chatRoomId The ID of the chat room
|
suspend fun sendFcm(request: FcmReq): FcmTokenResponse{
|
||||||
// * @return Result containing the list of chat messages or error
|
return apiService.updateFcm(request)
|
||||||
// */
|
}
|
||||||
// suspend fun getChatHistory(chatRoomId: Int): Result<ChatHistoryResponse> {
|
|
||||||
// return try {
|
suspend fun getListNotif(): Result<List<NotifItem>> {
|
||||||
// val response = apiService.getChatDetail(chatRoomId)
|
return try {
|
||||||
//
|
val response = apiService.getNotif()
|
||||||
// if (response.isSuccessful) {
|
|
||||||
// response.body()?.let {
|
if (response.isSuccessful){
|
||||||
// Result.Success(it)
|
val chat = response.body()?.notif ?: emptyList()
|
||||||
// } ?: Result.Error(Exception("Chat history response is empty"))
|
Result.Success(chat)
|
||||||
// } else {
|
} else {
|
||||||
// Result.Error(Exception(response.errorBody()?.string() ?: "Unknown error"))
|
Result.Error(Exception("Failed to fetch list notif. Code: ${response.code()}"))
|
||||||
// }
|
}
|
||||||
// } catch (e: Exception) {
|
} catch (e: Exception){
|
||||||
// Result.Error(e)
|
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
|
package com.alya.ecommerce_serang.ui
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.util.Log
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.activity.enableEdgeToEdge
|
import androidx.activity.enableEdgeToEdge
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
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.databinding.ActivityMainBinding
|
||||||
import com.alya.ecommerce_serang.ui.notif.WebSocketManager
|
import com.alya.ecommerce_serang.ui.notif.WebSocketManager
|
||||||
import com.alya.ecommerce_serang.utils.SessionManager
|
import com.alya.ecommerce_serang.utils.SessionManager
|
||||||
|
import com.google.firebase.FirebaseApp
|
||||||
|
import com.google.firebase.messaging.FirebaseMessaging
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class MainActivity : AppCompatActivity() {
|
class MainActivity : AppCompatActivity() {
|
||||||
|
private val TAG = "MainActivity"
|
||||||
|
|
||||||
private lateinit var binding: ActivityMainBinding
|
private lateinit var binding: ActivityMainBinding
|
||||||
private lateinit var apiService: ApiService
|
private lateinit var apiService: ApiService
|
||||||
private lateinit var sessionManager: SessionManager
|
private lateinit var sessionManager: SessionManager
|
||||||
|
|
||||||
// private val viewModel: NotifViewModel by viewModels()
|
// private val viewModel: NotifViewModel by viewModels()
|
||||||
private val navController by lazy {
|
private val navController by lazy {
|
||||||
(supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment).navController
|
(supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment).navController
|
||||||
@ -64,6 +71,11 @@ class MainActivity : AppCompatActivity() {
|
|||||||
)
|
)
|
||||||
windowInsets
|
windowInsets
|
||||||
}
|
}
|
||||||
|
// Initialize Firebase
|
||||||
|
FirebaseApp.initializeApp(this)
|
||||||
|
|
||||||
|
// Request FCM token at app startup
|
||||||
|
retrieveFCMToken()
|
||||||
|
|
||||||
requestNotificationPermissionIfNeeded()
|
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
|
package com.alya.ecommerce_serang.ui.auth
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.util.Log
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.activity.enableEdgeToEdge
|
import androidx.activity.enableEdgeToEdge
|
||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
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.api.retrofit.ApiConfig
|
||||||
import com.alya.ecommerce_serang.data.repository.Result
|
import com.alya.ecommerce_serang.data.repository.Result
|
||||||
import com.alya.ecommerce_serang.data.repository.UserRepository
|
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.BaseViewModelFactory
|
||||||
import com.alya.ecommerce_serang.utils.SessionManager
|
import com.alya.ecommerce_serang.utils.SessionManager
|
||||||
import com.alya.ecommerce_serang.utils.viewmodel.LoginViewModel
|
import com.alya.ecommerce_serang.utils.viewmodel.LoginViewModel
|
||||||
|
import com.google.firebase.FirebaseApp
|
||||||
|
import com.google.firebase.messaging.FirebaseMessaging
|
||||||
|
|
||||||
class LoginActivity : AppCompatActivity() {
|
class LoginActivity : AppCompatActivity() {
|
||||||
|
|
||||||
|
private val TAG = "LoginActivity"
|
||||||
|
|
||||||
private lateinit var binding: ActivityLoginBinding
|
private lateinit var binding: ActivityLoginBinding
|
||||||
private val loginViewModel: LoginViewModel by viewModels{
|
private val loginViewModel: LoginViewModel by viewModels{
|
||||||
BaseViewModelFactory {
|
BaseViewModelFactory {
|
||||||
val apiService = ApiConfig.getUnauthenticatedApiService()
|
val apiService = ApiConfig.getUnauthenticatedApiService()
|
||||||
val userRepository = UserRepository(apiService)
|
val userRepository = UserRepository(apiService)
|
||||||
LoginViewModel(userRepository)
|
LoginViewModel(userRepository, this)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -35,6 +42,10 @@ class LoginActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
setupListeners()
|
setupListeners()
|
||||||
observeLoginState()
|
observeLoginState()
|
||||||
|
|
||||||
|
FirebaseApp.initializeApp(this)
|
||||||
|
|
||||||
|
// Request FCM token at app startup
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setupListeners() {
|
private fun setupListeners() {
|
||||||
@ -58,6 +69,7 @@ class LoginActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
val sessionManager = SessionManager(this)
|
val sessionManager = SessionManager(this)
|
||||||
sessionManager.saveToken(accessToken)
|
sessionManager.saveToken(accessToken)
|
||||||
|
retrieveFCMToken()
|
||||||
// sessionManager.saveUserId(response.userId)
|
// sessionManager.saveUserId(response.userId)
|
||||||
|
|
||||||
Toast.makeText(this, "Login Successful", Toast.LENGTH_SHORT).show()
|
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
|
package com.alya.ecommerce_serang.ui.auth
|
||||||
|
|
||||||
import android.app.DatePickerDialog
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.widget.Toast
|
|
||||||
import androidx.activity.enableEdgeToEdge
|
import androidx.activity.enableEdgeToEdge
|
||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.core.view.ViewCompat
|
import androidx.core.view.ViewCompat
|
||||||
import androidx.core.view.WindowCompat
|
import androidx.core.view.WindowCompat
|
||||||
import androidx.core.view.WindowInsetsCompat
|
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.dto.RegisterRequest
|
||||||
import com.alya.ecommerce_serang.data.api.retrofit.ApiConfig
|
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.data.repository.UserRepository
|
||||||
import com.alya.ecommerce_serang.databinding.ActivityRegisterBinding
|
import com.alya.ecommerce_serang.databinding.ActivityRegisterBinding
|
||||||
import com.alya.ecommerce_serang.ui.MainActivity
|
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.BaseViewModelFactory
|
||||||
import com.alya.ecommerce_serang.utils.SessionManager
|
import com.alya.ecommerce_serang.utils.SessionManager
|
||||||
import com.alya.ecommerce_serang.utils.viewmodel.RegisterViewModel
|
import com.alya.ecommerce_serang.utils.viewmodel.RegisterViewModel
|
||||||
import java.text.SimpleDateFormat
|
|
||||||
import java.util.Calendar
|
|
||||||
import java.util.Locale
|
|
||||||
|
|
||||||
class RegisterActivity : AppCompatActivity() {
|
class RegisterActivity : AppCompatActivity() {
|
||||||
private lateinit var binding: ActivityRegisterBinding
|
private lateinit var binding: ActivityRegisterBinding
|
||||||
private lateinit var sessionManager: SessionManager
|
private lateinit var sessionManager: SessionManager
|
||||||
|
|
||||||
|
|
||||||
private val registerViewModel: RegisterViewModel by viewModels{
|
private val registerViewModel: RegisterViewModel by viewModels{
|
||||||
BaseViewModelFactory {
|
BaseViewModelFactory {
|
||||||
val apiService = ApiConfig.getUnauthenticatedApiService()
|
val apiService = ApiConfig.getUnauthenticatedApiService()
|
||||||
|
val orderRepository = OrderRepository(apiService)
|
||||||
val userRepository = UserRepository(apiService)
|
val userRepository = UserRepository(apiService)
|
||||||
RegisterViewModel(userRepository)
|
RegisterViewModel(userRepository, orderRepository, this)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -76,123 +78,27 @@ class RegisterActivity : AppCompatActivity() {
|
|||||||
windowInsets
|
windowInsets
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (savedInstanceState == null) {
|
||||||
// Observe OTP state
|
supportFragmentManager.beginTransaction()
|
||||||
observeOtpState()
|
.replace(R.id.fragment_container, RegisterStep1Fragment.newInstance())
|
||||||
|
.commit()
|
||||||
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()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun observeOtpState() {
|
// Function to navigate to the next fragment
|
||||||
registerViewModel.otpState.observe(this) { result ->
|
fun navigateToStep(step: Int, userData: RegisterRequest?) {
|
||||||
when (result) {
|
val fragment = when (step) {
|
||||||
is Result.Loading -> {
|
1 -> RegisterStep1Fragment.newInstance()
|
||||||
// Show loading indicator
|
2 -> RegisterStep2Fragment.newInstance(userData)
|
||||||
binding.progressBarOtp.visibility = android.view.View.VISIBLE
|
3 -> RegisterStep3Fragment.newInstance()
|
||||||
}
|
else -> null
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun observeRegisterState() {
|
fragment?.let {
|
||||||
registerViewModel.registerState.observe(this) { result ->
|
supportFragmentManager.beginTransaction()
|
||||||
when (result) {
|
.replace(R.id.fragment_container, it)
|
||||||
is Result.Loading -> {
|
.addToBackStack(null)
|
||||||
// Show loading indicator for registration
|
.commit()
|
||||||
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()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -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 productRating = intent.getFloatExtra(Constants.EXTRA_PRODUCT_RATING, 0f)
|
||||||
val storeName = intent.getStringExtra(Constants.EXTRA_STORE_NAME) ?: ""
|
val storeName = intent.getStringExtra(Constants.EXTRA_STORE_NAME) ?: ""
|
||||||
val chatRoomId = intent.getIntExtra(Constants.EXTRA_CHAT_ROOM_ID, 0)
|
val chatRoomId = intent.getIntExtra(Constants.EXTRA_CHAT_ROOM_ID, 0)
|
||||||
|
val storeImg = intent.getStringExtra(Constants.EXTRA_STORE_IMAGE) ?: ""
|
||||||
|
|
||||||
// Check if user is logged in
|
// Check if user is logged in
|
||||||
val token = sessionManager.getToken()
|
val token = sessionManager.getToken()
|
||||||
@ -137,7 +138,20 @@ class ChatActivity : AppCompatActivity() {
|
|||||||
return
|
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(
|
viewModel.setChatParameters(
|
||||||
storeId = storeId,
|
storeId = storeId,
|
||||||
productId = productId,
|
productId = productId,
|
||||||
@ -227,6 +241,7 @@ class ChatActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// Observe state changes using LiveData
|
// Observe state changes using LiveData
|
||||||
viewModel.state.observe(this, Observer { state ->
|
viewModel.state.observe(this, Observer { state ->
|
||||||
// Update messages
|
// Update messages
|
||||||
@ -244,6 +259,7 @@ class ChatActivity : AppCompatActivity() {
|
|||||||
binding.ratingBar.rating = state.productRating
|
binding.ratingBar.rating = state.productRating
|
||||||
binding.tvRating.text = state.productRating.toString()
|
binding.tvRating.text = state.productRating.toString()
|
||||||
binding.tvSellerName.text = state.storeName
|
binding.tvSellerName.text = state.storeName
|
||||||
|
binding.tvStoreName.text=state.storeName
|
||||||
|
|
||||||
// Load product image
|
// Load product image
|
||||||
if (!state.productImageUrl.isNullOrEmpty()) {
|
if (!state.productImageUrl.isNullOrEmpty()) {
|
||||||
@ -270,6 +286,7 @@ class ChatActivity : AppCompatActivity() {
|
|||||||
binding.editTextMessage.hint = getString(R.string.write_message)
|
binding.editTextMessage.hint = getString(R.string.write_message)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Show typing indicator
|
// Show typing indicator
|
||||||
binding.tvTypingIndicator.visibility =
|
binding.tvTypingIndicator.visibility =
|
||||||
if (state.isOtherUserTyping) View.VISIBLE else View.GONE
|
if (state.isOtherUserTyping) View.VISIBLE else View.GONE
|
||||||
@ -467,7 +484,8 @@ class ChatActivity : AppCompatActivity() {
|
|||||||
productImage: String? = null,
|
productImage: String? = null,
|
||||||
productRating: String? = null,
|
productRating: String? = null,
|
||||||
storeName: String? = null,
|
storeName: String? = null,
|
||||||
chatRoomId: Int = 0
|
chatRoomId: Int = 0,
|
||||||
|
storeImage: String? = null
|
||||||
) {
|
) {
|
||||||
val intent = Intent(context, ChatActivity::class.java).apply {
|
val intent = Intent(context, ChatActivity::class.java).apply {
|
||||||
putExtra(Constants.EXTRA_STORE_ID, storeId)
|
putExtra(Constants.EXTRA_STORE_ID, storeId)
|
||||||
@ -475,6 +493,7 @@ class ChatActivity : AppCompatActivity() {
|
|||||||
putExtra(Constants.EXTRA_PRODUCT_NAME, productName)
|
putExtra(Constants.EXTRA_PRODUCT_NAME, productName)
|
||||||
putExtra(Constants.EXTRA_PRODUCT_PRICE, productPrice)
|
putExtra(Constants.EXTRA_PRODUCT_PRICE, productPrice)
|
||||||
putExtra(Constants.EXTRA_PRODUCT_IMAGE, productImage)
|
putExtra(Constants.EXTRA_PRODUCT_IMAGE, productImage)
|
||||||
|
putExtra(Constants.EXTRA_STORE_IMAGE, storeImage)
|
||||||
|
|
||||||
// Convert productRating string to float if provided
|
// Convert productRating string to float if provided
|
||||||
if (productRating != null) {
|
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 val binding get() = _binding!!
|
||||||
private lateinit var socketService: SocketIOService
|
private lateinit var socketService: SocketIOService
|
||||||
private lateinit var sessionManager: SessionManager
|
private lateinit var sessionManager: SessionManager
|
||||||
|
|
||||||
private val viewModel: com.alya.ecommerce_serang.ui.chat.ChatViewModel by viewModels {
|
private val viewModel: com.alya.ecommerce_serang.ui.chat.ChatViewModel by viewModels {
|
||||||
BaseViewModelFactory {
|
BaseViewModelFactory {
|
||||||
val apiService = ApiConfig.getApiService(sessionManager)
|
val apiService = ApiConfig.getApiService(sessionManager)
|
||||||
@ -65,7 +66,8 @@ class ChatListFragment : Fragment() {
|
|||||||
productImage = null,
|
productImage = null,
|
||||||
productRating = null,
|
productRating = null,
|
||||||
storeName = chatItem.storeName,
|
storeName = chatItem.storeName,
|
||||||
chatRoomId = chatItem.chatRoomId
|
chatRoomId = chatItem.chatRoomId,
|
||||||
|
storeImage = chatItem.storeImage
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
binding.chatListRecyclerView.adapter = adapter
|
binding.chatListRecyclerView.adapter = adapter
|
||||||
@ -85,4 +87,8 @@ class ChatListFragment : Fragment() {
|
|||||||
super.onDestroyView()
|
super.onDestroyView()
|
||||||
_binding = null
|
_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.ChatItem
|
||||||
import com.alya.ecommerce_serang.data.api.response.chat.ChatItemList
|
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.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.ChatRepository
|
||||||
import com.alya.ecommerce_serang.data.repository.Result
|
import com.alya.ecommerce_serang.data.repository.Result
|
||||||
import com.alya.ecommerce_serang.utils.Constants
|
import com.alya.ecommerce_serang.utils.Constants
|
||||||
@ -23,6 +24,8 @@ import javax.inject.Inject
|
|||||||
class ChatViewModel @Inject constructor(
|
class ChatViewModel @Inject constructor(
|
||||||
private val chatRepository: ChatRepository,
|
private val chatRepository: ChatRepository,
|
||||||
private val socketService: SocketIOService,
|
private val socketService: SocketIOService,
|
||||||
|
|
||||||
|
|
||||||
private val sessionManager: SessionManager
|
private val sessionManager: SessionManager
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
@ -38,6 +41,9 @@ class ChatViewModel @Inject constructor(
|
|||||||
private val _chatList = MutableLiveData<Result<List<ChatItemList>>>()
|
private val _chatList = MutableLiveData<Result<List<ChatItemList>>>()
|
||||||
val chatList: LiveData<Result<List<ChatItemList>>> = _chatList
|
val chatList: LiveData<Result<List<ChatItemList>>> = _chatList
|
||||||
|
|
||||||
|
private val _storeDetail = MutableLiveData<Result<StoreProduct?>>()
|
||||||
|
val storeDetail : LiveData<Result<StoreProduct?>> get() = _storeDetail
|
||||||
|
|
||||||
// Store and product parameters
|
// Store and product parameters
|
||||||
private var storeId: Int = 0
|
private var storeId: Int = 0
|
||||||
private var productId: 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.api.retrofit.ApiConfig
|
||||||
import com.alya.ecommerce_serang.data.repository.ProductRepository
|
import com.alya.ecommerce_serang.data.repository.ProductRepository
|
||||||
import com.alya.ecommerce_serang.databinding.FragmentHomeBinding
|
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.notif.NotificationActivity
|
||||||
import com.alya.ecommerce_serang.ui.product.DetailProductActivity
|
import com.alya.ecommerce_serang.ui.product.DetailProductActivity
|
||||||
import com.alya.ecommerce_serang.utils.BaseViewModelFactory
|
import com.alya.ecommerce_serang.utils.BaseViewModelFactory
|
||||||
@ -129,6 +130,8 @@ class HomeFragment : Fragment() {
|
|||||||
// Setup cart and notification buttons
|
// Setup cart and notification buttons
|
||||||
binding.searchContainer.btnCart.setOnClickListener {
|
binding.searchContainer.btnCart.setOnClickListener {
|
||||||
// Navigate to cart
|
// Navigate to cart
|
||||||
|
val intent = Intent(requireContext(), CartActivity::class.java)
|
||||||
|
startActivity(intent)
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.searchContainer.btnNotification.setOnClickListener {
|
binding.searchContainer.btnNotification.setOnClickListener {
|
||||||
|
|||||||
@ -1,67 +1,136 @@
|
|||||||
package com.alya.ecommerce_serang.ui.notif
|
package com.alya.ecommerce_serang.ui.notif
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.core.app.NotificationCompat
|
import android.util.Log
|
||||||
import androidx.core.app.NotificationManagerCompat
|
import androidx.lifecycle.LiveData
|
||||||
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
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.Result
|
||||||
import com.alya.ecommerce_serang.data.repository.UserRepository
|
import com.alya.ecommerce_serang.data.repository.UserRepository
|
||||||
import com.alya.ecommerce_serang.utils.SessionManager
|
import com.alya.ecommerce_serang.utils.SessionManager
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
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 kotlinx.coroutines.launch
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class NotifViewModel @Inject constructor(
|
class NotifViewModel @Inject constructor(
|
||||||
private val notificationBuilder: NotificationCompat.Builder,
|
|
||||||
private val notificationManager: NotificationManagerCompat,
|
|
||||||
@ApplicationContext private val context: Context,
|
@ApplicationContext private val context: Context,
|
||||||
private val userRepository: UserRepository,
|
private val userRepository: UserRepository,
|
||||||
private val webSocketManager: WebSocketManager,
|
|
||||||
private val sessionManager: SessionManager
|
private val sessionManager: SessionManager
|
||||||
|
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
private val _userProfile = MutableStateFlow<Result<UserProfile?>>(Result.Loading)
|
private val _notifList = MutableLiveData<Result<List<NotifItem>>>()
|
||||||
val userProfile: StateFlow<Result<UserProfile?>> = _userProfile.asStateFlow()
|
val notifList: LiveData<Result<List<NotifItem>>> = _notifList
|
||||||
|
|
||||||
init {
|
private val _checkStore = MutableLiveData<Boolean>()
|
||||||
fetchUserProfile()
|
val checkStore: LiveData<Boolean> = _checkStore
|
||||||
}
|
|
||||||
|
|
||||||
// Fetch user profile to get necessary data
|
private val _notifStoreList = MutableLiveData<Result<List<NotifstoreItem>>>()
|
||||||
fun fetchUserProfile() {
|
val notifStoreList: LiveData<Result<List<NotifstoreItem>>> = _notifStoreList
|
||||||
|
|
||||||
|
fun getNotifList() {
|
||||||
|
Log.d(TAG, "getNotifList: Fetching personal notifications")
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
_userProfile.value = Result.Loading
|
try {
|
||||||
val result = userRepository.fetchUserProfile()
|
Log.d(TAG, "getNotifList: Setting state to Loading")
|
||||||
_userProfile.value = result
|
_notifList.value = Result.Loading
|
||||||
|
|
||||||
// If successful, save the user ID for WebSocket use
|
Log.d(TAG, "getNotifList: Calling repository to get notifications")
|
||||||
if (result is Result.Success && result.data != null) {
|
val result = userRepository.getListNotif()
|
||||||
sessionManager.saveUserId(result.data.userId.toString())
|
|
||||||
|
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 getNotifStoreList() {
|
||||||
fun startWebSocketConnection() {
|
Log.d(TAG, "getNotifStoreList: Fetching store notifications")
|
||||||
webSocketManager.startWebSocketConnection()
|
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 checkStoreUser() {
|
||||||
fun stopWebSocketConnection() {
|
Log.d(TAG, "checkStoreUser: Checking if user has a store")
|
||||||
webSocketManager.stopWebSocketConnection()
|
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)
|
companion object {
|
||||||
override fun onCleared() {
|
private const val TAG = "NotifViewModel" // Constant for logging tag
|
||||||
super.onCleared()
|
|
||||||
// No need to stop here - the service will manage its own lifecycle
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -1,118 +1,324 @@
|
|||||||
package com.alya.ecommerce_serang.ui.notif
|
package com.alya.ecommerce_serang.ui.notif
|
||||||
|
|
||||||
import android.content.pm.PackageManager
|
|
||||||
import android.os.Build
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.widget.Toast
|
import android.util.Log
|
||||||
|
import android.view.View
|
||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.core.app.ActivityCompat
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import androidx.lifecycle.Lifecycle
|
|
||||||
import androidx.lifecycle.lifecycleScope
|
|
||||||
import androidx.lifecycle.repeatOnLifecycle
|
|
||||||
import com.alya.ecommerce_serang.data.repository.Result
|
import com.alya.ecommerce_serang.data.repository.Result
|
||||||
import com.alya.ecommerce_serang.databinding.ActivityNotificationBinding
|
import com.alya.ecommerce_serang.databinding.ActivityNotificationBinding
|
||||||
|
import com.google.android.material.tabs.TabLayout
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
|
|
||||||
@AndroidEntryPoint // Required for Hilt
|
private const val TAG = "NotificationActivity"
|
||||||
|
|
||||||
|
@AndroidEntryPoint
|
||||||
class NotificationActivity : AppCompatActivity() {
|
class NotificationActivity : AppCompatActivity() {
|
||||||
|
|
||||||
private lateinit var binding: ActivityNotificationBinding
|
private lateinit var binding: ActivityNotificationBinding
|
||||||
private val viewModel: NotifViewModel by viewModels()
|
private val viewModel: NotifViewModel by viewModels()
|
||||||
|
|
||||||
// Permission request code
|
private lateinit var personalAdapter: PersonalNotificationAdapter
|
||||||
private val NOTIFICATION_PERMISSION_CODE = 100
|
private lateinit var storeAdapter: StoreNotificationAdapter
|
||||||
|
|
||||||
|
private var hasStore = false
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
Log.d(TAG, "onCreate: Starting NotificationActivity")
|
||||||
|
binding = ActivityNotificationBinding.inflate(layoutInflater)
|
||||||
|
setContentView(binding.root)
|
||||||
|
|
||||||
lifecycleScope.launch {
|
setupToolbar()
|
||||||
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
setupAdapters()
|
||||||
viewModel.userProfile.collect { result ->
|
setupTabLayout()
|
||||||
when (result) {
|
setupSwipeRefresh()
|
||||||
is com.alya.ecommerce_serang.data.repository.Result.Success -> {
|
setupObservers()
|
||||||
// User profile loaded successfully
|
|
||||||
// Potentially do something with user profile
|
// 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 -> {
|
showStoreNotifications()
|
||||||
// Handle error - show message, etc.
|
}
|
||||||
Toast.makeText(this@NotificationActivity,
|
}
|
||||||
"Failed to load profile",
|
}
|
||||||
Toast.LENGTH_SHORT
|
|
||||||
).show()
|
override fun onTabUnselected(tab: TabLayout.Tab) {}
|
||||||
}
|
|
||||||
Result.Loading -> {
|
override fun onTabReselected(tab: TabLayout.Tab) {}
|
||||||
// Show loading indicator if needed
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
// Observe store notifications
|
||||||
// viewModel.startWebSocketConnection()
|
viewModel.notifStoreList.observe(this) { result ->
|
||||||
|
Log.d(TAG, "notifStoreList observed: ${result.javaClass.simpleName}")
|
||||||
|
binding.swipeRefreshLayout.isRefreshing = false
|
||||||
|
|
||||||
binding = ActivityNotificationBinding.inflate(layoutInflater)
|
if (binding.tabLayout.selectedTabPosition == 1) {
|
||||||
setContentView(binding.root)
|
when (result) {
|
||||||
|
is Result.Success -> {
|
||||||
// Check and request notification permission for Android 13+
|
val notifications = result.data
|
||||||
requestNotificationPermissionIfNeeded()
|
Log.d(TAG, "Store notifications received: ${notifications?.size ?: 0}, hasStore=$hasStore")
|
||||||
|
if (!hasStore) {
|
||||||
// Set up button click listeners
|
showEmptyState("Anda belum memiliki toko", true)
|
||||||
// setupButtonListeners()
|
} else if (notifications.isNullOrEmpty()) {
|
||||||
|
showEmptyState("Belum Ada Notifikasi Toko", false)
|
||||||
|
} else {
|
||||||
}
|
hideEmptyState()
|
||||||
|
// Ensure adapter is attached
|
||||||
// private fun setupButtonListeners() {
|
if (binding.recyclerViewNotif.adapter != storeAdapter) {
|
||||||
// binding.simpleNotification.setOnClickListener {
|
Log.d(TAG, "Re-attaching storeAdapter to RecyclerView")
|
||||||
// viewModel.showSimpleNotification()
|
binding.recyclerViewNotif.adapter = storeAdapter
|
||||||
// }
|
}
|
||||||
//
|
storeAdapter.submitList(notifications)
|
||||||
// binding.updateNotification.setOnClickListener {
|
// Force a layout pass
|
||||||
// viewModel.updateSimpleNotification()
|
binding.recyclerViewNotif.post {
|
||||||
// }
|
Log.d(TAG, "Forcing layout pass on RecyclerView")
|
||||||
//
|
binding.recyclerViewNotif.requestLayout()
|
||||||
// binding.cancelNotification.setOnClickListener {
|
}
|
||||||
// viewModel.cancelSimpleNotification()
|
}
|
||||||
// }
|
}
|
||||||
// }
|
is Result.Error -> {
|
||||||
|
Log.e(TAG, "Error loading store notifications", result.exception)
|
||||||
private fun requestNotificationPermissionIfNeeded() {
|
showEmptyState("Gagal memuat notifikasi toko", false)
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
}
|
||||||
if (ContextCompat.checkSelfPermission(
|
is Result.Loading -> {
|
||||||
this,
|
Log.d(TAG, "Loading store notifications")
|
||||||
android.Manifest.permission.POST_NOTIFICATIONS
|
}
|
||||||
) != PackageManager.PERMISSION_GRANTED
|
}
|
||||||
) {
|
|
||||||
ActivityCompat.requestPermissions(
|
|
||||||
this,
|
|
||||||
arrayOf(android.Manifest.permission.POST_NOTIFICATIONS),
|
|
||||||
NOTIFICATION_PERMISSION_CODE
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle permission request result
|
private fun showPersonalNotifications() {
|
||||||
override fun onRequestPermissionsResult(
|
Log.d(TAG, "showPersonalNotifications called")
|
||||||
requestCode: Int,
|
val result = viewModel.notifList.value
|
||||||
permissions: Array<out String>,
|
|
||||||
grantResults: IntArray
|
|
||||||
) {
|
|
||||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
|
|
||||||
|
|
||||||
if (requestCode == NOTIFICATION_PERMISSION_CODE) {
|
if (result is Result.Success) {
|
||||||
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
val notifications = result.data
|
||||||
// Permission granted
|
Log.d(TAG, "showPersonalNotifications: Success with ${notifications?.size ?: 0} notifications")
|
||||||
Toast.makeText(this, "Notification permission granted", Toast.LENGTH_SHORT).show()
|
if (notifications.isNullOrEmpty()) {
|
||||||
|
showEmptyState("Belum Ada Notifikasi", false)
|
||||||
} else {
|
} else {
|
||||||
// Permission denied
|
hideEmptyState()
|
||||||
Toast.makeText(this, "Notification permission denied", Toast.LENGTH_SHORT).show()
|
if (binding.recyclerViewNotif.adapter != personalAdapter) {
|
||||||
// You might want to show a dialog explaining why notifications are important
|
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.view.ViewGroup
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
|
import androidx.activity.enableEdgeToEdge
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
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.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.alya.ecommerce_serang.data.api.dto.CheckoutData
|
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.OrderRequest
|
||||||
import com.alya.ecommerce_serang.data.api.dto.OrderRequestBuy
|
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.api.retrofit.ApiConfig
|
||||||
import com.alya.ecommerce_serang.data.repository.OrderRepository
|
import com.alya.ecommerce_serang.data.repository.OrderRepository
|
||||||
import com.alya.ecommerce_serang.databinding.ActivityCheckoutBinding
|
import com.alya.ecommerce_serang.databinding.ActivityCheckoutBinding
|
||||||
@ -30,6 +34,7 @@ class CheckoutActivity : AppCompatActivity() {
|
|||||||
private lateinit var binding: ActivityCheckoutBinding
|
private lateinit var binding: ActivityCheckoutBinding
|
||||||
private lateinit var sessionManager: SessionManager
|
private lateinit var sessionManager: SessionManager
|
||||||
private var paymentAdapter: PaymentMethodAdapter? = null
|
private var paymentAdapter: PaymentMethodAdapter? = null
|
||||||
|
private var paymentMethodsLoaded = false
|
||||||
|
|
||||||
private val viewModel: CheckoutViewModel by viewModels {
|
private val viewModel: CheckoutViewModel by viewModels {
|
||||||
BaseViewModelFactory {
|
BaseViewModelFactory {
|
||||||
@ -46,6 +51,21 @@ class CheckoutActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
sessionManager = SessionManager(this)
|
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
|
// Setup UI components
|
||||||
setupToolbar()
|
setupToolbar()
|
||||||
@ -57,6 +77,7 @@ class CheckoutActivity : AppCompatActivity() {
|
|||||||
private fun processIntentData() {
|
private fun processIntentData() {
|
||||||
// Determine if this is Buy Now or Cart checkout
|
// Determine if this is Buy Now or Cart checkout
|
||||||
val isBuyNow = intent.hasExtra(EXTRA_PRODUCT_ID) && !intent.hasExtra(EXTRA_CART_ITEM_IDS)
|
val isBuyNow = intent.hasExtra(EXTRA_PRODUCT_ID) && !intent.hasExtra(EXTRA_CART_ITEM_IDS)
|
||||||
|
val isWholesaleNow = intent.getBooleanExtra(EXTRA_ISWHOLESALE, false)
|
||||||
|
|
||||||
if (isBuyNow) {
|
if (isBuyNow) {
|
||||||
// Process Buy Now flow
|
// Process Buy Now flow
|
||||||
@ -67,23 +88,33 @@ class CheckoutActivity : AppCompatActivity() {
|
|||||||
productName = intent.getStringExtra(EXTRA_PRODUCT_NAME),
|
productName = intent.getStringExtra(EXTRA_PRODUCT_NAME),
|
||||||
productImage = intent.getStringExtra(EXTRA_PRODUCT_IMAGE),
|
productImage = intent.getStringExtra(EXTRA_PRODUCT_IMAGE),
|
||||||
quantity = intent.getIntExtra(EXTRA_QUANTITY, 1),
|
quantity = intent.getIntExtra(EXTRA_QUANTITY, 1),
|
||||||
price = intent.getDoubleExtra(EXTRA_PRICE, 0.0)
|
price = intent.getDoubleExtra(EXTRA_PRICE, 0.0),
|
||||||
|
isWholesale = isWholesaleNow
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
// Process Cart checkout flow
|
// Process Cart checkout flow
|
||||||
val cartItemIds = intent.getIntArrayExtra(EXTRA_CART_ITEM_IDS)?.toList() ?: emptyList()
|
val cartItemIds = intent.getIntArrayExtra(EXTRA_CART_ITEM_IDS)?.toList() ?: emptyList()
|
||||||
|
val isWholesaleArray = intent.getBooleanArrayExtra(EXTRA_CART_ITEM_WHOLESALE)
|
||||||
|
|
||||||
if (cartItemIds.isNotEmpty()) {
|
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 {
|
} else {
|
||||||
Toast.makeText(this, "Error: No cart items specified", Toast.LENGTH_SHORT).show()
|
Toast.makeText(this, "Error: No cart items specified", Toast.LENGTH_SHORT).show()
|
||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
viewModel.getPaymentMethods { paymentMethods ->
|
// viewModel.getPaymentMethods { paymentMethods ->
|
||||||
// Logging is just for debugging
|
// // Logging is just for debugging
|
||||||
Log.d("CheckoutActivity", "Loaded ${paymentMethods.size} payment methods")
|
// Log.d("CheckoutActivity", "Loaded ${paymentMethods.size} payment methods")
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setupToolbar() {
|
private fun setupToolbar() {
|
||||||
@ -97,6 +128,10 @@ class CheckoutActivity : AppCompatActivity() {
|
|||||||
viewModel.checkoutData.observe(this) { data ->
|
viewModel.checkoutData.observe(this) { data ->
|
||||||
setupProductRecyclerView(data)
|
setupProductRecyclerView(data)
|
||||||
updateOrderSummary()
|
updateOrderSummary()
|
||||||
|
|
||||||
|
if (data != null) {
|
||||||
|
viewModel.getPaymentMethods()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Observe address details
|
// Observe address details
|
||||||
@ -106,23 +141,27 @@ class CheckoutActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
viewModel.availablePaymentMethods.observe(this) { paymentMethods ->
|
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)
|
setupPaymentMethodsRecyclerView(paymentMethods)
|
||||||
|
paymentMethodsLoaded = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Observe selected payment
|
|
||||||
viewModel.selectedPayment.observe(this) { selectedPayment ->
|
viewModel.selectedPayment.observe(this) { selectedPayment ->
|
||||||
if (selectedPayment != null) {
|
if (selectedPayment != null) {
|
||||||
// Update the adapter to show the selected payment
|
Log.d("CheckoutActivity", "Observer notified of selected payment: ${selectedPayment.bankName}")
|
||||||
paymentAdapter?.setSelectedPaymentName(selectedPayment.name)
|
|
||||||
|
|
||||||
// Optional: Update other UI elements to show the selected payment
|
// Update the adapter ONLY if it exists
|
||||||
// For example: binding.tvSelectedPaymentMethod.text = selectedPayment.name
|
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
|
// Observe loading state
|
||||||
viewModel.isLoading.observe(this) { isLoading ->
|
viewModel.isLoading.observe(this) { isLoading ->
|
||||||
binding.btnPay.isEnabled = !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()) {
|
if (paymentMethods.isEmpty()) {
|
||||||
Log.e("CheckoutActivity", "Payment methods list is empty")
|
Log.e("CheckoutActivity", "Payment methods list is empty")
|
||||||
Toast.makeText(this, "No payment methods available", Toast.LENGTH_SHORT).show()
|
Toast.makeText(this, "No payment methods available", Toast.LENGTH_SHORT).show()
|
||||||
@ -156,18 +195,22 @@ class CheckoutActivity : AppCompatActivity() {
|
|||||||
// Debug logging
|
// Debug logging
|
||||||
Log.d("CheckoutActivity", "Setting up payment methods: ${paymentMethods.size} methods available")
|
Log.d("CheckoutActivity", "Setting up payment methods: ${paymentMethods.size} methods available")
|
||||||
|
|
||||||
paymentAdapter = PaymentMethodAdapter(paymentMethods) { payment ->
|
if (paymentAdapter == null) {
|
||||||
// We're using a hardcoded ID for now
|
paymentAdapter = PaymentMethodAdapter(paymentMethods) { payment ->
|
||||||
viewModel.setPaymentMethod(1)
|
Log.d("CheckoutActivity", "Payment selected in adapter: ${payment.bankName}")
|
||||||
}
|
|
||||||
|
|
||||||
binding.rvPaymentInfo.apply {
|
// Set this payment as selected in the ViewModel
|
||||||
layoutManager = LinearLayoutManager(this@CheckoutActivity)
|
viewModel.setPaymentMethod(payment.id)
|
||||||
adapter = paymentAdapter
|
}
|
||||||
|
|
||||||
|
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")
|
Log.d("CheckoutActivity", "Updating payment adapter with ${paymentMethods.size} methods")
|
||||||
|
|
||||||
// Simple test adapter
|
// Simple test adapter
|
||||||
@ -183,7 +226,7 @@ class CheckoutActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||||
val payment = paymentMethods[position]
|
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
|
// 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()
|
Toast.makeText(this, "Silakan pilih metode pembayaran", Toast.LENGTH_SHORT).show()
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@ -365,9 +414,12 @@ class CheckoutActivity : AppCompatActivity() {
|
|||||||
const val EXTRA_PRODUCT_IMAGE = "PRODUCT_IMAGE"
|
const val EXTRA_PRODUCT_IMAGE = "PRODUCT_IMAGE"
|
||||||
const val EXTRA_QUANTITY = "QUANTITY"
|
const val EXTRA_QUANTITY = "QUANTITY"
|
||||||
const val EXTRA_PRICE = "PRICE"
|
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
|
// Helper methods for starting activity
|
||||||
|
|
||||||
|
// TO DO: delete iswholesale klo ngga dibuthin
|
||||||
// For Buy Now
|
// For Buy Now
|
||||||
fun startForBuyNow(
|
fun startForBuyNow(
|
||||||
context: Context,
|
context: Context,
|
||||||
@ -377,7 +429,8 @@ class CheckoutActivity : AppCompatActivity() {
|
|||||||
productName: String?,
|
productName: String?,
|
||||||
productImage: String?,
|
productImage: String?,
|
||||||
quantity: Int,
|
quantity: Int,
|
||||||
price: Double
|
price: Double,
|
||||||
|
isWholesale: Boolean
|
||||||
) {
|
) {
|
||||||
val intent = Intent(context, CheckoutActivity::class.java).apply {
|
val intent = Intent(context, CheckoutActivity::class.java).apply {
|
||||||
putExtra(EXTRA_STORE_ID, storeId)
|
putExtra(EXTRA_STORE_ID, storeId)
|
||||||
@ -387,6 +440,7 @@ class CheckoutActivity : AppCompatActivity() {
|
|||||||
putExtra(EXTRA_PRODUCT_IMAGE, productImage)
|
putExtra(EXTRA_PRODUCT_IMAGE, productImage)
|
||||||
putExtra(EXTRA_QUANTITY, quantity)
|
putExtra(EXTRA_QUANTITY, quantity)
|
||||||
putExtra(EXTRA_PRICE, price)
|
putExtra(EXTRA_PRICE, price)
|
||||||
|
putExtra(EXTRA_ISWHOLESALE, isWholesale)
|
||||||
}
|
}
|
||||||
context.startActivity(intent)
|
context.startActivity(intent)
|
||||||
}
|
}
|
||||||
@ -394,10 +448,14 @@ class CheckoutActivity : AppCompatActivity() {
|
|||||||
// For Cart checkout
|
// For Cart checkout
|
||||||
fun startForCart(
|
fun startForCart(
|
||||||
context: Context,
|
context: Context,
|
||||||
cartItemIds: List<Int>
|
cartItemIds: List<Int>,
|
||||||
|
isWholesaleArray: BooleanArray? = null
|
||||||
) {
|
) {
|
||||||
val intent = Intent(context, CheckoutActivity::class.java).apply {
|
val intent = Intent(context, CheckoutActivity::class.java).apply {
|
||||||
putExtra(EXTRA_CART_ITEM_IDS, cartItemIds.toIntArray())
|
putExtra(EXTRA_CART_ITEM_IDS, cartItemIds.toIntArray())
|
||||||
|
if (isWholesaleArray != null) {
|
||||||
|
putExtra(EXTRA_CART_ITEM_WHOLESALE, isWholesaleArray)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
context.startActivity(intent)
|
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.OrderRequest
|
||||||
import com.alya.ecommerce_serang.data.api.dto.OrderRequestBuy
|
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.CartItemsItem
|
||||||
import com.alya.ecommerce_serang.data.api.response.customer.cart.DataItem
|
import com.alya.ecommerce_serang.data.api.response.customer.cart.DataItemCart
|
||||||
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.response.customer.profile.AddressesItem
|
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.OrderRepository
|
||||||
import com.alya.ecommerce_serang.data.repository.Result
|
import com.alya.ecommerce_serang.data.repository.Result
|
||||||
@ -24,12 +24,12 @@ class CheckoutViewModel(private val repository: OrderRepository) : ViewModel() {
|
|||||||
private val _addressDetails = MutableLiveData<AddressesItem?>()
|
private val _addressDetails = MutableLiveData<AddressesItem?>()
|
||||||
val addressDetails: LiveData<AddressesItem?> = _addressDetails
|
val addressDetails: LiveData<AddressesItem?> = _addressDetails
|
||||||
|
|
||||||
private val _availablePaymentMethods = MutableLiveData<List<PaymentInfoItem>>()
|
private val _availablePaymentMethods = MutableLiveData<List<PaymentItemDetail>>()
|
||||||
val availablePaymentMethods: LiveData<List<PaymentInfoItem>> = _availablePaymentMethods
|
val availablePaymentMethods: LiveData<List<PaymentItemDetail>> = _availablePaymentMethods
|
||||||
|
|
||||||
// Selected payment method
|
// Selected payment method
|
||||||
private val _selectedPayment = MutableLiveData<PaymentInfoItem?>()
|
private val _selectedPayment = MutableLiveData<PaymentItemDetail?>()
|
||||||
val selectedPayment: LiveData<PaymentInfoItem?> = _selectedPayment
|
val selectedPayment: LiveData<PaymentItemDetail?> = _selectedPayment
|
||||||
|
|
||||||
private val _isLoading = MutableLiveData<Boolean>()
|
private val _isLoading = MutableLiveData<Boolean>()
|
||||||
val isLoading: LiveData<Boolean> = _isLoading
|
val isLoading: LiveData<Boolean> = _isLoading
|
||||||
@ -40,6 +40,8 @@ class CheckoutViewModel(private val repository: OrderRepository) : ViewModel() {
|
|||||||
private val _orderCreated = MutableLiveData<Boolean>()
|
private val _orderCreated = MutableLiveData<Boolean>()
|
||||||
val orderCreated: LiveData<Boolean> = _orderCreated
|
val orderCreated: LiveData<Boolean> = _orderCreated
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Initialize "Buy Now" checkout
|
// Initialize "Buy Now" checkout
|
||||||
fun initializeBuyNow(
|
fun initializeBuyNow(
|
||||||
storeId: Int,
|
storeId: Int,
|
||||||
@ -48,7 +50,8 @@ class CheckoutViewModel(private val repository: OrderRepository) : ViewModel() {
|
|||||||
productName: String?,
|
productName: String?,
|
||||||
productImage: String?,
|
productImage: String?,
|
||||||
quantity: Int,
|
quantity: Int,
|
||||||
price: Double
|
price: Double,
|
||||||
|
isWholesale: Boolean
|
||||||
) {
|
) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
_isLoading.value = true
|
_isLoading.value = true
|
||||||
@ -64,7 +67,8 @@ class CheckoutViewModel(private val repository: OrderRepository) : ViewModel() {
|
|||||||
isNego = false, // Default value
|
isNego = false, // Default value
|
||||||
productId = productId,
|
productId = productId,
|
||||||
quantity = quantity,
|
quantity = quantity,
|
||||||
shipEtd = ""
|
shipEtd = "",
|
||||||
|
isReseller = isWholesale
|
||||||
)
|
)
|
||||||
|
|
||||||
// Create checkout data
|
// Create checkout data
|
||||||
@ -89,7 +93,7 @@ class CheckoutViewModel(private val repository: OrderRepository) : ViewModel() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Initialize checkout from cart
|
// Initialize checkout from cart
|
||||||
fun initializeFromCart(cartItemIds: List<Int>) {
|
fun initializeFromCart(cartItemIds: List<Int>, isWholesaleMap: Map<Int, Boolean> = emptyMap()) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
_isLoading.value = true
|
_isLoading.value = true
|
||||||
|
|
||||||
@ -100,7 +104,7 @@ class CheckoutViewModel(private val repository: OrderRepository) : ViewModel() {
|
|||||||
if (cartResult is Result.Success) {
|
if (cartResult is Result.Success) {
|
||||||
// Find matching cart items
|
// Find matching cart items
|
||||||
val matchingItems = mutableListOf<CartItemsItem>()
|
val matchingItems = mutableListOf<CartItemsItem>()
|
||||||
var storeData: DataItem? = null
|
var storeData: DataItemCart? = null
|
||||||
|
|
||||||
for (store in cartResult.data) {
|
for (store in cartResult.data) {
|
||||||
val storeItems = store.cartItems.filter { it.cartItemId in cartItemIds }
|
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) {
|
if (matchingItems.isNotEmpty() && storeData != null) {
|
||||||
// Create initial OrderRequest object
|
// Create initial OrderRequest object
|
||||||
val orderRequest = OrderRequest(
|
val orderRequest = OrderRequest(
|
||||||
addressId = 0, // Will be set when user selects address
|
addressId = 0,
|
||||||
paymentMethodId = 0, // Will be set when user selects payment
|
paymentMethodId = 0,
|
||||||
shipPrice = 0, // Will be set when user selects shipping
|
shipPrice = 0,
|
||||||
shipName = "",
|
shipName = "",
|
||||||
shipService = "",
|
shipService = "",
|
||||||
isNego = false,
|
isNego = false,
|
||||||
cartItemId = cartItemIds,
|
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
|
// Create checkout data
|
||||||
@ -131,8 +137,12 @@ class CheckoutViewModel(private val repository: OrderRepository) : ViewModel() {
|
|||||||
sellerName = storeData.storeName,
|
sellerName = storeData.storeName,
|
||||||
sellerId = storeData.storeId,
|
sellerId = storeData.storeId,
|
||||||
isBuyNow = false,
|
isBuyNow = false,
|
||||||
cartItems = matchingItems
|
cartItems = matchingItems,
|
||||||
|
cartItemWholesaleMap = isWholesaleMap // Store the wholesale map
|
||||||
)
|
)
|
||||||
|
|
||||||
|
calculateSubtotal()
|
||||||
|
calculateTotal()
|
||||||
} else {
|
} else {
|
||||||
_errorMessage.value = "No matching cart items found"
|
_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}"
|
_errorMessage.value = "Failed to fetch cart items: ${cartResult.exception.message}"
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Error initializing cart checkout", e)
|
|
||||||
_errorMessage.value = "Error: ${e.message}"
|
_errorMessage.value = "Error: ${e.message}"
|
||||||
} finally {
|
} finally {
|
||||||
_isLoading.value = false
|
_isLoading.value = false
|
||||||
@ -148,47 +157,51 @@ class CheckoutViewModel(private val repository: OrderRepository) : ViewModel() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getPaymentMethods(callback: (List<PaymentInfoItem>) -> Unit) {
|
fun getPaymentMethods() {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
try {
|
try {
|
||||||
val storeId = _checkoutData.value?.sellerId ?: return@launch
|
val storeId = _checkoutData.value?.sellerId ?: run {
|
||||||
|
Log.e(TAG, "StoreId is null - cannot fetch payment methods")
|
||||||
// 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 {
|
|
||||||
_availablePaymentMethods.value = emptyList()
|
_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) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Error fetching payment methods", e)
|
Log.e(TAG, "Exception in getPaymentMethods", e)
|
||||||
_availablePaymentMethods.value = emptyList()
|
_availablePaymentMethods.value = emptyList()
|
||||||
callback(emptyList())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Updated setPaymentMethod function
|
// Updated setPaymentMethod function
|
||||||
fun setPaymentMethod(paymentId: Int) {
|
fun setPaymentMethod(paymentId: Int) {
|
||||||
// We'll use the hardcoded ID (1) for now
|
|
||||||
val currentPaymentId = 1
|
|
||||||
|
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
try {
|
try {
|
||||||
// Get the available payment methods
|
// Get the available payment methods
|
||||||
@ -196,30 +209,34 @@ class CheckoutViewModel(private val repository: OrderRepository) : ViewModel() {
|
|||||||
|
|
||||||
if (paymentMethods.isNullOrEmpty()) {
|
if (paymentMethods.isNullOrEmpty()) {
|
||||||
// If no payment methods available, try to fetch them
|
// If no payment methods available, try to fetch them
|
||||||
getPaymentMethods { /* do nothing here */ }
|
getPaymentMethods()
|
||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use the first payment method (or specific one if you prefer)
|
val selectedPayment = paymentMethods.find { it.id == paymentId }
|
||||||
val selectedPayment = paymentMethods.first()
|
|
||||||
|
|
||||||
// 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
|
_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
|
val currentData = _checkoutData.value ?: return@launch
|
||||||
|
|
||||||
// Different handling for Buy Now vs Cart checkout
|
// Different handling for Buy Now vs Cart checkout
|
||||||
if (currentData.isBuyNow) {
|
if (currentData.isBuyNow) {
|
||||||
// For Buy Now checkout
|
// For Buy Now checkout
|
||||||
val buyRequest = currentData.orderRequest as OrderRequestBuy
|
val buyRequest = currentData.orderRequest as OrderRequestBuy
|
||||||
val updatedRequest = buyRequest.copy(paymentMethodId = currentPaymentId)
|
val updatedRequest = buyRequest.copy(paymentMethodId = paymentId)
|
||||||
_checkoutData.value = currentData.copy(orderRequest = updatedRequest)
|
_checkoutData.value = currentData.copy(orderRequest = updatedRequest)
|
||||||
} else {
|
} else {
|
||||||
// For Cart checkout
|
// For Cart checkout
|
||||||
val cartRequest = currentData.orderRequest as OrderRequest
|
val cartRequest = currentData.orderRequest as OrderRequest
|
||||||
val updatedRequest = cartRequest.copy(paymentMethodId = currentPaymentId)
|
val updatedRequest = cartRequest.copy(paymentMethodId = paymentId)
|
||||||
_checkoutData.value = currentData.copy(orderRequest = updatedRequest)
|
_checkoutData.value = currentData.copy(orderRequest = updatedRequest)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
@ -299,6 +316,39 @@ class CheckoutViewModel(private val repository: OrderRepository) : ViewModel() {
|
|||||||
try {
|
try {
|
||||||
val data = _checkoutData.value ?: throw Exception("No checkout data available")
|
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) {
|
val response = if (data.isBuyNow) {
|
||||||
// For Buy Now, use the dedicated endpoint
|
// For Buy Now, use the dedicated endpoint
|
||||||
val buyRequest = data.orderRequest as OrderRequestBuy
|
val buyRequest = data.orderRequest as OrderRequestBuy
|
||||||
@ -355,6 +405,8 @@ class CheckoutViewModel(private val repository: OrderRepository) : ViewModel() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val TAG = "CheckoutViewModel"
|
private const val TAG = "CheckoutViewModel"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,21 +1,23 @@
|
|||||||
package com.alya.ecommerce_serang.ui.order
|
package com.alya.ecommerce_serang.ui.order
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.alya.ecommerce_serang.R
|
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.alya.ecommerce_serang.databinding.ItemPaymentMethodBinding
|
||||||
import com.bumptech.glide.Glide
|
import com.bumptech.glide.Glide
|
||||||
import com.bumptech.glide.request.RequestOptions
|
import com.bumptech.glide.request.RequestOptions
|
||||||
|
|
||||||
class PaymentMethodAdapter(
|
class PaymentMethodAdapter(
|
||||||
private val paymentMethods: List<PaymentInfoItem>,
|
private val paymentMethods: List<PaymentItemDetail>,
|
||||||
private val onPaymentSelected: (PaymentInfoItem) -> Unit
|
private val onPaymentSelected: (PaymentItemDetail) -> Unit
|
||||||
) : RecyclerView.Adapter<PaymentMethodAdapter.PaymentMethodViewHolder>() {
|
) : RecyclerView.Adapter<PaymentMethodAdapter.PaymentMethodViewHolder>() {
|
||||||
|
|
||||||
// Selected payment name
|
// Track the selected payment by ID
|
||||||
private var selectedPaymentName: String? = null
|
private var selectedPaymentId: Int? = null
|
||||||
|
|
||||||
class PaymentMethodViewHolder(val binding: ItemPaymentMethodBinding) :
|
class PaymentMethodViewHolder(val binding: ItemPaymentMethodBinding) :
|
||||||
RecyclerView.ViewHolder(binding.root)
|
RecyclerView.ViewHolder(binding.root)
|
||||||
@ -36,18 +38,13 @@ class PaymentMethodAdapter(
|
|||||||
|
|
||||||
with(holder.binding) {
|
with(holder.binding) {
|
||||||
// Set payment method name
|
// Set payment method name
|
||||||
tvPaymentMethodName.text = payment.name
|
tvPaymentMethodName.text = payment.bankName
|
||||||
|
|
||||||
// // Set bank account number if available
|
// Set radio button state based on selected payment ID
|
||||||
// if (!payment.bankNum.isNullOrEmpty()) {
|
rbPaymentMethod.isChecked = payment.id == selectedPaymentId
|
||||||
// tvPaymentAccountNumber.visibility = View.VISIBLE
|
|
||||||
// tvPaymentAccountNumber.text = payment.bankNum
|
|
||||||
// } else {
|
|
||||||
// tvPaymentAccountNumber.visibility = View.GONE
|
|
||||||
// }
|
|
||||||
|
|
||||||
// Set radio button state based on selected payment name
|
// Debug log
|
||||||
rbPaymentMethod.isChecked = payment.name == selectedPaymentName
|
Log.d("PaymentAdapter", "Binding item ${payment.bankName}, checked=${rbPaymentMethod.isChecked}")
|
||||||
|
|
||||||
// Load payment icon if available
|
// Load payment icon if available
|
||||||
if (!payment.qrisImage.isNullOrEmpty()) {
|
if (!payment.qrisImage.isNullOrEmpty()) {
|
||||||
@ -55,31 +52,73 @@ class PaymentMethodAdapter(
|
|||||||
.load(payment.qrisImage)
|
.load(payment.qrisImage)
|
||||||
.apply(
|
.apply(
|
||||||
RequestOptions()
|
RequestOptions()
|
||||||
.placeholder(R.drawable.outline_store_24)
|
.placeholder(R.drawable.outline_store_24)
|
||||||
.error(R.drawable.outline_store_24))
|
.error(R.drawable.outline_store_24)
|
||||||
|
)
|
||||||
.into(ivPaymentMethod)
|
.into(ivPaymentMethod)
|
||||||
} else {
|
} else {
|
||||||
// Default icon for bank transfers
|
// Default icon for bank transfers
|
||||||
ivPaymentMethod.setImageResource(R.drawable.outline_store_24)
|
ivPaymentMethod.setImageResource(R.drawable.outline_store_24)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle click on the entire item
|
// IMPORTANT: We need to fix the click handling to prevent re-fetching
|
||||||
root.setOnClickListener {
|
val clickListener = View.OnClickListener {
|
||||||
onPaymentSelected(payment)
|
val previousSelectedId = selectedPaymentId
|
||||||
setSelectedPaymentName(payment.name)
|
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
|
// Apply the same click listener to both the root and the radio button
|
||||||
rbPaymentMethod.setOnClickListener {
|
root.setOnClickListener(clickListener)
|
||||||
onPaymentSelected(payment)
|
rbPaymentMethod.setOnClickListener(clickListener)
|
||||||
setSelectedPaymentName(payment.name)
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
// Set selected payment object
|
||||||
fun setSelectedPaymentName(paymentName: String) {
|
fun setSelectedPayment(payment: PaymentItemDetail) {
|
||||||
selectedPaymentName = paymentName
|
setSelectedPaymentId(payment.id)
|
||||||
notifyDataSetChanged() // Update all items to reflect selection change
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1,5 +1,6 @@
|
|||||||
package com.alya.ecommerce_serang.ui.order
|
package com.alya.ecommerce_serang.ui.order
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.ViewModel
|
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.api.response.customer.order.CourierCostsItem
|
||||||
import com.alya.ecommerce_serang.data.repository.OrderRepository
|
import com.alya.ecommerce_serang.data.repository.OrderRepository
|
||||||
import com.alya.ecommerce_serang.data.repository.Result
|
import com.alya.ecommerce_serang.data.repository.Result
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
class ShippingViewModel(
|
class ShippingViewModel(
|
||||||
@ -30,12 +32,71 @@ class ShippingViewModel(
|
|||||||
/**
|
/**
|
||||||
* Load shipping options based on address, product, and quantity
|
* 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) {
|
fun loadShippingOptions(addressId: Int, productId: Int, quantity: Int) {
|
||||||
// Reset previous state
|
|
||||||
_isLoading.value = true
|
_isLoading.value = true
|
||||||
_errorMessage.value = ""
|
_errorMessage.value = ""
|
||||||
|
|
||||||
// Prepare the request
|
|
||||||
val costProduct = CostProduct(
|
val costProduct = CostProduct(
|
||||||
productId = productId,
|
productId = productId,
|
||||||
quantity = quantity
|
quantity = quantity
|
||||||
@ -43,34 +104,47 @@ class ShippingViewModel(
|
|||||||
|
|
||||||
val request = CourierCostRequest(
|
val request = CourierCostRequest(
|
||||||
addressId = addressId,
|
addressId = addressId,
|
||||||
itemCost = listOf(costProduct) // Wrap in a list
|
itemCost = listOf(costProduct)
|
||||||
)
|
)
|
||||||
|
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
try {
|
var success = false
|
||||||
// Fetch courier costs
|
var attempt = 0
|
||||||
val result = repository.getCountCourierCost(request)
|
val maxAttempts = 3
|
||||||
|
|
||||||
when (result) {
|
while (!success && attempt < maxAttempts) {
|
||||||
is Result.Success -> {
|
attempt++
|
||||||
// Update shipping options directly with courier costs
|
|
||||||
_shippingOptions.value = result.data.courierCosts
|
try {
|
||||||
}
|
val result = repository.getCountCourierCost(request)
|
||||||
is Result.Error -> {
|
|
||||||
// Handle error case
|
when (result) {
|
||||||
_errorMessage.value = result.exception.message ?: "Unknown error occurred"
|
is Result.Success -> {
|
||||||
}
|
_shippingOptions.value = result.data.courierCosts
|
||||||
is Result.Loading -> {
|
success = true
|
||||||
// Typically handled by the loading state
|
}
|
||||||
|
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
|
isRequestingLocation = false
|
||||||
Toast.makeText(this, "Timeout lokasi, menggunakan lokasi default", Toast.LENGTH_SHORT).show()
|
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 getting last known location first
|
||||||
try {
|
try {
|
||||||
|
|||||||
@ -5,8 +5,12 @@ import androidx.lifecycle.LiveData
|
|||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
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.CompletedOrderRequest
|
||||||
import com.alya.ecommerce_serang.data.api.dto.OrdersItem
|
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.api.response.order.CompletedOrderResponse
|
||||||
import com.alya.ecommerce_serang.data.repository.OrderRepository
|
import com.alya.ecommerce_serang.data.repository.OrderRepository
|
||||||
import com.alya.ecommerce_serang.data.repository.Result
|
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>>()
|
private val _orderCompletionStatus = MutableLiveData<Result<CompletedOrderResponse>>()
|
||||||
val orderCompletionStatus: LiveData<Result<CompletedOrderResponse>> = _orderCompletionStatus
|
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>()
|
private val _isLoading = MutableLiveData<Boolean>()
|
||||||
val isLoading: LiveData<Boolean> = _isLoading
|
val isLoading: LiveData<Boolean> = _isLoading
|
||||||
|
|
||||||
@ -35,6 +51,9 @@ class HistoryViewModel(private val repository: OrderRepository) : ViewModel() {
|
|||||||
private val _isSuccess = MutableLiveData<Boolean>()
|
private val _isSuccess = MutableLiveData<Boolean>()
|
||||||
val isSuccess: LiveData<Boolean> = _isSuccess
|
val isSuccess: LiveData<Boolean> = _isSuccess
|
||||||
|
|
||||||
|
private val _error = MutableLiveData<String>()
|
||||||
|
val error: LiveData<String> get() = _error
|
||||||
|
|
||||||
fun getOrderList(status: String) {
|
fun getOrderList(status: String) {
|
||||||
_orders.value = ViewState.Loading
|
_orders.value = ViewState.Loading
|
||||||
viewModelScope.launch {
|
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.Activity
|
||||||
import android.app.Dialog
|
import android.app.Dialog
|
||||||
|
import android.content.ContextWrapper
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
@ -17,14 +18,20 @@ import android.widget.ImageView
|
|||||||
import android.widget.ProgressBar
|
import android.widget.ProgressBar
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
|
import androidx.fragment.app.FragmentActivity
|
||||||
import androidx.lifecycle.findViewTreeLifecycleOwner
|
import androidx.lifecycle.findViewTreeLifecycleOwner
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.alya.ecommerce_serang.R
|
import com.alya.ecommerce_serang.R
|
||||||
import com.alya.ecommerce_serang.data.api.dto.OrdersItem
|
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.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.button.MaterialButton
|
||||||
import com.google.android.material.textfield.TextInputLayout
|
import com.google.android.material.textfield.TextInputLayout
|
||||||
|
import com.google.gson.Gson
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.Calendar
|
import java.util.Calendar
|
||||||
@ -146,7 +153,8 @@ class OrderHistoryAdapter(
|
|||||||
visibility = View.VISIBLE
|
visibility = View.VISIBLE
|
||||||
text = itemView.context.getString(R.string.canceled_order_btn)
|
text = itemView.context.getString(R.string.canceled_order_btn)
|
||||||
setOnClickListener {
|
setOnClickListener {
|
||||||
showCancelOrderDialog(order.orderId.toString())
|
showCancelOrderBottomSheet(order.orderId)
|
||||||
|
viewModel.refreshOrders()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
deadlineDate.apply {
|
deadlineDate.apply {
|
||||||
@ -167,7 +175,8 @@ class OrderHistoryAdapter(
|
|||||||
visibility = View.VISIBLE
|
visibility = View.VISIBLE
|
||||||
text = itemView.context.getString(R.string.canceled_order_btn)
|
text = itemView.context.getString(R.string.canceled_order_btn)
|
||||||
setOnClickListener {
|
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)
|
text = itemView.context.getString(R.string.canceled_order_btn)
|
||||||
setOnClickListener {
|
setOnClickListener {
|
||||||
showCancelOrderDialog(order.orderId.toString())
|
showCancelOrderDialog(order.orderId.toString())
|
||||||
|
viewModel.refreshOrders()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -222,7 +232,7 @@ class OrderHistoryAdapter(
|
|||||||
text = itemView.context.getString(R.string.claim_complaint)
|
text = itemView.context.getString(R.string.claim_complaint)
|
||||||
setOnClickListener {
|
setOnClickListener {
|
||||||
showCancelOrderDialog(order.orderId.toString())
|
showCancelOrderDialog(order.orderId.toString())
|
||||||
// Handle click event
|
viewModel.refreshOrders()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
btnRight.apply {
|
btnRight.apply {
|
||||||
@ -231,22 +241,13 @@ class OrderHistoryAdapter(
|
|||||||
setOnClickListener {
|
setOnClickListener {
|
||||||
// Handle click event
|
// Handle click event
|
||||||
viewModel.confirmOrderCompleted(order.orderId, "completed")
|
viewModel.confirmOrderCompleted(order.orderId, "completed")
|
||||||
|
viewModel.refreshOrders()
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
deadlineDate.apply {
|
deadlineDate.apply {
|
||||||
visibility = View.VISIBLE
|
visibility = View.VISIBLE
|
||||||
text = formatShipmentDate(order.updatedAt, order.etd.toInt())
|
text = formatShipmentDate(order.updatedAt, order.etd ?: "0")
|
||||||
}
|
|
||||||
}
|
|
||||||
"delivered" -> {
|
|
||||||
// Untuk status delivered, tampilkan "Beri Ulasan"
|
|
||||||
btnRight.apply {
|
|
||||||
visibility = View.VISIBLE
|
|
||||||
text = itemView.context.getString(R.string.add_review)
|
|
||||||
setOnClickListener {
|
|
||||||
// Handle click event
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"completed" -> {
|
"completed" -> {
|
||||||
@ -262,6 +263,8 @@ class OrderHistoryAdapter(
|
|||||||
visibility = View.VISIBLE
|
visibility = View.VISIBLE
|
||||||
text = itemView.context.getString(R.string.add_review)
|
text = itemView.context.getString(R.string.add_review)
|
||||||
setOnClickListener {
|
setOnClickListener {
|
||||||
|
addReviewProduct(order)
|
||||||
|
viewModel.refreshOrders()
|
||||||
// Handle click event
|
// 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 {
|
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())
|
val inputFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.getDefault())
|
||||||
inputFormat.timeZone = TimeZone.getTimeZone("UTC")
|
inputFormat.timeZone = TimeZone.getTimeZone("UTC")
|
||||||
|
|
||||||
@ -339,7 +343,7 @@ class OrderHistoryAdapter(
|
|||||||
calendar.time = it
|
calendar.time = it
|
||||||
|
|
||||||
// Add estimated days
|
// Add estimated days
|
||||||
calendar.add(Calendar.DAY_OF_MONTH, estimate)
|
calendar.add(Calendar.DAY_OF_MONTH, estimateTD)
|
||||||
outputFormat.format(calendar.time)
|
outputFormat.format(calendar.time)
|
||||||
} ?: dateString
|
} ?: dateString
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
@ -485,10 +489,112 @@ class OrderHistoryAdapter(
|
|||||||
}
|
}
|
||||||
dialog.show()
|
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 {
|
companion object {
|
||||||
private const val REQUEST_IMAGE_PICK = 100
|
private const val REQUEST_IMAGE_PICK = 100
|
||||||
|
const val REQUEST_CODE_REVIEW = 101
|
||||||
private var imagePickCallback: ((Uri) -> Unit)? = null
|
private var imagePickCallback: ((Uri) -> Unit)? = null
|
||||||
|
|
||||||
// This method should be called from the activity's onActivityResult
|
// This method should be called from the activity's onActivityResult
|
||||||
|
|||||||
@ -47,11 +47,9 @@ class OrderHistoryFragment : Fragment() {
|
|||||||
1 -> getString(R.string.pending_orders)
|
1 -> getString(R.string.pending_orders)
|
||||||
2 -> getString(R.string.unpaid_orders)
|
2 -> getString(R.string.unpaid_orders)
|
||||||
3 -> getString(R.string.processed_orders)
|
3 -> getString(R.string.processed_orders)
|
||||||
4 -> getString(R.string.paid_orders)
|
4 -> getString(R.string.shipped_orders)
|
||||||
5 -> getString(R.string.shipped_orders)
|
5 -> getString(R.string.completed_orders)
|
||||||
6 -> getString(R.string.delivered_orders)
|
6 -> getString(R.string.canceled_orders)
|
||||||
7 -> getString(R.string.completed_orders)
|
|
||||||
8 -> getString(R.string.canceled_orders)
|
|
||||||
else -> "Tab $position"
|
else -> "Tab $position"
|
||||||
}
|
}
|
||||||
}.attach()
|
}.attach()
|
||||||
|
|||||||
@ -1,10 +1,13 @@
|
|||||||
package com.alya.ecommerce_serang.ui.order.history
|
package com.alya.ecommerce_serang.ui.order.history
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.viewModels
|
import androidx.fragment.app.viewModels
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
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.data.repository.Result
|
||||||
import com.alya.ecommerce_serang.databinding.FragmentOrderListBinding
|
import com.alya.ecommerce_serang.databinding.FragmentOrderListBinding
|
||||||
import com.alya.ecommerce_serang.ui.order.address.ViewState
|
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.BaseViewModelFactory
|
||||||
import com.alya.ecommerce_serang.utils.SessionManager
|
import com.alya.ecommerce_serang.utils.SessionManager
|
||||||
|
|
||||||
@ -121,10 +125,21 @@ class OrderListFragment : Fragment() {
|
|||||||
viewModel.getOrderList(status)
|
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) {
|
private fun navigateToOrderDetail(order: OrdersItem) {
|
||||||
// In a real app, you would navigate to order detail screen
|
val intent = Intent(requireContext(), DetailOrderStatusActivity::class.java).apply {
|
||||||
// For example: findNavController().navigate(OrderListFragmentDirections.actionToOrderDetail(order.orderId))
|
putExtra("ORDER_ID", order.orderId)
|
||||||
Toast.makeText(requireContext(), "Order ID: ${order.orderId}", Toast.LENGTH_SHORT).show()
|
putExtra("ORDER_STATUS", status) // Pass the current status
|
||||||
|
}
|
||||||
|
detailOrderLauncher.launch(intent)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroyView() {
|
override fun onDestroyView() {
|
||||||
@ -148,4 +163,5 @@ class OrderListFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -14,9 +14,7 @@ class OrderViewPagerAdapter(
|
|||||||
"pending", // Menunggu Tagihan
|
"pending", // Menunggu Tagihan
|
||||||
"unpaid", // Belum Dibayar
|
"unpaid", // Belum Dibayar
|
||||||
"processed", // Diproses
|
"processed", // Diproses
|
||||||
"paid", // Dibayar
|
|
||||||
"shipped", // Dikirim
|
"shipped", // Dikirim
|
||||||
"delivered", // Diterima
|
|
||||||
"completed", // Selesai
|
"completed", // Selesai
|
||||||
"canceled" // Dibatalkan
|
"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.enableEdgeToEdge
|
||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.appcompat.widget.SwitchCompat
|
||||||
import androidx.core.view.ViewCompat
|
import androidx.core.view.ViewCompat
|
||||||
import androidx.core.view.WindowCompat
|
import androidx.core.view.WindowCompat
|
||||||
import androidx.core.view.WindowInsetsCompat
|
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.ProductRepository
|
||||||
import com.alya.ecommerce_serang.data.repository.Result
|
import com.alya.ecommerce_serang.data.repository.Result
|
||||||
import com.alya.ecommerce_serang.databinding.ActivityDetailProductBinding
|
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.chat.ChatActivity
|
||||||
import com.alya.ecommerce_serang.ui.home.HorizontalProductAdapter
|
import com.alya.ecommerce_serang.ui.home.HorizontalProductAdapter
|
||||||
import com.alya.ecommerce_serang.ui.order.CheckoutActivity
|
import com.alya.ecommerce_serang.ui.order.CheckoutActivity
|
||||||
@ -45,6 +47,9 @@ class DetailProductActivity : AppCompatActivity() {
|
|||||||
private var productAdapter: HorizontalProductAdapter? = null
|
private var productAdapter: HorizontalProductAdapter? = null
|
||||||
private var reviewsAdapter: ReviewsAdapter? = null
|
private var reviewsAdapter: ReviewsAdapter? = null
|
||||||
private var currentQuantity = 1
|
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 {
|
private val viewModel: ProductUserViewModel by viewModels {
|
||||||
BaseViewModelFactory {
|
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 {
|
binding.btnBuyNow.setOnClickListener {
|
||||||
viewModel.productDetail.value?.productId?.let { id ->
|
viewModel.productDetail.value?.productId?.let { id ->
|
||||||
showBuyNowPopup(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) {
|
val fullImageUrl = when (val img = product.image) {
|
||||||
is String -> {
|
is String -> {
|
||||||
if (img.startsWith("/")) BASE_URL + img.substring(1) else img
|
if (img.startsWith("/")) BASE_URL + img.substring(1) else img
|
||||||
@ -296,6 +323,7 @@ class DetailProductActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
private fun showAddToCartPopup(productId: Int) {
|
private fun showAddToCartPopup(productId: Int) {
|
||||||
showQuantityDialog(productId, false)
|
showQuantityDialog(productId, false)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun showQuantityDialog(productId: Int, isBuyNow: Boolean) {
|
private fun showQuantityDialog(productId: Int, isBuyNow: Boolean) {
|
||||||
@ -303,26 +331,53 @@ class DetailProductActivity : AppCompatActivity() {
|
|||||||
val view = layoutInflater.inflate(R.layout.dialog_count_buy, null)
|
val view = layoutInflater.inflate(R.layout.dialog_count_buy, null)
|
||||||
bottomSheetDialog.setContentView(view)
|
bottomSheetDialog.setContentView(view)
|
||||||
|
|
||||||
val btnDecrease = view.findViewById<Button>(R.id.btnDecrease)
|
val btnDecrease = view.findViewById<ImageButton>(R.id.btnDecrease)
|
||||||
val btnIncrease = view.findViewById<Button>(R.id.btnIncrease)
|
val btnIncrease = view.findViewById<ImageButton>(R.id.btnIncrease)
|
||||||
|
|
||||||
val tvQuantity = view.findViewById<TextView>(R.id.tvQuantity)
|
val tvQuantity = view.findViewById<TextView>(R.id.tvQuantity)
|
||||||
val btnBuyNow = view.findViewById<Button>(R.id.btnBuyNow)
|
val btnBuyNow = view.findViewById<Button>(R.id.btnBuyNow)
|
||||||
val btnClose = view.findViewById<ImageButton>(R.id.btnCloseDialog)
|
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) {
|
if (!isBuyNow) {
|
||||||
btnBuyNow.setText(R.string.add_to_cart)
|
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()
|
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
|
val maxStock = viewModel.productDetail.value?.stock ?: 1
|
||||||
|
|
||||||
btnDecrease.setOnClickListener {
|
btnDecrease.setOnClickListener {
|
||||||
if (currentQuantity > 1) {
|
if (isWholesaleSelected) {
|
||||||
currentQuantity--
|
if (currentQuantity > minOrder) {
|
||||||
tvQuantity.text = currentQuantity.toString()
|
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++
|
currentQuantity++
|
||||||
tvQuantity.text = currentQuantity.toString()
|
tvQuantity.text = currentQuantity.toString()
|
||||||
} else {
|
} 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()
|
bottomSheetDialog.dismiss()
|
||||||
|
|
||||||
if (isBuyNow) {
|
if (isBuyNow) {
|
||||||
// If it's Buy Now, navigate directly to checkout without adding to cart
|
|
||||||
navigateToCheckout()
|
navigateToCheckout()
|
||||||
} else {
|
} else {
|
||||||
// If it's Add to Cart, add the item to the cart
|
|
||||||
val cartItem = CartItem(
|
val cartItem = CartItem(
|
||||||
productId = productId,
|
productId = productId,
|
||||||
quantity = currentQuantity
|
quantity = currentQuantity
|
||||||
@ -372,17 +425,34 @@ class DetailProductActivity : AppCompatActivity() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start checkout activity with buy now flow
|
if (isWholesaleSelected) {
|
||||||
CheckoutActivity.startForBuyNow(
|
// Start checkout activity with buy now flow
|
||||||
context = this,
|
//checkout klo grosiran
|
||||||
storeId = productDetail.storeId,
|
CheckoutActivity.startForBuyNow(
|
||||||
storeName = storeDetail.data.storeName,
|
context = this,
|
||||||
productId = productDetail.productId,
|
storeId = productDetail.storeId,
|
||||||
productName = productDetail.productName,
|
storeName = storeDetail.data.storeName,
|
||||||
productImage = productDetail.image,
|
productId = productDetail.productId,
|
||||||
quantity = currentQuantity,
|
productName = productDetail.productName,
|
||||||
price = productDetail.price.toDouble()
|
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(){
|
private fun navigateToChat(){
|
||||||
@ -402,7 +472,8 @@ class DetailProductActivity : AppCompatActivity() {
|
|||||||
productImage = productDetail.image,
|
productImage = productDetail.image,
|
||||||
productRating = productDetail.rating,
|
productRating = productDetail.rating,
|
||||||
storeName = storeDetail.data.storeName,
|
storeName = storeDetail.data.storeName,
|
||||||
chatRoomId = 0
|
chatRoomId = 0,
|
||||||
|
storeImage = storeDetail.data.storeImage
|
||||||
)
|
)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,21 +1,29 @@
|
|||||||
package com.alya.ecommerce_serang.ui.profile
|
package com.alya.ecommerce_serang.ui.profile
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.activity.enableEdgeToEdge
|
import androidx.activity.enableEdgeToEdge
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
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.R
|
||||||
import com.alya.ecommerce_serang.data.api.dto.UserProfile
|
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.ApiConfig
|
||||||
import com.alya.ecommerce_serang.data.api.retrofit.ApiService
|
import com.alya.ecommerce_serang.data.api.retrofit.ApiService
|
||||||
import com.alya.ecommerce_serang.data.repository.UserRepository
|
import com.alya.ecommerce_serang.data.repository.UserRepository
|
||||||
import com.alya.ecommerce_serang.databinding.ActivityDetailProfileBinding
|
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.BaseViewModelFactory
|
||||||
import com.alya.ecommerce_serang.utils.SessionManager
|
import com.alya.ecommerce_serang.utils.SessionManager
|
||||||
import com.alya.ecommerce_serang.utils.viewmodel.ProfileViewModel
|
import com.alya.ecommerce_serang.utils.viewmodel.ProfileViewModel
|
||||||
import com.bumptech.glide.Glide
|
import com.bumptech.glide.Glide
|
||||||
|
import com.google.gson.Gson
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
import java.util.TimeZone
|
import java.util.TimeZone
|
||||||
@ -24,6 +32,8 @@ class DetailProfileActivity : AppCompatActivity() {
|
|||||||
private lateinit var binding: ActivityDetailProfileBinding
|
private lateinit var binding: ActivityDetailProfileBinding
|
||||||
private lateinit var apiService: ApiService
|
private lateinit var apiService: ApiService
|
||||||
private lateinit var sessionManager: SessionManager
|
private lateinit var sessionManager: SessionManager
|
||||||
|
private var currentUserProfile: UserProfile? = null
|
||||||
|
|
||||||
|
|
||||||
private val viewModel: ProfileViewModel by viewModels {
|
private val viewModel: ProfileViewModel by viewModels {
|
||||||
BaseViewModelFactory {
|
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?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
binding = ActivityDetailProfileBinding.inflate(layoutInflater)
|
binding = ActivityDetailProfileBinding.inflate(layoutInflater)
|
||||||
@ -42,24 +61,61 @@ class DetailProfileActivity : AppCompatActivity() {
|
|||||||
apiService = ApiConfig.getApiService(sessionManager)
|
apiService = ApiConfig.getApiService(sessionManager)
|
||||||
|
|
||||||
enableEdgeToEdge()
|
enableEdgeToEdge()
|
||||||
// ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
|
|
||||||
// val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
|
setupClickListeners()
|
||||||
// v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
|
|
||||||
// insets
|
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.loadUserProfile()
|
||||||
|
|
||||||
viewModel.userProfile.observe(this){ user ->
|
viewModel.userProfile.observe(this) { user ->
|
||||||
user?.let { updateProfile(it) }
|
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 ->
|
viewModel.errorMessage.observe(this) { error ->
|
||||||
|
Log.e("DetailProfileActivity", "Error from ViewModel: $error")
|
||||||
Toast.makeText(this, error, Toast.LENGTH_SHORT).show()
|
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.tvNameUser.setText(user.name.toString())
|
||||||
binding.tvUsername.setText(user.username)
|
binding.tvUsername.setText(user.username)
|
||||||
@ -69,9 +125,16 @@ class DetailProfileActivity : AppCompatActivity() {
|
|||||||
Log.d("ProfileActivity", "Formatted Birth Date: ${formatDate(user.birthDate)}")
|
Log.d("ProfileActivity", "Formatted Birth Date: ${formatDate(user.birthDate)}")
|
||||||
binding.tvNumberPhoneUser.setText(user.phone)
|
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)
|
Glide.with(this)
|
||||||
.load(user.image)
|
.load(fullImageUrl)
|
||||||
.placeholder(R.drawable.baseline_account_circle_24)
|
.placeholder(R.drawable.baseline_account_circle_24)
|
||||||
.into(binding.profileImage)
|
.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
|
package com.alya.ecommerce_serang.ui.profile
|
||||||
|
|
||||||
|
import android.app.AlertDialog
|
||||||
|
import android.app.ProgressDialog
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
@ -9,18 +11,24 @@ import android.view.ViewGroup
|
|||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.viewModels
|
import androidx.fragment.app.viewModels
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.alya.ecommerce_serang.BuildConfig.BASE_URL
|
import com.alya.ecommerce_serang.BuildConfig.BASE_URL
|
||||||
import com.alya.ecommerce_serang.R
|
import com.alya.ecommerce_serang.R
|
||||||
import com.alya.ecommerce_serang.data.api.dto.UserProfile
|
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.ApiConfig
|
||||||
import com.alya.ecommerce_serang.data.repository.UserRepository
|
import com.alya.ecommerce_serang.data.repository.UserRepository
|
||||||
import com.alya.ecommerce_serang.databinding.FragmentProfileBinding
|
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.order.history.HistoryActivity
|
||||||
import com.alya.ecommerce_serang.ui.profile.mystore.MyStoreActivity
|
import com.alya.ecommerce_serang.ui.profile.mystore.MyStoreActivity
|
||||||
import com.alya.ecommerce_serang.utils.BaseViewModelFactory
|
import com.alya.ecommerce_serang.utils.BaseViewModelFactory
|
||||||
import com.alya.ecommerce_serang.utils.SessionManager
|
import com.alya.ecommerce_serang.utils.SessionManager
|
||||||
import com.alya.ecommerce_serang.utils.viewmodel.ProfileViewModel
|
import com.alya.ecommerce_serang.utils.viewmodel.ProfileViewModel
|
||||||
import com.bumptech.glide.Glide
|
import com.bumptech.glide.Glide
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
class ProfileFragment : Fragment() {
|
class ProfileFragment : Fragment() {
|
||||||
|
|
||||||
@ -53,10 +61,21 @@ class ProfileFragment : Fragment() {
|
|||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
observeUserProfile()
|
observeUserProfile()
|
||||||
viewModel.loadUserProfile()
|
viewModel.loadUserProfile()
|
||||||
|
viewModel.checkStoreUser()
|
||||||
|
|
||||||
binding.cardBukaToko.setOnClickListener{
|
binding.cardBukaToko.setOnClickListener{
|
||||||
val intentBuka = Intent(requireContext(), MyStoreActivity::class.java)
|
val hasStore = viewModel.checkStore.value
|
||||||
startActivity(intentBuka)
|
// 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{
|
binding.btnDetailProfile.setOnClickListener{
|
||||||
@ -73,6 +92,16 @@ class ProfileFragment : Fragment() {
|
|||||||
val intent = Intent(requireContext(), HistoryActivity::class.java)
|
val intent = Intent(requireContext(), HistoryActivity::class.java)
|
||||||
startActivity(intent)
|
startActivity(intent)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
binding.cardLogout.setOnClickListener({
|
||||||
|
logout()
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
binding.cardAddress.setOnClickListener({
|
||||||
|
val intent = Intent(requireContext(), AddressActivity::class.java)
|
||||||
|
startActivity(intent)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun observeUserProfile() {
|
private fun observeUserProfile() {
|
||||||
@ -103,4 +132,41 @@ class ProfileFragment : Fragment() {
|
|||||||
.placeholder(R.drawable.placeholder_image)
|
.placeholder(R.drawable.placeholder_image)
|
||||||
.into(profileImage)
|
.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.enableEdgeToEdge
|
||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import com.alya.ecommerce_serang.BuildConfig.BASE_URL
|
||||||
import com.alya.ecommerce_serang.R
|
import com.alya.ecommerce_serang.R
|
||||||
import com.alya.ecommerce_serang.data.api.dto.Store
|
import com.alya.ecommerce_serang.data.api.dto.Store
|
||||||
import com.alya.ecommerce_serang.data.api.retrofit.ApiConfig
|
import com.alya.ecommerce_serang.data.api.retrofit.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.profile.DetailStoreProfileActivity
|
||||||
import com.alya.ecommerce_serang.ui.profile.mystore.review.ReviewFragment
|
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.SellsActivity
|
||||||
import com.alya.ecommerce_serang.ui.profile.mystore.sells.SellsListFragment
|
|
||||||
import com.alya.ecommerce_serang.utils.BaseViewModelFactory
|
import com.alya.ecommerce_serang.utils.BaseViewModelFactory
|
||||||
import com.alya.ecommerce_serang.utils.SessionManager
|
import com.alya.ecommerce_serang.utils.SessionManager
|
||||||
import com.alya.ecommerce_serang.utils.viewmodel.MyStoreViewModel
|
import com.alya.ecommerce_serang.utils.viewmodel.MyStoreViewModel
|
||||||
import com.bumptech.glide.Glide
|
import com.bumptech.glide.Glide
|
||||||
import kotlin.getValue
|
|
||||||
|
|
||||||
class MyStoreActivity : AppCompatActivity() {
|
class MyStoreActivity : AppCompatActivity() {
|
||||||
private lateinit var binding: ActivityMyStoreBinding
|
private lateinit var binding: ActivityMyStoreBinding
|
||||||
@ -68,7 +67,7 @@ class MyStoreActivity : AppCompatActivity() {
|
|||||||
binding.tvStoreType.text = store.storeType
|
binding.tvStoreType.text = store.storeType
|
||||||
|
|
||||||
if (store.storeImage != null && store.storeImage.toString().isNotEmpty() && store.storeImage.toString() != "null") {
|
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")
|
Log.d("MyStoreActivity", "Loading store image from: $imageUrl")
|
||||||
|
|
||||||
Glide.with(this)
|
Glide.with(this)
|
||||||
|
|||||||
@ -20,6 +20,7 @@ object Constants {
|
|||||||
const val EXTRA_PRODUCT_PRICE = "product_price"
|
const val EXTRA_PRODUCT_PRICE = "product_price"
|
||||||
const val EXTRA_PRODUCT_IMAGE = "product_image"
|
const val EXTRA_PRODUCT_IMAGE = "product_image"
|
||||||
const val EXTRA_PRODUCT_RATING = "product_rating"
|
const val EXTRA_PRODUCT_RATING = "product_rating"
|
||||||
|
const val EXTRA_STORE_IMAGE = "store_image"
|
||||||
|
|
||||||
// Request codes
|
// Request codes
|
||||||
const val REQUEST_IMAGE_PICK = 1001
|
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
|
//clear data when log out
|
||||||
fun clearAll() {
|
fun clearAll() {
|
||||||
sharedPreferences.edit() {
|
sharedPreferences.edit() {
|
||||||
|
|||||||
@ -1,18 +1,38 @@
|
|||||||
package com.alya.ecommerce_serang.utils.viewmodel
|
package com.alya.ecommerce_serang.utils.viewmodel
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.util.Log
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
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.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.Result
|
||||||
import com.alya.ecommerce_serang.data.repository.UserRepository
|
import com.alya.ecommerce_serang.data.repository.UserRepository
|
||||||
|
import com.alya.ecommerce_serang.utils.SessionManager
|
||||||
import kotlinx.coroutines.launch
|
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>>()
|
private val _loginState = MutableLiveData<Result<LoginResponse>>()
|
||||||
val loginState: LiveData<Result<LoginResponse>> get() = _loginState
|
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) {
|
fun login(email: String, password: String) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
_loginState.value = Result.Loading
|
_loginState.value = Result.Loading
|
||||||
@ -20,4 +40,33 @@ class LoginViewModel(private val repository: UserRepository) : ViewModel() {
|
|||||||
_loginState.value = result
|
_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
|
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.LiveData
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.alya.ecommerce_serang.data.api.dto.UserProfile
|
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.Result
|
||||||
import com.alya.ecommerce_serang.data.repository.UserRepository
|
import com.alya.ecommerce_serang.data.repository.UserRepository
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@ -16,6 +21,15 @@ class ProfileViewModel(private val userRepository: UserRepository) : ViewModel()
|
|||||||
private val _errorMessage = MutableLiveData<String>()
|
private val _errorMessage = MutableLiveData<String>()
|
||||||
val errorMessage : LiveData<String> = _errorMessage
|
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(){
|
fun loadUserProfile(){
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
when (val result = userRepository.fetchUserProfile()){
|
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
|
package com.alya.ecommerce_serang.utils.viewmodel
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
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.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.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.Result
|
||||||
import com.alya.ecommerce_serang.data.repository.UserRepository
|
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
|
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)
|
// MutableLiveData for handling register state (Loading, Success, or Error)
|
||||||
private val _registerState = MutableLiveData<Result<String>>()
|
private val _registerState = MutableLiveData<Result<String>>()
|
||||||
val registerState: LiveData<Result<String>> = _registerState
|
val registerState: LiveData<Result<String>> = _registerState
|
||||||
@ -21,22 +52,61 @@ class RegisterViewModel(private val repository: UserRepository) : ViewModel() {
|
|||||||
private val _otpState = MutableLiveData<Result<Unit>>()
|
private val _otpState = MutableLiveData<Result<Unit>>()
|
||||||
val otpState: LiveData<Result<Unit>> = _otpState
|
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
|
// MutableLiveData to store messages from API responses
|
||||||
private val _message = MutableLiveData<String>()
|
private val _message = MutableLiveData<String>()
|
||||||
val message: LiveData<String> = _message
|
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.
|
* Function to request OTP by sending an email to the API.
|
||||||
* - It sets the OTP state to `Loading` before calling the repository.
|
* - It sets the OTP state to `Loading` before calling the repository.
|
||||||
* - If successful, it updates `_message` with the response message and signals success.
|
* - 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.
|
* - If an error occurs, it updates `_otpState` with `Result.Error` and logs the failure.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
fun requestOtp(email: String) {
|
fun requestOtp(email: String) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
_otpState.value = Result.Loading // Indicating API call in progress
|
_otpState.value = Result.Loading // Indicating API call in progress
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Call the repository function to request OTP
|
// Call the repository function to request OTP
|
||||||
|
// val authenticatedApiService = getAuthenticatedApiService()
|
||||||
|
// val authenticatedOrderRepo = UserRepository(authenticatedApiService)
|
||||||
val response: OtpResponse = repository.requestOtpRep(email)
|
val response: OtpResponse = repository.requestOtpRep(email)
|
||||||
|
|
||||||
// Log and store success message
|
// Log and store success message
|
||||||
@ -70,20 +140,179 @@ class RegisterViewModel(private val repository: UserRepository) : ViewModel() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// Call repository function to register the user
|
// Call repository function to register the user
|
||||||
val message = repository.registerUser(request)
|
val response: RegisterResponse = repository.registerUser(request)
|
||||||
|
|
||||||
// Store and display success message
|
Log.d(TAG, "Registration API call successful")
|
||||||
_message.value = message
|
Log.d(TAG, "Response message: ${response.message}")
|
||||||
_registerState.value = Result.Success(message) // Store success result
|
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) {
|
} 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
|
// Handle any errors and update state
|
||||||
_registerState.value = Result.Error(exception)
|
_registerState.value = Result.Error(exception)
|
||||||
|
|
||||||
_message.value = exception.localizedMessage ?: "Registration failed"
|
_message.value = exception.localizedMessage ?: "Registration failed"
|
||||||
|
Log.d(TAG, "Error message stored: ${exception.localizedMessage ?: "Registration failed"}")
|
||||||
|
|
||||||
|
|
||||||
// Log the error for debugging
|
// Log the error for debugging
|
||||||
Log.e("RegisterViewModel", "User registration failed", exception)
|
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"/>
|
<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:id="@+id/main"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="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>
|
</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"
|
android:contentDescription="@string/product_image"
|
||||||
tools:src="@drawable/placeholder_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 -->
|
<!-- Product Price and Name -->
|
||||||
<androidx.cardview.widget.CardView
|
<androidx.cardview.widget.CardView
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
@ -58,6 +101,50 @@
|
|||||||
android:textStyle="bold"
|
android:textStyle="bold"
|
||||||
tools:text="Rp65.000" />
|
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
|
<TextView
|
||||||
android:id="@+id/tvProductName"
|
android:id="@+id/tvProductName"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
@ -67,6 +154,7 @@
|
|||||||
android:textSize="16sp"
|
android:textSize="16sp"
|
||||||
tools:text="Keripik Ikan Tenggiri" />
|
tools:text="Keripik Ikan Tenggiri" />
|
||||||
|
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
@ -91,7 +179,7 @@
|
|||||||
android:layout_width="16dp"
|
android:layout_width="16dp"
|
||||||
android:layout_height="16dp"
|
android:layout_height="16dp"
|
||||||
android:contentDescription="@string/rating"
|
android:contentDescription="@string/rating"
|
||||||
android:src="@drawable/baseline_star_24"/>
|
android:src="@drawable/baseline_star_24" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/tvRating"
|
android:id="@+id/tvRating"
|
||||||
@ -103,10 +191,14 @@
|
|||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
</androidx.cardview.widget.CardView>
|
</androidx.cardview.widget.CardView>
|
||||||
|
|
||||||
<com.google.android.material.divider.MaterialDivider
|
<com.google.android.material.divider.MaterialDivider
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"/>
|
android:layout_height="wrap_content"/>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<!-- Buyer Reviews Section -->
|
<!-- Buyer Reviews Section -->
|
||||||
<androidx.cardview.widget.CardView
|
<androidx.cardview.widget.CardView
|
||||||
android:layout_width="match_parent"
|
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