Merge remote-tracking branch 'origin/master'

This commit is contained in:
Gracia
2025-04-12 16:50:38 +07:00
88 changed files with 4839 additions and 174 deletions

3
.idea/gradle.xml generated
View File

@ -6,13 +6,14 @@
<GradleProjectSettings>
<option name="testRunner" value="CHOOSE_PER_TEST" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="gradleJvm" value="openjdk-24" />
<option name="gradleJvm" value="jbr-21" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$" />
<option value="$PROJECT_DIR$/app" />
</set>
</option>
<option name="resolveExternalAnnotations" value="false" />
</GradleProjectSettings>
</option>
</component>

2
.idea/misc.xml generated
View File

@ -4,7 +4,7 @@
<option name="optimizeImportsOnTheFly" value="true" />
</component>
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="ProjectRootManager" version="2" languageLevel="JDK_X" default="true" project-jdk-name="24" project-jdk-type="JavaSDK">
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="jbr-21" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" />
</component>
<component name="ProjectType">

View File

@ -1,5 +1,4 @@
import org.gradle.api.tasks.compile.JavaCompile
import java.util.Properties
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.jetbrains.kotlin.android)
@ -9,9 +8,17 @@ plugins {
// id("com.google.dagger.hilt.android")
}
val localProperties = Properties().apply {
val localPropertiesFile = rootProject.file("local.properties")
if (localPropertiesFile.exists()) {
load(localPropertiesFile.inputStream())
}
}
android {
namespace = "com.alya.ecommerce_serang"
compileSdk = 35
compileSdk = 34
defaultConfig {
applicationId = "com.alya.ecommerce_serang"
@ -21,11 +28,19 @@ android {
versionName = "1.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
buildConfigField(
"String",
"BASE_URL",
"\"${localProperties["BASE_URL"] ?: "http://default-url.com/"}\""
)
}
buildTypes {
release {
buildConfigField("String", "BASE_URL", "\"http://192.168.1.15:3000/\"")
buildConfigField("String",
"BASE_URL",
"\"${localProperties["BASE_URL"] ?: "http://default-url.com/"}\"")
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
@ -33,7 +48,9 @@ android {
)
}
debug {
buildConfigField("String", "BASE_URL", "\"http://192.168.1.15:3000/\"")
buildConfigField("String",
"BASE_URL",
"\"${localProperties["BASE_URL"] ?: "http://default-url.com/"}\"")
}
}
compileOptions {
@ -86,4 +103,3 @@ dependencies {
// kapt("androidx.hilt:hilt-compiler:1.0.0")
}

View File

@ -4,6 +4,8 @@
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<application
android:allowBackup="true"
@ -17,6 +19,24 @@
android:theme="@style/Theme.Ecommerce_serang"
android:usesCleartextTraffic="true"
tools:targetApi="31">
<activity
android:name=".data.api.response.cart.CartActivity"
android:exported="false" />
<activity
android:name=".ui.order.address.EditAddressActivity"
android:exported="false" />
<activity
android:name=".ui.order.address.AddAddressActivity"
android:exported="false" />
<activity
android:name=".ui.order.address.AddressActivity"
android:exported="false" />
<activity
android:name=".ui.order.ShippingActivity"
android:exported="false" />
<activity
android:name=".ui.order.CheckoutActivity"
android:exported="false" />
<activity
android:name=".ui.profile.mystore.profile.payment_info.DetailPaymentInfoActivity"
android:exported="false" />

View File

@ -0,0 +1,11 @@
package com.alya.ecommerce_serang.data.api.dto
import com.google.gson.annotations.SerializedName
data class CartItem (
@SerializedName("product_id")
val productId: Int,
@SerializedName("quantity")
val quantity: Int
)

View File

@ -0,0 +1,16 @@
package com.alya.ecommerce_serang.data.api.dto
import com.alya.ecommerce_serang.data.api.response.cart.CartItemsItem
data class CheckoutData(
val orderRequest: Any, // Can be OrderRequest or OrderRequestBuy
val productName: String? = "",
val productImageUrl: String = "",
val productPrice: Double = 0.0,
val sellerName: String = "",
val sellerImageUrl: String? = null,
val sellerId: Int = 0,
val quantity: Int = 1,
val isBuyNow: Boolean = false,
val cartItems: List<CartItemsItem> = emptyList()
)

View File

@ -0,0 +1,19 @@
package com.alya.ecommerce_serang.data.api.dto
import com.google.gson.annotations.SerializedName
data class CourierCostRequest(
@SerializedName("address_id")
val addressId: Int,
@SerializedName("items")
val itemCost: CostProduct
)
data class CostProduct (
@SerializedName("product_id")
val productId: Int,
@SerializedName("quantity")
val quantity: Int
)

View File

@ -0,0 +1,41 @@
package com.alya.ecommerce_serang.data.api.dto
import com.google.gson.annotations.SerializedName
data class CreateAddressRequest (
@SerializedName("latitude")
val lat: Double,
@SerializedName("longitude")
val long: Double,
@SerializedName("street")
val street: String,
@SerializedName("subdistrict")
val subDistrict: String,
@SerializedName("city_id")
val cityId: Int,
@SerializedName("province_id")
val provId: Int,
@SerializedName("postal_code")
val postCode: String? = null,
@SerializedName("detail")
val detailAddress: String? = null,
@SerializedName("user_id")
val userId: Int,
@SerializedName("recipient")
val recipient: String,
@SerializedName("phone")
val phone: String,
@SerializedName("is_store_location")
val isStoreLocation: Boolean
)

View File

@ -0,0 +1,29 @@
package com.alya.ecommerce_serang.data.api.dto
import com.google.gson.annotations.SerializedName
data class OrderRequest (
@SerializedName("address_id")
val addressId : Int,
@SerializedName("payment_method_id")
val paymentMethodId : Int,
@SerializedName("ship_price")
val shipPrice : Int,
@SerializedName("ship_name")
val shipName : String,
@SerializedName("ship_service")
val shipService : String,
@SerializedName("is_negotiable")
val isNego: Boolean,
@SerializedName("cart_items_id")
val cartItemId: List<Int>,
@SerializedName("ship_etd")
val shipEtd: String
)

View File

@ -0,0 +1,33 @@
package com.alya.ecommerce_serang.data.api.dto
import com.google.gson.annotations.SerializedName
data class OrderRequestBuy (
@SerializedName("address_id")
val addressId : Int,
@SerializedName("payment_method_id")
val paymentMethodId : Int,
@SerializedName("ship_price")
val shipPrice : Int,
@SerializedName("ship_name")
val shipName : String,
@SerializedName("ship_service")
val shipService : String,
@SerializedName("is_negotiable")
val isNego: Boolean,
@SerializedName("product_id")
val productId: Int,
@SerializedName("quantity")
val quantity : Int,
@SerializedName("ship_etd")
val shipEtd: String
)

View File

@ -0,0 +1,11 @@
package com.alya.ecommerce_serang.data.api.dto
import com.google.gson.annotations.SerializedName
data class UpdateCart (
@SerializedName("cart_item_id")
val cartItemId: Int,
@SerializedName("quantity")
val quantity: Int
)

View File

@ -1,5 +1,6 @@
package com.alya.ecommerce_serang.data.api.response
import com.alya.ecommerce_serang.data.api.response.product.Product
import com.google.gson.annotations.SerializedName
data class CreateProductResponse(

View File

@ -1,33 +0,0 @@
package com.alya.ecommerce_serang.data.api.response
import com.google.gson.annotations.SerializedName
data class DetailStoreProductResponse(
@field:SerializedName("store")
val store: StoreProduct,
@field:SerializedName("message")
val message: String
)
data class StoreProduct(
@field:SerializedName("store_name")
val storeName: String,
@field:SerializedName("description")
val description: String,
@field:SerializedName("store_type")
val storeType: String,
@field:SerializedName("store_location")
val storeLocation: String,
@field:SerializedName("store_image")
val storeImage: Any,
@field:SerializedName("status")
val status: String
)

View File

@ -1,40 +0,0 @@
package com.alya.ecommerce_serang.data.api.response
import com.alya.ecommerce_serang.data.api.dto.Store
import com.google.gson.annotations.SerializedName
data class StoreResponse(
@field:SerializedName("shipping")
val shipping: List<ShippingItem>,
@field:SerializedName("payment")
val payment: List<PaymentItem>,
@field:SerializedName("store")
val store: Store,
@field:SerializedName("message")
val message: String
)
data class ShippingItem(
@field:SerializedName("courier")
val courier: String
)
data class PaymentItem(
@field:SerializedName("qris_image")
val qrisImage: String,
@field:SerializedName("bank_num")
val bankNum: String,
@field:SerializedName("bank_name")
val bankName: String,
@field:SerializedName("id")
val id: Int
)

View File

@ -1,4 +1,4 @@
package com.alya.ecommerce_serang.data.api.response
package com.alya.ecommerce_serang.data.api.response.auth
import com.google.gson.annotations.SerializedName

View File

@ -1,4 +1,4 @@
package com.alya.ecommerce_serang.data.api.response
package com.alya.ecommerce_serang.data.api.response.auth
import com.google.gson.annotations.SerializedName

View File

@ -1,4 +1,4 @@
package com.alya.ecommerce_serang.data.api.response
package com.alya.ecommerce_serang.data.api.response.auth
import com.google.gson.annotations.SerializedName

View File

@ -0,0 +1,30 @@
package com.alya.ecommerce_serang.data.api.response.cart
import com.google.gson.annotations.SerializedName
data class AddCartResponse(
@field:SerializedName("data")
val data: Data,
@field:SerializedName("message")
val message: String
)
data class Data(
@field:SerializedName("cart_id")
val cartId: Int,
@field:SerializedName("quantity")
val quantity: Int,
@field:SerializedName("product_id")
val productId: Int,
@field:SerializedName("created_at")
val createdAt: String,
@field:SerializedName("id")
val id: Int
)

View File

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

View File

@ -0,0 +1,48 @@
package com.alya.ecommerce_serang.data.api.response.cart
import com.google.gson.annotations.SerializedName
data class ListCartResponse(
@field:SerializedName("data")
val data: List<DataItem>,
@field:SerializedName("message")
val message: String
)
data class DataItem(
@field:SerializedName("store_id")
val storeId: Int,
@field:SerializedName("cart_items")
val cartItems: List<CartItemsItem>,
@field:SerializedName("store_name")
val storeName: String,
@field:SerializedName("most_recent_item")
val mostRecentItem: String
)
data class CartItemsItem(
@field:SerializedName("quantity")
val quantity: Int,
@field:SerializedName("price")
val price: Int,
@field:SerializedName("product_id")
val productId: Int,
@field:SerializedName("created_at")
val createdAt: String,
@field:SerializedName("cart_item_id")
val cartItemId: Int,
@field:SerializedName("product_name")
val productName: String
)

View File

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

View File

@ -0,0 +1,33 @@
package com.alya.ecommerce_serang.data.api.response.order
import com.google.gson.annotations.SerializedName
data class CourierCostResponse(
@field:SerializedName("courierCosts")
val courierCosts: List<CourierCostsItem>
)
data class CourierCostsItem(
@field:SerializedName("courier")
val courier: String,
@field:SerializedName("services")
val services: List<ServicesItem>
)
data class ServicesItem(
@field:SerializedName("cost")
val cost: Int,
@field:SerializedName("etd")
val etd: String,
@field:SerializedName("service")
val service: String,
@field:SerializedName("description")
val description: String
)

View File

@ -0,0 +1,105 @@
package com.alya.ecommerce_serang.data.api.response.order
import com.google.gson.annotations.SerializedName
data class CreateOrderResponse(
@field:SerializedName("shipping")
val shipping: Shipping,
@field:SerializedName("order_item")
val orderItem: List<OrderItemItem>,
@field:SerializedName("message")
val message: String,
@field:SerializedName("order")
val order: Order
)
data class Shipping(
@field:SerializedName("receipt_num")
val receiptNum: Int? = null,
@field:SerializedName("etd")
val etd: String,
@field:SerializedName("price")
val price: String,
@field:SerializedName("service")
val service: String,
@field:SerializedName("name")
val name: String,
@field:SerializedName("created_at")
val createdAt: String,
@field:SerializedName("id")
val id: Int,
@field:SerializedName("order_id")
val orderId: Int,
@field:SerializedName("status")
val status: String
)
data class OrderItemItem(
@field:SerializedName("quantity")
val quantity: Int,
@field:SerializedName("price")
val price: String,
@field:SerializedName("subtotal")
val subtotal: String,
@field:SerializedName("product_id")
val productId: Int,
@field:SerializedName("id")
val id: Int,
@field:SerializedName("order_id")
val orderId: Int
)
data class Order(
@field:SerializedName("payment_method_id")
val paymentMethodId: Int,
@field:SerializedName("auto_completed_at")
val autoCompletedAt: String? = null,
@field:SerializedName("updated_at")
val updatedAt: String,
@field:SerializedName("total_amount")
val totalAmount: String,
@field:SerializedName("user_id")
val userId: Int,
@field:SerializedName("address_id")
val addressId: Int,
@field:SerializedName("is_negotiable")
val isNegotiable: Boolean,
@field:SerializedName("created_at")
val createdAt: String,
@field:SerializedName("voucher_id")
val voucherId: String? = null,
@field:SerializedName("id")
val id: Int,
@field:SerializedName("status")
val status: String
)

View File

@ -0,0 +1,21 @@
package com.alya.ecommerce_serang.data.api.response.order
import com.google.gson.annotations.SerializedName
data class ListCityResponse(
@field:SerializedName("cities")
val cities: List<CitiesItem>,
@field:SerializedName("message")
val message: String
)
data class CitiesItem(
@field:SerializedName("city_name")
val cityName: String,
@field:SerializedName("city_id")
val cityId: String
)

View File

@ -0,0 +1,21 @@
package com.alya.ecommerce_serang.data.api.response.order
import com.google.gson.annotations.SerializedName
data class ListProvinceResponse(
@field:SerializedName("provinces")
val provinces: List<ProvincesItem>,
@field:SerializedName("message")
val message: String
)
data class ProvincesItem(
@field:SerializedName("province")
val province: String,
@field:SerializedName("province_id")
val provinceId: String
)

View File

@ -0,0 +1,129 @@
package com.alya.ecommerce_serang.data.api.response.order
import com.google.gson.annotations.SerializedName
data class OrderDetailResponse(
@field:SerializedName("orders")
val orders: Orders,
@field:SerializedName("message")
val message: String
)
data class OrderItemsItem(
@field:SerializedName("review_id")
val reviewId: Int? = null,
@field:SerializedName("quantity")
val quantity: Int,
@field:SerializedName("price")
val price: Int,
@field:SerializedName("subtotal")
val subtotal: Int,
@field:SerializedName("product_image")
val productImage: String? = null,
@field:SerializedName("store_name")
val storeName: String,
@field:SerializedName("product_price")
val productPrice: Int,
@field:SerializedName("product_name")
val productName: String
)
data class Orders(
@field:SerializedName("receipt_num")
val receiptNum: String,
@field:SerializedName("latitude")
val latitude: String,
@field:SerializedName("created_at")
val createdAt: String,
@field:SerializedName("voucher_code")
val voucherCode: String? = null,
@field:SerializedName("updated_at")
val updatedAt: String,
@field:SerializedName("etd")
val etd: String,
@field:SerializedName("street")
val street: String,
@field:SerializedName("cancel_date")
val cancelDate: String,
@field:SerializedName("longitude")
val longitude: String,
@field:SerializedName("shipment_status")
val shipmentStatus: String,
@field:SerializedName("order_items")
val orderItems: List<OrderItemsItem>,
@field:SerializedName("auto_completed_at")
val autoCompletedAt: String,
@field:SerializedName("is_store_location")
val isStoreLocation: Boolean,
@field:SerializedName("voucher_name")
val voucherName: String? = null,
@field:SerializedName("address_id")
val addressId: Int,
@field:SerializedName("payment_method_id")
val paymentMethodId: Int,
@field:SerializedName("cancel_reason")
val cancelReason: String,
@field:SerializedName("total_amount")
val totalAmount: String,
@field:SerializedName("user_id")
val userId: Int,
@field:SerializedName("province_id")
val provinceId: Int,
@field:SerializedName("courier")
val courier: String,
@field:SerializedName("subdistrict")
val subdistrict: String,
@field:SerializedName("service")
val service: String,
@field:SerializedName("shipment_price")
val shipmentPrice: String,
@field:SerializedName("voucher_id")
val voucherId: Int? = null,
@field:SerializedName("detail")
val detail: String,
@field:SerializedName("postal_code")
val postalCode: String,
@field:SerializedName("order_id")
val orderId: Int,
@field:SerializedName("city_id")
val cityId: Int
)

View File

@ -0,0 +1,91 @@
package com.alya.ecommerce_serang.data.api.response.order
import com.google.gson.annotations.SerializedName
data class OrderListResponse(
@field:SerializedName("orders")
val orders: List<OrdersItem>,
@field:SerializedName("message")
val message: String
)
data class OrdersItem(
@field:SerializedName("receipt_num")
val receiptNum: String,
@field:SerializedName("latitude")
val latitude: String,
@field:SerializedName("created_at")
val createdAt: String,
@field:SerializedName("voucher_code")
val voucherCode: String? = null,
@field:SerializedName("updated_at")
val updatedAt: String,
@field:SerializedName("street")
val street: String,
@field:SerializedName("longitude")
val longitude: String,
@field:SerializedName("shipment_status")
val shipmentStatus: String,
@field:SerializedName("order_items")
val orderItems: List<OrderItemsItem>,
@field:SerializedName("is_store_location")
val isStoreLocation: Boolean,
@field:SerializedName("voucher_name")
val voucherName: String? = null,
@field:SerializedName("address_id")
val addressId: Int,
@field:SerializedName("payment_method_id")
val paymentMethodId: Int,
@field:SerializedName("total_amount")
val totalAmount: String,
@field:SerializedName("user_id")
val userId: Int,
@field:SerializedName("province_id")
val provinceId: Int,
@field:SerializedName("courier")
val courier: String,
@field:SerializedName("subdistrict")
val subdistrict: String,
@field:SerializedName("service")
val service: String,
@field:SerializedName("shipment_price")
val shipmentPrice: String,
@field:SerializedName("voucher_id")
val voucherId: Int? = null,
@field:SerializedName("detail")
val detail: String,
@field:SerializedName("postal_code")
val postalCode: String,
@field:SerializedName("order_id")
val orderId: Int,
@field:SerializedName("city_id")
val cityId: Int
)

View File

@ -1,4 +1,4 @@
package com.alya.ecommerce_serang.data.api.response
package com.alya.ecommerce_serang.data.api.response.product
import com.alya.ecommerce_serang.data.api.dto.ProductsItem
import com.google.gson.annotations.SerializedName

View File

@ -1,13 +1,13 @@
package com.alya.ecommerce_serang.data.api.response
package com.alya.ecommerce_serang.data.api.response.product
import com.google.gson.annotations.SerializedName
data class AllStoreResponse(
@field:SerializedName("store")
@field:SerializedName("store")
val store: AllStore,
@field:SerializedName("message")
@field:SerializedName("message")
val message: String
)

View File

@ -1,4 +1,4 @@
package com.alya.ecommerce_serang.data.api.response
package com.alya.ecommerce_serang.data.api.response.product
import com.alya.ecommerce_serang.data.api.dto.CategoryItem
import com.google.gson.annotations.SerializedName

View File

@ -0,0 +1,63 @@
package com.alya.ecommerce_serang.data.api.response.product
import com.google.gson.annotations.SerializedName
data class DetailStoreProductResponse(
@field:SerializedName("store")
val store: StoreProduct,
@field:SerializedName("message")
val message: String
)
data class PaymentInfoItem(
@field:SerializedName("qris_image")
val qrisImage: String,
@field:SerializedName("bank_num")
val bankNum: String,
@field:SerializedName("name")
val name: String
)
data class StoreProduct(
@field:SerializedName("store_id")
val storeId: Int,
@field:SerializedName("shipping_service")
val shippingService: List<ShippingServiceItem>,
@field:SerializedName("store_rating")
val storeRating: String,
@field:SerializedName("store_name")
val storeName: String,
@field:SerializedName("description")
val description: String,
@field:SerializedName("store_type")
val storeType: String,
@field:SerializedName("payment_info")
val paymentInfo: List<PaymentInfoItem>,
@field:SerializedName("store_location")
val storeLocation: String,
@field:SerializedName("store_image")
val storeImage: String,
@field:SerializedName("status")
val status: String
)
data class ShippingServiceItem(
@field:SerializedName("courier")
val courier: String
)

View File

@ -1,13 +1,13 @@
package com.alya.ecommerce_serang.data.api.response
package com.alya.ecommerce_serang.data.api.response.product
import com.google.gson.annotations.SerializedName
data class ProductResponse(
@field:SerializedName("product")
@field:SerializedName("product")
val product: Product,
@field:SerializedName("message")
@field:SerializedName("message")
val message: String
)

View File

@ -1,13 +1,13 @@
package com.alya.ecommerce_serang.data.api.response
package com.alya.ecommerce_serang.data.api.response.product
import com.google.gson.annotations.SerializedName
data class ReviewProductResponse(
@field:SerializedName("reviews")
@field:SerializedName("reviews")
val reviews: List<ReviewsItem>,
@field:SerializedName("message")
@field:SerializedName("message")
val message: String
)

View File

@ -0,0 +1,139 @@
package com.alya.ecommerce_serang.data.api.response.product
import com.alya.ecommerce_serang.data.api.dto.Store
import com.google.gson.annotations.SerializedName
data class StoreResponse(
@field:SerializedName("shipping")
val shipping: List<ShippingItem>,
@field:SerializedName("payment")
val payment: List<PaymentItem>,
@field:SerializedName("store")
val store: Store,
@field:SerializedName("message")
val message: String
)
data class Store(
@field:SerializedName("approval_reason")
val approvalReason: String,
@field:SerializedName("store_status")
val storeStatus: String,
@field:SerializedName("sppirt")
val sppirt: String,
@field:SerializedName("user_name")
val userName: String,
@field:SerializedName("nib")
val nib: String,
@field:SerializedName("latitude")
val latitude: String,
@field:SerializedName("store_type_id")
val storeTypeId: Int,
@field:SerializedName("balance")
val balance: String,
@field:SerializedName("street")
val street: String,
@field:SerializedName("store_name")
val storeName: String,
@field:SerializedName("user_phone")
val userPhone: String,
@field:SerializedName("halal")
val halal: String,
@field:SerializedName("id")
val id: Int,
@field:SerializedName("email")
val email: String,
@field:SerializedName("store_image")
val storeImage: String? = null,
@field:SerializedName("longitude")
val longitude: String,
@field:SerializedName("store_id")
val storeId: Int,
@field:SerializedName("is_store_location")
val isStoreLocation: Boolean,
@field:SerializedName("ktp")
val ktp: String,
@field:SerializedName("approval_status")
val approvalStatus: String,
@field:SerializedName("npwp")
val npwp: String,
@field:SerializedName("store_type")
val storeType: String,
@field:SerializedName("is_on_leave")
val isOnLeave: Boolean,
@field:SerializedName("user_id")
val userId: Int,
@field:SerializedName("province_id")
val provinceId: Int,
@field:SerializedName("phone")
val phone: String,
@field:SerializedName("subdistrict")
val subdistrict: String,
@field:SerializedName("recipient")
val recipient: String,
@field:SerializedName("detail")
val detail: String,
@field:SerializedName("postal_code")
val postalCode: String,
@field:SerializedName("store_description")
val storeDescription: String,
@field:SerializedName("city_id")
val cityId: Int
)
data class ShippingItem(
@field:SerializedName("courier")
val courier: String
)
data class PaymentItem(
@field:SerializedName("qris_image")
val qrisImage: String,
@field:SerializedName("bank_num")
val bankNum: String,
@field:SerializedName("bank_name")
val bankName: String,
@field:SerializedName("id")
val id: Int
)

View File

@ -0,0 +1,54 @@
package com.alya.ecommerce_serang.data.api.response.profile
import com.google.gson.annotations.SerializedName
data class AddressResponse(
@field:SerializedName("addresses")
val addresses: List<AddressesItem>,
@field:SerializedName("message")
val message: String
)
data class AddressesItem(
@field:SerializedName("is_store_location")
val isStoreLocation: Boolean,
@field:SerializedName("latitude")
val latitude: String,
@field:SerializedName("user_id")
val userId: Int,
@field:SerializedName("province_id")
val provinceId: Int,
@field:SerializedName("phone")
val phone: String,
@field:SerializedName("street")
val street: String,
@field:SerializedName("subdistrict")
val subdistrict: String,
@field:SerializedName("recipient")
val recipient: String,
@field:SerializedName("id")
val id: Int,
@field:SerializedName("detail")
val detail: String,
@field:SerializedName("postal_code")
val postalCode: String,
@field:SerializedName("longitude")
val longitude: String,
@field:SerializedName("city_id")
val cityId: Int
)

View File

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

View File

@ -1,4 +1,4 @@
package com.alya.ecommerce_serang.data.api.response
package com.alya.ecommerce_serang.data.api.response.profile
import com.alya.ecommerce_serang.data.api.dto.UserProfile
import com.google.gson.annotations.SerializedName

View File

@ -1,19 +1,34 @@
package com.alya.ecommerce_serang.data.api.retrofit
import com.alya.ecommerce_serang.data.api.dto.CartItem
import com.alya.ecommerce_serang.data.api.dto.CourierCostRequest
import com.alya.ecommerce_serang.data.api.dto.CreateAddressRequest
import com.alya.ecommerce_serang.data.api.dto.LoginRequest
import com.alya.ecommerce_serang.data.api.dto.OrderRequest
import com.alya.ecommerce_serang.data.api.dto.OrderRequestBuy
import com.alya.ecommerce_serang.data.api.dto.OtpRequest
import com.alya.ecommerce_serang.data.api.dto.RegisterRequest
import com.alya.ecommerce_serang.data.api.response.AllProductResponse
import com.alya.ecommerce_serang.data.api.response.CategoryResponse
import com.alya.ecommerce_serang.data.api.response.DetailStoreProductResponse
import com.alya.ecommerce_serang.data.api.response.LoginResponse
import com.alya.ecommerce_serang.data.api.response.OtpResponse
import com.alya.ecommerce_serang.data.api.response.ProductResponse
import com.alya.ecommerce_serang.data.api.response.ProfileResponse
import com.alya.ecommerce_serang.data.api.response.RegisterResponse
import com.alya.ecommerce_serang.data.api.response.ReviewProductResponse
import com.alya.ecommerce_serang.data.api.response.StoreResponse
import com.alya.ecommerce_serang.data.api.dto.UpdateCart
import com.alya.ecommerce_serang.data.api.response.ViewStoreProductsResponse
import com.alya.ecommerce_serang.data.api.response.auth.LoginResponse
import com.alya.ecommerce_serang.data.api.response.auth.OtpResponse
import com.alya.ecommerce_serang.data.api.response.auth.RegisterResponse
import com.alya.ecommerce_serang.data.api.response.cart.AddCartResponse
import com.alya.ecommerce_serang.data.api.response.cart.ListCartResponse
import com.alya.ecommerce_serang.data.api.response.cart.UpdateCartResponse
import com.alya.ecommerce_serang.data.api.response.order.CourierCostResponse
import com.alya.ecommerce_serang.data.api.response.order.CreateOrderResponse
import com.alya.ecommerce_serang.data.api.response.order.ListCityResponse
import com.alya.ecommerce_serang.data.api.response.order.ListProvinceResponse
import com.alya.ecommerce_serang.data.api.response.product.AllProductResponse
import com.alya.ecommerce_serang.data.api.response.product.CategoryResponse
import com.alya.ecommerce_serang.data.api.response.product.DetailStoreProductResponse
import com.alya.ecommerce_serang.data.api.response.product.ProductResponse
import com.alya.ecommerce_serang.data.api.response.product.ReviewProductResponse
import com.alya.ecommerce_serang.data.api.response.product.StoreResponse
import com.alya.ecommerce_serang.data.api.response.profile.AddressResponse
import com.alya.ecommerce_serang.data.api.response.profile.CreateAddressResponse
import com.alya.ecommerce_serang.data.api.response.profile.ProfileResponse
import retrofit2.Call
import retrofit2.Response
import retrofit2.http.Body
@ -21,6 +36,7 @@ import retrofit2.http.Field
import retrofit2.http.FormUrlEncoded
import retrofit2.http.GET
import retrofit2.http.POST
import retrofit2.http.PUT
import retrofit2.http.Path
interface ApiService {
@ -64,6 +80,24 @@ interface ApiService {
@Path("id") storeId: Int
): Response<DetailStoreProductResponse>
@POST("order")
suspend fun postOrder(
@Body request: OrderRequest
): Response<CreateOrderResponse>
@POST("order")
suspend fun postOrderBuyNow(
@Body request: OrderRequestBuy
): Response<CreateOrderResponse>
@GET("profile/address")
suspend fun getAddress(
): Response<AddressResponse>
@POST("profile/addaddress")
suspend fun createAddress(
@Body createAddressRequest: CreateAddressRequest
): Response<CreateAddressResponse>
@GET("mystore")
suspend fun getStore (): Response<StoreResponse>
@ -89,4 +123,30 @@ interface ApiService {
@Field("is_active") isActive: String
): Response<Unit>
@GET("cart_item")
suspend fun getCart (): Response<ListCartResponse>
@POST("cart/add")
suspend fun addCart(
@Body cartRequest: CartItem
): Response<AddCartResponse>
@PUT("cart/update")
suspend fun updateCart(
@Body updateCart: UpdateCart
): Response<UpdateCartResponse>
@POST("couriercost")
suspend fun countCourierCost(
@Body courierCost : CourierCostRequest
): Response<CourierCostResponse>
@GET("cities/{id}")
suspend fun getCityProvId(
@Path("id") provId : Int
): Response<ListCityResponse>
@GET("provinces")
suspend fun getListProv(
): Response<ListProvinceResponse>
}

View File

@ -2,7 +2,7 @@ package com.alya.ecommerce_serang.data.repository
import android.util.Log
import com.alya.ecommerce_serang.data.api.dto.Store
import com.alya.ecommerce_serang.data.api.response.StoreResponse
import com.alya.ecommerce_serang.data.api.response.product.StoreResponse
import com.alya.ecommerce_serang.data.api.retrofit.ApiService
import retrofit2.HttpException
import java.io.IOException

View File

@ -0,0 +1,210 @@
package com.alya.ecommerce_serang.data.repository
import android.util.Log
import com.alya.ecommerce_serang.data.api.dto.CourierCostRequest
import com.alya.ecommerce_serang.data.api.dto.CreateAddressRequest
import com.alya.ecommerce_serang.data.api.dto.OrderRequest
import com.alya.ecommerce_serang.data.api.dto.OrderRequestBuy
import com.alya.ecommerce_serang.data.api.response.cart.DataItem
import com.alya.ecommerce_serang.data.api.response.order.CourierCostResponse
import com.alya.ecommerce_serang.data.api.response.order.CreateOrderResponse
import com.alya.ecommerce_serang.data.api.response.order.ListCityResponse
import com.alya.ecommerce_serang.data.api.response.order.ListProvinceResponse
import com.alya.ecommerce_serang.data.api.response.product.ProductResponse
import com.alya.ecommerce_serang.data.api.response.product.StoreProduct
import com.alya.ecommerce_serang.data.api.response.product.StoreResponse
import com.alya.ecommerce_serang.data.api.response.profile.AddressResponse
import com.alya.ecommerce_serang.data.api.response.profile.CreateAddressResponse
import com.alya.ecommerce_serang.data.api.retrofit.ApiService
import retrofit2.Response
class OrderRepository(private val apiService: ApiService) {
suspend fun fetchProductDetail(productId: Int): ProductResponse? {
return try {
val response = apiService.getDetailProduct(productId)
if (response.isSuccessful) {
val productResponse = response.body()
Log.d("Order Repository", "Product detail fetched successfully: ${productResponse?.product?.productName}")
productResponse
} else {
val errorBody = response.errorBody()?.string() ?: "Unknown error"
Log.e("Order Repository", "Error fetching product detail. Code: ${response.code()}, Error: $errorBody")
null
}
} catch (e: Exception) {
Log.e("Order Repository", "Exception fetching product", e)
null
}
}
suspend fun createOrder(orderRequest: OrderRequest): Response<CreateOrderResponse> {
return try {
Log.d("Order Repository", "Creating order. Request details: $orderRequest")
val response = apiService.postOrder(orderRequest)
if (response.isSuccessful) {
Log.d("Order Repository", "Order created successfully. Response: ${response.body()}")
} else {
val errorBody = response.errorBody()?.string() ?: "Unknown error"
Log.e("Order Repository", "Order creation failed. Code: ${response.code()}, Error: $errorBody")
}
response
} catch (e: Exception) {
Log.e("Order Repository", "Exception creating order", e)
throw e
}
}
suspend fun createOrderBuyNow(orderRequestBuy: OrderRequestBuy): Response<CreateOrderResponse> {
return try {
Log.d("Order Repository", "Creating buy now order. Request details: $orderRequestBuy")
val response = apiService.postOrderBuyNow(orderRequestBuy)
if (response.isSuccessful) {
Log.d("Order Repository", "Buy now order created successfully. Response: ${response.body()}")
} else {
val errorBody = response.errorBody()?.string() ?: "Unknown error"
Log.e("Order Repository", "Buy now order creation failed. Code: ${response.code()}, Error: $errorBody")
}
response
} catch (e: Exception) {
Log.e("Order Repository", "Exception creating buy now order", e)
throw e
}
}
suspend fun getStore(): StoreResponse? {
return try {
val response = apiService.getStore()
if (response.isSuccessful) {
val storeResponse = response.body()
Log.d("Order Repository", "Store information fetched successfully. Store count: ${storeResponse?.store?.storeName}")
storeResponse
} else {
val errorBody = response.errorBody()?.string() ?: "Unknown error"
Log.e("Order Repository", "Error fetching store. Code: ${response.code()}, Error: $errorBody")
null
}
} catch (e: Exception) {
Log.e("Order Repository", "Exception getting store", e)
null
}
}
suspend fun getAddress(): AddressResponse? {
return try {
val response = apiService.getAddress()
if (response.isSuccessful) {
val addressResponse = response.body()
Log.d("Order Repository", "Address information fetched successfully. Address count: ${addressResponse?.addresses?.size}")
addressResponse
} else {
val errorBody = response.errorBody()?.string() ?: "Unknown error"
Log.e("Order Repository", "Error fetching addresses. Code: ${response.code()}, Error: $errorBody")
null
}
} catch (e: Exception) {
Log.e("Order Repository", "Exception getting addresses", e)
null
}
}
suspend fun getCountCourierCost(courierCost: CourierCostRequest): Result<CourierCostResponse> {
return try {
Log.d("Order Repository", "Calculating courier cost. Request: $courierCost")
val response = apiService.countCourierCost(courierCost)
if (response.isSuccessful) {
response.body()?.let { courierCostResponse ->
Log.d("Order Repository", "Courier cost calculation successful. Courier costs: ${courierCostResponse.courierCosts.size}")
Result.Success(courierCostResponse)
} ?: run {
Result.Error(Exception("Failed to get courier cost: Empty response"))
}
} else {
val errorMsg = response.errorBody()?.string() ?: "Unknown error"
Log.e("Order Repository", "Error calculating courier cost. Code: ${response.code()}, Error: $errorMsg")
Result.Error(Exception(errorMsg))
}
} catch (e: Exception) {
Log.e("Order Repository", "Exception calculating courier cost", e)
Result.Error(e)
}
}
suspend fun getCart(): Result<List<DataItem>> {
return try {
val response = apiService.getCart()
if (response.isSuccessful) {
val cartData = response.body()?.data
if (!cartData.isNullOrEmpty()) {
Result.Success(cartData)
} else {
Log.e("Order Repository", "Cart data is empty")
Result.Error(Exception("Cart is empty"))
}
} else {
val errorMsg = response.errorBody()?.string() ?: "Unknown error"
Log.e("Order Repository", "Error fetching cart: $errorMsg")
Result.Error(Exception(errorMsg))
}
} catch (e: Exception) {
Log.e("Order Repository", "Exception fetching cart", e)
Result.Error(e)
}
}
suspend fun fetchStoreDetail(storeId: Int): Result<StoreProduct?> {
return try {
val response = apiService.getDetailStore(storeId)
if (response.isSuccessful) {
val store = response.body()?.store
if (store != null) {
Result.Success(store)
} else {
Result.Error(Exception("Store 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 store details", e)
Result.Error(e)
}
}
suspend fun addAddress(createAddressRequest: CreateAddressRequest): Result<CreateAddressResponse> {
return try {
val response = apiService.createAddress(createAddressRequest)
if (response.isSuccessful){
response.body()?.let {
Result.Success(it)
} ?: Result.Error(Exception("Add Address failed"))
} else {
Log.e("OrderRepository", "Error: ${response.errorBody()?.string()}")
Result.Error(Exception(response.errorBody()?.string() ?: "Unknown error"))
}
} 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
}
}

View File

@ -1,10 +1,13 @@
package com.alya.ecommerce_serang.data.repository
import android.util.Log
import com.alya.ecommerce_serang.data.api.dto.CartItem
import com.alya.ecommerce_serang.data.api.dto.CategoryItem
import com.alya.ecommerce_serang.data.api.dto.ProductsItem
import com.alya.ecommerce_serang.data.api.response.ProductResponse
import com.alya.ecommerce_serang.data.api.response.ReviewsItem
import com.alya.ecommerce_serang.data.api.response.cart.AddCartResponse
import com.alya.ecommerce_serang.data.api.response.product.ProductResponse
import com.alya.ecommerce_serang.data.api.response.product.ReviewsItem
import com.alya.ecommerce_serang.data.api.response.product.StoreProduct
import com.alya.ecommerce_serang.data.api.retrofit.ApiService
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
@ -19,26 +22,37 @@ class ProductRepository(private val apiService: ApiService) {
if (response.isSuccessful) {
// Return a Result.Success with the list of products
Result.Success(response.body()?.products ?: emptyList())
val products = response.body()?.products ?: emptyList()
Log.d(TAG, "Products fetched successfully. Total products: ${products.size}")
// Optional: Log some product details
products.take(3).forEach { product ->
Log.d(TAG, "Sample Product - ID: ${product.id}, Name: ${product.name}, Price: ${product.price}")
}
Result.Success(products)
} else {
// Return a Result.Error with a custom Exception
Log.e("ProductRepository", "Error: ${response.errorBody()?.string()}")
val errorBody = response.errorBody()?.string() ?: "Unknown error"
Log.e(TAG, "Failed to fetch products. Code: ${response.code()}, Error: $errorBody")
Result.Error(Exception("Failed to fetch products. Code: ${response.code()}"))
}
} catch (e: Exception) {
// Return a Result.Error with the exception caught
Log.e(TAG, "Exception while fetching products", e)
Result.Error(e)
}
}
suspend fun fetchProductDetail(productId: Int): ProductResponse? {
return try {
Log.d(TAG, "Fetching product detail for ID: $productId")
val response = apiService.getDetailProduct(productId)
if (response.isSuccessful) {
response.body()
val productResponse = response.body()
Log.d(TAG, "Product detail fetched successfully. Product: ${productResponse?.product?.productName}")
productResponse
} else {
Log.e("ProductRepository", "Error: ${response.errorBody()?.string()}")
val errorBody = response.errorBody()?.string() ?: "Unknown error"
Log.e(TAG, "Error fetching product detail. Code: ${response.code()}, Error: $errorBody")
null
}
} catch (e: Exception) {
@ -54,14 +68,14 @@ class ProductRepository(private val apiService: ApiService) {
if (response.isSuccessful) {
val categories = response.body()?.category ?: emptyList()
Log.d("Categories", "Fetched categories: $categories")
Log.d("ProductRepository", "Fetched categories: $categories")
categories.forEach { Log.d("Category Image", "Category: ${it.name}, Image: ${it.image}") }
Result.Success(categories)
} else {
Result.Error(Exception("Failed to fetch categories. Code: ${response.code()}"))
}
} catch (e: Exception) {
Log.e("Categories", "Error fetching categories", e)
Log.e("ProductRepository", "Error fetching categories", e)
Result.Error(e)
}
}
@ -80,6 +94,42 @@ class ProductRepository(private val apiService: ApiService) {
}
}
suspend fun addToCart(request: CartItem): Result<AddCartResponse> {
return try {
val response = apiService.addCart(request)
if (response.isSuccessful) {
response.body()?.let {
Result.Success(it)
} ?: Result.Error(Exception("Add Cart failed"))
} else {
Log.e("ProductRepository", "Error: ${response.errorBody()?.string()}")
Result.Error(Exception(response.errorBody()?.string() ?: "Unknown Error"))
}
} catch (e: Exception) {
Result.Error(e)
}
}
suspend fun fetchStoreDetail(storeId: Int): Result<StoreProduct?> {
return try {
val response = apiService.getDetailStore(storeId)
if (response.isSuccessful) {
val store = response.body()?.store
if (store != null) {
Result.Success(store)
} else {
Result.Error(Throwable("Empty response body"))
}
} else {
val errorMsg = response.errorBody()?.string() ?: "Unknown error"
Log.e("ProductRepository", "Error: $errorMsg")
Result.Error(Throwable(errorMsg))
}
} catch (e: Exception) {
Result.Error(e)
}
}
suspend fun fetchMyStoreProducts(): List<ProductsItem> {
val response = apiService.getStoreProduct()
if (response.isSuccessful) {
@ -127,6 +177,9 @@ class ProductRepository(private val apiService: ApiService) {
}
}
companion object {
private const val TAG = "ProductRepository"
}
}
// suspend fun fetchStoreDetail(storeId: Int): Store? {

View File

@ -4,18 +4,15 @@ import com.alya.ecommerce_serang.data.api.dto.LoginRequest
import com.alya.ecommerce_serang.data.api.dto.OtpRequest
import com.alya.ecommerce_serang.data.api.dto.RegisterRequest
import com.alya.ecommerce_serang.data.api.dto.UserProfile
import com.alya.ecommerce_serang.data.api.response.LoginResponse
import com.alya.ecommerce_serang.data.api.response.OtpResponse
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.retrofit.ApiService
class UserRepository(private val apiService: ApiService) {
//post data without message/response
suspend fun requestOtpRep(email: String): OtpResponse {
// fun requestOtpRep(email: String): Result<String> {
return apiService.getOTP(OtpRequest(email))
}
suspend fun registerUser(request: RegisterRequest): String {

View File

@ -61,6 +61,7 @@ class HorizontalProductAdapter(
val diffResult = DiffUtil.calculateDiff(diffCallback)
products = newProducts
diffResult.dispatchUpdatesTo(this)
notifyDataSetChanged()
}
fun updateLimitedProducts(newProducts: List<ProductsItem>) {

View File

@ -0,0 +1,79 @@
package com.alya.ecommerce_serang.ui.order
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.alya.ecommerce_serang.R
import com.alya.ecommerce_serang.data.api.dto.CheckoutData
import com.alya.ecommerce_serang.data.api.response.cart.CartItemsItem
import com.alya.ecommerce_serang.databinding.ItemOrderProductBinding
import com.alya.ecommerce_serang.databinding.ItemOrderSellerBinding
import com.bumptech.glide.Glide
import java.text.NumberFormat
import java.util.Locale
class CartCheckoutAdapter(private val checkoutData: CheckoutData) :
RecyclerView.Adapter<CartCheckoutAdapter.SellerViewHolder>() {
class SellerViewHolder(val binding: ItemOrderSellerBinding) : RecyclerView.ViewHolder(binding.root)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SellerViewHolder {
val binding = ItemOrderSellerBinding.inflate(
LayoutInflater.from(parent.context), parent, false
)
return SellerViewHolder(binding)
}
override fun getItemCount(): Int = 1 // Only one seller
override fun onBindViewHolder(holder: SellerViewHolder, position: Int) {
with(holder.binding) {
// Set seller name
tvStoreName.text = checkoutData.sellerName
// Set up products RecyclerView with multiple items
rvSellerOrderProduct.apply {
layoutManager = LinearLayoutManager(context)
adapter = MultiCartItemsAdapter(checkoutData.cartItems)
isNestedScrollingEnabled = false
}
}
}
}
class MultiCartItemsAdapter(private val cartItems: List<CartItemsItem>) :
RecyclerView.Adapter<MultiCartItemsAdapter.CartItemViewHolder>() {
class CartItemViewHolder(val binding: ItemOrderProductBinding) : RecyclerView.ViewHolder(binding.root)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CartItemViewHolder {
val binding = ItemOrderProductBinding.inflate(
LayoutInflater.from(parent.context), parent, false
)
return CartItemViewHolder(binding)
}
override fun getItemCount(): Int = cartItems.size
override fun onBindViewHolder(holder: CartItemViewHolder, position: Int) {
val item = cartItems[position]
with(holder.binding) {
// Set cart item details
tvProductName.text = item.productName
tvProductQuantity.text = "${item.quantity} buah"
tvProductPrice.text = formatCurrency(item.price.toDouble())
// Load placeholder image
Glide.with(ivProduct.context)
.load(R.drawable.placeholder_image)
.into(ivProduct)
}
}
private fun formatCurrency(amount: Double): String {
val formatter = NumberFormat.getCurrencyInstance(Locale("in", "ID"))
return formatter.format(amount).replace(",00", "")
}
}

View File

@ -0,0 +1,357 @@
package com.alya.ecommerce_serang.ui.order
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.LinearLayoutManager
import com.alya.ecommerce_serang.data.api.dto.CheckoutData
import com.alya.ecommerce_serang.data.api.dto.OrderRequest
import com.alya.ecommerce_serang.data.api.dto.OrderRequestBuy
import com.alya.ecommerce_serang.data.api.response.product.PaymentInfoItem
import com.alya.ecommerce_serang.data.api.retrofit.ApiConfig
import com.alya.ecommerce_serang.data.repository.OrderRepository
import com.alya.ecommerce_serang.databinding.ActivityCheckoutBinding
import com.alya.ecommerce_serang.ui.order.address.AddressActivity
import com.alya.ecommerce_serang.utils.BaseViewModelFactory
import com.alya.ecommerce_serang.utils.SessionManager
import java.text.NumberFormat
import java.util.Locale
class CheckoutActivity : AppCompatActivity() {
private lateinit var binding: ActivityCheckoutBinding
private lateinit var sessionManager: SessionManager
private var paymentAdapter: PaymentMethodAdapter? = null
private val viewModel: CheckoutViewModel by viewModels {
BaseViewModelFactory {
val apiService = ApiConfig.getApiService(sessionManager)
val orderRepository = OrderRepository(apiService)
CheckoutViewModel(orderRepository)
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityCheckoutBinding.inflate(layoutInflater)
setContentView(binding.root)
sessionManager = SessionManager(this)
// Setup UI components
setupToolbar()
setupObservers()
setupClickListeners()
processIntentData()
}
private fun processIntentData() {
// Determine if this is Buy Now or Cart checkout
val isBuyNow = intent.hasExtra(EXTRA_PRODUCT_ID) && !intent.hasExtra(EXTRA_CART_ITEM_IDS)
if (isBuyNow) {
// Process Buy Now flow
viewModel.initializeBuyNow(
storeId = intent.getIntExtra(EXTRA_STORE_ID, 0),
storeName = intent.getStringExtra(EXTRA_STORE_NAME),
productId = intent.getIntExtra(EXTRA_PRODUCT_ID, 0),
productName = intent.getStringExtra(EXTRA_PRODUCT_NAME),
productImage = intent.getStringExtra(EXTRA_PRODUCT_IMAGE),
quantity = intent.getIntExtra(EXTRA_QUANTITY, 1),
price = intent.getDoubleExtra(EXTRA_PRICE, 0.0)
)
} else {
// Process Cart checkout flow
val cartItemIds = intent.getIntArrayExtra(EXTRA_CART_ITEM_IDS)?.toList() ?: emptyList()
if (cartItemIds.isNotEmpty()) {
viewModel.initializeFromCart(cartItemIds)
} else {
Toast.makeText(this, "Error: No cart items specified", Toast.LENGTH_SHORT).show()
finish()
}
}
}
private fun setupToolbar() {
binding.toolbar.setNavigationOnClickListener {
finish()
}
}
private fun setupObservers() {
// Observe checkout data
viewModel.checkoutData.observe(this) { data ->
setupProductRecyclerView(data)
updateOrderSummary()
// Load payment methods
viewModel.getPaymentMethods { paymentMethods ->
if (paymentMethods.isNotEmpty()) {
setupPaymentMethodsRecyclerView(paymentMethods)
}
}
}
// Observe address details
viewModel.addressDetails.observe(this) { address ->
binding.tvPlacesAddress.text = address?.recipient
binding.tvAddress.text = "${address?.street}, ${address?.subdistrict}"
}
// Observe payment details
viewModel.paymentDetails.observe(this) { payment ->
if (payment != null) {
// Update selected payment in adapter by name instead of ID
paymentAdapter?.setSelectedPaymentName(payment.name)
}
}
// Observe loading state
viewModel.isLoading.observe(this) { isLoading ->
binding.btnPay.isEnabled = !isLoading
// Show/hide loading indicator if you have one
}
// Observe error messages
viewModel.errorMessage.observe(this) { message ->
if (message.isNotEmpty()) {
Toast.makeText(this, message, Toast.LENGTH_SHORT).show()
}
}
// Observe order creation
viewModel.orderCreated.observe(this) { created ->
if (created) {
Toast.makeText(this, "Order successfully created!", Toast.LENGTH_SHORT).show()
setResult(RESULT_OK)
finish()
}
}
}
private fun setupProductRecyclerView(checkoutData: CheckoutData) {
val adapter = if (checkoutData.isBuyNow || checkoutData.cartItems.size <= 1) {
CheckoutSellerAdapter(checkoutData)
} else {
CartCheckoutAdapter(checkoutData)
}
binding.rvProductItems.apply {
layoutManager = LinearLayoutManager(this@CheckoutActivity)
this.adapter = adapter
isNestedScrollingEnabled = false
}
}
private fun setupPaymentMethodsRecyclerView(paymentMethods: List<PaymentInfoItem>) {
paymentAdapter = PaymentMethodAdapter(paymentMethods) { payment ->
// When a payment method is selected
// Since PaymentInfoItem doesn't have an id field, we'll use the name as identifier
// You might need to convert the name to an ID if your backend expects an integer
val paymentId = payment.name.toIntOrNull() ?: 0
viewModel.setPaymentMethod(paymentId)
}
binding.rvPaymentMethods.apply {
layoutManager = LinearLayoutManager(this@CheckoutActivity)
adapter = paymentAdapter
}
}
private fun updateOrderSummary() {
viewModel.checkoutData.value?.let { data ->
// Update price information
binding.tvItemTotal.text = formatCurrency(viewModel.calculateSubtotal())
// Get shipping price
val shipPrice = if (data.isBuyNow) {
(data.orderRequest as OrderRequestBuy).shipPrice.toDouble()
} else {
(data.orderRequest as OrderRequest).shipPrice.toDouble()
}
binding.tvShippingFee.text = formatCurrency(shipPrice)
// Update total
val total = viewModel.calculateTotal()
binding.tvTotal.text = formatCurrency(total)
binding.tvBottomTotal.text = formatCurrency(total)
}
}
private fun updateShippingUI(shipName: String, shipService: String, shipEtd: String, shipPrice: Int) {
if (shipName.isNotEmpty() && shipService.isNotEmpty()) {
// Display shipping name and service in one line
binding.tvCourierName.text = "$shipName $shipService"
binding.tvDeliveryEstimate.text = "$shipEtd hari kerja"
binding.tvShippingPrice.text = formatCurrency(shipPrice.toDouble())
binding.rbJne.isChecked = true
}
}
private fun setupClickListeners() {
// Address selection
binding.tvChangeAddress.setOnClickListener {
val intent = Intent(this, AddressActivity::class.java)
addressSelectionLauncher.launch(intent)
}
// Shipping method selection
binding.layoutShippingMethod.setOnClickListener {
val addressId = viewModel.addressDetails.value?.id ?: 0
if (addressId <= 0) {
Toast.makeText(this, "Please select delivery address first", Toast.LENGTH_SHORT).show()
return@setOnClickListener
}
// Launch shipping selection with address and product info
val intent = Intent(this, ShippingActivity::class.java)
intent.putExtra(ShippingActivity.EXTRA_ADDRESS_ID, addressId)
// Add product info for courier cost calculation
val currentData = viewModel.checkoutData.value
if (currentData != null) {
if (currentData.isBuyNow) {
val buyRequest = currentData.orderRequest as OrderRequestBuy
intent.putExtra(ShippingActivity.EXTRA_PRODUCT_ID, buyRequest.productId)
intent.putExtra(ShippingActivity.EXTRA_QUANTITY, buyRequest.quantity)
} else {
// For cart, we'll pass the first item's info
val firstItem = currentData.cartItems.firstOrNull()
if (firstItem != null) {
intent.putExtra(ShippingActivity.EXTRA_PRODUCT_ID, firstItem.productId)
intent.putExtra(ShippingActivity.EXTRA_QUANTITY, firstItem.quantity)
}
}
}
shippingSelectionLauncher.launch(intent)
}
// Create order button
binding.btnPay.setOnClickListener {
if (validateOrder()) {
viewModel.createOrder()
}
}
// Voucher section (if implemented)
binding.layoutVoucher?.setOnClickListener {
Toast.makeText(this, "Voucher feature not implemented", Toast.LENGTH_SHORT).show()
}
}
private val addressSelectionLauncher = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) { result ->
if (result.resultCode == RESULT_OK) {
val addressId = result.data?.getIntExtra(AddressActivity.EXTRA_ADDRESS_ID, 0) ?: 0
if (addressId > 0) {
viewModel.setSelectedAddress(addressId)
}
}
}
private val shippingSelectionLauncher = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) { result ->
if (result.resultCode == RESULT_OK) {
val data = result.data ?: return@registerForActivityResult
val shipName = data.getStringExtra(ShippingActivity.EXTRA_SHIP_NAME) ?: return@registerForActivityResult
val shipService = data.getStringExtra(ShippingActivity.EXTRA_SHIP_SERVICE) ?: return@registerForActivityResult
val shipPrice = data.getIntExtra(ShippingActivity.EXTRA_SHIP_PRICE, 0)
val shipEtd = data.getStringExtra(ShippingActivity.EXTRA_SHIP_ETD) ?: ""
// Update shipping in ViewModel
viewModel.setShippingMethod(shipName, shipService, shipPrice, shipEtd)
// Update UI - display shipping name and service in one line
updateShippingUI(shipName, shipService, shipEtd, shipPrice)
}
}
private fun formatCurrency(amount: Double): String {
val formatter = NumberFormat.getCurrencyInstance(Locale("in", "ID"))
return formatter.format(amount).replace(",00", "")
}
private fun validateOrder(): Boolean {
// Check if address is selected
if (viewModel.addressDetails.value == null) {
Toast.makeText(this, "Silakan pilih alamat pengiriman", Toast.LENGTH_SHORT).show()
return false
}
// Check if shipping is selected
val checkoutData = viewModel.checkoutData.value ?: return false
val shipName = if (checkoutData.isBuyNow) {
(checkoutData.orderRequest as OrderRequestBuy).shipName
} else {
(checkoutData.orderRequest as OrderRequest).shipName
}
if (shipName.isEmpty()) {
Toast.makeText(this, "Silakan pilih metode pengiriman", Toast.LENGTH_SHORT).show()
return false
}
// Check if payment method is selected
if (viewModel.paymentDetails.value == null) {
Toast.makeText(this, "Silakan pilih metode pembayaran", Toast.LENGTH_SHORT).show()
return false
}
return true
}
companion object {
// Intent extras
const val EXTRA_CART_ITEM_IDS = "extra_cart_item_ids"
const val EXTRA_STORE_ID = "STORE_ID"
const val EXTRA_STORE_NAME = "STORE_NAME"
const val EXTRA_PRODUCT_ID = "PRODUCT_ID"
const val EXTRA_PRODUCT_NAME = "PRODUCT_NAME"
const val EXTRA_PRODUCT_IMAGE = "PRODUCT_IMAGE"
const val EXTRA_QUANTITY = "QUANTITY"
const val EXTRA_PRICE = "PRICE"
// Helper methods for starting activity
// For Buy Now
fun startForBuyNow(
context: Context,
storeId: Int,
storeName: String?,
productId: Int,
productName: String?,
productImage: String?,
quantity: Int,
price: Double
) {
val intent = Intent(context, CheckoutActivity::class.java).apply {
putExtra(EXTRA_STORE_ID, storeId)
putExtra(EXTRA_STORE_NAME, storeName)
putExtra(EXTRA_PRODUCT_ID, productId)
putExtra(EXTRA_PRODUCT_NAME, productName)
putExtra(EXTRA_PRODUCT_IMAGE, productImage)
putExtra(EXTRA_QUANTITY, quantity)
putExtra(EXTRA_PRICE, price)
}
context.startActivity(intent)
}
// For Cart checkout
fun startForCart(
context: Context,
cartItemIds: List<Int>
) {
val intent = Intent(context, CheckoutActivity::class.java).apply {
putExtra(EXTRA_CART_ITEM_IDS, cartItemIds.toIntArray())
}
context.startActivity(intent)
}
}
}

View File

@ -0,0 +1,44 @@
package com.alya.ecommerce_serang.ui.order
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.alya.ecommerce_serang.data.api.dto.CheckoutData
import com.alya.ecommerce_serang.databinding.ItemOrderSellerBinding
// Adapter for seller section that contains the product
class CheckoutSellerAdapter(private val checkoutData: CheckoutData) :
RecyclerView.Adapter<CheckoutSellerAdapter.SellerViewHolder>() {
class SellerViewHolder(val binding: ItemOrderSellerBinding) : RecyclerView.ViewHolder(binding.root)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SellerViewHolder {
val binding = ItemOrderSellerBinding.inflate(
LayoutInflater.from(parent.context), parent, false
)
return SellerViewHolder(binding)
}
override fun getItemCount(): Int = 1 // Only one seller
override fun onBindViewHolder(holder: SellerViewHolder, position: Int) {
with(holder.binding) {
// Set seller name
tvStoreName.text = checkoutData.sellerName
// Set up products RecyclerView
rvSellerOrderProduct.apply {
layoutManager = LinearLayoutManager(context)
adapter = if (checkoutData.isBuyNow) {
// Single product for Buy Now
SingleProductAdapter(checkoutData)
} else {
// Single cart item
SingleCartItemAdapter(checkoutData.cartItems.first())
}
isNestedScrollingEnabled = false
}
}
}
}

View File

@ -0,0 +1,330 @@
package com.alya.ecommerce_serang.ui.order
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.CheckoutData
import com.alya.ecommerce_serang.data.api.dto.OrderRequest
import com.alya.ecommerce_serang.data.api.dto.OrderRequestBuy
import com.alya.ecommerce_serang.data.api.response.cart.CartItemsItem
import com.alya.ecommerce_serang.data.api.response.cart.DataItem
import com.alya.ecommerce_serang.data.api.response.product.PaymentInfoItem
import com.alya.ecommerce_serang.data.api.response.profile.AddressesItem
import com.alya.ecommerce_serang.data.repository.OrderRepository
import com.alya.ecommerce_serang.data.repository.Result
import kotlinx.coroutines.launch
class CheckoutViewModel(private val repository: OrderRepository) : ViewModel() {
private val _checkoutData = MutableLiveData<CheckoutData>()
val checkoutData: LiveData<CheckoutData> = _checkoutData
private val _addressDetails = MutableLiveData<AddressesItem?>()
val addressDetails: LiveData<AddressesItem?> = _addressDetails
private val _paymentDetails = MutableLiveData<PaymentInfoItem?>()
val paymentDetails: LiveData<PaymentInfoItem?> = _paymentDetails
private val _isLoading = MutableLiveData<Boolean>()
val isLoading: LiveData<Boolean> = _isLoading
private val _errorMessage = MutableLiveData<String>()
val errorMessage: LiveData<String> = _errorMessage
private val _orderCreated = MutableLiveData<Boolean>()
val orderCreated: LiveData<Boolean> = _orderCreated
// Initialize "Buy Now" checkout
fun initializeBuyNow(
storeId: Int,
storeName: String?,
productId: Int,
productName: String?,
productImage: String?,
quantity: Int,
price: Double
) {
viewModelScope.launch {
_isLoading.value = true
try {
// Create initial OrderRequestBuy object
val orderRequest = OrderRequestBuy(
addressId = 0, // Will be set when user selects address
paymentMethodId = 0, // Will be set when user selects payment
shipPrice = 0, // Will be set when user selects shipping
shipName = "",
shipService = "",
isNego = false, // Default value
productId = productId,
quantity = quantity,
shipEtd = ""
)
// Create checkout data
_checkoutData.value = CheckoutData(
orderRequest = orderRequest,
productName = productName,
productImageUrl = productImage ?: "",
productPrice = price,
sellerName = storeName ?: "",
sellerId = storeId,
quantity = quantity,
isBuyNow = true
)
} catch (e: Exception) {
Log.e(TAG, "Error initializing Buy Now data", e)
_errorMessage.value = "Failed to initialize checkout: ${e.message}"
} finally {
_isLoading.value = false
}
}
}
// Initialize checkout from cart
fun initializeFromCart(cartItemIds: List<Int>) {
viewModelScope.launch {
_isLoading.value = true
try {
// Get cart data
val cartResult = repository.getCart()
if (cartResult is Result.Success) {
// Find matching cart items
val matchingItems = mutableListOf<CartItemsItem>()
var storeData: DataItem? = null
for (store in cartResult.data) {
val storeItems = store.cartItems.filter { it.cartItemId in cartItemIds }
if (storeItems.isNotEmpty()) {
matchingItems.addAll(storeItems)
storeData = store
break
}
}
if (matchingItems.isNotEmpty() && storeData != null) {
// Create initial OrderRequest object
val orderRequest = OrderRequest(
addressId = 0, // Will be set when user selects address
paymentMethodId = 0, // Will be set when user selects payment
shipPrice = 0, // Will be set when user selects shipping
shipName = "",
shipService = "",
isNego = false,
cartItemId = cartItemIds,
shipEtd = ""
)
// Create checkout data
_checkoutData.value = CheckoutData(
orderRequest = orderRequest,
productName = matchingItems.first().productName,
sellerName = storeData.storeName,
sellerId = storeData.storeId,
isBuyNow = false,
cartItems = matchingItems
)
} else {
_errorMessage.value = "No matching cart items found"
}
} else if (cartResult is Result.Error) {
_errorMessage.value = "Failed to fetch cart items: ${cartResult.exception.message}"
}
} catch (e: Exception) {
Log.e(TAG, "Error initializing cart checkout", e)
_errorMessage.value = "Error: ${e.message}"
} finally {
_isLoading.value = false
}
}
}
// Get payment methods from API
fun getPaymentMethods(callback: (List<PaymentInfoItem>) -> Unit) {
viewModelScope.launch {
try {
val storeId = _checkoutData.value?.sellerId ?: return@launch
// Use fetchStoreDetail instead of getStore
val storeResult = repository.fetchStoreDetail(storeId)
if (storeResult is Result.Success && storeResult.data != null) {
callback(storeResult.data.paymentInfo)
} else {
callback(emptyList())
}
} catch (e: Exception) {
Log.e(TAG, "Error fetching payment methods", e)
callback(emptyList())
}
}
}
// Set selected address
fun setSelectedAddress(addressId: Int) {
viewModelScope.launch {
_isLoading.value = true
try {
// Get address details from API
val addressResponse = repository.getAddress()
if (addressResponse != null && !addressResponse.addresses.isNullOrEmpty()) {
val address = addressResponse.addresses.find { it.id == addressId }
if (addressResponse != null && !addressResponse.addresses.isNullOrEmpty()) {
val address = addressResponse.addresses.find { it.id == addressId }
// No need for null check since _addressDetails now accepts nullable values
_addressDetails.value = address
// Update order request with address ID only if address isn't null
if (address != null) {
val currentData = _checkoutData.value ?: return@launch
if (currentData.isBuyNow) {
val buyRequest = currentData.orderRequest as OrderRequestBuy
val updatedRequest = buyRequest.copy(addressId = addressId)
_checkoutData.value = currentData.copy(orderRequest = updatedRequest)
} else {
val cartRequest = currentData.orderRequest as OrderRequest
val updatedRequest = cartRequest.copy(addressId = addressId)
_checkoutData.value = currentData.copy(orderRequest = updatedRequest)
}
}
}
}
} catch (e: Exception) {
_errorMessage.value = "Error loading address: ${e.message}"
} finally {
_isLoading.value = false
}
}
}
// Set shipping method
fun setShippingMethod(shipName: String, shipService: String, shipPrice: Int, shipEtd: String) {
val currentData = _checkoutData.value ?: return
if (currentData.isBuyNow) {
val buyRequest = currentData.orderRequest as OrderRequestBuy
val updatedRequest = buyRequest.copy(
shipName = shipName,
shipService = shipService,
shipPrice = shipPrice,
shipEtd = shipEtd
)
_checkoutData.value = currentData.copy(orderRequest = updatedRequest)
} else {
val cartRequest = currentData.orderRequest as OrderRequest
val updatedRequest = cartRequest.copy(
shipName = shipName,
shipService = shipService,
shipPrice = shipPrice,
shipEtd = shipEtd
)
_checkoutData.value = currentData.copy(orderRequest = updatedRequest)
}
}
// Set payment method
fun setPaymentMethod(paymentId: Int) {
viewModelScope.launch {
try {
val storeId = _checkoutData.value?.sellerId ?: return@launch
// Use fetchStoreDetail instead of getStore
val storeResult = repository.fetchStoreDetail(storeId)
if (storeResult is Result.Success && storeResult.data != null) {
// Find the selected payment in the payment info list
val payment = storeResult.data.paymentInfo.find { it.name == paymentId.toString() }
_paymentDetails.value = payment
// Update order request if payment isn't null
if (payment != null) {
val currentData = _checkoutData.value ?: return@launch
if (currentData.isBuyNow) {
val buyRequest = currentData.orderRequest as OrderRequestBuy
val updatedRequest = buyRequest.copy(paymentMethodId = paymentId)
_checkoutData.value = currentData.copy(orderRequest = updatedRequest)
} else {
val cartRequest = currentData.orderRequest as OrderRequest
val updatedRequest = cartRequest.copy(paymentMethodId = paymentId)
_checkoutData.value = currentData.copy(orderRequest = updatedRequest)
}
}
}
} catch (e: Exception) {
_errorMessage.value = "Error setting payment method: ${e.message}"
}
}
}
// Create order
fun createOrder() {
viewModelScope.launch {
_isLoading.value = true
try {
val data = _checkoutData.value ?: throw Exception("No checkout data available")
val response = if (data.isBuyNow) {
// For Buy Now, use the dedicated endpoint
val buyRequest = data.orderRequest as OrderRequestBuy
repository.createOrderBuyNow(buyRequest)
} else {
// For Cart checkout, use the standard order endpoint
val cartRequest = data.orderRequest as OrderRequest
repository.createOrder(cartRequest)
}
if (response.isSuccessful) {
_orderCreated.value = true
} else {
val errorMsg = response.errorBody()?.string() ?: "Unknown error"
_errorMessage.value = "Failed to create order: $errorMsg"
}
} catch (e: Exception) {
_errorMessage.value = "Error creating order: ${e.message}"
} finally {
_isLoading.value = false
}
}
}
// Calculate total price (subtotal + shipping)
fun calculateTotal(): Double {
val data = _checkoutData.value ?: return 0.0
return calculateSubtotal() + getShippingPrice()
}
// Calculate subtotal (without shipping)
fun calculateSubtotal(): Double {
val data = _checkoutData.value ?: return 0.0
return if (data.isBuyNow) {
// For Buy Now, use product price * quantity
val buyRequest = data.orderRequest as OrderRequestBuy
data.productPrice * buyRequest.quantity
} else {
// For Cart, sum all items
data.cartItems.sumOf { it.price * it.quantity.toDouble() }
}
}
// Get shipping price
private fun getShippingPrice(): Double {
val data = _checkoutData.value ?: return 0.0
return if (data.isBuyNow) {
(data.orderRequest as OrderRequestBuy).shipPrice.toDouble()
} else {
(data.orderRequest as OrderRequest).shipPrice.toDouble()
}
}
companion object {
private const val TAG = "CheckoutViewModel"
}
}

View File

@ -0,0 +1,90 @@
package com.alya.ecommerce_serang.ui.order
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.response.product.PaymentInfoItem
import com.alya.ecommerce_serang.databinding.ItemPaymentMethodBinding
import com.bumptech.glide.Glide
import com.bumptech.glide.request.RequestOptions
class PaymentMethodAdapter(
private val paymentMethods: List<PaymentInfoItem>,
private val onPaymentSelected: (PaymentInfoItem) -> Unit
) : RecyclerView.Adapter<PaymentMethodAdapter.PaymentMethodViewHolder>() {
// Track the selected position
private var selectedPosition = -1
class PaymentMethodViewHolder(val binding: ItemPaymentMethodBinding) :
RecyclerView.ViewHolder(binding.root)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PaymentMethodViewHolder {
val binding = ItemPaymentMethodBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
return PaymentMethodViewHolder(binding)
}
override fun getItemCount(): Int = paymentMethods.size
override fun onBindViewHolder(holder: PaymentMethodViewHolder, position: Int) {
val payment = paymentMethods[position]
with(holder.binding) {
// Set payment method name
tvPaymentMethodName.text = payment.name
// Set radio button state
rbPaymentMethod.isChecked = selectedPosition == position
// Load payment icon if available
if (payment.qrisImage.isNotEmpty()) {
Glide.with(ivPaymentMethod.context)
.load(payment.qrisImage)
.apply(RequestOptions()
.placeholder(R.drawable.outline_store_24)
.error(R.drawable.outline_store_24))
.into(ivPaymentMethod)
} else {
// Default icon for bank transfers
ivPaymentMethod.setImageResource(R.drawable.outline_store_24)
}
// Handle click on the entire item
root.setOnClickListener {
selectPayment(position)
onPaymentSelected(payment)
}
// Handle click on the radio button
rbPaymentMethod.setOnClickListener {
selectPayment(position)
onPaymentSelected(payment)
}
}
}
// Helper method to handle payment selection
private fun selectPayment(position: Int) {
if (selectedPosition != position) {
val previousPosition = selectedPosition
selectedPosition = position
// Update UI for previous and new selection
notifyItemChanged(previousPosition)
notifyItemChanged(position)
}
}
//selected by name
fun setSelectedPaymentName(paymentName: String) {
val position = paymentMethods.indexOfFirst { it.name == paymentName }
if (position != -1 && position != selectedPosition) {
selectPayment(position)
}
}
}

View File

@ -0,0 +1,151 @@
package com.alya.ecommerce_serang.ui.order
import android.content.Intent
import android.os.Bundle
import android.widget.Toast
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.isVisible
import androidx.recyclerview.widget.LinearLayoutManager
import com.alya.ecommerce_serang.data.api.retrofit.ApiConfig
import com.alya.ecommerce_serang.data.repository.OrderRepository
import com.alya.ecommerce_serang.databinding.ActivityShippingBinding
import com.alya.ecommerce_serang.utils.BaseViewModelFactory
import com.alya.ecommerce_serang.utils.SessionManager
class ShippingActivity : AppCompatActivity() {
private lateinit var binding: ActivityShippingBinding
private lateinit var sessionManager: SessionManager
private lateinit var shippingAdapter: ShippingAdapter
private val viewModel: ShippingViewModel by viewModels {
BaseViewModelFactory {
val apiService = ApiConfig.getApiService(sessionManager)
val repository = OrderRepository(apiService)
ShippingViewModel(repository)
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityShippingBinding.inflate(layoutInflater)
setContentView(binding.root)
// Initialize SessionManager
sessionManager = SessionManager(this)
// Get data from intent
val addressId = intent.getIntExtra(EXTRA_ADDRESS_ID, 0)
val productId = intent.getIntExtra(EXTRA_PRODUCT_ID, 0)
val quantity = intent.getIntExtra(EXTRA_QUANTITY, 1)
// Validate required information
if (addressId <= 0 || productId <= 0) {
Toast.makeText(this, "Missing required shipping information", Toast.LENGTH_SHORT).show()
finish()
return
}
// Setup UI components
setupToolbar()
setupRecyclerView()
setupObservers()
// Load shipping options
viewModel.loadShippingOptions(addressId, productId, quantity)
}
private fun setupToolbar() {
binding.toolbar.setNavigationOnClickListener {
finish()
}
}
private fun setupRecyclerView() {
shippingAdapter = ShippingAdapter { courierCostsItem, service ->
// Handle shipping method selection
returnSelectedShipping(
courierCostsItem.courier,
service.service,
service.cost,
service.etd
)
}
binding.rvShipmentOrder.apply {
layoutManager = LinearLayoutManager(this@ShippingActivity)
adapter = shippingAdapter
}
}
private fun setupObservers() {
// Observe shipping options
viewModel.shippingOptions.observe(this) { courierOptions ->
shippingAdapter.submitList(courierOptions)
updateEmptyState(courierOptions.isEmpty() || courierOptions.all { it.services.isEmpty() })
}
// Observe loading state
viewModel.isLoading.observe(this) { isLoading ->
// binding.progressBar.isVisible = isLoading
}
// Observe error messages
viewModel.errorMessage.observe(this) { message ->
if (message.isNotEmpty()) {
Toast.makeText(this, message, Toast.LENGTH_SHORT).show()
}
}
}
private fun updateEmptyState(isEmpty: Boolean) {
// binding.layoutEmptyShipping.isVisible = isEmpty
binding.rvShipmentOrder.isVisible = !isEmpty
}
private fun returnSelectedShipping(
shipName: String,
shipService: String,
shipPrice: Int,
shipEtd: String
) {
val intent = Intent().apply {
putExtra(EXTRA_SHIP_NAME, shipName)
putExtra(EXTRA_SHIP_SERVICE, shipService)
putExtra(EXTRA_SHIP_PRICE, shipPrice)
putExtra(EXTRA_SHIP_ETD, shipEtd)
}
setResult(RESULT_OK, intent)
finish()
}
companion object {
// Constants for intent extras
const val EXTRA_ADDRESS_ID = "extra_address_id"
const val EXTRA_PRODUCT_ID = "extra_product_id"
const val EXTRA_QUANTITY = "extra_quantity"
const val EXTRA_SHIP_NAME = "extra_ship_name"
const val EXTRA_SHIP_SERVICE = "extra_ship_service"
const val EXTRA_SHIP_PRICE = "extra_ship_price"
const val EXTRA_SHIP_ETD = "extra_ship_etd"
}
}
//val launcher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
// if (result.resultCode == RESULT_OK) {
// val data = result.data
// val shipName = data?.getStringExtra("ship_name")
// val shipPrice = data?.getIntExtra("ship_price", 0)
// val shipService = data?.getStringExtra("ship_service")
// // use the data as needed
// }
//}
//
//// launch the shipping activity
//val intent = Intent(this, ShippingActivity::class.java).apply {
// putExtra("address_id", addressId)
// putExtra("product_id", productId)
// putExtra("quantity", quantity)
//}
//launcher.launch(intent)

View File

@ -0,0 +1,96 @@
package com.alya.ecommerce_serang.ui.order
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.alya.ecommerce_serang.data.api.response.order.CourierCostsItem
import com.alya.ecommerce_serang.data.api.response.order.ServicesItem
import com.alya.ecommerce_serang.databinding.ItemShippingOrderBinding
class ShippingAdapter(
private val onItemSelected: (CourierCostsItem, ServicesItem) -> Unit
) : RecyclerView.Adapter<ShippingAdapter.ShippingViewHolder>() {
private val courierCostsList = mutableListOf<CourierCostsItem>()
private var selectedPosition = RecyclerView.NO_POSITION
private var selectedCourierPosition = RecyclerView.NO_POSITION
fun submitList(courierCostsList: List<CourierCostsItem>) {
this.courierCostsList.clear()
this.courierCostsList.addAll(courierCostsList)
notifyDataSetChanged()
}
inner class ShippingViewHolder(
private val binding: ItemShippingOrderBinding
) : RecyclerView.ViewHolder(binding.root) {
fun bind(courierCostsItem: CourierCostsItem, service: ServicesItem, isSelected: Boolean) {
binding.apply {
// Combine courier name and service
courierNameCost.text = "${courierCostsItem.courier} - ${service.service}"
estDate.text = "Estimasi ${service.etd} hari"
costPrice.text = "Rp${service.cost}"
// Single click handler for both item and radio button
val onClickAction = {
val newPosition = adapterPosition
if (newPosition != RecyclerView.NO_POSITION) {
// Update selected position
val oldPosition = selectedPosition
selectedPosition = newPosition
selectedCourierPosition = getParentCourierPosition(courierCostsItem)
// Notify only the changed items to improve performance
notifyItemChanged(oldPosition)
notifyItemChanged(newPosition)
// Call the callback with both courier and service
onItemSelected(courierCostsItem, service)
}
}
root.setOnClickListener { onClickAction() }
radioBtnCost.apply {
isChecked = isSelected
setOnClickListener { onClickAction() }
}
}
}
}
private fun getParentCourierPosition(courierCostsItem: CourierCostsItem): Int {
return courierCostsList.indexOfFirst { it == courierCostsItem }
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ShippingViewHolder {
val binding = ItemShippingOrderBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
return ShippingViewHolder(binding)
}
override fun onBindViewHolder(holder: ShippingViewHolder, position: Int) {
// Flatten the nested structure for binding
var currentPosition = 0
for (courierCostsItem in courierCostsList) {
for (service in courierCostsItem.services) {
if (currentPosition == position) {
holder.bind(
courierCostsItem,
service,
currentPosition == selectedPosition
)
return
}
currentPosition++
}
}
}
override fun getItemCount(): Int {
return courierCostsList.sumOf { it.services.size }
}
}

View File

@ -0,0 +1,74 @@
package com.alya.ecommerce_serang.ui.order
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.alya.ecommerce_serang.data.api.dto.CostProduct
import com.alya.ecommerce_serang.data.api.dto.CourierCostRequest
import com.alya.ecommerce_serang.data.api.response.order.CourierCostsItem
import com.alya.ecommerce_serang.data.repository.OrderRepository
import com.alya.ecommerce_serang.data.repository.Result
import kotlinx.coroutines.launch
class ShippingViewModel(
private val repository: OrderRepository
) : ViewModel() {
// Shipping options LiveData
private val _shippingOptions = MutableLiveData<List<CourierCostsItem>>()
val shippingOptions: LiveData<List<CourierCostsItem>> = _shippingOptions
// Loading state LiveData
private val _isLoading = MutableLiveData<Boolean>()
val isLoading: LiveData<Boolean> = _isLoading
// Error message LiveData
private val _errorMessage = MutableLiveData<String>()
val errorMessage: LiveData<String> = _errorMessage
/**
* Load shipping options based on address, product, and quantity
*/
fun loadShippingOptions(addressId: Int, productId: Int, quantity: Int) {
// Reset previous state
_isLoading.value = true
_errorMessage.value = ""
// Prepare the request
val request = CourierCostRequest(
addressId = addressId,
itemCost = CostProduct(
productId = productId,
quantity = quantity
)
)
viewModelScope.launch {
try {
// Fetch courier costs
val result = repository.getCountCourierCost(request)
when (result) {
is Result.Success -> {
// Update shipping options directly with courier costs
_shippingOptions.value = result.data.courierCosts
}
is Result.Error -> {
// Handle error case
_errorMessage.value = result.exception.message ?: "Unknown error occurred"
}
is Result.Loading -> {
// Typically handled by the loading state
}
}
} catch (e: Exception) {
// Catch any unexpected exceptions
_errorMessage.value = e.localizedMessage ?: "An unexpected error occurred"
} finally {
// Always set loading to false
_isLoading.value = false
}
}
}
}

View File

@ -0,0 +1,45 @@
package com.alya.ecommerce_serang.ui.order
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.response.cart.CartItemsItem
import com.alya.ecommerce_serang.databinding.ItemOrderProductBinding
import com.bumptech.glide.Glide
import java.text.NumberFormat
import java.util.Locale
class SingleCartItemAdapter(private val cartItem: CartItemsItem) :
RecyclerView.Adapter<SingleCartItemAdapter.CartItemViewHolder>() {
class CartItemViewHolder(val binding: ItemOrderProductBinding) : RecyclerView.ViewHolder(binding.root)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CartItemViewHolder {
val binding = ItemOrderProductBinding.inflate(
LayoutInflater.from(parent.context), parent, false
)
return CartItemViewHolder(binding)
}
override fun getItemCount(): Int = 1
override fun onBindViewHolder(holder: CartItemViewHolder, position: Int) {
with(holder.binding) {
// Set cart item details
tvProductName.text = cartItem.productName
tvProductQuantity.text = "${cartItem.quantity} buah"
tvProductPrice.text = formatCurrency(cartItem.price.toDouble())
// Load placeholder image
Glide.with(ivProduct.context)
.load(R.drawable.placeholder_image)
.into(ivProduct)
}
}
private fun formatCurrency(amount: Double): String {
val formatter = NumberFormat.getCurrencyInstance(Locale("in", "ID"))
return formatter.format(amount).replace(",00", "")
}
}

View File

@ -0,0 +1,54 @@
package com.alya.ecommerce_serang.ui.order
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.CheckoutData
import com.alya.ecommerce_serang.data.api.dto.OrderRequestBuy
import com.alya.ecommerce_serang.databinding.ItemOrderProductBinding
import com.bumptech.glide.Glide
import com.bumptech.glide.request.RequestOptions
import java.text.NumberFormat
import java.util.Locale
class SingleProductAdapter(private val checkoutData: CheckoutData) :
RecyclerView.Adapter<SingleProductAdapter.ProductViewHolder>() {
class ProductViewHolder(val binding: ItemOrderProductBinding) : RecyclerView.ViewHolder(binding.root)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ProductViewHolder {
val binding = ItemOrderProductBinding.inflate(
LayoutInflater.from(parent.context), parent, false
)
return ProductViewHolder(binding)
}
override fun getItemCount(): Int = 1
override fun onBindViewHolder(holder: ProductViewHolder, position: Int) {
with(holder.binding) {
// Set product details
tvProductName.text = checkoutData.productName
val quantity = (checkoutData.orderRequest as OrderRequestBuy).quantity
tvProductQuantity.text = "$quantity buah"
tvProductPrice.text = formatCurrency(checkoutData.productPrice)
// Load product image
Glide.with(ivProduct.context)
.load(checkoutData.productImageUrl)
.apply(
RequestOptions()
.placeholder(R.drawable.placeholder_image)
.error(R.drawable.placeholder_image))
.into(ivProduct)
}
}
private fun formatCurrency(amount: Double): String {
val formatter = NumberFormat.getCurrencyInstance(Locale("in", "ID"))
return formatter.format(amount).replace(",00", "")
}
}

View File

@ -0,0 +1,279 @@
package com.alya.ecommerce_serang.ui.order.address
import android.annotation.SuppressLint
import android.content.pm.PackageManager
import android.location.Location
import android.location.LocationListener
import android.location.LocationManager
import android.os.Bundle
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import com.alya.ecommerce_serang.data.api.dto.CreateAddressRequest
import com.alya.ecommerce_serang.data.api.dto.UserProfile
import com.alya.ecommerce_serang.data.api.response.order.CitiesItem
import com.alya.ecommerce_serang.data.api.response.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.databinding.ActivityAddAddressBinding
import com.alya.ecommerce_serang.utils.SavedStateViewModelFactory
import com.alya.ecommerce_serang.utils.SessionManager
import kotlinx.coroutines.launch
class AddAddressActivity : AppCompatActivity() {
private lateinit var binding: ActivityAddAddressBinding
private lateinit var apiService: ApiService
private lateinit var sessionManager: SessionManager
private lateinit var profileUser: UserProfile
private lateinit var locationManager: LocationManager
private var latitude: Double? = null
private var longitude: Double? = null
private val provinceAdapter by lazy { ProvinceAdapter(this) }
private val cityAdapter by lazy { CityAdapter(this) }
private val viewModel: AddAddressViewModel by viewModels {
SavedStateViewModelFactory(this) { savedStateHandle ->
val apiService = ApiConfig.getApiService(sessionManager)
val orderRepository = OrderRepository(apiService)
AddAddressViewModel(orderRepository, savedStateHandle)
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityAddAddressBinding.inflate(layoutInflater)
setContentView(binding.root)
sessionManager = SessionManager(this)
apiService = ApiConfig.getApiService(sessionManager)
locationManager = getSystemService(LOCATION_SERVICE) as LocationManager
setupToolbar()
setupAutoComplete()
setupButtonListeners()
collectFlows()
requestLocationPermission()
}
// private fun viewModelAddAddress(request: CreateAddressRequest) {
// // Call the private fun in your ViewModel using reflection or expose it in ViewModel
// val method = AddAddressViewModel::class.java.getDeclaredMethod("addAddress", CreateAddressRequest::class.java)
// method.isAccessible = true
// method.invoke(viewModel, request)
// }
// UI setup methods
private fun setupToolbar() {
binding.toolbar.setNavigationOnClickListener {
onBackPressedDispatcher.onBackPressed()
}
}
private fun setupAutoComplete() {
// Set adapters
binding.autoCompleteProvinsi.setAdapter(provinceAdapter)
binding.autoCompleteKabupaten.setAdapter(cityAdapter)
// Set listeners
binding.autoCompleteProvinsi.setOnItemClickListener { _, _, position, _ ->
provinceAdapter.getProvinceId(position)?.let { provinceId ->
viewModel.getCities(provinceId)
binding.autoCompleteKabupaten.text.clear()
}
}
binding.autoCompleteKabupaten.setOnItemClickListener { _, _, position, _ ->
cityAdapter.getCityId(position)?.let { cityId ->
viewModel.selectedCityId = cityId
}
}
}
private fun setupButtonListeners() {
binding.buttonSimpan.setOnClickListener {
validateAndSubmitForm()
}
}
private fun collectFlows() {
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
launch {
viewModel.provincesState.collect { state ->
handleProvinceState(state)
}
}
launch {
viewModel.citiesState.collect { state ->
handleCityState(state)
}
}
launch {
viewModel.addressSubmissionState.collect { state ->
handleAddressSubmissionState(state)
}
}
}
}
}
private fun handleProvinceState(state: ViewState<List<ProvincesItem>>) {
when (state) {
is ViewState.Loading -> null //showProvinceLoading(true)
is ViewState.Success -> {
provinceAdapter.updateData(state.data)
}
is ViewState.Error -> {
showError(state.message)
}
}
}
private fun handleCityState(state: ViewState<List<CitiesItem>>) {
when (state) {
is ViewState.Loading -> null //showCityLoading(true)
is ViewState.Success -> {
// showCityLoading(false)
cityAdapter.updateData(state.data)
}
is ViewState.Error -> {
// showCityLoading(false)
showError(state.message)
}
}
}
private fun handleAddressSubmissionState(state: ViewState<String>) {
when (state) {
is ViewState.Loading -> showSubmitLoading(true)
is ViewState.Success -> {
showSubmitLoading(false)
showSuccessAndFinish(state.data)
}
is ViewState.Error -> {
showSubmitLoading(false)
showError(state.message)
}
}
}
private fun showSubmitLoading(isLoading: Boolean) {
binding.buttonSimpan.isEnabled = !isLoading
binding.buttonSimpan.text = if (isLoading) "Menyimpan..." else "Simpan"
// You might want to show a progress bar as well
}
private fun showError(message: String) {
Toast.makeText(this, message, Toast.LENGTH_SHORT).show()
}
private fun showSuccessAndFinish(message: String) {
Toast.makeText(this, "Sukses: $message", Toast.LENGTH_SHORT).show()
onBackPressed()
}
private fun validateAndSubmitForm() {
val lat = latitude
val long = longitude
if (lat == null || long == null) {
showError("Lokasi belum terdeteksi")
return
}
val street = binding.etDetailAlamat.text.toString()
val subDistrict = binding.etKecamatan.text.toString()
val postalCode = binding.etKodePos.text.toString()
val recipient = binding.etNamaPenerima.text.toString()
val phone = binding.etNomorHp.text.toString()
val userId = profileUser.userId
val isStoreLocation = false
val provinceId = viewModel.selectedProvinceId
val cityId = viewModel.selectedCityId
if (street.isBlank() || recipient.isBlank() || phone.isBlank()) {
showError("Lengkapi semua field wajib")
return
}
if (provinceId == null) {
showError("Pilih provinsi terlebih dahulu")
return
}
if (cityId == null) {
showError("Pilih kota/kabupaten terlebih dahulu")
return
}
val request = CreateAddressRequest(
lat = lat,
long = long,
street = street,
subDistrict = subDistrict,
cityId = cityId,
provId = provinceId,
postCode = postalCode,
detailAddress = street,
userId = userId,
recipient = recipient,
phone = phone,
isStoreLocation = isStoreLocation
)
viewModel.addAddress(request)
}
private val locationPermissionLauncher =
registerForActivityResult(ActivityResultContracts.RequestPermission()) { granted ->
if (granted) requestLocation() else Toast.makeText(this, "Izin lokasi ditolak",Toast.LENGTH_SHORT).show()
}
private fun requestLocationPermission() {
locationPermissionLauncher.launch(android.Manifest.permission.ACCESS_FINE_LOCATION)
}
@SuppressLint("MissingPermission")
private fun requestLocation() {
val isGpsEnabled = locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER)
val isNetworkEnabled = locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER)
if (!isGpsEnabled && !isNetworkEnabled) {
Toast.makeText(this, "Provider lokasi tidak tersedia", Toast.LENGTH_SHORT).show()
return
}
val provider = if (isGpsEnabled) LocationManager.GPS_PROVIDER else LocationManager.NETWORK_PROVIDER
locationManager.requestSingleUpdate(provider, object : LocationListener {
override fun onLocationChanged(location: Location) {
latitude = location.latitude
longitude = location.longitude
}
override fun onStatusChanged(provider: String?, status: Int, extras: Bundle?) {}
override fun onProviderEnabled(provider: String) {}
override fun onProviderDisabled(provider: String) {
Toast.makeText(this@AddAddressActivity, "Provider dimatikan", Toast.LENGTH_SHORT).show()
}
}, null)
}
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if (requestCode == 100 && grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
requestLocation()
} else {
Toast.makeText(this, "Location permission denied", Toast.LENGTH_SHORT).show()
}
}
}

View File

@ -0,0 +1,103 @@
package com.alya.ecommerce_serang.ui.order.address
import android.util.Log
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.alya.ecommerce_serang.data.api.dto.CreateAddressRequest
import com.alya.ecommerce_serang.data.api.response.order.CitiesItem
import com.alya.ecommerce_serang.data.api.response.order.ProvincesItem
import com.alya.ecommerce_serang.data.repository.OrderRepository
import com.alya.ecommerce_serang.data.repository.Result
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
class AddAddressViewModel(private val repository: OrderRepository, private val savedStateHandle: SavedStateHandle): ViewModel() {
// Flow states for data
private val _addressSubmissionState = MutableStateFlow<ViewState<String>>(ViewState.Loading)
val addressSubmissionState = _addressSubmissionState.asStateFlow()
private val _provincesState = MutableStateFlow<ViewState<List<ProvincesItem>>>(ViewState.Loading)
val provincesState = _provincesState.asStateFlow()
private val _citiesState = MutableStateFlow<ViewState<List<CitiesItem>>>(ViewState.Loading)
val citiesState = _citiesState.asStateFlow()
// Stored in SavedStateHandle for configuration changes
var selectedProvinceId: Int?
get() = savedStateHandle.get<Int>("selectedProvinceId")
set(value) { savedStateHandle["selectedProvinceId"] = value }
var selectedCityId: Int?
get() = savedStateHandle.get<Int>("selectedCityId")
set(value) { savedStateHandle["selectedCityId"] = value }
init {
// Load provinces on initialization
getProvinces()
}
fun addAddress(request: CreateAddressRequest){
viewModelScope.launch {
when (val result = repository.addAddress(request)) {
is Result.Success -> {
val message = result.data.message // Ambil `message` dari CreateAddressResponse
_addressSubmissionState.value = ViewState.Success(message)
}
is Result.Error -> {
_addressSubmissionState.value =
ViewState.Error(result.exception.message ?: "Unknown error")
}
is Result.Loading -> {
// Optional, karena sudah set Loading di awal
}
}
}
}
fun getProvinces(){
viewModelScope.launch {
try {
val result = repository.getListProvinces()
result?.let {
_provincesState.value = ViewState.Success(it.provinces)
}
} catch (e: Exception) {
Log.e("AddAddressViewModel", "Error fetching provinces: ${e.message}")
}
}
}
fun getCities(provinceId: Int){
viewModelScope.launch {
try {
selectedProvinceId = provinceId
val result = repository.getListCities(provinceId)
result?.let {
_citiesState.value = ViewState.Success(it.cities)
}
} catch (e: Exception) {
Log.e("AddAddressViewModel", "Error fetching cities: ${e.message}")
}
}
}
fun setSelectedProvinceId(id: Int) {
selectedProvinceId = id
}
fun setSelectedCityId(id: Int) {
selectedCityId = id
}
companion object {
private const val TAG = "AddAddressViewModel"
}
}
sealed class ViewState<out T> {
object Loading : ViewState<Nothing>()
data class Success<T>(val data: T) : ViewState<T>()
data class Error(val message: String) : ViewState<Nothing>()
}

View File

@ -0,0 +1,91 @@
package com.alya.ecommerce_serang.ui.order.address
import android.content.Intent
import android.os.Bundle
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.LinearLayoutManager
import com.alya.ecommerce_serang.data.api.retrofit.ApiConfig
import com.alya.ecommerce_serang.data.api.retrofit.ApiService
import com.alya.ecommerce_serang.data.repository.OrderRepository
import com.alya.ecommerce_serang.databinding.ActivityAddressBinding
import com.alya.ecommerce_serang.utils.BaseViewModelFactory
import com.alya.ecommerce_serang.utils.SessionManager
class AddressActivity : AppCompatActivity() {
private lateinit var binding: ActivityAddressBinding
private lateinit var apiService: ApiService
private lateinit var sessionManager: SessionManager
private lateinit var adapter: AddressAdapter
private val viewModel: AddressViewModel by viewModels {
BaseViewModelFactory {
val apiService = ApiConfig.getApiService(sessionManager)
val orderRepository = OrderRepository(apiService)
AddressViewModel(orderRepository)
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityAddressBinding.inflate(layoutInflater)
setContentView(binding.root)
sessionManager = SessionManager(this)
apiService = ApiConfig.getApiService(sessionManager)
setupToolbar()
adapter = AddressAdapter { selectedId ->
viewModel.selectAddress(selectedId)
}
binding.toolbar.setNavigationOnClickListener {
onBackPressedWithResult()
}
binding.rvSellerOrder.layoutManager = LinearLayoutManager(this)
binding.rvSellerOrder.adapter = adapter
viewModel.fetchAddresses()
viewModel.addresses.observe(this) { addressList ->
adapter.submitList(addressList)
}
viewModel.selectedAddressId.observe(this) { selectedId ->
adapter.setSelectedAddressId(selectedId)
}
}
private fun setupToolbar() {
binding.toolbar.setNavigationOnClickListener {
finish()
}
}
// private fun updateEmptyState(isEmpty: Boolean) {
// binding.layoutEmptyAddresses.isVisible = isEmpty
// binding.rvAddresses.isVisible = !isEmpty
// }
private fun onBackPressedWithResult() {
viewModel.selectedAddressId.value?.let {
val intent = Intent()
intent.putExtra(EXTRA_ADDRESS_ID, it)
setResult(RESULT_OK, intent)
}
finish()
}
companion object {
const val EXTRA_ADDRESS_ID = "extra_address_id"
}
}
//override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
// super.onActivityResult(requestCode, resultCode, data)
// if (requestCode == REQUEST_ADDRESS && resultCode == RESULT_OK) {
// val selectedAddressId = data?.getIntExtra("selected_address_id", -1)
// // Use the selected address ID
// }
//}

View File

@ -0,0 +1,67 @@
package com.alya.ecommerce_serang.ui.order.address
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import com.alya.ecommerce_serang.R
import com.alya.ecommerce_serang.data.api.response.profile.AddressesItem
import com.google.android.material.card.MaterialCardView
class AddressAdapter(
private val onAddressClick: (Int) -> Unit
) : ListAdapter<AddressesItem, AddressAdapter.AddressViewHolder>(DIFF_CALLBACK) {
private var selectedAddressId: Int? = null
fun setSelectedAddressId(id: Int?) {
selectedAddressId = id
notifyDataSetChanged()
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AddressViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.item_card_address, parent, false)
return AddressViewHolder(view)
}
override fun onBindViewHolder(holder: AddressViewHolder, position: Int) {
val address = getItem(position)
holder.bind(address, selectedAddressId == address.id)
holder.itemView.setOnClickListener {
onAddressClick(address.id)
}
}
class AddressViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
private val tvName: TextView = itemView.findViewById(R.id.tv_name_address)
private val tvDetail: TextView = itemView.findViewById(R.id.tv_detail_address)
private val card: MaterialCardView = itemView as MaterialCardView
fun bind(address: AddressesItem, isSelected: Boolean) {
tvName.text = address.recipient
tvDetail.text = "${address.street}, ${address.subdistrict}, ${address.phone}"
card.setCardBackgroundColor(
ContextCompat.getColor(
itemView.context,
if (isSelected) R.color.blue_50 else R.color.white
)
)
}
}
companion object {
val DIFF_CALLBACK = object : DiffUtil.ItemCallback<AddressesItem>() {
override fun areItemsTheSame(oldItem: AddressesItem, newItem: AddressesItem) =
oldItem.id == newItem.id
override fun areContentsTheSame(oldItem: AddressesItem, newItem: AddressesItem) =
oldItem == newItem
}
}
}

View File

@ -0,0 +1,31 @@
package com.alya.ecommerce_serang.ui.order.address
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.alya.ecommerce_serang.data.api.response.profile.AddressesItem
import com.alya.ecommerce_serang.data.repository.OrderRepository
import kotlinx.coroutines.launch
class AddressViewModel(private val repository: OrderRepository): ViewModel() {
private val _addresses = MutableLiveData<List<AddressesItem>>()
val addresses: LiveData<List<AddressesItem>> get() = _addresses
private val _selectedAddressId = MutableLiveData<Int?>()
val selectedAddressId: LiveData<Int?> get() = _selectedAddressId
fun fetchAddresses() {
viewModelScope.launch {
val response = repository.getAddress()
response?.let {
_addresses.value = it.addresses
}
}
}
fun selectAddress(id: Int) {
_selectedAddressId.value = id
}
}

View File

@ -0,0 +1,21 @@
package com.alya.ecommerce_serang.ui.order.address
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 EditAddressActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContentView(R.layout.activity_edit_address)
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
insets
}
}
}

View File

@ -0,0 +1,49 @@
package com.alya.ecommerce_serang.ui.order.address
import android.content.Context
import android.widget.ArrayAdapter
import com.alya.ecommerce_serang.data.api.response.order.CitiesItem
import com.alya.ecommerce_serang.data.api.response.order.ProvincesItem
// UI adapters and helpers
class ProvinceAdapter(
context: Context,
resource: Int = android.R.layout.simple_dropdown_item_1line
) : ArrayAdapter<String>(context, resource, ArrayList()) {
private val provinces = ArrayList<ProvincesItem>()
fun updateData(newProvinces: List<ProvincesItem>) {
provinces.clear()
provinces.addAll(newProvinces)
clear()
addAll(provinces.map { it.province })
notifyDataSetChanged()
}
fun getProvinceId(position: Int): Int? {
return provinces.getOrNull(position)?.provinceId?.toIntOrNull()
}
}
class CityAdapter(
context: Context,
resource: Int = android.R.layout.simple_dropdown_item_1line
) : ArrayAdapter<String>(context, resource, ArrayList()) {
private val cities = ArrayList<CitiesItem>()
fun updateData(newCities: List<CitiesItem>) {
cities.clear()
cities.addAll(newCities)
clear()
addAll(cities.map { it.cityName })
notifyDataSetChanged()
}
fun getCityId(position: Int): Int? {
return cities.getOrNull(position)?.cityId?.toIntOrNull()
}
}

View File

@ -1,25 +1,37 @@
package com.alya.ecommerce_serang.ui.product
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.util.Log
import android.view.View
import android.widget.Button
import android.widget.ImageButton
import android.widget.TextView
import android.widget.Toast
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.LinearLayoutManager
import com.alya.ecommerce_serang.BuildConfig.BASE_URL
import com.alya.ecommerce_serang.R
import com.alya.ecommerce_serang.data.api.dto.CartItem
import com.alya.ecommerce_serang.data.api.dto.ProductsItem
import com.alya.ecommerce_serang.data.api.response.Product
import com.alya.ecommerce_serang.data.api.response.ReviewsItem
import com.alya.ecommerce_serang.data.api.response.product.Product
import com.alya.ecommerce_serang.data.api.response.product.ReviewsItem
import com.alya.ecommerce_serang.data.api.response.product.StoreProduct
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.ProductRepository
import com.alya.ecommerce_serang.data.repository.Result
import com.alya.ecommerce_serang.databinding.ActivityDetailProductBinding
import com.alya.ecommerce_serang.ui.home.HorizontalProductAdapter
import com.alya.ecommerce_serang.ui.order.CheckoutActivity
import com.alya.ecommerce_serang.utils.BaseViewModelFactory
import com.alya.ecommerce_serang.utils.SessionManager
import com.alya.ecommerce_serang.utils.viewmodel.ProductViewModel
import com.bumptech.glide.Glide
import com.google.android.material.bottomsheet.BottomSheetDialog
import java.text.NumberFormat
import java.util.Locale
class DetailProductActivity : AppCompatActivity() {
private lateinit var binding: ActivityDetailProductBinding
@ -27,12 +39,14 @@ class DetailProductActivity : AppCompatActivity() {
private lateinit var sessionManager: SessionManager
private var productAdapter: HorizontalProductAdapter? = null
private var reviewsAdapter: ReviewsAdapter? = null
private var currentQuantity = 1
private val viewModel: ProductViewModel by viewModels {
private val viewModel: ProductUserViewModel by viewModels {
BaseViewModelFactory {
val apiService = ApiConfig.getApiService(sessionManager)
val productRepository = ProductRepository(apiService)
ProductViewModel(productRepository)
ProductUserViewModel(productRepository)
}
}
override fun onCreate(savedInstanceState: Bundle?) {
@ -43,56 +57,148 @@ class DetailProductActivity : AppCompatActivity() {
sessionManager = SessionManager(this)
apiService = ApiConfig.getApiService(sessionManager)
setupUI()
setupObservers()
loadData()
}
private fun loadData() {
val productId = intent.getIntExtra("PRODUCT_ID", -1)
//nanti tambah get store id dari HomeFragment Product.storeId
if (productId == -1) {
Log.e("DetailProductActivity", "Invalid Product ID")
Toast.makeText(this, "Invalid product ID", Toast.LENGTH_SHORT).show()
finish() // Close activity if no valid ID
return
}
viewModel.loadProductDetail(productId)
viewModel.loadReviews(productId)
}
private fun setupObservers() {
viewModel.productDetail.observe(this) { product ->
if (product != null) {
Log.d("ProductDetail", "Name: ${product.productName}, Price: ${product.price}")
// Update UI here, e.g., show in a TextView or ImageView
viewModel.loadProductDetail(productId)
} else {
Log.e("ProductDetail", "Failed to fetch product details")
product?.let {
updateUI(it)
viewModel.loadOtherProducts(it.storeId)
}
}
observeProductDetail()
observeProductReviews()
}
private fun observeProductDetail() {
viewModel.productDetail.observe(this) { product ->
product?.let { updateUI(it) }
}
}
private fun observeProductReviews() {
viewModel.storeDetail.observe(this) { result ->
when (result) {
is Result.Success -> {
updateStoreInfo(result.data)
}
is Result.Error -> {
// Show error message, maybe a Toast or Snackbar
Toast.makeText(this, "Failed to load store: ${result.exception.message}", Toast.LENGTH_SHORT).show()
}
is Result.Loading -> {
// Show loading indicator if needed
}
}
}
viewModel.otherProducts.observe(this) { products ->
updateOtherProducts(products)
}
viewModel.reviewProduct.observe(this) { reviews ->
setupRecyclerViewReviewsProduct(reviews)
}
viewModel.isLoading.observe(this) { isLoading ->
binding.progressBarDetailProd.visibility = if (isLoading) View.VISIBLE else View.GONE
}
viewModel.error.observe(this) { errorMessage ->
if (errorMessage.isNotEmpty()) {
Toast.makeText(this, errorMessage, Toast.LENGTH_LONG).show()
}
}
viewModel.addCart.observe(this) { result ->
when (result) {
is Result.Success -> {
val cartId = result.data.data.cartId
Toast.makeText(this, result.data.message, Toast.LENGTH_SHORT).show()
}
is Result.Error -> {
Toast.makeText(this, "Failed to add to cart: ${result.exception.message}", Toast.LENGTH_SHORT).show()
}
is Result.Loading -> {
// Show loading indicator if needed
}
}
}
}
private fun updateStoreInfo(store: StoreProduct?) {
store?.let {
binding.tvSellerName.text = it.storeName
binding.tvSellerRating.text = it.storeRating
binding.tvSellerLocation.text = it.storeLocation
// Load store image using Glide
val fullImageUrl = when (val img = it.storeImage) {
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.ivSellerImage)
}
}
private fun updateOtherProducts(products: List<ProductsItem>) {
if (products.isEmpty()) {
binding.recyclerViewOtherProducts.visibility = View.GONE
binding.tvViewAllProducts.visibility = View.GONE
} else {
binding.recyclerViewOtherProducts.visibility = View.VISIBLE
binding.tvViewAllProducts.visibility = View.VISIBLE
productAdapter?.updateProducts(products)
} }
private fun setupUI() {
// binding.btnBack.setOnClickListener {
// finish()
// }
binding.tvViewAllReviews.setOnClickListener {
viewModel.productDetail.value?.productId?.let { productId ->
handleAllReviewsClick(productId)
}
}
binding.btnBuyNow.setOnClickListener {
viewModel.productDetail.value?.productId?.let { id ->
showBuyNowPopup(id)
}
}
binding.btnAddToCart.setOnClickListener {
viewModel.productDetail.value?.productId?.let { id ->
showAddToCartPopup(id)
}
}
setupRecyclerViewOtherProducts()
}
private fun updateUI(product: Product){
binding.tvProductName.text = product.productName
binding.tvPrice.text = product.price
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.tvSellerName.text = product.storeId.toString()
binding.tvViewAllReviews.setOnClickListener{
handleAllReviewsClick(product.productId)
}
val fullImageUrl = when (val img = product.image) {
is String -> {
@ -106,8 +212,6 @@ class DetailProductActivity : AppCompatActivity() {
.load(fullImageUrl)
.placeholder(R.drawable.placeholder_image)
.into(binding.ivProductImage)
setupRecyclerViewOtherProducts()
}
private fun handleAllReviewsClick(productId: Int) {
@ -119,7 +223,7 @@ class DetailProductActivity : AppCompatActivity() {
private fun setupRecyclerViewOtherProducts(){
productAdapter = HorizontalProductAdapter(
products = emptyList(),
onClick = { productsItem -> handleProductClick(productsItem) }
onClick = { productsItem -> handleProductClick(productsItem) }
)
binding.recyclerViewOtherProducts.apply {
@ -134,7 +238,15 @@ class DetailProductActivity : AppCompatActivity() {
private fun setupRecyclerViewReviewsProduct(reviewList: List<ReviewsItem>){
val limitedReviewList = if (reviewList.isNotEmpty()) listOf(reviewList.first()) else emptyList()
if (reviewList.isEmpty()) {
binding.recyclerViewReviews.visibility = View.GONE
binding.tvViewAllReviews.visibility = View.GONE
// binding.tvNoReviews.visibility = View.VISIBLE
} else {
binding.recyclerViewReviews.visibility = View.VISIBLE
binding.tvViewAllReviews.visibility = View.VISIBLE
}
// binding.tvNoReviews.visibility = View.GONE
reviewsAdapter = ReviewsAdapter(
reviewList = limitedReviewList
)
@ -154,4 +266,109 @@ class DetailProductActivity : AppCompatActivity() {
intent.putExtra("PRODUCT_ID", product.id) // Pass product ID
startActivity(intent)
}
private fun showBuyNowPopup(productId: Int) {
showQuantityDialog(productId, true)
}
private fun showAddToCartPopup(productId: Int) {
showQuantityDialog(productId, false)
}
private fun showQuantityDialog(productId: Int, isBuyNow: Boolean) {
val bottomSheetDialog = BottomSheetDialog(this)
val view = layoutInflater.inflate(R.layout.dialog_count_buy, null)
bottomSheetDialog.setContentView(view)
val btnDecrease = view.findViewById<Button>(R.id.btnDecrease)
val btnIncrease = view.findViewById<Button>(R.id.btnIncrease)
val tvQuantity = view.findViewById<TextView>(R.id.tvQuantity)
val btnBuyNow = view.findViewById<Button>(R.id.btnBuyNow)
val btnClose = view.findViewById<ImageButton>(R.id.btnCloseDialog)
// Set button text based on action
if (!isBuyNow) {
btnBuyNow.setText(R.string.add_to_cart)
}
currentQuantity = 1
tvQuantity.text = currentQuantity.toString()
val maxStock = viewModel.productDetail.value?.stock ?: 1
btnDecrease.setOnClickListener {
if (currentQuantity > 1) {
currentQuantity--
tvQuantity.text = currentQuantity.toString()
}
}
btnIncrease.setOnClickListener {
if (currentQuantity < maxStock) {
currentQuantity++
tvQuantity.text = currentQuantity.toString()
} else {
Toast.makeText(this, "Maximum stock reached", Toast.LENGTH_SHORT).show()
}
}
btnBuyNow.setOnClickListener {
bottomSheetDialog.dismiss()
if (isBuyNow) {
// If it's Buy Now, navigate directly to checkout without adding to cart
navigateToCheckout()
} else {
// If it's Add to Cart, add the item to the cart
val cartItem = CartItem(
productId = productId,
quantity = currentQuantity
)
viewModel.reqCart(cartItem)
}
}
btnClose.setOnClickListener {
bottomSheetDialog.dismiss()
}
bottomSheetDialog.show()
}
private fun formatCurrency(amount: Double): String {
val formatter = NumberFormat.getCurrencyInstance(Locale("in", "ID"))
return formatter.format(amount).replace(",00", "")
}
private fun navigateToCheckout() {
val productDetail = viewModel.productDetail.value ?: return
val storeDetail = viewModel.storeDetail.value
if (storeDetail !is Result.Success || storeDetail.data == null) {
Toast.makeText(this, "Store information not available", Toast.LENGTH_SHORT).show()
return
}
// Start checkout activity with buy now flow
CheckoutActivity.startForBuyNow(
context = this,
storeId = productDetail.storeId,
storeName = storeDetail.data.storeName,
productId = productDetail.productId,
productName = productDetail.productName,
productImage = productDetail.image,
quantity = currentQuantity,
price = productDetail.price.toDouble()
)
}
companion object {
const val EXTRA_PRODUCT_ID = "extra_product_id"
fun start(context: Context, productId: Int) {
val intent = Intent(context, DetailProductActivity::class.java)
intent.putExtra(EXTRA_PRODUCT_ID, productId)
context.startActivity(intent)
}
}
}

View File

@ -0,0 +1,135 @@
package com.alya.ecommerce_serang.ui.product
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.CartItem
import com.alya.ecommerce_serang.data.api.dto.ProductsItem
import com.alya.ecommerce_serang.data.api.response.cart.AddCartResponse
import com.alya.ecommerce_serang.data.api.response.product.Product
import com.alya.ecommerce_serang.data.api.response.product.ReviewsItem
import com.alya.ecommerce_serang.data.api.response.product.StoreProduct
import com.alya.ecommerce_serang.data.repository.ProductRepository
import com.alya.ecommerce_serang.data.repository.Result
import kotlinx.coroutines.launch
class ProductUserViewModel(private val repository: ProductRepository) : ViewModel() {
private val _productDetail = MutableLiveData<Product?>()
val productDetail: LiveData<Product?> get() = _productDetail
private val _storeDetail = MutableLiveData<Result<StoreProduct?>>()
val storeDetail : LiveData<Result<StoreProduct?>> get() = _storeDetail
private val _reviewProduct = MutableLiveData<List<ReviewsItem>>()
val reviewProduct: LiveData<List<ReviewsItem>> get() = _reviewProduct
private val _otherProducts = MutableLiveData<List<ProductsItem>>()
val otherProducts: LiveData<List<ProductsItem>> get() = _otherProducts
private val _addCart = MutableLiveData<Result<AddCartResponse>>()
val addCart: LiveData<Result<AddCartResponse>> get() = _addCart
private val _isLoading = MutableLiveData<Boolean>()
val isLoading: LiveData<Boolean> get() = _isLoading
private val _error = MutableLiveData<String>()
val error: LiveData<String> get() = _error
fun loadProductDetail(productId: Int) {
_isLoading.value = true
viewModelScope.launch {
try {
val result = repository.fetchProductDetail(productId)
_productDetail.value = result?.product
//Load store details if product has a store ID
result?.product?.storeId?.let { storeId ->
loadStoreDetail(storeId)
}
} catch (e: Exception) {
Log.e("ProductViewModel", "Error loading product details: ${e.message}")
_error.value = "Failed to load product details: ${e.message}"
} finally {
_isLoading.value = false
}
}
}
fun loadStoreDetail(storeId: Int) {
viewModelScope.launch {
try {
_storeDetail.value = Result.Loading
val result = repository.fetchStoreDetail(storeId)
_storeDetail.value = result
} catch (e: Exception) {
Log.e("ProductViewModel", "Error loading store details: ${e.message}")
_storeDetail.value = Result.Error(e)
}
}
}
fun loadReviews(productId: Int) {
viewModelScope.launch {
try {
val reviews = repository.fetchProductReview(productId)
_reviewProduct.value = reviews ?: emptyList()
} catch (e: Exception) {
Log.e("ProductViewModel", "Error loading reviews: ${e.message}")
_reviewProduct.value = emptyList()
}
}
}
fun loadOtherProducts(storeId: Int) {
viewModelScope.launch {
try {
val result = repository.getAllProducts() // Fetch products
if (result is Result.Success) {
val allProducts = result.data // Extract the list
val filteredProducts = allProducts.filter {
it.storeId == storeId && it.id != _productDetail.value?.productId
} // Filter by storeId and exclude current product
_otherProducts.value = filteredProducts // Update LiveData
} else if (result is Result.Error) {
Log.e("ProductViewModel", "Error loading other products: ${result.exception.message}")
_otherProducts.value = emptyList() // Set empty list on failure
}
} catch (e: Exception) {
Log.e("ProductViewModel", "Exception loading other products: ${e.message}")
_otherProducts.value = emptyList()
}
}
}
fun reqCart(request: CartItem){
viewModelScope.launch {
_isLoading.value = true
when (val result = repository.addToCart(request)) {
is Result.Success -> {
_addCart.value = result
_isLoading.value = false
}
is Result.Error -> {
_addCart.value = result
_error.value = result.exception.message ?: "Unknown error"
_isLoading.value = false
}
is Result.Loading -> {
_isLoading.value = true
}
}
}
}
}
// fun loadStoreDetail(storeId: Int){
// viewModelScope.launch {
// val storeResult = repository.fetchStoreDetail(storeId)
// _storeDetail.value = storeResult
// }
// }

View File

@ -13,18 +13,17 @@ import com.alya.ecommerce_serang.data.repository.ProductRepository
import com.alya.ecommerce_serang.databinding.ActivityReviewProductBinding
import com.alya.ecommerce_serang.utils.BaseViewModelFactory
import com.alya.ecommerce_serang.utils.SessionManager
import com.alya.ecommerce_serang.utils.viewmodel.ProductViewModel
class ReviewProductActivity : AppCompatActivity() {
private lateinit var binding: ActivityReviewProductBinding
private lateinit var apiService: ApiService
private var reviewsAdapter: ReviewsAdapter? = null
private lateinit var sessionManager: SessionManager
private val viewModel: ProductViewModel by viewModels {
private val viewModel: ProductUserViewModel by viewModels {
BaseViewModelFactory {
val apiService = ApiConfig.getApiService(sessionManager)
val productRepository = ProductRepository(apiService)
ProductViewModel(productRepository)
ProductUserViewModel(productRepository)
}
}

View File

@ -6,7 +6,7 @@ import android.view.ViewGroup
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import com.alya.ecommerce_serang.R
import com.alya.ecommerce_serang.data.api.response.ReviewsItem
import com.alya.ecommerce_serang.data.api.response.product.ReviewsItem
import java.text.SimpleDateFormat
import java.util.Locale
import java.util.TimeZone

View File

@ -1,10 +1,12 @@
package com.alya.ecommerce_serang.ui.profile
import android.os.Bundle
import android.util.Log
import android.widget.Toast
import androidx.activity.enableEdgeToEdge
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import com.alya.ecommerce_serang.R
import com.alya.ecommerce_serang.data.api.dto.UserProfile
import com.alya.ecommerce_serang.data.api.retrofit.ApiConfig
import com.alya.ecommerce_serang.data.api.retrofit.ApiService
@ -62,25 +64,33 @@ class DetailProfileActivity : AppCompatActivity() {
binding.tvNameUser.setText(user.name.toString())
binding.tvUsername.setText(user.username)
binding.tvEmailUser.setText(user.email)
binding.tvDateBirth.setText(formatDate(user.birthDate))
Log.d("ProfileActivity", "Raw Birth Date: ${user.birthDate}")
binding.tvDateBirth.setText(user.birthDate?.let { formatDate(it) } ?: "N/A")
Log.d("ProfileActivity", "Formatted Birth Date: ${formatDate(user.birthDate)}")
binding.tvNumberPhoneUser.setText(user.phone)
if (user.image != null && user.image is String) {
Glide.with(this)
.load(user.image)
.placeholder(R.drawable.baseline_account_circle_24)
.into(binding.profileImage)
}
}
private fun formatDate(dateString: String): String {
private fun formatDate(dateString: String?): String {
if (dateString.isNullOrEmpty()) return "N/A" // Return default if null
return try {
val inputFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.getDefault()) //from json
inputFormat.timeZone = TimeZone.getTimeZone("UTC") //get timezone
val outputFormat = SimpleDateFormat("dd MMMM yyyy", Locale.getDefault()) // new format
val date = inputFormat.parse(dateString) // Parse from json format
outputFormat.format(date!!) // convert to new format
val inputFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.getDefault())
inputFormat.timeZone = TimeZone.getTimeZone("UTC") // Ensure parsing in UTC
val outputFormat = SimpleDateFormat("dd-MM-yy", Locale.getDefault()) // Convert to "dd-MM-yy" format
val date = inputFormat.parse(dateString)
outputFormat.format(date ?: return "Invalid Date") // Ensure valid date
} catch (e: Exception) {
dateString // Return original if error occurs
Log.e("ERROR", "Date parsing error: ${e.message}") // Log errors for debugging
"Invalid Date"
}
}
}

View File

@ -1,7 +1,10 @@
package com.alya.ecommerce_serang.utils
import androidx.lifecycle.AbstractSavedStateViewModelFactory
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.savedstate.SavedStateRegistryOwner
class BaseViewModelFactory<VM : ViewModel>(
private val creator: () -> VM
@ -10,4 +13,20 @@ class BaseViewModelFactory<VM : ViewModel>(
@Suppress("UNCHECKED_CAST")
return creator() as T
}
}
// Add a new factory for SavedStateHandle ViewModels
class SavedStateViewModelFactory<VM : ViewModel>(
private val owner: SavedStateRegistryOwner,
private val creator: (SavedStateHandle) -> VM
) : AbstractSavedStateViewModelFactory(owner, null) {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(
key: String,
modelClass: Class<T>,
handle: SavedStateHandle
): T {
return creator(handle) as T
}
}

View File

@ -4,7 +4,7 @@ import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.alya.ecommerce_serang.data.api.response.LoginResponse
import com.alya.ecommerce_serang.data.api.response.auth.LoginResponse
import com.alya.ecommerce_serang.data.repository.Result
import com.alya.ecommerce_serang.data.repository.UserRepository
import kotlinx.coroutines.launch

View File

@ -7,9 +7,9 @@ import androidx.lifecycle.liveData
import androidx.lifecycle.viewModelScope
import com.alya.ecommerce_serang.data.api.dto.CategoryItem
import com.alya.ecommerce_serang.data.api.dto.ProductsItem
import com.alya.ecommerce_serang.data.api.dto.Store
import com.alya.ecommerce_serang.data.api.response.Product
import com.alya.ecommerce_serang.data.api.response.ReviewsItem
import com.alya.ecommerce_serang.data.api.response.product.Product
import com.alya.ecommerce_serang.data.api.response.product.ReviewsItem
import com.alya.ecommerce_serang.data.api.response.product.StoreProduct
import com.alya.ecommerce_serang.data.repository.ProductRepository
import com.alya.ecommerce_serang.data.repository.Result
import kotlinx.coroutines.launch
@ -19,8 +19,8 @@ class ProductViewModel(private val repository: ProductRepository) : ViewModel()
private val _productDetail = MutableLiveData<Product?>()
val productDetail: LiveData<Product?> get() = _productDetail
private val _storeDetail = MutableLiveData<Store?>()
val storeDetail : LiveData<Store?> get() = _storeDetail
private val _storeDetail = MutableLiveData<StoreProduct?>()
val storeDetail : LiveData<StoreProduct?> get() = _storeDetail
private val _reviewProduct = MutableLiveData<List<ReviewsItem>>()
val reviewProduct: LiveData<List<ReviewsItem>> get() = _reviewProduct

View File

@ -6,7 +6,7 @@ import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.alya.ecommerce_serang.data.api.dto.RegisterRequest
import com.alya.ecommerce_serang.data.api.response.OtpResponse
import com.alya.ecommerce_serang.data.api.response.auth.OtpResponse
import com.alya.ecommerce_serang.data.repository.Result
import com.alya.ecommerce_serang.data.repository.UserRepository
import kotlinx.coroutines.launch

View File

@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="@color/blue_500" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<path android:fillColor="@color/blue_500" android:pathData="M3,17.25V21h3.75L17.81,9.94l-3.75,-3.75L3,17.25zM20.71,7.04c0.39,-0.39 0.39,-1.02 0,-1.41l-2.34,-2.34c-0.39,-0.39 -1.02,-0.39 -1.41,0l-1.83,1.83 3.75,3.75 1.83,-1.83z"/>
</vector>

View File

@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#211E1E" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M12,2L12,2C8.13,2 5,5.13 5,9c0,1.74 0.5,3.37 1.41,4.84c0.95,1.54 2.2,2.86 3.16,4.4c0.47,0.75 0.81,1.45 1.17,2.26C11,21.05 11.21,22 12,22h0c0.79,0 1,-0.95 1.25,-1.5c0.37,-0.81 0.7,-1.51 1.17,-2.26c0.96,-1.53 2.21,-2.85 3.16,-4.4C18.5,12.37 19,10.74 19,9C19,5.13 15.87,2 12,2zM12,11.75c-1.38,0 -2.5,-1.12 -2.5,-2.5s1.12,-2.5 2.5,-2.5s2.5,1.12 2.5,2.5S13.38,11.75 12,11.75z"/>
</vector>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@android:color/white" />
<corners android:radius="16dp"/>
<padding android:left="16dp" android:top="16dp"
android:right="16dp" android:bottom="16dp"/>
</shape>

View File

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

View File

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

View File

@ -0,0 +1,204 @@
<?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"
android:background="@android:color/white"
tools:context=".ui.order.address.AddAddressActivity">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:navigationIcon="@drawable/ic_back_24"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:title="Tambah Alamat" />
<View
android:id="@+id/divider"
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="#E0E0E0"
app:layout_constraintTop_toBottomOf="@id/toolbar" />
<ScrollView
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintBottom_toTopOf="@id/buttonSimpan"
app:layout_constraintTop_toBottomOf="@id/divider">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Nama Penerima"
android:textColor="@android:color/black"
android:textSize="14sp" />
<EditText
android:id="@+id/etNamaPenerima"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:background="@drawable/edit_text_background"
android:hint="Isi nama penerima"
android:inputType="textPersonName"
android:padding="12dp"
android:textSize="14sp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="Nomor Hp"
android:textColor="@android:color/black"
android:textSize="14sp" />
<EditText
android:id="@+id/etNomorHp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:background="@drawable/edit_text_background"
android:hint="Isi nomor handphone aktif"
android:inputType="phone"
android:padding="12dp"
android:textSize="14sp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="Detail Alamat"
android:textColor="@android:color/black"
android:textSize="14sp" />
<EditText
android:id="@+id/etDetailAlamat"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:background="@drawable/edit_text_background"
android:gravity="top"
android:hint="Isi detail alamat (nomor rumah, lantai, dll)"
android:inputType="textMultiLine"
android:lines="3"
android:padding="12dp"
android:textSize="14sp" />
<!-- Provinsi -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="Provinsi"
android:textColor="@android:color/black"
android:textSize="14sp" />
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:hint="Pilih Provinsi"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.ExposedDropdownMenu">
<AutoCompleteTextView
android:id="@+id/autoCompleteProvinsi"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="none"
android:padding="12dp"
android:textSize="14sp" />
</com.google.android.material.textfield.TextInputLayout>
<!-- Kabupaten / Kota -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="Kabupaten / Kota"
android:textColor="@android:color/black"
android:textSize="14sp" />
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.ExposedDropdownMenu">
<AutoCompleteTextView
android:id="@+id/autoCompleteKabupaten"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="none"
android:hint="Masukkan Kabupaten"
android:padding="12dp"
android:textSize="14sp" />
</com.google.android.material.textfield.TextInputLayout>
<!-- Kecamatan / Desa -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="Kecamatan / Desa"
android:textColor="@android:color/black"
android:textSize="14sp" />
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:hint="Isi Kecamatan / Desa"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/etKecamatan"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="12dp"
android:textSize="14sp"
android:inputType="textCapWords" />
</com.google.android.material.textfield.TextInputLayout>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="Kode Pos"
android:textColor="@android:color/black"
android:textSize="14sp" />
<EditText
android:id="@+id/etKodePos"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:background="@drawable/edit_text_background"
android:hint="Isi jawaban Anda di sini"
android:inputType="number"
android:padding="12dp"
android:textSize="14sp" />
</LinearLayout>
</ScrollView>
<Button
android:id="@+id/buttonSimpan"
android:layout_width="match_parent"
android:layout_height="56dp"
android:layout_margin="16dp"
android:background="@drawable/button_address_background"
android:text="Simpan"
android:textAllCaps="false"
android:textColor="@android:color/white"
android:textSize="16sp"
app:layout_constraintBottom_toBottomOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,61 @@
<?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.address.AddressActivity">
<LinearLayout
android:id="@+id/linear_toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="wrap_content"
android:layout_height="?attr/actionBarSize"
android:paddingEnd="24dp"
app:navigationIcon="@drawable/ic_back_24"
app:title="Alamat Pengiriman " />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAlignment="textEnd"
android:layout_gravity="center"
android:paddingEnd="16dp"
android:paddingVertical="16dp"
android:textColor="@color/blue_500"
android:fontFamily="@font/dmsans_semibold"
android:textSize="14sp"
android:clickable="true"
android:text="Tambah Alamat"
tools:ignore="RtlCompat" />
</LinearLayout>
<View
android:id="@+id/divider"
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="#E0E0E0"
app:layout_constraintTop_toBottomOf="@id/linear_toolbar" />
<ScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintTop_toBottomOf="@+id/divider"
app:layout_constraintStart_toStartOf="parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_seller_order"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:listitem="@layout/item_card_address"/>
</ScrollView>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,10 @@
<?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=".data.api.response.cart.CartActivity">
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,425 @@
<?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"
android:background="@color/black_800"
tools:context=".ui.order.CheckoutActivity">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="@color/white"
android:elevation="2dp"
app:navigationIcon="@drawable/ic_back_24"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:title="Pemesanan" />
<ScrollView
android:id="@+id/scrollView"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintTop_toBottomOf="@id/toolbar"
app:layout_constraintBottom_toTopOf="@id/bottom_payment_bar">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<!-- Delivery Address Section -->
<androidx.cardview.widget.CardView
android:id="@+id/card_delivery_address"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="0dp"
android:layout_marginTop="0dp"
app:cardElevation="0dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp"
android:background="@color/white">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<ImageView
android:id="@+id/iv_location_icon"
android:layout_width="24dp"
android:layout_height="24dp"
android:src="@drawable/baseline_location_pin_24"
android:layout_gravity="center_vertical"
app:tint="#3D84FF" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Alamat Pengiriman"
android:textSize="16sp"
android:fontFamily="@font/dmsans_medium"
android:layout_marginStart="8dp" />
</LinearLayout>
<TextView
android:id="@+id/tv_places_address"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Rumah"
android:textColor="#5A5A5A"
android:paddingHorizontal="8dp"
android:paddingVertical="2dp"
android:textSize="12sp"
android:layout_marginTop="8dp"
android:layout_marginStart="32dp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginTop="8dp">
<TextView
android:id="@+id/tv_address"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Jl. Pegangasan Timur"
android:textSize="14sp"
android:layout_marginStart="32dp" />
<TextView
android:id="@+id/tv_change_address"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Pilih Alamat"
android:textColor="#3D84FF"
android:textSize="14sp" />
</LinearLayout>
</LinearLayout>
</androidx.cardview.widget.CardView>
<View
android:layout_width="match_parent"
android:layout_height="8dp"
android:background="@color/black_50" />
<!-- Product Items Section -->
<androidx.cardview.widget.CardView
android:id="@+id/card_product"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:cardElevation="0dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:background="@color/white"
android:padding="16dp">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_product_items"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
tools:listitem="@layout/item_order_seller" />
</LinearLayout>
</androidx.cardview.widget.CardView>
<View
android:layout_width="match_parent"
android:layout_height="8dp"
android:background="#F5F5F5" />
<!-- Voucher Section -->
<LinearLayout
android:id="@+id/layout_voucher"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:background="@color/white"
android:padding="16dp"
android:gravity="center_vertical">
<!-- <ImageView-->
<!-- android:layout_width="24dp"-->
<!-- android:layout_height="24dp"-->
<!-- android:src="@drawable/ic_voucher_24" />-->
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Gunakan Voucher"
android:textSize="14sp"
android:layout_marginStart="8dp" />
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:src="@drawable/ic_arrow_right" />
</LinearLayout>
<View
android:layout_width="match_parent"
android:layout_height="8dp"
android:background="@color/black_50" />
<!-- Shipping Method Section -->
<LinearLayout
android:id="@+id/layout_shipping_method"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:background="@color/white"
android:padding="16dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Metode Pengiriman"
android:textSize="14sp" />
<TextView
android:id="@+id/tv_shipping_option"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Opsi Pengiriman"
android:textColor="#3D84FF"
android:textSize="14sp" />
</LinearLayout>
<androidx.cardview.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
app:cardCornerRadius="8dp"
app:cardElevation="0dp"
app:cardBackgroundColor="#F5F5F5">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="12dp">
<RadioButton
android:id="@+id/rb_jne"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:checked="true" />
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical"
android:layout_marginStart="8dp">
<TextView
android:id="@+id/tv_courier_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="JNE"
android:textSize="16sp"
android:fontFamily="@font/dmsans_medium" />
<TextView
android:id="@+id/tv_delivery_estimate"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="3 - 4 hari kerja"
android:textSize="14sp"
android:textColor="#757575" />
</LinearLayout>
<TextView
android:id="@+id/tv_shipping_price"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Rp15.000"
android:textSize="16sp"
android:fontFamily="@font/dmsans_medium"
android:layout_gravity="center_vertical" />
</LinearLayout>
</androidx.cardview.widget.CardView>
</LinearLayout>
<View
android:layout_width="match_parent"
android:layout_height="8dp"
android:background="@color/black_50" />
<!-- Payment Method Section -->
<LinearLayout
android:id="@+id/layout_payment_method"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:background="@color/white"
android:padding="16dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Metode Pembayaran"
android:textSize="14sp"
android:layout_marginBottom="8dp" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_payment_methods"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:listitem="@layout/item_payment_method"
tools:itemCount="2" />
</LinearLayout>
<View
android:layout_width="match_parent"
android:layout_height="8dp"
android:background="@color/black_50" />
<!-- Price Summary Section -->
<LinearLayout
android:id="@+id/layout_price_summary"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:background="@color/white"
android:padding="16dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="1 item"
android:textSize="14sp" />
<TextView
android:id="@+id/tv_item_total"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Rp65.000"
android:textSize="14sp" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginTop="8dp">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Biaya Pengiriman"
android:textSize="14sp" />
<TextView
android:id="@+id/tv_shipping_fee"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Rp15.000"
android:textSize="14sp" />
</LinearLayout>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="@color/black_50"
android:layout_marginVertical="12dp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Total"
android:textSize="16sp"
android:fontFamily="@font/dmsans_bold" />
<TextView
android:id="@+id/tv_total"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Rp75.000"
android:textColor="#3D84FF"
android:textSize="16sp"
android:fontFamily="@font/dmsans_bold" />
</LinearLayout>
</LinearLayout>
</LinearLayout>
</ScrollView>
<!-- Bottom Payment Bar -->
<LinearLayout
android:id="@+id/bottom_payment_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="16dp"
android:background="@color/white"
android:elevation="8dp"
app:layout_constraintBottom_toBottomOf="parent">
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Total:"
android:textSize="14sp" />
<TextView
android:id="@+id/tv_bottom_total"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Rp75.000"
android:textColor="#3D84FF"
android:textSize="18sp"
android:fontFamily="@font/dmsans_bold" />
</LinearLayout>
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_pay"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Bayar"
android:textAllCaps="false"
android:paddingHorizontal="32dp"
app:cornerRadius="8dp"
android:backgroundTint="#3D84FF" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -406,6 +406,13 @@
</LinearLayout>
</androidx.core.widget.NestedScrollView>
<ProgressBar
android:id="@+id/progress_bar_detail_prod"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
android:layout_gravity="center"/>
<!-- Bottom Action Bar -->
<com.google.android.material.bottomappbar.BottomAppBar
android:id="@+id/bottomAppBar"

View File

@ -0,0 +1,10 @@
<?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.address.EditAddressActivity">
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,32 @@
<?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"
android:background="@color/cardview_light_background"
tools:context=".ui.order.ShippingActivity">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:navigationIcon="@drawable/ic_back_24"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:title="Pengiriman" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintTop_toBottomOf="@+id/toolbar"
app:layout_constraintStart_toStartOf="parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_shipment_order"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:listitem="@layout/item_shipping_order"/>
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,71 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/bg_popup_count"
android:padding="16dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:gravity="center_horizontal"
android:paddingTop="32dp">
<TextView
android:id="@+id/tvQuantityTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Jumlah Produk"
android:textSize="18sp"
android:textStyle="bold"
android:paddingBottom="16dp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:orientation="horizontal">
<Button
android:id="@+id/btnDecrease"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="-" />
<TextView
android:id="@+id/tvQuantity"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginHorizontal="24dp"
android:text="1"
android:textSize="18sp" />
<Button
android:id="@+id/btnIncrease"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="+" />
</LinearLayout>
<Button
android:id="@+id/btnBuyNow"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Beli Sekarang"
android:layout_marginTop="24dp"
android:backgroundTint="@color/blue_500"
android:textColor="@android:color/white" />
</LinearLayout>
<!-- Tombol X Close -->
<ImageButton
android:id="@+id/btnCloseDialog"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_gravity="end|top"
android:background="@android:color/transparent"
android:src="@android:drawable/ic_menu_close_clear_cancel"
android:contentDescription="Close" />
</FrameLayout>

View File

@ -0,0 +1,54 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginVertical="4dp"
android:layout_marginHorizontal="16dp"
app:cardCornerRadius="4dp"
app:cardUseCompatPadding="true"
xmlns:app="http://schemas.android.com/apk/res-auto">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/cardview_light_background"
android:padding="16dp">
<ImageView
android:id="@+id/iv_Location"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
android:src="@drawable/baseline_location_pin_24"/>
<TextView
android:id="@+id/tv_name_address"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="@font/dmsans_semibold"
android:textSize="16sp"
android:layout_marginHorizontal="8dp"
android:text="Nama Penerima"
app:layout_constraintStart_toEndOf="@id/iv_Location"
app:layout_constraintTop_toTopOf="parent"/>
<ImageView
android:id="@+id/iv_edit"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/baseline_edit_24"
android:layout_marginHorizontal="16dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
<TextView
android:id="@+id/tv_detail_address"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="Jl. Salak"
android:fontFamily="@font/dmsans_light"
android:textSize="14sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/iv_Location"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>

View File

@ -0,0 +1,61 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
xmlns:app="http://schemas.android.com/apk/res-auto">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="8dp"
android:gravity="center_vertical">
<ImageView
android:id="@+id/iv_product"
android:layout_width="64dp"
android:layout_height="64dp"
android:src="@drawable/placeholder_image"
android:scaleType="centerCrop"/>
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical"
android:layout_marginStart="16dp">
<TextView
android:id="@+id/tv_product_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Keripik Balado"
android:fontFamily="@font/dmsans_bold"/>
<TextView
android:id="@+id/tv_product_quantity"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="1 buah"
android:fontFamily="@font/dmsans_medium" />
<TextView
android:id="@+id/tv_product_price"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Rp65,000"
android:fontFamily="@font/dmsans_medium"/>
</LinearLayout>
</LinearLayout>
<com.google.android.material.divider.MaterialDivider
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:dividerInsetStart="16dp"
app:dividerInsetEnd="16dp"/>
</LinearLayout>

View File

@ -0,0 +1,44 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<LinearLayout
android:id="@+id/linear_seller_order"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_margin="8dp"
android:gravity="center_vertical">
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:src="@drawable/outline_store_24" />
<TextView
android:id="@+id/tv_store_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="SnackEnak"
android:textSize="16sp"
android:fontFamily="@font/dmsans_semibold"
android:layout_marginStart="8dp" />
</LinearLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_seller_order_product"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:listitem="@layout/item_order_product"/>
<com.google.android.material.divider.MaterialDivider
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:dividerInsetStart="16dp"
app:dividerInsetEnd="16dp"/>
</LinearLayout>

View File

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="16dp"
android:background="?attr/selectableItemBackground"
android:gravity="center_vertical">
<ImageView
android:id="@+id/iv_payment_method"
android:layout_width="32dp"
android:layout_height="32dp"
android:src="@drawable/outline_store_24"
android:layout_marginEnd="16dp"/>
<TextView
android:id="@+id/tv_payment_method_name"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Bank Transfer"
android:fontFamily="@font/dmsans_medium"/>
<RadioButton
android:id="@+id/rb_payment_method"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</LinearLayout>

View File

@ -0,0 +1,64 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
xmlns:app="http://schemas.android.com/apk/res-auto">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="start"
android:layout_marginStart="16dp"
android:layout_marginVertical="8dp"
android:layout_gravity="center_horizontal"
android:orientation="horizontal">
<RadioButton
android:id="@+id/radio_btn_cost"
android:layout_margin="4dp"
android:clickable="true"
android:gravity="center"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="@+id/courier_name_cost"
android:fontFamily="@font/dmsans_bold"
android:textSize="20sp"
android:padding="4dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="JNE"/>
<TextView
android:id="@+id/est_date"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="16sp"
android:paddingHorizontal="8dp"
android:text="Estimasi 3-4 hari"/>
</LinearLayout>
<TextView
android:id="@+id/cost_price"
android:textSize="16sp"
android:gravity="center_vertical"
android:layout_margin="16dp"
android:fontFamily="@font/dmsans_semibold"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:text="Rp15.0000"/>
</LinearLayout>
<View
android:id="@+id/divider"
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="#E0E0E0"
app:layout_constraintTop_toBottomOf="@id/linear_toolbar" />
</androidx.cardview.widget.CardView>

View File

@ -1,5 +1,5 @@
[versions]
agp = "8.9.1"
agp = "8.5.2"
glide = "4.16.0"
hiltAndroid = "2.51"
hiltLifecycleViewmodel = "1.0.0-alpha03"