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> <GradleProjectSettings>
<option name="testRunner" value="CHOOSE_PER_TEST" /> <option name="testRunner" value="CHOOSE_PER_TEST" />
<option name="externalProjectPath" value="$PROJECT_DIR$" /> <option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="gradleJvm" value="openjdk-24" /> <option name="gradleJvm" value="jbr-21" />
<option name="modules"> <option name="modules">
<set> <set>
<option value="$PROJECT_DIR$" /> <option value="$PROJECT_DIR$" />
<option value="$PROJECT_DIR$/app" /> <option value="$PROJECT_DIR$/app" />
</set> </set>
</option> </option>
<option name="resolveExternalAnnotations" value="false" />
</GradleProjectSettings> </GradleProjectSettings>
</option> </option>
</component> </component>

2
.idea/misc.xml generated
View File

@ -4,7 +4,7 @@
<option name="optimizeImportsOnTheFly" value="true" /> <option name="optimizeImportsOnTheFly" value="true" />
</component> </component>
<component name="ExternalStorageConfigurationManager" enabled="true" /> <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" /> <output url="file://$PROJECT_DIR$/build/classes" />
</component> </component>
<component name="ProjectType"> <component name="ProjectType">

View File

@ -1,5 +1,4 @@
import org.gradle.api.tasks.compile.JavaCompile import java.util.Properties
plugins { plugins {
alias(libs.plugins.android.application) alias(libs.plugins.android.application)
alias(libs.plugins.jetbrains.kotlin.android) alias(libs.plugins.jetbrains.kotlin.android)
@ -9,9 +8,17 @@ plugins {
// id("com.google.dagger.hilt.android") // id("com.google.dagger.hilt.android")
} }
val localProperties = Properties().apply {
val localPropertiesFile = rootProject.file("local.properties")
if (localPropertiesFile.exists()) {
load(localPropertiesFile.inputStream())
}
}
android { android {
namespace = "com.alya.ecommerce_serang" namespace = "com.alya.ecommerce_serang"
compileSdk = 35 compileSdk = 34
defaultConfig { defaultConfig {
applicationId = "com.alya.ecommerce_serang" applicationId = "com.alya.ecommerce_serang"
@ -21,11 +28,19 @@ android {
versionName = "1.0" versionName = "1.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
buildConfigField(
"String",
"BASE_URL",
"\"${localProperties["BASE_URL"] ?: "http://default-url.com/"}\""
)
} }
buildTypes { buildTypes {
release { release {
buildConfigField("String", "BASE_URL", "\"http://192.168.1.15:3000/\"") buildConfigField("String",
"BASE_URL",
"\"${localProperties["BASE_URL"] ?: "http://default-url.com/"}\"")
isMinifyEnabled = false isMinifyEnabled = false
proguardFiles( proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"), getDefaultProguardFile("proguard-android-optimize.txt"),
@ -33,7 +48,9 @@ android {
) )
} }
debug { debug {
buildConfigField("String", "BASE_URL", "\"http://192.168.1.15:3000/\"") buildConfigField("String",
"BASE_URL",
"\"${localProperties["BASE_URL"] ?: "http://default-url.com/"}\"")
} }
} }
compileOptions { compileOptions {
@ -86,4 +103,3 @@ dependencies {
// kapt("androidx.hilt:hilt-compiler:1.0.0") // 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.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <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 <application
android:allowBackup="true" android:allowBackup="true"
@ -17,6 +19,24 @@
android:theme="@style/Theme.Ecommerce_serang" android:theme="@style/Theme.Ecommerce_serang"
android:usesCleartextTraffic="true" android:usesCleartextTraffic="true"
tools:targetApi="31"> tools:targetApi="31">
<activity
android:name=".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 <activity
android:name=".ui.profile.mystore.profile.payment_info.DetailPaymentInfoActivity" android:name=".ui.profile.mystore.profile.payment_info.DetailPaymentInfoActivity"
android:exported="false" /> 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 package com.alya.ecommerce_serang.data.api.response
import com.alya.ecommerce_serang.data.api.response.product.Product
import com.google.gson.annotations.SerializedName import com.google.gson.annotations.SerializedName
data class CreateProductResponse( 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 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 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 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.alya.ecommerce_serang.data.api.dto.ProductsItem
import com.google.gson.annotations.SerializedName 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 import com.google.gson.annotations.SerializedName
data class AllStoreResponse( data class AllStoreResponse(
@field:SerializedName("store") @field:SerializedName("store")
val store: AllStore, val store: AllStore,
@field:SerializedName("message") @field:SerializedName("message")
val message: String 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.alya.ecommerce_serang.data.api.dto.CategoryItem
import com.google.gson.annotations.SerializedName 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 import com.google.gson.annotations.SerializedName
data class ProductResponse( data class ProductResponse(
@field:SerializedName("product") @field:SerializedName("product")
val product: Product, val product: Product,
@field:SerializedName("message") @field:SerializedName("message")
val message: String val message: String
) )

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 import com.google.gson.annotations.SerializedName
data class ReviewProductResponse( data class ReviewProductResponse(
@field:SerializedName("reviews") @field:SerializedName("reviews")
val reviews: List<ReviewsItem>, val reviews: List<ReviewsItem>,
@field:SerializedName("message") @field:SerializedName("message")
val message: String 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.alya.ecommerce_serang.data.api.dto.UserProfile
import com.google.gson.annotations.SerializedName import com.google.gson.annotations.SerializedName

View File

@ -1,19 +1,34 @@
package com.alya.ecommerce_serang.data.api.retrofit 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.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.OtpRequest
import com.alya.ecommerce_serang.data.api.dto.RegisterRequest import com.alya.ecommerce_serang.data.api.dto.RegisterRequest
import com.alya.ecommerce_serang.data.api.response.AllProductResponse import com.alya.ecommerce_serang.data.api.dto.UpdateCart
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.response.ViewStoreProductsResponse 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.Call
import retrofit2.Response import retrofit2.Response
import retrofit2.http.Body import retrofit2.http.Body
@ -21,6 +36,7 @@ import retrofit2.http.Field
import retrofit2.http.FormUrlEncoded import retrofit2.http.FormUrlEncoded
import retrofit2.http.GET import retrofit2.http.GET
import retrofit2.http.POST import retrofit2.http.POST
import retrofit2.http.PUT
import retrofit2.http.Path import retrofit2.http.Path
interface ApiService { interface ApiService {
@ -64,6 +80,24 @@ interface ApiService {
@Path("id") storeId: Int @Path("id") storeId: Int
): Response<DetailStoreProductResponse> ): 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") @GET("mystore")
suspend fun getStore (): Response<StoreResponse> suspend fun getStore (): Response<StoreResponse>
@ -89,4 +123,30 @@ interface ApiService {
@Field("is_active") isActive: String @Field("is_active") isActive: String
): Response<Unit> ): 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 android.util.Log
import com.alya.ecommerce_serang.data.api.dto.Store import com.alya.ecommerce_serang.data.api.dto.Store
import com.alya.ecommerce_serang.data.api.response.StoreResponse import com.alya.ecommerce_serang.data.api.response.product.StoreResponse
import com.alya.ecommerce_serang.data.api.retrofit.ApiService import com.alya.ecommerce_serang.data.api.retrofit.ApiService
import retrofit2.HttpException import retrofit2.HttpException
import java.io.IOException 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 package com.alya.ecommerce_serang.data.repository
import android.util.Log 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.CategoryItem
import com.alya.ecommerce_serang.data.api.dto.ProductsItem 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.cart.AddCartResponse
import com.alya.ecommerce_serang.data.api.response.ReviewsItem 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 com.alya.ecommerce_serang.data.api.retrofit.ApiService
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@ -19,26 +22,37 @@ class ProductRepository(private val apiService: ApiService) {
if (response.isSuccessful) { if (response.isSuccessful) {
// Return a Result.Success with the list of products // 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 { } else {
// Return a Result.Error with a custom Exception val errorBody = response.errorBody()?.string() ?: "Unknown error"
Log.e("ProductRepository", "Error: ${response.errorBody()?.string()}") Log.e(TAG, "Failed to fetch products. Code: ${response.code()}, Error: $errorBody")
Result.Error(Exception("Failed to fetch products. Code: ${response.code()}")) Result.Error(Exception("Failed to fetch products. Code: ${response.code()}"))
} }
} catch (e: Exception) { } catch (e: Exception) {
// Return a Result.Error with the exception caught Log.e(TAG, "Exception while fetching products", e)
Result.Error(e) Result.Error(e)
} }
} }
suspend fun fetchProductDetail(productId: Int): ProductResponse? { suspend fun fetchProductDetail(productId: Int): ProductResponse? {
return try { return try {
Log.d(TAG, "Fetching product detail for ID: $productId")
val response = apiService.getDetailProduct(productId) val response = apiService.getDetailProduct(productId)
if (response.isSuccessful) { if (response.isSuccessful) {
response.body() val productResponse = response.body()
Log.d(TAG, "Product detail fetched successfully. Product: ${productResponse?.product?.productName}")
productResponse
} else { } 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 null
} }
} catch (e: Exception) { } catch (e: Exception) {
@ -54,14 +68,14 @@ class ProductRepository(private val apiService: ApiService) {
if (response.isSuccessful) { if (response.isSuccessful) {
val categories = response.body()?.category ?: emptyList() 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}") } categories.forEach { Log.d("Category Image", "Category: ${it.name}, Image: ${it.image}") }
Result.Success(categories) Result.Success(categories)
} else { } else {
Result.Error(Exception("Failed to fetch categories. Code: ${response.code()}")) Result.Error(Exception("Failed to fetch categories. Code: ${response.code()}"))
} }
} catch (e: Exception) { } catch (e: Exception) {
Log.e("Categories", "Error fetching categories", e) Log.e("ProductRepository", "Error fetching categories", e)
Result.Error(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> { suspend fun fetchMyStoreProducts(): List<ProductsItem> {
val response = apiService.getStoreProduct() val response = apiService.getStoreProduct()
if (response.isSuccessful) { 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? { // 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.OtpRequest
import com.alya.ecommerce_serang.data.api.dto.RegisterRequest import com.alya.ecommerce_serang.data.api.dto.RegisterRequest
import com.alya.ecommerce_serang.data.api.dto.UserProfile import com.alya.ecommerce_serang.data.api.dto.UserProfile
import com.alya.ecommerce_serang.data.api.response.LoginResponse import com.alya.ecommerce_serang.data.api.response.auth.LoginResponse
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.api.retrofit.ApiService import com.alya.ecommerce_serang.data.api.retrofit.ApiService
class UserRepository(private val apiService: ApiService) { class UserRepository(private val apiService: ApiService) {
//post data without message/response
suspend fun requestOtpRep(email: String): OtpResponse { suspend fun requestOtpRep(email: String): OtpResponse {
// fun requestOtpRep(email: String): Result<String> {
return apiService.getOTP(OtpRequest(email)) return apiService.getOTP(OtpRequest(email))
} }
suspend fun registerUser(request: RegisterRequest): String { suspend fun registerUser(request: RegisterRequest): String {

View File

@ -61,6 +61,7 @@ class HorizontalProductAdapter(
val diffResult = DiffUtil.calculateDiff(diffCallback) val diffResult = DiffUtil.calculateDiff(diffCallback)
products = newProducts products = newProducts
diffResult.dispatchUpdatesTo(this) diffResult.dispatchUpdatesTo(this)
notifyDataSetChanged()
} }
fun updateLimitedProducts(newProducts: List<ProductsItem>) { 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 package com.alya.ecommerce_serang.ui.product
import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.util.Log import android.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.activity.viewModels
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import com.alya.ecommerce_serang.BuildConfig.BASE_URL import com.alya.ecommerce_serang.BuildConfig.BASE_URL
import com.alya.ecommerce_serang.R import com.alya.ecommerce_serang.R
import com.alya.ecommerce_serang.data.api.dto.CartItem
import com.alya.ecommerce_serang.data.api.dto.ProductsItem 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.product.Product
import com.alya.ecommerce_serang.data.api.response.ReviewsItem 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.ApiConfig
import com.alya.ecommerce_serang.data.api.retrofit.ApiService import com.alya.ecommerce_serang.data.api.retrofit.ApiService
import com.alya.ecommerce_serang.data.repository.ProductRepository 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.databinding.ActivityDetailProductBinding
import com.alya.ecommerce_serang.ui.home.HorizontalProductAdapter import com.alya.ecommerce_serang.ui.home.HorizontalProductAdapter
import com.alya.ecommerce_serang.ui.order.CheckoutActivity
import com.alya.ecommerce_serang.utils.BaseViewModelFactory import com.alya.ecommerce_serang.utils.BaseViewModelFactory
import com.alya.ecommerce_serang.utils.SessionManager import com.alya.ecommerce_serang.utils.SessionManager
import com.alya.ecommerce_serang.utils.viewmodel.ProductViewModel
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import com.google.android.material.bottomsheet.BottomSheetDialog
import java.text.NumberFormat
import java.util.Locale
class DetailProductActivity : AppCompatActivity() { class DetailProductActivity : AppCompatActivity() {
private lateinit var binding: ActivityDetailProductBinding private lateinit var binding: ActivityDetailProductBinding
@ -27,12 +39,14 @@ class DetailProductActivity : AppCompatActivity() {
private lateinit var sessionManager: SessionManager private lateinit var sessionManager: SessionManager
private var productAdapter: HorizontalProductAdapter? = null private var productAdapter: HorizontalProductAdapter? = null
private var reviewsAdapter: ReviewsAdapter? = null private var reviewsAdapter: ReviewsAdapter? = null
private var currentQuantity = 1
private val viewModel: ProductViewModel by viewModels {
private val viewModel: ProductUserViewModel by viewModels {
BaseViewModelFactory { BaseViewModelFactory {
val apiService = ApiConfig.getApiService(sessionManager) val apiService = ApiConfig.getApiService(sessionManager)
val productRepository = ProductRepository(apiService) val productRepository = ProductRepository(apiService)
ProductViewModel(productRepository) ProductUserViewModel(productRepository)
} }
} }
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
@ -43,56 +57,148 @@ class DetailProductActivity : AppCompatActivity() {
sessionManager = SessionManager(this) sessionManager = SessionManager(this)
apiService = ApiConfig.getApiService(sessionManager) apiService = ApiConfig.getApiService(sessionManager)
setupUI()
setupObservers()
loadData()
}
private fun loadData() {
val productId = intent.getIntExtra("PRODUCT_ID", -1) val productId = intent.getIntExtra("PRODUCT_ID", -1)
//nanti tambah get store id dari HomeFragment Product.storeId
if (productId == -1) { if (productId == -1) {
Log.e("DetailProductActivity", "Invalid Product ID") Log.e("DetailProductActivity", "Invalid Product ID")
Toast.makeText(this, "Invalid product ID", Toast.LENGTH_SHORT).show()
finish() // Close activity if no valid ID finish() // Close activity if no valid ID
return return
} }
viewModel.loadProductDetail(productId) viewModel.loadProductDetail(productId)
viewModel.loadReviews(productId) viewModel.loadReviews(productId)
}
private fun setupObservers() {
viewModel.productDetail.observe(this) { product -> viewModel.productDetail.observe(this) { product ->
if (product != null) { product?.let {
Log.d("ProductDetail", "Name: ${product.productName}, Price: ${product.price}") updateUI(it)
// Update UI here, e.g., show in a TextView or ImageView viewModel.loadOtherProducts(it.storeId)
viewModel.loadProductDetail(productId)
} else {
Log.e("ProductDetail", "Failed to fetch product details")
} }
} }
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 -> viewModel.reviewProduct.observe(this) { reviews ->
setupRecyclerViewReviewsProduct(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){ private fun updateUI(product: Product){
binding.tvProductName.text = product.productName binding.tvProductName.text = product.productName
binding.tvPrice.text = product.price binding.tvPrice.text = formatCurrency(product.price.toDouble())
binding.tvSold.text = product.totalSold.toString() binding.tvSold.text = product.totalSold.toString()
binding.tvRating.text = product.rating binding.tvRating.text = product.rating
binding.tvWeight.text = product.weight.toString() binding.tvWeight.text = product.weight.toString()
binding.tvStock.text = product.stock.toString() binding.tvStock.text = product.stock.toString()
binding.tvCategory.text = product.productCategory binding.tvCategory.text = product.productCategory
binding.tvDescription.text = product.description binding.tvDescription.text = product.description
binding.tvSellerName.text = product.storeId.toString()
binding.tvViewAllReviews.setOnClickListener{
handleAllReviewsClick(product.productId)
}
val fullImageUrl = when (val img = product.image) { val fullImageUrl = when (val img = product.image) {
is String -> { is String -> {
@ -106,8 +212,6 @@ class DetailProductActivity : AppCompatActivity() {
.load(fullImageUrl) .load(fullImageUrl)
.placeholder(R.drawable.placeholder_image) .placeholder(R.drawable.placeholder_image)
.into(binding.ivProductImage) .into(binding.ivProductImage)
setupRecyclerViewOtherProducts()
} }
private fun handleAllReviewsClick(productId: Int) { private fun handleAllReviewsClick(productId: Int) {
@ -119,7 +223,7 @@ class DetailProductActivity : AppCompatActivity() {
private fun setupRecyclerViewOtherProducts(){ private fun setupRecyclerViewOtherProducts(){
productAdapter = HorizontalProductAdapter( productAdapter = HorizontalProductAdapter(
products = emptyList(), products = emptyList(),
onClick = { productsItem -> handleProductClick(productsItem) } onClick = { productsItem -> handleProductClick(productsItem) }
) )
binding.recyclerViewOtherProducts.apply { binding.recyclerViewOtherProducts.apply {
@ -134,7 +238,15 @@ class DetailProductActivity : AppCompatActivity() {
private fun setupRecyclerViewReviewsProduct(reviewList: List<ReviewsItem>){ private fun setupRecyclerViewReviewsProduct(reviewList: List<ReviewsItem>){
val limitedReviewList = if (reviewList.isNotEmpty()) listOf(reviewList.first()) else emptyList() 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( reviewsAdapter = ReviewsAdapter(
reviewList = limitedReviewList reviewList = limitedReviewList
) )
@ -154,4 +266,109 @@ class DetailProductActivity : AppCompatActivity() {
intent.putExtra("PRODUCT_ID", product.id) // Pass product ID intent.putExtra("PRODUCT_ID", product.id) // Pass product ID
startActivity(intent) 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.databinding.ActivityReviewProductBinding
import com.alya.ecommerce_serang.utils.BaseViewModelFactory import com.alya.ecommerce_serang.utils.BaseViewModelFactory
import com.alya.ecommerce_serang.utils.SessionManager import com.alya.ecommerce_serang.utils.SessionManager
import com.alya.ecommerce_serang.utils.viewmodel.ProductViewModel
class ReviewProductActivity : AppCompatActivity() { class ReviewProductActivity : AppCompatActivity() {
private lateinit var binding: ActivityReviewProductBinding private lateinit var binding: ActivityReviewProductBinding
private lateinit var apiService: ApiService private lateinit var apiService: ApiService
private var reviewsAdapter: ReviewsAdapter? = null private var reviewsAdapter: ReviewsAdapter? = null
private lateinit var sessionManager: SessionManager private lateinit var sessionManager: SessionManager
private val viewModel: ProductViewModel by viewModels { private val viewModel: ProductUserViewModel by viewModels {
BaseViewModelFactory { BaseViewModelFactory {
val apiService = ApiConfig.getApiService(sessionManager) val apiService = ApiConfig.getApiService(sessionManager)
val productRepository = ProductRepository(apiService) val productRepository = ProductRepository(apiService)
ProductViewModel(productRepository) ProductUserViewModel(productRepository)
} }
} }

View File

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

View File

@ -1,10 +1,12 @@
package com.alya.ecommerce_serang.ui.profile package com.alya.ecommerce_serang.ui.profile
import android.os.Bundle import android.os.Bundle
import android.util.Log
import android.widget.Toast import android.widget.Toast
import androidx.activity.enableEdgeToEdge import androidx.activity.enableEdgeToEdge
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import com.alya.ecommerce_serang.R
import com.alya.ecommerce_serang.data.api.dto.UserProfile import com.alya.ecommerce_serang.data.api.dto.UserProfile
import com.alya.ecommerce_serang.data.api.retrofit.ApiConfig import com.alya.ecommerce_serang.data.api.retrofit.ApiConfig
import com.alya.ecommerce_serang.data.api.retrofit.ApiService import com.alya.ecommerce_serang.data.api.retrofit.ApiService
@ -62,25 +64,33 @@ class DetailProfileActivity : AppCompatActivity() {
binding.tvNameUser.setText(user.name.toString()) binding.tvNameUser.setText(user.name.toString())
binding.tvUsername.setText(user.username) binding.tvUsername.setText(user.username)
binding.tvEmailUser.setText(user.email) 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) binding.tvNumberPhoneUser.setText(user.phone)
if (user.image != null && user.image is String) { if (user.image != null && user.image is String) {
Glide.with(this) Glide.with(this)
.load(user.image) .load(user.image)
.placeholder(R.drawable.baseline_account_circle_24)
.into(binding.profileImage) .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 { return try {
val inputFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.getDefault()) //from json val inputFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.getDefault())
inputFormat.timeZone = TimeZone.getTimeZone("UTC") //get timezone inputFormat.timeZone = TimeZone.getTimeZone("UTC") // Ensure parsing in UTC
val outputFormat = SimpleDateFormat("dd MMMM yyyy", Locale.getDefault()) // new format
val date = inputFormat.parse(dateString) // Parse from json format val outputFormat = SimpleDateFormat("dd-MM-yy", Locale.getDefault()) // Convert to "dd-MM-yy" format
outputFormat.format(date!!) // convert to new format val date = inputFormat.parse(dateString)
outputFormat.format(date ?: return "Invalid Date") // Ensure valid date
} catch (e: Exception) { } 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 package com.alya.ecommerce_serang.utils
import androidx.lifecycle.AbstractSavedStateViewModelFactory
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.savedstate.SavedStateRegistryOwner
class BaseViewModelFactory<VM : ViewModel>( class BaseViewModelFactory<VM : ViewModel>(
private val creator: () -> VM private val creator: () -> VM
@ -11,3 +14,19 @@ class BaseViewModelFactory<VM : ViewModel>(
return creator() as T 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.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope 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.Result
import com.alya.ecommerce_serang.data.repository.UserRepository import com.alya.ecommerce_serang.data.repository.UserRepository
import kotlinx.coroutines.launch import kotlinx.coroutines.launch

View File

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

View File

@ -6,7 +6,7 @@ import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.alya.ecommerce_serang.data.api.dto.RegisterRequest 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.Result
import com.alya.ecommerce_serang.data.repository.UserRepository import com.alya.ecommerce_serang.data.repository.UserRepository
import kotlinx.coroutines.launch import kotlinx.coroutines.launch

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> </LinearLayout>
</androidx.core.widget.NestedScrollView> </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 --> <!-- Bottom Action Bar -->
<com.google.android.material.bottomappbar.BottomAppBar <com.google.android.material.bottomappbar.BottomAppBar
android:id="@+id/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] [versions]
agp = "8.9.1" agp = "8.5.2"
glide = "4.16.0" glide = "4.16.0"
hiltAndroid = "2.51" hiltAndroid = "2.51"
hiltLifecycleViewmodel = "1.0.0-alpha03" hiltLifecycleViewmodel = "1.0.0-alpha03"